aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/connection.cpp15
-rw-r--r--lib/connection.h5
-rw-r--r--lib/jobs/downloadfilejob.cpp97
-rw-r--r--lib/jobs/downloadfilejob.h3
-rw-r--r--lib/room.cpp59
-rw-r--r--lib/room.h3
6 files changed, 170 insertions, 12 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 77ab3b72..4a1130ae 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -1017,6 +1017,21 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
return job;
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(const QUrl& url,
+ const QString& key,
+ const QString& iv,
+ const QString& sha256,
+ const QString& localFilename)
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ auto* job =
+ callApi<DownloadFileJob>(idParts.front(), idParts.back(), key, iv, sha256, localFilename);
+ return job;
+}
+#endif
+
CreateRoomJob*
Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QString& name, const QString& topic,
diff --git a/lib/connection.h b/lib/connection.h
index e5cec34b..f9143d3e 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -567,6 +567,11 @@ public Q_SLOTS:
DownloadFileJob* downloadFile(const QUrl& url,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob* downloadFile(const QUrl& url, const QString &key,
+ const QString& iv, const QString& sha256,
+ const QString& localFilename = {});
+#endif
/**
* \brief Create a room (generic method)
* This method allows to customize room entirely to your liking,
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 0b0531ad..7a7a46f6 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -7,8 +7,25 @@
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QNetworkReply>
-using namespace Quotient;
+#ifdef Quotient_E2EE_ENABLED
+# include <QCryptographicHash>
+# include <openssl/evp.h>
+
+QByteArray decrypt(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv)
+{
+ QByteArray plaintext(ciphertext.size(), 0);
+ EVP_CIPHER_CTX *ctx;
+ int length;
+ ctx = EVP_CIPHER_CTX_new();
+ EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)key.data(), (const unsigned char *)iv.data());
+ EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size());
+ EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length);
+ EVP_CIPHER_CTX_free(ctx);
+ return plaintext;
+}
+#endif
+using namespace Quotient;
class DownloadFileJob::Private {
public:
Private() : tempFile(new QTemporaryFile()) {}
@@ -20,6 +37,12 @@ public:
QScopedPointer<QFile> targetFile;
QScopedPointer<QFile> tempFile;
+
+#ifdef Quotient_E2EE_ENABLED
+ QByteArray key;
+ QByteArray iv;
+ QByteArray sha256;
+#endif
};
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
@@ -37,6 +60,23 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
setObjectName(QStringLiteral("DownloadFileJob"));
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const QString& key,
+ const QString& iv,
+ const QString& sha256,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? new Private : new Private(localFilename))
+{
+ setObjectName(QStringLiteral("DownloadFileJob"));
+ auto _key = key;
+ d->key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1());
+ d->iv = QByteArray::fromBase64(iv.toLatin1());
+ d->sha256 = QByteArray::fromBase64(sha256.toLatin1());
+}
+#endif
QString DownloadFileJob::targetFileName() const
{
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
@@ -51,7 +91,7 @@ void DownloadFileJob::doPrepare()
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) {
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) {
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
setStatus(FileError, "Could not open the temporary download file");
@@ -99,18 +139,51 @@ void DownloadFileJob::beforeAbandon()
BaseJob::Status DownloadFileJob::prepareResult()
{
if (d->targetFile) {
- d->targetFile->close();
- if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
- return { FileError, "Couldn't finalise the download" };
+#ifdef Quotient_E2EE_ENABLED
+ if(d->key.size() != 0) {
+ d->tempFile->seek(0);
+ QByteArray encrypted = d->tempFile->readAll();
+ if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return IncorrectResponse;
+ }
+ auto decrypted = decrypt(encrypted, d->key, d->iv);
+ d->targetFile->write(decrypted);
+ d->targetFile->remove();
+ } else {
+#endif
+ d->targetFile->close();
+ if (!d->targetFile->remove()) {
+ qCWarning(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()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+#ifdef Quotient_E2EE_ENABLED
}
- if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
- << "to" << d->targetFile->fileName();
- return { FileError, "Couldn't finalise the download" };
+#endif
+ } else {
+#ifdef Quotient_E2EE_ENABLED
+ if(d->key.size() != 0) {
+ d->tempFile->seek(0);
+ auto encrypted = d->tempFile->readAll();
+
+ if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return IncorrectResponse;
+ }
+ auto decrypted = decrypt(encrypted, d->key, d->iv);
+ d->tempFile->write(decrypted);
+ } else {
+#endif
+ d->tempFile->close();
+#ifdef Quotient_E2EE_ENABLED
}
- } else
- d->tempFile->close();
+#endif
+ }
qCDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index 0752af89..f000b991 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -14,6 +14,9 @@ public:
DownloadFileJob(const QString& serverName, const QString& mediaId,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& key, const QString& iv, const QString& sha256, const QString& localFilename = {});
+#endif
QString targetFileName() const;
private:
diff --git a/lib/room.cpp b/lib/room.cpp
index b60a23f2..a4dfcb8f 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -2426,6 +2426,65 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
d->failedTransfer(eventId);
}
+#ifdef Quotient_E2EE_ENABLED
+void Room::downloadFile(const QString& eventId, const QString& key, const QString& iv, const QString& sha256, const QUrl& localFilename)
+{
+ if (auto ongoingTransfer = d->fileTransfers.constFind(eventId);
+ ongoingTransfer != d->fileTransfers.cend()
+ && ongoingTransfer->status == FileTransferInfo::Started) {
+ qCWarning(MAIN) << "Transfer for" << eventId
+ << "is ongoing; download won't start";
+ return;
+ }
+
+ Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(),
+ __FUNCTION__, "localFilename should point at a local file");
+ const auto* event = d->getEventWithFile(eventId);
+ if (!event) {
+ qCCritical(MAIN)
+ << eventId << "is not in the local timeline or has no file content";
+ Q_ASSERT(false);
+ return;
+ }
+ if (!event->contentJson().contains(QStringLiteral("file"))) {
+ qCWarning(MAIN) << "Event" << eventId
+ << "has an empty or malformed mxc URL; won't download";
+ return;
+ }
+ const auto fileUrl = QUrl(event->contentJson()["file"]["url"].toString());
+ auto filePath = localFilename.toLocalFile();
+ if (filePath.isEmpty()) { // Setup default file path
+ filePath =
+ fileUrl.path().mid(1) % '_' % d->fileNameToDownload(event);
+
+ if (filePath.size() > 200) // If too long, elide in the middle
+ filePath.replace(128, filePath.size() - 192, "---");
+
+ filePath = QDir::tempPath() % '/' % filePath;
+ qDebug(MAIN) << "File path:" << filePath;
+ }
+ auto job = connection()->downloadFile(fileUrl, key, iv, sha256, filePath);
+ if (isJobPending(job)) {
+ // If there was a previous transfer (completed or failed), overwrite it.
+ d->fileTransfers[eventId] = { job, job->targetFileName() };
+ connect(job, &BaseJob::downloadProgress, this,
+ [this, eventId](qint64 received, qint64 total) {
+ d->fileTransfers[eventId].update(received, total);
+ emit fileTransferProgress(eventId, received, total);
+ });
+ connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] {
+ d->fileTransfers[eventId].status = FileTransferInfo::Completed;
+ emit fileTransferCompleted(
+ eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()));
+ });
+ connect(job, &BaseJob::failure, this,
+ std::bind(&Private::failedTransfer, d, eventId,
+ job->errorString()));
+ } else
+ d->failedTransfer(eventId);
+}
+#endif
+
void Room::cancelFileTransfer(const QString& id)
{
const auto it = d->fileTransfers.find(id);
diff --git a/lib/room.h b/lib/room.h
index 85c51a87..ac21d08c 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -852,6 +852,9 @@ public Q_SLOTS:
const QString& overrideContentType = {});
// If localFilename is empty a temporary file is created
void downloadFile(const QString& eventId, const QUrl& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ void downloadFile(const QString& eventId, const QString& key, const QString &iv, const QString &sha256, const QUrl& localFilename = {});
+#endif
void cancelFileTransfer(const QString& id);
//! \brief Set a given event as last read and post a read receipt on it