aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Fella <fella@posteo.de>2022-03-08 00:06:36 +0100
committerTobias Fella <fella@posteo.de>2022-05-16 20:47:17 +0200
commitefa450920e5fc338e771e653ca0889e948d04ee7 (patch)
tree215efb53d0e06ed660a97593d56ffb4868dbc2e2
parent6f5ac9b7315d75692960e5eac7b1eb6867c0d203 (diff)
downloadlibquotient-efa450920e5fc338e771e653ca0889e948d04ee7.tar.gz
libquotient-efa450920e5fc338e771e653ca0889e948d04ee7.zip
Implement sending encrypted files
-rw-r--r--autotests/CMakeLists.txt1
-rw-r--r--autotests/testfilecrypto.cpp17
-rw-r--r--autotests/testfilecrypto.h12
-rw-r--r--lib/eventitem.cpp10
-rw-r--r--lib/eventitem.h3
-rw-r--r--lib/events/encryptedfile.cpp26
-rw-r--r--lib/events/encryptedfile.h1
-rw-r--r--lib/room.cpp67
-rw-r--r--lib/room.h2
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()));
diff --git a/lib/room.h b/lib/room.h
index 6e6071f0..d5a8366a 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -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