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. --- 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 +- 9 files changed, 335 insertions(+), 252 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/events') 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(); } -- 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/events') 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 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/events') 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 0f8335a32debc4c61d9fc9875c79c0ba6ba05357 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 27 May 2022 19:09:26 +0200 Subject: Move some Meg/Olm session logic from Room::Private to Connection::Private Functions (Room::Private::)createOlmSession, payloadForUserDevice and sendRoomKeyToDevices don't have a lot to do with the given Room object but deal with quite a few things stored in Connection. This commit moves them to Connection::Private, exposing sendSessionKeyToDevices (the new name for sendRoomKeyToDevices) in Connection so that Room could call it from Room::P::sendMegolmSession(). While moving these over, a few additional things were adjusted: - more functions marked as const - a few functions could be moved now from Connection to Connection::Private - false slots in Connection (such as picklingMode) are moved out of the slots block - keys.yml in Matrix CS API definitions has been adjusted to match the real structure of `/claim` response (see quotient-im/matrix-spec repo); csapi/keys.h has been regenerated accordingly. --- autotests/testolmaccount.cpp | 80 +++++++-------- lib/connection.cpp | 227 ++++++++++++++++++++++++++++++++++--------- lib/connection.h | 34 +++---- lib/csapi/keys.h | 4 +- lib/e2ee/qolmsession.cpp | 5 +- lib/e2ee/qolmsession.h | 2 +- lib/events/roomkeyevent.h | 4 +- lib/room.cpp | 113 +-------------------- 8 files changed, 249 insertions(+), 220 deletions(-) (limited to 'lib/events') diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index b509d12f..3fb8ac24 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -378,47 +378,47 @@ void TestOlmAccount::claimKeys() // Alice retrieves bob's keys & claims one signed one-time key. QHash deviceKeys; deviceKeys[bob->userId()] = QStringList(); - auto job = alice->callApi(deviceKeys); - connect(job, &BaseJob::result, this, [bob, alice, job, this] { - const auto& bobDevices = job->deviceKeys().value(bob->userId()); - QVERIFY(!bobDevices.empty()); - - // Retrieve the identity key for the current device. - const auto& bobEd25519 = - bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; - - const auto currentDevice = bobDevices[bob->deviceId()]; - - // Verify signature. - QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), - bob->userId())); - - QHash> oneTimeKeys; - oneTimeKeys[bob->userId()] = QHash(); - oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; - - auto job = alice->callApi(oneTimeKeys); - connect(job, &BaseJob::result, this, [bob, bobEd25519, job] { - const auto userId = bob->userId(); - const auto deviceId = bob->deviceId(); - - // The device exists. - QCOMPARE(job->oneTimeKeys().size(), 1); - QCOMPARE(job->oneTimeKeys().value(userId).size(), 1); - - // The key is the one bob sent. - const auto& oneTimeKey = - job->oneTimeKeys().value(userId).value(deviceId); - QVERIFY(oneTimeKey.canConvert()); - - const auto varMap = oneTimeKey.toMap(); - QVERIFY(std::any_of(varMap.constKeyValueBegin(), - varMap.constKeyValueEnd(), [](const auto& kv) { - return kv.first.startsWith( - SignedCurve25519Key); - })); - }); + auto queryKeysJob = alice->callApi(deviceKeys); + QSignalSpy requestSpy2(queryKeysJob, &BaseJob::result); + QVERIFY(requestSpy2.wait(10000)); + + const auto& bobDevices = queryKeysJob->deviceKeys().value(bob->userId()); + QVERIFY(!bobDevices.empty()); + + const auto currentDevice = bobDevices[bob->deviceId()]; + + // Verify signature. + QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), + bob->userId())); + // Retrieve the identity key for the current device. + const auto& bobEd25519 = + bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; + + QHash> oneTimeKeys; + oneTimeKeys[bob->userId()] = QHash(); + oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; + + auto claimKeysJob = alice->callApi(oneTimeKeys); + connect(claimKeysJob, &BaseJob::result, this, [bob, bobEd25519, claimKeysJob] { + const auto userId = bob->userId(); + const auto deviceId = bob->deviceId(); + + // The device exists. + QCOMPARE(claimKeysJob->oneTimeKeys().size(), 1); + QCOMPARE(claimKeysJob->oneTimeKeys().value(userId).size(), 1); + + // The key is the one bob sent. + const auto& oneTimeKeys = + claimKeysJob->oneTimeKeys().value(userId).value(deviceId); + for (auto it = oneTimeKeys.begin(); it != oneTimeKeys.end(); ++it) { + if (it.key().startsWith(SignedCurve25519Key) + && it.value().isObject()) + return; + } + QFAIL("The claimed one time key is not in /claim response"); }); + QSignalSpy completionSpy(claimKeysJob, &BaseJob::result); + QVERIFY(completionSpy.wait(10000)); } void TestOlmAccount::claimMultipleKeys() diff --git a/lib/connection.cpp b/lib/connection.cpp index 1193eb75..ab4a7dea 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,6 +39,7 @@ # include "e2ee/qolmaccount.h" # include "e2ee/qolminboundsession.h" # include "e2ee/qolmsession.h" +# include "e2ee/qolmutility.h" # include "e2ee/qolmutils.h" # if QT_VERSION_MAJOR >= 6 @@ -62,7 +63,6 @@ #include #include - using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -210,11 +210,11 @@ public: #ifdef Quotient_E2EE_ENABLED void loadSessions() { - olmSessions = q->database()->loadOlmSessions(q->picklingMode()); + olmSessions = q->database()->loadOlmSessions(picklingMode); } - void saveSession(QOlmSession& session, const QString& senderKey) + void saveSession(const QOlmSession& session, const QString& senderKey) const { - if (auto pickleResult = session.pickle(q->picklingMode())) + if (auto pickleResult = session.pickle(picklingMode)) q->database()->saveOlmSession(senderKey, session.sessionId(), *pickleResult, QDateTime::currentDateTime()); @@ -364,9 +364,27 @@ public: #endif // Quotient_E2EE_ENABLED } #ifdef Quotient_E2EE_ENABLED + bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; + void loadOutdatedUserDevices(); void saveDevicesList(); void loadDevicesList(); + + // This function assumes that an olm session with (user, device) exists + std::pair olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const; + bool createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const QJsonObject& oneTimeKeyObject); + QString curveKeyForUserDevice(const QString& userId, + const QString& device) const; + QString edKeyForUserDevice(const QString& userId, + const QString& device) const; + std::unique_ptr makeEventForSessionKey( + const QString& roomId, const QString& targetUserId, + const QString& targetDeviceId, const QByteArray& sessionId, + const QByteArray& sessionKey) const; #endif }; @@ -935,20 +953,23 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED if (!toDeviceEvents.empty()) { - qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() + << "to-device events"; visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); + qCDebug(E2EE) << "Unsupported algorithm" << event.id() + << "for event" << event.algorithm(); return; } - if (q->isKnownCurveKey(event.senderId(), event.senderKey())) { + if (isKnownCurveKey(event.senderId(), event.senderKey())) { handleEncryptedToDeviceEvent(event); return; } trackedUsers += event.senderId(); outdatedUsers += event.senderId(); encryptionUpdateRequired = true; - pendingEncryptedEvents.push_back(std::make_unique(event.fullJson())); + pendingEncryptedEvents.push_back( + makeEvent(event.fullJson())); }); } #endif @@ -1316,9 +1337,8 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) return forgetJob; } -SendToDeviceJob* -Connection::sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) +SendToDeviceJob* Connection::sendToDevices( + const QString& eventType, const UsersToDevicesToEvents& eventsMap) { QHash> json; json.reserve(int(eventsMap.size())); @@ -2063,7 +2083,7 @@ void Connection::Private::loadOutdatedUserDevices() saveDevicesList(); for(size_t i = 0; i < pendingEncryptedEvents.size();) { - if (q->isKnownCurveKey( + if (isKnownCurveKey( pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentPart("sender_key"_ls))) { handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); @@ -2193,13 +2213,13 @@ Database* Connection::database() const } UnorderedMap -Connection::loadRoomMegolmSessions(const Room* room) +Connection::loadRoomMegolmSessions(const Room* room) const { return database()->loadMegolmSessions(room->id(), picklingMode()); } void Connection::saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session) + const QOlmInboundGroupSession& session) const { database()->saveMegolmSession(room->id(), session.sessionId(), session.pickle(picklingMode()), @@ -2211,64 +2231,179 @@ QStringList Connection::devicesForUser(const QString& userId) const return d->deviceKeys[userId].keys(); } -QString Connection::curveKeyForUserDevice(const QString& userId, - const QString& device) const +QString Connection::Private::curveKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[userId][device].keys["curve25519:" % device]; + return deviceKeys[userId][device].keys["curve25519:" % device]; } -QString Connection::edKeyForUserDevice(const QString& userId, - const QString& device) const +QString Connection::Private::edKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[userId][device].keys["ed25519:" % device]; + return deviceKeys[userId][device].keys["ed25519:" % device]; } -bool Connection::isKnownCurveKey(const QString& userId, - const QString& curveKey) const +bool Connection::Private::isKnownCurveKey(const QString& userId, + const QString& curveKey) const { - auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey")); + auto query = database->prepareQuery( + QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId " + "AND curveKey=:curveKey")); query.bindValue(":matrixId", userId); query.bindValue(":curveKey", curveKey); - database()->execute(query); + database->execute(query); return query.next(); } -bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const +bool Connection::hasOlmSession(const QString& user, + const QString& deviceId) const { - const auto& curveKey = curveKeyForUserDevice(user, deviceId); + const auto& curveKey = d->curveKeyForUserDevice(user, deviceId); return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } -std::pair Connection::olmEncryptMessage( - const QString& userId, const QString& device, const QByteArray& message) const +std::pair Connection::Private::olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const { const auto& curveKey = curveKeyForUserDevice(userId, device); - QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); - const auto result = d->olmSessions[curveKey][0]->encrypt(message); - if (const auto pickle = - d->olmSessions[curveKey][0]->pickle(picklingMode())) { - database()->updateOlmSession(curveKey, - d->olmSessions[curveKey][0]->sessionId(), - *pickle); + const auto& olmSession = olmSessions.at(curveKey).front(); + QOlmMessage::Type type = olmSession->encryptMessageType(); + const auto result = olmSession->encrypt(message); + if (const auto pickle = olmSession->pickle(picklingMode)) { + database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle); } else { - qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); + qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } return { type, result.toCiphertext() }; } -void Connection::createOlmSession(const QString& theirIdentityKey, - const QString& theirOneTimeKey) const -{ - auto session = QOlmSession::createOutboundSession(olmAccount(), - theirIdentityKey, - theirOneTimeKey); +bool Connection::Private::createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const QJsonObject& oneTimeKeyObject) +{ + static QOlmUtility verifier; + qDebug(E2EE) << "Creating a new session for" << targetUserId + << targetDeviceId; + if (oneTimeKeyObject.isEmpty()) { + qWarning(E2EE) << "No one time key for" << targetUserId + << targetDeviceId; + return false; + } + auto signedOneTimeKey = oneTimeKeyObject.constBegin()->toObject(); + // Verify contents of signedOneTimeKey - for that, drop `signatures` and + // `unsigned` and then verify the object against the respective signature + const auto signature = + signedOneTimeKey.take("signatures"_ls)[targetUserId]["ed25519:"_ls % targetDeviceId] + .toString() + .toLatin1(); + signedOneTimeKey.remove("unsigned"_ls); + if (!verifier.ed25519Verify( + edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), + QJsonDocument(signedOneTimeKey).toJson(QJsonDocument::Compact), + signature)) { + qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId + << targetDeviceId << ". Skipping this device."; + return false; + } + const auto recipientCurveKey = + curveKeyForUserDevice(targetUserId, targetDeviceId); + auto session = + QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, + signedOneTimeKey["key"].toString()); if (!session) { qCWarning(E2EE) << "Failed to create olm session for " - << theirIdentityKey << session.error(); + << recipientCurveKey << session.error(); + return false; + } + saveSession(**session, recipientCurveKey); + olmSessions[recipientCurveKey].push_back(std::move(*session)); + return true; +} + +std::unique_ptr Connection::Private::makeEventForSessionKey( + const QString& roomId, const QString& targetUserId, + const QString& targetDeviceId, const QByteArray& sessionId, + const QByteArray& sessionKey) const +{ + // Noisy but nice for debugging + // qDebug(E2EE) << "Creating the payload for" << data->userId() << device << + // sessionId << sessionKey.toHex(); + const auto event = makeEvent("m.megolm.v1.aes-sha2", roomId, + sessionId, sessionKey, + data->userId()); + auto payloadJson = event->fullJson(); + payloadJson.insert("recipient"_ls, targetUserId); + payloadJson.insert(SenderKeyL, data->userId()); + payloadJson.insert("recipient_keys"_ls, + QJsonObject { { Ed25519Key, + edKeyForUserDevice(targetUserId, + targetDeviceId) } }); + payloadJson.insert("keys"_ls, + QJsonObject { + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); + payloadJson.insert("sender_device"_ls, data->deviceId()); + + const auto [type, cipherText] = olmEncryptMessage( + targetUserId, targetDeviceId, + QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted { + { curveKeyForUserDevice(targetUserId, targetDeviceId), + QJsonObject { { "type"_ls, type }, + { "body"_ls, QString(cipherText) } } } + }; + + return makeEvent(encrypted, + olmAccount->identityKeys().curve25519); +} + +void Connection::sendSessionKeyToDevices( + const QString& roomId, const QByteArray& sessionId, + const QByteArray& sessionKey, const QMultiHash& devices, + int index) +{ + qDebug(E2EE) << "Sending room key to devices:" << sessionId + << sessionKey.toHex(); + QHash> hash; + for (const auto& [userId, deviceId] : asKeyValueRange(devices)) + if (!hasOlmSession(userId, deviceId)) { + hash[userId].insert(deviceId, "signed_curve25519"_ls); + qDebug(E2EE) << "Adding" << userId << deviceId + << "to keys to claim"; + } + + if (hash.isEmpty()) return; - } - d->saveSession(**session, theirIdentityKey); - d->olmSessions[theirIdentityKey].push_back(std::move(*session)); + + auto job = callApi(hash); + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { + UsersToDevicesToEvents usersToDevicesToEvents; + const auto oneTimeKeys = job->oneTimeKeys(); + for (const auto& [targetUserId, targetDeviceId] : + asKeyValueRange(devices)) { + if (!hasOlmSession(targetUserId, targetDeviceId) + && !d->createOlmSession( + targetUserId, targetDeviceId, + oneTimeKeys[targetUserId][targetDeviceId])) + continue; + + usersToDevicesToEvents[targetUserId][targetDeviceId] = + d->makeEventForSessionKey(roomId, targetUserId, targetDeviceId, + sessionId, sessionKey); + } + if (!usersToDevicesToEvents.empty()) { + sendToDevices(EncryptedEvent::TypeId, usersToDevicesToEvents); + QVector> receivedDevices; + receivedDevices.reserve(devices.size()); + for (const auto& [user, device] : asKeyValueRange(devices)) + receivedDevices.push_back( + { user, device, d->curveKeyForUserDevice(user, device) }); + + database()->setDevicesReceivedKey(roomId, receivedDevices, + sessionId, index); + } + }); } QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( diff --git a/lib/connection.h b/lib/connection.h index a2824744..5b806350 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -319,22 +319,26 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database() const; + PicklingMode picklingMode() const; UnorderedMap loadRoomMegolmSessions( - const Room* room); + const Room* room) const; void saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session); + const QOlmInboundGroupSession& session) const; bool hasOlmSession(const QString& user, const QString& deviceId) const; QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( const QString& roomId) const; - void saveCurrentOutboundMegolmSession(const QString& roomId, const QOlmOutboundGroupSession &session) const; - - //This assumes that an olm session with (user, device) exists - std::pair olmEncryptMessage( - const QString& userId, const QString& device, - const QByteArray& message) const; - void createOlmSession(const QString& theirIdentityKey, - const QString& theirOneTimeKey) const; + void saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const; + + void sendSessionKeyToDevices(const QString& roomId, + const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash& devices, + int index); + + QJsonObject decryptNotification(const QJsonObject ¬ification); + QStringList devicesForUser(const QString& userId) const; #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -695,16 +699,8 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); - PicklingMode picklingMode() const; - QJsonObject decryptNotification(const QJsonObject ¬ification); - - QStringList devicesForUser(const QString& userId) const; - QString curveKeyForUserDevice(const QString& userId, - const QString& device) const; - QString edKeyForUserDevice(const QString& userId, - const QString& device) const; - bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; #endif + Q_SIGNALS: /// \brief Initial server resolution has failed /// diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index ce1ca9ed..bcf1ad41 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -207,9 +207,9 @@ public: /// /// See the [key algorithms](/client-server-api/#key-algorithms) section for /// information on the Key Object format. - QHash> oneTimeKeys() const + QHash> oneTimeKeys() const { - return loadFromJson>>( + return loadFromJson>>( "one_time_keys"_ls); } }; diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 2b149aac..2a98d5d8 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -96,12 +96,13 @@ QOlmExpected QOlmSession::createOutboundSession( return std::make_unique(olmOutboundSession); } -QOlmExpected QOlmSession::pickle(const PicklingMode &mode) +QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); QByteArray key = toKey(mode); const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); + pickledBuf.data(), + pickledBuf.length()); if (error == olm_error()) { return lastError(m_session); diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index faae16ef..021092c7 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -31,7 +31,7 @@ public: const QString& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. static QOlmExpected unpickle( diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 2bda3086..3093db41 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,7 +12,9 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); - explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey, + const QString& senderId); QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } diff --git a/lib/room.cpp b/lib/room.cpp index 07d03467..6ec06aa8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -457,28 +457,6 @@ public: addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } - std::unique_ptr payloadForUserDevice(QString user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) - { - // Noisy but nice for debugging - //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); - QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = user; - payloadJson["sender"] = connection->user()->id(); - QJsonObject recipientObject; - recipientObject["ed25519"] = connection->edKeyForUserDevice(user, device); - payloadJson["recipient_keys"] = recipientObject; - QJsonObject senderObject; - senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); - payloadJson["keys"] = senderObject; - payloadJson["sender_device"] = connection->deviceId(); - auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); - QJsonObject encrypted; - encrypted[connection->curveKeyForUserDevice(user, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; - - return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); - } - QMultiHash getDevicesWithoutKey() const { QMultiHash devices; @@ -490,91 +468,7 @@ public: id, devices, currentOutboundMegolmSession->sessionId()); } - bool createOlmSession(const QString& user, const QString& device, - const QJsonObject& oneTimeKeyObject) const - { - static QOlmUtility verifier; - qDebug(E2EE) << "Creating a new session for" << user << device; - if (oneTimeKeyObject.isEmpty()) { - qWarning(E2EE) << "No one time key for" << user << device; - return false; - } - const auto oneTimeKeyForId = *oneTimeKeyObject.constBegin(); - const auto signature = - oneTimeKeyForId["signatures"][user]["ed25519:"_ls % device] - .toString() - .toLatin1(); - auto signedObject = oneTimeKeyForId.toObject(); - signedObject.remove("unsigned"_ls); - signedObject.remove("signatures"_ls); - const auto signedData = - QJsonDocument(signedObject).toJson(QJsonDocument::Compact); - if (!verifier.ed25519Verify( - connection->edKeyForUserDevice(user, device).toLatin1(), - signedData, signature)) { - qWarning(E2EE) << "Failed to verify one-time-key signature for" - << user << device << ". Skipping this device."; - return false; - } - const auto recipientCurveKey = - connection->curveKeyForUserDevice(user, device); - connection->createOlmSession(recipientCurveKey, - oneTimeKeyForId["key"].toString()); - return true; - } - - void sendRoomKeyToDevices(const QByteArray& sessionId, - const QByteArray& sessionKey, - const QMultiHash& devices, - int index) - { - qDebug(E2EE) << "Sending room key to devices:" << sessionId - << sessionKey.toHex(); - QHash> hash; - for (const auto& [userId, deviceId] : asKeyValueRange(devices)) - if (!connection->hasOlmSession(userId, deviceId)) { - hash[userId].insert(deviceId, "signed_curve25519"_ls); - qDebug(E2EE) - << "Adding" << userId << deviceId << "to keys to claim"; - } - - if (hash.isEmpty()) - return; - - auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, - [job, this, sessionId, sessionKey, devices, index] { - Connection::UsersToDevicesToEvents usersToDevicesToEvents; - const auto data = job->jsonData(); - for (const auto& [user, device] : asKeyValueRange(devices)) { - if (!connection->hasOlmSession(user, device) - && !createOlmSession( - user, device, - data["one_time_keys"][user][device].toObject())) - continue; - - usersToDevicesToEvents[user][device] = - payloadForUserDevice(user, device, sessionId, - sessionKey); - } - if (!usersToDevicesToEvents.empty()) { - connection->sendToDevices("m.room.encrypted"_ls, - usersToDevicesToEvents); - QVector> receivedDevices; - receivedDevices.reserve(devices.size()); - for (const auto& [user, device] : asKeyValueRange(devices)) - receivedDevices.push_back( - { user, device, - connection->curveKeyForUserDevice(user, device) }); - - connection->database()->setDevicesReceivedKey(id, - receivedDevices, - sessionId, index); - } - }); - } - - void sendMegolmSession(const QMultiHash& devices) { + void sendMegolmSession(const QMultiHash& devices) const { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); @@ -584,8 +478,9 @@ public: } // Send the session to other people - sendRoomKeyToDevices(sessionId, *sessionKey, devices, - currentOutboundMegolmSession->sessionMessageIndex()); + connection->sendSessionKeyToDevices( + id, sessionId, *sessionKey, devices, + currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3