aboutsummaryrefslogtreecommitdiff
path: root/lib/events
diff options
context:
space:
mode:
Diffstat (limited to 'lib/events')
-rw-r--r--lib/events/encryptedfile.cpp119
-rw-r--r--lib/events/encryptedfile.h63
-rw-r--r--lib/events/eventcontent.cpp68
-rw-r--r--lib/events/eventcontent.h53
-rw-r--r--lib/events/filesourceinfo.cpp181
-rw-r--r--lib/events/filesourceinfo.h89
-rw-r--r--lib/events/roomavatarevent.h4
-rw-r--r--lib/events/roommessageevent.cpp8
-rw-r--r--lib/events/stickerevent.cpp2
9 files changed, 335 insertions, 252 deletions
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 <carlschwan@kde.org>
-//
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "encryptedfile.h"
-#include "logging.h"
-
-#ifdef Quotient_E2EE_ENABLED
-#include <openssl/evp.h>
-#include <QtCore/QCryptographicHash>
-#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<const unsigned char*>(
- keyBytes.data()),
- reinterpret_cast<const unsigned char*>(
- QByteArray::fromBase64(iv.toLatin1()).data()));
- EVP_DecryptUpdate(
- ctx, reinterpret_cast<unsigned char*>(plaintext.data()), &length,
- reinterpret_cast<const unsigned char*>(ciphertext.data()),
- ciphertext.size());
- EVP_DecryptFinal_ex(ctx,
- reinterpret_cast<unsigned char*>(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, QByteArray> 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<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data()));
- const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
- QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
- 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};
-#else
- return {};
-#endif
-}
-
-void JsonObjectConverter<EncryptedFile>::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<EncryptedFile>::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<JWK>::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<JWK>::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 <carlschwan@kde.org>
-//
-// 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<QString, QString> hashes MEMBER hashes CONSTANT)
- Q_PROPERTY(QString v MEMBER v CONSTANT)
-
-public:
- QUrl url;
- JWK key;
- QString iv;
- QHash<QString, QString> hashes;
- QString v;
-
- QByteArray decryptFile(const QByteArray &ciphertext) const;
- static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray& plainText);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<EncryptedFile> {
- static void dumpTo(QJsonObject& jo, const EncryptedFile& pod);
- static void fillFrom(const QJsonObject& jo, EncryptedFile& pod);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<JWK> {
- 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> 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> 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<qint64>(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> 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> 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> encryptedFile)
+Thumbnail::Thumbnail(
+ const QJsonObject& infoJson,
+ const Omittable<EncryptedFileMetadata>& 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 <QtCore/QJsonObject>
+#include <QtCore/QMetaType>
#include <QtCore/QMimeType>
#include <QtCore/QSize>
#include <QtCore/QUrl>
-#include <QtCore/QMetaType>
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> : InfoT + url and thumbnail data
+ // UrlBasedContent<InfoT> : InfoT + thumbnail data
// PlayableContent<InfoT> : + duration attribute
// FileInfo
// FileContent = UrlBasedContent<FileInfo>
@@ -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> 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> 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<EncryptedFile> 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> encryptedFile = none,
const QString& originalFilename = {});
- ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
- Omittable<EncryptedFile> 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> encryptedFile = none);
+ const Omittable<EncryptedFileMetadata>& 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 InfoT>
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<Omittable<EncryptedFile>>(json["file"]),
json["filename"].toString())
, thumbnail(FileInfo::originalInfoJson)
{
+ const auto efmJson = json.value("file"_ls).toObject();
+ if (!efmJson.isEmpty())
+ InfoT::source = fromJson<EncryptedFileMetadata>(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 <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "filesourceinfo.h"
+
+#include "logging.h"
+
+#ifdef Quotient_E2EE_ENABLED
+# include "e2ee/qolmutils.h"
+
+# include <QtCore/QCryptographicHash>
+
+# include <openssl/evp.h>
+#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<const unsigned char*>(keyBytes.data()),
+ reinterpret_cast<const unsigned char*>(
+ QByteArray::fromBase64(iv.toLatin1()).data()));
+ EVP_DecryptUpdate(
+ ctx, reinterpret_cast<unsigned char*>(plaintext.data()), &length,
+ reinterpret_cast<const unsigned char*>(ciphertext.data()),
+ ciphertext.size());
+ EVP_DecryptFinal_ex(ctx,
+ reinterpret_cast<unsigned char*>(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, QByteArray> 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<const unsigned char*>(k.data()),
+ reinterpret_cast<const unsigned char*>(iv.data()));
+ const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
+ QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
+ 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();
+ EncryptedFileMetadata efm = { {},
+ key,
+ ivBase64.left(ivBase64.indexOf('=')),
+ { { QStringLiteral("sha256"),
+ hash.left(hash.indexOf('=')) } },
+ "v2"_ls };
+ return { efm, cipherText };
+#else
+ return {};
+#endif
+}
+
+void JsonObjectConverter<EncryptedFileMetadata>::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<EncryptedFileMetadata>::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<JWK>::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<JWK>::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 <typename... FunctorTs>
+struct Overloads : FunctorTs... {
+ using FunctorTs::operator()...;
+};
+
+template <typename... FunctorTs>
+Overloads(FunctorTs&&...) -> Overloads<FunctorTs...>;
+
+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<QLatin1String, 2>& 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<FileSourceInfo> == 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 <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+#include <array>
+
+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<QString, QString> hashes MEMBER hashes CONSTANT)
+ Q_PROPERTY(QString v MEMBER v CONSTANT)
+
+public:
+ QUrl url;
+ JWK key;
+ QString iv;
+ QHash<QString, QString> hashes;
+ QString v;
+
+ static std::pair<EncryptedFileMetadata, QByteArray> encryptFile(
+ const QByteArray& plainText);
+ QByteArray decryptFile(const QByteArray& ciphertext) const;
+};
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<EncryptedFileMetadata> {
+ static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod);
+ static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod);
+};
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<JWK> {
+ static void dumpTo(QJsonObject& jo, const JWK& pod);
+ static void fillFrom(const QJsonObject& jo, JWK& pod);
+};
+
+using FileSourceInfo = std::variant<QUrl, EncryptedFileMetadata>;
+
+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<QLatin1String, 2>& 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();
}