aboutsummaryrefslogtreecommitdiff
path: root/autotests
diff options
context:
space:
mode:
authorAlexey Rusakov <Kitsune-Ral@users.sf.net>2022-02-26 12:57:08 +0100
committerGitHub <noreply@github.com>2022-02-26 12:57:08 +0100
commit62039b527507aa6c45fbf7ac787da6234d2faac3 (patch)
treebe84dde38d9328f6278fce1b4f9e900d7e9dce2a /autotests
parent2aab2a0c95bbd3f12890badb58a825bb57d8e613 (diff)
parentb0e1455989405ef46eb6d9ed2cd559a1164d04f4 (diff)
downloadlibquotient-62039b527507aa6c45fbf7ac787da6234d2faac3.tar.gz
libquotient-62039b527507aa6c45fbf7ac787da6234d2faac3.zip
Merge pull request #477 from TobiasFella/work/readencryptedmessages
Diffstat (limited to 'autotests')
-rw-r--r--autotests/CMakeLists.txt6
-rwxr-xr-xautotests/run-tests.sh34
-rw-r--r--autotests/testgroupsession.cpp54
-rw-r--r--autotests/testgroupsession.h14
-rw-r--r--autotests/testolmaccount.cpp510
-rw-r--r--autotests/testolmaccount.h32
-rw-r--r--autotests/testolmsession.cpp80
-rw-r--r--autotests/testolmsession.h14
-rw-r--r--autotests/testolmutility.cpp128
-rw-r--r--autotests/testolmutility.h15
10 files changed, 887 insertions, 0 deletions
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
index 9efab0d1..671d6c08 100644
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@ -13,3 +13,9 @@ 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)
+endif()
diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh
new file mode 100755
index 00000000..0d58e460
--- /dev/null
+++ b/autotests/run-tests.sh
@@ -0,0 +1,34 @@
+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
+docker run -d \
+ --name synapse \
+ -p 1234:8008 \
+ -p 8448:8008 \
+ -p 8008:8008 \
+ -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0
+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'
+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'
+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/testgroupsession.cpp b/autotests/testgroupsession.cpp
new file mode 100644
index 00000000..2566669e
--- /dev/null
+++ b/autotests/testgroupsession.cpp
@@ -0,0 +1,54 @@
+// 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 = std::get<QByteArray>(ogs->pickle(Unencrypted {}));
+ auto ogs2 = std::get<QOlmOutboundGroupSessionPtr>(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {}));
+ QCOMPARE(ogsId, ogs2->sessionId());
+
+ auto igs = QOlmInboundGroupSession::create(std::get<QByteArray>(ogs->sessionKey()));
+ 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 = std::get<QOlmInboundGroupSessionPtr>(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {}));
+ QCOMPARE(igsId, igs->sessionId());
+}
+
+void TestGroupSession::groupSessionCryptoValid()
+{
+ auto ogs = QOlmOutboundGroupSession::create();
+ auto igs = QOlmInboundGroupSession::create(std::get<QByteArray>(ogs->sessionKey()));
+ QCOMPARE(ogs->sessionId(), igs->sessionId());
+
+ const auto plainText = QStringLiteral("Hello world!");
+ const auto ciphertext = std::get<QByteArray>(ogs->encrypt(plainText));
+ // ciphertext valid base64?
+ QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0);
+
+ const auto decryptionResult = std::get<std::pair<QString, uint32_t>>(igs->decrypt(ciphertext));
+
+ //// 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/testolmaccount.cpp b/autotests/testolmaccount.cpp
new file mode 100644
index 00000000..9989665a
--- /dev/null
+++ b/autotests/testolmaccount.cpp
@@ -0,0 +1,510 @@
+// 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/encryptedfile.h>
+#include <networkaccessmanager.h>
+#include <room.h>
+
+using namespace Quotient;
+
+void TestOlmAccount::pickleUnpickledTest()
+{
+ QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount.createNewAccount();
+ auto identityKeys = olmAccount.identityKeys();
+ auto pickled = std::get<QByteArray>(olmAccount.pickle(Unencrypted{}));
+ QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice"));
+ olmAccount2.unpickle(pickled, Unencrypted{});
+ 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;
+ const auto verify = utility.ed25519Verify(ed25519Key, message, signature);
+ QVERIFY(std::holds_alternative<bool>(verify));
+ QVERIFY(std::get<bool>(verify) == true);
+}
+
+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"
+ }})");
+
+ EncryptedFile file;
+ JsonObjectConverter<EncryptedFile>::fillFrom(doc.object(), file);
+
+ 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");
+}
+
+#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));
+
+void TestOlmAccount::uploadIdentityKey()
+{
+ CREATE_CONNECTION(conn, "alice1", "secret", "AlicePhone")
+
+ auto olmAccount = conn->olmAccount();
+ auto idKeys = olmAccount->identityKeys();
+
+ QVERIFY(idKeys.curve25519.size() > 10);
+
+ OneTimeKeys 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");
+ });
+ 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();
+
+ QHash<QString, QVariant> 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] {
+ QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ 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));
+}
+
+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();
+ QHash<QString, QVariant> oneTimeKeysHash;
+ const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys);
+ for (const auto &[keyId, key] : asKeyValueRange(signedKey)) {
+ QVariant var;
+ var.setValue(key);
+ oneTimeKeysHash[keyId] = var;
+ }
+ auto request = new UploadKeysJob(none, oneTimeKeysHash);
+ connect(request, &BaseJob::result, this, [request, nKeys, conn] {
+ QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ 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));
+}
+
+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] {
+ QCOMPARE(request->oneTimeKeyCounts().size(), 1);
+ 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));
+}
+
+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().size(), 1);
+ 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().size(), 1);
+ 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().size(), 1);
+ 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 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(oneTimeKey.canConvert<QVariantMap>());
+
+ const auto varMap = oneTimeKey.toMap();
+ QVERIFY(std::any_of(varMap.constKeyValueBegin(),
+ varMap.constKeyValueEnd(), [](const auto& kv) {
+ return kv.first.startsWith(
+ SignedCurve25519Key);
+ }));
+ });
+ });
+}
+
+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().size(), 1);
+ 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().size(), 1);
+ 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().size(), 1);
+ 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().size(), 1);
+ 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..5436c392
--- /dev/null
+++ b/autotests/testolmsession.cpp
@@ -0,0 +1,80 @@
+// 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("accountA:foo.com", "Device1UserA");
+ accountA.unpickle(pickledAccountA, Unencrypted{});
+ auto accountB = QOlmAccount("accountB:foo.com", "Device1UserB");
+ accountB.unpickle(pickledAccountB, Unencrypted{});
+
+ const QByteArray identityKeyA("qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o");
+ const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk");
+ const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg");
+ const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ");
+ auto outbound = std::get<QOlmSessionPtr>(accountA
+ .createOutboundSession(identityKeyB, oneTimeKeyB));
+
+ 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 = std::get<QOlmSessionPtr>(accountB.createInboundSession(preKey));
+ 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(std::get<bool>(inboundSession->matchesInboundSession(m)));
+ }
+
+ const auto decrypted = std::get<QString>(inboundSession->decrypt(encrypted));
+
+ QCOMPARE(decrypted, "Hello world!");
+}
+
+void TestOlmSession::correctSessionOrdering()
+{
+ // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM
+ auto session1 = std::get<QOlmSessionPtr>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{}));
+ // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0
+ auto session2 = std::get<QOlmSessionPtr>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {}));
+ // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME
+ auto session3 = std::get<QOlmSessionPtr>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{}));
+
+ 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..b4532c8d
--- /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"
+
+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 { "@alice:matrix.org", "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(),
+ (void *)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 = std::get<bool>(utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1));
+
+ //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS");
+ QCOMPARE(res2, true);
+}
+
+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();
+};