1 | // SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> |
2 | // SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> |
3 | // SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> |
4 | // SPDX-License-Identifier: LGPL-2.1-or-later |
5 | |
6 | #pragma once |
7 | |
8 | #include <Quotient/converters.h> |
9 | |
10 | #include <QtCore/QMetaType> |
11 | #include <QtCore/QStringBuilder> |
12 | |
13 | #include <array> |
14 | |
15 | #ifdef Quotient_E2EE_ENABLED |
16 | # include <Quotient/expected.h> |
17 | |
18 | # include <olm/error.h> |
19 | # include <span> |
20 | # include <variant> |
21 | #endif |
22 | |
23 | namespace Quotient { |
24 | |
25 | constexpr inline auto AlgorithmKeyL = "algorithm"_ls ; |
26 | constexpr inline auto RotationPeriodMsKeyL = "rotation_period_ms"_ls ; |
27 | constexpr inline auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls ; |
28 | |
29 | constexpr inline auto AlgorithmKey = "algorithm"_ls ; |
30 | constexpr inline auto RotationPeriodMsKey = "rotation_period_ms"_ls ; |
31 | constexpr inline auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls ; |
32 | |
33 | constexpr inline auto Ed25519Key = "ed25519"_ls ; |
34 | constexpr inline auto Curve25519Key = "curve25519"_ls ; |
35 | constexpr inline auto SignedCurve25519Key = "signed_curve25519"_ls ; |
36 | |
37 | constexpr inline auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls ; |
38 | constexpr inline auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls ; |
39 | |
40 | constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey, |
41 | MegolmV1AesSha2AlgoKey }; |
42 | |
43 | inline bool isSupportedAlgorithm(const QString& algorithm) |
44 | { |
45 | return std::find(first: SupportedAlgorithms.cbegin(), last: SupportedAlgorithms.cend(), |
46 | val: algorithm) |
47 | != SupportedAlgorithms.cend(); |
48 | } |
49 | |
50 | #ifdef Quotient_E2EE_ENABLED |
51 | |
52 | #define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \ |
53 | qFatal("%s, internal error: %s", Message_, LastError_) |
54 | |
55 | #define QOLM_INTERNAL_ERROR(Message_) \ |
56 | QOLM_INTERNAL_ERROR_X(Message_, lastError()) |
57 | |
58 | #define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \ |
59 | do { \ |
60 | const QString errorMsg{ (Message_) }; \ |
61 | if (InternalCondition_) \ |
62 | QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \ |
63 | qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \ |
64 | } while (false) /* End of macro */ |
65 | |
66 | #define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \ |
67 | QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \ |
68 | lastError()) |
69 | |
70 | template <typename T> |
71 | using QOlmExpected = Expected<T, OlmErrorCode>; |
72 | |
73 | //! \brief Initialise a buffer object for use with Olm calls |
74 | //! |
75 | //! Qt and Olm use different size types; this causes the warning noise |
76 | QUOTIENT_API QByteArray byteArrayForOlm(size_t bufferSize); |
77 | |
78 | //! \brief Get a size of Qt container coerced to size_t |
79 | //! |
80 | //! It's a safe cast since size_t can easily accommodate the range between |
81 | //! 0 and INTMAX - 1 that Qt containers support; yet compilers complain... |
82 | inline size_t unsignedSize(const auto& qtBuffer) |
83 | { |
84 | return static_cast<size_t>(qtBuffer.size()); |
85 | } |
86 | |
87 | class QUOTIENT_API FixedBufferBase { |
88 | public: |
89 | enum InitOptions { Uninitialized, FillWithZeros, FillWithRandom }; |
90 | |
91 | static constexpr auto TotalSecureHeapSize = 65'536; |
92 | |
93 | auto size() const { return data_ == nullptr ? 0 : size_; } |
94 | auto empty() const { return data_ == nullptr || size_ == 0; } |
95 | |
96 | void clear(); |
97 | |
98 | QByteArray viewAsByteArray() const |
99 | { |
100 | // Ensure static_cast<int> is safe |
101 | static_assert(TotalSecureHeapSize < std::numeric_limits<int>::max()); |
102 | return QByteArray::fromRawData(reinterpret_cast<const char*>(data_), |
103 | static_cast<int>(size_)); |
104 | } |
105 | QByteArray toBase64(QByteArray::Base64Options options) const |
106 | { |
107 | return viewAsByteArray().toBase64(options); |
108 | } |
109 | |
110 | Q_DISABLE_COPY(FixedBufferBase) |
111 | FixedBufferBase& operator=(FixedBufferBase&&) = delete; |
112 | |
113 | protected: |
114 | FixedBufferBase(size_t bufferSize, InitOptions options); |
115 | ~FixedBufferBase() { clear(); } |
116 | |
117 | FixedBufferBase(FixedBufferBase&& other) |
118 | : data_(std::exchange(other.data_, nullptr)), size_(other.size_) |
119 | {} |
120 | |
121 | void fillFrom(QByteArray&& source); |
122 | |
123 | uint8_t* dataForWriting() { return data_; } |
124 | const uint8_t* data() const { return data_; } |
125 | |
126 | private: |
127 | uint8_t* data_ = nullptr; |
128 | size_t size_ = 0; |
129 | }; |
130 | |
131 | template <size_t ExtentN = std::dynamic_extent, bool DataIsWriteable = true> |
132 | class QUOTIENT_API FixedBuffer : public FixedBufferBase { |
133 | public: |
134 | static constexpr auto extent = ExtentN; // Matching std::span |
135 | static_assert(extent == std::dynamic_extent |
136 | || (extent < TotalSecureHeapSize / 2 && extent % 4 == 0)); |
137 | |
138 | explicit FixedBuffer(InitOptions fillMode) requires(extent |
139 | != std::dynamic_extent) |
140 | : FixedBufferBase(ExtentN, fillMode) |
141 | {} |
142 | explicit FixedBuffer(size_t bufferSize, InitOptions fillMode) |
143 | requires(extent == std::dynamic_extent) |
144 | : FixedBufferBase(bufferSize, fillMode) |
145 | {} |
146 | |
147 | using FixedBufferBase::data; |
148 | uint8_t* data() requires DataIsWriteable { return dataForWriting(); } |
149 | }; |
150 | |
151 | inline auto getRandom(size_t bytes) |
152 | { |
153 | return FixedBuffer<>{ bytes, FixedBufferBase::FillWithRandom }; |
154 | } |
155 | |
156 | template <size_t SizeN> |
157 | inline auto getRandom() |
158 | { |
159 | return FixedBuffer<SizeN>{ FixedBufferBase::FillWithRandom }; |
160 | } |
161 | |
162 | class PicklingKey : public FixedBuffer<128, /*DataIsWriteable=*/false> { |
163 | private: |
164 | // `using` would have exposed the constructor as it's public in the parent |
165 | explicit PicklingKey(InitOptions options) : FixedBuffer(options) |
166 | { |
167 | Q_ASSERT(options != FillWithZeros); |
168 | } |
169 | |
170 | public: |
171 | static PicklingKey generate() { return PicklingKey(FillWithRandom); } |
172 | static PicklingKey fromByteArray(QByteArray&& keySource) |
173 | { |
174 | PicklingKey k(Uninitialized); |
175 | k.fillFrom(std::move(keySource)); |
176 | return k; |
177 | } |
178 | static PicklingKey mock() { return PicklingKey(Uninitialized); } |
179 | }; |
180 | |
181 | #endif // Quotient_E2EE_ENABLED |
182 | |
183 | struct IdentityKeys |
184 | { |
185 | // Despite being Base64 payloads, these keys are stored in QStrings because |
186 | // in the vast majority of cases they are used to read from or write to |
187 | // QJsonObjects, and that effectively requires QStrings |
188 | QString curve25519; |
189 | QString ed25519; |
190 | }; |
191 | |
192 | //! Struct representing the one-time keys. |
193 | struct UnsignedOneTimeKeys |
194 | { |
195 | QHash<QString, QHash<QString, QString>> keys; |
196 | |
197 | //! Get the HashMap containing the curve25519 one-time keys. |
198 | QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; } |
199 | }; |
200 | |
201 | class SignedOneTimeKey { |
202 | public: |
203 | explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId, |
204 | const QString& deviceId, |
205 | const QByteArray& signature) |
206 | : payload { { "key"_ls , unsignedKey }, |
207 | { "signatures"_ls , |
208 | QJsonObject { |
209 | { userId, QJsonObject { { "ed25519:"_ls % deviceId, |
210 | QString::fromUtf8(ba: signature) } } } } } } |
211 | {} |
212 | explicit SignedOneTimeKey(const QJsonObject& jo = {}) |
213 | : payload(jo) |
214 | {} |
215 | |
216 | //! Unpadded Base64-encoded 32-byte Curve25519 public key |
217 | QByteArray key() const { return payload["key" _ls].toString().toLatin1(); } |
218 | |
219 | //! \brief Signatures of the key object |
220 | //! |
221 | //! The signature is calculated using the process described at |
222 | //! https://spec.matrix.org/v1.3/appendices/#signing-json |
223 | auto signatures() const |
224 | { |
225 | return fromJson<QHash<QString, QHash<QString, QString>>>( |
226 | json: payload["signatures" _ls]); |
227 | } |
228 | |
229 | QByteArray signature(QStringView userId, QStringView deviceId) const |
230 | { |
231 | return payload["signatures" _ls][userId]["ed25519:"_ls % deviceId] |
232 | .toString() |
233 | .toLatin1(); |
234 | } |
235 | |
236 | //! Whether the key is a fallback key |
237 | bool isFallback() const { return payload["fallback" _ls].toBool(); } |
238 | auto toJson() const { return payload; } |
239 | auto toJsonForVerification() const |
240 | { |
241 | auto json = payload; |
242 | json.remove(key: "signatures"_ls ); |
243 | json.remove(key: "unsigned"_ls ); |
244 | return QJsonDocument(json).toJson(format: QJsonDocument::Compact); |
245 | } |
246 | |
247 | private: |
248 | QJsonObject payload; |
249 | }; |
250 | |
251 | using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>; |
252 | |
253 | } // namespace Quotient |
254 | |
255 | Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) |
256 | |