From 6409462a9643531218b4085385806779f7a22fd8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Jul 2021 03:31:29 +0200 Subject: Rate limit olm session creation --- src/Olm.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'src/Olm.cpp') diff --git a/src/Olm.cpp b/src/Olm.cpp index d20bf9a4..d421e336 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1138,9 +1138,23 @@ send_encrypted_to_device_messages(const std::map, qint64> rateLimit; + auto currentTime = QDateTime::currentSecsSinceEpoch(); + if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < + currentTime) { + claims.one_time_keys[user][device] = + mtx::crypto::SIGNED_CURVE25519; + pks[user][device].ed25519 = d.keys.at("ed25519:" + device); + pks[user][device].curve25519 = + d.keys.at("curve25519:" + device); + + rateLimit.insert(QPair(user, device), currentTime); + } else { + nhlog::crypto()->warn("Not creating new session with {}:{} " + "because of rate limit", + user, + device); + } continue; } -- cgit 1.5.1 From e7877ae5af533f75ee8fade52f968c453df7c201 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Jul 2021 12:44:08 +0200 Subject: Fix crash when we don't have keys for other device when receiving an olm message from it --- src/Olm.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'src/Olm.cpp') diff --git a/src/Olm.cpp b/src/Olm.cpp index d421e336..e3ca1c34 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -286,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey bool from_their_device = false; for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) { - if (key.keys.at("ed25519:" + device_id) == sender_ed25519) { - from_their_device = true; - break; - } + auto c_key = key.keys.find("curve25519:" + device_id); + auto e_key = key.keys.find("ed25519:" + device_id); + + if (c_key == key.keys.end() || e_key == key.keys.end()) { + nhlog::crypto()->warn( + "Skipping device {} as we have no keys for it.", + device_id); + } else if (c_key->second == msg.sender_key && + e_key->second == sender_ed25519) { + from_their_device = true; + break; } } if (!from_their_device) { -- cgit 1.5.1 From 25e7a985b82bbeff9c3c5b4d15b44f2539768260 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Aug 2021 00:59:46 +0200 Subject: Add option to only send encrypted messages to verified devices fixes #636 --- src/Cache.cpp | 49 ++++++++++++++++++-- src/Cache_p.h | 3 +- src/Olm.cpp | 3 +- src/UserSettingsPage.cpp | 113 +++++++++++++++++++++++++++++------------------ src/UserSettingsPage.h | 7 +++ 5 files changed, 127 insertions(+), 48 deletions(-) (limited to 'src/Olm.cpp') diff --git a/src/Cache.cpp b/src/Cache.cpp index 7d0b1a89..291df053 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3542,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id) } std::map> -Cache::getMembersWithKeys(const std::string &room_id) +Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { std::string_view keys; @@ -3559,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id) auto res = keysDb.get(txn, user_id, keys); if (res) { - members[std::string(user_id)] = - json::parse(keys).get(); + auto k = json::parse(keys).get(); + if (verified_only) { + auto verif = verificationStatus(std::string(user_id)); + if (verif.user_verified == crypto::Trust::Verified || + !verif.verified_devices.empty()) { + auto keyCopy = k; + keyCopy.device_keys.clear(); + + std::copy_if( + k.device_keys.begin(), + k.device_keys.end(), + std::inserter(keyCopy.device_keys, + keyCopy.device_keys.end()), + [&verif](const auto &key) { + auto curve25519 = key.second.keys.find( + "curve25519:" + key.first); + if (curve25519 == key.second.keys.end()) + return false; + if (auto t = + verif.verified_device_keys.find( + curve25519->second); + t == + verif.verified_device_keys.end() || + t->second != crypto::Trust::Verified) + return false; + + return key.first == + key.second.device_id && + std::find( + verif.verified_devices.begin(), + verif.verified_devices.end(), + key.first) != + verif.verified_devices.end(); + }); + + if (!keyCopy.device_keys.empty()) + members[std::string(user_id)] = + std::move(keyCopy); + } + } else { + members[std::string(user_id)] = std::move(k); + } } else { - members[std::string(user_id)] = {}; + if (!verified_only) + members[std::string(user_id)] = {}; } } cursor.close(); diff --git a/src/Cache_p.h b/src/Cache_p.h index 18b9601f..5d700658 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -48,7 +48,8 @@ public: // user cache stores user keys std::optional userKeys(const std::string &user_id); std::map> getMembersWithKeys( - const std::string &room_id); + const std::string &room_id, + bool verified_only); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, diff --git a/src/Olm.cpp b/src/Olm.cpp index e3ca1c34..048a6c0f 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -524,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, auto own_user_id = http::client()->user_id().to_string(); - auto members = cache::client()->getMembersWithKeys(room_id); + auto members = cache::client()->getMembersWithKeys( + room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); std::map> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index a062780a..ab6ac492 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -90,13 +90,11 @@ UserSettings::load(std::optional profile) decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - shareKeysWithTrustedUsers_ = - settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); - auto tempPresence = settings.value("user/presence", "").toString().toStdString(); - auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + auto tempPresence = settings.value("user/presence", "").toString().toStdString(); + auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); if (presenceValue < 0) presenceValue = 0; presence_ = static_cast(presenceValue); @@ -123,6 +121,12 @@ UserSettings::load(std::optional profile) userId_ = settings.value(prefix + "auth/user_id", "").toString(); deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) + .toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + disableCertificateValidation_ = settings.value("disable_certificate_validation", false).toBool(); @@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer) save(); } +void +UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) +{ + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; + + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); +} + void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { @@ -610,8 +625,6 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); @@ -650,6 +663,11 @@ UserSettings::save() settings.setValue(prefix + "auth/user_id", userId_); settings.setValue(prefix + "auth/device_id", deviceId_); + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue("disable_certificate_validation", disableCertificateValidation_); settings.sync(); @@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - shareKeysWithTrustedUsers_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; trayToggle_->setChecked(settings_->tray()); startInTrayToggle_->setChecked(settings_->startInTray()); avatarCircles_->setChecked(settings_->avatarCircles()); decryptSidebar_->setChecked(settings_->decryptSidebar()); privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); groupViewToggle_->setChecked(settings_->groupView()); timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); @@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge formLayout_->addRow(new HorizontalLine{this}); boxWrap(tr("Device ID"), deviceIdValue_); boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap( - tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified.")); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); @@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge } }); + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { settings_->setShareKeysWithTrustedUsers(enabled); }); @@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *) groupViewToggle_->setState(settings_->groupView()); decryptSidebar_->setState(settings_->decryptSidebar()); privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); avatarCircles_->setState(settings_->avatarCircles()); typingNotifications_->setState(settings_->typingNotifications()); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index acb08569..096aab81 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -88,6 +88,8 @@ class UserSettings : public QObject setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) Q_PROPERTY( bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) @@ -152,6 +154,7 @@ public: void setScreenShareRemoteVideo(bool state); void setScreenShareHideCursor(bool state); void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); void setShareKeysWithTrustedUsers(bool state); void setProfile(QString profile); void setUserId(QString userId); @@ -208,6 +211,7 @@ public: bool screenShareHideCursor() const { return screenShareHideCursor_; } bool useStunServer() const { return useStunServer_; } bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } QString profile() const { return profile_; } QString userId() const { return userId_; } QString accessToken() const { return accessToken_; } @@ -252,6 +256,7 @@ signals: void screenShareRemoteVideoChanged(bool state); void screenShareHideCursorChanged(bool state); void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); void shareKeysWithTrustedUsersChanged(bool state); void profileChanged(QString profile); void userIdChanged(QString userId); @@ -284,6 +289,7 @@ private: bool privacyScreen_; int privacyScreenTimeout_; bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; bool mobileMode_; int timelineMaxWidth_; int roomListWidth_; @@ -372,6 +378,7 @@ private: Toggle *privacyScreen_; QSpinBox *privacyScreenTimeout_; Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; Toggle *mobileMode_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; -- cgit 1.5.1 From 72bbad7485db6ac1803f81344c29b93d9fa70945 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 22:51:09 +0200 Subject: Show encryption errors in qml and add request keys button --- CMakeLists.txt | 13 +- resources/qml/MessageView.qml | 2 + resources/qml/TimelineRow.qml | 3 + resources/qml/delegates/Encrypted.qml | 48 ++++++ resources/qml/delegates/MessageDelegate.qml | 11 ++ resources/qml/delegates/Reply.qml | 2 + resources/res.qrc | 9 +- src/Olm.cpp | 2 +- src/Olm.h | 9 +- src/timeline/EventStore.cpp | 256 ++++++++++++---------------- src/timeline/EventStore.h | 9 +- src/timeline/TimelineModel.cpp | 16 ++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 2 + 14 files changed, 220 insertions(+), 165 deletions(-) create mode 100644 resources/qml/delegates/Encrypted.qml (limited to 'src/Olm.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 55b58da1..049ed8a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -541,32 +541,33 @@ qt5_wrap_cpp(MOC_HEADERS src/AvatarProvider.h src/BlurhashProvider.h - src/Cache_p.h src/CacheCryptoStructs.h + src/Cache_p.h src/CallDevices.h src/CallManager.h src/ChatPage.h src/Clipboard.h + src/CombinedImagePackModel.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h + src/ImagePackListModel.h src/InviteesModel.h src/LoginPage.h src/MainWindow.h src/MemberList.h src/MxcImageProvider.h - src/ReadReceiptsModel.h + src/Olm.h src/RegisterPage.h + src/RoomsModel.h src/SSOHandler.h - src/CombinedImagePackModel.h src/SingleImagePackModel.h - src/ImagePackListModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h - src/RoomsModel.h src/WebRTCSession.h src/WelcomePage.h - ) + src/ReadReceiptsModel.h +) # # Bundle translations. diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index f3e15d84..79cbd700 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -349,6 +349,7 @@ ScrollView { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status required property int index @@ -456,6 +457,7 @@ ScrollView { callType: wrapper.callType reactions: wrapper.reactions trustlevel: wrapper.trustlevel + encryptionError: wrapper.encryptionError timestamp: wrapper.timestamp status: wrapper.status relatedEventCacheBuster: wrapper.relatedEventCacheBuster diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 6345f44c..c612479a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -38,6 +38,7 @@ Item { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status required property int relatedEventCacheBuster @@ -110,6 +111,7 @@ Item { roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" + encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? "" relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 } @@ -136,6 +138,7 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + encryptionError: r.encryptionError relatedEventCacheBuster: r.relatedEventCacheBuster isReply: false } diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml new file mode 100644 index 00000000..cd00a9d4 --- /dev/null +++ b/resources/qml/delegates/Encrypted.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +ColumnLayout { + id: r + + required property int encryptionError + required property string eventId + + width: parent ? parent.width : undefined + + MatrixText { + text: { + switch (encryptionError) { + case Olm.MissingSession: + return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient."); + case Olm.MissingSessionIndex: + return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message."); + case Olm.DbError: + return qsTr("There was an internal error reading the decryption key from the database."); + case Olm.DecryptionFailed: + return qsTr("There was an error decrypting this message."); + case Olm.ParsingFailed: + return qsTr("The message couldn't be parsed."); + case Olm.ReplayAttack: + return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!"); + default: + return qsTr("Unknown decryption error"); + } + } + color: Nheko.colors.buttonText + width: r ? r.width : undefined + } + + Button { + palette: Nheko.colors + visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex + text: qsTr("Request key") + onClicked: room.requestKeyForEvent(eventId) + } + +} diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index a98c2a8b..a8bdf183 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -29,6 +29,7 @@ Item { required property string roomTopic required property string roomName required property string callType + required property int encryptionError required property int relatedEventCacheBuster height: chooser.childrenRect.height @@ -189,6 +190,16 @@ Item { } + DelegateChoice { + roleValue: MtxEvent.Encrypted + + Encrypted { + encryptionError: d.encryptionError + eventId: d.eventId + } + + } + DelegateChoice { roleValue: MtxEvent.Name diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 3e02a940..8bbce10e 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -30,6 +30,7 @@ Item { property string roomTopic property string roomName property string callType + property int encryptionError property int relatedEventCacheBuster width: parent.width @@ -97,6 +98,7 @@ Item { roomName: r.roomName callType: r.callType relatedEventCacheBuster: r.relatedEventCacheBuster + encryptionError: r.encryptionError enabled: false width: parent.width isReply: true diff --git a/resources/res.qrc b/resources/res.qrc index d7187f42..f50265ca 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -143,14 +143,15 @@ qml/emoji/StickerPicker.qml qml/UserProfile.qml qml/delegates/MessageDelegate.qml - qml/delegates/TextMessage.qml - qml/delegates/NoticeMessage.qml - qml/delegates/ImageMessage.qml - qml/delegates/PlayableMediaMessage.qml + qml/delegates/Encrypted.qml qml/delegates/FileMessage.qml + qml/delegates/ImageMessage.qml + qml/delegates/NoticeMessage.qml qml/delegates/Pill.qml qml/delegates/Placeholder.qml + qml/delegates/PlayableMediaMessage.qml qml/delegates/Reply.qml + qml/delegates/TextMessage.qml qml/device-verification/Waiting.qml qml/device-verification/DeviceVerification.qml qml/device-verification/DigitVerification.qml diff --git a/src/Olm.cpp b/src/Olm.cpp index 048a6c0f..293b12de 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1069,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index, mtx::events::collections::TimelineEvent te; mtx::events::collections::from_json(body, te); - return {std::nullopt, std::nullopt, std::move(te.data)}; + return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; } catch (std::exception &e) { return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; } diff --git a/src/Olm.h b/src/Olm.h index a18cbbfb..ac1a1617 100644 --- a/src/Olm.h +++ b/src/Olm.h @@ -14,9 +14,11 @@ constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; namespace olm { +Q_NAMESPACE -enum class DecryptionErrorCode +enum DecryptionErrorCode { + NoError, MissingSession, // Session was not found, retrieve from backup or request from other devices // and try again MissingSessionIndex, // Session was found, but it does not reach back enough to this index, @@ -25,14 +27,13 @@ enum class DecryptionErrorCode DecryptionFailed, // libolm error ParsingFailed, // Failed to parse the actual event ReplayAttack, // Megolm index reused - UnknownFingerprint, // Unknown device Fingerprint }; +Q_ENUM_NS(DecryptionErrorCode) struct DecryptionResult { - std::optional error; + DecryptionErrorCode error; std::optional error_message; - std::optional event; }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 9a91ff79..742f8dbb 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -20,8 +20,7 @@ Q_DECLARE_METATYPE(Reaction) -QCache EventStore::decryptedEvents_{ - 1000}; +QCache EventStore::decryptedEvents_{1000}; QCache EventStore::events_by_id_{ 1000}; QCache EventStore::events_{1000}; @@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *) mtx::events::msg::Encrypted>) { auto event = decryptEvent({room_id_, e.event_id}, e); - if (auto dec = - std::get_if>(event)) { - emit updateFlowEventId( - event_id.event_id.to_string()); + if (event->event) { + if (auto dec = std::get_if< + mtx::events::RoomEvent< + mtx::events::msg:: + KeyVerificationRequest>>( + &event->event.value())) { + emit updateFlowEventId( + event_id.event_id + .to_string()); + } } } }); @@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events) if (auto encrypted = std::get_if>( &event)) { - mtx::events::collections::TimelineEvents *d_event = - decryptEvent({room_id_, encrypted->event_id}, *encrypted); - if (std::visit( + auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (d_event->event && + std::visit( [](auto e) { return (e.sender != utils::localUser().toStdString()); }, - *d_event)) { - handle_room_verification(*d_event); + *d_event->event)) { + handle_room_verification(*d_event->event); } } } @@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt) events_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent({room_id_, encrypted->event_id}, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } @@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); } -mtx::events::collections::TimelineEvents * +olm::DecryptionResult * EventStore::decryptEvent(const IdIndex &idx, const mtx::events::EncryptedEvent &e) { @@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id = e.content.session_id; index.sender_key = e.content.sender_key; - auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) { - auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event)); + auto asCacheEntry = [&idx](olm::DecryptionResult &&event) { + auto event_ptr = new olm::DecryptionResult(std::move(event)); decryptedEvents_.insert(idx, event_ptr); return event_ptr; }; auto decryptionResult = olm::decryptEvent(index, e); - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - if (decryptionResult.error) { - switch (*decryptionResult.error) { + switch (decryptionResult.error) { case olm::DecryptionErrorCode::MissingSession: case olm::DecryptionErrorCode::MissingSessionIndex: { - if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession) - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't " - "be " - "decrypted.") - .toStdString(); - else - dummy.content.body = - tr("-- Encrypted Event (Key not valid for this index) --", - "Placeholder, when the message can't be decrypted with this " - "key since it is not valid for this index ") - .toStdString(); nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", index.room_id, index.session_id, e.sender); - // we may not want to request keys during initial sync and such - if (suppressKeyRequests) - break; - // TODO: Check if this actually works and look in key backup - auto copy = e; - copy.room_id = room_id_; - if (pending_key_requests.count(e.content.session_id)) { - pending_key_requests.at(e.content.session_id) - .events.push_back(copy); - } else { - PendingKeyRequests request; - request.request_id = - "key_request." + http::client()->generate_txn_id(); - request.events.push_back(copy); - olm::send_key_request_for(copy, request.request_id); - pending_key_requests[e.content.session_id] = request; - } + + requestSession(e, false); break; } case olm::DecryptionErrorCode::DbError: @@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); break; case olm::DecryptionErrorCode::DecryptionFailed: nhlog::crypto()->critical( @@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg( - QString::fromStdString(decryptionResult.error_message.value_or(""))) - .toStdString(); break; case olm::DecryptionErrorCode::ParsingFailed: - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse " - "it, because " - "Nheko/mtxclient don't support that event type yet.") - .toStdString(); break; case olm::DecryptionErrorCode::ReplayAttack: nhlog::crypto()->critical( @@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx, e.event_id, room_id_, index.sender_key); - dummy.content.body = - tr("-- Replay attack! This message index was reused! --").toStdString(); break; - case olm::DecryptionErrorCode::UnknownFingerprint: - // TODO: don't fail, just show in UI. - nhlog::crypto()->critical("Message by unverified fingerprint {}", - index.sender_key); - dummy.content.body = - tr("-- Message by unverified device! --").toStdString(); + case olm::DecryptionErrorCode::NoError: + // unreachable break; } - return asCacheEntry(std::move(dummy)); - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = - olm::client()->decrypt_group_message(session.get(), e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); - return asCacheEntry(std::move(dummy)); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg(e.what()) - .toStdString(); - return asCacheEntry(std::move(dummy)); - } - - // 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; - body["unsigned"] = e.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], e.content.relations); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) { - auto encInfo = mtx::accessors::file(temp_events[0]); - - if (encInfo) - emit newEncryptedImage(encInfo.value()); - - return asCacheEntry(std::move(temp_events[0])); + return asCacheEntry(std::move(decryptionResult)); } auto encInfo = mtx::accessors::file(decryptionResult.event.value()); if (encInfo) emit newEncryptedImage(encInfo.value()); - return asCacheEntry(std::move(decryptionResult.event.value())); + return asCacheEntry(std::move(decryptionResult)); +} + +void +EventStore::requestSession(const mtx::events::EncryptedEvent &ev, + bool manual) +{ + // we may not want to request keys during initial sync and such + if (suppressKeyRequests) + return; + + // TODO: Look in key backup + auto copy = ev; + copy.room_id = room_id_; + if (pending_key_requests.count(ev.content.session_id)) { + auto &r = pending_key_requests.at(ev.content.session_id); + r.events.push_back(copy); + + // automatically request once every 10 min, manually every 1 min + qint64 delay = manual ? 60 : (60 * 10); + if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) { + r.requested_at = QDateTime::currentSecsSinceEpoch(); + olm::send_key_request_for(copy, r.request_id); + } + } else { + PendingKeyRequests request; + request.request_id = "key_request." + http::client()->generate_txn_id(); + request.requested_at = QDateTime::currentSecsSinceEpoch(); + request.events.push_back(copy); + olm::send_key_request_for(copy, request.request_id); + pending_key_requests[ev.content.session_id] = request; + } } void @@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool events_by_id_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent(index, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } +olm::DecryptionErrorCode +EventStore::decryptionError(std::string id) +{ + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); + + if (id.empty()) + return olm::DecryptionErrorCode::NoError; + + IdIndex index{room_id_, std::move(id)}; + auto edits_ = edits(index.id); + if (!edits_.empty()) { + index.id = mtx::accessors::event_id(edits_.back()); + auto event_ptr = + new mtx::events::collections::TimelineEvents(std::move(edits_.back())); + events_by_id_.insert(index, event_ptr); + } + + auto event_ptr = events_by_id_.object(index); + if (!event_ptr) { + auto event = cache::client()->getEvent(room_id_, index.id); + if (!event) { + return olm::DecryptionErrorCode::NoError; + } + event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); + events_by_id_.insert(index, event_ptr); + } + + if (auto encrypted = + std::get_if>(event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + return decrypted->error; + } + + return olm::DecryptionErrorCode::NoError; +} + void EventStore::fetchMore() { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index 7c404102..59c1c7c0 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -15,6 +15,7 @@ #include #include +#include "Olm.h" #include "Reaction.h" class EventStore : public QObject @@ -78,6 +79,9 @@ public: mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); QVariantList reactions(const std::string &event_id); + olm::DecryptionErrorCode decryptionError(std::string id); + void requestSession(const mtx::events::EncryptedEvent &ev, + bool manual); int size() const { @@ -119,7 +123,7 @@ public slots: private: std::vector edits(const std::string &event_id); - mtx::events::collections::TimelineEvents *decryptEvent( + olm::DecryptionResult *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent &e); void handle_room_verification(mtx::events::collections::TimelineEvents event); @@ -129,7 +133,7 @@ private: uint64_t first = std::numeric_limits::max(), last = std::numeric_limits::max(); - static QCache decryptedEvents_; + static QCache decryptedEvents_; static QCache events_; static QCache events_by_id_; @@ -137,6 +141,7 @@ private: { std::string request_id; std::vector> events; + qint64 requested_at; }; std::map pending_key_requests; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 10d9788d..99e00a67 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -452,6 +452,7 @@ TimelineModel::roleNames() const {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, {Trustlevel, "trustlevel"}, + {EncryptionError, "encryptionError"}, {ReplyTo, "replyTo"}, {Reactions, "reactions"}, {RoomId, "roomId"}, @@ -639,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return crypto::Trust::Unverified; } + case EncryptionError: + return events.decryptionError(event_id(event)); + case ReplyTo: return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { @@ -690,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r m.insert(names[RoomName], data(event, static_cast(RoomName))); m.insert(names[RoomTopic], data(event, static_cast(RoomTopic))); m.insert(names[CallType], data(event, static_cast(CallType))); + m.insert(names[EncryptionError], data(event, static_cast(EncryptionError))); return QVariant(m); } @@ -1551,6 +1556,17 @@ TimelineModel::scrollTimerEvent() } } +void +TimelineModel::requestKeyForEvent(QString id) +{ + auto encrypted_event = events.get(id.toStdString(), "", false); + if (encrypted_event) { + if (auto ev = std::get_if>( + encrypted_event)) + events.requestSession(*ev, true); + } +} + void TimelineModel::copyLinkToEvent(QString eventId) const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b5c8ca37..ad7cfbbb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -212,6 +212,7 @@ public: IsEditable, IsEncrypted, Trustlevel, + EncryptionError, ReplyTo, Reactions, RoomId, @@ -264,6 +265,8 @@ public: endResetModel(); } + Q_INVOKABLE void requestKeyForEvent(QString id); + std::vector<::Reaction> reactions(const std::string &event_id) { auto list = events.reactions(event_id); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 76bc127e..b23ed278 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -157,6 +157,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "MtxEvent", "Can't instantiate enum!"); + qmlRegisterUncreatableMetaObject( + olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject( crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject(verification::staticMetaObject, -- cgit 1.5.1