aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp153
-rw-r--r--lib/connection.h2
-rw-r--r--lib/encryptionmanager.cpp177
-rw-r--r--lib/encryptionmanager.h9
-rw-r--r--lib/events/encryptedevent.cpp2
-rw-r--r--lib/events/roomkeyevent.cpp11
-rw-r--r--lib/events/roomkeyevent.h25
-rw-r--r--lib/events/roommemberevent.cpp20
-rw-r--r--lib/events/roommemberevent.h3
-rw-r--r--lib/room.cpp250
-rw-r--r--lib/room.h10
-rw-r--r--lib/syncdata.cpp7
-rw-r--r--lib/syncdata.h5
13 files changed, 540 insertions, 134 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 5bddbb83..6ad24fba 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -19,7 +19,9 @@
#include "connection.h"
#include "connectiondata.h"
+#ifdef Quotient_E2EE_ENABLED
#include "encryptionmanager.h"
+#endif // Quotient_E2EE_ENABLED
#include "room.h"
#include "settings.h"
#include "user.h"
@@ -43,6 +45,10 @@
#include "jobs/mediathumbnailjob.h"
#include "jobs/syncjob.h"
+#ifdef Quotient_E2EE_ENABLED
+#include "account.h" // QtOlm
+#endif // Quotient_E2EE_ENABLED
+
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QElapsedTimer>
@@ -105,7 +111,9 @@ public:
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
+#ifdef Quotient_E2EE_ENABLED
QScopedPointer<EncryptionManager> encryptionManager;
+#endif // Quotient_E2EE_ENABLED
SyncJob* syncJob = nullptr;
@@ -148,6 +156,70 @@ public:
{
return q->stateCacheDir().filePath("state.json");
}
+
+ RoomEventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
+ {
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+ return {};
+#else // Quotient_E2EE_ENABLED
+ if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey)
+ {
+ return {};
+ }
+ QString identityKey =
+ encryptionManager->account()->curve25519IdentityKey();
+ QJsonObject personalCipherObject =
+ encryptedEvent.ciphertext(identityKey);
+ if (personalCipherObject.isEmpty()) {
+ qCDebug(E2EE) << "Encrypted event is not for the current device";
+ return {};
+ }
+ QString decrypted = encryptionManager->sessionDecryptMessage(
+ personalCipherObject, encryptedEvent.senderKey().toLatin1());
+ if (decrypted.isEmpty()) {
+ qCDebug(E2EE) << "Problem with new session from senderKey:"
+ << encryptedEvent.senderKey()
+ << encryptionManager->account()->oneTimeKeys();
+ return {};
+ }
+
+ RoomEventPtr decryptedEvent = makeEvent<RoomMessageEvent>(
+ QJsonDocument::fromJson(decrypted.toUtf8()).object());
+
+ if (decryptedEvent->senderId() != encryptedEvent.senderId()) {
+ qCDebug(E2EE) << "Found user" << decryptedEvent->senderId()
+ << "instead of sender" << encryptedEvent.senderId()
+ << "in Olm plaintext";
+ return {};
+ }
+
+ // TODO: keys to constants
+ QJsonObject decryptedEventObject = decryptedEvent->fullJson();
+ QString recipient =
+ decryptedEventObject.value("recipient"_ls).toString();
+ if (recipient != data->userId()) {
+ qCDebug(E2EE) << "Found user" << recipient << "instead of us"
+ << data->userId() << "in Olm plaintext";
+ return {};
+ }
+ QString ourKey = decryptedEventObject.value("recipient_keys"_ls)
+ .toObject()
+ .value(Ed25519Key)
+ .toString();
+ if (ourKey
+ != QString::fromUtf8(
+ encryptionManager->account()->ed25519IdentityKey())) {
+ qCDebug(E2EE) << "Found key" << ourKey
+ << "instead of ours own ed25519 key"
+ << encryptionManager->account()->ed25519IdentityKey()
+ << "in Olm plaintext";
+ return {};
+ }
+
+ return decryptedEvent;
+#endif // Quotient_E2EE_ENABLED
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
@@ -243,17 +315,12 @@ void Connection::doConnectToServer(const QString& user, const QString& password,
connect(loginJob, &BaseJob::success, this, [this, loginJob] {
d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
loginJob->deviceId());
-
- AccountSettings accountSettings(loginJob->userId());
- d->encryptionManager.reset(
- new EncryptionManager(accountSettings.encryptionAccountPickle()));
- if (accountSettings.encryptionAccountPickle().isEmpty()) {
- accountSettings.setEncryptionAccountPickle(
- d->encryptionManager->olmAccountPickle());
- }
-
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+#else // Quotient_E2EE_ENABLED
d->encryptionManager->uploadIdentityKeys(this);
d->encryptionManager->uploadOneTimeKeys(this);
+#endif // Quotient_E2EE_ENABLED
});
connect(loginJob, &BaseJob::failure, this, [this, loginJob] {
emit loginError(loginJob->errorString(), loginJob->rawDataSample());
@@ -309,6 +376,17 @@ void Connection::Private::connectWithToken(const QString& userId,
q->setObjectName(userId % '/' % deviceId);
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
<< "by user" << userId << "from device" << deviceId;
+ AccountSettings accountSettings(userId);
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+#else // Quotient_E2EE_ENABLED
+ encryptionManager.reset(
+ new EncryptionManager(accountSettings.encryptionAccountPickle()));
+ if (accountSettings.encryptionAccountPickle().isEmpty()) {
+ accountSettings.setEncryptionAccountPickle(
+ encryptionManager->olmAccountPickle());
+ }
+#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
emit q->connected();
q->reloadCapabilities();
@@ -535,6 +613,61 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
d->dcLocalAdditions.clear();
d->dcLocalRemovals.clear();
}
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+#else // Quotient_E2EE_ENABLED
+ // handling m.room_key to-device encrypted event
+ for (auto&& toDeviceEvent : data.takeToDeviceEvents()) {
+ if (toDeviceEvent->type() == EncryptedEvent::typeId()) {
+ event_ptr_tt<EncryptedEvent> encryptedEvent =
+ makeEvent<EncryptedEvent>(toDeviceEvent->fullJson());
+ if (encryptedEvent->algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
+ qCDebug(E2EE)
+ << "Encrypted event" << encryptedEvent->id() << "algorithm"
+ << encryptedEvent->algorithm() << "is not supported";
+ return;
+ }
+
+ // TODO: full maintaining of the device keys
+ // with device_lists sync extention and /keys/query
+ qCDebug(E2EE) << "Getting device keys for the m.room_key sender:"
+ << encryptedEvent->senderId();
+ // d->encryptionManager->updateDeviceKeys();
+
+ RoomEventPtr decryptedEvent =
+ d->sessionDecryptMessage(*encryptedEvent.get());
+ // since we are waiting for the RoomKeyEvent:
+ event_ptr_tt<RoomKeyEvent> roomKeyEvent =
+ makeEvent<RoomKeyEvent>(decryptedEvent->fullJson());
+ if (!roomKeyEvent) {
+ qCDebug(E2EE) << "Failed to decrypt olm event from user"
+ << encryptedEvent->senderId();
+ return;
+ }
+ Room* detectedRoom = room(roomKeyEvent->roomId());
+ if (!detectedRoom) {
+ qCDebug(E2EE)
+ << "Encrypted event room id" << encryptedEvent->roomId()
+ << "is not found at the connection";
+ return;
+ }
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent.get(),
+ encryptedEvent->senderKey());
+ }
+ }
+ // handling device_one_time_keys_count
+ auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount();
+ if (!d->encryptionManager)
+ {
+ qCDebug(E2EE) << "Encryption manager is not there yet";
+ return;
+ }
+ if (!deviceOneTimeKeysCount.isEmpty())
+ {
+ d->encryptionManager->updateOneTimeKeyCounts(this,
+ deviceOneTimeKeysCount);
+ }
+#endif // Quotient_E2EE_ENABLED
}
void Connection::stopSync()
@@ -958,10 +1091,12 @@ QString Connection::deviceId() const { return d->data->deviceId(); }
QByteArray Connection::accessToken() const { return d->data->accessToken(); }
+#ifdef Quotient_E2EE_ENABLED
QtOlm::Account* Connection::olmAccount() const
{
return d->encryptionManager->account();
}
+#endif // Quotient_E2EE_ENABLED
SyncJob* Connection::syncJob() const { return d->syncJob; }
diff --git a/lib/connection.h b/lib/connection.h
index e4109fd4..b57f0ca8 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -304,7 +304,9 @@ public:
QString userId() const;
QString deviceId() const;
QByteArray accessToken() const;
+#ifdef Quotient_E2EE_ENABLED
QtOlm::Account* olmAccount() const;
+#endif // Quotient_E2EE_ENABLED
Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
index 22387cf9..0895fae9 100644
--- a/lib/encryptionmanager.cpp
+++ b/lib/encryptionmanager.cpp
@@ -1,3 +1,4 @@
+#ifdef Quotient_E2EE_ENABLED
#include "encryptionmanager.h"
#include "connection.h"
@@ -9,6 +10,10 @@
#include <QtCore/QStringBuilder>
#include <account.h> // QtOlm
+#include <session.h> // QtOlm
+#include <message.h> // QtOlm
+#include <errors.h> // QtOlm
+#include <utils.h> // QtOlm
#include <functional>
#include <memory>
@@ -20,7 +25,8 @@ class EncryptionManager::Private {
public:
explicit Private(const QByteArray& encryptionAccountPickle,
float signedKeysProportion, float oneTimeKeyThreshold)
- : signedKeysProportion(move(signedKeysProportion))
+ : q(nullptr)
+ , signedKeysProportion(move(signedKeysProportion))
, oneTimeKeyThreshold(move(oneTimeKeyThreshold))
{
Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1));
@@ -44,18 +50,23 @@ public:
* until the limit is reached and it starts discarding keys, starting by
* the oldest.
*/
- targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below
+ targetKeysNumber = olmAccount->maxOneTimeKeys() / 2;
targetOneTimeKeyCounts = {
{ SignedCurve25519Key,
qRound(signedKeysProportion * targetKeysNumber) },
{ Curve25519Key,
qRound((1 - signedKeysProportion) * targetKeysNumber) }
};
+ updateKeysToUpload();
}
~Private() = default;
+ EncryptionManager* q;
+
UploadKeysJob* uploadIdentityKeysJob = nullptr;
+ UploadKeysJob* uploadOneTimeKeysInitJob = nullptr;
UploadKeysJob* uploadOneTimeKeysJob = nullptr;
+ QueryKeysJob* queryKeysJob = nullptr;
QScopedPointer<Account> olmAccount;
@@ -74,6 +85,95 @@ public:
}
QHash<QString, int> oneTimeKeysToUploadCounts;
QHash<QString, int> targetOneTimeKeyCounts;
+
+ // A map from senderKey to InboundSession
+ QMap<QString, InboundSession*> sessions; // TODO: cache
+ void updateDeviceKeys(
+ const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>&
+ deviceKeys)
+ {
+ for (auto userId : deviceKeys.keys()) {
+ for (auto deviceId : deviceKeys.value(userId).keys()) {
+ QueryKeysJob::DeviceInformation info =
+ deviceKeys.value(userId).value(deviceId);
+ // TODO: ed25519Verify, etc
+ }
+ }
+ }
+ QString sessionDecrypt(Message* message, const QString& senderKey)
+ {
+ QString decrypted;
+ QList<InboundSession*> senderSessions = sessions.values(senderKey);
+ // Try to decrypt message body using one of the known sessions for that
+ // device
+ bool sessionsPassed = false;
+ for (auto senderSession : senderSessions) {
+ if (senderSession == senderSessions.last()) {
+ sessionsPassed = true;
+ }
+ try {
+ decrypted = senderSession->decrypt(message);
+ qCDebug(E2EE)
+ << "Success decrypting Olm event using existing session"
+ << senderSession->id();
+ break;
+ } catch (OlmError* e) {
+ if (message->messageType() == 0) {
+ PreKeyMessage preKeyMessage =
+ PreKeyMessage(message->cipherText());
+ if (senderSession->matches(&preKeyMessage, senderKey)) {
+ // We had a matching session for a pre-key message, but
+ // it didn't work. This means something is wrong, so we
+ // fail now.
+ qCDebug(E2EE)
+ << "Error decrypting pre-key message with existing "
+ "Olm session"
+ << senderSession->id() << "reason:" << e->what();
+ return QString();
+ }
+ }
+ // Simply keep trying otherwise
+ }
+ }
+ if (sessionsPassed || senderSessions.empty()) {
+ if (message->messageType() > 0) {
+ // Not a pre-key message, we should have had a matching session
+ if (!sessions.empty()) {
+ qCDebug(E2EE) << "Error decrypting with existing sessions";
+ return QString();
+ }
+ qCDebug(E2EE) << "No existing sessions";
+ return QString();
+ }
+ // We have a pre-key message without any matching session, in this
+ // case we should try to create one.
+ InboundSession* newSession;
+ qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey;
+ PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText());
+ try {
+ newSession = new InboundSession(olmAccount.data(),
+ &preKeyMessage,
+ senderKey.toLatin1(), q);
+ } catch (OlmError* e) {
+ qCDebug(E2EE) << "Error decrypting pre-key message when trying "
+ "to establish a new session:"
+ << e->what();
+ return QString();
+ }
+ qCDebug(E2EE) << "Created new Olm session" << newSession->id();
+ try {
+ decrypted = newSession->decrypt(message);
+ } catch (OlmError* e) {
+ qCDebug(E2EE)
+ << "Error decrypting pre-key message with new session"
+ << e->what();
+ return QString();
+ }
+ olmAccount->removeOneTimeKeys(newSession);
+ sessions.insert(senderKey, newSession);
+ }
+ return decrypted;
+ }
};
EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
@@ -83,7 +183,9 @@ EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
, d(std::make_unique<Private>(std::move(encryptionAccountPickle),
std::move(signedKeysProportion),
std::move(oneTimeKeyThreshold)))
-{}
+{
+ d->q = this;
+}
EncryptionManager::~EncryptionManager() = default;
@@ -132,20 +234,19 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection)
d->olmAccount->sign(deviceKeysJsonObject) } } }
};
+ d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] {
d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
- qDebug() << QString("Uploaded identity keys.");
});
- d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
}
void EncryptionManager::uploadOneTimeKeys(Connection* connection,
bool forceUpdate)
{
if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
- auto job = connection->callApi<UploadKeysJob>();
- connect(job, &BaseJob::success, this, [job, this] {
- d->setOneTimeKeyCounts(job->oneTimeKeyCounts());
+ d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
+ connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
+ d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
});
}
@@ -170,9 +271,17 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection,
if (oneTimeKeysCounter < signedKeysToUploadCount) {
QJsonObject message { { QStringLiteral("key"),
it.value().toString() } };
- key = d->olmAccount->sign(message);
- keyType = SignedCurve25519Key;
+ QByteArray signedMessage = d->olmAccount->sign(message);
+ QJsonObject signatures {
+ { connection->userId(),
+ QJsonObject { { Ed25519Key + QStringLiteral(":")
+ + connection->deviceId(),
+ QString::fromUtf8(signedMessage) } } }
+ };
+ message.insert(QStringLiteral("signatures"), signatures);
+ key = message;
+ keyType = SignedCurve25519Key;
} else {
key = it.value();
keyType = Curve25519Key;
@@ -180,13 +289,50 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection,
++oneTimeKeysCounter;
oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key);
}
-
- d->uploadOneTimeKeysJob = connection->callApi<UploadKeysJob>(none,
- oneTimeKeys);
+ d->uploadOneTimeKeysJob =
+ connection->callApi<UploadKeysJob>(none, oneTimeKeys);
+ connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] {
+ d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts());
+ });
d->olmAccount->markKeysAsPublished();
- qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
+ qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
.arg(signedKeysToUploadCount)
- .arg(unsignedKeysToUploadCount);
+ .arg(unsignedKeysToUploadCount);
+}
+
+void EncryptionManager::updateOneTimeKeyCounts(
+ Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount)
+{
+ d->oneTimeKeyCounts = deviceOneTimeKeysCount;
+ if (d->oneTimeKeyShouldUpload()) {
+ qCDebug(E2EE) << "Uploading new one-time keys.";
+ uploadOneTimeKeys(connection);
+ }
+}
+
+void Quotient::EncryptionManager::updateDeviceKeys(
+ Connection* connection, const QHash<QString, QStringList>& deviceKeys)
+{
+ d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys);
+ connect(d->queryKeysJob, &BaseJob::success, this,
+ [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); });
+}
+
+QString EncryptionManager::sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
+{
+ QString decrypted;
+ int type = personalCipherObject.value(TypeKeyL).toInt(-1);
+ QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
+ if (type == 0) {
+ PreKeyMessage preKeyMessage { body };
+ decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage),
+ senderKey);
+ } else if (type == 1) {
+ Message message { body };
+ decrypted = d->sessionDecrypt(&message, senderKey);
+ }
+ return decrypted;
}
QByteArray EncryptionManager::olmAccountPickle()
@@ -221,3 +367,4 @@ bool EncryptionManager::Private::oneTimeKeyShouldUpload()
}
return false;
}
+#endif // Quotient_E2EE_ENABLED
diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h
index b210a85a..5df15e83 100644
--- a/lib/encryptionmanager.h
+++ b/lib/encryptionmanager.h
@@ -1,3 +1,4 @@
+#ifdef Quotient_E2EE_ENABLED
#pragma once
#include <QtCore/QObject>
@@ -26,6 +27,13 @@ public:
void uploadIdentityKeys(Connection* connection);
void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false);
+ void
+ updateOneTimeKeyCounts(Connection* connection,
+ const QHash<QString, int>& deviceOneTimeKeysCount);
+ void updateDeviceKeys(Connection* connection,
+ const QHash<QString, QStringList>& deviceKeys);
+ QString sessionDecryptMessage(const QJsonObject& personalCipherObject,
+ const QByteArray& senderKey);
QByteArray olmAccountPickle();
QtOlm::Account* account() const;
@@ -36,3 +44,4 @@ private:
};
} // namespace Quotient
+#endif // Quotient_E2EE_ENABLED
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
index b5cedc69..dccfa540 100644
--- a/lib/events/encryptedevent.cpp
+++ b/lib/events/encryptedevent.cpp
@@ -28,5 +28,5 @@ EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey,
EncryptedEvent::EncryptedEvent(const QJsonObject& obj)
: RoomEvent(typeId(), obj)
{
- qCDebug(EVENTS) << "Encrypted event" << id();
+ qCDebug(E2EE) << "Encrypted event from" << senderId();
}
diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp
new file mode 100644
index 00000000..1fb2e9f5
--- /dev/null
+++ b/lib/events/roomkeyevent.cpp
@@ -0,0 +1,11 @@
+#include "roomkeyevent.h"
+
+using namespace Quotient;
+
+RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj)
+{
+ _algorithm = contentJson()["algorithm"_ls].toString();
+ _roomId = contentJson()["room_id"_ls].toString();
+ _sessionId = contentJson()["session_id"_ls].toString();
+ _sessionKey = contentJson()["session_key"_ls].toString();
+}
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
new file mode 100644
index 00000000..e4bcfd71
--- /dev/null
+++ b/lib/events/roomkeyevent.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "event.h"
+
+namespace Quotient {
+class RoomKeyEvent : public Event
+{
+public:
+ DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent)
+
+ RoomKeyEvent(const QJsonObject& obj);
+
+ const QString algorithm() const { return _algorithm; }
+ const QString roomId() const { return _roomId; }
+ const QString sessionId() const { return _sessionId; }
+ const QString sessionKey() const { return _sessionKey; }
+
+private:
+ QString _algorithm;
+ QString _roomId;
+ QString _sessionId;
+ QString _sessionKey;
+};
+REGISTER_EVENT_TYPE(RoomKeyEvent)
+} // namespace Quotient
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index d4b2be45..35cbdb3a 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -79,6 +79,12 @@ bool RoomMemberEvent::isInvite() const
return membership() == MembershipType::Invite && changesMembership();
}
+bool RoomMemberEvent::isRejectedInvite() const
+{
+ return membership() == MembershipType::Leave && prevContent()
+ && prevContent()->membership == MembershipType::Invite;
+}
+
bool RoomMemberEvent::isJoin() const
{
return membership() == MembershipType::Join && changesMembership();
@@ -88,7 +94,19 @@ bool RoomMemberEvent::isLeave() const
{
return membership() == MembershipType::Leave && prevContent()
&& prevContent()->membership != membership()
- && prevContent()->membership != MembershipType::Ban;
+ && prevContent()->membership != MembershipType::Ban
+ && prevContent()->membership != MembershipType::Invite;
+}
+
+bool RoomMemberEvent::isBan() const
+{
+ return membership() == MembershipType::Ban && changesMembership();
+}
+
+bool RoomMemberEvent::isUnban() const
+{
+ return membership() == MembershipType::Leave && prevContent()
+ && prevContent()->membership == MembershipType::Ban;
}
bool RoomMemberEvent::isRename() const
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index 0ca439e1..783b8207 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -88,7 +88,10 @@ public:
QUrl avatarUrl() const { return content().avatarUrl; }
QString reason() const { return content().reason; }
bool changesMembership() const;
+ bool isBan() const;
+ bool isUnban() const;
bool isInvite() const;
+ bool isRejectedInvite() const;
bool isJoin() const;
bool isLeave() const;
bool isRename() const;
diff --git a/lib/room.cpp b/lib/room.cpp
index b29f6f48..2945111a 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -68,11 +68,15 @@
#include <array>
#include <cmath>
#include <functional>
+
+#ifdef Quotient_E2EE_ENABLED
+#include <account.h> // QtOlm
+#include <errors.h> // QtOlm
#include <groupsession.h> // QtOlm
-#include <message.h> // QtOlm
-#include <session.h> // QtOlm
+#endif // Quotient_E2EE_ENABLED
using namespace Quotient;
+using namespace QtOlm;
using namespace std::placeholders;
using std::move;
#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
@@ -340,6 +344,91 @@ public:
QJsonObject toJson() const;
+#ifdef Quotient_E2EE_ENABLED
+ // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
+ QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
+ groupSessionIndexRecord; // TODO: cache
+ // A map from senderKey to a map of sessionId to InboundGroupSession
+ // Not using QMultiHash, because we want to quickly return
+ // a number of relations for a given event without enumerating them.
+ QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO:
+ // cache
+ bool addInboundGroupSession(QString senderKey, QString sessionId,
+ QString sessionKey)
+ {
+ if (groupSessions.contains({ senderKey, sessionId })) {
+ qCDebug(E2EE) << "Inbound Megolm session" << sessionId
+ << "with senderKey" << senderKey << "already exists";
+ return false;
+ }
+
+ InboundGroupSession* megolmSession;
+ try {
+ megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
+ InboundGroupSession::Init,
+ q);
+ } catch (OlmError* e) {
+ qCDebug(E2EE) << "Unable to create new InboundGroupSession"
+ << e->what();
+ return false;
+ }
+ if (megolmSession->id() != sessionId) {
+ qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent "
+ "from sender with key"
+ << senderKey;
+ return false;
+ }
+ groupSessions.insert({ senderKey, sessionId }, megolmSession);
+ return true;
+ }
+
+ QString groupSessionDecryptMessage(QByteArray cipher,
+ const QString& senderKey,
+ const QString& sessionId,
+ const QString& eventId,
+ QDateTime timestamp)
+ {
+ std::pair<QString, uint32_t> decrypted;
+ QPair<QString, QString> senderSessionPairKey =
+ qMakePair(senderKey, sessionId);
+ if (!groupSessions.contains(senderSessionPairKey)) {
+ qCDebug(E2EE) << "Unable to decrypt event" << eventId
+ << "The sender's device has not sent us the keys for "
+ "this message";
+ return QString();
+ }
+ InboundGroupSession* senderSession =
+ groupSessions.value(senderSessionPairKey);
+ if (!senderSession) {
+ qCDebug(E2EE) << "Unable to decrypt event" << eventId
+ << "senderSessionPairKey:" << senderSessionPairKey;
+ return QString();
+ }
+ try {
+ decrypted = senderSession->decrypt(cipher);
+ } catch (OlmError* e) {
+ qCDebug(E2EE) << "Unable to decrypt event" << eventId
+ << "with matching megolm session:" << e->what();
+ return QString();
+ }
+ QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(
+ qMakePair(senderSession->id(), decrypted.second));
+ if (properties.first.isEmpty()) {
+ groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
+ decrypted.second),
+ qMakePair(eventId, timestamp));
+ } else {
+ if ((properties.first != eventId)
+ || (properties.second != timestamp)) {
+ qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
+ return QString();
+ }
+ }
+
+ return decrypted.first;
+ }
+#endif // Quotient_E2EE_ENABLED
+
private:
using users_shortlist_t = std::array<User*, 3>;
template <typename ContT>
@@ -364,7 +453,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
- qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id;
+ qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -1151,86 +1240,48 @@ const StateEventBase* Room::getCurrentState(const QString& evtType,
return d->getCurrentState({ evtType, stateKey });
}
-RoomEventPtr Room::decryptMessage(EncryptedEvent* encryptedEvent)
+RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
{
- if (encryptedEvent->algorithm() == OlmV1Curve25519AesSha2AlgoKey) {
- QString identityKey =
- connection()->olmAccount()->curve25519IdentityKey();
- QJsonObject personalCipherObject =
- encryptedEvent->ciphertext(identityKey);
- if (personalCipherObject.isEmpty()) {
- qCDebug(E2EE) << "Encrypted event is not for the current device";
+#ifndef Quotient_E2EE_ENABLED
+ Q_UNUSED(encryptedEvent);
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+ return {};
+#else // Quotient_E2EE_ENABLED
+ if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
+ QString decrypted = d->groupSessionDecryptMessage(
+ encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
+ encryptedEvent.sessionId(), encryptedEvent.id(),
+ encryptedEvent.timestamp());
+ if (decrypted.isEmpty()) {
return {};
}
- return makeEvent<RoomMessageEvent>(decryptMessage(
- personalCipherObject, encryptedEvent->senderKey().toLatin1()));
- }
- if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) {
- return makeEvent<RoomMessageEvent>(decryptMessage(
- encryptedEvent->ciphertext(), encryptedEvent->senderKey(),
- encryptedEvent->deviceId(), encryptedEvent->sessionId()));
+ return makeEvent<RoomMessageEvent>(
+ QJsonDocument::fromJson(decrypted.toUtf8()).object());
}
+ qCDebug(E2EE) << "Algorithm of the encrypted event with id"
+ << encryptedEvent.id() << "is not for the current device";
return {};
+#endif // Quotient_E2EE_ENABLED
}
-QString Room::decryptMessage(QJsonObject personalCipherObject,
- QByteArray senderKey)
+void Room::handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey)
{
- QString decrypted;
-
- using namespace QtOlm;
- // TODO: new objects to private fields:
- InboundSession* session;
-
- int type = personalCipherObject.value(TypeKeyL).toInt(-1);
- QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
-
- PreKeyMessage preKeyMessage { body };
- session =
- new InboundSession(connection()->olmAccount(), &preKeyMessage, senderKey, this);
- if (type == 0) {
- if (!session->matches(&preKeyMessage, senderKey)) {
- connection()->olmAccount()->removeOneTimeKeys(session);
- }
- try {
- decrypted = session->decrypt(&preKeyMessage);
- } catch (std::runtime_error& e) {
- qCWarning(EVENTS) << "Decrypt failed:" << e.what();
- }
+#ifndef Quotient_E2EE_ENABLED
+ Q_UNUSED(roomKeyEvent);
+ Q_UNUSED(senderKey);
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+ return;
+#else // Quotient_E2EE_ENABLED
+ if (roomKeyEvent->algorithm() != MegolmV1AesSha2AlgoKey) {
+ qCWarning(E2EE) << "Ignoring unsupported algorithm"
+ << roomKeyEvent->algorithm() << "in m.room_key event";
}
- else if (type == 1) {
- Message message { body };
- if (!session->matches(&preKeyMessage, senderKey)) {
- qCWarning(EVENTS) << "Invalid encrypted message";
- }
- try {
- decrypted = session->decrypt(&message);
- } catch (std::runtime_error& e) {
- qCWarning(EVENTS) << "Decrypt failed:" << e.what();
- }
+ if (d->addInboundGroupSession(senderKey, roomKeyEvent->sessionId(),
+ roomKeyEvent->sessionKey())) {
+ qCDebug(E2EE) << "added new inboundGroupSession:"
+ << d->groupSessions.count();
}
-
- return decrypted;
-}
-
-QString Room::sessionKey(const QString& senderKey, const QString& deviceId,
- const QString& sessionId) const
-{
- // TODO: handling an m.room_key event
- return "";
-}
-
-QString Room::decryptMessage(QByteArray cipher, const QString& senderKey,
- const QString& deviceId, const QString& sessionId)
-{
- QString decrypted;
- using namespace QtOlm;
- InboundGroupSession* groupSession;
- groupSession = new InboundGroupSession(
- sessionKey(senderKey, deviceId, sessionId).toLatin1());
- groupSession->decrypt(cipher);
- // TODO: avoid replay attacks
- return decrypted;
+#endif // Quotient_E2EE_ENABLED
}
int Room::joinedCount() const
@@ -1253,7 +1304,7 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
{
if (!summary.merge(newSummary))
return Change::NoChange;
- qCDebug(MAIN).nospace().noquote()
+ qCDebug(STATE).nospace().noquote()
<< "Updated room summary for " << q->objectName() << ": " << summary;
emit q->memberListChanged();
return Change::SummaryChange;
@@ -1425,18 +1476,15 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
d->unreadMessages = data.unreadCount;
- roomChanges |= Change::UnreadNotifsChange;
emit unreadMessagesChanged(this);
}
if (data.highlightCount != d->highlightCount) {
d->highlightCount = data.highlightCount;
- roomChanges |= Change::UnreadNotifsChange;
emit highlightCountChanged();
}
if (data.notificationCount != d->notificationCount) {
d->notificationCount = data.notificationCount;
- roomChanges |= Change::UnreadNotifsChange;
emit notificationCountChanged();
}
if (roomChanges != Change::NoChange) {
@@ -1737,10 +1785,11 @@ void Room::checkVersion()
// or the server capabilities have been loaded.
emit stabilityUpdated(defaultVersion, stableVersions);
if (!stableVersions.contains(version())) {
- qCDebug(MAIN) << this << "version is" << version()
- << "which the server doesn't count as stable";
+ qCDebug(STATE) << this << "version is" << version()
+ << "which the server doesn't count as stable";
if (canSwitchVersions())
- qCDebug(MAIN) << "The current user has enough privileges to fix it";
+ qCDebug(STATE)
+ << "The current user has enough privileges to fix it";
}
}
@@ -2047,7 +2096,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
if (currentState.value(evtKey) == oldEvent.get()) {
Q_ASSERT(ti.index() >= 0); // Historical states can't be in
// currentState
- qCDebug(EVENTS).nospace()
+ qCDebug(STATE).nospace()
<< "Redacting state " << oldEvent->matrixType() << "/"
<< oldEvent->stateKey();
// Retarget the current state to the newly made event.
@@ -2078,7 +2127,7 @@ RoomEventPtr makeReplaced(const RoomEvent& target,
const RoomMessageEvent& replacement)
{
auto originalJson = target.originalJsonObject();
- originalJson[ContentKeyL] = replacement.contentJson();
+ originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls);
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
auto relations = unsignedData.take("m.relations"_ls).toObject();
@@ -2101,15 +2150,15 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent)
auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
if (ti->replacedBy() == newEvent.id()) {
- qCDebug(EVENTS) << "Event" << ti->id() << "is already replaced with"
- << newEvent.id();
+ qCDebug(STATE) << "Event" << ti->id() << "is already replaced with"
+ << newEvent.id();
return true;
}
// Make a new event from the redacted JSON and put it in the timeline
// instead of the redacted one. oldEvent will be deleted on return.
auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent));
- qCDebug(EVENTS) << "Replaced" << oldEvent->id() << "with" << newEvent.id();
+ qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id();
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
}
@@ -2158,7 +2207,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
}); targetIt != events.end())
*targetIt = makeRedacted(**targetIt, *r);
else
- qCDebug(EVENTS)
+ qCDebug(STATE)
<< "Redaction" << r->id() << "ignored: target event"
<< r->redactedEvent() << "is not found";
// If the target event comes later, it comes already redacted.
@@ -2197,10 +2246,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
size_t totalInserted = 0;
for (auto it = events.begin(); it != events.end();) {
auto nextPendingPair =
- findFirstOf(it, events.end(), unsyncedEvents.begin(),
- unsyncedEvents.end(), isEchoEvent);
- const auto& remoteEcho = nextPendingPair.first;
- const auto& localEcho = nextPendingPair.second;
+ findFirstOf(it, events.end(), unsyncedEvents.begin(),
+ unsyncedEvents.end(), isEchoEvent);
+ const auto& remoteEcho = nextPendingPair.first;
+ const auto& localEcho = nextPendingPair.second;
if (it != remoteEcho) {
RoomEventsRange eventsSpan { it, remoteEcho };
@@ -2258,9 +2307,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
}
}
- qCDebug(MESSAGES) << "Room" << q->objectName() << "received"
- << totalInserted << "new events; the last event is now"
- << timeline.back();
+ qCDebug(STATE) << "Room" << q->objectName() << "received"
+ << totalInserted << "new events; the last event is now"
+ << timeline.back();
// The first event in the just-added batch (referred to by `from`)
// defines whose read marker can possibly be promoted any further over
@@ -2271,9 +2320,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
auto firstWriter = q->user((*from)->senderId());
if (q->readMarker(firstWriter) != timeline.crend()) {
roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
- qCDebug(MESSAGES)
- << "Auto-promoted read marker for" << firstWriter->id() << "to"
- << *q->readMarker(firstWriter);
+ qCDebug(STATE) << "Auto-promoted read marker for"
+ << firstWriter->id() << "to"
+ << *q->readMarker(firstWriter);
}
updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
@@ -2310,9 +2359,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto insertedSize = moveEventsToTimeline(events, Older);
const auto from = timeline.crend() - insertedSize;
- qCDebug(MESSAGES) << "Room" << displayname << "received" << insertedSize
- << "past events; the oldest event is now"
- << timeline.front();
+ qCDebug(STATE) << "Room" << displayname << "received" << insertedSize
+ << "past events; the oldest event is now" << timeline.front();
q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages(timeline.front().index(), from->index());
@@ -2407,7 +2455,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
break;
case MembershipType::Join:
if (evt.membership() == MembershipType::Invite)
- qCWarning(STATE) << "Invalid membership change from "
+ qCWarning(MAIN) << "Invalid membership change from "
"Join to Invite:"
<< evt;
if (evt.membership() != prevMembership) {
@@ -2569,7 +2617,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
emit accountDataAboutToChange(event->matrixType());
currentData = move(event);
qCDebug(STATE) << "Updated account data of type"
- << currentData->matrixType();
+ << currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
return Change::AccountDataChange;
}
diff --git a/lib/room.h b/lib/room.h
index ad19792e..d78a7bfc 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -26,6 +26,7 @@
#include "events/accountdataevents.h"
#include "events/encryptedevent.h"
+#include "events/roomkeyevent.h"
#include "events/roommessageevent.h"
#include "events/roomcreateevent.h"
#include "events/roomtombstoneevent.h"
@@ -208,13 +209,8 @@ public:
int memberCount() const;
int timelineSize() const;
bool usesEncryption() const;
- RoomEventPtr decryptMessage(EncryptedEvent* encryptedEvent);
- QString decryptMessage(QJsonObject personalCipherObject,
- QByteArray senderKey);
- QString sessionKey(const QString& senderKey, const QString& deviceId,
- const QString& sessionId) const;
- QString decryptMessage(QByteArray cipher, const QString& senderKey,
- const QString& deviceId, const QString& sessionId);
+ RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent);
+ void handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey);
int joinedCount() const;
int invitedCount() const;
int totalMemberCount() const;
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index 89c512a2..6e68e2cd 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -178,6 +178,13 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
accountData = load<Events>(json, "account_data"_ls);
toDeviceEvents = load<Events>(json, "to_device"_ls);
+ auto deviceOneTimeKeysCountVariantHash =
+ json.value("device_one_time_keys_count"_ls).toObject().toVariantHash();
+ for (auto key : deviceOneTimeKeysCountVariantHash.keys()) {
+ deviceOneTimeKeysCount_.insert(
+ key, deviceOneTimeKeysCountVariantHash.value(key).toInt());
+ }
+
auto rooms = json.value("rooms"_ls).toObject();
JoinStates::Int ii = 1; // ii is used to make a JoinState value
auto totalRooms = 0;
diff --git a/lib/syncdata.h b/lib/syncdata.h
index d55438d7..6e7183ee 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -92,6 +92,10 @@ public:
Events&& takePresenceData();
Events&& takeAccountData();
Events&& takeToDeviceEvents();
+ const QHash<QString, int>& deviceOneTimeKeysCount() const
+ {
+ return deviceOneTimeKeysCount_;
+ }
SyncDataList&& takeRoomData();
QString nextBatch() const { return nextBatch_; }
@@ -108,6 +112,7 @@ private:
Events toDeviceEvents;
SyncDataList roomData;
QStringList unresolvedRoomIds;
+ QHash<QString, int> deviceOneTimeKeysCount_;
static QJsonObject loadJson(const QString& fileName);
};