diff options
author | Tobias Fella <fella@posteo.de> | 2022-03-08 00:06:36 +0100 |
---|---|---|
committer | Tobias Fella <fella@posteo.de> | 2022-05-16 20:47:17 +0200 |
commit | efa450920e5fc338e771e653ca0889e948d04ee7 (patch) | |
tree | 215efb53d0e06ed660a97593d56ffb4868dbc2e2 | |
parent | 6f5ac9b7315d75692960e5eac7b1eb6867c0d203 (diff) | |
download | libquotient-efa450920e5fc338e771e653ca0889e948d04ee7.tar.gz libquotient-efa450920e5fc338e771e653ca0889e948d04ee7.zip |
Implement sending encrypted files
-rw-r--r-- | autotests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | autotests/testfilecrypto.cpp | 17 | ||||
-rw-r--r-- | autotests/testfilecrypto.h | 12 | ||||
-rw-r--r-- | lib/eventitem.cpp | 10 | ||||
-rw-r--r-- | lib/eventitem.h | 3 | ||||
-rw-r--r-- | lib/events/encryptedfile.cpp | 26 | ||||
-rw-r--r-- | lib/events/encryptedfile.h | 1 | ||||
-rw-r--r-- | lib/room.cpp | 67 | ||||
-rw-r--r-- | lib/room.h | 2 |
9 files changed, 113 insertions, 26 deletions
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 671d6c08..c11901bf 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -18,4 +18,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) quotient_add_test(NAME testolmutility) + quotient_add_test(NAME testfilecrypto) endif() diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp new file mode 100644 index 00000000..e6bec1fe --- /dev/null +++ b/autotests/testfilecrypto.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testfilecrypto.h" +#include "events/encryptedfile.h" +#include <qtest.h> + +using namespace Quotient; +void TestFileCrypto::encryptDecryptData() +{ + QByteArray data = "ABCDEF"; + auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto decrypted = file.decryptFile(cipherText); + QCOMPARE(data, decrypted); +} +QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h new file mode 100644 index 00000000..9096a8c7 --- /dev/null +++ b/autotests/testfilecrypto.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QtTest/QtTest> + +class TestFileCrypto : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void encryptDecryptData(); +}; diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index a2d65d8d..302ae053 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -26,6 +26,16 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) setStatus(EventStatus::FileUploaded); } +void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) +{ + if (auto* rme = getAs<RoomMessageEvent>()) { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([encryptedFile](EventContent::TypedBase& ec) { + ec.fileInfo()->file = encryptedFile; + }); + } +} + // Not exactly sure why but this helps with the linker not finding // Quotient::EventStatus::staticMetaObject when building Quaternion #include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index f04ef323..d8313736 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,6 +9,8 @@ #include <any> #include <utility> +#include "events/encryptedfile.h" + namespace Quotient { namespace EventStatus { @@ -114,6 +116,7 @@ public: void setDeparted() { setStatus(EventStatus::Departed); } void setFileUploaded(const QUrl& remoteUrl); + void setEncryptedFile(const EncryptedFile& encryptedFile); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index d4a517bd..e90be428 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -8,6 +8,7 @@ #ifdef Quotient_E2EE_ENABLED #include <openssl/evp.h> #include <QtCore/QCryptographicHash> +#include "e2ee/qolmutils.h" #endif using namespace Quotient; @@ -27,7 +28,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const { int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, @@ -44,7 +45,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const + length, &length); EVP_CIPHER_CTX_free(ctx); - return plaintext; + return plaintext.left(ciphertext.size()); } #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " @@ -53,6 +54,27 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const #endif } +std::pair<EncryptedFile, QByteArray> EncryptedFile::encryptFile(const QByteArray &plainText) +{ + 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(); + QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data())); + EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()), &length, reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size()); + EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(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}; +} + void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo, const EncryptedFile& pod) { diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index d0c4a030..2ce35086 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,6 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; + static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray &plainText); }; template <> diff --git a/lib/room.cpp b/lib/room.cpp index 7db9f8e9..763ca31c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -299,8 +299,7 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - //TODO deleteWhenFinishedis ugly, find out if there's something nicer - QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); + QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -2076,6 +2075,16 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; return {}; } + + return doSendEvent(addAsPending(std::move(event))); +} + +QString Room::Private::doSendEvent(const RoomEvent* pEvent) +{ + const auto txnId = pEvent->transactionId(); + // TODO, #133: Enqueue the job rather than immediately trigger it. + const RoomEvent* _event = pEvent; + if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); @@ -2083,10 +2092,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if this is necessary //TODO check if we increment the sent message count - event->setRoomId(id); - const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative<QOlmError>(encrypted)) { @@ -2098,23 +2105,14 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); - event->setTransactionId(encryptedEvent->transactionId()); // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - addAsPending(std::move(event)); - return doSendEvent(encryptedEvent, true); + _event = encryptedEvent; } - return doSendEvent(addAsPending(std::move(event))); -} - -QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) -{ - const auto txnId = pEvent->transactionId(); - // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, id, - pEvent->matrixType(), txnId, - pEvent->contentJson())) { + _event->matrixType(), txnId, + _event->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { @@ -2128,7 +2126,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -2140,8 +2138,8 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (deleteWhenFinished){ - delete pEvent; + if (q->usesEncryption()){ + delete _event; } }); } else @@ -2266,13 +2264,16 @@ 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) { + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable<EncryptedFile> encryptedFile) { 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()); @@ -2508,6 +2509,20 @@ 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> encryptedFile = std::nullopt; +#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()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + encryptedFile = e; + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; @@ -2516,9 +2531,15 @@ 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] { + connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + 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())); + } }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> encryptedFile = std::nullopt); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead |