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 | |
21 | using namespace Quotient; |
22 | |
23 | #ifdef Quotient_E2EE_ENABLED |
24 | QByteArray 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 | |
56 | std::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 | |
92 | void 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 | |
102 | void 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 | |
112 | void 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 | |
121 | void 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 | |
130 | QUrl 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 | |
139 | void 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 | |
148 | void 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 | |
160 | namespace { |
161 | // A map from roomId/eventId pair to file source info |
162 | QHash<std::pair<QString, QString>, EncryptedFileMetadata> infos; |
163 | QReadWriteLock lock; |
164 | } |
165 | |
166 | void 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 | |
173 | void FileMetadataMap::remove(const QString& roomId, const QString& eventId) |
174 | { |
175 | const QWriteLocker l(&lock); |
176 | infos.remove(key: { roomId, eventId }); |
177 | } |
178 | |
179 | EncryptedFileMetadata FileMetadataMap::lookup(const QString& roomId, |
180 | const QString& eventId) |
181 | { |
182 | const QReadLocker l(&lock); |
183 | return infos.value(key: { roomId, eventId }); |
184 | } |
185 | |