aboutsummaryrefslogtreecommitdiff
path: root/autotests
diff options
context:
space:
mode:
Diffstat (limited to 'autotests')
-rw-r--r--autotests/CMakeLists.txt23
-rw-r--r--autotests/adjust-config.sh55
-rw-r--r--autotests/callcandidateseventtest.cpp60
-rwxr-xr-xautotests/run-tests.sh32
-rw-r--r--autotests/testfilecrypto.cpp22
-rw-r--r--autotests/testfilecrypto.h12
-rw-r--r--autotests/testgroupsession.cpp57
-rw-r--r--autotests/testgroupsession.h14
-rw-r--r--autotests/testkeyverification.cpp59
-rw-r--r--autotests/testolmaccount.cpp479
-rw-r--r--autotests/testolmaccount.h32
-rw-r--r--autotests/testolmsession.cpp86
-rw-r--r--autotests/testolmsession.h14
-rw-r--r--autotests/testolmutility.cpp128
-rw-r--r--autotests/testolmutility.h15
-rw-r--r--autotests/testutils.h32
-rw-r--r--autotests/utiltests.cpp45
17 files changed, 1165 insertions, 0 deletions
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
new file mode 100644
index 00000000..48edb168
--- /dev/null
+++ b/autotests/CMakeLists.txt
@@ -0,0 +1,23 @@
+# SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+include(CMakeParseArguments)
+
+function(QUOTIENT_ADD_TEST)
+ cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
+ add_executable(${ARG_NAME} ${ARG_NAME}.cpp)
+ target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test Quotient)
+ add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME})
+endfunction()
+
+quotient_add_test(NAME callcandidateseventtest)
+quotient_add_test(NAME utiltests)
+if(${PROJECT_NAME}_ENABLE_E2EE)
+ quotient_add_test(NAME testolmaccount)
+ quotient_add_test(NAME testgroupsession)
+ quotient_add_test(NAME testolmsession)
+ quotient_add_test(NAME testolmutility)
+ quotient_add_test(NAME testfilecrypto)
+ quotient_add_test(NAME testkeyverification)
+endif()
diff --git a/autotests/adjust-config.sh b/autotests/adjust-config.sh
new file mode 100644
index 00000000..68ea58ab
--- /dev/null
+++ b/autotests/adjust-config.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+CMD=""
+
+$CMD perl -pi -w -e \
+ '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;' homeserver.yaml
+
+(
+cat <<HEREDOC
+rc_message:
+ per_second: 10000
+ burst_count: 100000
+rc_registration:
+ per_second: 10000
+ burst_count: 30000
+rc_login:
+ address:
+ per_second: 10000
+ burst_count: 30000
+ account:
+ per_second: 10000
+ burst_count: 30000
+ failed_attempts:
+ per_second: 10000
+ burst_count: 30000
+rc_admin_redaction:
+ per_second: 1000
+ burst_count: 5000
+rc_joins:
+ local:
+ per_second: 10000
+ burst_count: 100000
+ remote:
+ per_second: 10000
+ burst_count: 100000
+HEREDOC
+) | $CMD tee -a homeserver.yaml
+
+$CMD perl -pi -w -e \
+ 's/^#enable_registration: false/enable_registration: true/g;' homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/^#enable_registration_without_verification: .+/enable_registration_without_verification: true/g;' homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/tls: false/tls: true/g;' homeserver.yaml
+$CMD perl -pi -w -e \
+ '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 localhost.tls.key -out localhost.tls.crt -days 365 -subj '/CN=localhost' -nodes
+
+$CMD chmod 0777 localhost.tls.crt
+$CMD chmod 0777 localhost.tls.key
diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp
new file mode 100644
index 00000000..257e0ef2
--- /dev/null
+++ b/autotests/callcandidateseventtest.cpp
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "events/callevents.h"
+
+#include <QtTest/QtTest>
+
+class TestCallCandidatesEvent : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void fromJson();
+};
+
+void TestCallCandidatesEvent::fromJson()
+{
+ auto document = QJsonDocument::fromJson(R"({
+ "age": 242352,
+ "content": {
+ "call_id": "12345",
+ "candidates": [
+ {
+ "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
+ "sdpMLineIndex": 0,
+ "sdpMid": "audio"
+ }
+ ],
+ "version": 0
+ },
+ "event_id": "$WLGTSEFSEF:localhost",
+ "origin_server_ts": 1431961217939,
+ "room_id": "!Cuyf34gef24t:localhost",
+ "sender": "@example:localhost",
+ "type": "m.call.candidates"
+ })");
+
+ QVERIFY(document.isObject());
+
+ auto object = document.object();
+
+ using namespace Quotient;
+ const auto& callCandidatesEvent = loadEvent<CallCandidatesEvent>(object);
+ QVERIFY(callCandidatesEvent);
+ QVERIFY(callCandidatesEvent->is<CallCandidatesEvent>());
+
+ QCOMPARE(callCandidatesEvent->version(), 0);
+ QCOMPARE(callCandidatesEvent->callId(), QStringLiteral("12345"));
+ QCOMPARE(callCandidatesEvent->candidates().count(), 1);
+
+ const auto& candidate = callCandidatesEvent->candidates().at(0).toObject();
+ QCOMPARE(candidate.value("sdpMid").toString(), QStringLiteral("audio"));
+ QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0);
+ QCOMPARE(candidate.value("candidate").toString(),
+ QStringLiteral("candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0"));
+}
+
+QTEST_APPLESS_MAIN(TestCallCandidatesEvent)
+#include "callcandidateseventtest.moc"
diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh
new file mode 100755
index 00000000..e7a228ef
--- /dev/null
+++ b/autotests/run-tests.sh
@@ -0,0 +1,32 @@
+mkdir -p data
+chmod 0777 data
+
+SYNAPSE_IMAGE='matrixdotorg/synapse:v1.61.1'
+
+rm ~/.local/share/testolmaccount -rf
+docker run -v `pwd`/data:/data --rm \
+ -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no $SYNAPSE_IMAGE 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 $SYNAPSE_IMAGE
+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
+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
+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"
+
+GTEST_COLOR=1 ctest --verbose "$@"
+
diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp
new file mode 100644
index 00000000..29521060
--- /dev/null
+++ b/autotests/testfilecrypto.cpp
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "testfilecrypto.h"
+
+#include "events/filesourceinfo.h"
+
+#include <qtest.h>
+
+using namespace Quotient;
+void TestFileCrypto::encryptDecryptData()
+{
+ QByteArray data = "ABCDEF";
+ 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());
+ QCOMPARE(decrypted, data);
+}
+QTEST_APPLESS_MAIN(TestFileCrypto)
diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h
new file mode 100644
index 00000000..9096a8c7
--- /dev/null
+++ b/autotests/testfilecrypto.h
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QtTest/QtTest>
+
+class TestFileCrypto : public QObject
+{
+ Q_OBJECT
+private Q_SLOTS:
+ void encryptDecryptData();
+};
diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp
new file mode 100644
index 00000000..1054a160
--- /dev/null
+++ b/autotests/testgroupsession.cpp
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "testgroupsession.h"
+#include "e2ee/qolminboundsession.h"
+#include "e2ee/qolmoutboundsession.h"
+#include "e2ee/qolmutils.h"
+
+using namespace Quotient;
+
+void TestGroupSession::groupSessionPicklingValid()
+{
+ auto ogs = QOlmOutboundGroupSession::create();
+ const auto ogsId = ogs->sessionId();
+ QVERIFY(QByteArray::fromBase64(ogsId).size() > 0);
+ QCOMPARE(0, ogs->sessionMessageIndex());
+
+ auto&& ogsPickled = ogs->pickle(Unencrypted {});
+ auto ogs2 =
+ QOlmOutboundGroupSession::unpickle(std::move(ogsPickled), Unencrypted{})
+ .value();
+ QCOMPARE(ogsId, ogs2->sessionId());
+
+ auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value();
+ const auto igsId = igs->sessionId();
+ // ID is valid base64?
+ QVERIFY(QByteArray::fromBase64(igsId).size() > 0);
+
+ //// no messages have been sent yet
+ QCOMPARE(0, igs->firstKnownIndex());
+
+ auto igsPickled = igs->pickle(Unencrypted {});
+ igs = QOlmInboundGroupSession::unpickle(std::move(igsPickled), Unencrypted{})
+ .value();
+ QCOMPARE(igsId, igs->sessionId());
+}
+
+void TestGroupSession::groupSessionCryptoValid()
+{
+ auto ogs = QOlmOutboundGroupSession::create();
+ auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value();
+ QCOMPARE(ogs->sessionId(), igs->sessionId());
+
+ const auto plainText = "Hello world!";
+ const auto ciphertext = ogs->encrypt(plainText);
+ // ciphertext valid base64?
+ QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0);
+
+ const auto decryptionResult = igs->decrypt(ciphertext).value();
+
+ //// correct plaintext?
+ QCOMPARE(plainText, decryptionResult.first);
+
+ QCOMPARE(0, decryptionResult.second);
+}
+QTEST_GUILESS_MAIN(TestGroupSession)
diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h
new file mode 100644
index 00000000..6edf0d16
--- /dev/null
+++ b/autotests/testgroupsession.h
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QTest>
+
+class TestGroupSession : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void groupSessionPicklingValid();
+ void groupSessionCryptoValid();
+};
diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp
new file mode 100644
index 00000000..1fa6d8c6
--- /dev/null
+++ b/autotests/testkeyverification.cpp
@@ -0,0 +1,59 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#include <QTest>
+#include "testutils.h"
+#include <qt_connection_util.h>
+
+class TestKeyVerificationSession : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testVerification()
+ {
+ CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop")
+ CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone")
+
+ QPointer<KeyVerificationSession> aSession{};
+ connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) {
+ aSession = session;
+ QVERIFY(session->remoteDeviceId() == b->deviceId());
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY);
+ connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){
+ QVERIFY(session->state() == KeyVerificationSession::ACCEPTED || session->state() == KeyVerificationSession::READY);
+ connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION);
+ session->sendMac();
+ });
+ });
+ });
+ a->startKeyVerificationSession(b->deviceId());
+ connect(b.get(), &Connection::newKeyVerificationSession, this, [=](KeyVerificationSession* session) {
+ QVERIFY(session->remoteDeviceId() == a->deviceId());
+ QVERIFY(session->state() == KeyVerificationSession::INCOMING);
+ session->sendReady();
+ // KeyVerificationSession::READY is skipped because we have only one method
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT);
+ connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY || session->state() == KeyVerificationSession::ACCEPTED);
+ connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=]() {
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION);
+ QVERIFY(aSession);
+ QVERIFY(aSession->sasEmojis() == session->sasEmojis());
+ session->sendMac();
+ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC);
+ });
+ });
+
+ });
+ b->syncLoop();
+ a->syncLoop();
+ QSignalSpy spy(aSession, &KeyVerificationSession::finished);
+ spy.wait(10000);
+ }
+};
+QTEST_GUILESS_MAIN(TestKeyVerificationSession)
+#include "testkeyverification.moc"
diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp
new file mode 100644
index 00000000..53a0c955
--- /dev/null
+++ b/autotests/testolmaccount.cpp
@@ -0,0 +1,479 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-FileCopyrightText: 2020 mtxclient developers
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "testolmaccount.h"
+
+#include <connection.h>
+#include <csapi/joining.h>
+#include <e2ee/qolmaccount.h>
+#include <e2ee/qolmutility.h>
+#include <events/encryptionevent.h>
+#include <events/filesourceinfo.h>
+#include <networkaccessmanager.h>
+#include <room.h>
+
+#include "testutils.h"
+
+using namespace Quotient;
+
+void TestOlmAccount::pickleUnpickledTest()
+{
+ QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount.createNewAccount();
+ auto identityKeys = olmAccount.identityKeys();
+ auto pickled = olmAccount.pickle(Unencrypted{});
+ QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ auto unpickleResult = olmAccount2.unpickle(std::move(pickled),
+ Unencrypted{});
+ QCOMPARE(unpickleResult, 0);
+ auto identityKeys2 = olmAccount2.identityKeys();
+ QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519);
+ QCOMPARE(identityKeys.ed25519, identityKeys2.ed25519);
+}
+
+void TestOlmAccount::identityKeysValid()
+{
+ QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount.createNewAccount();
+ const auto identityKeys = olmAccount.identityKeys();
+ const auto curve25519 = identityKeys.curve25519;
+ const auto ed25519 = identityKeys.ed25519;
+ // verify encoded keys length
+ QCOMPARE(curve25519.size(), 43);
+ QCOMPARE(ed25519.size(), 43);
+
+ // encoded as valid base64?
+ QVERIFY(QByteArray::fromBase64(curve25519).size() > 0);
+ QVERIFY(QByteArray::fromBase64(ed25519).size() > 0);
+}
+
+void TestOlmAccount::signatureValid()
+{
+ QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount.createNewAccount();
+ const auto message = "Hello world!";
+ const auto signature = olmAccount.sign(message);
+ QVERIFY(QByteArray::fromBase64(signature).size() > 0);
+
+ QOlmUtility utility;
+ const auto identityKeys = olmAccount.identityKeys();
+ const auto ed25519Key = identityKeys.ed25519;
+ QVERIFY(utility.ed25519Verify(ed25519Key, message, signature));
+}
+
+void TestOlmAccount::oneTimeKeysValid()
+{
+ QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount.createNewAccount();
+ const auto maxNumberOfOneTimeKeys = olmAccount.maxNumberOfOneTimeKeys();
+ QCOMPARE(100, maxNumberOfOneTimeKeys);
+
+ const auto oneTimeKeysEmpty = olmAccount.oneTimeKeys();
+ QVERIFY(oneTimeKeysEmpty.curve25519().isEmpty());
+
+ olmAccount.generateOneTimeKeys(20);
+ const auto oneTimeKeysFilled = olmAccount.oneTimeKeys();
+ QCOMPARE(20, oneTimeKeysFilled.curve25519().count());
+}
+
+void TestOlmAccount::deviceKeys()
+{
+ // copied from mtxclient
+ DeviceKeys device1;
+ device1.userId = "@alice:example.com";
+ device1.deviceId = "JLAFKJWSCS";
+ device1.keys = {{"curve25519:JLAFKJWSCS", "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"},
+ {"ed25519:JLAFKJWSCS", "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"}};
+
+ // TODO that should be the default value
+ device1.algorithms =
+ QStringList { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey };
+
+ device1.signatures = {
+ {"@alice:example.com",
+ {{"ed25519:JLAFKJWSCS",
+ "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/"
+ "a+myXS367WT6NAIcBA"}}}};
+
+ QJsonObject j;
+ JsonObjectConverter<DeviceKeys>::dumpTo(j, device1);
+ QJsonDocument doc(j);
+ QCOMPARE(doc.toJson(QJsonDocument::Compact), "{\"algorithms\":[\"m.olm.v1.curve25519-aes-sha2\",\"m.megolm.v1.aes-sha2\"],"
+ "\"device_id\":\"JLAFKJWSCS\",\"keys\":{\"curve25519:JLAFKJWSCS\":"
+ "\"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI\",\"ed25519:JLAFKJWSCS\":"
+ "\"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/"
+ "HJkoKuEww9ULI\"},\"signatures\":{\"@alice:example.com\":{\"ed25519:JLAFKJWSCS\":"
+ "\"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/"
+ "a+myXS367WT6NAIcBA\"}},\"user_id\":\"@alice:example.com\"}");
+
+ auto doc2 = QJsonDocument::fromJson(R"({
+ "user_id": "@alice:example.com",
+ "device_id": "JLAFKJWSCS",
+ "algorithms": [
+ "m.olm.v1.curve25519-aes-sha2",
+ "m.megolm.v1.aes-sha2"
+ ],
+ "keys": {
+ "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
+ "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
+ },
+ "signatures": {
+ "@alice:example.com": {
+ "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
+ }
+ },
+ "unsigned": {
+ "device_display_name": "Alice's mobile phone"
+ }
+ })");
+
+ DeviceKeys device2;
+ JsonObjectConverter<DeviceKeys>::fillFrom(doc2.object(), device2);
+
+ QCOMPARE(device2.userId, device1.userId);
+ QCOMPARE(device2.deviceId, device1.deviceId);
+ QCOMPARE(device2.keys, device1.keys);
+ QCOMPARE(device2.algorithms, device1.algorithms);
+ QCOMPARE(device2.signatures, device1.signatures);
+
+ // UnsignedDeviceInfo is missing from the generated DeviceKeys object :(
+ // QCOMPARE(device2.unsignedInfo.deviceDisplayName, "Alice's mobile phone");
+}
+
+void TestOlmAccount::encryptedFile()
+{
+ auto doc = QJsonDocument::fromJson(R"({
+ "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe",
+ "v": "v2",
+ "key": {
+ "alg": "A256CTR",
+ "ext": true,
+ "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0",
+ "key_ops": ["encrypt","decrypt"],
+ "kty": "oct"
+ },
+ "iv": "w+sE15fzSc0AAAAAAAAAAA",
+ "hashes": {
+ "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA"
+ }})");
+
+ const auto file = fromJson<EncryptedFileMetadata>(doc);
+
+ QCOMPARE(file.v, "v2");
+ QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA");
+ QCOMPARE(file.hashes["sha256"], "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA");
+ QCOMPARE(file.key.alg, "A256CTR");
+ QCOMPARE(file.key.ext, true);
+ QCOMPARE(file.key.k, "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0");
+ QCOMPARE(file.key.keyOps.count(), 2);
+ QCOMPARE(file.key.kty, "oct");
+}
+
+void TestOlmAccount::uploadIdentityKey()
+{
+ CREATE_CONNECTION(conn, "alice1", "secret", "AlicePhone")
+
+ auto olmAccount = conn->olmAccount();
+ auto idKeys = olmAccount->identityKeys();
+
+ QVERIFY(idKeys.curve25519.size() > 10);
+
+ UnsignedOneTimeKeys unused;
+ auto request = olmAccount->createUploadKeyRequest(unused);
+ connect(request, &BaseJob::result, this, [request, conn] {
+ 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);
+ QVERIFY(spy3.wait(10000));
+}
+
+void TestOlmAccount::uploadOneTimeKeys()
+{
+ CREATE_CONNECTION(conn, "alice2", "secret", "AlicePhone")
+ auto olmAccount = conn->olmAccount();
+
+ auto nKeys = olmAccount->generateOneTimeKeys(5);
+ QCOMPARE(nKeys, 5);
+
+ auto oneTimeKeys = olmAccount->oneTimeKeys();
+
+ OneTimeKeys oneTimeKeysHash;
+ const auto curve = oneTimeKeys.curve25519();
+ for (const auto &[keyId, key] : asKeyValueRange(curve)) {
+ oneTimeKeysHash["curve25519:"+keyId] = key;
+ }
+ auto request = new UploadKeysJob(none, oneTimeKeysHash);
+ connect(request, &BaseJob::result, this, [request, conn] {
+ if (!request->status().good())
+ QFAIL("upload failed");
+ QCOMPARE(request->oneTimeKeyCounts().value(Curve25519Key), 5);
+ });
+ conn->run(request);
+ QSignalSpy spy3(request, &BaseJob::result);
+ QVERIFY(spy3.wait(10000));
+}
+
+void TestOlmAccount::uploadSignedOneTimeKeys()
+{
+ CREATE_CONNECTION(conn, "alice3", "secret", "AlicePhone")
+ auto olmAccount = conn->olmAccount();
+ auto nKeys = olmAccount->generateOneTimeKeys(5);
+ QCOMPARE(nKeys, 5);
+
+ auto oneTimeKeys = olmAccount->oneTimeKeys();
+ OneTimeKeys oneTimeKeysHash;
+ const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys);
+ for (const auto &[keyId, key] : asKeyValueRange(signedKey)) {
+ oneTimeKeysHash[keyId] = key;
+ }
+ auto request = new UploadKeysJob(none, oneTimeKeysHash);
+ connect(request, &BaseJob::result, this, [request, nKeys, conn] {
+ if (!request->status().good())
+ QFAIL("upload failed");
+ QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), nKeys);
+ });
+ conn->run(request);
+ QSignalSpy spy3(request, &BaseJob::result);
+ QVERIFY(spy3.wait(10000));
+}
+
+void TestOlmAccount::uploadKeys()
+{
+ CREATE_CONNECTION(conn, "alice4", "secret", "AlicePhone")
+ auto olmAccount = conn->olmAccount();
+ auto idks = olmAccount->identityKeys();
+ olmAccount->generateOneTimeKeys(1);
+ auto otks = olmAccount->oneTimeKeys();
+ auto request = olmAccount->createUploadKeyRequest(otks);
+ connect(request, &BaseJob::result, this, [request, conn] {
+ if (!request->status().good())
+ QFAIL("upload failed");
+ QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
+ });
+ conn->run(request);
+ QSignalSpy spy3(request, &BaseJob::result);
+ QVERIFY(spy3.wait(10000));
+}
+
+void TestOlmAccount::queryTest()
+{
+ CREATE_CONNECTION(alice, "alice5", "secret", "AlicePhone")
+ CREATE_CONNECTION(bob, "bob1", "secret", "BobPhone")
+
+ // Create and upload keys for both users.
+ auto aliceOlm = alice->olmAccount();
+ aliceOlm->generateOneTimeKeys(1);
+ auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys());
+ connect(aliceRes, &BaseJob::result, this, [aliceRes] {
+ QCOMPARE(aliceRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
+ });
+ QSignalSpy spy(aliceRes, &BaseJob::result);
+ alice->run(aliceRes);
+ QVERIFY(spy.wait(10000));
+
+ auto bobOlm = bob->olmAccount();
+ bobOlm->generateOneTimeKeys(1);
+ auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys());
+ connect(bobRes, &BaseJob::result, this, [bobRes] {
+ QCOMPARE(bobRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
+ });
+ QSignalSpy spy1(bobRes, &BaseJob::result);
+ bob->run(bobRes);
+ QVERIFY(spy1.wait(10000));
+
+ {
+ // Each user is requests each other's keys.
+ QHash<QString, QStringList> deviceKeys;
+ deviceKeys[bob->userId()] = QStringList();
+ auto job = alice->callApi<QueryKeysJob>(deviceKeys);
+ QSignalSpy spy(job, &BaseJob::result);
+ connect(job, &BaseJob::result, this, [job, bob, bobOlm] {
+ QCOMPARE(job->failures().size(), 0);
+
+ const auto& aliceDevices = job->deviceKeys().value(bob->userId());
+ QVERIFY(!aliceDevices.empty());
+
+ const auto& devKeys = aliceDevices.value(bob->deviceId());
+ QCOMPARE(devKeys.userId, bob->userId());
+ QCOMPARE(devKeys.deviceId, bob->deviceId());
+ QCOMPARE(devKeys.keys, bobOlm->deviceKeys().keys);
+ QCOMPARE(devKeys.signatures, bobOlm->deviceKeys().signatures);
+ });
+ QVERIFY(spy.wait(10000));
+ }
+
+ {
+ QHash<QString, QStringList> deviceKeys;
+ deviceKeys[alice->userId()] = QStringList();
+ auto job = bob->callApi<QueryKeysJob>(deviceKeys);
+ QSignalSpy spy(job, &BaseJob::result);
+ connect(job, &BaseJob::result, this, [job, alice, aliceOlm] {
+ QCOMPARE(job->failures().size(), 0);
+
+ const auto& bobDevices = job->deviceKeys().value(alice->userId());
+ QVERIFY(!bobDevices.empty());
+
+ auto devKeys = bobDevices[alice->deviceId()];
+ QCOMPARE(devKeys.userId, alice->userId());
+ QCOMPARE(devKeys.deviceId, alice->deviceId());
+ QCOMPARE(devKeys.keys, aliceOlm->deviceKeys().keys);
+ QCOMPARE(devKeys.signatures, aliceOlm->deviceKeys().signatures);
+ });
+ QVERIFY(spy.wait(10000));
+ }
+}
+
+void TestOlmAccount::claimKeys()
+{
+ CREATE_CONNECTION(alice, "alice6", "secret", "AlicePhone")
+ CREATE_CONNECTION(bob, "bob2", "secret", "BobPhone")
+
+ // Bob uploads his keys.
+ auto *bobOlm = bob->olmAccount();
+ bobOlm->generateOneTimeKeys(1);
+ auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys());
+
+ connect(request, &BaseJob::result, this, [request, bob] {
+ QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1);
+ });
+ bob->run(request);
+
+ QSignalSpy requestSpy(request, &BaseJob::result);
+ QVERIFY(requestSpy.wait(10000));
+
+ // Alice retrieves bob's keys & claims one signed one-time key.
+ QHash<QString, QStringList> deviceKeys;
+ deviceKeys[bob->userId()] = QStringList();
+ 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)
+ && std::holds_alternative<SignedOneTimeKey>(it.value()))
+ return;
+ }
+ QFAIL("The claimed one time key is not in /claim response");
+ });
+ QSignalSpy completionSpy(claimKeysJob, &BaseJob::result);
+ QVERIFY(completionSpy.wait(10000));
+}
+
+void TestOlmAccount::claimMultipleKeys()
+{
+ // Login with alice multiple times
+ CREATE_CONNECTION(alice, "alice7", "secret", "AlicePhone")
+ CREATE_CONNECTION(alice1, "alice7", "secret", "AlicePhone")
+ CREATE_CONNECTION(alice2, "alice7", "secret", "AlicePhone")
+
+ auto olm = alice->olmAccount();
+ olm->generateOneTimeKeys(10);
+ auto res = olm->createUploadKeyRequest(olm->oneTimeKeys());
+ QSignalSpy spy(res, &BaseJob::result);
+ connect(res, &BaseJob::result, this, [res] {
+ QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
+ });
+ alice->run(res);
+ QVERIFY(spy.wait(10000));
+
+ auto olm1 = alice1->olmAccount();
+ olm1->generateOneTimeKeys(10);
+ auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys());
+ QSignalSpy spy1(res1, &BaseJob::result);
+ connect(res1, &BaseJob::result, this, [res1] {
+ QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
+ });
+ alice1->run(res1);
+ QVERIFY(spy1.wait(10000));
+
+ auto olm2 = alice2->olmAccount();
+ olm2->generateOneTimeKeys(10);
+ auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys());
+ QSignalSpy spy2(res2, &BaseJob::result);
+ connect(res2, &BaseJob::result, this, [res2] {
+ QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10);
+ });
+ alice2->run(res2);
+ QVERIFY(spy2.wait(10000));
+
+ // Bob will claim all keys from alice
+ CREATE_CONNECTION(bob, "bob3", "secret", "BobPhone")
+
+ QStringList devices_;
+ devices_ << alice->deviceId()
+ << alice1->deviceId()
+ << alice2->deviceId();
+
+ QHash<QString, QHash<QString, QString>> oneTimeKeys;
+ oneTimeKeys[alice->userId()] = QHash<QString, QString>();
+ for (const auto &d : devices_) {
+ oneTimeKeys[alice->userId()][d] = SignedCurve25519Key;
+ }
+ auto job = bob->callApi<ClaimKeysJob>(oneTimeKeys);
+ QSignalSpy jobSpy(job, &BaseJob::finished);
+ QVERIFY(jobSpy.wait(10000));
+ const auto userId = alice->userId();
+
+ QCOMPARE(job->oneTimeKeys().value(userId).size(), 3);
+}
+
+void TestOlmAccount::enableEncryption()
+{
+ CREATE_CONNECTION(alice, "alice9", "secret", "AlicePhone")
+
+ auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {});
+ QSignalSpy createRoomSpy(job, &BaseJob::success);
+ QVERIFY(createRoomSpy.wait(10000));
+ alice->sync();
+ connect(alice.get(), &Connection::syncDone, this, [alice](){
+ qDebug() << "foo";
+ alice->sync();
+ });
+ while(alice->roomsCount(JoinState::Join) == 0) {
+ QThread::sleep(100);
+ }
+ auto room = alice->rooms(JoinState::Join)[0];
+ room->activateEncryption();
+ QSignalSpy encryptionSpy(room, &Room::encryption);
+ QVERIFY(encryptionSpy.wait(10000));
+ QVERIFY(room->usesEncryption());
+}
+
+QTEST_GUILESS_MAIN(TestOlmAccount)
diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h
new file mode 100644
index 00000000..367092f6
--- /dev/null
+++ b/autotests/testolmaccount.h
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QtTest/QtTest>
+#include <QString>
+
+namespace Quotient {
+ class Connection;
+}
+
+class TestOlmAccount : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void pickleUnpickledTest();
+ void identityKeysValid();
+ void signatureValid();
+ void oneTimeKeysValid();
+ //void removeOneTimeKeys();
+ void deviceKeys();
+ void encryptedFile();
+ void uploadIdentityKey();
+ void uploadOneTimeKeys();
+ void uploadSignedOneTimeKeys();
+ void uploadKeys();
+ void queryTest();
+ void claimKeys();
+ void claimMultipleKeys();
+ void enableEncryption();
+};
diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp
new file mode 100644
index 00000000..18b0d5f2
--- /dev/null
+++ b/autotests/testolmsession.cpp
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmsession.h"
+#include "testolmsession.h"
+
+using namespace Quotient;
+
+std::pair<QOlmSessionPtr, QOlmSessionPtr> createSessionPair()
+{
+ QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x");
+ QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr");
+ auto accountA = QOlmAccount(u"accountA:foo.com", u"Device1UserA");
+ if (accountA.unpickle(std::move(pickledAccountA), Unencrypted{})
+ != OLM_SUCCESS)
+ qFatal("Failed to unpickle account A: %s", accountA.lastError());
+
+ auto accountB = QOlmAccount(u"accountB:foo.com", u"Device1UserB");
+ if (accountB.unpickle(std::move(pickledAccountB), Unencrypted{})
+ != OLM_SUCCESS)
+ qFatal("Failed to unpickle account B: %s", accountB.lastError());
+
+
+ const QByteArray identityKeyA("qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o");
+ const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk");
+ const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg");
+ const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ");
+ auto outbound =
+ accountA.createOutboundSession(identityKeyB, oneTimeKeyB).value();
+
+ const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey
+
+ if (preKey.type() != QOlmMessage::PreKey) {
+ // We can't call QFail here because it's an helper function returning a value
+ throw "Wrong first message type received, can't create session";
+ }
+ auto inbound = accountB.createInboundSession(preKey).value();
+ return { std::move(inbound), std::move(outbound) };
+}
+
+void TestOlmSession::olmOutboundSessionCreation()
+{
+ const auto [_, outboundSession] = createSessionPair();
+ QCOMPARE(0, outboundSession->hasReceivedMessage());
+}
+
+void TestOlmSession::olmEncryptDecrypt()
+{
+ const auto [inboundSession, outboundSession] = createSessionPair();
+ const auto encrypted = outboundSession->encrypt("Hello world!");
+ if (encrypted.type() == QOlmMessage::PreKey) {
+ QOlmMessage m(encrypted); // clone
+ QVERIFY(inboundSession->matchesInboundSession(m));
+ }
+
+ const auto decrypted = inboundSession->decrypt(encrypted).value();
+
+ QCOMPARE(decrypted, "Hello world!");
+}
+
+void TestOlmSession::correctSessionOrdering()
+{
+ // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM
+ auto session1 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{}).value();
+ // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0
+ auto session2 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {}).value();
+ // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME
+ auto session3 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{}).value();
+
+ const auto session1Id = session1->sessionId();
+ const auto session2Id = session2->sessionId();
+ const auto session3Id = session3->sessionId();
+
+ std::vector<QOlmSessionPtr> sessionList;
+ sessionList.push_back(std::move(session1));
+ sessionList.push_back(std::move(session2));
+ sessionList.push_back(std::move(session3));
+
+ std::sort(sessionList.begin(), sessionList.end());
+ QCOMPARE(sessionList[0]->sessionId(), session2Id);
+ QCOMPARE(sessionList[1]->sessionId(), session3Id);
+ QCOMPARE(sessionList[2]->sessionId(), session1Id);
+}
+
+QTEST_GUILESS_MAIN(TestOlmSession)
diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h
new file mode 100644
index 00000000..9a5798fa
--- /dev/null
+++ b/autotests/testolmsession.h
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QtTest/QtTest>
+
+class TestOlmSession : public QObject
+{
+ Q_OBJECT
+private Q_SLOTS:
+ void olmOutboundSessionCreation();
+ void olmEncryptDecrypt();
+ void correctSessionOrdering();
+};
diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp
new file mode 100644
index 00000000..4de5afdf
--- /dev/null
+++ b/autotests/testolmutility.cpp
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "testolmutility.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolmutility.h"
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+void TestOlmUtility::canonicalJSON()
+{
+ // Examples taken from
+ // https://matrix.org/docs/spec/appendices.html#canonical-json
+ auto data = QJsonDocument::fromJson(QByteArrayLiteral(R"({
+ "auth": {
+ "success": true,
+ "mxid": "@john.doe:example.com",
+ "profile": {
+ "display_name": "John Doe",
+ "three_pids": [{
+ "medium": "email",
+ "address": "john.doe@example.org"
+ }, {
+ "medium": "msisdn",
+ "address": "123456789"
+ }]
+ }}})"));
+
+ QCOMPARE(data.toJson(QJsonDocument::Compact),
+ "{\"auth\":{\"mxid\":\"@john.doe:example.com\",\"profile\":{\"display_name\":\"John "
+ "Doe\",\"three_pids\":[{\"address\":\"john.doe@example.org\",\"medium\":\"email\"},{"
+ "\"address\":\"123456789\",\"medium\":\"msisdn\"}]},\"success\":true}}");
+
+ auto data0 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"b":"2","a":"1"})"));
+ QCOMPARE(data0.toJson(QJsonDocument::Compact), "{\"a\":\"1\",\"b\":\"2\"}");
+
+ auto data1 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "本": 2, "日": 1 })"));
+ QCOMPARE(data1.toJson(QJsonDocument::Compact), "{\"日\":1,\"本\":2}");
+
+ auto data2 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"a": "\u65E5"})"));
+ QCOMPARE(data2.toJson(QJsonDocument::Compact), "{\"a\":\"日\"}");
+
+ auto data3 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "a": null })"));
+ QCOMPARE(data3.toJson(QJsonDocument::Compact), "{\"a\":null}");
+}
+
+void TestOlmUtility::verifySignedOneTimeKey()
+{
+ QOlmAccount aliceOlm { u"@alice:matrix.org", u"aliceDevice" };
+ aliceOlm.createNewAccount();
+ aliceOlm.generateOneTimeKeys(1);
+ auto keys = aliceOlm.oneTimeKeys();
+
+ auto firstKey = *keys.curve25519().begin();
+ auto msgObj = QJsonObject({{"key", firstKey}});
+ auto sig = aliceOlm.sign(msgObj);
+
+ auto msg = QJsonDocument(msgObj).toJson(QJsonDocument::Compact);
+
+ auto utilityBuf = new uint8_t[olm_utility_size()];
+ auto utility = olm_utility(utilityBuf);
+
+
+ QByteArray signatureBuf1(sig.length(), '\0');
+ std::copy(sig.begin(), sig.end(), signatureBuf1.begin());
+
+ auto res =
+ olm_ed25519_verify(utility, aliceOlm.identityKeys().ed25519.data(),
+ aliceOlm.identityKeys().ed25519.size(), msg.data(),
+ msg.size(), sig.data(), sig.size());
+
+ QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS");
+ QCOMPARE(res, 0);
+
+ delete[](reinterpret_cast<uint8_t *>(utility));
+
+ QOlmUtility utility2;
+ auto res2 = utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg,
+ signatureBuf1);
+
+ //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS");
+ QVERIFY(res2);
+}
+
+void TestOlmUtility::validUploadKeysRequest()
+{
+ const auto userId = QStringLiteral("@alice:matrix.org");
+ const auto deviceId = QStringLiteral("FKALSOCCC");
+
+ QOlmAccount alice { userId, deviceId };
+ alice.createNewAccount();
+ alice.generateOneTimeKeys(1);
+
+ auto idSig = alice.signIdentityKeys();
+
+ QJsonObject body
+ {
+ {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}},
+ {"user_id", userId},
+ {"device_id", deviceId},
+ {"keys",
+ QJsonObject{
+ {QStringLiteral("curve25519:") + deviceId, QString::fromUtf8(alice.identityKeys().curve25519)},
+ {QStringLiteral("ed25519:") + deviceId, QString::fromUtf8(alice.identityKeys().ed25519)}
+ }
+ },
+ {"signatures",
+ QJsonObject{
+ {userId,
+ QJsonObject{
+ {"ed25519:" + deviceId, QString::fromUtf8(idSig)}
+ }
+ }
+ }
+ }
+ };
+
+ DeviceKeys deviceKeys = alice.deviceKeys();
+ QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact),
+ QJsonDocument(body).toJson(QJsonDocument::Compact));
+
+ QVERIFY(verifyIdentitySignature(fromJson<DeviceKeys>(body), deviceId, userId));
+ QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId));
+}
+QTEST_GUILESS_MAIN(TestOlmUtility)
diff --git a/autotests/testolmutility.h b/autotests/testolmutility.h
new file mode 100644
index 00000000..f2a3ca45
--- /dev/null
+++ b/autotests/testolmutility.h
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QTest>
+
+class TestOlmUtility : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void canonicalJSON();
+ void verifySignedOneTimeKey();
+ void validUploadKeysRequest();
+};
diff --git a/autotests/testutils.h b/autotests/testutils.h
new file mode 100644
index 00000000..7d016a34
--- /dev/null
+++ b/autotests/testutils.h
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <connection.h>
+#include <networkaccessmanager.h>
+
+#include <QtTest/QSignalSpy>
+
+using namespace Quotient;
+
+#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \
+ NetworkAccessManager::instance()->ignoreSslErrors(true); \
+ auto VAR = std::make_shared<Connection>(); \
+ (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \
+ connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \
+ (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \
+ }); \
+ connect((VAR).get(), &Connection::networkError, [](const QString& error) { \
+ QWARN(qUtf8Printable(error)); \
+ QFAIL("Network error: make sure synapse is running"); \
+ }); \
+ connect((VAR).get(), &Connection::loginError, [](const QString& error) { \
+ QWARN(qUtf8Printable(error)); \
+ QFAIL("Login failed"); \
+ }); \
+ QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \
+ QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \
+ QVERIFY(spy##VAR.wait(10000)); \
+ QVERIFY(spy2##VAR.wait(10000));
diff --git a/autotests/utiltests.cpp b/autotests/utiltests.cpp
new file mode 100644
index 00000000..e3ec63d0
--- /dev/null
+++ b/autotests/utiltests.cpp
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "omittable.h"
+
+#include <QtTest/QtTest>
+
+// compile-time Omittable<> tests
+using namespace Quotient;
+
+Omittable<int> testFn(bool) { return 0; }
+bool testFn2(int) { return false; }
+static_assert(
+ std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)),
+ Omittable<int>>);
+static_assert(
+ std::is_same_v<
+ decltype(std::declval<Omittable<bool>>().then_or(testFn, 0)), int>);
+static_assert(
+ std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)),
+ Omittable<int>>);
+static_assert(std::is_same_v<decltype(std::declval<Omittable<int>>()
+ .then(testFn2)
+ .then(testFn)),
+ Omittable<int>>);
+static_assert(std::is_same_v<decltype(std::declval<Omittable<bool>>()
+ .then(testFn)
+ .then_or(testFn2, false)),
+ bool>);
+
+constexpr auto visitTestFn(int, bool) { return false; }
+static_assert(
+ std::is_same_v<Omittable<bool>, decltype(lift(testFn2, Omittable<int>()))>);
+static_assert(std::is_same_v<Omittable<bool>,
+ decltype(lift(visitTestFn, Omittable<int>(),
+ Omittable<bool>()))>);
+
+class TestUtils : public QObject {
+ Q_OBJECT
+private Q_SLOTS:
+ // TODO
+};
+
+QTEST_APPLESS_MAIN(TestUtils)
+#include "utiltests.moc"