From 98fdf62391fdc5135d8324476903a4c43345e732 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 6 May 2022 22:39:34 +0200 Subject: Fix race condition in consumeRoomData() QCoreApplication::processEvents() is well-known to be a _wrong_ solution to the unresponsive UI problem; despite that, connection.cpp has long had that call to let UI update itself while processing bulky room updates (mainly from the initial sync). This commit finally fixes this, after an (admittedly rare) race condition has been hit, as follows: 0. Pre-requisite: quotest runs all the tests and is about to leave the room; there's an ongoing sync request. 1. Quotest calls /leave 2. Sync returns, with the batch of _several_ rooms (that's important) 3. The above code handles the first room in the batch 4. processEvents() is called, just in time for the /leave response. 5. The /leave response handler in quotest ends up calling Connection::logout() (processEvents() still hasn't returned). 6. Connection::logout() calls abandon() on the ongoing SyncJob, pulling the rug from under onSyncSuccess()/consumeRoomData(). 7. processEvents() returns and the above code proceeds to the next room - only to find that the roomDataList (that is a ref to a structure owned by SyncJob), is now pointing to garbage. Morals of the story: 1. processEvents() effectively makes code multi-threaded: one flow is suspended and another one may run _on the same data_. After the first flow is resumed, it cannot make any assumptions regarding which data the second flow touched and/or changed. 2. The library had quite a few cases of using &&-refs, avoiding even move operations but also leaving ownership of the data with the original producer (SyncJob). If the lifetime of that producer ends too soon, those refs become dangling. The fix makes two important things, respectively: 2. Ownership of room data is now transfered to the processing side, the moment it is scheduled (see below), in the form of moving into a lambda capture. 1. Instead of processEvents(), processing of room data is scheduled via QMetaObject::invokeMethod(), uncoupling the moment when the data was received in SyncJob from the moment they are processed in Room::updateData() (and all the numerous signal-slots it calls). Also: Room::baseStateLoaded now causes Connection::loadedRoomState, not the other way round - this is more natural and doesn't need Connection to keep firstTimeRooms map around. --- lib/jobs/syncjob.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/jobs') diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 830a7c71..b7bfbbb3 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -15,7 +15,7 @@ public: explicit SyncJob(const QString& since, const Filter& filter, int timeout = -1, const QString& presence = {}); - SyncData&& takeData() { return std::move(d); } + SyncData takeData() { return std::move(d); } protected: Status prepareResult() override; -- cgit v1.2.3 From 0b5e72a2c6502f22a752b72b4df5fa25746fdd25 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 08:51:22 +0200 Subject: Refactor EncryptedFile and EC::FileInfo::file Besides having a misleading name (and it goes back to the spec), EncryptedFile under `file` key preempts the `url` (or `thumbnail_url`) string value so only one of the two should exist. This is a case for using std::variant<> - despite its clumsy syntax, it can actually simplify and streamline code when all the necessary bits are in place (such as conversion to JSON and getting the common piece - the URL - out of it). This commit replaces `FileInfo::url` and `FileInfo::file` with a common field `source` of type `FileSourceInfo` that is an alias for a variant type covering both underlying types; and `url()` is reintroduced as a function instead, to allow simplified access to whichever URL is available inside the variant. Oh, and EncryptedFile is EncryptedFileMetadata now, to clarify that it does not represent the file payload itself but rather the data necessary to obtain that payload. --- CMakeLists.txt | 2 +- autotests/testfilecrypto.cpp | 6 +- autotests/testolmaccount.cpp | 5 +- lib/connection.cpp | 11 ++- lib/connection.h | 5 +- lib/converters.h | 8 ++ lib/eventitem.cpp | 21 ++--- lib/eventitem.h | 9 +- lib/events/encryptedfile.cpp | 119 -------------------------- lib/events/encryptedfile.h | 63 -------------- lib/events/eventcontent.cpp | 68 +++++++-------- lib/events/eventcontent.h | 53 ++++++------ lib/events/filesourceinfo.cpp | 181 ++++++++++++++++++++++++++++++++++++++++ lib/events/filesourceinfo.h | 89 ++++++++++++++++++++ lib/events/roomavatarevent.h | 4 +- lib/events/roommessageevent.cpp | 8 +- lib/events/stickerevent.cpp | 2 +- lib/jobs/downloadfilejob.cpp | 13 +-- lib/jobs/downloadfilejob.h | 5 +- lib/mxcreply.cpp | 10 ++- lib/room.cpp | 85 +++++++++---------- lib/room.h | 3 +- quotest/quotest.cpp | 2 +- 23 files changed, 427 insertions(+), 345 deletions(-) delete mode 100644 lib/events/encryptedfile.cpp delete mode 100644 lib/events/encryptedfile.h create mode 100644 lib/events/filesourceinfo.cpp create mode 100644 lib/events/filesourceinfo.h (limited to 'lib/jobs') diff --git a/CMakeLists.txt b/CMakeLists.txt index 404ba87c..635efd90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,7 +168,7 @@ list(APPEND lib_SRCS lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp lib/events/stickerevent.h lib/events/stickerevent.cpp lib/events/keyverificationevent.h lib/events/keyverificationevent.cpp - lib/events/encryptedfile.h lib/events/encryptedfile.cpp + lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp lib/jobs/basejob.h lib/jobs/basejob.cpp lib/jobs/syncjob.h lib/jobs/syncjob.cpp diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index f9212376..b86114a4 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -3,14 +3,16 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testfilecrypto.h" -#include "events/encryptedfile.h" + +#include "events/filesourceinfo.h" + #include using namespace Quotient; void TestFileCrypto::encryptDecryptData() { QByteArray data = "ABCDEF"; - auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto [file, cipherText] = EncryptedFileMetadata::encryptFile(data); auto decrypted = file.decryptFile(cipherText); // AES CTR produces ciphertext of the same size as the original QCOMPARE(cipherText.size(), data.size()); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index e31ff6d3..b509d12f 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -156,8 +156,7 @@ void TestOlmAccount::encryptedFile() "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" }})"); - EncryptedFile file; - JsonObjectConverter::fillFrom(doc.object(), file); + const auto file = fromJson(doc); QCOMPARE(file.v, "v2"); QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA"); diff --git a/lib/connection.cpp b/lib/connection.cpp index dba18cb1..0994d85a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1137,15 +1137,14 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, } #ifdef Quotient_E2EE_ENABLED -DownloadFileJob* Connection::downloadFile(const QUrl& url, - const EncryptedFile& file, - const QString& localFilename) +DownloadFileJob* Connection::downloadFile( + const QUrl& url, const EncryptedFileMetadata& fileMetadata, + const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); - auto* job = - callApi(idParts.front(), idParts.back(), file, localFilename); - return job; + return callApi(idParts.front(), idParts.back(), + fileMetadata, localFilename); } #endif diff --git a/lib/connection.h b/lib/connection.h index f8744752..656e597c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -51,7 +51,7 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; class Database; -struct EncryptedFile; +struct EncryptedFileMetadata; class QOlmAccount; class QOlmInboundGroupSession; @@ -601,7 +601,8 @@ public Q_SLOTS: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file, + DownloadFileJob* downloadFile(const QUrl& url, + const EncryptedFileMetadata& file, const QString& localFilename = {}); #endif /** diff --git a/lib/converters.h b/lib/converters.h index 5e3becb8..49cb1ed9 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -16,6 +16,7 @@ #include #include +#include class QVariant; @@ -224,6 +225,13 @@ struct QUOTIENT_API JsonConverter { static QVariant load(const QJsonValue& jv); }; +template +inline QJsonValue toJson(const std::variant& v) +{ + return std::visit( + [](const auto& value) { return QJsonValue { toJson(value) }; }, v); +} + template struct JsonConverter> { static QJsonValue dump(const Omittable& from) diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 302ae053..a2e2a156 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -8,32 +8,23 @@ using namespace Quotient; -void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData) { // TODO: eventually we might introduce hasFileContent to RoomEvent, // and unify the code below. if (auto* rme = getAs()) { Q_ASSERT(rme->hasFileContent()); - rme->editContent([remoteUrl](EventContent::TypedBase& ec) { - ec.fileInfo()->url = remoteUrl; + rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) { + ec.fileInfo()->source = uploadedFileData; }); } if (auto* rae = getAs()) { Q_ASSERT(rae->content().fileInfo()); - rae->editContent( - [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; }); - } - setStatus(EventStatus::FileUploaded); -} - -void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) -{ - if (auto* rme = getAs()) { - Q_ASSERT(rme->hasFileContent()); - rme->editContent([encryptedFile](EventContent::TypedBase& ec) { - ec.fileInfo()->file = encryptedFile; + rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) { + fi.source = uploadedFileData; }); } + setStatus(EventStatus::FileUploaded); } // Not exactly sure why but this helps with the linker not finding diff --git a/lib/eventitem.h b/lib/eventitem.h index d8313736..5e001d88 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -3,14 +3,14 @@ #pragma once -#include "events/stateevent.h" #include "quotient_common.h" +#include "events/filesourceinfo.h" +#include "events/stateevent.h" + #include #include -#include "events/encryptedfile.h" - namespace Quotient { namespace EventStatus { @@ -115,8 +115,7 @@ public: QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } - void setFileUploaded(const QUrl& remoteUrl); - void setEncryptedFile(const EncryptedFile& encryptedFile); + void setFileUploaded(const FileSourceInfo &uploadedFileData); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp deleted file mode 100644 index 33ebb514..00000000 --- a/lib/events/encryptedfile.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "encryptedfile.h" -#include "logging.h" - -#ifdef Quotient_E2EE_ENABLED -#include -#include -#include "e2ee/qolmutils.h" -#endif - -using namespace Quotient; - -QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const -{ -#ifdef Quotient_E2EE_ENABLED - auto _key = key.k; - const auto keyBytes = QByteArray::fromBase64( - _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); - const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); - if (sha256 - != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return {}; - } - { - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - - 1, - '\0'); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, - reinterpret_cast( - keyBytes.data()), - reinterpret_cast( - QByteArray::fromBase64(iv.toLatin1()).data())); - EVP_DecryptUpdate( - ctx, reinterpret_cast(plaintext.data()), &length, - reinterpret_cast(ciphertext.data()), - ciphertext.size()); - EVP_DecryptFinal_ex(ctx, - reinterpret_cast(plaintext.data()) - + length, - &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext.left(ciphertext.size()); - } -#else - qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " - "cannot decrypt the file"; - return ciphertext; -#endif -} - -std::pair EncryptedFile::encryptFile(const QByteArray &plainText) -{ -#ifdef Quotient_E2EE_ENABLED - QByteArray k = getRandom(32); - auto kBase64 = k.toBase64(); - QByteArray iv = getRandom(16); - JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; - - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); - const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); - QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); - EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); - EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); - EVP_CIPHER_CTX_free(ctx); - - auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64(); - auto ivBase64 = iv.toBase64(); - EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; - return {file, cipherText}; -#else - return {}; -#endif -} - -void JsonObjectConverter::dumpTo(QJsonObject& jo, - const EncryptedFile& pod) -{ - addParam<>(jo, QStringLiteral("url"), pod.url); - addParam<>(jo, QStringLiteral("key"), pod.key); - addParam<>(jo, QStringLiteral("iv"), pod.iv); - addParam<>(jo, QStringLiteral("hashes"), pod.hashes); - addParam<>(jo, QStringLiteral("v"), pod.v); -} - -void JsonObjectConverter::fillFrom(const QJsonObject& jo, - EncryptedFile& pod) -{ - fromJson(jo.value("url"_ls), pod.url); - fromJson(jo.value("key"_ls), pod.key); - fromJson(jo.value("iv"_ls), pod.iv); - fromJson(jo.value("hashes"_ls), pod.hashes); - fromJson(jo.value("v"_ls), pod.v); -} - -void JsonObjectConverter::dumpTo(QJsonObject &jo, const JWK &pod) -{ - addParam<>(jo, QStringLiteral("kty"), pod.kty); - addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); - addParam<>(jo, QStringLiteral("alg"), pod.alg); - addParam<>(jo, QStringLiteral("k"), pod.k); - addParam<>(jo, QStringLiteral("ext"), pod.ext); -} - -void JsonObjectConverter::fillFrom(const QJsonObject &jo, JWK &pod) -{ - fromJson(jo.value("kty"_ls), pod.kty); - fromJson(jo.value("key_ops"_ls), pod.keyOps); - fromJson(jo.value("alg"_ls), pod.alg); - fromJson(jo.value("k"_ls), pod.k); - fromJson(jo.value("ext"_ls), pod.ext); -} diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h deleted file mode 100644 index 022ac91e..00000000 --- a/lib/events/encryptedfile.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "converters.h" - -namespace Quotient { -/** - * JSON Web Key object as specified in - * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes - * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. - */ -struct JWK -{ - Q_GADGET - Q_PROPERTY(QString kty MEMBER kty CONSTANT) - Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) - Q_PROPERTY(QString alg MEMBER alg CONSTANT) - Q_PROPERTY(QString k MEMBER k CONSTANT) - Q_PROPERTY(bool ext MEMBER ext CONSTANT) - -public: - QString kty; - QStringList keyOps; - QString alg; - QString k; - bool ext; -}; - -struct QUOTIENT_API EncryptedFile -{ - Q_GADGET - Q_PROPERTY(QUrl url MEMBER url CONSTANT) - Q_PROPERTY(JWK key MEMBER key CONSTANT) - Q_PROPERTY(QString iv MEMBER iv CONSTANT) - Q_PROPERTY(QHash hashes MEMBER hashes CONSTANT) - Q_PROPERTY(QString v MEMBER v CONSTANT) - -public: - QUrl url; - JWK key; - QString iv; - QHash hashes; - QString v; - - QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair encryptFile(const QByteArray& plainText); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const EncryptedFile& pod); - static void fillFrom(const QJsonObject& jo, EncryptedFile& pod); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const JWK& pod); - static void fillFrom(const QJsonObject& jo, JWK& pod); -}; -} // namespace Quotient diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 6218e3b8..36b647cb 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -19,23 +19,21 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QFileInfo &fi) - : mimeType(QMimeDatabase().mimeTypeForFile(fi)) - , url(QUrl::fromLocalFile(fi.filePath())) - , payloadSize(fi.size()) - , originalName(fi.fileName()) +FileInfo::FileInfo(const QFileInfo& fi) + : source(QUrl::fromLocalFile(fi.filePath())), + mimeType(QMimeDatabase().mimeTypeForFile(fi)), + payloadSize(fi.size()), + originalName(fi.fileName()) { Q_ASSERT(fi.isFile()); } -FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, - Omittable encryptedFile, - QString originalFilename) - : mimeType(mimeType) - , url(move(u)) +FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize, + const QMimeType& mimeType, QString originalFilename) + : source(move(sourceInfo)) + , mimeType(mimeType) , payloadSize(payloadSize) , originalName(move(originalFilename)) - , file(move(encryptedFile)) { if (!isValid()) qCWarning(MESSAGES) @@ -44,28 +42,28 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, "0.7; for local resources, use FileInfo(QFileInfo) instead"; } -FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, +FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, QString originalFilename) - : originalInfoJson(infoJson) + : source(move(sourceInfo)) + , originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(move(mxcUrl)) , payloadSize(fromJson(infoJson["size"_ls])) , originalName(move(originalFilename)) - , file(move(encryptedFile)) { - if(url.isEmpty() && file.has_value()) { - url = file->url; - } if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } bool FileInfo::isValid() const { - return url.scheme() == "mxc" - && (url.authority() + url.path()).count('/') == 1; + const auto& u = url(); + return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1; +} + +QUrl FileInfo::url() const +{ + return getUrlFromSourceInfo(source); } QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) @@ -75,7 +73,6 @@ QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) infoJson.insert(QStringLiteral("size"), info.payloadSize); if (info.mimeType.isValid()) infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name()); - //TODO add encryptedfile return infoJson; } @@ -83,17 +80,16 @@ ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) : FileInfo(fi), imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, - QSize imageSize, Omittable encryptedFile, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize, + const QMimeType& type, QSize imageSize, const QString& originalFilename) - : FileInfo(mxcUrl, fileSize, type, move(encryptedFile), originalFilename) + : FileInfo(move(sourceInfo), fileSize, type, originalFilename) , imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename) - : FileInfo(mxcUrl, infoJson, move(encryptedFile), originalFilename) + : FileInfo(move(sourceInfo), infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} @@ -107,16 +103,20 @@ QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) return infoJson; } -Thumbnail::Thumbnail(const QJsonObject& infoJson, - Omittable encryptedFile) +Thumbnail::Thumbnail( + const QJsonObject& infoJson, + const Omittable& encryptedFileMetadata) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject(), move(encryptedFile)) -{} + infoJson["thumbnail_info"_ls].toObject()) +{ + if (encryptedFileMetadata) + source = *encryptedFileMetadata; +} void Thumbnail::dumpTo(QJsonObject& infoJson) const { - if (url.isValid()) - infoJson.insert(QStringLiteral("thumbnail_url"), url.toString()); + if (url().isValid()) + fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source); if (!imageSize.isEmpty()) infoJson.insert(QStringLiteral("thumbnail_info"), toInfoJson(*this)); diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index bbd35618..23281876 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -6,14 +6,14 @@ // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). -#include "encryptedfile.h" +#include "filesourceinfo.h" #include "quotient_export.h" #include +#include #include #include #include -#include class QFileInfo; @@ -50,7 +50,7 @@ namespace EventContent { // A quick classes inheritance structure follows (the definitions are // spread across eventcontent.h and roommessageevent.h): - // UrlBasedContent : InfoT + url and thumbnail data + // UrlBasedContent : InfoT + thumbnail data // PlayableContent : + duration attribute // FileInfo // FileContent = UrlBasedContent @@ -89,34 +89,32 @@ namespace EventContent { //! //! \param fi a QFileInfo object referring to an existing file explicit FileInfo(const QFileInfo& fi); - explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, + explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, const QMimeType& mimeType = {}, - Omittable encryptedFile = none, QString originalFilename = {}); //! \brief Construct from a JSON `info` payload //! //! Make sure to pass the `info` subobject of content JSON, not the //! whole JSON content. - FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, + FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, QString originalFilename = {}); bool isValid() const; + QUrl url() const; //! \brief Extract media id from the URL //! //! This can be used, e.g., to construct a QML-facing image:// //! URI as follows: //! \code "image://provider/" + info.mediaId() \endcode - QString mediaId() const { return url.authority() + url.path(); } + QString mediaId() const { return url().authority() + url().path(); } public: + FileSourceInfo source; QJsonObject originalInfoJson; QMimeType mimeType; - QUrl url; qint64 payloadSize = 0; QString originalName; - Omittable file = none; }; QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); @@ -126,12 +124,10 @@ namespace EventContent { public: ImageInfo() = default; explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); - explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, + explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, const QMimeType& type = {}, QSize imageSize = {}, - Omittable encryptedFile = none, const QString& originalFilename = {}); - ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, + ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename = {}); public: @@ -144,12 +140,13 @@ namespace EventContent { //! //! This class saves/loads a thumbnail to/from `info` subobject of //! the JSON representation of event content; namely, `info/thumbnail_url` - //! and `info/thumbnail_info` fields are used. + //! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and + //! `info/thumbnail_info` fields are used. class QUOTIENT_API Thumbnail : public ImageInfo { public: using ImageInfo::ImageInfo; Thumbnail(const QJsonObject& infoJson, - Omittable encryptedFile = none); + const Omittable& encryptedFile = none); //! \brief Add thumbnail information to the passed `info` JSON object void dumpTo(QJsonObject& infoJson) const; @@ -169,10 +166,10 @@ namespace EventContent { //! \brief A template class for content types with a URL and additional info //! - //! Types that derive from this class template take `url` and, - //! optionally, `filename` values from the top-level JSON object and - //! the rest of information from the `info` subobject, as defined by - //! the parameter type. + //! Types that derive from this class template take `url` (or, if the file + //! is encrypted, `file`) and, optionally, `filename` values from + //! the top-level JSON object and the rest of information from the `info` + //! subobject, as defined by the parameter type. //! \tparam InfoT base info class - FileInfo or ImageInfo template class UrlBasedContent : public TypedBase, public InfoT { @@ -181,10 +178,12 @@ namespace EventContent { explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - fromJson>(json["file"]), json["filename"].toString()) , thumbnail(FileInfo::originalInfoJson) { + const auto efmJson = json.value("file"_ls).toObject(); + if (!efmJson.isEmpty()) + InfoT::source = fromJson(efmJson); // Two small hacks on originalJson to expose mediaIds to QML originalJson.insert("mediaId", InfoT::mediaId()); originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); @@ -204,11 +203,7 @@ namespace EventContent { void fillJson(QJsonObject& json) const override { - if (!InfoT::file.has_value()) { - json.insert("url", InfoT::url.toString()); - } else { - json.insert("file", Quotient::toJson(*InfoT::file)); - } + Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); if (!InfoT::originalName.isEmpty()) json.insert("filename", InfoT::originalName); auto infoJson = toInfoJson(*this); @@ -223,7 +218,7 @@ namespace EventContent { //! //! Available fields: //! - corresponding to the top-level JSON: - //! - url + //! - source (corresponding to `url` or `file` in JSON) //! - filename (extension to the spec) //! - corresponding to the `info` subobject: //! - payloadSize (`size` in JSON) @@ -241,12 +236,12 @@ namespace EventContent { //! //! Available fields: //! - corresponding to the top-level JSON: - //! - url + //! - source (corresponding to `url` or `file` in JSON) //! - filename //! - corresponding to the `info` subobject: //! - payloadSize (`size` in JSON) //! - mimeType (`mimetype` in JSON) - //! - thumbnail.url (`thumbnail_url` in JSON) + //! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) //! - corresponding to the `info/thumbnail_info` subobject: //! - thumbnail.payloadSize //! - thumbnail.mimeType diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp new file mode 100644 index 00000000..a64c7da8 --- /dev/null +++ b/lib/events/filesourceinfo.cpp @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "filesourceinfo.h" + +#include "logging.h" + +#ifdef Quotient_E2EE_ENABLED +# include "e2ee/qolmutils.h" + +# include + +# include +#endif + +using namespace Quotient; + +QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) const +{ +#ifdef Quotient_E2EE_ENABLED + auto _key = key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + const auto sha256 = + QByteArray::fromBase64(hashes["sha256"_ls].toLatin1()); + if (sha256 + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return {}; + } + { + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(keyBytes.data()), + reinterpret_cast( + QByteArray::fromBase64(iv.toLatin1()).data())); + EVP_DecryptUpdate( + ctx, reinterpret_cast(plaintext.data()), &length, + reinterpret_cast(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); + } +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +std::pair EncryptedFileMetadata::encryptFile( + const QByteArray& plainText) +{ +#ifdef Quotient_E2EE_ENABLED + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = { "oct"_ls, + { "encrypt"_ls, "decrypt"_ls }, + "A256CTR"_ls, + QString(k.toBase64()) + .replace(u'/', u'_') + .replace(u'+', u'-') + .left(kBase64.indexOf('=')), + true }; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(k.data()), + reinterpret_cast(iv.data())); + const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); + QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); + EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), + &length, + reinterpret_cast(plainText.data()), + plainText.size()); + EVP_EncryptFinal_ex(ctx, + reinterpret_cast(cipherText.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256) + .toBase64(); + auto ivBase64 = iv.toBase64(); + EncryptedFileMetadata efm = { {}, + key, + ivBase64.left(ivBase64.indexOf('=')), + { { QStringLiteral("sha256"), + hash.left(hash.indexOf('=')) } }, + "v2"_ls }; + return { efm, cipherText }; +#else + return {}; +#endif +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const EncryptedFileMetadata& pod) +{ + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + EncryptedFileMetadata& pod) +{ + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, const JWK& pod) +{ + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, JWK& pod) +{ + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); +} + +template +struct Overloads : FunctorTs... { + using FunctorTs::operator()...; +}; + +template +Overloads(FunctorTs&&...) -> Overloads; + +QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi) +{ + return std::visit(Overloads { [](const QUrl& url) { return url; }, + [](const EncryptedFileMetadata& efm) { + return efm.url; + } }, + fsi); +} + +void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl) +{ + std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; }, + [&newUrl](EncryptedFileMetadata& efm) { + efm.url = newUrl; + } }, + fsi); +} + +void Quotient::fillJson(QJsonObject& jo, + const std::array& jsonKeys, + const FileSourceInfo& fsi) +{ + // NB: Keeping variant_size_v out of the function signature for readability. + // NB2: Can't use jsonKeys directly inside static_assert as its value is + // unknown so the compiler cannot ensure size() is constexpr (go figure...) + static_assert( + std::variant_size_v == decltype(jsonKeys) {}.size()); + jo.insert(jsonKeys[fsi.index()], toJson(fsi)); +} diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h new file mode 100644 index 00000000..885601be --- /dev/null +++ b/lib/events/filesourceinfo.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include + +namespace Quotient { +/** + * JSON Web Key object as specified in + * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes + * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. + */ +struct JWK +{ + Q_GADGET + Q_PROPERTY(QString kty MEMBER kty CONSTANT) + Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) + Q_PROPERTY(QString alg MEMBER alg CONSTANT) + Q_PROPERTY(QString k MEMBER k CONSTANT) + Q_PROPERTY(bool ext MEMBER ext CONSTANT) + +public: + QString kty; + QStringList keyOps; + QString alg; + QString k; + bool ext; +}; + +struct QUOTIENT_API EncryptedFileMetadata { + Q_GADGET + Q_PROPERTY(QUrl url MEMBER url CONSTANT) + Q_PROPERTY(JWK key MEMBER key CONSTANT) + Q_PROPERTY(QString iv MEMBER iv CONSTANT) + Q_PROPERTY(QHash hashes MEMBER hashes CONSTANT) + Q_PROPERTY(QString v MEMBER v CONSTANT) + +public: + QUrl url; + JWK key; + QString iv; + QHash hashes; + QString v; + + static std::pair encryptFile( + const QByteArray& plainText); + QByteArray decryptFile(const QByteArray& ciphertext) const; +}; + +template <> +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod); +}; + +template <> +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); +}; + +using FileSourceInfo = std::variant; + +QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi); + +QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl); + +// The way FileSourceInfo is stored in JSON requires an extra parameter so +// the original template is not applicable +template <> +void fillJson(QJsonObject&, const FileSourceInfo&) = delete; + +//! \brief Export FileSourceInfo to a JSON object +//! +//! Depending on what is stored inside FileSourceInfo, this function will insert +//! - a key-to-string pair where key is taken from jsonKeys[0] and the string +//! is the URL, if FileSourceInfo stores a QUrl; +//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object +//! is the result of converting EncryptedFileMetadata to JSON, +//! if FileSourceInfo stores EncryptedFileMetadata +QUOTIENT_API void fillJson(QJsonObject& jo, + const std::array& jsonKeys, + const FileSourceInfo& fsi); + +} // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index c54b5801..af291696 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -26,10 +26,10 @@ public: const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - mxcUrl, fileSize, mimeType, imageSize, none, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, originalFilename }) {} - QUrl url() const { return content().url; } + QUrl url() const { return content().url(); } }; REGISTER_EVENT_TYPE(RoomAvatarEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index d9d3fbe0..2a6ae93c 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -148,21 +148,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), none, + QImageReader(filePath).size(), file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, - QMediaResource(localUrl).resolution(), none, + QMediaResource(localUrl).resolution(), file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, none, + return new AudioContent(localUrl, file.size(), mimeType, file.fileName()); } - return new FileContent(localUrl, file.size(), mimeType, none, file.fileName()); + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp index 628fd154..6d318f0e 100644 --- a/lib/events/stickerevent.cpp +++ b/lib/events/stickerevent.cpp @@ -22,5 +22,5 @@ const EventContent::ImageContent &StickerEvent::image() const QUrl StickerEvent::url() const { - return m_imageContent.url; + return m_imageContent.url(); } diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index d00fc5f4..85c235c7 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -8,8 +8,9 @@ #include #ifdef Quotient_E2EE_ENABLED -# include -# include "events/encryptedfile.h" +# include "events/filesourceinfo.h" + +# include #endif using namespace Quotient; @@ -26,7 +27,7 @@ public: QScopedPointer tempFile; #ifdef Quotient_E2EE_ENABLED - Omittable encryptedFile; + Omittable encryptedFile; #endif }; @@ -49,7 +50,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, #ifdef Quotient_E2EE_ENABLED DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, - const EncryptedFile& file, + const EncryptedFileMetadata& file, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? makeImpl() @@ -126,7 +127,7 @@ BaseJob::Status DownloadFileJob::prepareResult() d->tempFile->seek(0); QByteArray encrypted = d->tempFile->readAll(); - EncryptedFile file = *d->encryptedFile; + EncryptedFileMetadata file = *d->encryptedFile; const auto decrypted = file.decryptFile(encrypted); d->targetFile->write(decrypted); d->tempFile->remove(); @@ -151,7 +152,7 @@ BaseJob::Status DownloadFileJob::prepareResult() d->tempFile->seek(0); const auto encrypted = d->tempFile->readAll(); - EncryptedFile file = *d->encryptedFile; + EncryptedFileMetadata file = *d->encryptedFile; const auto decrypted = file.decryptFile(encrypted); d->tempFile->write(decrypted); } else { diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index ffa3d055..cbbfd244 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -4,7 +4,8 @@ #pragma once #include "csapi/content-repo.h" -#include "events/encryptedfile.h" + +#include "events/filesourceinfo.h" namespace Quotient { class QUOTIENT_API DownloadFileJob : public GetContentJob { @@ -16,7 +17,7 @@ public: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {}); #endif QString targetFileName() const; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 319d514a..b7993ad5 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -8,7 +8,7 @@ #include "room.h" #ifdef Quotient_E2EE_ENABLED -#include "events/encryptedfile.h" +#include "events/filesourceinfo.h" #endif using namespace Quotient; @@ -20,7 +20,7 @@ public: : m_reply(r) {} QNetworkReply* m_reply; - Omittable m_encryptedFile; + Omittable m_encryptedFile; QIODevice* m_device = nullptr; }; @@ -47,7 +47,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) if(!d->m_encryptedFile.has_value()) { d->m_device = d->m_reply; } else { - EncryptedFile file = *d->m_encryptedFile; + EncryptedFileMetadata file = *d->m_encryptedFile; auto buffer = new QBuffer(this); buffer->setData(file.decryptFile(d->m_reply->readAll())); buffer->open(ReadOnly); @@ -64,7 +64,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) auto eventIt = room->findInTimeline(eventId); if(eventIt != room->historyEdge()) { auto event = eventIt->viewAs(); - d->m_encryptedFile = event->content()->fileInfo()->file; + if (auto* efm = std::get_if( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; } #endif } diff --git a/lib/room.cpp b/lib/room.cpp index 7022a49d..20ea1159 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1525,7 +1525,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) const auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); return connection()->getUrlForApi( - thumbnail->url, thumbnail->imageSize); + thumbnail->url(), thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; @@ -1536,7 +1536,7 @@ QUrl Room::urlToDownload(const QString& eventId) const if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); - return connection()->getUrlForApi(fileInfo->url); + return connection()->getUrlForApi(fileInfo->url()); } return {}; } @@ -2275,28 +2275,26 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable encryptedFile) { - if (tId != txnId) - return; + [this, txnId](const QString& tId, const QUrl&, + const FileSourceInfo fileMetadata) { + if (tId != txnId) + return; - const auto it = q->findPendingEvent(txnId); - if (it != unsyncedEvents.end()) { - it->setFileUploaded(mxcUri); - if (encryptedFile) { - it->setEncryptedFile(*encryptedFile); - } - emit q->pendingEventChanged( - int(it - unsyncedEvents.begin())); - doSendEvent(it->get()); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was " - "cancelled"; - } - }); + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(fileMetadata); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) + << "File uploaded to" << getUrlFromSourceInfo(fileMetadata) + << "but the event referring to it was " + "cancelled"; + } + }); connect(q, &Room::fileTransferFailed, transferJob, [this, txnId](const QString& tId) { if (tId != txnId) @@ -2322,13 +2320,13 @@ QString Room::postFile(const QString& plainText, Q_ASSERT(content != nullptr && content->fileInfo() != nullptr); const auto* const fileInfo = content->fileInfo(); Q_ASSERT(fileInfo != nullptr); - QFileInfo localFile { fileInfo->url.toLocalFile() }; + QFileInfo localFile { fileInfo->url().toLocalFile() }; Q_ASSERT(localFile.isFile()); return d->doPostFile( makeEvent( plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content), - fileInfo->url); + fileInfo->url()); } #if QT_VERSION_MAJOR < 6 @@ -2520,18 +2518,19 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); - Omittable encryptedFile { none }; + FileSourceInfo fileMetadata; #ifdef Quotient_E2EE_ENABLED QTemporaryFile tempFile; if (usesEncryption()) { tempFile.open(); QFile file(localFilename.toLocalFile()); file.open(QFile::ReadOnly); - auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + QByteArray data; + std::tie(fileMetadata, data) = + EncryptedFileMetadata::encryptFile(file.readAll()); tempFile.write(data); tempFile.close(); fileName = QFileInfo(tempFile).absoluteFilePath(); - encryptedFile = e; } #endif auto job = connection()->uploadFile(fileName, overrideContentType); @@ -2542,17 +2541,13 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - if (encryptedFile) { - auto file = *encryptedFile; - file.url = QUrl(job->contentUri()); - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); - } else { - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); - } - - }); + connect(job, &BaseJob::success, this, + [this, id, localFilename, job, fileMetadata]() mutable { + // The lambda is mutable to change encryptedFileMetadata + d->fileTransfers[id].status = FileTransferInfo::Completed; + setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, fileMetadata); + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); @@ -2585,11 +2580,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) << "has an empty or malformed mxc URL; won't download"; return; } - const auto fileUrl = fileInfo->url; + const auto fileUrl = fileInfo->url(); auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Setup default file path filePath = - fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event); + fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event); if (filePath.size() > 200) // If too long, elide in the middle filePath.replace(128, filePath.size() - 192, "---"); @@ -2599,9 +2594,9 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) } DownloadFileJob *job = nullptr; #ifdef Quotient_E2EE_ENABLED - if(fileInfo->file.has_value()) { - auto file = *fileInfo->file; - job = connection()->downloadFile(fileUrl, file, filePath); + if (auto* fileMetadata = + std::get_if(&fileInfo->source)) { + job = connection()->downloadFile(fileUrl, *fileMetadata, filePath); } else { #endif job = connection()->downloadFile(fileUrl, filePath); @@ -2619,7 +2614,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( - eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()), none); + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, diff --git a/lib/room.h b/lib/room.h index c3bdc4a0..0636c4bb 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,8 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile); + void fileTransferCompleted(QString id, QUrl localFile, + FileSourceInfo fileMetadata); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 1eed865f..6bcd71cd 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -516,7 +516,7 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, && e.hasFileContent() && e.content()->fileInfo()->originalName == fileName && testDownload(targetRoom->connection()->makeMediaUrl( - e.content()->fileInfo()->url))); + e.content()->fileInfo()->url()))); }, [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); }); -- cgit v1.2.3 From c2d87291dbf8bd240e3e96138ec52aa5da22416b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:50:30 +0200 Subject: Move encryptFile/decryptFile out of EncryptedFileMetadata These are not operations on EncryptedFileMetadata but rather on a combination of EncryptedFileMetadata and ciphertext. If C++ had multimethods these could be bound to such a combination. --- autotests/testfilecrypto.cpp | 4 ++-- lib/events/filesourceinfo.cpp | 11 ++++++----- lib/events/filesourceinfo.h | 9 +++++---- lib/jobs/downloadfilejob.cpp | 4 ++-- lib/mxcreply.cpp | 4 ++-- lib/room.cpp | 3 +-- 6 files changed, 18 insertions(+), 17 deletions(-) (limited to 'lib/jobs') diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index b86114a4..29521060 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -12,8 +12,8 @@ using namespace Quotient; void TestFileCrypto::encryptDecryptData() { QByteArray data = "ABCDEF"; - auto [file, cipherText] = EncryptedFileMetadata::encryptFile(data); - auto decrypted = file.decryptFile(cipherText); + auto [file, cipherText] = encryptFile(data); + auto decrypted = decryptFile(cipherText, file); // AES CTR produces ciphertext of the same size as the original QCOMPARE(cipherText.size(), data.size()); QCOMPARE(decrypted.size(), data.size()); diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index a64c7da8..43e8e44c 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -16,14 +16,15 @@ using namespace Quotient; -QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) const +QByteArray Quotient::decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata) { #ifdef Quotient_E2EE_ENABLED - auto _key = key.k; + auto _key = metadata.key.k; const auto keyBytes = QByteArray::fromBase64( _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); const auto sha256 = - QByteArray::fromBase64(hashes["sha256"_ls].toLatin1()); + QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()); if (sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; @@ -37,7 +38,7 @@ QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) cons ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(keyBytes.data()), reinterpret_cast( - QByteArray::fromBase64(iv.toLatin1()).data())); + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); EVP_DecryptUpdate( ctx, reinterpret_cast(plaintext.data()), &length, reinterpret_cast(ciphertext.data()), @@ -56,7 +57,7 @@ QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) cons #endif } -std::pair EncryptedFileMetadata::encryptFile( +std::pair Quotient::encryptFile( const QByteArray& plainText) { #ifdef Quotient_E2EE_ENABLED diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h index 885601be..8f7e3cbe 100644 --- a/lib/events/filesourceinfo.h +++ b/lib/events/filesourceinfo.h @@ -45,12 +45,13 @@ public: QString iv; QHash hashes; QString v; - - static std::pair encryptFile( - const QByteArray& plainText); - QByteArray decryptFile(const QByteArray& ciphertext) const; }; +QUOTIENT_API std::pair encryptFile( + const QByteArray& plainText); +QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata); + template <> struct QUOTIENT_API JsonObjectConverter { static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 85c235c7..032b24f2 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -128,7 +128,7 @@ BaseJob::Status DownloadFileJob::prepareResult() QByteArray encrypted = d->tempFile->readAll(); EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); + const auto decrypted = decryptFile(encrypted, file); d->targetFile->write(decrypted); d->tempFile->remove(); } else { @@ -153,7 +153,7 @@ BaseJob::Status DownloadFileJob::prepareResult() const auto encrypted = d->tempFile->readAll(); EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); + const auto decrypted = decryptFile(encrypted, file); d->tempFile->write(decrypted); } else { #endif diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index b7993ad5..4174cfd8 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -47,9 +47,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) if(!d->m_encryptedFile.has_value()) { d->m_device = d->m_reply; } else { - EncryptedFileMetadata file = *d->m_encryptedFile; auto buffer = new QBuffer(this); - buffer->setData(file.decryptFile(d->m_reply->readAll())); + buffer->setData( + decryptFile(d->m_reply->readAll(), *d->m_encryptedFile)); buffer->open(ReadOnly); d->m_device = buffer; } diff --git a/lib/room.cpp b/lib/room.cpp index 0cef1025..4cb01a39 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2524,8 +2524,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, QFile file(localFilename.toLocalFile()); file.open(QFile::ReadOnly); QByteArray data; - std::tie(fileMetadata, data) = - EncryptedFileMetadata::encryptFile(file.readAll()); + std::tie(fileMetadata, data) = encryptFile(file.readAll()); tempFile.write(data); tempFile.close(); fileName = QFileInfo(tempFile).absoluteFilePath(); -- cgit v1.2.3 From 0e1f49a4ab8e6903709f387c154c2bf131a1370c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:56:56 +0200 Subject: DownloadFileJob: refactor file decryption --- lib/jobs/downloadfilejob.cpp | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) (limited to 'lib/jobs') diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 032b24f2..99c2bf9b 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -27,7 +27,7 @@ public: QScopedPointer tempFile; #ifdef Quotient_E2EE_ENABLED - Omittable encryptedFile; + Omittable encryptedFileMetadata; #endif }; @@ -57,7 +57,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, : makeImpl(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); - d->encryptedFile = file; + d->encryptedFileMetadata = file; } #endif QString DownloadFileJob::targetFileName() const @@ -119,17 +119,21 @@ void DownloadFileJob::beforeAbandon() d->tempFile->remove(); } +void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata, + QFile& targetFile) +{ + sourceFile.seek(0); + const auto encrypted = sourceFile.readAll(); // TODO: stream decryption + const auto decrypted = decryptFile(encrypted, metadata); + targetFile.write(decrypted); +} + BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - QByteArray encrypted = d->tempFile->readAll(); - - EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = decryptFile(encrypted, file); - d->targetFile->write(decrypted); + if (d->encryptedFileMetadata.has_value()) { + decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); d->tempFile->remove(); } else { #endif @@ -148,13 +152,20 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif } else { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - const auto encrypted = d->tempFile->readAll(); - - EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = decryptFile(encrypted, file); - d->tempFile->write(decrypted); + if (d->encryptedFileMetadata.has_value()) { + QTemporaryFile tempTempFile; // Assuming it to be next to tempFile + decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); + d->tempFile->close(); + if (!d->tempFile->remove()) { + qCWarning(JOBS) + << "Failed to remove the decrypted file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!tempTempFile.rename(d->tempFile->fileName())) { + qCWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + << "to" << d->tempFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } } else { #endif d->tempFile->close(); -- cgit v1.2.3 From c2e9256b1c334bdadcc208429084cbc83496fb4b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:57:23 +0200 Subject: Cleanup and address Sonar warnings --- lib/connection.h | 2 +- lib/events/eventcontent.cpp | 9 ++++---- lib/events/eventcontent.h | 6 +++--- lib/events/filesourceinfo.cpp | 49 ++++++++++++++++++++----------------------- lib/jobs/downloadfilejob.cpp | 10 ++++----- lib/room.cpp | 2 +- 6 files changed, 37 insertions(+), 41 deletions(-) (limited to 'lib/jobs') diff --git a/lib/connection.h b/lib/connection.h index 72383abb..43e285c1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -605,7 +605,7 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED DownloadFileJob* downloadFile(const QUrl& url, - const EncryptedFileMetadata& file, + const EncryptedFileMetadata& fileMetadata, const QString& localFilename = {}); #endif /** diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 36b647cb..8db3b7e3 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -103,14 +103,13 @@ QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) return infoJson; } -Thumbnail::Thumbnail( - const QJsonObject& infoJson, - const Omittable& encryptedFileMetadata) +Thumbnail::Thumbnail(const QJsonObject& infoJson, + const Omittable& efm) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), infoJson["thumbnail_info"_ls].toObject()) { - if (encryptedFileMetadata) - source = *encryptedFileMetadata; + if (efm) + source = *efm; } void Thumbnail::dumpTo(QJsonObject& infoJson) const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 23281876..ea240122 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -146,7 +146,7 @@ namespace EventContent { public: using ImageInfo::ImageInfo; Thumbnail(const QJsonObject& infoJson, - const Omittable& encryptedFile = none); + const Omittable& efm = none); //! \brief Add thumbnail information to the passed `info` JSON object void dumpTo(QJsonObject& infoJson) const; @@ -181,8 +181,8 @@ namespace EventContent { json["filename"].toString()) , thumbnail(FileInfo::originalInfoJson) { - const auto efmJson = json.value("file"_ls).toObject(); - if (!efmJson.isEmpty()) + if (const auto efmJson = json.value("file"_ls).toObject(); + !efmJson.isEmpty()) InfoT::source = fromJson(efmJson); // Two small hacks on originalJson to expose mediaIds to QML originalJson.insert("mediaId", InfoT::mediaId()); diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index 43e8e44c..11f93d80 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -20,36 +20,33 @@ QByteArray Quotient::decryptFile(const QByteArray& ciphertext, const EncryptedFileMetadata& metadata) { #ifdef Quotient_E2EE_ENABLED - auto _key = metadata.key.k; - const auto keyBytes = QByteArray::fromBase64( - _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); - const auto sha256 = - QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()); - if (sha256 + if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()) != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; return {}; } - { - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); - EVP_DecryptInit_ex( - ctx, EVP_aes_256_ctr(), nullptr, - reinterpret_cast(keyBytes.data()), - reinterpret_cast( - QByteArray::fromBase64(metadata.iv.toLatin1()).data())); - EVP_DecryptUpdate( - ctx, reinterpret_cast(plaintext.data()), &length, - reinterpret_cast(ciphertext.data()), - ciphertext.size()); - EVP_DecryptFinal_ex(ctx, - reinterpret_cast(plaintext.data()) - + length, - &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext.left(ciphertext.size()); - } + + auto _key = metadata.key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(keyBytes.data()), + reinterpret_cast( + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); + EVP_DecryptUpdate(ctx, reinterpret_cast(plaintext.data()), + &length, + reinterpret_cast(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " "cannot decrypt the file"; diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 99c2bf9b..759d52c9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -139,11 +139,11 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif d->targetFile->close(); if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; + qWarning(JOBS) << "Failed to remove the target file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() << "to" << d->targetFile->fileName(); return { FileError, "Couldn't finalise the download" }; } @@ -157,12 +157,12 @@ BaseJob::Status DownloadFileJob::prepareResult() decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); d->tempFile->close(); if (!d->tempFile->remove()) { - qCWarning(JOBS) + qWarning(JOBS) << "Failed to remove the decrypted file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!tempTempFile.rename(d->tempFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() << "to" << d->tempFile->fileName(); return { FileError, "Couldn't finalise the download" }; } @@ -173,6 +173,6 @@ BaseJob::Status DownloadFileJob::prepareResult() } #endif } - qCDebug(JOBS) << "Saved a file as" << targetFileName(); + qDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/room.cpp b/lib/room.cpp index 4cb01a39..26fe80e3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2274,7 +2274,7 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, [this, txnId](const QString& tId, const QUrl&, - const FileSourceInfo fileMetadata) { + const FileSourceInfo& fileMetadata) { if (tId != txnId) return; -- cgit v1.2.3 From 6b355d1aa87072143e09ea5269e8cf465318a64f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 21:26:43 +0200 Subject: Drop pre-Qt 5.15 code --- lib/connection.cpp | 16 ---------------- lib/converters.h | 10 +--------- lib/eventitem.h | 2 +- lib/jobs/basejob.cpp | 8 +------- lib/logging.h | 7 +------ lib/mxcreply.cpp | 8 +------- lib/networkaccessmanager.cpp | 15 +-------------- lib/quotient_common.h | 15 +-------------- lib/room.cpp | 7 ++----- lib/syncdata.cpp | 7 +------ 10 files changed, 10 insertions(+), 85 deletions(-) (limited to 'lib/jobs') diff --git a/lib/connection.cpp b/lib/connection.cpp index c390cc05..2319a38a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -49,10 +49,6 @@ # include #endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -# include -#endif - #include #include #include @@ -1826,16 +1822,10 @@ void Connection::saveRoomState(Room* r) const QFile outRoomFile { stateCacheDir().filePath( SyncData::fileNameForRoom(r->id())) }; if (outRoomFile.open(QFile::WriteOnly)) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(r->toJson()).toCbor() : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { r->toJson() }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif outRoomFile.write(data.data(), data.size()); qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); } else { @@ -1905,15 +1895,9 @@ void Connection::saveState() const } #endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { rootObj }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; outFile.write(data.data(), data.size()); diff --git a/lib/converters.h b/lib/converters.h index da30cae6..c985fd60 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -188,15 +188,7 @@ inline QDateTime fromJson(const QJsonValue& jv) return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); } -inline QJsonValue toJson(const QDate& val) { - return toJson( -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QDateTime(val) -#else - val.startOfDay() -#endif - ); -} +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> inline QDate fromJson(const QJsonValue& jv) { diff --git a/lib/eventitem.h b/lib/eventitem.h index 5e001d88..e476c66c 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -14,7 +14,7 @@ namespace Quotient { namespace EventStatus { - QUO_NAMESPACE + Q_NAMESPACE_EXPORT(QUOTIENT_API) /** Special marks an event can assume * diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b6858b5a..fe70911e 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -301,16 +301,10 @@ void BaseJob::Private::sendRequest() QNetworkRequest::NoLessSafeRedirectPolicy); req.setMaximumRedirectsAllowed(10); req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); - req.setAttribute( -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - QNetworkRequest::Http2AllowedAttribute -#else - QNetworkRequest::HTTP2AllowedAttribute -#endif // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at // what seems like an attempt to write to a closed channel. If/when that // changes, false should be turned to true below. - , false); + req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); diff --git a/lib/logging.h b/lib/logging.h index fc0a4c99..c8d17210 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -44,12 +44,7 @@ inline QDebug formatJson(QDebug debug_object) //! Suppress full qualification of enums/QFlags when logging inline QDebug terse(QDebug dbg) { - return -#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) - dbg.setVerbosity(0), dbg; -#else - dbg.verbosity(QDebug::MinimumVerbosity); -#endif + return dbg.verbosity(QDebug::MinimumVerbosity); } inline qint64 profilerMinNsecs() diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 4174cfd8..c7547be8 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -71,12 +71,6 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) #endif } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -#define ERROR_SIGNAL errorOccurred -#else -#define ERROR_SIGNAL error -#endif - MxcReply::MxcReply() : d(ZeroImpl()) { @@ -88,7 +82,7 @@ MxcReply::MxcReply() setError(QNetworkReply::ProtocolInvalidOperationError, BadRequestPhrase); setFinished(true); - emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError); + emit errorOccurred(QNetworkReply::ProtocolInvalidOperationError); emit finished(); }, Qt::QueuedConnection); } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index f4e7b1af..38ab07cc 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -68,24 +68,11 @@ void NetworkAccessManager::clearIgnoredSslErrors() d->ignoredSslErrors.clear(); } -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - // See #109; in newer Qt, bearer management is deprecated altogether - NetworkAccessManager::connect(nam, - &QNetworkAccessManager::networkAccessibleChanged, [nam] { - nam->setNetworkAccessible(QNetworkAccessManager::Accessible); - }); -#endif - return nam; -} - NetworkAccessManager* NetworkAccessManager::instance() { static QThreadStorage storage; if(!storage.hasLocalData()) { - storage.setLocalData(createNam()); + storage.setLocalData(new NetworkAccessManager()); } return storage.localData(); } diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 2b785a39..8bcd5ca6 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -51,21 +51,8 @@ #define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -// The first line forward-declares the namespace static metaobject with -// QUOTIENT_API so that dynamically linked clients could serialise flag/enum -// values from the namespace; Qt before 5.14 doesn't help with that. The second -// line is needed for moc to do its job on the namespace. -#define QUO_NAMESPACE \ - extern QUOTIENT_API const QMetaObject staticMetaObject; \ - Q_NAMESPACE -#else -// Since Qt 5.14.0, it's all packed in a single macro -#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) -#endif - namespace Quotient { -QUO_NAMESPACE +Q_NAMESPACE_EXPORT(QUOTIENT_API) // std::array {} needs explicit template parameters on macOS because // Apple stdlib doesn't have deduction guides for std::array. C++20 has diff --git a/lib/room.cpp b/lib/room.cpp index f692c354..e07de123 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1974,11 +1974,8 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) if (changes & Change::Highlights) emit q->highlightCountChanged(); - qCDebug(MAIN) << terse << changes << "= hex" << -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - Qt:: -#endif - hex << uint(changes) << "in" << q->objectName(); + qCDebug(MAIN) << terse << changes << "= hex" << Qt::hex << uint(changes) + << "in" << q->objectName(); emit q->changed(changes); if (saveState) connection->saveRoomState(q); diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 95d3c7e4..ff3dc0c2 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -179,12 +179,7 @@ QJsonObject SyncData::loadJson(const QString& fileName) const auto json = data.startsWith('{') ? QJsonDocument::fromJson(data).object() -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - : QCborValue::fromCbor(data).toJsonValue().toObject() -#else - : QJsonDocument::fromBinaryData(data).object() -#endif - ; + : QCborValue::fromCbor(data).toJsonValue().toObject(); if (json.isEmpty()) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; -- cgit v1.2.3 From 5a63f8a18645d612decdcc853335df0682c41d03 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 21:29:27 +0200 Subject: Drop make_array(); use std::to_array() where needed make_array() has been introduced to cover for shortcomings on macOS and Windows. These shortcomings are no more there, so we can just use the standardrlibrary. --- lib/e2ee/e2ee.h | 5 +++-- lib/events/encryptionevent.cpp | 4 +--- lib/jobs/basejob.cpp | 20 ++++++++++---------- lib/quotient_common.h | 27 +++++++-------------------- 4 files changed, 21 insertions(+), 35 deletions(-) (limited to 'lib/jobs') diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index f97eb27a..1efd0f16 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -37,8 +37,9 @@ constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; inline bool isSupportedAlgorithm(const QString& algorithm) { - static constexpr auto SupportedAlgorithms = - make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); + static constexpr std::array SupportedAlgorithms { + OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey + }; return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), algorithm) != SupportedAlgorithms.cend(); diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 6e994cd4..eb15f38e 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -9,9 +9,7 @@ #include namespace Quotient { -static const std::array encryptionStrings = { - { MegolmV1AesSha2AlgoKey } -}; +static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; template <> struct JsonConverter { diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index fe70911e..da645a2d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -138,9 +138,8 @@ public: QTimer timer; QTimer retryTimer; - static constexpr std::array errorStrategy { - { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } } - }; + static constexpr auto errorStrategy = std::to_array( + { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }); int maxRetries = int(errorStrategy.size()); int retriesTaken = 0; @@ -152,10 +151,8 @@ public: [[nodiscard]] QString dumpRequest() const { - // FIXME: use std::array {} when Apple stdlib gets deduction guides for it - static const auto verbs = - make_array(QStringLiteral("GET"), QStringLiteral("PUT"), - QStringLiteral("POST"), QStringLiteral("DELETE")); + static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls, + "DELETE"_ls }; const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) @@ -748,11 +745,14 @@ QString BaseJob::statusCaption() const } } -int BaseJob::error() const { return d->status.code; } +int BaseJob::error() const { + return d->status.code; } -QString BaseJob::errorString() const { return d->status.message; } +QString BaseJob::errorString() const { + return d->status.message; } -QUrl BaseJob::errorUrl() const { return d->errorUrl; } +QUrl BaseJob::errorUrl() const { + return d->errorUrl; } void BaseJob::setStatus(Status s) { diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 8bcd5ca6..136e9f79 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -41,7 +41,6 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) -// Apple Clang hasn't caught up with explicit(bool) yet #if __cpp_conditional_explicit >= 201806L #define QUO_IMPLICIT explicit(false) #else @@ -54,19 +53,6 @@ namespace Quotient { Q_NAMESPACE_EXPORT(QUOTIENT_API) -// std::array {} needs explicit template parameters on macOS because -// Apple stdlib doesn't have deduction guides for std::array. C++20 has -// to_array() but that can't be borrowed, this time because of MSVC: -// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038 -// Therefore a simpler (but also slightly more wobbly - it resolves the element -// type using std::common_type<>) make_array facility is implemented here. -template -constexpr auto make_array(Ts&&... items) -{ - return std::array, sizeof...(items)>( - { std::forward(items)... }); -} - // TODO: code like this should be generated from the CS API definition //! \brief Membership states @@ -87,9 +73,10 @@ enum class Membership : unsigned int { }; QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) -constexpr auto MembershipStrings = make_array( - // The order MUST be the same as the order in the original enum - "join", "leave", "invite", "knock", "ban"); +constexpr std::array MembershipStrings { + // The order MUST be the same as the order in the Membership enum + "join", "leave", "invite", "knock", "ban" +}; //! \brief Local user join-state names //! @@ -105,10 +92,10 @@ enum class JoinState : std::underlying_type_t { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -[[maybe_unused]] constexpr auto JoinStateStrings = make_array( +[[maybe_unused]] constexpr std::array JoinStateStrings { MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ -); +}; //! \brief Network job running policy flags //! @@ -135,7 +122,7 @@ enum RoomType { }; Q_ENUM_NS(RoomType) -[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space"); +[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) -- cgit v1.2.3 From 610631675826bb572bff97ce7d16d07097f14e3f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 20 Jun 2022 18:16:19 +0200 Subject: Clean up RequestData from Sonar warnings Also: make ImplPtr more flexible. --- lib/jobs/requestdata.cpp | 6 ++---- lib/jobs/requestdata.h | 22 +++++++++------------- lib/util.h | 29 +++++++++++++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) (limited to 'lib/jobs') diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 2c001ccc..ab249f6d 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -14,7 +14,7 @@ using namespace Quotient; auto fromData(const QByteArray& data) { - auto source = std::make_unique(); + auto source = makeImpl(); source->setData(data); source->open(QIODevice::ReadOnly); return source; @@ -33,7 +33,5 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} RequestData::RequestData(QIODevice* source) - : _source(std::unique_ptr(source)) + : _source(acquireImpl(source)) {} - -RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 41ad833a..accc8f71 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -3,11 +3,7 @@ #pragma once -#include "quotient_export.h" - -#include - -#include +#include "util.h" class QJsonObject; class QJsonArray; @@ -23,17 +19,17 @@ namespace Quotient { */ class QUOTIENT_API RequestData { public: - RequestData(const QByteArray& a = {}); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source); - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); + // NOLINTBEGIN(google-explicit-constructor): that check should learn about + // explicit(false) + QUO_IMPLICIT RequestData(const QByteArray& a = {}); + QUO_IMPLICIT RequestData(const QJsonObject& jo); + QUO_IMPLICIT RequestData(const QJsonArray& ja); + QUO_IMPLICIT RequestData(QIODevice* source); + // NOLINTEND(google-explicit-constructor) QIODevice* source() const { return _source.get(); } private: - std::unique_ptr _source; + ImplPtr _source; }; } // namespace Quotient diff --git a/lib/util.h b/lib/util.h index 8a30f457..46b1767e 100644 --- a/lib/util.h +++ b/lib/util.h @@ -135,8 +135,8 @@ inline std::pair findFirstOf(InputIt first, InputIt last, //! to define default constructors/operator=() out of line. //! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html //! for inspiration -template -using ImplPtr = std::unique_ptr; +template +using ImplPtr = std::unique_ptr; // Why this works (see also the link above): because this defers the moment // of requiring sizeof of ImplType to the place where makeImpl is invoked @@ -156,18 +156,27 @@ using ImplPtr = std::unique_ptr; //! //! Since std::make_unique is not compatible with ImplPtr, this should be used //! in constructors of frontend classes to create implementation instances. -template -inline ImplPtr makeImpl(ArgTs&&... args) +template +inline ImplPtr makeImpl(ArgTs&&... args) { - return ImplPtr { new ImplType(std::forward(args)...), - [](ImplType* impl) { delete impl; } }; + return ImplPtr { + new ImplType(std::forward(args)...), + [](TypeToDelete* impl) { delete impl; } + }; } -template -constexpr ImplPtr ZeroImpl() +template +inline ImplPtr acquireImpl(ImplType* from) { - return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; + return ImplPtr { from, [](TypeToDelete* impl) { + delete impl; + } }; +} + +template +constexpr ImplPtr ZeroImpl() +{ + return { nullptr, [](TypeToDelete*) { /* nullptr doesn't need deletion */ } }; } //! \brief Multiplex several functors in one -- cgit v1.2.3