diff options
Diffstat (limited to 'lib/e2ee')
-rw-r--r-- | lib/e2ee/e2ee.h | 139 | ||||
-rw-r--r-- | lib/e2ee/qolmaccount.cpp | 275 | ||||
-rw-r--r-- | lib/e2ee/qolmaccount.h | 123 | ||||
-rw-r--r-- | lib/e2ee/qolminboundsession.cpp | 192 | ||||
-rw-r--r-- | lib/e2ee/qolminboundsession.h | 60 | ||||
-rw-r--r-- | lib/e2ee/qolmmessage.cpp | 31 | ||||
-rw-r--r-- | lib/e2ee/qolmmessage.h | 42 | ||||
-rw-r--r-- | lib/e2ee/qolmoutboundsession.cpp | 152 | ||||
-rw-r--r-- | lib/e2ee/qolmoutboundsession.h | 63 | ||||
-rw-r--r-- | lib/e2ee/qolmsession.cpp | 231 | ||||
-rw-r--r-- | lib/e2ee/qolmsession.h | 84 | ||||
-rw-r--r-- | lib/e2ee/qolmutility.cpp | 53 | ||||
-rw-r--r-- | lib/e2ee/qolmutility.h | 41 | ||||
-rw-r--r-- | lib/e2ee/qolmutils.cpp | 22 | ||||
-rw-r--r-- | lib/e2ee/qolmutils.h | 55 |
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 |