diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/connection.cpp | 157 | ||||
-rw-r--r-- | lib/connection.h | 32 | ||||
-rw-r--r-- | lib/database.cpp | 31 | ||||
-rw-r--r-- | lib/database.h | 11 | ||||
-rw-r--r-- | lib/events/keyverificationevent.h | 143 | ||||
-rw-r--r-- | lib/events/roomevent.h | 2 | ||||
-rw-r--r-- | lib/events/roomkeyevent.cpp | 21 | ||||
-rw-r--r-- | lib/events/roomkeyevent.h | 3 | ||||
-rw-r--r-- | lib/keyverificationsession.cpp | 512 | ||||
-rw-r--r-- | lib/keyverificationsession.h | 147 | ||||
-rw-r--r-- | lib/room.cpp | 1 |
11 files changed, 987 insertions, 73 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index 98720f3b..79d7ae55 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -32,16 +32,21 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include <variant> #ifdef Quotient_E2EE_ENABLED # include "database.h" +# include "keyverificationsession.h" + # include "e2ee/qolmaccount.h" # include "e2ee/qolminboundsession.h" # include "e2ee/qolmsession.h" # include "e2ee/qolmutility.h" # include "e2ee/qolmutils.h" +# include "events/keyverificationevent.h" #endif // Quotient_E2EE_ENABLED + #if QT_VERSION_MAJOR >= 6 # include <qt6keychain/keychain.h> #else @@ -115,6 +120,7 @@ public: QHash<QString, int> oneTimeKeysCount; std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents; void handleEncryptedToDeviceEvent(const EncryptedEvent& event); + bool processIfVerificationEvent(const Event &evt, bool encrypted); // A map from SenderKey to vector of InboundSession UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions; @@ -365,11 +371,9 @@ public: const OneTimeKeys &oneTimeKeyObject); QString curveKeyForUserDevice(const QString& userId, const QString& device) const; - QString edKeyForUserDevice(const QString& userId, - const QString& device) const; - QJsonObject encryptSessionKeyEvent(QJsonObject payloadJson, - const QString& targetUserId, - const QString& targetDeviceId) const; + QJsonObject assembleEncryptedContent(QJsonObject payloadJson, + const QString& targetUserId, + const QString& targetDeviceId) const; #endif void saveAccessTokenToKeychain() const @@ -967,7 +971,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) if (!toDeviceEvents.empty()) { qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; - for (auto&& tdEvt : toDeviceEvents) + for (auto&& tdEvt : toDeviceEvents) { if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) { if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { qCDebug(E2EE) << "Unsupported algorithm" << event->id() @@ -982,12 +986,48 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) outdatedUsers += event->senderId(); encryptionUpdateRequired = true; pendingEncryptedEvents.push_back(std::move(event)); + continue; } + processIfVerificationEvent(*tdEvt, false); + } } #endif } #ifdef Quotient_E2EE_ENABLED +bool Connection::Private::processIfVerificationEvent(const Event& evt, + bool encrypted) +{ + return switchOnType(evt, + [this, encrypted](const KeyVerificationRequestEvent& event) { + auto session = + new KeyVerificationSession(q->userId(), event, q, encrypted); + emit q->newKeyVerificationSession(session); + return true; + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + return true; + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + return true; + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + return true; + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + return true; + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + return true; + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + return true; + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); + return true; + }, false); +} + void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) { const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); @@ -996,18 +1036,23 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve return; } + if (processIfVerificationEvent(*decryptedEvent, true)) + return; switchOnType(*decryptedEvent, - [this, &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + [this, &event, + olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), + olmSessionId); } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } }, [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); + qCWarning(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); }); } #endif @@ -2113,8 +2158,8 @@ void Connection::Private::saveDevicesList() query.prepare(QStringLiteral( "INSERT INTO tracked_devices" - "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) " - "VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) " + "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);" )); for (const auto& user : deviceKeys.keys()) { for (const auto& device : deviceKeys[user]) { @@ -2128,6 +2173,8 @@ void Connection::Private::saveDevicesList() query.bindValue(":curveKey", device.keys[curveKeyId]); query.bindValue(":edKeyId", edKeyId); query.bindValue(":edKey", device.keys[edKeyId]); + // If the device gets saved here, it can't be verified + query.bindValue(":verified", false); q->database()->execute(query); } @@ -2232,10 +2279,10 @@ QString Connection::Private::curveKeyForUserDevice(const QString& userId, return deviceKeys[userId][device].keys["curve25519:" % device]; } -QString Connection::Private::edKeyForUserDevice(const QString& userId, - const QString& device) const +QString Connection::edKeyForUserDevice(const QString& userId, + const QString& deviceId) const { - return deviceKeys[userId][device].keys["ed25519:" % device]; + return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId]; } bool Connection::Private::isKnownCurveKey(const QString& userId, @@ -2297,7 +2344,7 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, const auto signature = signedOneTimeKey->signature(targetUserId, targetDeviceId); if (!verifier.ed25519Verify( - edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), + q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), signedOneTimeKey->toJsonForVerification(), signature)) { qWarning(E2EE) << "Failed to verify one-time-key signature for" @@ -2320,15 +2367,21 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, return true; } -QJsonObject Connection::Private::encryptSessionKeyEvent( +QJsonObject Connection::Private::assembleEncryptedContent( QJsonObject payloadJson, const QString& targetUserId, const QString& targetDeviceId) const { + payloadJson.insert(SenderKeyL, data->userId()); +// eventJson.insert("sender_device"_ls, data->deviceId()); + payloadJson.insert("keys"_ls, + QJsonObject{ + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); payloadJson.insert("recipient"_ls, targetUserId); - payloadJson.insert("recipient_keys"_ls, - QJsonObject { { Ed25519Key, - edKeyForUserDevice(targetUserId, - targetDeviceId) } }); + payloadJson.insert( + "recipient_keys"_ls, + QJsonObject{ { Ed25519Key, + q->edKeyForUserDevice(targetUserId, targetDeviceId) } }); const auto [type, cipherText] = olmEncryptMessage( targetUserId, targetDeviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); @@ -2337,7 +2390,6 @@ QJsonObject Connection::Private::encryptSessionKeyEvent( QJsonObject { { "type"_ls, type }, { "body"_ls, QString(cipherText) } } } }; - return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519) .contentJson(); } @@ -2360,18 +2412,8 @@ void Connection::sendSessionKeyToDevices( if (hash.isEmpty()) return; - auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, sessionId, - sessionKey, userId()) - .fullJson(); - keyEventJson.insert(SenderKeyL, userId()); - keyEventJson.insert("sender_device"_ls, deviceId()); - keyEventJson.insert( - "keys"_ls, - QJsonObject { - { Ed25519Key, QString(olmAccount()->identityKeys().ed25519) } }); - auto job = callApi<ClaimKeysJob>(hash); - connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, keyEventJson, devices, index] { + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent; for (const auto oneTimeKeys = job->oneTimeKeys(); const auto& [targetUserId, targetDeviceId] : @@ -2385,10 +2427,14 @@ void Connection::sendSessionKeyToDevices( // Noisy but nice for debugging // qDebug(E2EE) << "Creating the payload for" << targetUserId // << targetDeviceId << sessionId << sessionKey.toHex(); + const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, + roomId, sessionId, sessionKey) + .fullJson(); + usersToDevicesToContent[targetUserId][targetDeviceId] = - d->encryptSessionKeyEvent(keyEventJson, targetUserId, + d->assembleEncryptedContent(keyEventJson, targetUserId, targetDeviceId); - } + } if (!usersToDevicesToContent.empty()) { sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); QVector<std::tuple<QString, QString, QString>> receivedDevices; @@ -2417,4 +2463,43 @@ void Connection::saveCurrentOutboundMegolmSession( session); } +void Connection::startKeyVerificationSession(const QString& deviceId) +{ + auto* const session = new KeyVerificationSession(userId(), deviceId, this); + emit newKeyVerificationSession(session); +} + +void Connection::sendToDevice(const QString& targetUserId, + const QString& targetDeviceId, Event event, + bool encrypted) +{ + const auto contentJson = + encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId, + targetDeviceId) + : event.contentJson(); + sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(), + { { targetUserId, { { targetDeviceId, contentJson } } } }); +} + +bool Connection::isVerifiedSession(const QString& megolmSessionId) const +{ + auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", megolmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto olmSessionId = query.value("olmSessionId").toString(); + query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", olmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto curveKey = query.value("senderKey"_ls).toString(); + query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next() && query.value("verified").toBool(); +} #endif diff --git a/lib/connection.h b/lib/connection.h index 0e0abc39..39921938 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -23,8 +23,9 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" -#include "e2ee/qolmmessage.h" #include "e2ee/qolmoutboundsession.h" +#include "keyverificationsession.h" +#include "events/keyverificationevent.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -319,17 +320,27 @@ public: QOlmAccount* olmAccount() const; Database* database() const; PicklingMode picklingMode() const; + UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions( const Room* room) const; void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session) const; - bool hasOlmSession(const QString& user, const QString& deviceId) const; - QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( const QString& roomId) const; void saveCurrentOutboundMegolmSession( const QString& roomId, const QOlmOutboundGroupSession& session) const; + QString edKeyForUserDevice(const QString& userId, + const QString& deviceId) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; + + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, + Event event, bool encrypted); + + /// Returns true if this megolm session comes from a verified device + bool isVerifiedSession(const QString& megolmSessionId) const; + void sendSessionKeyToDevices(const QString& roomId, const QByteArray& sessionId, const QByteArray& sessionKey, @@ -689,6 +700,8 @@ public Q_SLOTS: virtual LeaveRoomJob* leaveRoom(Room* room); #ifdef Quotient_E2EE_ENABLED + void startKeyVerificationSession(const QString& deviceId); + void encryptionUpdate(Room *room); #endif @@ -850,6 +863,19 @@ Q_SIGNALS: void turnServersChanged(const QJsonObject& servers); void devicesListLoaded(); +#ifdef Quotient_E2EE_ENABLED + void incomingKeyVerificationReady(const KeyVerificationReadyEvent& event); + void incomingKeyVerificationStart(const KeyVerificationStartEvent& event); + void incomingKeyVerificationAccept(const KeyVerificationAcceptEvent& event); + void incomingKeyVerificationKey(const KeyVerificationKeyEvent& event); + void incomingKeyVerificationMac(const KeyVerificationMacEvent& event); + void incomingKeyVerificationDone(const KeyVerificationDoneEvent& event); + void incomingKeyVerificationCancel(const KeyVerificationCancelEvent& event); + + void newKeyVerificationSession(KeyVerificationSession* session); + void sessionVerified(const QString& userId, const QString& deviceId); +#endif + protected: /** * @brief Access the underlying ConnectionData class diff --git a/lib/database.cpp b/lib/database.cpp index ed7bd794..4eb82cf5 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -31,7 +31,8 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa case 0: migrateTo1(); [[fallthrough]]; case 1: migrateTo2(); [[fallthrough]]; case 2: migrateTo3(); [[fallthrough]]; - case 3: migrateTo4(); + case 3: migrateTo4(); [[fallthrough]]; + case 4: migrateTo5(); } } @@ -102,7 +103,7 @@ void Database::migrateTo2() { qCDebug(DATABASE) << "Migrating database to version 2"; transaction(); - //TODO remove this column again - we don't need it after all + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); @@ -141,6 +142,16 @@ void Database::migrateTo4() commit(); } +void Database::migrateTo5() +{ + qCDebug(DATABASE) << "Migrating database to version 5"; + transaction(); + + execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;")); + execute(QStringLiteral("PRAGMA user_version = 5")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); @@ -392,3 +403,19 @@ void Database::updateOlmSession(const QString& senderKey, const QString& session commit(); } +void Database::setSessionVerified(const QString& edKeyId) +{ + auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;")); + query.bindValue(":edKeyId", edKeyId); + transaction(); + execute(query); + commit(); +} + +bool Database::isSessionVerified(const QString& edKey) +{ + auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey")); + query.bindValue(":edKey", edKey); + execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/database.h b/lib/database.h index 4091d61b..8a133f8e 100644 --- a/lib/database.h +++ b/lib/database.h @@ -12,8 +12,6 @@ #include "e2ee/e2ee.h" namespace Quotient { -class User; -class Room; class QUOTIENT_API Database : public QObject { @@ -59,7 +57,8 @@ public: const QByteArray& pickle); // Returns a map UserId -> [DeviceId] that have not received key yet - QMultiHash<QString, QString> devicesWithoutKey(const QString& roomId, QMultiHash<QString, QString> devices, + QMultiHash<QString, QString> devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, const QString& sessionId); // 'devices' contains tuples {userId, deviceId, curveKey} void setDevicesReceivedKey( @@ -67,12 +66,16 @@ public: const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index); + bool isSessionVerified(const QString& edKey); + void setSessionVerified(const QString& edKeyId); + private: void migrateTo1(); void migrateTo2(); void migrateTo3(); void migrateTo4(); + void migrateTo5(); QString m_matrixId; }; -} +} // namespace Quotient diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 78457e0c..f635d07b 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -1,17 +1,33 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> // SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once + #include "event.h" namespace Quotient { +static constexpr auto SasV1Method = "m.sas.v1"_ls; + /// Requests a key verification with another user's devices. /// Typically sent as a to-device event. class QUOTIENT_API KeyVerificationRequestEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) - explicit KeyVerificationRequestEvent(const QJsonObject& obj); + explicit KeyVerificationRequestEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationRequestEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods, + const QDateTime& timestamp) + : KeyVerificationRequestEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) }, + { "timestamp"_ls, toJson(timestamp) } })) + {} /// The device ID which is initiating the request. QUO_CONTENT_GETTER(QString, fromDevice) @@ -27,16 +43,60 @@ public: /// made. If the request is in the future by more than 5 minutes or /// more than 10 minutes in the past, the message should be ignored /// by the receiver. - QUO_CONTENT_GETTER(uint64_t, timestamp) + QUO_CONTENT_GETTER(QDateTime, timestamp) }; REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) +class QUOTIENT_API KeyVerificationReadyEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent) + + explicit KeyVerificationReadyEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationReadyEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods) + : KeyVerificationReadyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) } })) + {} + + /// The device ID which is accepting the request. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The transaction id of the verification request + QUO_CONTENT_GETTER(QString, transactionId) + + /// The verification methods supported by the sender. + QUO_CONTENT_GETTER(QStringList, methods) +}; +REGISTER_EVENT_TYPE(KeyVerificationReadyEvent) + + /// Begins a key verification process. class QUOTIENT_API KeyVerificationStartEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) - explicit KeyVerificationStartEvent(const QJsonObject &obj); + explicit KeyVerificationStartEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationStartEvent(const QString& transactionId, + const QString& fromDevice) + : KeyVerificationStartEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "method"_ls, SasV1Method }, + { "hashes"_ls, QJsonArray{ "sha256"_ls } }, + { "key_agreement_protocols"_ls, + QJsonArray{ "curve25519-hkdf-sha256"_ls } }, + { "message_authentication_codes"_ls, + QJsonArray{ "hkdf-hmac-sha256"_ls } }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls } } })) + {} /// The device ID which is initiating the process. QUO_CONTENT_GETTER(QString, fromDevice) @@ -57,7 +117,7 @@ public: /// \note Only exist if method is m.sas.v1 QStringList keyAgreementProtocols() const { - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + Q_ASSERT(method() == SasV1Method); return contentPart<QStringList>("key_agreement_protocols"_ls); } @@ -65,7 +125,7 @@ public: /// \note Only exist if method is m.sas.v1 QStringList hashes() const { - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + Q_ASSERT(method() == SasV1Method); return contentPart<QStringList>("hashes"_ls); } @@ -73,7 +133,7 @@ public: /// \note Only exist if method is m.sas.v1 QStringList messageAuthenticationCodes() const { - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + Q_ASSERT(method() == SasV1Method); return contentPart<QStringList>("message_authentication_codes"_ls); } @@ -82,7 +142,7 @@ public: /// \note Only exist if method is m.sas.v1 QString shortAuthenticationString() const { - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + Q_ASSERT(method() == SasV1Method); return contentPart<QString>("short_authentification_string"_ls); } }; @@ -94,7 +154,21 @@ class QUOTIENT_API KeyVerificationAcceptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) - explicit KeyVerificationAcceptEvent(const QJsonObject& obj); + explicit KeyVerificationAcceptEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationAcceptEvent(const QString& transactionId, + const QString& commitment) + : KeyVerificationAcceptEvent(basicJson( + TypeId, { { "transaction_id"_ls, transactionId }, + { "method"_ls, SasV1Method }, + { "key_agreement_protocol"_ls, "curve25519-hkdf-sha256" }, + { "hash"_ls, "sha256" }, + { "message_authentication_code"_ls, "hkdf-hmac-sha256" }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls, } }, + { "commitment"_ls, commitment } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -131,7 +205,18 @@ class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) - explicit KeyVerificationCancelEvent(const QJsonObject &obj); + explicit KeyVerificationCancelEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationCancelEvent(const QString& transactionId, + const QString& reason) + : KeyVerificationCancelEvent( + basicJson(TypeId, { + { "transaction_id"_ls, transactionId }, + { "reason"_ls, reason }, + { "code"_ls, reason } // Not a typo + })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -147,11 +232,18 @@ REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. -class KeyVerificationKeyEvent : public Event { +class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) - explicit KeyVerificationKeyEvent(const QJsonObject &obj); + explicit KeyVerificationKeyEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationKeyEvent(const QString& transactionId, const QString& key) + : KeyVerificationKeyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "key"_ls, key } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -166,7 +258,16 @@ class QUOTIENT_API KeyVerificationMacEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) - explicit KeyVerificationMacEvent(const QJsonObject &obj); + explicit KeyVerificationMacEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + KeyVerificationMacEvent(const QString& transactionId, const QString& keys, + const QJsonObject& mac) + : KeyVerificationMacEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "keys"_ls, keys }, + { "mac"_ls, mac } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -180,4 +281,22 @@ public: } }; REGISTER_EVENT_TYPE(KeyVerificationMacEvent) + +class QUOTIENT_API KeyVerificationDoneEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent) + + explicit KeyVerificationDoneEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} + explicit KeyVerificationDoneEvent(const QString& transactionId) + : KeyVerificationDoneEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) + {} + + /// The same transactionId as before + QUO_CONTENT_GETTER(QString, transactionId) +}; +REGISTER_EVENT_TYPE(KeyVerificationDoneEvent) + } // namespace Quotient diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 7f724689..9461340b 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -62,7 +62,7 @@ public: #ifdef Quotient_E2EE_ENABLED void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent); - const RoomEvent* originalEvent() { return _originalEvent.get(); } + const RoomEvent* originalEvent() const { return _originalEvent.get(); } const QJsonObject encryptedJson() const; #endif diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 68962950..3a8601d1 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -5,21 +5,18 @@ using namespace Quotient; -RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) +RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(TypeId, obj) { if (roomId().isEmpty()) qCWarning(E2EE) << "Room key event has empty room id"; } -RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString& sessionId, const QString& sessionKey, const QString& senderId) - : Event(typeId(), { - {"content", QJsonObject{ - {"algorithm", algorithm}, - {"room_id", roomId}, - {"session_id", sessionId}, - {"session_key", sessionKey}, - }}, - {"sender", senderId}, - {"type", "m.room_key"}, - }) +RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey) + : Event(TypeId, basicJson(TypeId, { + { "algorithm", algorithm }, + { "room_id", roomId }, + { "session_id", sessionId }, + { "session_key", sessionKey }, + })) {} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 9eb2854b..0dfdf383 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -13,8 +13,7 @@ public: explicit RoomKeyEvent(const QJsonObject& obj); explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, - const QString& sessionId, const QString& sessionKey, - const QString& senderId); + const QString& sessionId, const QString& sessionKey); QUO_CONTENT_GETTER(QString, algorithm) QUO_CONTENT_GETTER(QString, roomId) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..2c468c3e --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,512 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationsession.h" + +#include "connection.h" +#include "database.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "olm/sas.h" + +#include "events/event.h" + +#include <QtCore/QCryptographicHash> +#include <QtCore/QTimer> +#include <QtCore/QUuid> + +#include <chrono> + +using namespace Quotient; +using namespace std::chrono; + +const QStringList supportedMethods = { SasV1Method }; + +QStringList commonSupportedMethods(const QStringList& remoteMethods) +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (supportedMethods.contains(method)) { + result += method; + } + } + return result; +} + +KeyVerificationSession::KeyVerificationSession( + QString remoteUserId, const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted) + : QObject(connection) + , m_remoteUserId(std::move(remoteUserId)) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + const auto& currentTime = QDateTime::currentDateTime(); + const auto timeoutTime = + std::min(event.timestamp().addSecs(600), currentTime.addSecs(120)); + const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; + if (timeout > 5s) + init(timeout); + // Otherwise don't even bother starting up +} + +KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, + Connection* connection) + : QObject(connection) + , m_remoteUserId(std::move(userId)) + , m_remoteDeviceId(std::move(deviceId)) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + init(600s); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init(milliseconds timeout) +{ + connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleReady(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationStart, this, [this](const KeyVerificationStartEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleStart(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationAccept, this, [this](const KeyVerificationAcceptEvent& event) { + if (event.transactionId() == m_transactionId) { + handleAccept(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationKey, this, [this](const KeyVerificationKeyEvent& event) { + if (event.transactionId() == m_transactionId) { + handleKey(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationMac, this, [this](const KeyVerificationMacEvent& event) { + if (event.transactionId() == m_transactionId) { + handleMac(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationDone, this, [this](const KeyVerificationDoneEvent& event) { + if (event.transactionId() == m_transactionId) { + handleDone(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationCancel, this, [this](const KeyVerificationCancelEvent& event) { + if (event.transactionId() == m_transactionId) { + handleCancel(event); + } + }); + + QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); + + m_sas = olm_sas(new std::byte[olm_sas_size()]); + auto randomSize = olm_create_sas_random_length(m_sas); + auto random = getRandom(randomSize); + olm_create_sas(m_sas, random.data(), randomSize); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + olm_clear_sas(m_sas); + delete[] reinterpret_cast<std::byte*>(m_sas); +} + +struct EmojiStoreEntry : EmojiEntry { + QHash<QString, QString> translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson<QString>(json["emoji"]), + fromJson<QString>(json["description"]) } + , translatedDescriptions{ fromJson<QHash<QString, QString>>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector<EmojiStoreEntry>; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson<EmojiStore>( + QJsonDocument::fromJson(dataFile.readAll()).array()); +} + +EmojiEntry emojiForCode(int code, const QString& language) +{ + static const EmojiStore emojiStore = loadEmojiStore(); + const auto& entry = emojiStore[code]; + if (!language.isEmpty()) + if (const auto translatedDescription = + emojiStore[code].translatedDescriptions.value(language); + !translatedDescription.isNull()) + return { entry.emoji, translatedDescription }; + + return SLICE(entry, EmojiEntry); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); + + if (startSentByUs) { + const auto paddedCommitment = + QCryptographicHash::hash((eventKey % m_startEvent).toLatin1(), + QCryptographicHash::Sha256) + .toBase64(); + const QLatin1String unpaddedCommitment(paddedCommitment.constData(), + paddedCommitment.indexOf('=')); + if (unpaddedCommitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + setState(WAITINGFORVERIFICATION); + + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array<std::byte, 6> output{}; + const auto infoTemplate = + startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls + : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + const auto info = infoTemplate + .arg(m_connection->userId(), m_connection->deviceId(), + key.data(), m_remoteUserId, m_remoteDeviceId, + eventKey, m_transactionId) + .toLatin1(); + olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), + output.size()); + + static constexpr auto x3f = std::byte{ 0x3f }; + const std::array<std::byte, 7> code{ + output[0] >> 2, + (output[0] << 4 & x3f) | output[1] >> 4, + (output[1] << 2 & x3f) | output[2] >> 6, + output[2] & x3f, + output[3] >> 2, + (output[3] << 4 & x3f) | output[4] >> 4, + (output[4] << 2 & x3f) | output[5] >> 6 + }; + + const auto uiLanguages = QLocale().uiLanguages(); + const auto preferredLanguage = uiLanguages.isEmpty() + ? QString() + : uiLanguages.front().section('-', 0, 0); + for (const auto& c : code) + m_sasEmojis += emojiForCode(std::to_integer<int>(c), preferredLanguage); + + emit sasEmojisChanged(); + emit keyReceived(); +} + +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + const auto macInfo = + (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls + : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls) + .arg(m_connection->userId(), m_connection->deviceId(), + m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId) + .toLatin1(); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), + macInfo.data(), macInfo.size(), outputBytes.data(), + outputBytes.size()); + return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); +} + +void KeyVerificationSession::sendDone() +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationDoneEvent(m_transactionId), + m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); + m_startEvent = QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + std::move(event), m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + if (state() != WAITINGFORREADY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (state() != READY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleAccept(const KeyVerificationAcceptEvent& event) +{ + if(state() != WAITINGFORACCEPT) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_connection->database()->setSessionVerified(edKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent&) +{ + if (state() != DONE) { + cancelVerification(UNEXPECTED_MESSAGE); + } +} + +void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) +{ + setError(stringToError(event.code())); + setState(CANCELED); +} + +QVector<EmojiEntry> KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h new file mode 100644 index 00000000..aa0295cb --- /dev/null +++ b/lib/keyverificationsession.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/keyverificationevent.h" + +#include <QtCore/QObject> + +struct OlmSAS; + +namespace Quotient { +class Connection; + +struct QUOTIENT_API EmojiEntry { + QString emoji; + QString description; + + Q_GADGET + Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) + Q_PROPERTY(QString description MEMBER description CONSTANT) +}; + +/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. + Start a new session using Connection::startKeyVerificationSession. + The object is delete after finished is emitted. +*/ +class QUOTIENT_API KeyVerificationSession : public QObject +{ + Q_OBJECT + +public: + enum State { + INCOMING, ///< There is a request for verification incoming + //! We sent a request for verification and are waiting for ready + WAITINGFORREADY, + //! Either party sent a ready as a response to a request; the user + //! selects a method + READY, + WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept + ACCEPTED, ///< The other party sent an accept and is waiting for a key + WAITINGFORKEY, ///< We're waiting for a key + //! We're waiting for the *user* to verify the emojis + WAITINGFORVERIFICATION, + WAITINGFORMAC, ///< We're waiting for the mac + CANCELED, ///< The session has been canceled + DONE, ///< The verification is done + }; + Q_ENUM(State) + + enum Error { + NONE, + TIMEOUT, + REMOTE_TIMEOUT, + USER, + REMOTE_USER, + UNEXPECTED_MESSAGE, + REMOTE_UNEXPECTED_MESSAGE, + UNKNOWN_TRANSACTION, + REMOTE_UNKNOWN_TRANSACTION, + UNKNOWN_METHOD, + REMOTE_UNKNOWN_METHOD, + KEY_MISMATCH, + REMOTE_KEY_MISMATCH, + USER_MISMATCH, + REMOTE_USER_MISMATCH, + INVALID_MESSAGE, + REMOTE_INVALID_MESSAGE, + SESSION_ACCEPTED, + REMOTE_SESSION_ACCEPTED, + MISMATCHED_COMMITMENT, + REMOTE_MISMATCHED_COMMITMENT, + MISMATCHED_SAS, + REMOTE_MISMATCHED_SAS, + }; + Q_ENUM(Error) + + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + Q_PROPERTY(QVector<EmojiEntry> sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + + KeyVerificationSession(QString remoteUserId, + const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted); + KeyVerificationSession(QString userId, QString deviceId, + Connection* connection); + ~KeyVerificationSession() override; + Q_DISABLE_COPY_MOVE(KeyVerificationSession) + + QVector<EmojiEntry> sasEmojis() const; + State state() const; + + Error error() const; + +public Q_SLOTS: + void sendRequest(); + void sendReady(); + void sendMac(); + void sendStartSas(); + void sendKey(); + void sendDone(); + void cancelVerification(Error error); + +Q_SIGNALS: + void startReceived(); + void keyReceived(); + void sasEmojisChanged(); + void stateChanged(); + void errorChanged(); + void finished(); + +private: + const QString m_remoteUserId; + const QString m_remoteDeviceId; + const QString m_transactionId; + Connection* m_connection; + OlmSAS* m_sas = nullptr; + QVector<EmojiEntry> m_sasEmojis; + bool startSentByUs = false; + State m_state = INCOMING; + Error m_error = NONE; + QString m_startEvent; + QString m_commitment; + bool macReceived = false; + bool m_encrypted; + QStringList m_remoteSupportedMethods; + + void handleReady(const KeyVerificationReadyEvent& event); + void handleStart(const KeyVerificationStartEvent& event); + void handleAccept(const KeyVerificationAcceptEvent& event); + void handleKey(const KeyVerificationKeyEvent& event); + void handleMac(const KeyVerificationMacEvent& event); + void handleDone(const KeyVerificationDoneEvent&); + void handleCancel(const KeyVerificationCancelEvent& event); + void init(std::chrono::milliseconds timeout); + void setState(State state); + void setError(Error error); + static QString errorToString(Error error); + static Error stringToError(const QString& error); + + QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); + QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::EmojiEntry) diff --git a/lib/room.cpp b/lib/room.cpp index 007446dc..ebc6e6af 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -70,7 +70,6 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" -#include "e2ee/qolmoutboundsession.h" #include "e2ee/qolmutility.h" #include "database.h" #endif // Quotient_E2EE_ENABLED |