libQuotient
A Qt library for building matrix clients
e2ee_common.h
Go to the documentation of this file.
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 #include <Quotient/expected.h>
10 
11 #include <QtCore/QMetaType>
12 #include <QtCore/QStringBuilder>
13 
14 #include <array>
15 #include <span>
16 #include <variant>
17 
18 #include <olm/error.h>
19 
20 namespace Quotient {
21 
22 constexpr inline auto AlgorithmKeyL = "algorithm"_L1;
23 constexpr inline auto RotationPeriodMsKeyL = "rotation_period_ms"_L1;
24 constexpr inline auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_L1;
25 
26 constexpr inline auto AlgorithmKey = "algorithm"_L1;
27 constexpr inline auto RotationPeriodMsKey = "rotation_period_ms"_L1;
28 constexpr inline auto RotationPeriodMsgsKey = "rotation_period_msgs"_L1;
29 
30 constexpr inline auto Ed25519Key = "ed25519"_L1;
31 constexpr inline auto Curve25519Key = "curve25519"_L1;
32 constexpr inline auto SignedCurve25519Key = "signed_curve25519"_L1;
33 
34 constexpr inline auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_L1;
35 constexpr inline auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_L1;
36 
37 constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey,
38  MegolmV1AesSha2AlgoKey };
39 
40 inline bool isSupportedAlgorithm(const QString& algorithm)
41 {
42  return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(),
43  algorithm)
44  != SupportedAlgorithms.cend();
45 }
46 
47 #define QOLM_INTERNAL_ERROR_X(Message_, LastError_)
48  qFatal("%s, internal error: %s", QUO_CSTR(Message_), LastError_)
49 
50 #define QOLM_INTERNAL_ERROR(Message_)
51  QOLM_INTERNAL_ERROR_X((Message_), lastError())
52 
53 #define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_)
54  do {
55  if (InternalCondition_)
56  QOLM_INTERNAL_ERROR_X((Message_), (LastErrorText_));
57  qWarning(E2EE).nospace() << (Message_) << ": " << (LastErrorText_);
58  } while (false) /* End of macro */
59 
60 #define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_)
61  QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), lastError())
62 
63 template <typename T>
64 using QOlmExpected = Expected<T, OlmErrorCode>;
65 
66 //! \brief Initialise a buffer object for use with Olm calls
67 //!
68 //! Qt and Olm use different size types; this causes the warning noise
69 QUOTIENT_API QByteArray byteArrayForOlm(size_t bufferSize);
70 
71 //! \brief Get a size of a container coerced to size_t
72 //!
73 //! This is mainly aimed at Qt containers because they have signed size; but it can also be called
74 //! on other containers or even C arrays, e.g. - to spare generic code from special-casing.
75 //! For Qt containers, it's a safe cast since size_t can always accommodate the range between 0 and
76 //! SIZE_MAX / 2 - 1 that they support; yet compilers complain...
77 inline size_t unsignedSize(const auto& buffer)
78  requires (sizeof(std::size(buffer)) <= sizeof(size_t))
79 {
80  return static_cast<size_t>(std::size(buffer));
81 }
82 
83 // Can't use std::byte normally recommended for the purpose because both Olm
84 // and OpenSSL get uint8_t* pointers, and std::byte* is not implicitly
85 // convertible to uint8_t* (and adding explicit casts in each case kinda defeats
86 // the purpose of all the span machinery below meant to replace reinterpret_ or
87 // any other casts).
88 
89 using byte_t = uint8_t;
90 
91 template <size_t N = std::dynamic_extent>
92 using byte_view_t = std::span<const byte_t, N>;
93 
94 template <size_t N = std::dynamic_extent>
95 using byte_span_t = std::span<byte_t, N>;
96 
97 namespace _impl {
98  QUOTIENT_API void checkForSpanShortfall(QByteArray::size_type inputSize, int neededSize);
99 
100  template <typename SpanT>
101  inline auto spanFromBytes(auto& byteArray)
102  {
103  // OpenSSL only handles int sizes; Release builds will cut the tail off
104  Q_ASSERT_X(std::in_range<int>(std::size(byteArray)), __func__, "Too long array for OpenSSL");
105  if constexpr (SpanT::extent != std::dynamic_extent) {
106  static_assert(std::in_range<int>(SpanT::extent));
107  checkForSpanShortfall(std::size(byteArray), static_cast<int>(SpanT::extent));
108  }
109  return SpanT(std::bit_cast<typename SpanT::pointer>(std::data(byteArray)),
110  std::min(SpanT::extent, unsignedSize(byteArray)));
111  }
112 } // namespace _impl
113 
114 //! \brief Obtain a std::span<const byte_t, N> looking into the passed buffer
115 //!
116 //! This function returns an adaptor object that is suitable for OpenSSL/Olm
117 //! invocations (via std::span<>::data() accessor) so that you don't have
118 //! to wrap your containers into reinterpret/bit_casts on every OpenSSL call.
119 //! \note The caller is responsible for making sure that bytes.size() is small
120 //! enough to fit into an int (OpenSSL only handles int sizes atm) but
121 //! also large enough to have at least N bytes if N is not `std::dynamic_extent`
122 //! \sa asWritableCBytes for the case when you need to pass a buffer for writing
123 template <size_t N = std::dynamic_extent>
124 inline auto asCBytes(const auto& buf)
125 {
126  return _impl::spanFromBytes<byte_view_t<N>>(buf);
127 }
128 
129 //! Obtain a std::span<byte_t, N> looking into the passed buffer
130 template <size_t N = std::dynamic_extent>
131 inline auto asWritableCBytes(auto& buf)
132 {
133  return _impl::spanFromBytes<byte_span_t<N>>(buf);
134 }
135 
136 inline auto viewAsByteArray(const auto& aRange) -> auto
137  requires (sizeof(*aRange.data()) == sizeof(char))
138 { // -> auto to activate SFINAE, it's always QByteArray when well-formed
139  return QByteArray::fromRawData(std::bit_cast<const char*>(std::data(aRange)),
140  static_cast<int>(std::size(aRange)));
141 }
142 
143 //! Non-template base for owning byte span classes
144 class QUOTIENT_API FixedBufferBase {
145 public:
146  enum InitOptions { Uninitialized, FillWithZeros, FillWithRandom };
147 
148  using value_type = byte_t;
149  using size_type = size_t;
150 
151  static constexpr auto TotalSecureHeapSize = 65'536;
152 
153  auto size() const { return data_ == nullptr ? 0 : size_; }
154  auto empty() const { return data_ == nullptr || size_ == 0; }
155 
156  void clear();
157 
158  //! \brief Access the bytes of the fixed buffer via QByteArray interface
159  //!
160  //! This uses QByteArray::fromRawData() to create a QByteArray object that
161  //! refers to the original fixed buffer, without copying.
162  //! \warning the lifetime of the returned QByteArray should not exceed the
163  //! lifetime of the underlying buffer; in particular, you should
164  //! never try using the result of viewAsByteArray() as a return
165  //! value of your function
166  //! \sa copyToByteArray
167  QByteArray viewAsByteArray() const
168  {
169  static_assert(std::in_range<QByteArray::size_type>(TotalSecureHeapSize));
170  return QByteArray::fromRawData(std::bit_cast<const char*>(data_),
171  static_cast<QByteArray::size_type>(size_));
172  }
173 
174  //! \brief Copy the contents of the buffer to a QByteArray
175  //!
176  //! Unlike viewAsByteArray(), this function actually copies the buffer to
177  //! non-secure memory.
178  QByteArray copyToByteArray(QByteArray::size_type untilPos = -1) const
179  {
180  if (untilPos < 0 || static_cast<size_type>(untilPos) > size_)
181  untilPos = static_cast<QByteArray::size_type>(size_);
182  return { std::bit_cast<const char*>(data_), untilPos };
183  }
184 
185  QByteArray toBase64() const { return viewAsByteArray().toBase64(); }
186  QByteArray toBase64(QByteArray::Base64Options options) const
187  {
188  return viewAsByteArray().toBase64(options);
189  }
190 
191  Q_DISABLE_COPY(FixedBufferBase)
192  FixedBufferBase& operator=(FixedBufferBase&&) = delete;
193 
194 protected:
195  FixedBufferBase(size_type bufferSize, InitOptions options);
196  ~FixedBufferBase() { clear(); }
197 
198  FixedBufferBase(FixedBufferBase&& other)
199  : data_(std::exchange(other.data_, nullptr)), size_(other.size_)
200  {}
201 
202  void fillFrom(QByteArray&& source);
203 
204  value_type* dataForWriting() { return data_; }
205  const value_type* data() const { return data_; }
206 
207 private:
208  value_type* data_ = nullptr;
209  size_type size_ = 0;
210 };
211 
212 template <size_t ExtentN = std::dynamic_extent, bool DataIsWriteable = true>
213 class QUOTIENT_API FixedBuffer : public FixedBufferBase {
214 public:
215  static constexpr auto extent = ExtentN; // Matching std::span
216  static_assert(extent == std::dynamic_extent
217  || (extent < TotalSecureHeapSize / 2 && extent % 4 == 0));
218 
219  explicit FixedBuffer(InitOptions fillMode = FillWithZeros)
220  requires(extent != std::dynamic_extent)
221  : FixedBufferBase(ExtentN, fillMode)
222  {}
223  explicit FixedBuffer(size_type bufferSize)
224  requires(extent == std::dynamic_extent)
225  : FixedBuffer(bufferSize, FillWithZeros)
226  {}
227  explicit FixedBuffer(size_type bufferSize, InitOptions fillMode)
228  requires(extent == std::dynamic_extent)
229  : FixedBufferBase(bufferSize, fillMode)
230  {}
231 
232  using FixedBufferBase::data;
233  value_type* data() requires DataIsWriteable { return dataForWriting(); }
234 
235  Q_IMPLICIT operator byte_view_t<ExtentN>() const
236  {
237  return byte_view_t<ExtentN>(data(), size());
238  }
239 
240  Q_IMPLICIT operator byte_span_t<ExtentN>()
241  requires DataIsWriteable
242  {
243  return byte_span_t<ExtentN>(dataForWriting(), size());
244  }
245 };
246 
247 inline auto getRandom(size_t bytes)
248 {
249  return FixedBuffer<>{ bytes, FixedBufferBase::FillWithRandom };
250 }
251 
252 template <size_t SizeN>
253 inline auto getRandom()
254 {
255  return FixedBuffer<SizeN>{ FixedBufferBase::FillWithRandom };
256 }
257 
258 //! \brief Fill the buffer with the securely generated random bytes
259 //!
260 //! You should use this throughout Quotient where pseudo-random generators
261 //! are not enough (i.e. in crypto cases). Don't use it when proper randomness
262 //! is not critical; it tries to rely on system entropy that is in (somewhat)
263 //! limited supply.
264 //! There's no fancy stuff internally, it's just a way to unify secure RNG usage
265 //! in Quotient. See the function definition for details if you want/need.
266 QUOTIENT_API void fillFromSecureRng(std::span<byte_t> bytes);
267 
268 class PicklingKey : public FixedBuffer<128, /*DataIsWriteable=*/false> {
269 private:
270  // `using` would have exposed the constructor as it's public in the parent
271  explicit PicklingKey(InitOptions options) : FixedBuffer(options)
272  {
273  Q_ASSERT(options != FillWithZeros);
274  }
275 
276 public:
277  static PicklingKey generate() { return PicklingKey(FillWithRandom); }
278  static PicklingKey fromByteArray(QByteArray&& keySource)
279  {
280  PicklingKey k(Uninitialized);
281  k.fillFrom(std::move(keySource));
282  return k;
283  }
284  static PicklingKey mock() { return PicklingKey(Uninitialized); }
285 };
286 
287 struct IdentityKeys
288 {
289  // Despite being Base64 payloads, these keys are stored in QStrings because
290  // in the vast majority of cases they are used to read from or write to
291  // QJsonObjects, and that effectively requires QStrings
292  QString curve25519;
293  QString ed25519;
294 };
295 
296 //! Struct representing the one-time keys.
297 struct UnsignedOneTimeKeys
298 {
299  QHash<QString, QHash<QString, QString>> keys;
300 
301  //! Get the HashMap containing the curve25519 one-time keys.
302  QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; }
303 };
304 
305 class QUOTIENT_API SignedOneTimeKey {
306 public:
307  explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId,
308  const QString& deviceId,
309  const QByteArray& signature)
310  : payload{
311  { "key"_L1, unsignedKey },
312  { "signatures"_L1,
313  QJsonObject{
314  { userId, QJsonObject{ { "ed25519:"_L1 % deviceId,
315  QString::fromUtf8(signature) } } } } }
316  }
317  {}
318  explicit SignedOneTimeKey(const QJsonObject& jo = {})
319  : payload(jo)
320  {}
321 
322  //! Unpadded Base64-encoded 32-byte Curve25519 public key
323  QByteArray key() const { return payload["key"_L1].toString().toLatin1(); }
324 
325  //! \brief Signatures of the key object
326  //!
327  //! The signature is calculated using the process described at
328  //! https://spec.matrix.org/v1.3/appendices/#signing-json
329  auto signatures() const
330  {
331  return fromJson<QHash<QString, QHash<QString, QString>>>(
332  payload["signatures"_L1]);
333  }
334 
335  QByteArray signature(QStringView userId, QStringView deviceId) const
336  {
337  return payload["signatures"_L1][userId]["ed25519:"_L1 % deviceId]
338  .toString()
339  .toLatin1();
340  }
341 
342  //! Whether the key is a fallback key
343  bool isFallback() const { return payload["fallback"_L1].toBool(); }
344  auto toJson() const { return payload; }
345  auto toJsonForVerification() const
346  {
347  auto json = payload;
348  json.remove("signatures"_L1);
349  json.remove("unsigned"_L1);
350  return QJsonDocument(json).toJson(QJsonDocument::Compact);
351  }
352 
353 private:
354  QJsonObject payload;
355 };
356 
357 using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>;
358 
359 } // namespace Quotient
360 
361 Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey)