1// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
2//
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#include "filesourceinfo.h"
6
7#include "../logging_categories_p.h"
8
9#include "../util.h"
10
11#include <QtCore/QReadWriteLock>
12
13#ifdef Quotient_E2EE_ENABLED
14# include <Quotient/e2ee/e2ee_common.h>
15
16# include <QtCore/QCryptographicHash>
17
18# include <openssl/evp.h>
19#endif
20
21using namespace Quotient;
22
23#ifdef Quotient_E2EE_ENABLED
24QByteArray Quotient::decryptFile(const QByteArray& ciphertext,
25 const EncryptedFileMetadata& metadata)
26{
27 if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1())
28 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
29 qCWarning(E2EE) << "Hash verification failed for file";
30 return {};
31 }
32
33 auto _key = metadata.key.k;
34 const auto keyBytes = QByteArray::fromBase64(
35 _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1());
36 int length = -1;
37 auto* ctx = EVP_CIPHER_CTX_new();
38 QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0');
39 EVP_DecryptInit_ex(
40 ctx, EVP_aes_256_ctr(), nullptr,
41 reinterpret_cast<const unsigned char*>(keyBytes.data()),
42 reinterpret_cast<const unsigned char*>(
43 QByteArray::fromBase64(metadata.iv.toLatin1()).data()));
44 EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()),
45 &length,
46 reinterpret_cast<const unsigned char*>(ciphertext.data()),
47 static_cast<int>(ciphertext.size()));
48 EVP_DecryptFinal_ex(ctx,
49 reinterpret_cast<unsigned char*>(plaintext.data())
50 + length,
51 &length);
52 EVP_CIPHER_CTX_free(ctx);
53 return plaintext.left(ciphertext.size());
54}
55
56std::pair<EncryptedFileMetadata, QByteArray> Quotient::encryptFile(
57 const QByteArray& plainText)
58{
59 auto k = getRandom<32>();
60 auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding
61 | QByteArray::OmitTrailingEquals);
62 auto iv = getRandom<16>();
63 const JWK key = {
64 "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, QString::fromLatin1(kBase64), true
65 };
66
67 int length = -1;
68 auto* ctx = EVP_CIPHER_CTX_new();
69 EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, k.data(), iv.data());
70 const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
71 QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
72 EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()),
73 &length,
74 reinterpret_cast<const unsigned char*>(plainText.data()),
75 static_cast<int>(plainText.size()));
76 EVP_EncryptFinal_ex(ctx,
77 reinterpret_cast<unsigned char*>(cipherText.data())
78 + length,
79 &length);
80 EVP_CIPHER_CTX_free(ctx);
81
82 auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256)
83 .toBase64(QByteArray::OmitTrailingEquals);
84 auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals);
85 const EncryptedFileMetadata efm = {
86 {}, key, QString::fromLatin1(ivBase64), { { QStringLiteral("sha256"), QString::fromLatin1(hash) } }, "v2"_ls
87 };
88 return { efm, cipherText };
89}
90#endif
91
92void JsonObjectConverter<EncryptedFileMetadata>::dumpTo(
93 QJsonObject& jo, const EncryptedFileMetadata& pod)
94{
95 addParam<>(container&: jo, QStringLiteral("url"), value: pod.url);
96 addParam<>(container&: jo, QStringLiteral("key"), value: pod.key);
97 addParam<>(container&: jo, QStringLiteral("iv"), value: pod.iv);
98 addParam<>(container&: jo, QStringLiteral("hashes"), value: pod.hashes);
99 addParam<>(container&: jo, QStringLiteral("v"), value: pod.v);
100}
101
102void JsonObjectConverter<EncryptedFileMetadata>::fillFrom(
103 const QJsonObject& jo, EncryptedFileMetadata& pod)
104{
105 fromJson(json: jo.value(key: "url"_ls), pod&: pod.url);
106 fromJson(json: jo.value(key: "key"_ls), pod&: pod.key);
107 fromJson(json: jo.value(key: "iv"_ls), pod&: pod.iv);
108 fromJson(json: jo.value(key: "hashes"_ls), pod&: pod.hashes);
109 fromJson(json: jo.value(key: "v"_ls), pod&: pod.v);
110}
111
112void JsonObjectConverter<JWK>::dumpTo(QJsonObject& jo, const JWK& pod)
113{
114 addParam<>(container&: jo, QStringLiteral("kty"), value: pod.kty);
115 addParam<>(container&: jo, QStringLiteral("key_ops"), value: pod.keyOps);
116 addParam<>(container&: jo, QStringLiteral("alg"), value: pod.alg);
117 addParam<>(container&: jo, QStringLiteral("k"), value: pod.k);
118 addParam<>(container&: jo, QStringLiteral("ext"), value: pod.ext);
119}
120
121void JsonObjectConverter<JWK>::fillFrom(const QJsonObject& jo, JWK& pod)
122{
123 fromJson(json: jo.value(key: "kty"_ls), pod&: pod.kty);
124 fromJson(json: jo.value(key: "key_ops"_ls), pod&: pod.keyOps);
125 fromJson(json: jo.value(key: "alg"_ls), pod&: pod.alg);
126 fromJson(json: jo.value(key: "k"_ls), pod&: pod.k);
127 fromJson(json: jo.value(key: "ext"_ls), pod&: pod.ext);
128}
129
130QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi)
131{
132 return std::visit(visitor: Overloads { [](const QUrl& url) { return url; },
133 [](const EncryptedFileMetadata& efm) {
134 return efm.url;
135 } },
136 variants: fsi);
137}
138
139void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl)
140{
141 std::visit(visitor: Overloads { [&newUrl](QUrl& url) { url = newUrl; },
142 [&newUrl](EncryptedFileMetadata& efm) {
143 efm.url = newUrl;
144 } },
145 variants&: fsi);
146}
147
148void Quotient::fillJson(QJsonObject& jo,
149 const std::array<QLatin1String, 2>& jsonKeys,
150 const FileSourceInfo& fsi)
151{
152 // NB: Keeping variant_size_v out of the function signature for readability.
153 // NB2: Can't use jsonKeys directly inside static_assert as its value is
154 // unknown so the compiler cannot ensure size() is constexpr (go figure...)
155 static_assert(
156 std::variant_size_v<FileSourceInfo> == decltype(jsonKeys) {}.size());
157 jo.insert(key: jsonKeys[fsi.index()], value: toJson(v: fsi));
158}
159
160namespace {
161 // A map from roomId/eventId pair to file source info
162 QHash<std::pair<QString, QString>, EncryptedFileMetadata> infos;
163 QReadWriteLock lock;
164}
165
166void FileMetadataMap::add(const QString& roomId, const QString& eventId,
167 const EncryptedFileMetadata& fileMetadata)
168{
169 const QWriteLocker l(&lock);
170 infos.insert(key: { roomId, eventId }, value: fileMetadata);
171}
172
173void FileMetadataMap::remove(const QString& roomId, const QString& eventId)
174{
175 const QWriteLocker l(&lock);
176 infos.remove(key: { roomId, eventId });
177}
178
179EncryptedFileMetadata FileMetadataMap::lookup(const QString& roomId,
180 const QString& eventId)
181{
182 const QReadLocker l(&lock);
183 return infos.value(key: { roomId, eventId });
184}
185