Show encryption errors in qml and add request keys button

This commit is contained in:
Nicolas Werner 2021-08-07 22:51:09 +02:00
parent 9f742fe23d
commit 72bbad7485
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
14 changed files with 220 additions and 165 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -143,14 +143,15 @@
<file>qml/emoji/StickerPicker.qml</file>
<file>qml/UserProfile.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>
<file>qml/delegates/ImageMessage.qml</file>
<file>qml/delegates/PlayableMediaMessage.qml</file>
<file>qml/delegates/Encrypted.qml</file>
<file>qml/delegates/FileMessage.qml</file>
<file>qml/delegates/ImageMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>
<file>qml/delegates/Pill.qml</file>
<file>qml/delegates/Placeholder.qml</file>
<file>qml/delegates/PlayableMediaMessage.qml</file>
<file>qml/delegates/Reply.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/device-verification/DeviceVerification.qml</file>
<file>qml/device-verification/DigitVerification.qml</file>

View file

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

View file

@ -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<DecryptionErrorCode> error;
DecryptionErrorCode error;
std::optional<std::string> error_message;
std::optional<mtx::events::collections::TimelineEvents> event;
};

View file

@ -20,8 +20,7 @@
Q_DECLARE_METATYPE(Reaction)
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
1000};
QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
1000};
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> 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<mtx::events::RoomEvent<
mtx::events::msg::
KeyVerificationRequest>>(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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
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<mtx::events::msg::Encrypted> &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<mtx::events::msg::Notice> 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<mtx::events::collections::TimelineEvents> 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<mtx::events::msg::Encrypted> &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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
auto decrypted = decryptEvent(index, *encrypted);
return decrypted->error;
}
return olm::DecryptionErrorCode::NoError;
}
void
EventStore::fetchMore()
{

View file

@ -15,6 +15,7 @@
#include <mtx/responses/messages.hpp>
#include <mtx/responses/sync.hpp>
#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<mtx::events::msg::Encrypted> &ev,
bool manual);
int size() const
{
@ -119,7 +123,7 @@ public slots:
private:
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
mtx::events::collections::TimelineEvents *decryptEvent(
olm::DecryptionResult *decryptEvent(
const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
void handle_room_verification(mtx::events::collections::TimelineEvents event);
@ -129,7 +133,7 @@ private:
uint64_t first = std::numeric_limits<uint64_t>::max(),
last = std::numeric_limits<uint64_t>::max();
static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
static QCache<Index, mtx::events::collections::TimelineEvents> events_;
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
@ -137,6 +141,7 @@ private:
{
std::string request_id;
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
qint64 requested_at;
};
std::map<std::string, PendingKeyRequests> pending_key_requests;

View file

@ -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<int>(RoomName)));
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
m.insert(names[EncryptionError], data(event, static_cast<int>(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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
encrypted_event))
events.requestSession(*ev, true);
}
}
void
TimelineModel::copyLinkToEvent(QString eventId) const
{

View file

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

View file

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