aboutsummaryrefslogtreecommitdiff
path: root/lib/e2ee
diff options
context:
space:
mode:
Diffstat (limited to 'lib/e2ee')
-rw-r--r--lib/e2ee/e2ee.h139
-rw-r--r--lib/e2ee/qolmaccount.cpp275
-rw-r--r--lib/e2ee/qolmaccount.h123
-rw-r--r--lib/e2ee/qolminboundsession.cpp192
-rw-r--r--lib/e2ee/qolminboundsession.h60
-rw-r--r--lib/e2ee/qolmmessage.cpp31
-rw-r--r--lib/e2ee/qolmmessage.h42
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp152
-rw-r--r--lib/e2ee/qolmoutboundsession.h63
-rw-r--r--lib/e2ee/qolmsession.cpp231
-rw-r--r--lib/e2ee/qolmsession.h84
-rw-r--r--lib/e2ee/qolmutility.cpp53
-rw-r--r--lib/e2ee/qolmutility.h41
-rw-r--r--lib/e2ee/qolmutils.cpp22
-rw-r--r--lib/e2ee/qolmutils.h55
15 files changed, 1563 insertions, 0 deletions
diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h
new file mode 100644
index 00000000..5999c0be
--- /dev/null
+++ b/lib/e2ee/e2ee.h
@@ -0,0 +1,139 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+#include <QtCore/QMetaType>
+#include <QtCore/QStringBuilder>
+
+#include <array>
+
+#ifdef Quotient_E2EE_ENABLED
+# include "expected.h"
+
+# include <olm/error.h>
+# include <variant>
+#endif
+
+namespace Quotient {
+
+constexpr auto AlgorithmKeyL = "algorithm"_ls;
+constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
+constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
+
+constexpr auto AlgorithmKey = "algorithm"_ls;
+constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls;
+constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls;
+
+constexpr auto Ed25519Key = "ed25519"_ls;
+constexpr auto Curve25519Key = "curve25519"_ls;
+constexpr auto SignedCurve25519Key = "signed_curve25519"_ls;
+
+constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls;
+constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls;
+
+constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey,
+ MegolmV1AesSha2AlgoKey };
+
+inline bool isSupportedAlgorithm(const QString& algorithm)
+{
+ return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(),
+ algorithm)
+ != SupportedAlgorithms.cend();
+}
+
+#ifdef Quotient_E2EE_ENABLED
+struct Unencrypted {};
+struct Encrypted {
+ QByteArray key;
+};
+
+using PicklingMode = std::variant<Unencrypted, Encrypted>;
+
+class QOlmSession;
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+class QOlmInboundGroupSession;
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+
+class QOlmOutboundGroupSession;
+using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
+
+template <typename T>
+using QOlmExpected = Expected<T, OlmErrorCode>;
+#endif
+
+struct IdentityKeys
+{
+ QByteArray curve25519;
+ QByteArray ed25519;
+};
+
+//! Struct representing the one-time keys.
+struct UnsignedOneTimeKeys
+{
+ QHash<QString, QHash<QString, QString>> keys;
+
+ //! Get the HashMap containing the curve25519 one-time keys.
+ QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; }
+};
+
+class SignedOneTimeKey {
+public:
+ explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId,
+ const QString& deviceId,
+ const QByteArray& signature)
+ : payload { { "key"_ls, unsignedKey },
+ { "signatures"_ls,
+ QJsonObject {
+ { userId, QJsonObject { { "ed25519:"_ls % deviceId,
+ QString(signature) } } } } } }
+ {}
+ explicit SignedOneTimeKey(const QJsonObject& jo = {})
+ : payload(jo)
+ {}
+
+ //! Unpadded Base64-encoded 32-byte Curve25519 public key
+ QByteArray key() const { return payload["key"_ls].toString().toLatin1(); }
+
+ //! \brief Signatures of the key object
+ //!
+ //! The signature is calculated using the process described at
+ //! https://spec.matrix.org/v1.3/appendices/#signing-json
+ auto signatures() const
+ {
+ return fromJson<QHash<QString, QHash<QString, QString>>>(
+ payload["signatures"_ls]);
+ }
+
+ QByteArray signature(QStringView userId, QStringView deviceId) const
+ {
+ return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId]
+ .toString()
+ .toLatin1();
+ }
+
+ //! Whether the key is a fallback key
+ bool isFallback() const { return payload["fallback"_ls].toBool(); }
+ auto toJson() const { return payload; }
+ auto toJsonForVerification() const
+ {
+ auto json = payload;
+ json.remove("signatures"_ls);
+ json.remove("unsigned"_ls);
+ return QJsonDocument(json).toJson(QJsonDocument::Compact);
+ }
+
+private:
+ QJsonObject payload;
+};
+
+using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>;
+
+} // namespace Quotient
+
+Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey)
diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp
new file mode 100644
index 00000000..345ab16b
--- /dev/null
+++ b/lib/e2ee/qolmaccount.cpp
@@ -0,0 +1,275 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmaccount.h"
+
+#include "connection.h"
+#include "e2ee/qolmsession.h"
+#include "e2ee/qolmutility.h"
+#include "e2ee/qolmutils.h"
+
+#include "csapi/keys.h"
+
+#include <QtCore/QRandomGenerator>
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+// Convert olm error to enum
+OlmErrorCode QOlmAccount::lastErrorCode() const {
+ return olm_account_last_error_code(m_account);
+}
+
+const char* QOlmAccount::lastError() const
+{
+ return olm_account_last_error(m_account);
+}
+
+QOlmAccount::QOlmAccount(QStringView userId, QStringView deviceId,
+ QObject* parent)
+ : QObject(parent)
+ , m_userId(userId.toString())
+ , m_deviceId(deviceId.toString())
+{}
+
+QOlmAccount::~QOlmAccount()
+{
+ olm_clear_account(m_account);
+ delete[](reinterpret_cast<uint8_t *>(m_account));
+}
+
+void QOlmAccount::createNewAccount()
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ if (const auto randomLength = olm_create_account_random_length(m_account);
+ olm_create_account(m_account, RandomBuffer(randomLength), randomLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to create a new account");
+
+ emit needsSave();
+}
+
+OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled,
+ const PicklingMode& mode)
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ if (const auto key = toKey(mode);
+ olm_unpickle_account(m_account, key.data(), key.length(),
+ pickled.data(), pickled.size())
+ == olm_error()) {
+ // Probably log the user out since we have no way of getting to the keys
+ return lastErrorCode();
+ }
+ return OLM_SUCCESS;
+}
+
+QByteArray QOlmAccount::pickle(const PicklingMode &mode)
+{
+ const QByteArray key = toKey(mode);
+ const size_t pickleLength = olm_pickle_account_length(m_account);
+ QByteArray pickleBuffer(pickleLength, '\0');
+ if (olm_pickle_account(m_account, key.data(), key.length(),
+ pickleBuffer.data(), pickleLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable("Failed to pickle Olm account "
+ + accountId()));
+
+ return pickleBuffer;
+}
+
+IdentityKeys QOlmAccount::identityKeys() const
+{
+ const size_t keyLength = olm_account_identity_keys_length(m_account);
+ QByteArray keyBuffer(keyLength, '\0');
+ if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength)
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR(
+ qPrintable("Failed to get " % accountId() % " identity keys"));
+ }
+ const auto key = QJsonDocument::fromJson(keyBuffer).object();
+ return IdentityKeys {
+ key.value(QStringLiteral("curve25519")).toString().toUtf8(),
+ key.value(QStringLiteral("ed25519")).toString().toUtf8()
+ };
+}
+
+QByteArray QOlmAccount::sign(const QByteArray &message) const
+{
+ QByteArray signatureBuffer(olm_account_signature_length(m_account), '\0');
+
+ if (olm_account_sign(m_account, message.data(), message.length(),
+ signatureBuffer.data(), signatureBuffer.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to sign a message");
+
+ return signatureBuffer;
+}
+
+QByteArray QOlmAccount::sign(const QJsonObject &message) const
+{
+ return sign(QJsonDocument(message).toJson(QJsonDocument::Compact));
+}
+
+QByteArray QOlmAccount::signIdentityKeys() const
+{
+ const auto keys = identityKeys();
+ return sign(QJsonObject{
+ { "algorithms", QJsonArray{ "m.olm.v1.curve25519-aes-sha2",
+ "m.megolm.v1.aes-sha2" } },
+ { "user_id", m_userId },
+ { "device_id", m_deviceId },
+ { "keys", QJsonObject{ { QStringLiteral("curve25519:") + m_deviceId,
+ QString::fromUtf8(keys.curve25519) },
+ { QStringLiteral("ed25519:") + m_deviceId,
+ QString::fromUtf8(keys.ed25519) } } } });
+}
+
+size_t QOlmAccount::maxNumberOfOneTimeKeys() const
+{
+ return olm_account_max_number_of_one_time_keys(m_account);
+}
+
+size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys)
+{
+ const auto randomLength =
+ olm_account_generate_one_time_keys_random_length(m_account,
+ numberOfKeys);
+ const auto result = olm_account_generate_one_time_keys(
+ m_account, numberOfKeys, RandomBuffer(randomLength), randomLength);
+
+ if (result == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable(
+ "Failed to generate one-time keys for account " + accountId()));
+
+ emit needsSave();
+ return result;
+}
+
+UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const
+{
+ const auto oneTimeKeyLength = olm_account_one_time_keys_length(m_account);
+ QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '\0');
+
+ if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(),
+ oneTimeKeyLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable(
+ "Failed to obtain one-time keys for account" % accountId()));
+
+ const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object();
+ UnsignedOneTimeKeys oneTimeKeys;
+ fromJson(json, oneTimeKeys.keys);
+ return oneTimeKeys;
+}
+
+OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const
+{
+ OneTimeKeys signedOneTimeKeys;
+ for (const auto& curveKeys = keys.curve25519();
+ const auto& [keyId, key] : asKeyValueRange(curveKeys))
+ signedOneTimeKeys.insert("signed_curve25519:" % keyId,
+ SignedOneTimeKey {
+ key, m_userId, m_deviceId,
+ sign(QJsonObject { { "key", key } }) });
+ return signedOneTimeKeys;
+}
+
+OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session)
+{
+ if (olm_remove_one_time_keys(m_account, session.raw()) == olm_error()) {
+ qWarning(E2EE).nospace()
+ << "Failed to remove one-time keys for session "
+ << session.sessionId() << ": " << lastError();
+ return lastErrorCode();
+ }
+ emit needsSave();
+ return OLM_SUCCESS;
+}
+
+OlmAccount* QOlmAccount::data() { return m_account; }
+
+DeviceKeys QOlmAccount::deviceKeys() const
+{
+ static QStringList Algorithms(SupportedAlgorithms.cbegin(),
+ SupportedAlgorithms.cend());
+
+ const auto idKeys = identityKeys();
+ return DeviceKeys{
+ .userId = m_userId,
+ .deviceId = m_deviceId,
+ .algorithms = Algorithms,
+ .keys{ { "curve25519:" + m_deviceId, idKeys.curve25519 },
+ { "ed25519:" + m_deviceId, idKeys.ed25519 } },
+ .signatures{
+ { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } }
+ };
+}
+
+UploadKeysJob* QOlmAccount::createUploadKeyRequest(
+ const UnsignedOneTimeKeys& oneTimeKeys) const
+{
+ return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys));
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession(
+ const QOlmMessage& preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSession(this, preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSessionFrom(this, theirIdentityKey,
+ preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey)
+{
+ return QOlmSession::createOutboundSession(this, theirIdentityKey,
+ theirOneTimeKey);
+}
+
+void QOlmAccount::markKeysAsPublished()
+{
+ olm_account_mark_keys_as_published(m_account);
+ emit needsSave();
+}
+
+bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys,
+ const QString& deviceId,
+ const QString& userId)
+{
+ const auto signKeyId = "ed25519:" + deviceId;
+ const auto signingKey = deviceKeys.keys[signKeyId];
+ const auto signature = deviceKeys.signatures[userId][signKeyId];
+
+ return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature);
+}
+
+bool Quotient::ed25519VerifySignature(const QString& signingKey,
+ const QJsonObject& obj,
+ const QString& signature)
+{
+ if (signature.isEmpty())
+ return false;
+
+ QJsonObject obj1 = obj;
+
+ obj1.remove("unsigned");
+ obj1.remove("signatures");
+
+ auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact);
+
+ QByteArray signingKeyBuf = signingKey.toUtf8();
+ QOlmUtility utility;
+ auto signatureBuf = signature.toUtf8();
+ return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf);
+}
+
+QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; }
diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h
new file mode 100644
index 00000000..a5faa82a
--- /dev/null
+++ b/lib/e2ee/qolmaccount.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+
+#include "csapi/keys.h"
+
+#include <QtCore/QObject>
+
+struct OlmAccount;
+
+namespace Quotient {
+
+//! An olm account manages all cryptographic keys used on a device.
+//! \code{.cpp}
+//! const auto olmAccount = new QOlmAccount(this);
+//! \endcode
+class QUOTIENT_API QOlmAccount : public QObject
+{
+ Q_OBJECT
+public:
+ QOlmAccount(QStringView userId, QStringView deviceId,
+ QObject* parent = nullptr);
+ ~QOlmAccount() override;
+
+ //! Creates a new instance of OlmAccount. During the instantiation
+ //! the Ed25519 fingerprint key pair and the Curve25519 identity key
+ //! pair are generated. For more information see <a
+ //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>.
+ //! This needs to be called before any other action or use unpickle() instead.
+ void createNewAccount();
+
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`.
+ //! This needs to be called before any other action or use createNewAccount() instead.
+ [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled,
+ const PicklingMode& mode);
+
+ //! Serialises an OlmAccount to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode);
+
+ //! Returns the account's public identity keys already formatted as JSON
+ IdentityKeys identityKeys() const;
+
+ //! Returns the signature of the supplied message.
+ QByteArray sign(const QByteArray &message) const;
+ QByteArray sign(const QJsonObject& message) const;
+
+ //! Sign identity keys.
+ QByteArray signIdentityKeys() const;
+
+ //! Maximum number of one time keys that this OlmAccount can
+ //! currently hold.
+ size_t maxNumberOfOneTimeKeys() const;
+
+ //! Generates the supplied number of one time keys.
+ size_t generateOneTimeKeys(size_t numberOfKeys);
+
+ //! Gets the OlmAccount's one time keys formatted as JSON.
+ UnsignedOneTimeKeys oneTimeKeys() const;
+
+ //! Sign all one time keys.
+ OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const;
+
+ UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const;
+
+ DeviceKeys deviceKeys() const;
+
+ //! Remove the one time key used to create the supplied session.
+ [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param preKeyMessage An Olm pre-key message that was encrypted for this account.
+ QOlmExpected<QOlmSessionPtr> createInboundSession(
+ const QOlmMessage& preKeyMessage);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param theirIdentityKey - The identity key of the Olm account that
+ //! encrypted this Olm message.
+ QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage);
+
+ //! Creates an outbound session for sending messages to a specific
+ /// identity and one time key.
+ QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey);
+
+ void markKeysAsPublished();
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ // HACK do not use directly
+ QOlmAccount(OlmAccount *account);
+ OlmAccount *data();
+
+Q_SIGNALS:
+ void needsSave();
+
+private:
+ OlmAccount *m_account = nullptr; // owning
+ QString m_userId;
+ QString m_deviceId;
+
+ QString accountId() const;
+};
+
+QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys,
+ const QString& deviceId,
+ const QString& userId);
+
+//! checks if the signature is signed by the signing_key
+QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey,
+ const QJsonObject& obj,
+ const QString& signature);
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp
new file mode 100644
index 00000000..18275dc0
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.cpp
@@ -0,0 +1,192 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolminboundsession.h"
+#include "qolmutils.h"
+#include "../logging.h"
+
+#include <cstring>
+#include <iostream>
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const {
+ return olm_inbound_group_session_last_error_code(m_groupSession);
+}
+
+const char* QOlmInboundGroupSession::lastError() const
+{
+ return olm_inbound_group_session_last_error(m_groupSession);
+}
+
+QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session)
+ : m_groupSession(session)
+{}
+
+QOlmInboundGroupSession::~QOlmInboundGroupSession()
+{
+ olm_clear_inbound_group_session(m_groupSession);
+ //delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::create(
+ const QByteArray& key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ if (olm_init_inbound_group_session(
+ olmInboundGroupSession,
+ reinterpret_cast<const uint8_t*>(key.constData()), key.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode()
+ qWarning(E2EE) << "Failed to create an inbound group session:"
+ << olm_inbound_group_session_last_error(
+ olmInboundGroupSession);
+ return olm_inbound_group_session_last_error_code(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::importSession(
+ const QByteArray& key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+
+ if (olm_import_inbound_group_session(
+ olmInboundGroupSession,
+ reinterpret_cast<const uint8_t*>(key.data()), key.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastError()
+ qWarning(E2EE) << "Failed to import an inbound group session:"
+ << olm_inbound_group_session_last_error(
+ olmInboundGroupSession);
+ return olm_inbound_group_session_last_error_code(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const
+{
+ QByteArray pickledBuf(
+ olm_pickle_inbound_group_session_length(m_groupSession), '\0');
+ if (const auto key = toKey(mode);
+ olm_pickle_inbound_group_session(m_groupSession, key.data(),
+ key.length(), pickledBuf.data(),
+ pickledBuf.length())
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session");
+ }
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle(
+ QByteArray&& pickled, const PicklingMode& mode)
+{
+ const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ auto key = toKey(mode);
+ if (olm_unpickle_inbound_group_session(groupSession, key.data(),
+ key.length(), pickled.data(),
+ pickled.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastError()
+ qWarning(E2EE) << "Failed to unpickle an inbound group session:"
+ << olm_inbound_group_session_last_error(groupSession);
+ return olm_inbound_group_session_last_error_code(groupSession);
+ }
+ key.clear();
+
+ return std::make_unique<QOlmInboundGroupSession>(groupSession);
+}
+
+QOlmExpected<std::pair<QByteArray, uint32_t>> QOlmInboundGroupSession::decrypt(
+ const QByteArray& message)
+{
+ // This is for capturing the output of olm_group_decrypt
+ uint32_t messageIndex = 0;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(message.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(
+ m_groupSession,
+ reinterpret_cast<uint8_t*>(messageBuf.data()),
+ messageBuf.length()),
+ '\0');
+
+ messageBuf = QByteArray(message.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()),
+ messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex);
+ if (plaintextLen == olm_error()) {
+ qWarning(E2EE) << "Failed to decrypt the message:" << lastError();
+ return lastErrorCode();
+ }
+
+ QByteArray output(plaintextLen, '\0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextLen);
+
+ return std::make_pair(output, messageIndex);
+}
+
+QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession(
+ uint32_t messageIndex)
+{
+ const auto keyLength = olm_export_inbound_group_session_length(m_groupSession);
+ QByteArray keyBuf(keyLength, '\0');
+ if (olm_export_inbound_group_session(
+ m_groupSession, reinterpret_cast<uint8_t*>(keyBuf.data()),
+ keyLength, messageIndex)
+ == olm_error()) {
+ QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to export the inbound group session");
+ return lastErrorCode();
+ }
+ return keyBuf;
+}
+
+uint32_t QOlmInboundGroupSession::firstKnownIndex() const
+{
+ return olm_inbound_group_session_first_known_index(m_groupSession);
+}
+
+QByteArray QOlmInboundGroupSession::sessionId() const
+{
+ QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession),
+ '\0');
+ if (olm_inbound_group_session_id(
+ m_groupSession, reinterpret_cast<uint8_t*>(sessionIdBuf.data()),
+ sessionIdBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain the group session id");
+
+ return sessionIdBuf;
+}
+
+bool QOlmInboundGroupSession::isVerified() const
+{
+ return olm_inbound_group_session_is_verified(m_groupSession) != 0;
+}
+
+QString QOlmInboundGroupSession::olmSessionId() const
+{
+ return m_olmSessionId;
+}
+void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId)
+{
+ m_olmSessionId = newOlmSessionId;
+}
+
+QString QOlmInboundGroupSession::senderId() const
+{
+ return m_senderId;
+}
+void QOlmInboundGroupSession::setSenderId(const QString& senderId)
+{
+ m_senderId = senderId;
+}
diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h
new file mode 100644
index 00000000..b9710354
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmInboundGroupSession;
+
+namespace Quotient {
+
+//! An in-bound group session is responsible for decrypting incoming
+//! communication in a Megolm session.
+class QUOTIENT_API QOlmInboundGroupSession
+{
+public:
+ ~QOlmInboundGroupSession();
+ //! Creates a new instance of `OlmInboundGroupSession`.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> create(const QByteArray& key);
+ //! Import an inbound group session, from a previous export.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> importSession(const QByteArray& key);
+ //! Serialises an `OlmInboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode& mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling
+ //! an `OlmInboundGroupSession`.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle(
+ QByteArray&& pickled, const PicklingMode& mode);
+ //! Decrypts ciphertext received for this group session.
+ QOlmExpected<std::pair<QByteArray, uint32_t> > decrypt(const QByteArray& message);
+ //! Export the base64-encoded ratchet key for this session, at the given index,
+ //! in a format which can be used by import.
+ QOlmExpected<QByteArray> exportSession(uint32_t messageIndex);
+ //! Get the first message index we know how to decrypt.
+ uint32_t firstKnownIndex() const;
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+ bool isVerified() const;
+
+ //! The olm session that this session was received from.
+ //! Required to get the device this session is from.
+ QString olmSessionId() const;
+ void setOlmSessionId(const QString& newOlmSessionId);
+
+ //! The sender of this session.
+ QString senderId() const;
+ void setSenderId(const QString& senderId);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ QOlmInboundGroupSession(OlmInboundGroupSession* session);
+private:
+ OlmInboundGroupSession* m_groupSession;
+ QString m_olmSessionId;
+ QString m_senderId;
+};
+
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+} // namespace Quotient
diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp
new file mode 100644
index 00000000..b9cb8bd2
--- /dev/null
+++ b/lib/e2ee/qolmmessage.cpp
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmmessage.h"
+
+#include "util.h"
+
+using namespace Quotient;
+
+QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type)
+ : QByteArray(std::move(ciphertext))
+ , m_messageType(type)
+{
+ Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty");
+}
+
+QOlmMessage::Type QOlmMessage::type() const
+{
+ return m_messageType;
+}
+
+QByteArray QOlmMessage::toCiphertext() const
+{
+ return SLICE(*this, QByteArray);
+}
+
+QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext)
+{
+ return QOlmMessage(ciphertext, QOlmMessage::General);
+}
diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h
new file mode 100644
index 00000000..ea73b3e3
--- /dev/null
+++ b/lib/e2ee/qolmmessage.h
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "quotient_export.h"
+
+#include <QtCore/QByteArray>
+#include <qobjectdefs.h>
+#include <olm/olm.h>
+
+namespace Quotient {
+
+/*! \brief A wrapper around an olm encrypted message
+ *
+ * This class encapsulates a Matrix olm encrypted message,
+ * passed in either of 2 forms: a general message or a pre-key message.
+ *
+ * The class provides functions to get a type and the ciphertext.
+ */
+class QUOTIENT_API QOlmMessage : public QByteArray {
+ Q_GADGET
+public:
+ enum Type {
+ PreKey = OLM_MESSAGE_TYPE_PRE_KEY,
+ General = OLM_MESSAGE_TYPE_MESSAGE,
+ };
+ Q_ENUM(Type)
+
+ explicit QOlmMessage(QByteArray ciphertext, Type type = General);
+
+ static QOlmMessage fromCiphertext(const QByteArray &ciphertext);
+
+ Q_INVOKABLE Type type() const;
+ Q_INVOKABLE QByteArray toCiphertext() const;
+
+private:
+ Type m_messageType = General;
+};
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
new file mode 100644
index 00000000..1176d790
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmoutboundsession.h"
+
+#include "logging.h"
+#include "qolmutils.h"
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const {
+ return olm_outbound_group_session_last_error_code(m_groupSession);
+}
+
+const char* QOlmOutboundGroupSession::lastError() const
+{
+ return olm_outbound_group_session_last_error(m_groupSession);
+}
+
+QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session)
+ : m_groupSession(session)
+{}
+
+QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
+{
+ olm_clear_outbound_group_session(m_groupSession);
+ delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create()
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ if (const auto randomLength = olm_init_outbound_group_session_random_length(
+ olmOutboundGroupSession);
+ olm_init_outbound_group_session(olmOutboundGroupSession,
+ RandomBuffer(randomLength).bytes(),
+ randomLength)
+ == olm_error()) {
+ // FIXME: create the session object earlier
+ QOLM_INTERNAL_ERROR_X("Failed to initialise an outbound group session",
+ olm_outbound_group_session_last_error(
+ olmOutboundGroupSession));
+ }
+
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(
+ olm_pickle_outbound_group_session_length(m_groupSession), '\0');
+ auto key = toKey(mode);
+ if (olm_pickle_outbound_group_session(m_groupSession, key.data(),
+ key.length(), pickledBuf.data(),
+ pickledBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session");
+
+ key.clear();
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(
+ QByteArray&& pickled, const PicklingMode& mode)
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ auto key = toKey(mode);
+ if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(),
+ key.length(), pickled.data(),
+ pickled.length())
+ == olm_error()) {
+ // FIXME: create the session object earlier and use lastError()
+ qWarning(E2EE) << "Failed to unpickle an outbound group session:"
+ << olm_outbound_group_session_last_error(
+ olmOutboundGroupSession);
+ return olm_outbound_group_session_last_error_code(
+ olmOutboundGroupSession);
+ }
+
+ key.clear();
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const
+{
+ const auto messageMaxLength =
+ olm_group_encrypt_message_length(m_groupSession, plaintext.length());
+ QByteArray messageBuf(messageMaxLength, '\0');
+ if (olm_group_encrypt(m_groupSession,
+ reinterpret_cast<const uint8_t*>(plaintext.data()),
+ plaintext.length(),
+ reinterpret_cast<uint8_t*>(messageBuf.data()),
+ messageBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to encrypt a message");
+
+ return messageBuf;
+}
+
+uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const
+{
+ return olm_outbound_group_session_message_index(m_groupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::sessionId() const
+{
+ const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession);
+ QByteArray idBuffer(idMaxLength, '\0');
+ if (olm_outbound_group_session_id(
+ m_groupSession, reinterpret_cast<uint8_t*>(idBuffer.data()),
+ idBuffer.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain group session id");
+
+ return idBuffer;
+}
+
+QByteArray QOlmOutboundGroupSession::sessionKey() const
+{
+ const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession);
+ QByteArray keyBuffer(keyMaxLength, '\0');
+ if (olm_outbound_group_session_key(
+ m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()),
+ keyMaxLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain group session key");
+
+ return keyBuffer;
+}
+
+int QOlmOutboundGroupSession::messageCount() const
+{
+ return m_messageCount;
+}
+
+void QOlmOutboundGroupSession::setMessageCount(int messageCount)
+{
+ m_messageCount = messageCount;
+}
+
+QDateTime QOlmOutboundGroupSession::creationTime() const
+{
+ return m_creationTime;
+}
+
+void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime)
+{
+ m_creationTime = creationTime;
+}
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
new file mode 100644
index 00000000..d36fbf69
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmOutboundGroupSession;
+
+namespace Quotient {
+
+//! An out-bound group session is responsible for encrypting outgoing
+//! communication in a Megolm session.
+class QUOTIENT_API QOlmOutboundGroupSession
+{
+public:
+ ~QOlmOutboundGroupSession();
+ //! Creates a new instance of `QOlmOutboundGroupSession`.
+ //! Throw OlmError on errors
+ static QOlmOutboundGroupSessionPtr create();
+ //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by
+ //! pickling a `QOlmOutboundGroupSession`.
+ static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle(
+ QByteArray&& pickled, const PicklingMode& mode);
+
+ //! Encrypts a plaintext message using the session.
+ QByteArray encrypt(const QByteArray& plaintext) const;
+
+ //! Get the current message index for this session.
+ //!
+ //! Each message is sent with an increasing index; this returns the
+ //! index for the next message.
+ uint32_t sessionMessageIndex() const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Get the base64-encoded current ratchet key for this session.
+ //!
+ //! Each message is sent with a different ratchet key. This function returns the
+ //! ratchet key that will be used for the next message.
+ QByteArray sessionKey() const;
+ QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession);
+
+ int messageCount() const;
+ void setMessageCount(int messageCount);
+
+ QDateTime creationTime() const;
+ void setCreationTime(const QDateTime& creationTime);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+private:
+ OlmOutboundGroupSession *m_groupSession;
+ int m_messageCount = 0;
+ QDateTime m_creationTime = QDateTime::currentDateTime();
+};
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
new file mode 100644
index 00000000..e3f69132
--- /dev/null
+++ b/lib/e2ee/qolmsession.cpp
@@ -0,0 +1,231 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmsession.h"
+
+#include "e2ee/qolmutils.h"
+#include "logging.h"
+
+#include <cstring>
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmSession::lastErrorCode() const {
+ return olm_session_last_error_code(m_session);
+}
+
+const char* QOlmSession::lastError() const
+{
+ return olm_session_last_error(m_session);
+}
+
+Quotient::QOlmSession::~QOlmSession()
+{
+ olm_clear_session(m_session);
+ delete[](reinterpret_cast<uint8_t *>(m_session));
+}
+
+OlmSession* QOlmSession::create()
+{
+ return olm_session(new uint8_t[olm_session_size()]);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInbound(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from,
+ const QString& theirIdentityKey)
+{
+ if (preKeyMessage.type() != QOlmMessage::PreKey) {
+ qCCritical(E2EE) << "The message is not a pre-key; will try to create "
+ "the inbound session anyway";
+ }
+
+ const auto olmSession = create();
+
+ QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ size_t error = 0;
+ if (from) {
+ error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ } else {
+ error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ }
+
+ if (error == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto lastErr = olm_session_last_error_code(olmSession);
+ qCWarning(E2EE) << "Error when creating inbound session" << lastErr;
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage)
+{
+ return createInbound(account, preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage)
+{
+ return createInbound(account, preKeyMessage, true, theirIdentityKey);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession(
+ QOlmAccount* account, const QByteArray& theirIdentityKey,
+ const QByteArray& theirOneTimeKey)
+{
+ auto* olmOutboundSession = create();
+ if (const auto randomLength =
+ olm_create_outbound_session_random_length(olmOutboundSession);
+ olm_create_outbound_session(
+ olmOutboundSession, account->data(), theirIdentityKey.data(),
+ theirIdentityKey.length(), theirOneTimeKey.data(),
+ theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength)
+ == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto lastErr = olm_session_last_error_code(olmOutboundSession);
+ QOLM_FAIL_OR_LOG_X(lastErr == OLM_NOT_ENOUGH_RANDOM,
+ "Failed to create an outbound Olm session",
+ olm_session_last_error(olmOutboundSession));
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmOutboundSession);
+}
+
+QByteArray QOlmSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0');
+ QByteArray key = toKey(mode);
+ if (olm_pickle_session(m_session, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to pickle an Olm session");
+
+ key.clear();
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(QByteArray&& pickled,
+ const PicklingMode& mode)
+{
+ auto *olmSession = create();
+ auto key = toKey(mode);
+ if (olm_unpickle_session(olmSession, key.data(), key.length(),
+ pickled.data(), pickled.length())
+ == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto errorCode = olm_session_last_error_code(olmSession);
+ QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to unpickle an Olm session",
+ olm_session_last_error(olmSession));
+ return errorCode;
+ }
+
+ key.clear();
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext)
+{
+ const auto messageMaxLength =
+ olm_encrypt_message_length(m_session, plaintext.length());
+ QByteArray messageBuf(messageMaxLength, '\0');
+ // NB: The type has to be calculated before calling olm_encrypt()
+ const auto messageType = olm_encrypt_message_type(m_session);
+ if (const auto randomLength = olm_encrypt_random_length(m_session);
+ olm_encrypt(m_session, plaintext.data(), plaintext.length(),
+ RandomBuffer(randomLength), randomLength, messageBuf.data(),
+ messageBuf.length())
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR("Failed to encrypt the message");
+ }
+
+ return QOlmMessage(messageBuf, QOlmMessage::Type(messageType));
+}
+
+QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const
+{
+ const auto ciphertext = message.toCiphertext();
+ const auto messageTypeValue = message.type();
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(ciphertext.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(
+ m_session, messageTypeValue, messageBuf.data(), messageBuf.length());
+ if (plaintextMaxLen == olm_error()) {
+ qWarning(E2EE) << "Couldn't calculate decrypted message length:"
+ << lastError();
+ return lastErrorCode();
+ }
+
+ QByteArray plaintextBuf(plaintextMaxLen, '\0');
+ QByteArray messageBuf2(ciphertext.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf2.begin());
+
+ const auto plaintextResultLen =
+ olm_decrypt(m_session, messageTypeValue, messageBuf2.data(),
+ messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen);
+ if (plaintextResultLen == olm_error()) {
+ QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to decrypt the message");
+ return lastErrorCode();
+ }
+ plaintextBuf.truncate(plaintextResultLen);
+ return plaintextBuf;
+}
+
+QByteArray QOlmSession::sessionId() const
+{
+ const auto idMaxLength = olm_session_id_length(m_session);
+ QByteArray idBuffer(idMaxLength, '\0');
+ if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain Olm session id");
+
+ return idBuffer;
+}
+
+bool QOlmSession::hasReceivedMessage() const
+{
+ return olm_session_has_received_message(m_session);
+}
+
+bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey);
+ QByteArray oneTimeKeyBuf(preKeyMessage.data());
+ const auto maybeMatches =
+ olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(),
+ oneTimeKeyBuf.length());
+ if (maybeMatches == olm_error())
+ qWarning(E2EE) << "Error matching an inbound session:" << lastError();
+
+ return maybeMatches == 1; // Any errors are treated as non-match
+}
+
+bool QOlmSession::matchesInboundSessionFrom(
+ const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const
+{
+ const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ const auto maybeMatches = olm_matches_inbound_session_from(
+ m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
+ oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+
+ if (maybeMatches == olm_error())
+ qCWarning(E2EE) << "Error matching an inbound session:" << lastError();
+
+ return maybeMatches == 1;
+}
+
+QOlmSession::QOlmSession(OlmSession *session)
+ : m_session(session)
+{}
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
new file mode 100644
index 00000000..400fb854
--- /dev/null
+++ b/lib/e2ee/qolmsession.h
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmaccount.h"
+
+struct OlmSession;
+
+namespace Quotient {
+
+//! Either an outbound or inbound session for secure communication.
+class QUOTIENT_API QOlmSession
+{
+public:
+ ~QOlmSession();
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ static QOlmExpected<QOlmSessionPtr> createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage);
+
+ static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage);
+
+ static QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ QOlmAccount* account, const QByteArray& theirIdentityKey,
+ const QByteArray& theirOneTimeKey);
+
+ //! Serialises an `QOlmSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+
+ //! Deserialises from encrypted Base64 previously made with pickle()
+ static QOlmExpected<QOlmSessionPtr> unpickle(QByteArray&& pickled,
+ const PicklingMode& mode);
+
+ //! Encrypts a plaintext message using the session.
+ QOlmMessage encrypt(const QByteArray& plaintext);
+
+ //! Decrypts a message using this session. Decoding is lossy, meaning if
+ //! the decrypted plaintext contains invalid UTF-8 symbols, they will
+ //! be returned as `U+FFFD` (�).
+ QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Checker for any received messages for this session.
+ bool hasReceivedMessage() const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ bool matchesInboundSession(const QOlmMessage& preKeyMessage) const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ bool matchesInboundSessionFrom(
+ const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const;
+
+ friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs)
+ {
+ return lhs.sessionId() < rhs.sessionId();
+ }
+
+ friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs)
+ {
+ return *lhs < *rhs;
+ }
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ OlmSession* raw() const { return m_session; }
+
+ QOlmSession(OlmSession* session);
+private:
+ //! Helper function for creating new sessions and handling errors.
+ static OlmSession* create();
+ static QOlmExpected<QOlmSessionPtr> createInbound(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage,
+ bool from = false, const QString& theirIdentityKey = "");
+ OlmSession* m_session;
+};
+} //namespace Quotient
diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp
new file mode 100644
index 00000000..46f7f4f3
--- /dev/null
+++ b/lib/e2ee/qolmutility.cpp
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutility.h"
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmUtility::lastErrorCode() const {
+ return olm_utility_last_error_code(m_utility);
+}
+
+const char* QOlmUtility::lastError() const
+{
+ return olm_utility_last_error(m_utility);
+}
+
+QOlmUtility::QOlmUtility()
+{
+ auto utility = new uint8_t[olm_utility_size()];
+ m_utility = olm_utility(utility);
+}
+
+QOlmUtility::~QOlmUtility()
+{
+ olm_clear_utility(m_utility);
+ delete[](reinterpret_cast<uint8_t *>(m_utility));
+}
+
+QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const
+{
+ const auto outputLen = olm_sha256_length(m_utility);
+ QByteArray outputBuf(outputLen, '\0');
+ olm_sha256(m_utility, inputBuf.data(), inputBuf.length(),
+ outputBuf.data(), outputBuf.length());
+
+ return QString::fromUtf8(outputBuf);
+}
+
+QString QOlmUtility::sha256Utf8Msg(const QString &message) const
+{
+ return sha256Bytes(message.toUtf8());
+}
+
+bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message,
+ QByteArray signature)
+{
+ return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(),
+ message.size(), signature.data(), signature.size())
+ == 0;
+}
diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h
new file mode 100644
index 00000000..508767bf
--- /dev/null
+++ b/lib/e2ee/qolmutility.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmUtility;
+
+namespace Quotient {
+
+//! Allows you to make use of crytographic hashing via SHA-2 and
+//! verifying ed25519 signatures.
+class QUOTIENT_API QOlmUtility
+{
+public:
+ QOlmUtility();
+ ~QOlmUtility();
+
+ //! Returns a sha256 of the supplied byte slice.
+ QString sha256Bytes(const QByteArray &inputBuf) const;
+
+ //! Convenience function that converts the UTF-8 message
+ //! to bytes and then calls `sha256Bytes()`, returning its output.
+ QString sha256Utf8Msg(const QString &message) const;
+
+ //! Verify a ed25519 signature.
+ //! \param key QByteArray The public part of the ed25519 key that signed the message.
+ //! \param message QByteArray The message that was signed.
+ //! \param signature QByteArray The signature of the message.
+ bool ed25519Verify(const QByteArray &key,
+ const QByteArray &message, QByteArray signature);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+private:
+ OlmUtility *m_utility;
+};
+}
diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp
new file mode 100644
index 00000000..c6e51bcd
--- /dev/null
+++ b/lib/e2ee/qolmutils.cpp
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutils.h"
+#include <QtCore/QRandomGenerator>
+
+using namespace Quotient;
+
+QByteArray Quotient::toKey(const Quotient::PicklingMode &mode)
+{
+ if (std::holds_alternative<Quotient::Unencrypted>(mode)) {
+ return {};
+ }
+ return std::get<Quotient::Encrypted>(mode).key;
+}
+
+RandomBuffer::RandomBuffer(size_t size)
+ : QByteArray(static_cast<int>(size), '\0')
+{
+ QRandomGenerator::system()->generate(begin(), end());
+}
diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h
new file mode 100644
index 00000000..17eee7a3
--- /dev/null
+++ b/lib/e2ee/qolmutils.h
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+
+// Convert PicklingMode to key
+QUOTIENT_API QByteArray toKey(const PicklingMode &mode);
+
+class QUOTIENT_API RandomBuffer : public QByteArray {
+public:
+ explicit RandomBuffer(size_t size);
+ ~RandomBuffer() { clear(); }
+
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ QUO_IMPLICIT operator void*() { return data(); }
+ char* chars() { return data(); }
+ uint8_t* bytes() { return reinterpret_cast<uint8_t*>(data()); }
+
+ Q_DISABLE_COPY(RandomBuffer)
+ RandomBuffer(RandomBuffer&&) = default;
+ void operator=(RandomBuffer&&) = delete;
+};
+
+[[deprecated("Create RandomBuffer directly")]] inline auto getRandom(
+ size_t bufferSize)
+{
+ return RandomBuffer(bufferSize);
+}
+
+#define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \
+ qFatal("%s, internal error: %s", Message_, LastError_)
+
+#define QOLM_INTERNAL_ERROR(Message_) \
+ QOLM_INTERNAL_ERROR_X(Message_, lastError())
+
+#define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \
+ do { \
+ const QString errorMsg{ (Message_) }; \
+ if (InternalCondition_) \
+ QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \
+ qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \
+ } while (false) /* End of macro */
+
+#define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \
+ QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \
+ lastError())
+
+} // namespace Quotient