aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml2
-rw-r--r--CMakeLists.txt11
-rw-r--r--[-rwxr-xr-x]autotests/adjust-config.sh (renamed from .ci/adjust-config.sh)22
-rwxr-xr-xautotests/run-tests.sh28
-rw-r--r--autotests/testfilecrypto.cpp8
-rw-r--r--autotests/testolmaccount.cpp119
-rw-r--r--lib/accountregistry.cpp74
-rw-r--r--lib/accountregistry.h31
-rw-r--r--lib/connection.cpp315
-rw-r--r--lib/connection.h36
-rw-r--r--lib/converters.h1
-rw-r--r--lib/database.cpp21
-rw-r--r--lib/database.h42
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp4
-rw-r--r--lib/e2ee/qolmoutboundsession.h4
-rw-r--r--lib/e2ee/qolmsession.cpp5
-rw-r--r--lib/e2ee/qolmsession.h2
-rw-r--r--lib/eventitem.cpp21
-rw-r--r--lib/eventitem.h9
-rw-r--r--lib/events/encryptedfile.cpp119
-rw-r--r--lib/events/encryptedfile.h63
-rw-r--r--lib/events/eventcontent.cpp65
-rw-r--r--lib/events/eventcontent.h53
-rw-r--r--lib/events/filesourceinfo.cpp179
-rw-r--r--lib/events/filesourceinfo.h90
-rw-r--r--lib/events/roomavatarevent.h4
-rw-r--r--lib/events/roomkeyevent.h4
-rw-r--r--lib/events/roommessageevent.cpp8
-rw-r--r--lib/events/stickerevent.cpp2
-rw-r--r--lib/jobs/downloadfilejob.cpp56
-rw-r--r--lib/jobs/downloadfilejob.h5
-rw-r--r--lib/mxcreply.cpp12
-rw-r--r--lib/room.cpp221
-rw-r--r--lib/room.h3
-rw-r--r--quotest/quotest.cpp2
35 files changed, 961 insertions, 680 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9b9383db..dac67b3b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -171,7 +171,7 @@ jobs:
- name: Build and install QtKeychain
run: |
cd ..
- git clone https://github.com/frankosterfeld/qtkeychain.git
+ git clone -b v0.13.2 https://github.com/frankosterfeld/qtkeychain.git
cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS
cmake --build qtkeychain/build --target install
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ce950ea3..6f9ca9d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -87,6 +87,8 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModul
get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE)
message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}")
+find_package(${Qt}Keychain REQUIRED)
+
if (${PROJECT_NAME}_ENABLE_E2EE)
find_package(${Qt} ${QtMinVersion} REQUIRED Sql)
find_package(Olm 3.1.3 REQUIRED)
@@ -108,7 +110,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE)
if (OpenSSL_FOUND)
message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}")
endif()
- find_package(${Qt}Keychain REQUIRED)
endif()
@@ -168,7 +169,7 @@ list(APPEND lib_SRCS
lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp
lib/events/stickerevent.h lib/events/stickerevent.cpp
lib/events/keyverificationevent.h lib/events/keyverificationevent.cpp
- lib/events/encryptedfile.h lib/events/encryptedfile.cpp
+ lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp
lib/jobs/requestdata.h lib/jobs/requestdata.cpp
lib/jobs/basejob.h lib/jobs/basejob.cpp
lib/jobs/syncjob.h lib/jobs/syncjob.cpp
@@ -312,14 +313,14 @@ if (${PROJECT_NAME}_ENABLE_E2EE)
target_link_libraries(${PROJECT_NAME} Olm::Olm
OpenSSL::Crypto
OpenSSL::SSL
- ${Qt}::Sql
- ${QTKEYCHAIN_LIBRARIES})
+ ${Qt}::Sql)
set(FIND_DEPS "find_dependency(Olm)
find_dependency(OpenSSL)
find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in
endif()
-target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui)
+target_include_directories(${PROJECT_NAME} PRIVATE ${QTKEYCHAIN_INCLUDE_DIRS})
+target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES})
if (Qt STREQUAL Qt5) # See #483
target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia)
diff --git a/.ci/adjust-config.sh b/autotests/adjust-config.sh
index b2ca52b2..68ea58ab 100755..100644
--- a/.ci/adjust-config.sh
+++ b/autotests/adjust-config.sh
@@ -3,9 +3,9 @@
CMD=""
$CMD perl -pi -w -e \
- 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' data/homeserver.yaml
+ 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' homeserver.yaml
$CMD perl -pi -w -e \
- 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' data/homeserver.yaml
+ 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' homeserver.yaml
(
cat <<HEREDOC
@@ -36,18 +36,20 @@ rc_joins:
per_second: 10000
burst_count: 100000
HEREDOC
-) | $CMD tee -a data/homeserver.yaml
+) | $CMD tee -a homeserver.yaml
$CMD perl -pi -w -e \
- 's/#enable_registration: false/enable_registration: true/g;' data/homeserver.yaml
+ 's/^#enable_registration: false/enable_registration: true/g;' homeserver.yaml
$CMD perl -pi -w -e \
- 's/tls: false/tls: true/g;' data/homeserver.yaml
+ 's/^#enable_registration_without_verification: .+/enable_registration_without_verification: true/g;' homeserver.yaml
$CMD perl -pi -w -e \
- 's/#tls_certificate_path:/tls_certificate_path:/g;' data/homeserver.yaml
+ 's/tls: false/tls: true/g;' homeserver.yaml
$CMD perl -pi -w -e \
- 's/#tls_private_key_path:/tls_private_key_path:/g;' data/homeserver.yaml
+ 's/#tls_certificate_path:/tls_certificate_path:/g;' homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/#tls_private_key_path:/tls_private_key_path:/g;' homeserver.yaml
-$CMD openssl req -x509 -newkey rsa:4096 -keyout data/localhost.tls.key -out data/localhost.tls.crt -days 365 -subj '/CN=localhost' -nodes
+$CMD openssl req -x509 -newkey rsa:4096 -keyout localhost.tls.key -out localhost.tls.crt -days 365 -subj '/CN=localhost' -nodes
-$CMD chmod 0777 data/localhost.tls.crt
-$CMD chmod 0777 data/localhost.tls.key
+$CMD chmod 0777 localhost.tls.crt
+$CMD chmod 0777 localhost.tls.key
diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh
index 0d58e460..05a215af 100755
--- a/autotests/run-tests.sh
+++ b/autotests/run-tests.sh
@@ -1,34 +1,30 @@
mkdir -p data
chmod 0777 data
+
rm ~/.local/share/testolmaccount -rf
docker run -v `pwd`/data:/data --rm \
- -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate
-./.ci/adjust-config.sh
+ -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate
+(cd data && . ../autotests/adjust-config.sh)
docker run -d \
--name synapse \
-p 1234:8008 \
-p 8448:8008 \
-p 8008:8008 \
- -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0
+ -v `pwd`/data:/data matrixdotorg/synapse:latest
trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT
echo Waiting for synapse to start...
until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done
echo Register alice
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice1 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice2 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice3 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice4 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice5 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice6 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice7 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice8 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice9 -p secret -c /data/homeserver.yaml https://localhost:8008'
+for i in 1 2 3 4 5 6 7 8 9; do
+ docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u alice$i -p secret -c /data/homeserver.yaml https://localhost:8008"
+done
echo Register bob
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob1 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob2 -p secret -c /data/homeserver.yaml https://localhost:8008'
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob3 -p secret -c /data/homeserver.yaml https://localhost:8008'
+for i in 1 2 3; do
+ docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u bob$i -p secret -c /data/homeserver.yaml https://localhost:8008"
+done
echo Register carl
-docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008'
+docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008"
GTEST_COLOR=1 ctest --verbose "$@"
+
diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp
index f9212376..29521060 100644
--- a/autotests/testfilecrypto.cpp
+++ b/autotests/testfilecrypto.cpp
@@ -3,15 +3,17 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "testfilecrypto.h"
-#include "events/encryptedfile.h"
+
+#include "events/filesourceinfo.h"
+
#include <qtest.h>
using namespace Quotient;
void TestFileCrypto::encryptDecryptData()
{
QByteArray data = "ABCDEF";
- auto [file, cipherText] = EncryptedFile::encryptFile(data);
- auto decrypted = file.decryptFile(cipherText);
+ auto [file, cipherText] = encryptFile(data);
+ auto decrypted = decryptFile(cipherText, file);
// AES CTR produces ciphertext of the same size as the original
QCOMPARE(cipherText.size(), data.size());
QCOMPARE(decrypted.size(), data.size());
diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp
index c85718dd..60f4ab38 100644
--- a/autotests/testolmaccount.cpp
+++ b/autotests/testolmaccount.cpp
@@ -10,7 +10,7 @@
#include <e2ee/qolmaccount.h>
#include <e2ee/qolmutility.h>
#include <events/encryptionevent.h>
-#include <events/encryptedfile.h>
+#include <events/filesourceinfo.h>
#include <networkaccessmanager.h>
#include <room.h>
@@ -156,8 +156,7 @@ void TestOlmAccount::encryptedFile()
"sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA"
}})");
- EncryptedFile file;
- JsonObjectConverter<EncryptedFile>::fillFrom(doc.object(), file);
+ const auto file = fromJson<EncryptedFileMetadata>(doc);
QCOMPARE(file.v, "v2");
QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA");
@@ -201,10 +200,13 @@ void TestOlmAccount::uploadIdentityKey()
UnsignedOneTimeKeys unused;
auto request = olmAccount->createUploadKeyRequest(unused);
connect(request, &BaseJob::result, this, [request, conn] {
- QCOMPARE(request->oneTimeKeyCounts().size(), 0);
- });
- connect(request, &BaseJob::failure, this, [] {
- QFAIL("upload failed");
+ if (!request->status().good())
+ QFAIL("upload failed");
+ const auto& oneTimeKeyCounts = request->oneTimeKeyCounts();
+ // Allow the response to have entries with zero counts
+ QCOMPARE(std::accumulate(oneTimeKeyCounts.begin(),
+ oneTimeKeyCounts.end(), 0),
+ 0);
});
conn->run(request);
QSignalSpy spy3(request, &BaseJob::result);
@@ -228,12 +230,10 @@ void TestOlmAccount::uploadOneTimeKeys()
}
auto request = new UploadKeysJob(none, oneTimeKeysHash);
connect(request, &BaseJob::result, this, [request, conn] {
- QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ if (!request->status().good())
+ QFAIL("upload failed");
QCOMPARE(request->oneTimeKeyCounts().value(Curve25519Key), 5);
});
- connect(request, &BaseJob::failure, this, [] {
- QFAIL("upload failed");
- });
conn->run(request);
QSignalSpy spy3(request, &BaseJob::result);
QVERIFY(spy3.wait(10000));
@@ -254,12 +254,10 @@ void TestOlmAccount::uploadSignedOneTimeKeys()
}
auto request = new UploadKeysJob(none, oneTimeKeysHash);
connect(request, &BaseJob::result, this, [request, nKeys, conn] {
- QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ if (!request->status().good())
+ QFAIL("upload failed");
QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), nKeys);
});
- connect(request, &BaseJob::failure, this, [] {
- QFAIL("upload failed");
- });
conn->run(request);
QSignalSpy spy3(request, &BaseJob::result);
QVERIFY(spy3.wait(10000));
@@ -274,12 +272,10 @@ void TestOlmAccount::uploadKeys()
auto otks = olmAccount->oneTimeKeys();
auto request = olmAccount->createUploadKeyRequest(otks);
connect(request, &BaseJob::result, this, [request, conn] {
- QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ if (!request->status().good())
+ QFAIL("upload failed");
QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
});
- connect(request, &BaseJob::failure, this, [] {
- QFAIL("upload failed");
- });
conn->run(request);
QSignalSpy spy3(request, &BaseJob::result);
QVERIFY(spy3.wait(10000));
@@ -295,7 +291,6 @@ void TestOlmAccount::queryTest()
aliceOlm->generateOneTimeKeys(1);
auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys());
connect(aliceRes, &BaseJob::result, this, [aliceRes] {
- QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1);
QCOMPARE(aliceRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
});
QSignalSpy spy(aliceRes, &BaseJob::result);
@@ -306,7 +301,6 @@ void TestOlmAccount::queryTest()
bobOlm->generateOneTimeKeys(1);
auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys());
connect(bobRes, &BaseJob::result, this, [bobRes] {
- QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1);
QCOMPARE(bobRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
});
QSignalSpy spy1(bobRes, &BaseJob::result);
@@ -366,7 +360,6 @@ void TestOlmAccount::claimKeys()
auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys());
connect(request, &BaseJob::result, this, [request, bob] {
- QCOMPARE(request->oneTimeKeyCounts().size(), 1);
QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
});
bob->run(request);
@@ -377,45 +370,47 @@ void TestOlmAccount::claimKeys()
// Alice retrieves bob's keys & claims one signed one-time key.
QHash<QString, QStringList> deviceKeys;
deviceKeys[bob->userId()] = QStringList();
- auto job = alice->callApi<QueryKeysJob>(deviceKeys);
- connect(job, &BaseJob::result, this, [bob, alice, job, this] {
- const auto& bobDevices = job->deviceKeys().value(bob->userId());
- QVERIFY(!bobDevices.empty());
-
- // Retrieve the identity key for the current device.
- const auto& bobEd25519 =
- bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()];
-
- const auto currentDevice = bobDevices[bob->deviceId()];
-
- // Verify signature.
- QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(),
- bob->userId()));
-
- QHash<QString, QHash<QString, QString>> oneTimeKeys;
- oneTimeKeys[bob->userId()] = QHash<QString, QString>();
- oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key;
-
- auto job = alice->callApi<ClaimKeysJob>(oneTimeKeys);
- connect(job, &BaseJob::result, this, [bob, bobEd25519, job] {
- const auto userId = bob->userId();
- const auto deviceId = bob->deviceId();
-
- // The device exists.
- QCOMPARE(job->oneTimeKeys().size(), 1);
- QCOMPARE(job->oneTimeKeys().value(userId).size(), 1);
-
- // The key is the one bob sent.
- const auto& oneTimeKey =
- job->oneTimeKeys().value(userId).value(deviceId);
- QVERIFY(std::any_of(oneTimeKey.constKeyValueBegin(),
- oneTimeKey.constKeyValueEnd(),
- [](const auto& kv) {
- return kv.first.startsWith(
- SignedCurve25519Key);
- }));
- });
+ auto queryKeysJob = alice->callApi<QueryKeysJob>(deviceKeys);
+ QSignalSpy requestSpy2(queryKeysJob, &BaseJob::result);
+ QVERIFY(requestSpy2.wait(10000));
+
+ const auto& bobDevices = queryKeysJob->deviceKeys().value(bob->userId());
+ QVERIFY(!bobDevices.empty());
+
+ const auto currentDevice = bobDevices[bob->deviceId()];
+
+ // Verify signature.
+ QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(),
+ bob->userId()));
+ // Retrieve the identity key for the current device.
+ const auto& bobEd25519 =
+ bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()];
+
+ QHash<QString, QHash<QString, QString>> oneTimeKeys;
+ oneTimeKeys[bob->userId()] = QHash<QString, QString>();
+ oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key;
+
+ auto claimKeysJob = alice->callApi<ClaimKeysJob>(oneTimeKeys);
+ connect(claimKeysJob, &BaseJob::result, this, [bob, bobEd25519, claimKeysJob] {
+ const auto userId = bob->userId();
+ const auto deviceId = bob->deviceId();
+
+ // The device exists.
+ QCOMPARE(claimKeysJob->oneTimeKeys().size(), 1);
+ QCOMPARE(claimKeysJob->oneTimeKeys().value(userId).size(), 1);
+
+ // The key is the one bob sent.
+ const auto& oneTimeKeys =
+ claimKeysJob->oneTimeKeys().value(userId).value(deviceId);
+ for (auto it = oneTimeKeys.begin(); it != oneTimeKeys.end(); ++it) {
+ if (it.key().startsWith(SignedCurve25519Key)
+ && it.value().isObject())
+ return;
+ }
+ QFAIL("The claimed one time key is not in /claim response");
});
+ QSignalSpy completionSpy(claimKeysJob, &BaseJob::result);
+ QVERIFY(completionSpy.wait(10000));
}
void TestOlmAccount::claimMultipleKeys()
@@ -430,7 +425,6 @@ void TestOlmAccount::claimMultipleKeys()
auto res = olm->createUploadKeyRequest(olm->oneTimeKeys());
QSignalSpy spy(res, &BaseJob::result);
connect(res, &BaseJob::result, this, [res] {
- QCOMPARE(res->oneTimeKeyCounts().size(), 1);
QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
});
alice->run(res);
@@ -441,7 +435,6 @@ void TestOlmAccount::claimMultipleKeys()
auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys());
QSignalSpy spy1(res1, &BaseJob::result);
connect(res1, &BaseJob::result, this, [res1] {
- QCOMPARE(res1->oneTimeKeyCounts().size(), 1);
QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
});
alice1->run(res1);
@@ -452,7 +445,6 @@ void TestOlmAccount::claimMultipleKeys()
auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys());
QSignalSpy spy2(res2, &BaseJob::result);
connect(res2, &BaseJob::result, this, [res2] {
- QCOMPARE(res2->oneTimeKeyCounts().size(), 1);
QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
});
alice2->run(res2);
@@ -476,7 +468,6 @@ void TestOlmAccount::claimMultipleKeys()
QVERIFY(jobSpy.wait(10000));
const auto userId = alice->userId();
- QCOMPARE(job->oneTimeKeys().size(), 1);
QCOMPARE(job->oneTimeKeys().value(userId).size(), 3);
}
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp
index 616b54b4..b3025fa4 100644
--- a/lib/accountregistry.cpp
+++ b/lib/accountregistry.cpp
@@ -5,6 +5,7 @@
#include "accountregistry.h"
#include "connection.h"
+#include <QtCore/QCoreApplication>
using namespace Quotient;
@@ -15,14 +16,16 @@ void AccountRegistry::add(Connection* a)
beginInsertRows(QModelIndex(), size(), size());
push_back(a);
endInsertRows();
+ emit accountCountChanged();
}
void AccountRegistry::drop(Connection* a)
{
- const auto idx = indexOf(a);
- beginRemoveRows(QModelIndex(), idx, idx);
- remove(idx);
- endRemoveRows();
+ if (const auto idx = indexOf(a); idx != -1) {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ remove(idx);
+ endRemoveRows();
+ }
Q_ASSERT(!contains(a));
}
@@ -54,8 +57,6 @@ QHash<int, QByteArray> AccountRegistry::roleNames() const
return { { AccountRole, "connection" } };
}
-
-
Connection* AccountRegistry::get(const QString& userId)
{
for (const auto &connection : *this) {
@@ -64,3 +65,64 @@ Connection* AccountRegistry::get(const QString& userId)
}
return nullptr;
}
+
+QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId)
+{
+ qCDebug(MAIN) << "Reading access token from keychain for" << userId;
+ auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
+ job->setKey(userId);
+ job->start();
+
+ return job;
+}
+
+void AccountRegistry::invokeLogin()
+{
+ const auto accounts = SettingsGroup("Accounts").childGroups();
+ for (const auto& accountId : accounts) {
+ AccountSettings account { accountId };
+ m_accountsLoading += accountId;
+ emit accountsLoadingChanged();
+
+ if (account.homeserver().isEmpty())
+ continue;
+
+ auto accessTokenLoadingJob =
+ loadAccessTokenFromKeychain(account.userId());
+ connect(accessTokenLoadingJob, &QKeychain::Job::finished, this,
+ [accountId, this, accessTokenLoadingJob]() {
+ if (accessTokenLoadingJob->error()
+ != QKeychain::Error::NoError) {
+ emit keychainError(accessTokenLoadingJob->error());
+ return;
+ }
+
+ AccountSettings account { accountId };
+ auto connection = new Connection(account.homeserver());
+ connect(connection, &Connection::connected, this,
+ [connection] {
+ connection->loadState();
+ connection->setLazyLoading(true);
+
+ connection->syncLoop();
+ });
+ connect(connection, &Connection::loginError, this,
+ [this, connection](const QString& error,
+ const QString& details) {
+ emit loginError(connection, error, details);
+ });
+ connect(connection, &Connection::resolveError, this,
+ [this, connection](const QString& error) {
+ emit resolveError(connection, error);
+ });
+ connection->assumeIdentity(
+ account.userId(), accessTokenLoadingJob->binaryData(),
+ account.deviceId());
+ });
+ }
+}
+
+QStringList AccountRegistry::accountsLoading() const
+{
+ return m_accountsLoading;
+}
diff --git a/lib/accountregistry.h b/lib/accountregistry.h
index 2f6dffdf..38cfe6c6 100644
--- a/lib/accountregistry.h
+++ b/lib/accountregistry.h
@@ -5,15 +5,31 @@
#pragma once
#include "quotient_export.h"
+#include "settings.h"
#include <QtCore/QAbstractListModel>
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
+
+namespace QKeychain {
+class ReadPasswordJob;
+}
+
namespace Quotient {
class Connection;
class QUOTIENT_API AccountRegistry : public QAbstractListModel,
private QVector<Connection*> {
Q_OBJECT
+ /// Number of accounts that are currently fully loaded
+ Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged)
+ /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc).
+ /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded
+ Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged)
public:
using const_iterator = QVector::const_iterator;
using const_reference = QVector::const_reference;
@@ -52,6 +68,21 @@ public:
[[nodiscard]] int rowCount(
const QModelIndex& parent = QModelIndex()) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+ QStringList accountsLoading() const;
+
+ void invokeLogin();
+Q_SIGNALS:
+ void accountCountChanged();
+ void accountsLoadingChanged();
+
+ void keychainError(QKeychain::Error error);
+ void loginError(Connection* connection, QString message, QString details);
+ void resolveError(Connection* connection, QString error);
+
+private:
+ QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId);
+ QStringList m_accountsLoading;
};
inline QUOTIENT_API AccountRegistry Accounts {};
diff --git a/lib/connection.cpp b/lib/connection.cpp
index dba18cb1..102fb16d 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -39,14 +39,15 @@
# include "e2ee/qolmaccount.h"
# include "e2ee/qolminboundsession.h"
# include "e2ee/qolmsession.h"
+# include "e2ee/qolmutility.h"
# include "e2ee/qolmutils.h"
-# if QT_VERSION_MAJOR >= 6
-# include <qt6keychain/keychain.h>
-# else
-# include <qt5keychain/keychain.h>
-# endif
#endif // Quotient_E2EE_ENABLED
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
# include <QtCore/QCborValue>
@@ -62,7 +63,6 @@
#include <QtCore/QStringBuilder>
#include <QtNetwork/QDnsLookup>
-
using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
@@ -210,11 +210,11 @@ public:
#ifdef Quotient_E2EE_ENABLED
void loadSessions() {
- olmSessions = q->database()->loadOlmSessions(q->picklingMode());
+ olmSessions = q->database()->loadOlmSessions(picklingMode);
}
- void saveSession(QOlmSession& session, const QString& senderKey)
+ void saveSession(const QOlmSession& session, const QString& senderKey) const
{
- if (auto pickleResult = session.pickle(q->picklingMode()))
+ if (auto pickleResult = session.pickle(picklingMode))
q->database()->saveOlmSession(senderKey, session.sessionId(),
*pickleResult,
QDateTime::currentDateTime());
@@ -364,10 +364,56 @@ public:
#endif // Quotient_E2EE_ENABLED
}
#ifdef Quotient_E2EE_ENABLED
+ bool isKnownCurveKey(const QString& userId, const QString& curveKey) const;
+
void loadOutdatedUserDevices();
void saveDevicesList();
void loadDevicesList();
+
+ // This function assumes that an olm session with (user, device) exists
+ std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const;
+ bool createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const QJsonObject& oneTimeKeyObject);
+ QString curveKeyForUserDevice(const QString& userId,
+ const QString& device) const;
+ QString edKeyForUserDevice(const QString& userId,
+ const QString& device) const;
+ std::unique_ptr<EncryptedEvent> makeEventForSessionKey(
+ const QString& roomId, const QString& targetUserId,
+ const QString& targetDeviceId, const QByteArray& sessionId,
+ const QByteArray& sessionKey) const;
#endif
+
+ void saveAccessTokenToKeychain() const
+ {
+ qCDebug(MAIN) << "Saving access token to keychain for" << q->userId();
+ auto job = new QKeychain::WritePasswordJob(qAppName());
+ job->setAutoDelete(false);
+ job->setKey(q->userId());
+ job->setBinaryData(data->accessToken());
+ job->start();
+ //TODO error handling
+ }
+
+ void dropAccessToken()
+ {
+ qCDebug(MAIN) << "Removing access token from keychain for" << q->userId();
+ auto job = new QKeychain::DeletePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->start();
+
+ auto pickleJob = new QKeychain::DeletePasswordJob(qAppName());
+ pickleJob->setAutoDelete(true);
+ pickleJob->setKey(q->userId() + "-Pickle"_ls);
+ pickleJob->start();
+ //TODO error handling
+
+ data->setToken({});
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
@@ -546,11 +592,10 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
data->setToken(loginJob->accessToken().toLatin1());
data->setDeviceId(loginJob->deviceId());
completeSetup(loginJob->userId());
-#ifndef Quotient_E2EE_ENABLED
- qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
-#else // Quotient_E2EE_ENABLED
+ saveAccessTokenToKeychain();
+#ifdef Quotient_E2EE_ENABLED
database->clear();
-#endif // Quotient_E2EE_ENABLED
+#endif
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
@@ -607,9 +652,7 @@ void Connection::Private::completeSetup(const QString& mxId)
olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
-#ifdef Quotient_E2EE_ENABLED
loadSessions();
-#endif
if (database->accountPickle().isEmpty()) {
// create new account and save unpickle data
@@ -682,7 +725,8 @@ void Connection::logout()
|| d->logoutJob->error() == BaseJob::ContentAccessError) {
if (d->syncLoopConnection)
disconnect(d->syncLoopConnection);
- d->data->setToken({});
+ SettingsGroup("Accounts").remove(userId());
+ d->dropAccessToken();
emit loggedOut();
deleteLater();
} else { // logout() somehow didn't proceed - restore the session state
@@ -935,20 +979,23 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
{
#ifdef Quotient_E2EE_ENABLED
if (!toDeviceEvents.empty()) {
- qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events";
+ qCDebug(E2EE) << "Consuming" << toDeviceEvents.size()
+ << "to-device events";
visitEach(toDeviceEvents, [this](const EncryptedEvent& event) {
if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
- qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm();
+ qCDebug(E2EE) << "Unsupported algorithm" << event.id()
+ << "for event" << event.algorithm();
return;
}
- if (q->isKnownCurveKey(event.senderId(), event.senderKey())) {
+ if (isKnownCurveKey(event.senderId(), event.senderKey())) {
handleEncryptedToDeviceEvent(event);
return;
}
trackedUsers += event.senderId();
outdatedUsers += event.senderId();
encryptionUpdateRequired = true;
- pendingEncryptedEvents.push_back(std::make_unique<EncryptedEvent>(event.fullJson()));
+ pendingEncryptedEvents.push_back(
+ makeEvent<EncryptedEvent>(event.fullJson()));
});
}
#endif
@@ -1137,15 +1184,14 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
}
#ifdef Quotient_E2EE_ENABLED
-DownloadFileJob* Connection::downloadFile(const QUrl& url,
- const EncryptedFile& file,
- const QString& localFilename)
+DownloadFileJob* Connection::downloadFile(
+ const QUrl& url, const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename)
{
auto mediaId = url.authority() + url.path();
auto idParts = splitMediaId(mediaId);
- auto* job =
- callApi<DownloadFileJob>(idParts.front(), idParts.back(), file, localFilename);
- return job;
+ return callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ fileMetadata, localFilename);
}
#endif
@@ -1317,24 +1363,16 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
return forgetJob;
}
-SendToDeviceJob*
-Connection::sendToDevices(const QString& eventType,
- const UsersToDevicesToEvents& eventsMap)
+SendToDeviceJob* Connection::sendToDevices(
+ const QString& eventType, const UsersToDevicesToEvents& eventsMap)
{
QHash<QString, QHash<QString, QJsonObject>> json;
json.reserve(int(eventsMap.size()));
- std::for_each(eventsMap.begin(), eventsMap.end(),
- [&json](const auto& userTodevicesToEvents) {
- auto& jsonUser = json[userTodevicesToEvents.first];
- const auto& devicesToEvents = userTodevicesToEvents.second;
- std::for_each(devicesToEvents.begin(),
- devicesToEvents.end(),
- [&jsonUser](const auto& deviceToEvents) {
- jsonUser.insert(
- deviceToEvents.first,
- deviceToEvents.second->contentJson());
- });
- });
+ for (const auto& [userId, devicesToEvents] : eventsMap) {
+ auto& jsonUser = json[userId];
+ for (const auto& [deviceId, event] : devicesToEvents)
+ jsonUser.insert(deviceId, event->contentJson());
+ }
return callApi<SendToDeviceJob>(BackgroundRequest, eventType,
generateTxnId(), json);
}
@@ -2071,7 +2109,7 @@ void Connection::Private::loadOutdatedUserDevices()
saveDevicesList();
for(size_t i = 0; i < pendingEncryptedEvents.size();) {
- if (q->isKnownCurveKey(
+ if (isKnownCurveKey(
pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(),
pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) {
handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]);
@@ -2195,19 +2233,19 @@ QJsonObject Connection::decryptNotification(const QJsonObject &notification)
return decrypted ? decrypted->fullJson() : QJsonObject();
}
-Database* Connection::database()
+Database* Connection::database() const
{
return d->database;
}
UnorderedMap<QString, QOlmInboundGroupSessionPtr>
-Connection::loadRoomMegolmSessions(const Room* room)
+Connection::loadRoomMegolmSessions(const Room* room) const
{
return database()->loadMegolmSessions(room->id(), picklingMode());
}
void Connection::saveMegolmSession(const Room* room,
- const QOlmInboundGroupSession& session)
+ const QOlmInboundGroupSession& session) const
{
database()->saveMegolmSession(room->id(), session.sessionId(),
session.pickle(picklingMode()),
@@ -2219,64 +2257,193 @@ QStringList Connection::devicesForUser(const QString& userId) const
return d->deviceKeys[userId].keys();
}
-QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const
+QString Connection::Private::curveKeyForUserDevice(const QString& userId,
+ const QString& device) const
{
- return d->deviceKeys[user][device].keys["curve25519:" % device];
+ return deviceKeys[userId][device].keys["curve25519:" % device];
}
-QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const
+QString Connection::Private::edKeyForUserDevice(const QString& userId,
+ const QString& device) const
{
- return d->deviceKeys[user][device].keys["ed25519:" % device];
+ return deviceKeys[userId][device].keys["ed25519:" % device];
}
-bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey)
+bool Connection::Private::isKnownCurveKey(const QString& userId,
+ const QString& curveKey) const
{
- auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey"));
- query.bindValue(":matrixId", user);
+ auto query = database->prepareQuery(
+ QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId "
+ "AND curveKey=:curveKey"));
+ query.bindValue(":matrixId", userId);
query.bindValue(":curveKey", curveKey);
- database()->execute(query);
+ database->execute(query);
return query.next();
}
-bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const
+bool Connection::hasOlmSession(const QString& user,
+ const QString& deviceId) const
{
- const auto& curveKey = curveKeyForUserDevice(user, deviceId);
+ const auto& curveKey = d->curveKeyForUserDevice(user, deviceId);
return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty();
}
-QPair<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(const QString& user, const QString& device, const QByteArray& message)
+std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const
{
- const auto& curveKey = curveKeyForUserDevice(user, device);
- QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType();
- auto result = d->olmSessions[curveKey][0]->encrypt(message);
- auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode());
- if (pickle) {
- database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle);
+ const auto& curveKey = curveKeyForUserDevice(userId, device);
+ const auto& olmSession = olmSessions.at(curveKey).front();
+ QOlmMessage::Type type = olmSession->encryptMessageType();
+ const auto result = olmSession->encrypt(message);
+ if (const auto pickle = olmSession->pickle(picklingMode)) {
+ database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle);
} else {
- qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error();
+ qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error();
}
return { type, result.toCiphertext() };
}
-void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey)
-{
- auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey);
+bool Connection::Private::createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const QJsonObject& oneTimeKeyObject)
+{
+ static QOlmUtility verifier;
+ qDebug(E2EE) << "Creating a new session for" << targetUserId
+ << targetDeviceId;
+ if (oneTimeKeyObject.isEmpty()) {
+ qWarning(E2EE) << "No one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ auto signedOneTimeKey = oneTimeKeyObject.constBegin()->toObject();
+ // Verify contents of signedOneTimeKey - for that, drop `signatures` and
+ // `unsigned` and then verify the object against the respective signature
+ const auto signature =
+ signedOneTimeKey.take("signatures"_ls)[targetUserId]["ed25519:"_ls % targetDeviceId]
+ .toString()
+ .toLatin1();
+ signedOneTimeKey.remove("unsigned"_ls);
+ if (!verifier.ed25519Verify(
+ edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(),
+ QJsonDocument(signedOneTimeKey).toJson(QJsonDocument::Compact),
+ signature)) {
+ qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId
+ << targetDeviceId << ". Skipping this device.";
+ return false;
+ }
+ const auto recipientCurveKey =
+ curveKeyForUserDevice(targetUserId, targetDeviceId);
+ auto session =
+ QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey,
+ signedOneTimeKey["key"].toString());
if (!session) {
- qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error();
+ qCWarning(E2EE) << "Failed to create olm session for "
+ << recipientCurveKey << session.error();
+ return false;
+ }
+ saveSession(**session, recipientCurveKey);
+ olmSessions[recipientCurveKey].push_back(std::move(*session));
+ return true;
+}
+
+std::unique_ptr<EncryptedEvent> Connection::Private::makeEventForSessionKey(
+ const QString& roomId, const QString& targetUserId,
+ const QString& targetDeviceId, const QByteArray& sessionId,
+ const QByteArray& sessionKey) const
+{
+ // Noisy but nice for debugging
+ // qDebug(E2EE) << "Creating the payload for" << data->userId() << device <<
+ // sessionId << sessionKey.toHex();
+ const auto event = makeEvent<RoomKeyEvent>("m.megolm.v1.aes-sha2", roomId,
+ sessionId, sessionKey,
+ data->userId());
+ auto payloadJson = event->fullJson();
+ payloadJson.insert("recipient"_ls, targetUserId);
+ payloadJson.insert(SenderKeyL, data->userId());
+ payloadJson.insert("recipient_keys"_ls,
+ QJsonObject { { Ed25519Key,
+ edKeyForUserDevice(targetUserId,
+ targetDeviceId) } });
+ payloadJson.insert("keys"_ls,
+ QJsonObject {
+ { Ed25519Key,
+ QString(olmAccount->identityKeys().ed25519) } });
+ payloadJson.insert("sender_device"_ls, data->deviceId());
+
+ const auto [type, cipherText] = olmEncryptMessage(
+ targetUserId, targetDeviceId,
+ QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
+ QJsonObject encrypted {
+ { curveKeyForUserDevice(targetUserId, targetDeviceId),
+ QJsonObject { { "type"_ls, type },
+ { "body"_ls, QString(cipherText) } } }
+ };
+
+ return makeEvent<EncryptedEvent>(encrypted,
+ olmAccount->identityKeys().curve25519);
+}
+
+void Connection::sendSessionKeyToDevices(
+ const QString& roomId, const QByteArray& sessionId,
+ const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices,
+ int index)
+{
+ qDebug(E2EE) << "Sending room key to devices:" << sessionId
+ << sessionKey.toHex();
+ QHash<QString, QHash<QString, QString>> hash;
+ for (const auto& [userId, deviceId] : asKeyValueRange(devices))
+ if (!hasOlmSession(userId, deviceId)) {
+ hash[userId].insert(deviceId, "signed_curve25519"_ls);
+ qDebug(E2EE) << "Adding" << userId << deviceId
+ << "to keys to claim";
+ }
+
+ if (hash.isEmpty())
return;
- }
- d->saveSession(**session, theirIdentityKey);
- d->olmSessions[theirIdentityKey].push_back(std::move(*session));
+
+ auto job = callApi<ClaimKeysJob>(hash);
+ connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] {
+ UsersToDevicesToEvents usersToDevicesToEvents;
+ const auto oneTimeKeys = job->oneTimeKeys();
+ for (const auto& [targetUserId, targetDeviceId] :
+ asKeyValueRange(devices)) {
+ if (!hasOlmSession(targetUserId, targetDeviceId)
+ && !d->createOlmSession(
+ targetUserId, targetDeviceId,
+ oneTimeKeys[targetUserId][targetDeviceId]))
+ continue;
+
+ usersToDevicesToEvents[targetUserId][targetDeviceId] =
+ d->makeEventForSessionKey(roomId, targetUserId, targetDeviceId,
+ sessionId, sessionKey);
+ }
+ if (!usersToDevicesToEvents.empty()) {
+ sendToDevices(EncryptedEvent::TypeId, usersToDevicesToEvents);
+ QVector<std::tuple<QString, QString, QString>> receivedDevices;
+ receivedDevices.reserve(devices.size());
+ for (const auto& [user, device] : asKeyValueRange(devices))
+ receivedDevices.push_back(
+ { user, device, d->curveKeyForUserDevice(user, device) });
+
+ database()->setDevicesReceivedKey(roomId, receivedDevices,
+ sessionId, index);
+ }
+ });
}
-QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room)
+QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(
+ const QString& roomId) const
{
- return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode);
+ return d->database->loadCurrentOutboundMegolmSession(roomId,
+ d->picklingMode);
}
-void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data)
+void Connection::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const
{
- d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data);
+ d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode,
+ session);
}
#endif
diff --git a/lib/connection.h b/lib/connection.h
index f8744752..5b806350 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -51,7 +51,7 @@ class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
class Database;
-struct EncryptedFile;
+struct EncryptedFileMetadata;
class QOlmAccount;
class QOlmInboundGroupSession;
@@ -318,20 +318,27 @@ public:
bool isLoggedIn() const;
#ifdef Quotient_E2EE_ENABLED
QOlmAccount* olmAccount() const;
- Database* database();
+ Database* database() const;
+ PicklingMode picklingMode() const;
UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(
- const Room* room);
+ const Room* room) const;
void saveMegolmSession(const Room* room,
- const QOlmInboundGroupSession& session);
+ const QOlmInboundGroupSession& session) const;
bool hasOlmSession(const QString& user, const QString& deviceId) const;
- QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room);
- void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data);
+ QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(
+ const QString& roomId) const;
+ void saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const;
+ void sendSessionKeyToDevices(const QString& roomId,
+ const QByteArray& sessionId,
+ const QByteArray& sessionKey,
+ const QMultiHash<QString, QString>& devices,
+ int index);
- //This assumes that an olm session with (user, device) exists
- QPair<QOlmMessage::Type, QByteArray> olmEncryptMessage(const QString& userId, const QString& device, const QByteArray& message);
- void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey);
+ QJsonObject decryptNotification(const QJsonObject &notification);
+ QStringList devicesForUser(const QString& userId) const;
#endif // Quotient_E2EE_ENABLED
Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
@@ -601,7 +608,8 @@ public Q_SLOTS:
const QString& localFilename = {});
#ifdef Quotient_E2EE_ENABLED
- DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file,
+ DownloadFileJob* downloadFile(const QUrl& url,
+ const EncryptedFileMetadata& fileMetadata,
const QString& localFilename = {});
#endif
/**
@@ -691,14 +699,8 @@ public Q_SLOTS:
#ifdef Quotient_E2EE_ENABLED
void encryptionUpdate(Room *room);
- PicklingMode picklingMode() const;
- QJsonObject decryptNotification(const QJsonObject &notification);
-
- QStringList devicesForUser(const QString& user) const;
- QString curveKeyForUserDevice(const QString &user, const QString& device) const;
- QString edKeyForUserDevice(const QString& user, const QString& device) const;
- bool isKnownCurveKey(const QString& user, const QString& curveKey);
#endif
+
Q_SIGNALS:
/// \brief Initial server resolution has failed
///
diff --git a/lib/converters.h b/lib/converters.h
index 64a5cfb6..da30cae6 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -16,6 +16,7 @@
#include <type_traits>
#include <vector>
+#include <variant>
class QVariant;
diff --git a/lib/database.cpp b/lib/database.cpp
index 0119b35c..193ff54e 100644
--- a/lib/database.cpp
+++ b/lib/database.cpp
@@ -307,20 +307,22 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi
commit();
}
-void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session)
+void Database::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode,
+ const QOlmOutboundGroupSession& session)
{
- const auto pickle = session->pickle(picklingMode);
+ const auto pickle = session.pickle(picklingMode);
if (pickle) {
auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"));
deleteQuery.bindValue(":roomId", roomId);
- deleteQuery.bindValue(":sessionId", session->sessionId());
+ deleteQuery.bindValue(":sessionId", session.sessionId());
auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);"));
insertQuery.bindValue(":roomId", roomId);
- insertQuery.bindValue(":sessionId", session->sessionId());
+ insertQuery.bindValue(":sessionId", session.sessionId());
insertQuery.bindValue(":pickle", pickle.value());
- insertQuery.bindValue(":creationTime", session->creationTime());
- insertQuery.bindValue(":messageCount", session->messageCount());
+ insertQuery.bindValue(":creationTime", session.creationTime());
+ insertQuery.bindValue(":messageCount", session.messageCount());
transaction();
execute(deleteQuery);
@@ -362,7 +364,9 @@ void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::t
commit();
}
-QHash<QString, QStringList> Database::devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId)
+QMultiHash<QString, QString> Database::devicesWithoutKey(
+ const QString& roomId, QMultiHash<QString, QString> devices,
+ const QString& sessionId)
{
auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId"));
query.bindValue(":roomId", roomId);
@@ -371,7 +375,8 @@ QHash<QString, QStringList> Database::devicesWithoutKey(const QString& roomId, Q
execute(query);
commit();
while (query.next()) {
- devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString());
+ devices.remove(query.value("userId").toString(),
+ query.value("deviceId").toString());
}
return devices;
}
diff --git a/lib/database.h b/lib/database.h
index 45348c8d..4091d61b 100644
--- a/lib/database.h
+++ b/lib/database.h
@@ -32,22 +32,40 @@ public:
QByteArray accountPickle();
void setAccountPickle(const QByteArray &pickle);
void clear();
- void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle, const QDateTime& timestamp);
- UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(const PicklingMode& picklingMode);
- UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode);
- void saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId);
- void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts);
- std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index);
+ void saveOlmSession(const QString& senderKey, const QString& sessionId,
+ const QByteArray& pickle, const QDateTime& timestamp);
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(
+ const PicklingMode& picklingMode);
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadMegolmSessions(
+ const QString& roomId, const PicklingMode& picklingMode);
+ void saveMegolmSession(const QString& roomId, const QString& sessionId,
+ const QByteArray& pickle, const QString& senderId,
+ const QString& olmSessionId);
+ void addGroupSessionIndexRecord(const QString& roomId,
+ const QString& sessionId, uint32_t index,
+ const QString& eventId, qint64 ts);
+ std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId,
+ const QString& sessionId,
+ qint64 index);
void clearRoomData(const QString& roomId);
- void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp);
- QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode);
- void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data);
- void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle);
+ void setOlmSessionLastReceived(const QString& sessionId,
+ const QDateTime& timestamp);
+ QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode);
+ void saveCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode,
+ const QOlmOutboundGroupSession& session);
+ void updateOlmSession(const QString& senderKey, const QString& sessionId,
+ const QByteArray& pickle);
// Returns a map UserId -> [DeviceId] that have not received key yet
- QHash<QString, QStringList> devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId);
+ QMultiHash<QString, QString> devicesWithoutKey(const QString& roomId, QMultiHash<QString, QString> devices,
+ const QString& sessionId);
// 'devices' contains tuples {userId, deviceId, curveKey}
- void setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index);
+ void setDevicesReceivedKey(
+ const QString& roomId,
+ const QVector<std::tuple<QString, QString, QString>>& devices,
+ const QString& sessionId, int index);
private:
void migrateTo1();
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
index 76188d08..a2eff2c8 100644
--- a/lib/e2ee/qolmoutboundsession.cpp
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -44,7 +44,7 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create()
return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
}
-QOlmExpected<QByteArray> QOlmOutboundGroupSession::pickle(const PicklingMode &mode)
+QOlmExpected<QByteArray> QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const
{
QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0');
QByteArray key = toKey(mode);
@@ -79,7 +79,7 @@ QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(con
return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
}
-QOlmExpected<QByteArray> QOlmOutboundGroupSession::encrypt(const QString &plaintext)
+QOlmExpected<QByteArray> QOlmOutboundGroupSession::encrypt(const QString &plaintext) const
{
QByteArray plaintextBuf = plaintext.toUtf8();
const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length());
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
index c20613d3..9a82d22a 100644
--- a/lib/e2ee/qolmoutboundsession.h
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -21,14 +21,14 @@ public:
//! Throw OlmError on errors
static QOlmOutboundGroupSessionPtr create();
//! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
- QOlmExpected<QByteArray> pickle(const PicklingMode &mode);
+ QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const;
//! Deserialises from encrypted Base64 that was previously obtained by
//! pickling a `QOlmOutboundGroupSession`.
static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle(
const QByteArray& pickled, const PicklingMode& mode);
//! Encrypts a plaintext message using the session.
- QOlmExpected<QByteArray> encrypt(const QString& plaintext);
+ QOlmExpected<QByteArray> encrypt(const QString& plaintext) const;
//! Get the current message index for this session.
//!
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
index 2b149aac..2a98d5d8 100644
--- a/lib/e2ee/qolmsession.cpp
+++ b/lib/e2ee/qolmsession.cpp
@@ -96,12 +96,13 @@ QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession(
return std::make_unique<QOlmSession>(olmOutboundSession);
}
-QOlmExpected<QByteArray> QOlmSession::pickle(const PicklingMode &mode)
+QOlmExpected<QByteArray> QOlmSession::pickle(const PicklingMode &mode) const
{
QByteArray pickledBuf(olm_pickle_session_length(m_session), '0');
QByteArray key = toKey(mode);
const auto error = olm_pickle_session(m_session, key.data(), key.length(),
- pickledBuf.data(), pickledBuf.length());
+ pickledBuf.data(),
+ pickledBuf.length());
if (error == olm_error()) {
return lastError(m_session);
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
index faae16ef..021092c7 100644
--- a/lib/e2ee/qolmsession.h
+++ b/lib/e2ee/qolmsession.h
@@ -31,7 +31,7 @@ public:
const QString& theirOneTimeKey);
//! Serialises an `QOlmSession` to encrypted Base64.
- QOlmExpected<QByteArray> pickle(const PicklingMode &mode);
+ QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const;
//! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`.
static QOlmExpected<QOlmSessionPtr> unpickle(
diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp
index 302ae053..a2e2a156 100644
--- a/lib/eventitem.cpp
+++ b/lib/eventitem.cpp
@@ -8,32 +8,23 @@
using namespace Quotient;
-void PendingEventItem::setFileUploaded(const QUrl& remoteUrl)
+void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData)
{
// TODO: eventually we might introduce hasFileContent to RoomEvent,
// and unify the code below.
if (auto* rme = getAs<RoomMessageEvent>()) {
Q_ASSERT(rme->hasFileContent());
- rme->editContent([remoteUrl](EventContent::TypedBase& ec) {
- ec.fileInfo()->url = remoteUrl;
+ rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) {
+ ec.fileInfo()->source = uploadedFileData;
});
}
if (auto* rae = getAs<RoomAvatarEvent>()) {
Q_ASSERT(rae->content().fileInfo());
- rae->editContent(
- [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; });
- }
- setStatus(EventStatus::FileUploaded);
-}
-
-void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile)
-{
- if (auto* rme = getAs<RoomMessageEvent>()) {
- Q_ASSERT(rme->hasFileContent());
- rme->editContent([encryptedFile](EventContent::TypedBase& ec) {
- ec.fileInfo()->file = encryptedFile;
+ rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) {
+ fi.source = uploadedFileData;
});
}
+ setStatus(EventStatus::FileUploaded);
}
// Not exactly sure why but this helps with the linker not finding
diff --git a/lib/eventitem.h b/lib/eventitem.h
index d8313736..5e001d88 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -3,14 +3,14 @@
#pragma once
-#include "events/stateevent.h"
#include "quotient_common.h"
+#include "events/filesourceinfo.h"
+#include "events/stateevent.h"
+
#include <any>
#include <utility>
-#include "events/encryptedfile.h"
-
namespace Quotient {
namespace EventStatus {
@@ -115,8 +115,7 @@ public:
QString annotation() const { return _annotation; }
void setDeparted() { setStatus(EventStatus::Departed); }
- void setFileUploaded(const QUrl& remoteUrl);
- void setEncryptedFile(const EncryptedFile& encryptedFile);
+ void setFileUploaded(const FileSourceInfo &uploadedFileData);
void setReachedServer(const QString& eventId)
{
setStatus(EventStatus::ReachedServer);
diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp
deleted file mode 100644
index 33ebb514..00000000
--- a/lib/events/encryptedfile.cpp
+++ /dev/null
@@ -1,119 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
-//
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "encryptedfile.h"
-#include "logging.h"
-
-#ifdef Quotient_E2EE_ENABLED
-#include <openssl/evp.h>
-#include <QtCore/QCryptographicHash>
-#include "e2ee/qolmutils.h"
-#endif
-
-using namespace Quotient;
-
-QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const
-{
-#ifdef Quotient_E2EE_ENABLED
- auto _key = key.k;
- const auto keyBytes = QByteArray::fromBase64(
- _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1());
- const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1());
- if (sha256
- != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
- qCWarning(E2EE) << "Hash verification failed for file";
- return {};
- }
- {
- int length;
- auto* ctx = EVP_CIPHER_CTX_new();
- QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH
- - 1,
- '\0');
- EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr,
- reinterpret_cast<const unsigned char*>(
- keyBytes.data()),
- reinterpret_cast<const unsigned char*>(
- QByteArray::fromBase64(iv.toLatin1()).data()));
- EVP_DecryptUpdate(
- ctx, reinterpret_cast<unsigned char*>(plaintext.data()), &length,
- reinterpret_cast<const unsigned char*>(ciphertext.data()),
- ciphertext.size());
- EVP_DecryptFinal_ex(ctx,
- reinterpret_cast<unsigned char*>(plaintext.data())
- + length,
- &length);
- EVP_CIPHER_CTX_free(ctx);
- return plaintext.left(ciphertext.size());
- }
-#else
- qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, "
- "cannot decrypt the file";
- return ciphertext;
-#endif
-}
-
-std::pair<EncryptedFile, QByteArray> EncryptedFile::encryptFile(const QByteArray &plainText)
-{
-#ifdef Quotient_E2EE_ENABLED
- QByteArray k = getRandom(32);
- auto kBase64 = k.toBase64();
- QByteArray iv = getRandom(16);
- JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true};
-
- int length;
- auto* ctx = EVP_CIPHER_CTX_new();
- EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data()));
- const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
- QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
- EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()), &length, reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size());
- EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(cipherText.data()) + length, &length);
- EVP_CIPHER_CTX_free(ctx);
-
- auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64();
- auto ivBase64 = iv.toBase64();
- EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls};
- return {file, cipherText};
-#else
- return {};
-#endif
-}
-
-void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo,
- const EncryptedFile& pod)
-{
- addParam<>(jo, QStringLiteral("url"), pod.url);
- addParam<>(jo, QStringLiteral("key"), pod.key);
- addParam<>(jo, QStringLiteral("iv"), pod.iv);
- addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
- addParam<>(jo, QStringLiteral("v"), pod.v);
-}
-
-void JsonObjectConverter<EncryptedFile>::fillFrom(const QJsonObject& jo,
- EncryptedFile& pod)
-{
- fromJson(jo.value("url"_ls), pod.url);
- fromJson(jo.value("key"_ls), pod.key);
- fromJson(jo.value("iv"_ls), pod.iv);
- fromJson(jo.value("hashes"_ls), pod.hashes);
- fromJson(jo.value("v"_ls), pod.v);
-}
-
-void JsonObjectConverter<JWK>::dumpTo(QJsonObject &jo, const JWK &pod)
-{
- addParam<>(jo, QStringLiteral("kty"), pod.kty);
- addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps);
- addParam<>(jo, QStringLiteral("alg"), pod.alg);
- addParam<>(jo, QStringLiteral("k"), pod.k);
- addParam<>(jo, QStringLiteral("ext"), pod.ext);
-}
-
-void JsonObjectConverter<JWK>::fillFrom(const QJsonObject &jo, JWK &pod)
-{
- fromJson(jo.value("kty"_ls), pod.kty);
- fromJson(jo.value("key_ops"_ls), pod.keyOps);
- fromJson(jo.value("alg"_ls), pod.alg);
- fromJson(jo.value("k"_ls), pod.k);
- fromJson(jo.value("ext"_ls), pod.ext);
-}
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
deleted file mode 100644
index 022ac91e..00000000
--- a/lib/events/encryptedfile.h
+++ /dev/null
@@ -1,63 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
-//
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#pragma once
-
-#include "converters.h"
-
-namespace Quotient {
-/**
- * JSON Web Key object as specified in
- * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
- * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
- */
-struct JWK
-{
- Q_GADGET
- Q_PROPERTY(QString kty MEMBER kty CONSTANT)
- Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
- Q_PROPERTY(QString alg MEMBER alg CONSTANT)
- Q_PROPERTY(QString k MEMBER k CONSTANT)
- Q_PROPERTY(bool ext MEMBER ext CONSTANT)
-
-public:
- QString kty;
- QStringList keyOps;
- QString alg;
- QString k;
- bool ext;
-};
-
-struct QUOTIENT_API EncryptedFile
-{
- Q_GADGET
- Q_PROPERTY(QUrl url MEMBER url CONSTANT)
- Q_PROPERTY(JWK key MEMBER key CONSTANT)
- Q_PROPERTY(QString iv MEMBER iv CONSTANT)
- Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT)
- Q_PROPERTY(QString v MEMBER v CONSTANT)
-
-public:
- QUrl url;
- JWK key;
- QString iv;
- QHash<QString, QString> hashes;
- QString v;
-
- QByteArray decryptFile(const QByteArray &ciphertext) const;
- static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray& plainText);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<EncryptedFile> {
- static void dumpTo(QJsonObject& jo, const EncryptedFile& pod);
- static void fillFrom(const QJsonObject& jo, EncryptedFile& pod);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<JWK> {
- static void dumpTo(QJsonObject& jo, const JWK& pod);
- static void fillFrom(const QJsonObject& jo, JWK& pod);
-};
-} // namespace Quotient
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index 6218e3b8..8db3b7e3 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -19,23 +19,21 @@ QJsonObject Base::toJson() const
return o;
}
-FileInfo::FileInfo(const QFileInfo &fi)
- : mimeType(QMimeDatabase().mimeTypeForFile(fi))
- , url(QUrl::fromLocalFile(fi.filePath()))
- , payloadSize(fi.size())
- , originalName(fi.fileName())
+FileInfo::FileInfo(const QFileInfo& fi)
+ : source(QUrl::fromLocalFile(fi.filePath())),
+ mimeType(QMimeDatabase().mimeTypeForFile(fi)),
+ payloadSize(fi.size()),
+ originalName(fi.fileName())
{
Q_ASSERT(fi.isFile());
}
-FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
- Omittable<EncryptedFile> encryptedFile,
- QString originalFilename)
- : mimeType(mimeType)
- , url(move(u))
+FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize,
+ const QMimeType& mimeType, QString originalFilename)
+ : source(move(sourceInfo))
+ , mimeType(mimeType)
, payloadSize(payloadSize)
, originalName(move(originalFilename))
- , file(move(encryptedFile))
{
if (!isValid())
qCWarning(MESSAGES)
@@ -44,28 +42,28 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
"0.7; for local resources, use FileInfo(QFileInfo) instead";
}
-FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile,
+FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
QString originalFilename)
- : originalInfoJson(infoJson)
+ : source(move(sourceInfo))
+ , originalInfoJson(infoJson)
, mimeType(
QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
- , url(move(mxcUrl))
, payloadSize(fromJson<qint64>(infoJson["size"_ls]))
, originalName(move(originalFilename))
- , file(move(encryptedFile))
{
- if(url.isEmpty() && file.has_value()) {
- url = file->url;
- }
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
bool FileInfo::isValid() const
{
- return url.scheme() == "mxc"
- && (url.authority() + url.path()).count('/') == 1;
+ const auto& u = url();
+ return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1;
+}
+
+QUrl FileInfo::url() const
+{
+ return getUrlFromSourceInfo(source);
}
QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info)
@@ -75,7 +73,6 @@ QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info)
infoJson.insert(QStringLiteral("size"), info.payloadSize);
if (info.mimeType.isValid())
infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name());
- //TODO add encryptedfile
return infoJson;
}
@@ -83,17 +80,16 @@ ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
: FileInfo(fi), imageSize(imageSize)
{}
-ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type,
- QSize imageSize, Omittable<EncryptedFile> encryptedFile,
+ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize,
+ const QMimeType& type, QSize imageSize,
const QString& originalFilename)
- : FileInfo(mxcUrl, fileSize, type, move(encryptedFile), originalFilename)
+ : FileInfo(move(sourceInfo), fileSize, type, originalFilename)
, imageSize(imageSize)
{}
-ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile,
+ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
const QString& originalFilename)
- : FileInfo(mxcUrl, infoJson, move(encryptedFile), originalFilename)
+ : FileInfo(move(sourceInfo), infoJson, originalFilename)
, imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt())
{}
@@ -108,15 +104,18 @@ QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info)
}
Thumbnail::Thumbnail(const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile)
+ const Omittable<EncryptedFileMetadata>& efm)
: ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()),
- infoJson["thumbnail_info"_ls].toObject(), move(encryptedFile))
-{}
+ infoJson["thumbnail_info"_ls].toObject())
+{
+ if (efm)
+ source = *efm;
+}
void Thumbnail::dumpTo(QJsonObject& infoJson) const
{
- if (url.isValid())
- infoJson.insert(QStringLiteral("thumbnail_url"), url.toString());
+ if (url().isValid())
+ fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source);
if (!imageSize.isEmpty())
infoJson.insert(QStringLiteral("thumbnail_info"),
toInfoJson(*this));
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index bbd35618..ea240122 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -6,14 +6,14 @@
// This file contains generic event content definitions, applicable to room
// message events as well as other events (e.g., avatars).
-#include "encryptedfile.h"
+#include "filesourceinfo.h"
#include "quotient_export.h"
#include <QtCore/QJsonObject>
+#include <QtCore/QMetaType>
#include <QtCore/QMimeType>
#include <QtCore/QSize>
#include <QtCore/QUrl>
-#include <QtCore/QMetaType>
class QFileInfo;
@@ -50,7 +50,7 @@ namespace EventContent {
// A quick classes inheritance structure follows (the definitions are
// spread across eventcontent.h and roommessageevent.h):
- // UrlBasedContent<InfoT> : InfoT + url and thumbnail data
+ // UrlBasedContent<InfoT> : InfoT + thumbnail data
// PlayableContent<InfoT> : + duration attribute
// FileInfo
// FileContent = UrlBasedContent<FileInfo>
@@ -89,34 +89,32 @@ namespace EventContent {
//!
//! \param fi a QFileInfo object referring to an existing file
explicit FileInfo(const QFileInfo& fi);
- explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1,
+ explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1,
const QMimeType& mimeType = {},
- Omittable<EncryptedFile> encryptedFile = none,
QString originalFilename = {});
//! \brief Construct from a JSON `info` payload
//!
//! Make sure to pass the `info` subobject of content JSON, not the
//! whole JSON content.
- FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile,
+ FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
QString originalFilename = {});
bool isValid() const;
+ QUrl url() const;
//! \brief Extract media id from the URL
//!
//! This can be used, e.g., to construct a QML-facing image://
//! URI as follows:
//! \code "image://provider/" + info.mediaId() \endcode
- QString mediaId() const { return url.authority() + url.path(); }
+ QString mediaId() const { return url().authority() + url().path(); }
public:
+ FileSourceInfo source;
QJsonObject originalInfoJson;
QMimeType mimeType;
- QUrl url;
qint64 payloadSize = 0;
QString originalName;
- Omittable<EncryptedFile> file = none;
};
QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info);
@@ -126,12 +124,10 @@ namespace EventContent {
public:
ImageInfo() = default;
explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {});
- explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1,
+ explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1,
const QMimeType& type = {}, QSize imageSize = {},
- Omittable<EncryptedFile> encryptedFile = none,
const QString& originalFilename = {});
- ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile,
+ ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
const QString& originalFilename = {});
public:
@@ -144,12 +140,13 @@ namespace EventContent {
//!
//! This class saves/loads a thumbnail to/from `info` subobject of
//! the JSON representation of event content; namely, `info/thumbnail_url`
- //! and `info/thumbnail_info` fields are used.
+ //! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and
+ //! `info/thumbnail_info` fields are used.
class QUOTIENT_API Thumbnail : public ImageInfo {
public:
using ImageInfo::ImageInfo;
Thumbnail(const QJsonObject& infoJson,
- Omittable<EncryptedFile> encryptedFile = none);
+ const Omittable<EncryptedFileMetadata>& efm = none);
//! \brief Add thumbnail information to the passed `info` JSON object
void dumpTo(QJsonObject& infoJson) const;
@@ -169,10 +166,10 @@ namespace EventContent {
//! \brief A template class for content types with a URL and additional info
//!
- //! Types that derive from this class template take `url` and,
- //! optionally, `filename` values from the top-level JSON object and
- //! the rest of information from the `info` subobject, as defined by
- //! the parameter type.
+ //! Types that derive from this class template take `url` (or, if the file
+ //! is encrypted, `file`) and, optionally, `filename` values from
+ //! the top-level JSON object and the rest of information from the `info`
+ //! subobject, as defined by the parameter type.
//! \tparam InfoT base info class - FileInfo or ImageInfo
template <class InfoT>
class UrlBasedContent : public TypedBase, public InfoT {
@@ -181,10 +178,12 @@ namespace EventContent {
explicit UrlBasedContent(const QJsonObject& json)
: TypedBase(json)
, InfoT(QUrl(json["url"].toString()), json["info"].toObject(),
- fromJson<Omittable<EncryptedFile>>(json["file"]),
json["filename"].toString())
, thumbnail(FileInfo::originalInfoJson)
{
+ if (const auto efmJson = json.value("file"_ls).toObject();
+ !efmJson.isEmpty())
+ InfoT::source = fromJson<EncryptedFileMetadata>(efmJson);
// Two small hacks on originalJson to expose mediaIds to QML
originalJson.insert("mediaId", InfoT::mediaId());
originalJson.insert("thumbnailMediaId", thumbnail.mediaId());
@@ -204,11 +203,7 @@ namespace EventContent {
void fillJson(QJsonObject& json) const override
{
- if (!InfoT::file.has_value()) {
- json.insert("url", InfoT::url.toString());
- } else {
- json.insert("file", Quotient::toJson(*InfoT::file));
- }
+ Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source);
if (!InfoT::originalName.isEmpty())
json.insert("filename", InfoT::originalName);
auto infoJson = toInfoJson(*this);
@@ -223,7 +218,7 @@ namespace EventContent {
//!
//! Available fields:
//! - corresponding to the top-level JSON:
- //! - url
+ //! - source (corresponding to `url` or `file` in JSON)
//! - filename (extension to the spec)
//! - corresponding to the `info` subobject:
//! - payloadSize (`size` in JSON)
@@ -241,12 +236,12 @@ namespace EventContent {
//!
//! Available fields:
//! - corresponding to the top-level JSON:
- //! - url
+ //! - source (corresponding to `url` or `file` in JSON)
//! - filename
//! - corresponding to the `info` subobject:
//! - payloadSize (`size` in JSON)
//! - mimeType (`mimetype` in JSON)
- //! - thumbnail.url (`thumbnail_url` in JSON)
+ //! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON)
//! - corresponding to the `info/thumbnail_info` subobject:
//! - thumbnail.payloadSize
//! - thumbnail.mimeType
diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp
new file mode 100644
index 00000000..11f93d80
--- /dev/null
+++ b/lib/events/filesourceinfo.cpp
@@ -0,0 +1,179 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "filesourceinfo.h"
+
+#include "logging.h"
+
+#ifdef Quotient_E2EE_ENABLED
+# include "e2ee/qolmutils.h"
+
+# include <QtCore/QCryptographicHash>
+
+# include <openssl/evp.h>
+#endif
+
+using namespace Quotient;
+
+QByteArray Quotient::decryptFile(const QByteArray& ciphertext,
+ const EncryptedFileMetadata& metadata)
+{
+#ifdef Quotient_E2EE_ENABLED
+ if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1())
+ != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return {};
+ }
+
+ auto _key = metadata.key.k;
+ const auto keyBytes = QByteArray::fromBase64(
+ _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1());
+ int length;
+ auto* ctx = EVP_CIPHER_CTX_new();
+ QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0');
+ EVP_DecryptInit_ex(
+ ctx, EVP_aes_256_ctr(), nullptr,
+ reinterpret_cast<const unsigned char*>(keyBytes.data()),
+ reinterpret_cast<const unsigned char*>(
+ QByteArray::fromBase64(metadata.iv.toLatin1()).data()));
+ EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()),
+ &length,
+ reinterpret_cast<const unsigned char*>(ciphertext.data()),
+ ciphertext.size());
+ EVP_DecryptFinal_ex(ctx,
+ reinterpret_cast<unsigned char*>(plaintext.data())
+ + length,
+ &length);
+ EVP_CIPHER_CTX_free(ctx);
+ return plaintext.left(ciphertext.size());
+#else
+ qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, "
+ "cannot decrypt the file";
+ return ciphertext;
+#endif
+}
+
+std::pair<EncryptedFileMetadata, QByteArray> Quotient::encryptFile(
+ const QByteArray& plainText)
+{
+#ifdef Quotient_E2EE_ENABLED
+ QByteArray k = getRandom(32);
+ auto kBase64 = k.toBase64();
+ QByteArray iv = getRandom(16);
+ JWK key = { "oct"_ls,
+ { "encrypt"_ls, "decrypt"_ls },
+ "A256CTR"_ls,
+ QString(k.toBase64())
+ .replace(u'/', u'_')
+ .replace(u'+', u'-')
+ .left(kBase64.indexOf('=')),
+ true };
+
+ int length;
+ auto* ctx = EVP_CIPHER_CTX_new();
+ EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr,
+ reinterpret_cast<const unsigned char*>(k.data()),
+ reinterpret_cast<const unsigned char*>(iv.data()));
+ const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
+ QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
+ EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()),
+ &length,
+ reinterpret_cast<const unsigned char*>(plainText.data()),
+ plainText.size());
+ EVP_EncryptFinal_ex(ctx,
+ reinterpret_cast<unsigned char*>(cipherText.data())
+ + length,
+ &length);
+ EVP_CIPHER_CTX_free(ctx);
+
+ auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256)
+ .toBase64();
+ auto ivBase64 = iv.toBase64();
+ EncryptedFileMetadata efm = { {},
+ key,
+ ivBase64.left(ivBase64.indexOf('=')),
+ { { QStringLiteral("sha256"),
+ hash.left(hash.indexOf('=')) } },
+ "v2"_ls };
+ return { efm, cipherText };
+#else
+ return {};
+#endif
+}
+
+void JsonObjectConverter<EncryptedFileMetadata>::dumpTo(QJsonObject& jo,
+ const EncryptedFileMetadata& pod)
+{
+ addParam<>(jo, QStringLiteral("url"), pod.url);
+ addParam<>(jo, QStringLiteral("key"), pod.key);
+ addParam<>(jo, QStringLiteral("iv"), pod.iv);
+ addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
+ addParam<>(jo, QStringLiteral("v"), pod.v);
+}
+
+void JsonObjectConverter<EncryptedFileMetadata>::fillFrom(const QJsonObject& jo,
+ EncryptedFileMetadata& pod)
+{
+ fromJson(jo.value("url"_ls), pod.url);
+ fromJson(jo.value("key"_ls), pod.key);
+ fromJson(jo.value("iv"_ls), pod.iv);
+ fromJson(jo.value("hashes"_ls), pod.hashes);
+ fromJson(jo.value("v"_ls), pod.v);
+}
+
+void JsonObjectConverter<JWK>::dumpTo(QJsonObject& jo, const JWK& pod)
+{
+ addParam<>(jo, QStringLiteral("kty"), pod.kty);
+ addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps);
+ addParam<>(jo, QStringLiteral("alg"), pod.alg);
+ addParam<>(jo, QStringLiteral("k"), pod.k);
+ addParam<>(jo, QStringLiteral("ext"), pod.ext);
+}
+
+void JsonObjectConverter<JWK>::fillFrom(const QJsonObject& jo, JWK& pod)
+{
+ fromJson(jo.value("kty"_ls), pod.kty);
+ fromJson(jo.value("key_ops"_ls), pod.keyOps);
+ fromJson(jo.value("alg"_ls), pod.alg);
+ fromJson(jo.value("k"_ls), pod.k);
+ fromJson(jo.value("ext"_ls), pod.ext);
+}
+
+template <typename... FunctorTs>
+struct Overloads : FunctorTs... {
+ using FunctorTs::operator()...;
+};
+
+template <typename... FunctorTs>
+Overloads(FunctorTs&&...) -> Overloads<FunctorTs...>;
+
+QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi)
+{
+ return std::visit(Overloads { [](const QUrl& url) { return url; },
+ [](const EncryptedFileMetadata& efm) {
+ return efm.url;
+ } },
+ fsi);
+}
+
+void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl)
+{
+ std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; },
+ [&newUrl](EncryptedFileMetadata& efm) {
+ efm.url = newUrl;
+ } },
+ fsi);
+}
+
+void Quotient::fillJson(QJsonObject& jo,
+ const std::array<QLatin1String, 2>& jsonKeys,
+ const FileSourceInfo& fsi)
+{
+ // NB: Keeping variant_size_v out of the function signature for readability.
+ // NB2: Can't use jsonKeys directly inside static_assert as its value is
+ // unknown so the compiler cannot ensure size() is constexpr (go figure...)
+ static_assert(
+ std::variant_size_v<FileSourceInfo> == decltype(jsonKeys) {}.size());
+ jo.insert(jsonKeys[fsi.index()], toJson(fsi));
+}
diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h
new file mode 100644
index 00000000..8f7e3cbe
--- /dev/null
+++ b/lib/events/filesourceinfo.h
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+#include <array>
+
+namespace Quotient {
+/**
+ * JSON Web Key object as specified in
+ * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
+ * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
+ */
+struct JWK
+{
+ Q_GADGET
+ Q_PROPERTY(QString kty MEMBER kty CONSTANT)
+ Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
+ Q_PROPERTY(QString alg MEMBER alg CONSTANT)
+ Q_PROPERTY(QString k MEMBER k CONSTANT)
+ Q_PROPERTY(bool ext MEMBER ext CONSTANT)
+
+public:
+ QString kty;
+ QStringList keyOps;
+ QString alg;
+ QString k;
+ bool ext;
+};
+
+struct QUOTIENT_API EncryptedFileMetadata {
+ Q_GADGET
+ Q_PROPERTY(QUrl url MEMBER url CONSTANT)
+ Q_PROPERTY(JWK key MEMBER key CONSTANT)
+ Q_PROPERTY(QString iv MEMBER iv CONSTANT)
+ Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT)
+ Q_PROPERTY(QString v MEMBER v CONSTANT)
+
+public:
+ QUrl url;
+ JWK key;
+ QString iv;
+ QHash<QString, QString> hashes;
+ QString v;
+};
+
+QUOTIENT_API std::pair<EncryptedFileMetadata, QByteArray> encryptFile(
+ const QByteArray& plainText);
+QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext,
+ const EncryptedFileMetadata& metadata);
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<EncryptedFileMetadata> {
+ static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod);
+ static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod);
+};
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<JWK> {
+ static void dumpTo(QJsonObject& jo, const JWK& pod);
+ static void fillFrom(const QJsonObject& jo, JWK& pod);
+};
+
+using FileSourceInfo = std::variant<QUrl, EncryptedFileMetadata>;
+
+QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi);
+
+QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl);
+
+// The way FileSourceInfo is stored in JSON requires an extra parameter so
+// the original template is not applicable
+template <>
+void fillJson(QJsonObject&, const FileSourceInfo&) = delete;
+
+//! \brief Export FileSourceInfo to a JSON object
+//!
+//! Depending on what is stored inside FileSourceInfo, this function will insert
+//! - a key-to-string pair where key is taken from jsonKeys[0] and the string
+//! is the URL, if FileSourceInfo stores a QUrl;
+//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object
+//! is the result of converting EncryptedFileMetadata to JSON,
+//! if FileSourceInfo stores EncryptedFileMetadata
+QUOTIENT_API void fillJson(QJsonObject& jo,
+ const std::array<QLatin1String, 2>& jsonKeys,
+ const FileSourceInfo& fsi);
+
+} // namespace Quotient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index c54b5801..af291696 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -26,10 +26,10 @@ public:
const QSize& imageSize = {},
const QString& originalFilename = {})
: RoomAvatarEvent(EventContent::ImageContent {
- mxcUrl, fileSize, mimeType, imageSize, none, originalFilename })
+ mxcUrl, fileSize, mimeType, imageSize, originalFilename })
{}
- QUrl url() const { return content().url; }
+ QUrl url() const { return content().url(); }
};
REGISTER_EVENT_TYPE(RoomAvatarEvent)
} // namespace Quotient
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
index 2bda3086..3093db41 100644
--- a/lib/events/roomkeyevent.h
+++ b/lib/events/roomkeyevent.h
@@ -12,7 +12,9 @@ public:
DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent)
explicit RoomKeyEvent(const QJsonObject& obj);
- explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId);
+ explicit RoomKeyEvent(const QString& algorithm, const QString& roomId,
+ const QString& sessionId, const QString& sessionKey,
+ const QString& senderId);
QString algorithm() const { return contentPart<QString>("algorithm"_ls); }
QString roomId() const { return contentPart<QString>(RoomIdKeyL); }
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index d9d3fbe0..2a6ae93c 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -148,21 +148,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
auto mimeTypeName = mimeType.name();
if (mimeTypeName.startsWith("image/"))
return new ImageContent(localUrl, file.size(), mimeType,
- QImageReader(filePath).size(), none,
+ QImageReader(filePath).size(),
file.fileName());
// duration can only be obtained asynchronously and can only be reliably
// done by starting to play the file. Left for a future implementation.
if (mimeTypeName.startsWith("video/"))
return new VideoContent(localUrl, file.size(), mimeType,
- QMediaResource(localUrl).resolution(), none,
+ QMediaResource(localUrl).resolution(),
file.fileName());
if (mimeTypeName.startsWith("audio/"))
- return new AudioContent(localUrl, file.size(), mimeType, none,
+ return new AudioContent(localUrl, file.size(), mimeType,
file.fileName());
}
- return new FileContent(localUrl, file.size(), mimeType, none, file.fileName());
+ return new FileContent(localUrl, file.size(), mimeType, file.fileName());
}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp
index 628fd154..6d318f0e 100644
--- a/lib/events/stickerevent.cpp
+++ b/lib/events/stickerevent.cpp
@@ -22,5 +22,5 @@ const EventContent::ImageContent &StickerEvent::image() const
QUrl StickerEvent::url() const
{
- return m_imageContent.url;
+ return m_imageContent.url();
}
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index d00fc5f4..759d52c9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -8,8 +8,9 @@
#include <QtNetwork/QNetworkReply>
#ifdef Quotient_E2EE_ENABLED
-# include <QtCore/QCryptographicHash>
-# include "events/encryptedfile.h"
+# include "events/filesourceinfo.h"
+
+# include <QtCore/QCryptographicHash>
#endif
using namespace Quotient;
@@ -26,7 +27,7 @@ public:
QScopedPointer<QFile> tempFile;
#ifdef Quotient_E2EE_ENABLED
- Omittable<EncryptedFile> encryptedFile;
+ Omittable<EncryptedFileMetadata> encryptedFileMetadata;
#endif
};
@@ -49,14 +50,14 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
#ifdef Quotient_E2EE_ENABLED
DownloadFileJob::DownloadFileJob(const QString& serverName,
const QString& mediaId,
- const EncryptedFile& file,
+ const EncryptedFileMetadata& file,
const QString& localFilename)
: GetContentJob(serverName, mediaId)
, d(localFilename.isEmpty() ? makeImpl<Private>()
: makeImpl<Private>(localFilename))
{
setObjectName(QStringLiteral("DownloadFileJob"));
- d->encryptedFile = file;
+ d->encryptedFileMetadata = file;
}
#endif
QString DownloadFileJob::targetFileName() const
@@ -118,27 +119,31 @@ void DownloadFileJob::beforeAbandon()
d->tempFile->remove();
}
+void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata,
+ QFile& targetFile)
+{
+ sourceFile.seek(0);
+ const auto encrypted = sourceFile.readAll(); // TODO: stream decryption
+ const auto decrypted = decryptFile(encrypted, metadata);
+ targetFile.write(decrypted);
+}
+
BaseJob::Status DownloadFileJob::prepareResult()
{
if (d->targetFile) {
#ifdef Quotient_E2EE_ENABLED
- if (d->encryptedFile.has_value()) {
- d->tempFile->seek(0);
- QByteArray encrypted = d->tempFile->readAll();
-
- EncryptedFile file = *d->encryptedFile;
- const auto decrypted = file.decryptFile(encrypted);
- d->targetFile->write(decrypted);
+ if (d->encryptedFileMetadata.has_value()) {
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile);
d->tempFile->remove();
} else {
#endif
d->targetFile->close();
if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
+ qWarning(JOBS) << "Failed to remove the target file placeholder";
return { FileError, "Couldn't finalise the download" };
}
if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
<< "to" << d->targetFile->fileName();
return { FileError, "Couldn't finalise the download" };
}
@@ -147,13 +152,20 @@ BaseJob::Status DownloadFileJob::prepareResult()
#endif
} else {
#ifdef Quotient_E2EE_ENABLED
- if (d->encryptedFile.has_value()) {
- d->tempFile->seek(0);
- const auto encrypted = d->tempFile->readAll();
-
- EncryptedFile file = *d->encryptedFile;
- const auto decrypted = file.decryptFile(encrypted);
- d->tempFile->write(decrypted);
+ if (d->encryptedFileMetadata.has_value()) {
+ QTemporaryFile tempTempFile; // Assuming it to be next to tempFile
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile);
+ d->tempFile->close();
+ if (!d->tempFile->remove()) {
+ qWarning(JOBS)
+ << "Failed to remove the decrypted file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!tempTempFile.rename(d->tempFile->fileName())) {
+ qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName()
+ << "to" << d->tempFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
} else {
#endif
d->tempFile->close();
@@ -161,6 +173,6 @@ BaseJob::Status DownloadFileJob::prepareResult()
}
#endif
}
- qCDebug(JOBS) << "Saved a file as" << targetFileName();
+ qDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index ffa3d055..cbbfd244 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -4,7 +4,8 @@
#pragma once
#include "csapi/content-repo.h"
-#include "events/encryptedfile.h"
+
+#include "events/filesourceinfo.h"
namespace Quotient {
class QUOTIENT_API DownloadFileJob : public GetContentJob {
@@ -16,7 +17,7 @@ public:
const QString& localFilename = {});
#ifdef Quotient_E2EE_ENABLED
- DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {});
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {});
#endif
QString targetFileName() const;
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
index 319d514a..4174cfd8 100644
--- a/lib/mxcreply.cpp
+++ b/lib/mxcreply.cpp
@@ -8,7 +8,7 @@
#include "room.h"
#ifdef Quotient_E2EE_ENABLED
-#include "events/encryptedfile.h"
+#include "events/filesourceinfo.h"
#endif
using namespace Quotient;
@@ -20,7 +20,7 @@ public:
: m_reply(r)
{}
QNetworkReply* m_reply;
- Omittable<EncryptedFile> m_encryptedFile;
+ Omittable<EncryptedFileMetadata> m_encryptedFile;
QIODevice* m_device = nullptr;
};
@@ -47,9 +47,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
if(!d->m_encryptedFile.has_value()) {
d->m_device = d->m_reply;
} else {
- EncryptedFile file = *d->m_encryptedFile;
auto buffer = new QBuffer(this);
- buffer->setData(file.decryptFile(d->m_reply->readAll()));
+ buffer->setData(
+ decryptFile(d->m_reply->readAll(), *d->m_encryptedFile));
buffer->open(ReadOnly);
d->m_device = buffer;
}
@@ -64,7 +64,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
auto eventIt = room->findInTimeline(eventId);
if(eventIt != room->historyEdge()) {
auto event = eventIt->viewAs<RoomMessageEvent>();
- d->m_encryptedFile = event->content()->fileInfo()->file;
+ if (auto* efm = std::get_if<EncryptedFileMetadata>(
+ &event->content()->fileInfo()->source))
+ d->m_encryptedFile = *efm;
}
#endif
}
diff --git a/lib/room.cpp b/lib/room.cpp
index f85591c7..c745975f 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -443,9 +443,11 @@ public:
return q->getCurrentState<EncryptionEvent>()->rotationPeriodMsgs();
}
void createMegolmSession() {
- qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id();
+ qCDebug(E2EE) << "Creating new outbound megolm session for room "
+ << q->objectName();
currentOutboundMegolmSession = QOlmOutboundGroupSession::create();
- connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
const auto sessionKey = currentOutboundMegolmSession->sessionKey();
if(!sessionKey) {
@@ -455,112 +457,30 @@ public:
addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls);
}
- std::unique_ptr<EncryptedEvent> payloadForUserDevice(QString user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey)
+ QMultiHash<QString, QString> getDevicesWithoutKey() const
{
- // Noisy but nice for debugging
- //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex();
- const auto event = makeEvent<RoomKeyEvent>("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id());
- QJsonObject payloadJson = event->fullJson();
- payloadJson["recipient"] = user;
- payloadJson["sender"] = connection->user()->id();
- QJsonObject recipientObject;
- recipientObject["ed25519"] = connection->edKeyForUserDevice(user, device);
- payloadJson["recipient_keys"] = recipientObject;
- QJsonObject senderObject;
- senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519);
- payloadJson["keys"] = senderObject;
- payloadJson["sender_device"] = connection->deviceId();
- auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
- QJsonObject encrypted;
- encrypted[connection->curveKeyForUserDevice(user, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}};
-
- return makeEvent<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
- }
-
- QHash<QString, QStringList> getDevicesWithoutKey() const
- {
- QHash<QString, QStringList> devices;
- for (const auto& user : q->users()) {
- devices[user->id()] = q->connection()->devicesForUser(user->id());
- }
- return q->connection()->database()->devicesWithoutKey(q->id(), devices, QString(currentOutboundMegolmSession->sessionId()));
- }
+ QMultiHash<QString, QString> devices;
+ for (const auto& user : q->users())
+ for (const auto& deviceId : connection->devicesForUser(user->id()))
+ devices.insert(user->id(), deviceId);
- void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash<QString, QStringList> devices, int index)
- {
- qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex();
- QHash<QString, QHash<QString, QString>> hash;
- for (const auto& user : devices.keys()) {
- QHash<QString, QString> u;
- for(const auto &device : devices[user]) {
- if (!connection->hasOlmSession(user, device)) {
- u[device] = "signed_curve25519"_ls;
- qCDebug(E2EE) << "Adding" << user << device << "to keys to claim";
- }
- }
- if (!u.isEmpty()) {
- hash[user] = u;
- }
- }
- if (hash.isEmpty()) {
- return;
- }
- auto job = connection->callApi<ClaimKeysJob>(hash);
- connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){
- Connection::UsersToDevicesToEvents usersToDevicesToEvents;
- const auto data = job->jsonData();
- for(const auto &user : devices.keys()) {
- for(const auto &device : devices[user]) {
- const auto recipientCurveKey = connection->curveKeyForUserDevice(user, device);
- if (!connection->hasOlmSession(user, device)) {
- qCDebug(E2EE) << "Creating a new session for" << user << device;
- if(data["one_time_keys"][user][device].toObject().isEmpty()) {
- qWarning() << "No one time key for" << user << device;
- continue;
- }
- const auto keyId = data["one_time_keys"][user][device].toObject().keys()[0];
- const auto oneTimeKey = data["one_time_keys"][user][device][keyId]["key"].toString();
- const auto signature = data["one_time_keys"][user][device][keyId]["signatures"][user][QStringLiteral("ed25519:") + device].toString().toLatin1();
- auto signedData = data["one_time_keys"][user][device][keyId].toObject();
- signedData.remove("unsigned");
- signedData.remove("signatures");
- auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user, device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature);
- if (!signatureMatch) {
- qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user << device << ". Skipping this device.";
- continue;
- } else {
- }
- connection->createOlmSession(recipientCurveKey, oneTimeKey);
- }
- usersToDevicesToEvents[user][device] = payloadForUserDevice(user, device, sessionId, sessionKey);
- }
- }
- if (!usersToDevicesToEvents.empty()) {
- connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents);
- QVector<std::tuple<QString, QString, QString>> receivedDevices;
- for (const auto& user : devices.keys()) {
- for (const auto& device : devices[user]) {
- receivedDevices += {user, device, q->connection()->curveKeyForUserDevice(user, device) };
- }
- }
- connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index);
- }
- });
+ return connection->database()->devicesWithoutKey(
+ id, devices, currentOutboundMegolmSession->sessionId());
}
- void sendMegolmSession(const QHash<QString, QStringList>& devices) {
+ void sendMegolmSession(const QMultiHash<QString, QString>& devices) const {
// Save the session to this device
const auto sessionId = currentOutboundMegolmSession->sessionId();
- const auto _sessionKey = currentOutboundMegolmSession->sessionKey();
- if(!_sessionKey) {
+ const auto sessionKey = currentOutboundMegolmSession->sessionKey();
+ if(!sessionKey) {
qCWarning(E2EE) << "Error loading session key";
return;
}
- const auto sessionKey = *_sessionKey;
- const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519;
// Send the session to other people
- sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex());
+ connection->sendSessionKeyToDevices(
+ id, sessionId, *sessionKey, devices,
+ currentOutboundMegolmSession->sessionMessageIndex());
}
#endif // Quotient_E2EE_ENABLED
@@ -592,7 +512,8 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
}
});
d->groupSessions = connection->loadRoomMegolmSessions(this);
- d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this);
+ d->currentOutboundMegolmSession =
+ connection->loadCurrentOutboundMegolmSession(this->id());
if (d->shouldRotateMegolmSession()) {
d->currentOutboundMegolmSession = nullptr;
}
@@ -1525,7 +1446,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) const
auto* thumbnail = event->content()->thumbnailInfo();
Q_ASSERT(thumbnail != nullptr);
return connection()->getUrlForApi<MediaThumbnailJob>(
- thumbnail->url, thumbnail->imageSize);
+ thumbnail->url(), thumbnail->imageSize);
}
qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
return {};
@@ -1536,7 +1457,7 @@ QUrl Room::urlToDownload(const QString& eventId) const
if (auto* event = d->getEventWithFile(eventId)) {
auto* fileInfo = event->content()->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url);
+ return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url());
}
return {};
}
@@ -2101,12 +2022,12 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
if (!hasValidMegolmSession() || shouldRotateMegolmSession()) {
createMegolmSession();
}
- const auto devicesWithoutKey = getDevicesWithoutKey();
- sendMegolmSession(devicesWithoutKey);
+ sendMegolmSession(getDevicesWithoutKey());
const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson());
currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1);
- connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
if(!encrypted) {
qWarning(E2EE) << "Error encrypting message" << encrypted.error();
return {};
@@ -2130,7 +2051,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] {
auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end()) {
- qCWarning(EVENTS) << "Pending event for transaction" << txnId
+ qWarning(EVENTS) << "Pending event for transaction" << txnId
<< "not found - got synced so soon?";
return;
}
@@ -2140,7 +2061,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
Room::connect(call, &BaseJob::failure, q,
std::bind(&Room::Private::onEventSendingFailure, this,
txnId, call));
- Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] {
+ Room::connect(call, &BaseJob::success, q, [this, call, txnId] {
auto it = q->findPendingEvent(txnId);
if (it != unsyncedEvents.end()) {
if (it->deliveryStatus() != EventStatus::ReachedServer) {
@@ -2148,7 +2069,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
} else
- qCDebug(EVENTS) << "Pending event for transaction" << txnId
+ qDebug(EVENTS) << "Pending event for transaction" << txnId
<< "already merged";
emit q->messageSent(txnId, call->eventId());
@@ -2206,11 +2127,9 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
-// Lambda defers actual tr() invocation to the moment when translations are
-// initialised
-const auto FileTransferCancelledMsg = [] {
- return Room::tr("File transfer cancelled");
-};
+// Using a function defers actual tr() invocation to the moment when
+// translations are initialised
+auto FileTransferCancelledMsg() { return Room::tr("File transfer cancelled"); }
void Room::discardMessage(const QString& txnId)
{
@@ -2275,28 +2194,26 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl)
// Below, the upload job is used as a context object to clean up connections
const auto& transferJob = fileTransfers.value(txnId).job;
connect(q, &Room::fileTransferCompleted, transferJob,
- [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable<EncryptedFile> encryptedFile) {
- if (tId != txnId)
- return;
+ [this, txnId](const QString& tId, const QUrl&,
+ const FileSourceInfo& fileMetadata) {
+ if (tId != txnId)
+ return;
- const auto it = q->findPendingEvent(txnId);
- if (it != unsyncedEvents.end()) {
- it->setFileUploaded(mxcUri);
- if (encryptedFile) {
- it->setEncryptedFile(*encryptedFile);
- }
- emit q->pendingEventChanged(
- int(it - unsyncedEvents.begin()));
- doSendEvent(it->get());
- } else {
- // Normally in this situation we should instruct
- // the media server to delete the file; alas, there's no
- // API specced for that.
- qCWarning(MAIN) << "File uploaded to" << mxcUri
- << "but the event referring to it was "
- "cancelled";
- }
- });
+ const auto it = q->findPendingEvent(txnId);
+ if (it != unsyncedEvents.end()) {
+ it->setFileUploaded(fileMetadata);
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
+ doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN)
+ << "File uploaded to" << getUrlFromSourceInfo(fileMetadata)
+ << "but the event referring to it was "
+ "cancelled";
+ }
+ });
connect(q, &Room::fileTransferFailed, transferJob,
[this, txnId](const QString& tId) {
if (tId != txnId)
@@ -2322,13 +2239,13 @@ QString Room::postFile(const QString& plainText,
Q_ASSERT(content != nullptr && content->fileInfo() != nullptr);
const auto* const fileInfo = content->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- QFileInfo localFile { fileInfo->url.toLocalFile() };
+ QFileInfo localFile { fileInfo->url().toLocalFile() };
Q_ASSERT(localFile.isFile());
return d->doPostFile(
makeEvent<RoomMessageEvent>(
plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content),
- fileInfo->url);
+ fileInfo->url());
}
#if QT_VERSION_MAJOR < 6
@@ -2523,18 +2440,18 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__,
"localFilename should point at a local file");
auto fileName = localFilename.toLocalFile();
- Omittable<EncryptedFile> encryptedFile { none };
+ FileSourceInfo fileMetadata;
#ifdef Quotient_E2EE_ENABLED
QTemporaryFile tempFile;
if (usesEncryption()) {
tempFile.open();
QFile file(localFilename.toLocalFile());
file.open(QFile::ReadOnly);
- auto [e, data] = EncryptedFile::encryptFile(file.readAll());
+ QByteArray data;
+ std::tie(fileMetadata, data) = encryptFile(file.readAll());
tempFile.write(data);
tempFile.close();
fileName = QFileInfo(tempFile).absoluteFilePath();
- encryptedFile = e;
}
#endif
auto job = connection()->uploadFile(fileName, overrideContentType);
@@ -2545,17 +2462,13 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
d->fileTransfers[id].update(sent, total);
emit fileTransferProgress(id, sent, total);
});
- connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] {
- d->fileTransfers[id].status = FileTransferInfo::Completed;
- if (encryptedFile) {
- auto file = *encryptedFile;
- file.url = QUrl(job->contentUri());
- emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file);
- } else {
- emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none);
- }
-
- });
+ connect(job, &BaseJob::success, this,
+ [this, id, localFilename, job, fileMetadata]() mutable {
+ // The lambda is mutable to change encryptedFileMetadata
+ d->fileTransfers[id].status = FileTransferInfo::Completed;
+ setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri()));
+ emit fileTransferCompleted(id, localFilename, fileMetadata);
+ });
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, id, job->errorString()));
emit newFileTransfer(id, localFilename);
@@ -2588,11 +2501,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
<< "has an empty or malformed mxc URL; won't download";
return;
}
- const auto fileUrl = fileInfo->url;
+ const auto fileUrl = fileInfo->url();
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty()) { // Setup default file path
filePath =
- fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event);
+ fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event);
if (filePath.size() > 200) // If too long, elide in the middle
filePath.replace(128, filePath.size() - 192, "---");
@@ -2602,9 +2515,9 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
}
DownloadFileJob *job = nullptr;
#ifdef Quotient_E2EE_ENABLED
- if(fileInfo->file.has_value()) {
- auto file = *fileInfo->file;
- job = connection()->downloadFile(fileUrl, file, filePath);
+ if (auto* fileMetadata =
+ std::get_if<EncryptedFileMetadata>(&fileInfo->source)) {
+ job = connection()->downloadFile(fileUrl, *fileMetadata, filePath);
} else {
#endif
job = connection()->downloadFile(fileUrl, filePath);
@@ -2622,7 +2535,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] {
d->fileTransfers[eventId].status = FileTransferInfo::Completed;
emit fileTransferCompleted(
- eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()), none);
+ eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()));
});
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, eventId,
diff --git a/lib/room.h b/lib/room.h
index c3bdc4a0..0636c4bb 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -999,7 +999,8 @@ Q_SIGNALS:
void newFileTransfer(QString id, QUrl localFile);
void fileTransferProgress(QString id, qint64 progress, qint64 total);
- void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> encryptedFile);
+ void fileTransferCompleted(QString id, QUrl localFile,
+ FileSourceInfo fileMetadata);
void fileTransferFailed(QString id, QString errorMessage = {});
// fileTransferCancelled() is no more here; use fileTransferFailed() and
// check the transfer status instead
diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp
index 1eed865f..6bcd71cd 100644
--- a/quotest/quotest.cpp
+++ b/quotest/quotest.cpp
@@ -516,7 +516,7 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest,
&& e.hasFileContent()
&& e.content()->fileInfo()->originalName == fileName
&& testDownload(targetRoom->connection()->makeMediaUrl(
- e.content()->fileInfo()->url)));
+ e.content()->fileInfo()->url())));
},
[this, thisTest](const RoomEvent&) { FAIL_TEST(); });
});