aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp157
-rw-r--r--lib/connection.h32
-rw-r--r--lib/database.cpp31
-rw-r--r--lib/database.h11
-rw-r--r--lib/events/keyverificationevent.h143
-rw-r--r--lib/events/roomevent.h2
-rw-r--r--lib/events/roomkeyevent.cpp21
-rw-r--r--lib/events/roomkeyevent.h3
-rw-r--r--lib/keyverificationsession.cpp512
-rw-r--r--lib/keyverificationsession.h147
-rw-r--r--lib/room.cpp1
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