aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.ci/adjust-config.sh53
-rw-r--r--.github/workflows/ci.yml13
-rw-r--r--.gitignore4
-rw-r--r--.gitmodules3
m---------3rdparty/libQtOlm0
-rw-r--r--CMakeLists.txt85
-rw-r--r--autotests/CMakeLists.txt6
-rwxr-xr-xautotests/run-tests.sh23
-rw-r--r--autotests/testgroupsession.cpp54
-rw-r--r--autotests/testgroupsession.h14
-rw-r--r--autotests/testolmaccount.cpp576
-rw-r--r--autotests/testolmaccount.h33
-rw-r--r--autotests/testolmsession.cpp80
-rw-r--r--autotests/testolmsession.h14
-rw-r--r--autotests/testolmutility.cpp129
-rw-r--r--autotests/testolmutility.h15
-rw-r--r--lib/connection.cpp495
-rw-r--r--lib/connection.h28
-rw-r--r--lib/database.cpp244
-rw-r--r--lib/database.h43
-rw-r--r--lib/e2ee.h35
-rw-r--r--lib/e2ee/e2ee.h132
-rw-r--r--lib/e2ee/qolmaccount.cpp328
-rw-r--r--lib/e2ee/qolmaccount.h123
-rw-r--r--lib/e2ee/qolmerrors.cpp25
-rw-r--r--lib/e2ee/qolmerrors.h26
-rw-r--r--lib/e2ee/qolminboundsession.cpp151
-rw-r--r--lib/e2ee/qolminboundsession.h48
-rw-r--r--lib/e2ee/qolmmessage.cpp35
-rw-r--r--lib/e2ee/qolmmessage.h41
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp125
-rw-r--r--lib/e2ee/qolmoutboundsession.h52
-rw-r--r--lib/e2ee/qolmsession.cpp251
-rw-r--r--lib/e2ee/qolmsession.h76
-rw-r--r--lib/e2ee/qolmutility.cpp61
-rw-r--r--lib/e2ee/qolmutility.h45
-rw-r--r--lib/e2ee/qolmutils.cpp23
-rw-r--r--lib/e2ee/qolmutils.h15
-rw-r--r--lib/encryptionmanager.cpp373
-rw-r--r--lib/encryptionmanager.h50
-rw-r--r--lib/events/encryptedevent.cpp21
-rw-r--r--lib/events/encryptedevent.h3
-rw-r--r--lib/events/encryptedfile.cpp31
-rw-r--r--lib/events/encryptedfile.h2
-rw-r--r--lib/events/encryptionevent.cpp2
-rw-r--r--lib/events/eventcontent.cpp1
-rw-r--r--lib/events/keyverificationevent.cpp164
-rw-r--r--lib/events/keyverificationevent.h167
-rw-r--r--lib/events/roomevent.cpp15
-rw-r--r--lib/events/roomevent.h10
-rw-r--r--lib/jobs/downloadfilejob.cpp73
-rw-r--r--lib/jobs/downloadfilejob.h4
-rw-r--r--lib/logging.cpp1
-rw-r--r--lib/logging.h1
-rw-r--r--lib/mxcreply.cpp36
-rw-r--r--lib/networkaccessmanager.cpp11
-rw-r--r--lib/networkaccessmanager.h1
-rw-r--r--lib/room.cpp202
-rw-r--r--lib/settings.cpp6
-rw-r--r--lib/syncdata.cpp34
-rw-r--r--lib/syncdata.h23
61 files changed, 4068 insertions, 667 deletions
diff --git a/.ci/adjust-config.sh b/.ci/adjust-config.sh
new file mode 100755
index 00000000..b2ca52b2
--- /dev/null
+++ b/.ci/adjust-config.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+CMD=""
+
+$CMD perl -pi -w -e \
+ 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' data/homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' data/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 data/homeserver.yaml
+
+$CMD perl -pi -w -e \
+ 's/#enable_registration: false/enable_registration: true/g;' data/homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/tls: false/tls: true/g;' data/homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/#tls_certificate_path:/tls_certificate_path:/g;' data/homeserver.yaml
+$CMD perl -pi -w -e \
+ 's/#tls_private_key_path:/tls_private_key_path:/g;' data/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 chmod 0777 data/localhost.tls.crt
+$CMD chmod 0777 data/localhost.tls.key
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 35a5c6f9..1d902bd3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -150,6 +150,12 @@ jobs:
unzip -o sonar-scanner-cli*.zip
popd
+ - name: Install OpenSSL
+ if: ${{ contains(matrix.os, 'ubuntu') && matrix.e2ee }}
+ run: |
+ sudo apt-get install libssl-dev
+ echo "openssl version" >>$GITHUB_ENV
+
- name: Build and install olm
if: matrix.e2ee
working-directory: ${{ runner.workspace }}
@@ -159,6 +165,13 @@ jobs:
cmake --build build/olm --target install
echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV
+ - name: Build and install QtKeychain
+ run: |
+ cd ..
+ git clone https://github.com/frankosterfeld/qtkeychain.git
+ cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS
+ cmake --build qtkeychain/build --target install
+
- name: Pull CS API and build GTAD
if: matrix.update-api
working-directory: ${{ runner.workspace }}
diff --git a/.gitignore b/.gitignore
index 769bdf45..d414f49f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,7 @@ Makefile
Quotient_autogen/
.cmake/
tests/.cmake/
+
+# clangd
+.cache/
+compile_commands.json
diff --git a/.gitmodules b/.gitmodules
index eb4c1815..e69de29b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "3rdparty/libQtOlm"]
- path = 3rdparty/libQtOlm
- url = https://gitlab.com/b0/libqtolm.git
diff --git a/3rdparty/libQtOlm b/3rdparty/libQtOlm
deleted file mode 160000
-Subproject f2d8e235a4af0625fdedaaf727fef5d51293bf1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 89eb996a..15726240 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -88,39 +88,29 @@ get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE)
message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}")
if (${PROJECT_NAME}_ENABLE_E2EE)
- if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM)
- AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h)
- add_subdirectory(3rdparty/libQtOlm)
- include_directories(3rdparty/libQtOlm)
- if (NOT DEFINED USE_INTREE_LIBQOLM)
- set (USE_INTREE_LIBQOLM 1)
- endif ()
- endif ()
- if (USE_INTREE_LIBQOLM)
- message( STATUS "Using in-tree libQtOlm")
- find_package(Git QUIET)
- if (GIT_FOUND)
- execute_process(COMMAND
- "${GIT_EXECUTABLE}" rev-parse -q HEAD
- WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm
- OUTPUT_VARIABLE QTOLM_GIT_SHA1
- OUTPUT_STRIP_TRAILING_WHITESPACE)
- message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}")
- endif (GIT_FOUND)
- else ()
- set(SAVED_CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
- set(CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
- find_package(QtOlm 3.0.1 REQUIRED)
- set_package_properties(QtOlm PROPERTIES
- DESCRIPTION "QtOlm is a Qt wrapper around libOlm"
- PURPOSE "libQtOlm is required to support end-to-end encryption. See also BUILDING.md"
- URL "https://gitlab.com/b0/libqtolm"
- )
- if (QtOlm_FOUND)
- message(STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}")
- endif()
- endif ()
-endif ()
+ find_package(${Qt} ${QtMinVersion} REQUIRED Sql)
+ find_package(Olm 3.1.3 REQUIRED)
+ set_package_properties(Olm PROPERTIES
+ DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets"
+ URL "https://gitlab.matrix.org/matrix-org/olm"
+ TYPE REQUIRED
+ )
+ if (Olm_FOUND)
+ message(STATUS "Using libOlm ${Olm_VERSION} at ${Olm_DIR}")
+ endif()
+
+ find_package(OpenSSL 1.1.0 REQUIRED)
+ set_package_properties(OpenSSL PROPERTIES
+ DESCRIPTION "Open source SSL and TLS implementation and cryptographic library"
+ URL "https://www.openssl.org/"
+ TYPE REQUIRED
+ )
+ if (OpenSSL_FOUND)
+ message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}")
+ endif()
+ find_package(${Qt}Keychain REQUIRED)
+endif()
+
# Set up source files
list(APPEND lib_SRCS
@@ -145,7 +135,6 @@ list(APPEND lib_SRCS
lib/networksettings.h lib/networksettings.cpp
lib/converters.h lib/converters.cpp
lib/util.h lib/util.cpp
- lib/encryptionmanager.h lib/encryptionmanager.cpp
lib/eventitem.h lib/eventitem.cpp
lib/accountregistry.h lib/accountregistry.cpp
lib/mxcreply.h lib/mxcreply.cpp
@@ -175,12 +164,28 @@ list(APPEND lib_SRCS
lib/events/encryptedevent.h lib/events/encryptedevent.cpp
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/jobs/requestdata.h lib/jobs/requestdata.cpp
lib/jobs/basejob.h lib/jobs/basejob.cpp
lib/jobs/syncjob.h lib/jobs/syncjob.cpp
lib/jobs/mediathumbnailjob.h lib/jobs/mediathumbnailjob.cpp
lib/jobs/downloadfilejob.h lib/jobs/downloadfilejob.cpp
)
+if (${PROJECT_NAME}_ENABLE_E2EE)
+ list(APPEND lib_SRCS
+ lib/database.h lib/database.cpp
+ lib/e2ee/qolmaccount.h lib/e2ee/qolmaccount.cpp
+ lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp
+ lib/e2ee/qolminboundsession.h lib/e2ee/qolminboundsession.cpp
+ lib/e2ee/qolmoutboundsession.h lib/e2ee/qolmoutboundsession.cpp
+ lib/e2ee/qolmutils.h lib/e2ee/qolmutils.cpp
+ lib/e2ee/qolmutility.h lib/e2ee/qolmutility.cpp
+ lib/e2ee/qolmerrors.h lib/e2ee/qolmerrors.cpp
+ lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp
+ lib/e2ee/qolmmessage.h lib/e2ee/qolmmessage.cpp
+ )
+endif()
# Configure API files generation
@@ -298,10 +303,18 @@ target_include_directories(${PROJECT_NAME} PUBLIC
$<INSTALL_INTERFACE:${${PROJECT_NAME}_INSTALL_INCLUDEDIR}>
)
if (${PROJECT_NAME}_ENABLE_E2EE)
- target_link_libraries(${PROJECT_NAME} QtOlm)
- set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in
+ target_link_libraries(${PROJECT_NAME} Olm::Olm
+ OpenSSL::Crypto
+ OpenSSL::SSL
+ ${Qt}::Sql
+ ${QTKEYCHAIN_LIBRARIES})
+ 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)
+
if (Qt STREQUAL Qt5) # See #483
target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia)
endif()
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..b49f37a1
--- /dev/null
+++ b/autotests/run-tests.sh
@@ -0,0 +1,23 @@
+mkdir -p data
+chmod 0777 data
+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
+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 alice -p secret -c /data/homeserver.yaml https://localhost:8008'
+echo Register bob
+docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -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'
+
+cd build/ && GTEST_COLOR=1 ctest --verbose
+rm -rf ./data/*
+docker rm -f synapse 2>&1>/dev/null
diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp
new file mode 100644
index 00000000..5024ccea
--- /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 TestOlmSession::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 TestOlmSession::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_MAIN(TestOlmSession)
diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h
new file mode 100644
index 00000000..7743295f
--- /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 TestOlmSession : 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..62b786d0
--- /dev/null
+++ b/autotests/testolmaccount.cpp
@@ -0,0 +1,576 @@
+// 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 <e2ee/qolmaccount.h>
+#include <e2ee/qolmutility.h>
+#include <connection.h>
+#include <events/encryptedfile.h>
+#include <networkaccessmanager.h>
+#include <room.h>
+#include <csapi/joining.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 {"m.olm.v1.curve25519-aes-sha2",
+ "m.megolm.v1.aes-sha2"};
+
+ 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("@alice:localhost:443"); \
+ connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [=] { \
+ (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \
+ }); \
+ connect( (VAR) .get(), &Connection::networkError, [](QString error) { \
+ QWARN(qUtf8Printable(error)); \
+ QFAIL("Network error: make sure synapse is running"); \
+ }); \
+ connect( (VAR) .get(), &Connection::loginError, [](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, "alice", "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, "alice", "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()["curve25519"], 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, "alice", "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()["signed_curve25519"], 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, "alice", "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()["signed_curve25519"], 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, "alice", "secret", "AlicePhone")
+ CREATE_CONNECTION(bob, "bob", "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()["signed_curve25519"], 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()["signed_curve25519"], 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);
+
+ auto aliceDevices = job->deviceKeys()[bob->userId()];
+ QVERIFY(aliceDevices.size() > 0);
+
+ auto devKeys = aliceDevices[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);
+
+ auto bobDevices = job->deviceKeys()[alice->userId()];
+ QVERIFY(bobDevices.size() > 0);
+
+ 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, "alice", "secret", "AlicePhone")
+ CREATE_CONNECTION(bob, "alice", "secret", "AlicePhone")
+
+ // 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()["signed_curve25519"], 1);
+ });
+ bob->run(request);
+
+ QSignalSpy requestSpy(request, &BaseJob::result);
+ QVERIFY(requestSpy.wait(10000));
+
+ // Alice retrieves bob's keys & claims one signed one-time key.
+ auto *aliceOlm = alice->olmAccount();
+ QHash<QString, QStringList> deviceKeys;
+ deviceKeys[bob->userId()] = QStringList();
+ auto job = alice->callApi<QueryKeysJob>(deviceKeys);
+ connect(job, &BaseJob::result, this, [bob, alice, aliceOlm, job, this] {
+ auto bobDevices = job->deviceKeys()[bob->userId()];
+ QVERIFY(bobDevices.size() > 0);
+
+ // Retrieve the identity key for the current device.
+ auto bobEd25519 =
+ bobDevices[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, [aliceOlm, bob, bobEd25519, job] {
+ const auto userId = bob->userId();
+ const auto deviceId = bob->deviceId();
+
+ // The device exists.
+ QCOMPARE(job->oneTimeKeys().size(), 1);
+ QCOMPARE(job->oneTimeKeys()[userId].size(), 1);
+
+ // The key is the one bob sent.
+ auto oneTimeKey = job->oneTimeKeys()[userId][deviceId];
+ QVERIFY(oneTimeKey.canConvert<QVariantMap>());
+
+ QVariantMap varMap = oneTimeKey.toMap();
+ bool found = false;
+ for (const auto &key : varMap.keys()) {
+ if (key.startsWith(QStringLiteral("signed_curve25519"))) {
+ found = true;
+ }
+ }
+ QVERIFY(found);
+
+ //auto algo = oneTimeKey.begin().key();
+ //auto contents = oneTimeKey.begin().value();
+ });
+ });
+}
+
+void TestOlmAccount::claimMultipleKeys()
+{
+ // Login with alice multiple times
+ CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone")
+ CREATE_CONNECTION(alice1, "alice", "secret", "AlicePhone")
+ CREATE_CONNECTION(alice2, "alice", "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()["signed_curve25519"], 10);
+ });
+ alice->run(res);
+
+ 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()["signed_curve25519"], 10);
+ });
+ alice1->run(res1);
+
+ 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()["signed_curve25519"], 10);
+ });
+ alice2->run(res2);
+
+ QVERIFY(spy.wait(10000));
+ QVERIFY(spy1.wait(10000));
+ QVERIFY(spy2.wait(1000)); // TODO this is failing even with 10000
+
+ // Bob will claim all keys from alice
+ CREATE_CONNECTION(bob, "bob", "secret", "BobPhone")
+
+ QStringList devices_;
+ devices_ << alice->deviceId()
+ << alice1->deviceId()
+ << alice2->deviceId();
+
+ QHash<QString, QHash<QString, QString>> oneTimeKeys;
+ for (const auto &d : devices_) {
+ oneTimeKeys[alice->userId()] = QHash<QString, QString>();
+ oneTimeKeys[alice->userId()][d] = SignedCurve25519Key;
+ }
+ auto job = bob->callApi<ClaimKeysJob>(oneTimeKeys);
+ connect(job, &BaseJob::result, this, [bob, job] {
+ const auto userId = bob->userId();
+ const auto deviceId = bob->deviceId();
+
+ // The device exists.
+ QCOMPARE(job->oneTimeKeys().size(), 1);
+ QCOMPARE(job->oneTimeKeys()[userId].size(), 3);
+ });
+}
+
+void TestOlmAccount::keyChange()
+{
+ CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone")
+
+ auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), QStringList());
+ connect(job, &BaseJob::result, this, [alice, job, this] {
+ // Alice syncs to get the first next_batch token.
+ alice->sync();
+ connect(alice.get(), &Connection::syncDone, this, [alice, this] {
+ const auto nextBatchToken = alice->nextBatchToken();
+
+ // generate keys and change existing one
+ auto aliceOlm = alice->olmAccount();
+ aliceOlm->generateOneTimeKeys(1);
+ auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys());
+ QSignalSpy spy(aliceRes, &BaseJob::result);
+
+ alice->run(aliceRes);
+ QVERIFY(spy.wait(10000));
+
+ // The key changes should contain her username
+ // because of the key uploading.
+
+ auto changeJob = alice->callApi<GetKeysChangesJob>(nextBatchToken, "");
+ connect(changeJob, &BaseJob::result, this, [changeJob, alice] {
+ QCOMPARE(changeJob->changed().size(), 1);
+ QCOMPARE(changeJob->left().size(), 0);
+ QCOMPARE(changeJob->changed()[0], alice->userId());
+ });
+ QSignalSpy spy2(changeJob, &BaseJob::result);
+ QVERIFY(spy2.wait(10000));
+ });
+ QSignalSpy spy2(alice.get(), &Connection::syncDone);
+ QVERIFY(spy2.wait(10000));
+ });
+ QSignalSpy spy(job, &BaseJob::result);
+ QVERIFY(spy.wait(10000));
+}
+
+void TestOlmAccount::enableEncryption()
+{
+ CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone")
+ CREATE_CONNECTION(bob, "bob", "secret", "BobPhone")
+
+ QString joinedRoom;
+
+ auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"});
+ connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) {
+ room->activateEncryption();
+ QSignalSpy spy(room, &Room::encryption);
+
+ joinedRoom = room->id();
+ auto job = bob->joinRoom(room->id());
+ QSignalSpy spy1(job, &BaseJob::result);
+ QVERIFY(spy.wait(10000));
+ QVERIFY(spy1.wait(10000));
+ });
+
+ QSignalSpy spy(job, &BaseJob::result);
+ QVERIFY(spy.wait(10000));
+
+ bob->sync();
+ connect(bob.get(), &Connection::syncDone, this, [bob, joinedRoom, this] {
+ auto &events = bob->room(joinedRoom)->messageEvents();
+ bool hasEncryption = false;
+ for (auto it = events.rbegin(); it != events.rend(); ++it) {
+ auto event = it->event();
+ if (eventCast<const EncryptedEvent>(event)) {
+ hasEncryption = true;
+ } else {
+ qDebug() << event->matrixType() << typeId<EncryptedEvent>() << event->type();
+ if ( event->matrixType() == "m.room.encryption") {
+ qDebug() << event->contentJson();
+ }
+ }
+ }
+ QVERIFY(bob->room(joinedRoom)->usesEncryption());
+ QVERIFY(hasEncryption);
+ });
+ QSignalSpy spy2(bob.get(), &Connection::syncDone);
+ QVERIFY(spy2.wait(10000));
+}
+
+QTEST_MAIN(TestOlmAccount)
diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h
new file mode 100644
index 00000000..f1f80454
--- /dev/null
+++ b/autotests/testolmaccount.h
@@ -0,0 +1,33 @@
+// 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 keyChange();
+ void enableEncryption();
+};
diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp
new file mode 100644
index 00000000..41baf8e3
--- /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_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..d0476af0
--- /dev/null
+++ b/autotests/testolmutility.cpp
@@ -0,0 +1,129 @@
+// 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().keyValueBegin()->second;
+ 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_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/lib/connection.cpp b/lib/connection.cpp
index 67bc83f9..14188ace 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -7,9 +7,6 @@
#include "connection.h"
#include "connectiondata.h"
-#ifdef Quotient_E2EE_ENABLED
-# include "encryptionmanager.h"
-#endif // Quotient_E2EE_ENABLED
#include "room.h"
#include "settings.h"
#include "user.h"
@@ -38,7 +35,16 @@
#include "jobs/syncjob.h"
#ifdef Quotient_E2EE_ENABLED
-# include "account.h" // QtOlm
+# include "e2ee/qolmaccount.h"
+# include "e2ee/qolmutils.h"
+# include "database.h"
+# include "e2ee/qolminboundsession.h"
+
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
#endif // Quotient_E2EE_ENABLED
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
@@ -55,6 +61,7 @@
#include <QtCore/QStringBuilder>
#include <QtNetwork/QDnsLookup>
+
using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
@@ -102,13 +109,28 @@ public:
QMetaObject::Connection syncLoopConnection {};
int syncTimeout = -1;
+#ifdef Quotient_E2EE_ENABLED
+ QSet<QString> trackedUsers;
+ QSet<QString> outdatedUsers;
+ QHash<QString, QHash<QString, DeviceKeys>> deviceKeys;
+ QueryKeysJob *currentQueryKeysJob = nullptr;
+ bool encryptionUpdateRequired = false;
+ PicklingMode picklingMode = Unencrypted {};
+ Database *database = nullptr;
+
+ // A map from SenderKey to vector of InboundSession
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions;
+
+#endif
+
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
QVector<GetLoginFlowsJob::LoginFlow> loginFlows;
#ifdef Quotient_E2EE_ENABLED
- QScopedPointer<EncryptionManager> encryptionManager;
+ std::unique_ptr<QOlmAccount> olmAccount;
+ bool isUploadingKeys = false;
#endif // Quotient_E2EE_ENABLED
QPointer<GetWellknownJob> resolverJob = nullptr;
@@ -151,6 +173,7 @@ public:
void consumeAccountData(Events&& accountDataEvents);
void consumePresenceData(Events&& presenceData);
void consumeToDeviceEvents(Events&& toDeviceEvents);
+ void consumeDevicesList(DevicesList&& devicesList);
template <typename EventT>
EventT* unpackAccountData() const
@@ -181,29 +204,107 @@ public:
return q->stateCacheDir().filePath("state.json");
}
+#ifdef Quotient_E2EE_ENABLED
+ void loadSessions() {
+ olmSessions = q->database()->loadOlmSessions(q->picklingMode());
+ }
+ void saveSession(QOlmSessionPtr& session, const QString &senderKey) {
+ auto pickleResult = session->pickle(q->picklingMode());
+ if (std::holds_alternative<QOlmError>(pickleResult)) {
+ qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get<QOlmError>(pickleResult);
+ return;
+ }
+ q->database()->saveOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickleResult));
+ }
+ QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount)
+ {
+ Q_ASSERT(message.type() == QOlmMessage::PreKey);
+ for(auto& session : olmSessions[senderKey]) {
+ const auto matches = session->matchesInboundSessionFrom(senderKey, message);
+ if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) {
+ qCDebug(E2EE) << "Found inbound session";
+ const auto result = session->decrypt(message);
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ } else {
+ qCDebug(E2EE) << "Failed to decrypt prekey message";
+ return {};
+ }
+ }
+ }
+ qCDebug(E2EE) << "Creating new inbound session";
+ auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message);
+ if(std::holds_alternative<QOlmError>(newSessionResult)) {
+ qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get<QOlmError>(newSessionResult);
+ return {};
+ }
+ auto newSession = std::move(std::get<QOlmSessionPtr>(newSessionResult));
+ auto error = olmAccount->removeOneTimeKeys(newSession);
+ if (error) {
+ qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId();
+ }
+ const auto result = newSession->decrypt(message);
+ saveSession(newSession, senderKey);
+ olmSessions[senderKey].push_back(std::move(newSession));
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ } else {
+ qCDebug(E2EE) << "Failed to decrypt prekey message with new session";
+ return {};
+ }
+ }
+ QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey)
+ {
+ Q_ASSERT(message.type() == QOlmMessage::General);
+ for(auto& session : olmSessions[senderKey]) {
+ const auto result = session->decrypt(message);
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ }
+ }
+ qCWarning(E2EE) << "Failed to decrypt message";
+ return {};
+ }
+
+ QString sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr<QOlmAccount>& account)
+ {
+ QString decrypted;
+ int type = personalCipherObject.value(TypeKeyL).toInt(-1);
+ QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
+ if (type == 0) {
+ QOlmMessage preKeyMessage(body, QOlmMessage::PreKey);
+ decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account);
+ } else if (type == 1) {
+ QOlmMessage message(body, QOlmMessage::General);
+ decrypted = sessionDecryptGeneral(message, senderKey);
+ }
+ return decrypted;
+ }
+#endif
+
EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
-#else // Quotient_E2EE_ENABLED
+#else
if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey)
return {};
- const auto identityKey =
- encryptionManager->account()->curve25519IdentityKey();
+ const auto identityKey = olmAccount->identityKeys().curve25519;
const auto personalCipherObject =
encryptedEvent.ciphertext(identityKey);
if (personalCipherObject.isEmpty()) {
qCDebug(E2EE) << "Encrypted event is not for the current device";
return {};
}
- const auto decrypted = encryptionManager->sessionDecryptMessage(
- personalCipherObject, encryptedEvent.senderKey().toLatin1());
+ const auto decrypted = sessionDecryptMessage(
+ personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount);
if (decrypted.isEmpty()) {
qCDebug(E2EE) << "Problem with new session from senderKey:"
<< encryptedEvent.senderKey()
- << encryptionManager->account()->oneTimeKeys();
+ << olmAccount->oneTimeKeys().keys;
return {};
}
@@ -220,22 +321,18 @@ public:
// TODO: keys to constants
const auto decryptedEventObject = decryptedEvent->fullJson();
- const auto recipient =
- decryptedEventObject.value("recipient"_ls).toString();
+ const auto recipient = decryptedEventObject.value("recipient"_ls).toString();
if (recipient != data->userId()) {
qCDebug(E2EE) << "Found user" << recipient << "instead of us"
<< data->userId() << "in Olm plaintext";
return {};
}
- const auto ourKey =
- decryptedEventObject.value("recipient_keys"_ls).toObject()
- .value(Ed25519Key).toString();
- if (ourKey
- != QString::fromUtf8(
- encryptionManager->account()->ed25519IdentityKey())) {
+ const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject()
+ .value(Ed25519Key).toString();
+ if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) {
qCDebug(E2EE) << "Found key" << ourKey
<< "instead of ours own ed25519 key"
- << encryptionManager->account()->ed25519IdentityKey()
+ << olmAccount->identityKeys().ed25519
<< "in Olm plaintext";
return {};
}
@@ -243,12 +340,20 @@ public:
return std::move(decryptedEvent);
#endif // Quotient_E2EE_ENABLED
}
+#ifdef Quotient_E2EE_ENABLED
+ void loadOutdatedUserDevices();
+ void saveDevicesList();
+ void loadDevicesList();
+#endif
};
Connection::Connection(const QUrl& server, QObject* parent)
: QObject(parent)
, d(makeImpl<Private>(std::make_unique<ConnectionData>(server)))
{
+#ifdef Quotient_E2EE_ENABLED
+ //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount);
+#endif
d->q = this; // All d initialization should occur before this line
}
@@ -421,8 +526,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
- encryptionManager->uploadIdentityKeys(q);
- encryptionManager->uploadOneTimeKeys(q);
+ database->clear();
#endif // Quotient_E2EE_ENABLED
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
@@ -443,11 +547,58 @@ void Connection::Private::completeSetup(const QString& mxId)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
AccountSettings accountSettings(data->userId());
- encryptionManager.reset(
- new EncryptionManager(accountSettings.encryptionAccountPickle()));
- if (accountSettings.encryptionAccountPickle().isEmpty()) {
- accountSettings.setEncryptionAccountPickle(
- encryptionManager->olmAccountPickle());
+
+ QKeychain::ReadPasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ QEventLoop loop;
+ QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error() == QKeychain::Error::EntryNotFound) {
+ picklingMode = Encrypted { getRandom(128) };
+ QKeychain::WritePasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ job.setBinaryData(std::get<Encrypted>(picklingMode).key);
+ QEventLoop loop;
+ QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error()) {
+ qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString();
+ }
+ } else if(job.error() != QKeychain::Error::NoError) {
+ //TODO Error, do something
+ qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error();
+ } else {
+ qCDebug(E2EE) << "Successfully loaded pickling key from keychain";
+ picklingMode = Encrypted { job.binaryData() };
+ }
+
+ database = new Database(data->userId(), q);
+
+ // init olmAccount
+ 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
+ olmAccount->createNewAccount();
+ auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys());
+ connect(job, &BaseJob::failure, q, [job]{
+ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString();
+ });
+ } else {
+ // account already existing
+ auto pickle = database->accountPickle();
+ olmAccount->unpickle(pickle, picklingMode);
}
#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
@@ -602,24 +753,39 @@ QJsonObject toJson(const DirectChatsMap& directChats)
void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
{
+#ifdef Quotient_E2EE_ENABLED
+ if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) {
+ d->isUploadingKeys = true;
+ d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() / 2 - data.deviceOneTimeKeysCount()["signed_curve25519"]);
+ auto keys = d->olmAccount->oneTimeKeys();
+ auto job = d->olmAccount->createUploadKeyRequest(keys);
+ run(job, ForegroundRequest);
+ connect(job, &BaseJob::success, this, [this](){
+ d->olmAccount->markKeysAsPublished();
+ });
+ connect(job, &BaseJob::result, this, [this](){
+ d->isUploadingKeys = false;
+ });
+ }
+ static bool first = true;
+ if(first) {
+ d->loadDevicesList();
+ first = false;
+ }
+
+ d->consumeDevicesList(data.takeDevicesList());
+#endif // Quotient_E2EE_ENABLED
d->data->setLastEvent(data.nextBatch());
d->consumeRoomData(data.takeRoomData(), fromCache);
d->consumeAccountData(data.takeAccountData());
d->consumePresenceData(data.takePresenceData());
d->consumeToDeviceEvents(data.takeToDeviceEvents());
#ifdef Quotient_E2EE_ENABLED
- // handling device_one_time_keys_count
- if (!d->encryptionManager)
- {
- qCDebug(E2EE) << "Encryption manager is not there yet, updating "
- "one-time key counts will be skipped";
- return;
+ if(d->encryptionUpdateRequired) {
+ d->loadOutdatedUserDevices();
+ d->encryptionUpdateRequired = false;
}
- if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount();
- !deviceOneTimeKeysCount.isEmpty())
- d->encryptionManager->updateOneTimeKeyCounts(this,
- deviceOneTimeKeysCount);
-#endif // Quotient_E2EE_ENABLED
+#endif
}
void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
@@ -747,34 +913,55 @@ void Connection::Private::consumePresenceData(Events&& presenceData)
void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
{
#ifdef Quotient_E2EE_ENABLED
- // handling m.room_key to-device encrypted event
- visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) {
- if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
- qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm"
- << ee.algorithm() << "is not supported";
- return;
- }
+ if (!toDeviceEvents.empty()) {
+ 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();
+ return;
+ }
+ const auto decryptedEvent = sessionDecryptMessage(event);
+ if(!decryptedEvent) {
+ qCWarning(E2EE) << "Failed to decrypt event" << event.id();
+ return;
+ }
- // TODO: full maintaining of the device keys
- // with device_lists sync extention and /keys/query
- qCDebug(E2EE) << "Getting device keys for the m.room_key sender:"
- << ee.senderId();
- // encryptionManager->updateDeviceKeys();
-
- switchOnType(*sessionDecryptMessage(ee),
- [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) {
- if (auto* detectedRoom = q->room(roomKeyEvent.roomId()))
- detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
- else
- qCDebug(E2EE)
- << "Encrypted event room id" << roomKeyEvent.roomId()
- << "is not found at the connection" << q->objectName();
- },
- [](const Event& evt) {
- qCDebug(E2EE) << "Skipping encrypted to_device event, type"
- << evt.matrixType();
- });
- });
+ switchOnType(*decryptedEvent,
+ [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) {
+ if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
+ } else {
+ qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId()
+ << "is not found at the connection" << q->objectName();
+ }
+ },
+ [](const Event& evt) {
+ qCDebug(E2EE) << "Skipping encrypted to_device event, type"
+ << evt.matrixType();
+ });
+ });
+ }
+#endif
+}
+
+void Connection::Private::consumeDevicesList(DevicesList&& devicesList)
+{
+#ifdef Quotient_E2EE_ENABLED
+ bool hasNewOutdatedUser = false;
+ for(const auto &changed : devicesList.changed) {
+ if(trackedUsers.contains(changed)) {
+ outdatedUsers += changed;
+ hasNewOutdatedUser = true;
+ }
+ }
+ for(const auto &left : devicesList.left) {
+ trackedUsers -= left;
+ outdatedUsers -= left;
+ deviceKeys.remove(left);
+ }
+ if(hasNewOutdatedUser) {
+ loadOutdatedUserDevices();
+ }
#endif
}
@@ -914,6 +1101,19 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
return job;
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(const QUrl& url,
+ const EncryptedFile& file,
+ 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;
+}
+#endif
+
CreateRoomJob*
Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QString& name, const QString& topic,
@@ -1227,9 +1427,9 @@ QByteArray Connection::accessToken() const
bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); }
#ifdef Quotient_E2EE_ENABLED
-QtOlm::Account* Connection::olmAccount() const
+QOlmAccount *Connection::olmAccount() const
{
- return d->encryptionManager->account();
+ return d->olmAccount.get();
}
#endif // Quotient_E2EE_ENABLED
@@ -1772,3 +1972,164 @@ QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() co
}
return result;
}
+
+#ifdef Quotient_E2EE_ENABLED
+void Connection::Private::loadOutdatedUserDevices()
+{
+ QHash<QString, QStringList> users;
+ for(const auto &user : outdatedUsers) {
+ users[user] += QStringList();
+ }
+ if(currentQueryKeysJob) {
+ currentQueryKeysJob->abandon();
+ currentQueryKeysJob = nullptr;
+ }
+ auto queryKeysJob = q->callApi<QueryKeysJob>(users);
+ currentQueryKeysJob = queryKeysJob;
+ connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){
+ currentQueryKeysJob = nullptr;
+ const auto data = queryKeysJob->deviceKeys();
+ for(const auto &[user, keys] : asKeyValueRange(data)) {
+ deviceKeys[user].clear();
+ for(const auto &device : keys) {
+ if(device.userId != user) {
+ qCWarning(E2EE) << "mxId mismatch during device key verification:" << device.userId << user;
+ continue;
+ }
+ if(!device.algorithms.contains("m.olm.v1.curve25519-aes-sha2") || !device.algorithms.contains("m.megolm.v1.aes-sha2")) {
+ qCWarning(E2EE) << "Unsupported encryption algorithms found" << device.algorithms;
+ continue;
+ }
+ if(!verifyIdentitySignature(device, device.deviceId, device.userId)) {
+ qCWarning(E2EE) << "Failed to verify devicekeys signature. Skipping this device";
+ continue;
+ }
+ deviceKeys[user][device.deviceId] = device;
+ }
+ outdatedUsers -= user;
+ }
+ saveDevicesList();
+ });
+}
+
+void Connection::Private::saveDevicesList()
+{
+ q->database()->transaction();
+ auto query = q->database()->prepareQuery(QStringLiteral("DELETE FROM tracked_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral("INSERT INTO tracked_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : trackedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral("DELETE FROM outdated_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral("INSERT INTO outdated_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : outdatedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral("INSERT INTO tracked_devices(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);"));
+ for (const auto& user : deviceKeys.keys()) {
+ for (const auto& device : deviceKeys[user]) {
+ auto keys = device.keys.keys();
+ auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1];
+ auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1];
+
+ query.bindValue(":matrixId", user);
+ query.bindValue(":deviceId", device.deviceId);
+ query.bindValue(":curveKeyId", curveKeyId);
+ query.bindValue(":curveKey", device.keys[curveKeyId]);
+ query.bindValue(":edKeyId", edKeyId);
+ query.bindValue(":edKey", device.keys[edKeyId]);
+
+ q->database()->execute(query);
+ }
+ }
+ q->database()->commit();
+}
+
+void Connection::Private::loadDevicesList()
+{
+ auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ trackedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ outdatedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys {
+ query.value("matrixId").toString(),
+ query.value("deviceId").toString(),
+ { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
+ {{query.value("curveKeyId").toString(), query.value("curveKey").toString()},
+ {query.value("edKeyId").toString(), query.value("edKey").toString()}},
+ {} // Signatures are not saved/loaded as they are not needed after initial validation
+ };
+ }
+
+}
+
+void Connection::encryptionUpdate(Room *room)
+{
+ for(const auto &user : room->users()) {
+ if(!d->trackedUsers.contains(user->id())) {
+ d->trackedUsers += user->id();
+ d->outdatedUsers += user->id();
+ d->encryptionUpdateRequired = true;
+ }
+ }
+}
+
+PicklingMode Connection::picklingMode() const
+{
+ return d->picklingMode;
+}
+#endif
+
+void Connection::saveOlmAccount()
+{
+ qCDebug(E2EE) << "Saving olm account";
+#ifdef Quotient_E2EE_ENABLED
+ auto pickle = d->olmAccount->pickle(d->picklingMode);
+ d->database->setAccountPickle(std::get<QByteArray>(pickle));
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+QJsonObject Connection::decryptNotification(const QJsonObject &notification)
+{
+ auto room = this->room(notification["room_id"].toString());
+ auto event = makeEvent<EncryptedEvent>(notification["event"].toObject());
+ auto decrypted = room->decryptMessage(*event);
+ if(!decrypted) {
+ return QJsonObject();
+ }
+ return decrypted->fullJson();
+}
+
+Database* Connection::database()
+{
+ return d->database;
+}
+
+UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room)
+{
+ return database()->loadMegolmSessions(room->id(), picklingMode());
+}
+
+void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session)
+{
+ database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode()));
+}
+#endif
diff --git a/lib/connection.h b/lib/connection.h
index dc2eaad1..165d8d68 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -22,9 +22,9 @@
#include <functional>
-namespace QtOlm {
-class Account;
-}
+#ifdef Quotient_E2EE_ENABLED
+#include "e2ee/e2ee.h"
+#endif
Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow)
@@ -48,6 +48,11 @@ class DownloadFileJob;
class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
+class Database;
+struct EncryptedFile;
+
+class QOlmAccount;
+class QOlmInboundGroupSession;
using LoginFlow = GetLoginFlowsJob::LoginFlow;
@@ -310,7 +315,10 @@ public:
QByteArray accessToken() const;
bool isLoggedIn() const;
#ifdef Quotient_E2EE_ENABLED
- QtOlm::Account* olmAccount() const;
+ QOlmAccount* olmAccount() const;
+ Database* database();
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room);
+ void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session);
#endif // Quotient_E2EE_ENABLED
Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
@@ -489,6 +497,9 @@ public:
setUserFactory(defaultUserFactory<T>);
}
+ /// Saves the olm account data to disk. Usually doesn't need to be called manually.
+ void saveOlmAccount();
+
public Q_SLOTS:
/// \brief Set the homeserver base URL and retrieve its login flows
///
@@ -576,6 +587,10 @@ public Q_SLOTS:
DownloadFileJob* downloadFile(const QUrl& url,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file,
+ const QString& localFilename = {});
+#endif
/**
* \brief Create a room (generic method)
* This method allows to customize room entirely to your liking,
@@ -661,6 +676,11 @@ public Q_SLOTS:
/** \deprecated Do not use this directly, use Room::leaveRoom() instead */
virtual LeaveRoomJob* leaveRoom(Room* room);
+#ifdef Quotient_E2EE_ENABLED
+ void encryptionUpdate(Room *room);
+ PicklingMode picklingMode() const;
+ QJsonObject decryptNotification(const QJsonObject &notification);
+#endif
Q_SIGNALS:
/// \brief Initial server resolution has failed
///
diff --git a/lib/database.cpp b/lib/database.cpp
new file mode 100644
index 00000000..b91b6ef1
--- /dev/null
+++ b/lib/database.cpp
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "database.h"
+
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QDebug>
+#include <QtCore/QDir>
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmsession.h"
+#include "e2ee/qolminboundsession.h"
+
+using namespace Quotient;
+Database::Database(const QString& matrixId, QObject* parent)
+ : QObject(parent)
+ , m_matrixId(matrixId)
+{
+ m_matrixId.replace(':', '_');
+ QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId));
+ QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId);
+ QDir(databasePath).mkpath(databasePath);
+ database().setDatabaseName(databasePath + QStringLiteral("/quotient.db3"));
+ database().open();
+
+ switch(version()) {
+ case 0: migrateTo1();
+ }
+}
+
+int Database::version()
+{
+ auto query = execute(QStringLiteral("PRAGMA user_version;"));
+ if (query.next()) {
+ bool ok;
+ int value = query.value(0).toInt(&ok);
+ qCDebug(DATABASE) << "Database version" << value;
+ if (ok)
+ return value;
+ } else {
+ qCritical() << "Failed to check database version";
+ }
+ return -1;
+}
+
+QSqlQuery Database::execute(const QString &queryString)
+{
+ auto query = database().exec(queryString);
+ if (query.lastError().type() != QSqlError::NoError) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+QSqlQuery Database::execute(QSqlQuery &query)
+{
+ if (!query.exec()) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+void Database::transaction()
+{
+ database().transaction();
+}
+
+void Database::commit()
+{
+ database().commit();
+}
+
+void Database::migrateTo1()
+{
+ qCDebug(DATABASE) << "Migrating database to version 1";
+ transaction();
+ execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);"));
+ execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);"));
+
+ execute(QStringLiteral("PRAGMA user_version = 1;"));
+ commit();
+}
+
+QByteArray Database::accountPickle()
+{
+ auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;"));
+ execute(query);
+ if (query.next()) {
+ return query.value(QStringLiteral("pickle")).toByteArray();
+ }
+ return {};
+}
+
+void Database::setAccountPickle(const QByteArray &pickle)
+{
+ auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);"));
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(deleteQuery);
+ execute(query);
+ commit();
+}
+
+void Database::clear()
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;"));
+ auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;"));
+ auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;"));
+
+ transaction();
+ execute(query);
+ execute(sessionsQuery);
+ execute(megolmSessionsQuery);
+ execute(groupSessionIndexRecordQuery);
+ commit();
+
+}
+
+void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle) VALUES(:senderKey, :sessionId, :pickle);"));
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(query);
+ commit();
+}
+
+UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;"));
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions;
+ while (query.next()) {
+ auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
+ if (std::holds_alternative<QOlmError>(session)) {
+ qCWarning(E2EE) << "Failed to unpickle olm session";
+ continue;
+ }
+ sessions[query.value("senderKey").toString()].push_back(std::move(std::get<QOlmSessionPtr>(session)));
+ }
+ return sessions;
+}
+
+UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ query.bindValue(":roomId", roomId);
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> sessions;
+ while (query.next()) {
+ auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
+ if (std::holds_alternative<QOlmError>(session)) {
+ qCWarning(E2EE) << "Failed to unpickle megolm session";
+ continue;
+ }
+ sessions[{query.value("senderKey").toString(), query.value("sessionId").toString()}] = std::move(std::get<QOlmInboundGroupSessionPtr>(session));
+ }
+ return sessions;
+}
+
+void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, pickle) VALUES(:roomId, :senderKey, :sessionId, :pickle);"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(query);
+ commit();
+}
+
+void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts)
+{
+ auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);");
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ query.bindValue(":eventId", eventId);
+ query.bindValue(":ts", ts);
+ transaction();
+ execute(query);
+ commit();
+}
+
+std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ transaction();
+ execute(query);
+ commit();
+ if (!query.next()) {
+ return {};
+ }
+ return {query.value("eventId").toString(), query.value("ts").toLongLong()};
+}
+
+QSqlDatabase Database::database()
+{
+ return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId));
+}
+
+QSqlQuery Database::prepareQuery(const QString& queryString)
+{
+ QSqlQuery query(database());
+ query.prepare(queryString);
+ return query;
+}
+
+void Database::clearRoomData(const QString& roomId)
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;"));
+ transaction();
+ execute(query);
+ execute(query2);
+ execute(query3);
+ commit();
+}
diff --git a/lib/database.h b/lib/database.h
new file mode 100644
index 00000000..96256a55
--- /dev/null
+++ b/lib/database.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtSql/QSqlQuery>
+#include <QtCore/QVector>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+class Database : public QObject
+{
+ Q_OBJECT
+
+public:
+ Database(const QString& matrixId, QObject* parent);
+
+ int version();
+ void transaction();
+ void commit();
+ QSqlQuery execute(const QString &queryString);
+ QSqlQuery execute(QSqlQuery &query);
+ QSqlDatabase database();
+ QSqlQuery prepareQuery(const QString& quaryString);
+
+ QByteArray accountPickle();
+ void setAccountPickle(const QByteArray &pickle);
+ void clear();
+ void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle);
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(const PicklingMode& picklingMode);
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode);
+ void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle);
+ 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);
+
+private:
+ void migrateTo1();
+ QString m_matrixId;
+};
+}
diff --git a/lib/e2ee.h b/lib/e2ee.h
deleted file mode 100644
index 4044aa02..00000000
--- a/lib/e2ee.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#pragma once
-
-#include "util.h"
-
-#include <QtCore/QStringList>
-
-namespace Quotient {
-inline const auto CiphertextKeyL = "ciphertext"_ls;
-inline const auto SenderKeyKeyL = "sender_key"_ls;
-inline const auto DeviceIdKeyL = "device_id"_ls;
-inline const auto SessionIdKeyL = "session_id"_ls;
-
-inline const auto AlgorithmKeyL = "algorithm"_ls;
-inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
-inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
-
-inline const auto AlgorithmKey = QStringLiteral("algorithm");
-inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms");
-inline const auto RotationPeriodMsgsKey =
- QStringLiteral("rotation_period_msgs");
-
-inline const auto Ed25519Key = QStringLiteral("ed25519");
-inline const auto Curve25519Key = QStringLiteral("curve25519");
-inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519");
-inline const auto OlmV1Curve25519AesSha2AlgoKey =
- QStringLiteral("m.olm.v1.curve25519-aes-sha2");
-inline const auto MegolmV1AesSha2AlgoKey =
- QStringLiteral("m.megolm.v1.aes-sha2");
-inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey,
- MegolmV1AesSha2AlgoKey };
-} // namespace Quotient
diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h
new file mode 100644
index 00000000..41cd2878
--- /dev/null
+++ b/lib/e2ee/e2ee.h
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <optional>
+#include <string>
+#include "converters.h"
+#include <variant>
+
+#include <QMap>
+#include <QHash>
+#include <QStringList>
+#include <QMetaType>
+
+#include "util.h"
+
+namespace Quotient {
+
+inline const auto CiphertextKeyL = "ciphertext"_ls;
+inline const auto SenderKeyKeyL = "sender_key"_ls;
+inline const auto DeviceIdKeyL = "device_id"_ls;
+inline const auto SessionIdKeyL = "session_id"_ls;
+
+inline const auto AlgorithmKeyL = "algorithm"_ls;
+inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
+inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
+
+inline const auto AlgorithmKey = QStringLiteral("algorithm");
+inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms");
+inline const auto RotationPeriodMsgsKey =
+ QStringLiteral("rotation_period_msgs");
+
+inline const auto Ed25519Key = QStringLiteral("ed25519");
+inline const auto Curve25519Key = QStringLiteral("curve25519");
+inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519");
+inline const auto OlmV1Curve25519AesSha2AlgoKey =
+ QStringLiteral("m.olm.v1.curve25519-aes-sha2");
+inline const auto MegolmV1AesSha2AlgoKey =
+ QStringLiteral("m.megolm.v1.aes-sha2");
+inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey,
+ MegolmV1AesSha2AlgoKey };
+struct Unencrypted {};
+struct Encrypted {
+ QByteArray key;
+};
+
+using PicklingMode = std::variant<Unencrypted, Encrypted>;
+
+class QOlmSession;
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+class QOlmInboundGroupSession;
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+
+template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
+template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
+
+struct IdentityKeys
+{
+ QByteArray curve25519;
+ QByteArray ed25519;
+};
+
+//! Struct representing the one-time keys.
+struct OneTimeKeys
+{
+ QMap<QString, QMap<QString, QString>> keys;
+
+ //! Get the HashMap containing the curve25519 one-time keys.
+ QMap<QString, QString> curve25519() const;
+
+ //! Get a reference to the hashmap corresponding to given key type.
+ std::optional<QMap<QString, QString>> get(QString keyType) const;
+};
+
+//! Struct representing the signed one-time keys.
+class SignedOneTimeKey
+{
+public:
+ SignedOneTimeKey() = default;
+ SignedOneTimeKey(const SignedOneTimeKey &) = default;
+ SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default;
+ //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key.
+ QString key;
+
+ //! Required. Signatures of the key object.
+ //! The signature is calculated using the process described at Signing JSON.
+ QHash<QString, QHash<QString, QString>> signatures;
+};
+
+
+template <>
+struct JsonObjectConverter<SignedOneTimeKey> {
+ static void fillFrom(const QJsonObject& jo,
+ SignedOneTimeKey& result)
+ {
+ fromJson(jo.value("key"_ls), result.key);
+ fromJson(jo.value("signatures"_ls), result.signatures);
+ }
+
+ static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result)
+ {
+ addParam<>(jo, QStringLiteral("key"), result.key);
+ addParam<>(jo, QStringLiteral("signatures"), result.signatures);
+ }
+};
+
+bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs);
+
+template <typename T>
+class asKeyValueRange
+{
+public:
+ asKeyValueRange(T &data)
+ : m_data{data}
+ {
+ }
+
+ auto begin() { return m_data.keyValueBegin(); }
+
+ auto end() { return m_data.keyValueEnd(); }
+
+private:
+ T &m_data;
+};
+
+} // namespace Quotient
+
+Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey)
diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp
new file mode 100644
index 00000000..34ee7ea0
--- /dev/null
+++ b/lib/e2ee/qolmaccount.cpp
@@ -0,0 +1,328 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmaccount.h"
+#include "connection.h"
+#include "csapi/keys.h"
+#include "e2ee/qolmutils.h"
+#include "e2ee/qolmutility.h"
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QDebug>
+#include <iostream>
+
+using namespace Quotient;
+
+QMap<QString, QString> OneTimeKeys::curve25519() const
+{
+ return keys[QStringLiteral("curve25519")];
+}
+
+std::optional<QMap<QString, QString>> OneTimeKeys::get(QString keyType) const
+{
+ if (!keys.contains(keyType)) {
+ return std::nullopt;
+ }
+ return keys[keyType];
+}
+
+bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs)
+{
+ return lhs.curve25519 == rhs.curve25519 && lhs.ed25519 == rhs.ed25519;
+}
+
+// Convert olm error to enum
+QOlmError lastError(OlmAccount *account) {
+ return fromString(olm_account_last_error(account));
+}
+
+QByteArray getRandom(size_t bufferSize)
+{
+ QByteArray buffer(bufferSize, '0');
+ std::generate(buffer.begin(), buffer.end(), std::rand);
+ return buffer;
+}
+
+QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent)
+ : QObject(parent)
+ , m_userId(userId)
+ , m_deviceId(deviceId)
+{
+}
+
+QOlmAccount::~QOlmAccount()
+{
+ olm_clear_account(m_account);
+ delete[](reinterpret_cast<uint8_t *>(m_account));
+}
+
+void QOlmAccount::createNewAccount()
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ size_t randomSize = olm_create_account_random_length(m_account);
+ QByteArray randomData = getRandom(randomSize);
+ const auto error = olm_create_account(m_account, randomData.data(), randomSize);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ Q_EMIT needsSave();
+}
+
+void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode)
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ const QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size());
+ if (error == olm_error()) {
+ qCWarning(E2EE) << "Failed to unpickle olm account";
+ //TODO: Do something that is not dying
+ // Probably log the user out since we have no way of getting to the keys
+ //throw lastError(m_account);
+ }
+}
+
+std::variant<QByteArray, QOlmError> QOlmAccount::pickle(const PicklingMode &mode)
+{
+ const QByteArray key = toKey(mode);
+ const size_t pickleLength = olm_pickle_account_length(m_account);
+ QByteArray pickleBuffer(pickleLength, '0');
+ const auto error = olm_pickle_account(m_account, key.data(),
+ key.length(), pickleBuffer.data(), pickleLength);
+ if (error == olm_error()) {
+ return lastError(m_account);
+ }
+ return pickleBuffer;
+}
+
+IdentityKeys QOlmAccount::identityKeys() const
+{
+ const size_t keyLength = olm_account_identity_keys_length(m_account);
+ QByteArray keyBuffer(keyLength, '0');
+ const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object();
+ return IdentityKeys {
+ key.value(QStringLiteral("curve25519")).toString().toUtf8(),
+ key.value(QStringLiteral("ed25519")).toString().toUtf8()
+ };
+}
+
+QByteArray QOlmAccount::sign(const QByteArray &message) const
+{
+ QByteArray signatureBuffer(olm_account_signature_length(m_account), '0');
+
+ const auto error = olm_account_sign(m_account, message.data(), message.length(),
+ signatureBuffer.data(), signatureBuffer.length());
+
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ return signatureBuffer;
+}
+
+QByteArray QOlmAccount::sign(const QJsonObject &message) const
+{
+ return sign(QJsonDocument(message).toJson(QJsonDocument::Compact));
+}
+
+QByteArray QOlmAccount::signIdentityKeys() const
+{
+ const auto keys = identityKeys();
+ QJsonObject body
+ {
+ {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}},
+ {"user_id", m_userId},
+ {"device_id", m_deviceId},
+ {"keys",
+ QJsonObject{
+ {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)},
+ {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)}
+ }
+ }
+ };
+ return sign(QJsonDocument(body).toJson(QJsonDocument::Compact));
+
+}
+
+size_t QOlmAccount::maxNumberOfOneTimeKeys() const
+{
+ return olm_account_max_number_of_one_time_keys(m_account);
+}
+
+size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const
+{
+ const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys);
+ QByteArray randomBuffer = getRandom(randomLength);
+ const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength);
+
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ Q_EMIT needsSave();
+ return error;
+}
+
+OneTimeKeys QOlmAccount::oneTimeKeys() const
+{
+ const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account);
+ QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0');
+
+ const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object();
+ OneTimeKeys oneTimeKeys;
+
+ for (const QString& key1 : json.keys()) {
+ auto oneTimeKeyObject = json[key1].toObject();
+ auto keyMap = QMap<QString, QString>();
+ for (const QString &key2 : oneTimeKeyObject.keys()) {
+ keyMap[key2] = oneTimeKeyObject[key2].toString();
+ }
+ oneTimeKeys.keys[key1] = keyMap;
+ }
+ return oneTimeKeys;
+}
+
+QMap<QString, SignedOneTimeKey> QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const
+{
+ QMap<QString, SignedOneTimeKey> signedOneTimeKeys;
+ for (const auto &keyid : keys.curve25519().keys()) {
+ const auto oneTimeKey = keys.curve25519()[keyid];
+ QByteArray sign = signOneTimeKey(oneTimeKey);
+ signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign);
+ }
+ return signedOneTimeKeys;
+}
+
+SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const
+{
+ SignedOneTimeKey sign{};
+ sign.key = key;
+ sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}};
+ return sign;
+}
+
+QByteArray QOlmAccount::signOneTimeKey(const QString &key) const
+{
+ QJsonDocument j(QJsonObject{{"key", key}});
+ return sign(j.toJson(QJsonDocument::Compact));
+}
+
+std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const
+{
+ const auto error = olm_remove_one_time_keys(m_account, session->raw());
+
+ if (error == olm_error()) {
+ return lastError(m_account);
+ }
+ Q_EMIT needsSave();
+ return std::nullopt;
+}
+
+OlmAccount *QOlmAccount::data()
+{
+ return m_account;
+}
+
+DeviceKeys QOlmAccount::deviceKeys() const
+{
+ DeviceKeys deviceKeys;
+ deviceKeys.userId = m_userId;
+ deviceKeys.deviceId = m_deviceId;
+ deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"};
+
+ const auto idKeys = identityKeys();
+ deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519;
+ deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519;
+
+ const auto sign = signIdentityKeys();
+ deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign;
+
+ return deviceKeys;
+}
+
+UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys)
+{
+ auto keys = deviceKeys();
+
+ if (oneTimeKeys.curve25519().isEmpty()) {
+ return new UploadKeysJob(keys);
+ }
+
+ // Sign & append the one time keys.
+ auto temp = signOneTimeKeys(oneTimeKeys);
+ QHash<QString, QVariant> oneTimeKeysSigned;
+ for (const auto &[keyId, key] : asKeyValueRange(temp)) {
+ oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key));
+ }
+
+ return new UploadKeysJob(keys, oneTimeKeysSigned);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSession(this, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey)
+{
+ return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey);
+}
+
+void QOlmAccount::markKeysAsPublished()
+{
+ olm_account_mark_keys_as_published(m_account);
+ Q_EMIT needsSave();
+}
+
+bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys,
+ const QString &deviceId,
+ const QString &userId)
+{
+ const auto signKeyId = "ed25519:" + deviceId;
+ const auto signingKey = deviceKeys.keys[signKeyId];
+ const auto signature = deviceKeys.signatures[userId][signKeyId];
+
+ if (signature.isEmpty()) {
+ return false;
+ }
+
+ return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature);
+}
+
+bool Quotient::ed25519VerifySignature(const QString &signingKey,
+ const QJsonObject &obj,
+ const QString &signature)
+{
+ if (signature.isEmpty()) {
+ return false;
+ }
+ QJsonObject obj1 = obj;
+
+ obj1.remove("unsigned");
+ obj1.remove("signatures");
+
+ auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact);
+
+ QByteArray signingKeyBuf = signingKey.toUtf8();
+ QOlmUtility utility;
+ auto signatureBuf = signature.toUtf8();
+ auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf);
+ if (std::holds_alternative<QOlmError>(result)) {
+ return false;
+ }
+
+ return std::get<bool>(result);
+}
diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h
new file mode 100644
index 00000000..00afc0e6
--- /dev/null
+++ b/lib/e2ee/qolmaccount.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#pragma once
+
+#include "csapi/keys.h"
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmsession.h"
+#include <QObject>
+
+struct OlmAccount;
+
+namespace Quotient {
+
+class QOlmSession;
+class Connection;
+
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+//! An olm account manages all cryptographic keys used on a device.
+//! \code{.cpp}
+//! const auto olmAccount = new QOlmAccount(this);
+//! \endcode
+class QOlmAccount : public QObject
+{
+ Q_OBJECT
+public:
+ QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr);
+ ~QOlmAccount();
+
+ //! Creates a new instance of OlmAccount. During the instantiation
+ //! the Ed25519 fingerprint key pair and the Curve25519 identity key
+ //! pair are generated. For more information see <a
+ //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>.
+ //! This needs to be called before any other action or use unpickle() instead.
+ void createNewAccount();
+
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`.
+ //! This needs to be called before any other action or use createNewAccount() instead.
+ void unpickle(QByteArray &pickled, const PicklingMode &mode);
+
+ //! Serialises an OlmAccount to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+
+ //! Returns the account's public identity keys already formatted as JSON
+ IdentityKeys identityKeys() const;
+
+ //! Returns the signature of the supplied message.
+ QByteArray sign(const QByteArray &message) const;
+ QByteArray sign(const QJsonObject& message) const;
+
+ //! Sign identity keys.
+ QByteArray signIdentityKeys() const;
+
+ //! Maximum number of one time keys that this OlmAccount can
+ //! currently hold.
+ size_t maxNumberOfOneTimeKeys() const;
+
+ //! Generates the supplied number of one time keys.
+ size_t generateOneTimeKeys(size_t numberOfKeys) const;
+
+ //! Gets the OlmAccount's one time keys formatted as JSON.
+ OneTimeKeys oneTimeKeys() const;
+
+ //! Sign all one time keys.
+ QMap<QString, SignedOneTimeKey> signOneTimeKeys(const OneTimeKeys &keys) const;
+
+ //! Sign one time key.
+ QByteArray signOneTimeKey(const QString &key) const;
+
+ SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const;
+
+ UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys);
+
+ DeviceKeys deviceKeys() const;
+
+ //! Remove the one time key used to create the supplied session.
+ [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(const QOlmSessionPtr &session) const;
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param message An Olm pre-key message that was encrypted for this account.
+ std::variant<QOlmSessionPtr, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param theirIdentityKey - The identity key of the Olm account that
+ //! encrypted this Olm message.
+ std::variant<QOlmSessionPtr, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage);
+
+ //! Creates an outbound session for sending messages to a specific
+ /// identity and one time key.
+ std::variant<QOlmSessionPtr, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey);
+
+ void markKeysAsPublished();
+
+ // HACK do not use directly
+ QOlmAccount(OlmAccount *account);
+ OlmAccount *data();
+
+Q_SIGNALS:
+ void needsSave() const;
+
+private:
+ OlmAccount *m_account = nullptr; // owning
+ QString m_userId;
+ QString m_deviceId;
+};
+
+bool verifyIdentitySignature(const DeviceKeys &deviceKeys,
+ const QString &deviceId,
+ const QString &userId);
+
+//! checks if the signature is signed by the signing_key
+bool ed25519VerifySignature(const QString &signingKey,
+ const QJsonObject &obj,
+ const QString &signature);
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp
new file mode 100644
index 00000000..5a60b7e6
--- /dev/null
+++ b/lib/e2ee/qolmerrors.cpp
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#include "qolmerrors.h"
+#include "util.h"
+#include <QtCore/QLatin1String>
+
+Quotient::QOlmError Quotient::fromString(const char* error_raw) {
+ const QLatin1String error { error_raw };
+ if (error_raw == "BAD_ACCOUNT_KEY"_ls) {
+ return QOlmError::BadAccountKey;
+ } else if (error_raw == "BAD_MESSAGE_KEY_ID"_ls) {
+ return QOlmError::BadMessageKeyId;
+ } else if (error_raw == "INVALID_BASE64"_ls) {
+ return QOlmError::InvalidBase64;
+ } else if (error_raw == "NOT_ENOUGH_RANDOM"_ls) {
+ return QOlmError::NotEnoughRandom;
+ } else if (error_raw == "OUTPUT_BUFFER_TOO_SMALL"_ls) {
+ return QOlmError::OutputBufferTooSmall;
+ } else {
+ return QOlmError::Unknown;
+ }
+}
diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h
new file mode 100644
index 00000000..24e87d95
--- /dev/null
+++ b/lib/e2ee/qolmerrors.h
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+namespace Quotient {
+//! All errors that could be caused by an operation regarding Olm
+//! Errors are named exactly like the ones in libolm.
+enum QOlmError
+{
+ BadAccountKey,
+ BadMessageFormat,
+ BadMessageKeyId,
+ BadMessageMac,
+ BadMessageVersion,
+ InvalidBase64,
+ NotEnoughRandom,
+ OutputBufferTooSmall,
+ UnknownMessageIndex,
+ Unknown,
+};
+
+QOlmError fromString(const char* error_raw);
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp
new file mode 100644
index 00000000..2e9cc716
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.cpp
@@ -0,0 +1,151 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolminboundsession.h"
+#include <iostream>
+#include <cstring>
+
+using namespace Quotient;
+
+QOlmError lastError(OlmInboundGroupSession *session) {
+ return fromString(olm_inbound_group_session_last_error(session));
+}
+
+QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session)
+ : m_groupSession(session)
+{
+}
+
+QOlmInboundGroupSession::~QOlmInboundGroupSession()
+{
+ olm_clear_inbound_group_session(m_groupSession);
+ //delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::create(const QByteArray &key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ const auto error = olm_init_inbound_group_session(olmInboundGroupSession,
+ reinterpret_cast<const uint8_t *>(key.constData()), key.size());
+
+ if (error == olm_error()) {
+ throw lastError(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::import(const QByteArray &key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ QByteArray keyBuf = key;
+
+ const auto error = olm_import_inbound_group_session(olmInboundGroupSession,
+ reinterpret_cast<const uint8_t *>(keyBuf.data()), keyBuf.size());
+ if (error == olm_error()) {
+ throw lastError(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QByteArray toKey(const PicklingMode &mode)
+{
+ if (std::holds_alternative<Unencrypted>(mode)) {
+ return "";
+ }
+ return std::get<Encrypted>(mode).key;
+}
+
+QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0');
+ const QByteArray key = toKey(mode);
+ const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(),
+ pickledBuf.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return pickledBuf;
+}
+
+std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+{
+ QByteArray pickledBuf = pickled;
+ const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.size());
+ if (error == olm_error()) {
+ return lastError(groupSession);
+ }
+ key.clear();
+
+ return std::make_unique<QOlmInboundGroupSession>(groupSession);
+}
+
+std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message)
+{
+ // This is for capturing the output of olm_group_decrypt
+ uint32_t messageIndex = 0;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(message.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession,
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()), '0');
+
+ messageBuf = QByteArray(message.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()),
+ messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex);
+
+ // Error code or plaintext length is returned
+ const auto decryptError = plaintextLen;
+
+ if (decryptError == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ QByteArray output(plaintextLen, '0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextLen);
+
+ return std::make_pair<QString, qint32>(QString(output), messageIndex);
+}
+
+std::variant<QByteArray, QOlmError> QOlmInboundGroupSession::exportSession(uint32_t messageIndex)
+{
+ const auto keyLength = olm_export_inbound_group_session_length(m_groupSession);
+ QByteArray keyBuf(keyLength, '0');
+ const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast<uint8_t *>(keyBuf.data()), keyLength, messageIndex);
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+ return keyBuf;
+}
+
+uint32_t QOlmInboundGroupSession::firstKnownIndex() const
+{
+ return olm_inbound_group_session_first_known_index(m_groupSession);
+}
+
+QByteArray QOlmInboundGroupSession::sessionId() const
+{
+ QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0');
+ const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(sessionIdBuf.data()),
+ sessionIdBuf.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return sessionIdBuf;
+}
+
+bool QOlmInboundGroupSession::isVerified() const
+{
+ return olm_inbound_group_session_is_verified(m_groupSession) != 0;
+}
diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h
new file mode 100644
index 00000000..7d52991c
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+#include <variant>
+#include <memory>
+#include "olm/olm.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+
+//! An in-bound group session is responsible for decrypting incoming
+//! communication in a Megolm session.
+struct QOlmInboundGroupSession
+{
+public:
+ ~QOlmInboundGroupSession();
+ //! Creates a new instance of `OlmInboundGroupSession`.
+ static std::unique_ptr<QOlmInboundGroupSession> create(const QByteArray &key);
+ //! Import an inbound group session, from a previous export.
+ static std::unique_ptr<QOlmInboundGroupSession> import(const QByteArray &key);
+ //! Serialises an `OlmInboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling
+ //! an `OlmInboundGroupSession`.
+ static std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode);
+ //! Decrypts ciphertext received for this group session.
+ std::variant<std::pair<QString, uint32_t>, QOlmError> decrypt(const QByteArray &message);
+ //! Export the base64-encoded ratchet key for this session, at the given index,
+ //! in a format which can be used by import.
+ std::variant<QByteArray, QOlmError> exportSession(uint32_t messageIndex);
+ //! Get the first message index we know how to decrypt.
+ uint32_t firstKnownIndex() const;
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+ bool isVerified() const;
+ QOlmInboundGroupSession(OlmInboundGroupSession *session);
+private:
+ OlmInboundGroupSession *m_groupSession;
+};
+
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+using OlmInboundGroupSessionPtr = std::unique_ptr<OlmInboundGroupSession>;
+} // namespace Quotient
diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp
new file mode 100644
index 00000000..15008b75
--- /dev/null
+++ b/lib/e2ee/qolmmessage.cpp
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmmessage.h"
+
+using namespace Quotient;
+
+QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type)
+ : QByteArray(std::move(ciphertext))
+ , m_messageType(type)
+{
+ Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty");
+}
+
+QOlmMessage::QOlmMessage(const QOlmMessage &message)
+ : QByteArray(message)
+ , m_messageType(message.type())
+{
+}
+
+QOlmMessage::Type QOlmMessage::type() const
+{
+ return m_messageType;
+}
+
+QByteArray QOlmMessage::toCiphertext() const
+{
+ return QByteArray(*this);
+}
+
+QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext)
+{
+ return QOlmMessage(ciphertext, QOlmMessage::General);
+}
diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h
new file mode 100644
index 00000000..52aba78c
--- /dev/null
+++ b/lib/e2ee/qolmmessage.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QByteArray>
+
+namespace Quotient {
+
+/*! \brief A wrapper around an olm encrypted message
+ *
+ * This class encapsulates a Matrix olm encrypted message,
+ * passed in either of 2 forms: a general message or a pre-key message.
+ *
+ * The class provides functions to get a type and the ciphertext.
+ */
+class QOlmMessage : public QByteArray {
+ Q_GADGET
+public:
+ enum Type {
+ General,
+ PreKey,
+ };
+ Q_ENUM(Type)
+
+ QOlmMessage() = default;
+ explicit QOlmMessage(const QByteArray &ciphertext, Type type = General);
+ explicit QOlmMessage(const QOlmMessage &message);
+
+ static QOlmMessage fromCiphertext(const QByteArray &ciphertext);
+
+ Q_INVOKABLE Type type() const;
+ Q_INVOKABLE QByteArray toCiphertext() const;
+
+private:
+ Type m_messageType = General;
+};
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
new file mode 100644
index 00000000..da32417b
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -0,0 +1,125 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmoutboundsession.h"
+#include "e2ee/qolmutils.h"
+
+using namespace Quotient;
+
+QOlmError lastError(OlmOutboundGroupSession *session) {
+ return fromString(olm_outbound_group_session_last_error(session));
+}
+
+QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session)
+ : m_groupSession(session)
+{
+}
+
+QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
+{
+ olm_clear_outbound_group_session(m_groupSession);
+ delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create()
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession);
+ QByteArray randomBuf = getRandom(randomLength);
+
+ const auto error = olm_init_outbound_group_session(olmOutboundGroupSession,
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length());
+
+ if (error == olm_error()) {
+ throw lastError(olmOutboundGroupSession);
+ }
+
+ const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession);
+ QByteArray keyBuffer(keyMaxLength, '0');
+ olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()),
+ keyMaxLength);
+
+ randomBuf.clear();
+
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const PicklingMode &mode)
+{
+ QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0');
+ QByteArray key = toKey(mode);
+ const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ key.clear();
+
+ return pickledBuf;
+}
+
+std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode)
+{
+ QByteArray pickledBuf = pickled;
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(),
+ pickled.data(), pickled.length());
+ if (error == olm_error()) {
+ return lastError(olmOutboundGroupSession);
+ }
+ const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession);
+ QByteArray idBuffer(idMaxLength, '0');
+ olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+
+ key.clear();
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::encrypt(const QString &plaintext)
+{
+ QByteArray plaintextBuf = plaintext.toUtf8();
+ const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length());
+ QByteArray messageBuf(messageMaxLength, '0');
+ const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast<uint8_t *>(plaintextBuf.data()),
+ plaintextBuf.length(), reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ return messageBuf;
+}
+
+uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const
+{
+ return olm_outbound_group_session_message_index(m_groupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::sessionId() const
+{
+ const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession);
+ QByteArray idBuffer(idMaxLength, '0');
+ const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return idBuffer;
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::sessionKey() const
+{
+ const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession);
+ QByteArray keyBuffer(keyMaxLength, '0');
+ const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()),
+ keyMaxLength);
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+ return keyBuffer;
+}
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
new file mode 100644
index 00000000..39263c77
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "olm/olm.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/e2ee.h"
+#include <memory>
+
+namespace Quotient {
+
+//! An out-bound group session is responsible for encrypting outgoing
+//! communication in a Megolm session.
+class QOlmOutboundGroupSession
+{
+public:
+ ~QOlmOutboundGroupSession();
+ //! Creates a new instance of `QOlmOutboundGroupSession`.
+ //! Throw OlmError on errors
+ static std::unique_ptr<QOlmOutboundGroupSession> create();
+ //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ //! Deserialises from encrypted Base64 that was previously obtained by
+ //! pickling a `QOlmOutboundGroupSession`.
+ static std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode);
+ //! Encrypts a plaintext message using the session.
+ std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext);
+
+ //! Get the current message index for this session.
+ //!
+ //! Each message is sent with an increasing index; this returns the
+ //! index for the next message.
+ uint32_t sessionMessageIndex() const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Get the base64-encoded current ratchet key for this session.
+ //!
+ //! Each message is sent with a different ratchet key. This function returns the
+ //! ratchet key that will be used for the next message.
+ std::variant<QByteArray, QOlmError> sessionKey() const;
+ QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession);
+private:
+ OlmOutboundGroupSession *m_groupSession;
+};
+
+using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
+using OlmOutboundGroupSessionPtr = std::unique_ptr<OlmOutboundGroupSession>;
+}
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
new file mode 100644
index 00000000..e575ff39
--- /dev/null
+++ b/lib/e2ee/qolmsession.cpp
@@ -0,0 +1,251 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmsession.h"
+#include "e2ee/qolmutils.h"
+#include "logging.h"
+#include <cstring>
+#include <QDebug>
+
+using namespace Quotient;
+
+QOlmError lastError(OlmSession* session) {
+ return fromString(olm_session_last_error(session));
+}
+
+Quotient::QOlmSession::~QOlmSession()
+{
+ olm_clear_session(m_session);
+ delete[](reinterpret_cast<uint8_t *>(m_session));
+}
+
+OlmSession* QOlmSession::create()
+{
+ return olm_session(new uint8_t[olm_session_size()]);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey)
+{
+ if (preKeyMessage.type() != QOlmMessage::PreKey) {
+ qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat;
+ }
+
+ const auto olmSession = create();
+
+ QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ size_t error = 0;
+ if (from) {
+ error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ } else {
+ error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ }
+
+ if (error == olm_error()) {
+ const auto lastErr = lastError(olmSession);
+ qCWarning(E2EE) << "Error when creating inbound session" << lastErr;
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage)
+{
+ return createInbound(account, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage)
+{
+ return createInbound(account, preKeyMessage, true, theirIdentityKey);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey)
+{
+ auto *olmOutboundSession = create();
+ const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession);
+ QByteArray randomBuf = getRandom(randomLen);
+
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8();
+ const auto error = olm_create_outbound_session(olmOutboundSession,
+ account->data(),
+ reinterpret_cast<uint8_t *>(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(),
+ reinterpret_cast<uint8_t *>(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(),
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length());
+
+ if (error == olm_error()) {
+ const auto lastErr = lastError(olmOutboundSession);
+ if (lastErr == QOlmError::NotEnoughRandom) {
+ throw lastErr;
+ }
+ return lastErr;
+ }
+
+ randomBuf.clear();
+ return std::make_unique<QOlmSession>(olmOutboundSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode)
+{
+ 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());
+
+ if (error == olm_error()) {
+ return lastError(m_session);
+ }
+
+ key.clear();
+
+ return pickledBuf;
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+{
+ QByteArray pickledBuf = pickled;
+ auto *olmSession = create();
+ QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_session(olmSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length());
+ if (error == olm_error()) {
+ return lastError(olmSession);
+ }
+
+ key.clear();
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmMessage QOlmSession::encrypt(const QString &plaintext)
+{
+ QByteArray plaintextBuf = plaintext.toUtf8();
+ const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length());
+ QByteArray messageBuf(messageMaxLen, '0');
+ const auto messageType = encryptMessageType();
+ const auto randomLen = olm_encrypt_random_length(m_session);
+ QByteArray randomBuf = getRandom(randomLen);
+ const auto error = olm_encrypt(m_session,
+ reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(),
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length(),
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (error == olm_error()) {
+ throw lastError(m_session);
+ }
+
+ return QOlmMessage(messageBuf, messageType);
+}
+
+std::variant<QString, QOlmError> QOlmSession::decrypt(const QOlmMessage &message) const
+{
+ const auto messageType = message.type();
+ const auto ciphertext = message.toCiphertext();
+ const auto messageTypeValue = messageType == QOlmMessage::Type::General
+ ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(ciphertext.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue,
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (plaintextMaxLen == olm_error()) {
+ return lastError(m_session);
+ }
+
+ QByteArray plaintextBuf(plaintextMaxLen, '0');
+ QByteArray messageBuf2(ciphertext.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf2.begin());
+
+ const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue,
+ reinterpret_cast<uint8_t *>(messageBuf2.data()), messageBuf2.length(),
+ reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextMaxLen);
+
+ if (plaintextResultLen == olm_error()) {
+ const auto lastErr = lastError(m_session);
+ if (lastErr == QOlmError::OutputBufferTooSmall) {
+ throw lastErr;
+ }
+ return lastErr;
+ }
+ QByteArray output(plaintextResultLen, '0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen);
+ plaintextBuf.clear();
+ return output;
+}
+
+QOlmMessage::Type QOlmSession::encryptMessageType()
+{
+ const auto messageTypeResult = olm_encrypt_message_type(m_session);
+ if (messageTypeResult == olm_error()) {
+ throw lastError(m_session);
+ }
+ if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) {
+ return QOlmMessage::PreKey;
+ }
+ return QOlmMessage::General;
+}
+
+QByteArray QOlmSession::sessionId() const
+{
+ const auto idMaxLength = olm_session_id_length(m_session);
+ QByteArray idBuffer(idMaxLength, '0');
+ const auto error = olm_session_id(m_session, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+ if (error == olm_error()) {
+ throw lastError(m_session);
+ }
+ return idBuffer;
+}
+
+bool QOlmSession::hasReceivedMessage() const
+{
+ return olm_session_has_received_message(m_session);
+}
+
+std::variant<bool, QOlmError> QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey);
+ QByteArray oneTimeKeyBuf(preKeyMessage.data());
+ const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length());
+
+ if (matchesResult == olm_error()) {
+ return lastError(m_session);
+ }
+ switch (matchesResult) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return QOlmError::Unknown;
+ }
+}
+std::variant<bool, QOlmError> QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const
+{
+ const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
+ oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_session);
+ }
+ switch (error) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return QOlmError::Unknown;
+ }
+}
+
+QOlmSession::QOlmSession(OlmSession *session)
+ : m_session(session)
+{
+}
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
new file mode 100644
index 00000000..1febfa0f
--- /dev/null
+++ b/lib/e2ee/qolmsession.h
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QDebug>
+#include <olm/olm.h> // FIXME: OlmSession
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolmaccount.h"
+
+namespace Quotient {
+
+class QOlmAccount;
+class QOlmSession;
+
+
+//! Either an outbound or inbound session for secure communication.
+class QOlmSession
+{
+public:
+ ~QOlmSession();
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage);
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage);
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey);
+ //! Serialises an `QOlmSession` to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`.
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode);
+ //! Encrypts a plaintext message using the session.
+ QOlmMessage encrypt(const QString &plaintext);
+
+ //! Decrypts a message using this session. Decoding is lossy, meaing if
+ //! the decrypted plaintext contains invalid UTF-8 symbols, they will
+ //! be returned as `U+FFFD` (�).
+ std::variant<QString, QOlmError> decrypt(const QOlmMessage &message) const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! The type of the next message that will be returned from encryption.
+ QOlmMessage::Type encryptMessageType();
+
+ //! Checker for any received messages for this session.
+ bool hasReceivedMessage() const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ std::variant<bool, QOlmError> matchesInboundSession(const QOlmMessage &preKeyMessage) const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ std::variant<bool, QOlmError> matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const;
+
+ friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs)
+ {
+ return lhs.sessionId() < rhs.sessionId();
+ }
+
+ friend bool operator<(const std::unique_ptr<QOlmSession> &lhs, const std::unique_ptr<QOlmSession> &rhs) {
+ return *lhs < *rhs;
+ }
+
+ OlmSession *raw() const
+ {
+ return m_session;
+ }
+ QOlmSession(OlmSession* session);
+private:
+ //! Helper function for creating new sessions and handling errors.
+ static OlmSession* create();
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = "");
+ OlmSession* m_session;
+};
+} //namespace Quotient
diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp
new file mode 100644
index 00000000..303f6d75
--- /dev/null
+++ b/lib/e2ee/qolmutility.cpp
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutility.h"
+#include "olm/olm.h"
+#include <QDebug>
+
+using namespace Quotient;
+
+// Convert olm error to enum
+QOlmError lastError(OlmUtility *utility) {
+ return fromString(olm_utility_last_error(utility));
+}
+
+QOlmUtility::QOlmUtility()
+{
+ auto utility = new uint8_t[olm_utility_size()];
+ m_utility = olm_utility(utility);
+}
+
+QOlmUtility::~QOlmUtility()
+{
+ olm_clear_utility(m_utility);
+ delete[](reinterpret_cast<uint8_t *>(m_utility));
+}
+
+QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const
+{
+ const auto outputLen = olm_sha256_length(m_utility);
+ QByteArray outputBuf(outputLen, '0');
+ olm_sha256(m_utility, inputBuf.data(), inputBuf.length(),
+ outputBuf.data(), outputBuf.length());
+
+ return QString::fromUtf8(outputBuf);
+}
+
+QString QOlmUtility::sha256Utf8Msg(const QString &message) const
+{
+ return sha256Bytes(message.toUtf8());
+}
+
+std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key,
+ const QByteArray &message, const QByteArray &signature)
+{
+ QByteArray signatureBuf(signature.length(), '0');
+ std::copy(signature.begin(), signature.end(), signatureBuf.begin());
+
+ const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(),
+ message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size());
+
+ const auto error = ret;
+ if (error == olm_error()) {
+ return lastError(m_utility);
+ }
+
+ if (ret != 0) {
+ return false;
+ }
+ return true;
+}
diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h
new file mode 100644
index 00000000..b360d625
--- /dev/null
+++ b/lib/e2ee/qolmutility.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QObject>
+#include <variant>
+#include "e2ee/qolmerrors.h"
+
+struct OlmUtility;
+
+namespace Quotient {
+
+class QOlmSession;
+class Connection;
+
+//! Allows you to make use of crytographic hashing via SHA-2 and
+//! verifying ed25519 signatures.
+class QOlmUtility
+{
+public:
+ QOlmUtility();
+ ~QOlmUtility();
+
+ //! Returns a sha256 of the supplied byte slice.
+ QString sha256Bytes(const QByteArray &inputBuf) const;
+
+ //! Convenience function that converts the UTF-8 message
+ //! to bytes and then calls `sha256Bytes()`, returning its output.
+ QString sha256Utf8Msg(const QString &message) const;
+
+ //! Verify a ed25519 signature.
+ //! \param key QByteArray The public part of the ed25519 key that signed the message.
+ //! \param message QByteArray The message that was signed.
+ //! \param signature QByteArray The signature of the message.
+ std::variant<bool, QOlmError> ed25519Verify(const QByteArray &key,
+ const QByteArray &message, const QByteArray &signature);
+
+
+private:
+ OlmUtility *m_utility;
+
+};
+}
diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp
new file mode 100644
index 00000000..6f7937e8
--- /dev/null
+++ b/lib/e2ee/qolmutils.cpp
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutils.h"
+#include <QtCore/QRandomGenerator>
+
+using namespace Quotient;
+
+QByteArray Quotient::toKey(const Quotient::PicklingMode &mode)
+{
+ if (std::holds_alternative<Quotient::Unencrypted>(mode)) {
+ return {};
+ }
+ return std::get<Quotient::Encrypted>(mode).key;
+}
+
+QByteArray Quotient::getRandom(size_t bufferSize)
+{
+ QByteArray buffer(bufferSize, '0');
+ QRandomGenerator::system()->generate(buffer.begin(), buffer.end());
+ return buffer;
+}
diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h
new file mode 100644
index 00000000..bbd71332
--- /dev/null
+++ b/lib/e2ee/qolmutils.h
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+// Convert PicklingMode to key
+QByteArray toKey(const PicklingMode &mode);
+QByteArray getRandom(size_t bufferSize);
+}
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
deleted file mode 100644
index 37f3b7c3..00000000
--- a/lib/encryptionmanager.cpp
+++ /dev/null
@@ -1,373 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#ifdef Quotient_E2EE_ENABLED
-#include "encryptionmanager.h"
-
-#include "connection.h"
-#include "e2ee.h"
-
-#include "csapi/keys.h"
-
-#include <QtCore/QHash>
-#include <QtCore/QStringBuilder>
-
-#include <account.h> // QtOlm
-#include <session.h> // QtOlm
-#include <message.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <utils.h> // QtOlm
-#include <functional>
-#include <memory>
-
-using namespace Quotient;
-using namespace QtOlm;
-using std::move;
-
-class EncryptionManager::Private {
-public:
- explicit Private(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion, float oneTimeKeyThreshold)
- : q(nullptr)
- , signedKeysProportion(move(signedKeysProportion))
- , oneTimeKeyThreshold(move(oneTimeKeyThreshold))
- {
- Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1));
- Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1));
- if (encryptionAccountPickle.isEmpty()) {
- olmAccount.reset(new Account());
- } else {
- olmAccount.reset(
- new Account(encryptionAccountPickle)); // TODO: passphrase even
- // with qtkeychain?
- }
- /*
- * Note about targetKeysNumber:
- *
- * From: https://github.com/Zil0/matrix-python-sdk/
- * File: matrix_client/crypto/olm_device.py
- *
- * Try to maintain half the number of one-time keys libolm can hold
- * uploaded on the HS. This is because some keys will be claimed by
- * peers but not used instantly, and we want them to stay in libolm,
- * until the limit is reached and it starts discarding keys, starting by
- * the oldest.
- */
- targetKeysNumber = olmAccount->maxOneTimeKeys() / 2;
- targetOneTimeKeyCounts = {
- { SignedCurve25519Key,
- qRound(signedKeysProportion * targetKeysNumber) },
- { Curve25519Key,
- qRound((1 - signedKeysProportion) * targetKeysNumber) }
- };
- updateKeysToUpload();
- }
- ~Private() = default;
-
- EncryptionManager* q;
-
- UploadKeysJob* uploadIdentityKeysJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysInitJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysJob = nullptr;
- QueryKeysJob* queryKeysJob = nullptr;
-
- QScopedPointer<Account> olmAccount;
-
- float signedKeysProportion;
- float oneTimeKeyThreshold;
- int targetKeysNumber;
-
- void updateKeysToUpload();
- bool oneTimeKeyShouldUpload();
-
- QHash<QString, int> oneTimeKeyCounts;
- void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue)
- {
- oneTimeKeyCounts = oneTimeKeyCountsNewValue;
- updateKeysToUpload();
- }
- QHash<QString, int> oneTimeKeysToUploadCounts;
- QHash<QString, int> targetOneTimeKeyCounts;
-
- // A map from senderKey to InboundSession
- QMap<QString, InboundSession*> sessions; // TODO: cache
- void updateDeviceKeys(
- const QHash<QString,
- QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys)
- {
- for (auto userId : deviceKeys.keys()) {
- for (auto deviceId : deviceKeys.value(userId).keys()) {
- auto info = deviceKeys.value(userId).value(deviceId);
- // TODO: ed25519Verify, etc
- }
- }
- }
- QString sessionDecrypt(Message* message, const QString& senderKey)
- {
- QString decrypted;
- QList<InboundSession*> senderSessions = sessions.values(senderKey);
- // Try to decrypt message body using one of the known sessions for that
- // device
- bool sessionsPassed = false;
- for (auto senderSession : senderSessions) {
- if (senderSession == senderSessions.last()) {
- sessionsPassed = true;
- }
- try {
- decrypted = senderSession->decrypt(message);
- qCDebug(E2EE)
- << "Success decrypting Olm event using existing session"
- << senderSession->id();
- break;
- } catch (OlmError* e) {
- if (message->messageType() == 0) {
- PreKeyMessage preKeyMessage =
- PreKeyMessage(message->cipherText());
- if (senderSession->matches(&preKeyMessage, senderKey)) {
- // We had a matching session for a pre-key message, but
- // it didn't work. This means something is wrong, so we
- // fail now.
- qCDebug(E2EE)
- << "Error decrypting pre-key message with existing "
- "Olm session"
- << senderSession->id() << "reason:" << e->what();
- return QString();
- }
- }
- // Simply keep trying otherwise
- }
- }
- if (sessionsPassed || senderSessions.empty()) {
- if (message->messageType() > 0) {
- // Not a pre-key message, we should have had a matching session
- if (!sessions.empty()) {
- qCDebug(E2EE) << "Error decrypting with existing sessions";
- return QString();
- }
- qCDebug(E2EE) << "No existing sessions";
- return QString();
- }
- // We have a pre-key message without any matching session, in this
- // case we should try to create one.
- InboundSession* newSession;
- qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey;
- PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText());
- try {
- newSession = new InboundSession(olmAccount.data(),
- &preKeyMessage,
- senderKey.toLatin1(), q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Error decrypting pre-key message when trying "
- "to establish a new session:"
- << e->what();
- return QString();
- }
- qCDebug(E2EE) << "Created new Olm session" << newSession->id();
- try {
- decrypted = newSession->decrypt(message);
- } catch (OlmError* e) {
- qCDebug(E2EE)
- << "Error decrypting pre-key message with new session"
- << e->what();
- return QString();
- }
- olmAccount->removeOneTimeKeys(newSession);
- sessions.insert(senderKey, newSession);
- }
- return decrypted;
- }
-};
-
-EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion,
- float oneTimeKeyThreshold, QObject* parent)
- : QObject(parent)
- , d(std::make_unique<Private>(std::move(encryptionAccountPickle),
- std::move(signedKeysProportion),
- std::move(oneTimeKeyThreshold)))
-{
- d->q = this;
-}
-
-EncryptionManager::~EncryptionManager() = default;
-
-void EncryptionManager::uploadIdentityKeys(Connection* connection)
-{
- // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload
- DeviceKeys deviceKeys {
- /*
- * The ID of the user the device belongs to. Must match the user ID used
- * when logging in. The ID of the device these keys belong to. Must
- * match the device ID used when logging in. The encryption algorithms
- * supported by this device.
- */
- connection->userId(),
- connection->deviceId(),
- SupportedAlgorithms,
- /*
- * Public identity keys. The names of the properties should be in the
- * format <algorithm>:<device_id>. The keys themselves should be encoded
- * as specified by the key algorithm.
- */
- { { Curve25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->curve25519IdentityKey() },
- { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->ed25519IdentityKey() } },
- /* signatures should be provided after the unsigned deviceKeys
- generation */
- {}
- };
-
- QJsonObject deviceKeysJsonObject = toJson(deviceKeys);
- /* additionally removing signatures key,
- * since we could not initialize deviceKeys
- * without an empty signatures value:
- */
- deviceKeysJsonObject.remove(QStringLiteral("signatures"));
- /*
- * Signatures for the device key object.
- * A map from user ID, to a map from <algorithm>:<device_id> to the
- * signature. The signature is calculated using the process called Signing
- * JSON.
- */
- deviceKeys.signatures = {
- { connection->userId(),
- { { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->sign(deviceKeysJsonObject) } } }
- };
-
- d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
- connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
- });
-}
-
-void EncryptionManager::uploadOneTimeKeys(Connection* connection,
- bool forceUpdate)
-{
- if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
- d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
- connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadOneTimeKeysInitJob->oneTimeKeyCounts());
- });
- }
-
- int signedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0);
- int unsignedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0);
-
- d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount
- + unsignedKeysToUploadCount);
-
- QHash<QString, QVariant> oneTimeKeys = {};
- const auto& olmAccountCurve25519OneTimeKeys =
- d->olmAccount->curve25519OneTimeKeys();
-
- int oneTimeKeysCounter = 0;
- for (auto it = olmAccountCurve25519OneTimeKeys.cbegin();
- it != olmAccountCurve25519OneTimeKeys.cend(); ++it) {
- QString keyId = it.key();
- QString keyType;
- QVariant key;
- if (oneTimeKeysCounter < signedKeysToUploadCount) {
- QJsonObject message { { QStringLiteral("key"),
- it.value().toString() } };
-
- QByteArray signedMessage = d->olmAccount->sign(message);
- QJsonObject signatures {
- { connection->userId(),
- QJsonObject { { Ed25519Key + QStringLiteral(":")
- + connection->deviceId(),
- QString::fromUtf8(signedMessage) } } }
- };
- message.insert(QStringLiteral("signatures"), signatures);
- key = message;
- keyType = SignedCurve25519Key;
- } else {
- key = it.value();
- keyType = Curve25519Key;
- }
- ++oneTimeKeysCounter;
- oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key);
- }
- d->uploadOneTimeKeysJob =
- connection->callApi<UploadKeysJob>(none, oneTimeKeys);
- connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts());
- });
- d->olmAccount->markKeysAsPublished();
- qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
- .arg(signedKeysToUploadCount)
- .arg(unsignedKeysToUploadCount);
-}
-
-void EncryptionManager::updateOneTimeKeyCounts(
- Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount)
-{
- d->oneTimeKeyCounts = deviceOneTimeKeysCount;
- if (d->oneTimeKeyShouldUpload()) {
- qCDebug(E2EE) << "Uploading new one-time keys.";
- uploadOneTimeKeys(connection);
- }
-}
-
-void Quotient::EncryptionManager::updateDeviceKeys(
- Connection* connection, const QHash<QString, QStringList>& deviceKeys)
-{
- d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys);
- connect(d->queryKeysJob, &BaseJob::success, this,
- [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); });
-}
-
-QString EncryptionManager::sessionDecryptMessage(
- const QJsonObject& personalCipherObject, const QByteArray& senderKey)
-{
- QString decrypted;
- int type = personalCipherObject.value(TypeKeyL).toInt(-1);
- QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
- if (type == 0) {
- PreKeyMessage preKeyMessage { body };
- decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage),
- senderKey);
- } else if (type == 1) {
- Message message { body };
- decrypted = d->sessionDecrypt(&message, senderKey);
- }
- return decrypted;
-}
-
-QByteArray EncryptionManager::olmAccountPickle()
-{
- return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain?
-}
-
-QtOlm::Account* EncryptionManager::account() const
-{
- return d->olmAccount.data();
-}
-
-void EncryptionManager::Private::updateKeysToUpload()
-{
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- int numKeys = oneTimeKeyCounts.value(it.key(), 0);
- int numToCreate = qMax(it.value() - numKeys, 0);
- oneTimeKeysToUploadCounts.insert(it.key(), numToCreate);
- }
-}
-
-bool EncryptionManager::Private::oneTimeKeyShouldUpload()
-{
- if (oneTimeKeyCounts.empty())
- return true;
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- if (oneTimeKeyCounts.value(it.key(), 0)
- < it.value() * oneTimeKeyThreshold)
- return true;
- }
- return false;
-}
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h
deleted file mode 100644
index 714f95fd..00000000
--- a/lib/encryptionmanager.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#ifdef Quotient_E2EE_ENABLED
-#pragma once
-
-#include <QtCore/QObject>
-
-#include <functional>
-#include <memory>
-
-namespace QtOlm {
-class Account;
-}
-
-namespace Quotient {
-class Connection;
-
-class EncryptionManager : public QObject {
- Q_OBJECT
-
-public:
- // TODO: store constats separately?
- // TODO: 0.5 oneTimeKeyThreshold instead of 0.1?
- explicit EncryptionManager(
- const QByteArray& encryptionAccountPickle = QByteArray(),
- float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1),
- QObject* parent = nullptr);
- ~EncryptionManager();
-
- void uploadIdentityKeys(Connection* connection);
- void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false);
- void
- updateOneTimeKeyCounts(Connection* connection,
- const QHash<QString, int>& deviceOneTimeKeysCount);
- void updateDeviceKeys(Connection* connection,
- const QHash<QString, QStringList>& deviceKeys);
- QString sessionDecryptMessage(const QJsonObject& personalCipherObject,
- const QByteArray& senderKey);
- QByteArray olmAccountPickle();
-
- QtOlm::Account* account() const;
-
-private:
- class Private;
- std::unique_ptr<Private> d;
-};
-
-} // namespace Quotient
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
index 0290f973..1b5e4441 100644
--- a/lib/events/encryptedevent.cpp
+++ b/lib/events/encryptedevent.cpp
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "encryptedevent.h"
+#include "roommessageevent.h"
+#include "events/eventloader.h"
using namespace Quotient;
@@ -30,3 +32,22 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj)
{
qCDebug(E2EE) << "Encrypted event from" << senderId();
}
+
+RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const
+{
+ auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object();
+ eventObject["event_id"] = id();
+ eventObject["sender"] = senderId();
+ eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch();
+ if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) {
+ auto content = eventObject["content"].toObject();
+ content["m.relates_to"] = relatesToJson.toObject();
+ eventObject["content"] = content;
+ }
+ if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) {
+ auto unsign = eventObject["unsigned"].toObject();
+ unsign["redacts"] = redactsJson.toString();
+ eventObject["unsigned"] = unsign;
+ }
+ return loadEvent<RoomEvent>(eventObject);
+}
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index 81343a29..c838bbd8 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -3,7 +3,7 @@
#pragma once
-#include "e2ee.h"
+#include "e2ee/e2ee.h"
#include "roomevent.h"
namespace Quotient {
@@ -61,6 +61,7 @@ public:
/* device_id and session_id are required with Megolm */
QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); }
QString sessionId() const { return contentPart<QString>(SessionIdKeyL); }
+ RoomEventPtr createDecrypted(const QString &decrypted) const;
};
REGISTER_EVENT_TYPE(EncryptedEvent)
diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp
new file mode 100644
index 00000000..74119127
--- /dev/null
+++ b/lib/events/encryptedfile.cpp
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "encryptedfile.h"
+#include "logging.h"
+
+#include <openssl/evp.h>
+#include <QtCore/QCryptographicHash>
+
+using namespace Quotient;
+
+QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const
+{
+ QString _key = key.k;
+ _key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1());
+ const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1());
+ if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return QByteArray();
+ }
+ QByteArray plaintext(ciphertext.size(), 0);
+ EVP_CIPHER_CTX *ctx;
+ int length;
+ ctx = EVP_CIPHER_CTX_new();
+ EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)_key.data(), (const unsigned char *)iv.toLatin1().data());
+ EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size());
+ EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length);
+ EVP_CIPHER_CTX_free(ctx);
+ return plaintext;
+}
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
index 24ac9de1..6199be8e 100644
--- a/lib/events/encryptedfile.h
+++ b/lib/events/encryptedfile.h
@@ -44,6 +44,8 @@ public:
QString iv;
QHash<QString, QString> hashes;
QString v;
+
+ QByteArray decryptFile(const QByteArray &ciphertext) const;
};
template <>
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index aa05a96e..6272c668 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -4,7 +4,7 @@
#include "encryptionevent.h"
-#include "e2ee.h"
+#include "e2ee/e2ee.h"
#include <array>
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index 4ce130a6..9d7edf20 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -74,6 +74,7 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("size"), payloadSize);
if (mimeType.isValid())
infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
+ //TODO add encryptedfile
}
ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp
new file mode 100644
index 00000000..4803955d
--- /dev/null
+++ b/lib/events/keyverificationevent.cpp
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "keyverificationevent.h"
+
+using namespace Quotient;
+
+KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationRequestEvent::fromDevice() const
+{
+ return contentPart<QString>("from_device"_ls);
+}
+
+QString KeyVerificationRequestEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QStringList KeyVerificationRequestEvent::methods() const
+{
+ return contentPart<QStringList>("methods"_ls);
+}
+
+uint64_t KeyVerificationRequestEvent::timestamp() const
+{
+ return contentPart<double>("timestamp"_ls);
+}
+
+KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationStartEvent::fromDevice() const
+{
+ return contentPart<QString>("from_device"_ls);
+}
+
+QString KeyVerificationStartEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationStartEvent::method() const
+{
+ return contentPart<QString>("method"_ls);
+}
+
+Omittable<QString> KeyVerificationStartEvent::nextMethod() const
+{
+ return contentPart<Omittable<QString>>("method_ls");
+}
+
+QStringList KeyVerificationStartEvent::keyAgreementProtocols() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("key_agreement_protocols"_ls);
+}
+
+QStringList KeyVerificationStartEvent::hashes() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("hashes"_ls);
+
+}
+
+QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("message_authentication_codes"_ls);
+}
+
+QString KeyVerificationStartEvent::shortAuthenticationString() const
+{
+ return contentPart<QString>("short_authentification_string"_ls);
+}
+
+KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationAcceptEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationAcceptEvent::method() const
+{
+ return contentPart<QString>("method"_ls);
+}
+
+QString KeyVerificationAcceptEvent::keyAgreementProtocol() const
+{
+ return contentPart<QString>("key_agreement_protocol"_ls);
+}
+
+QString KeyVerificationAcceptEvent::hashData() const
+{
+ return contentPart<QString>("hash"_ls);
+}
+
+QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const
+{
+ return contentPart<QStringList>("short_authentification_string"_ls);
+}
+
+QString KeyVerificationAcceptEvent::commitement() const
+{
+ return contentPart<QString>("commitment"_ls);
+}
+
+KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationCancelEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationCancelEvent::reason() const
+{
+ return contentPart<QString>("reason"_ls);
+}
+
+QString KeyVerificationCancelEvent::code() const
+{
+ return contentPart<QString>("code"_ls);
+}
+
+KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationKeyEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationKeyEvent::key() const
+{
+ return contentPart<QString>("key"_ls);
+}
+
+KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationMacEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationMacEvent::keys() const
+{
+ return contentPart<QString>("keys"_ls);
+}
+
+QHash<QString, QString> KeyVerificationMacEvent::mac() const
+{
+ return contentPart<QHash<QString, QString>>("mac"_ls);
+}
diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h
new file mode 100644
index 00000000..13e7dcdd
--- /dev/null
+++ b/lib/events/keyverificationevent.h
@@ -0,0 +1,167 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "event.h"
+
+namespace Quotient {
+
+/// Requests a key verification with another user's devices.
+/// Typically sent as a to-device event.
+class KeyVerificationRequestEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent)
+
+ explicit KeyVerificationRequestEvent(const QJsonObject& obj);
+
+ /// The device ID which is initiating the request.
+ QString fromDevice() const;
+
+ /// An opaque identifier for the verification request. Must
+ /// be unique with respect to the devices involved.
+ QString transactionId() const;
+
+ /// The verification methods supported by the sender.
+ QStringList methods() const;
+
+ /// The POSIX timestamp in milliseconds for when the request was
+ /// made. If the request is in the future by more than 5 minutes or
+ /// more than 10 minutes in the past, the message should be ignored
+ /// by the receiver.
+ uint64_t timestamp() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationRequestEvent)
+
+/// Begins a key verification process.
+class KeyVerificationStartEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent)
+
+ explicit KeyVerificationStartEvent(const QJsonObject &obj);
+
+ /// The device ID which is initiating the process.
+ QString fromDevice() const;
+
+ /// An opaque identifier for the verification request. Must
+ /// be unique with respect to the devices involved.
+ QString transactionId() const;
+
+ /// The verification method to use.
+ QString method() const;
+
+ /// Optional method to use to verify the other user's key with.
+ Omittable<QString> nextMethod() const;
+
+ // SAS.V1 methods
+
+ /// The key agreement protocols the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList keyAgreementProtocols() const;
+
+ /// The hash methods the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList hashes() const;
+
+ /// The message authentication codes that the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList messageAuthenticationCodes() const;
+
+ /// The SAS methods the sending device (and the sending device's
+ /// user) understands.
+ /// \note Only exist if method is m.sas.v1
+ QString shortAuthenticationString() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationStartEvent)
+
+/// Accepts a previously sent m.key.verification.start message.
+/// Typically sent as a to-device event.
+class KeyVerificationAcceptEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent)
+
+ explicit KeyVerificationAcceptEvent(const QJsonObject& obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The verification method to use. Must be 'm.sas.v1'.
+ QString method() const;
+
+ /// The key agreement protocol the device is choosing to use, out of
+ /// the options in the m.key.verification.start message.
+ QString keyAgreementProtocol() const;
+
+ /// The hash method the device is choosing to use, out of the
+ /// options in the m.key.verification.start message.
+ QString hashData() const;
+
+ /// The message authentication code the device is choosing to use, out
+ /// of the options in the m.key.verification.start message.
+ QString messageAuthenticationCode() const;
+
+ /// The SAS methods both devices involved in the verification process understand.
+ QStringList shortAuthenticationString() const;
+
+ /// The hash (encoded as unpadded base64) of the concatenation of the
+ /// device's ephemeral public key (encoded as unpadded base64) and the
+ /// canonical JSON representation of the m.key.verification.start message.
+ QString commitement() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent)
+
+class KeyVerificationCancelEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent)
+
+ explicit KeyVerificationCancelEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// A human readable description of the code. The client should only
+ /// rely on this string if it does not understand the code.
+ QString reason() const;
+
+ /// The error code for why the process/request was cancelled by the user.
+ QString code() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationCancelEvent)
+
+/// Sends the ephemeral public key for a device to the partner device.
+/// Typically sent as a to-device event.
+class KeyVerificationKeyEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent)
+
+ explicit KeyVerificationKeyEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QString key() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationKeyEvent)
+
+/// Sends the MAC of a device's key to the partner device.
+class KeyVerificationMacEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent)
+
+ explicit KeyVerificationMacEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QString keys() const;
+
+ QHash<QString, QString> mac() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationMacEvent)
+} // namespace Quotient
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 3502e3f7..2f482871 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -122,3 +122,18 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json)
if (callId().isEmpty())
qCWarning(EVENTS) << id() << "is a call event with an empty call id";
}
+
+#ifdef Quotient_E2EE_ENABLED
+void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent)
+{
+ _originalEvent = std::move(originalEvent);
+}
+
+const QJsonObject RoomEvent::encryptedJson() const
+{
+ if(!_originalEvent) {
+ return {};
+ }
+ return _originalEvent->fullJson();
+}
+#endif
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index dcee1170..c4b0131a 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -60,11 +60,21 @@ public:
//! callback for that in RoomEvent.
void addId(const QString& newId);
+#ifdef Quotient_E2EE_ENABLED
+ void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent);
+ const RoomEvent* originalEvent() { return _originalEvent.get(); }
+ const QJsonObject encryptedJson() const;
+#endif
+
protected:
void dumpTo(QDebug dbg) const override;
private:
event_ptr_tt<RedactionEvent> _redactedBecause;
+
+#ifdef Quotient_E2EE_ENABLED
+ event_ptr_tt<RoomEvent> _originalEvent;
+#endif
};
using RoomEventPtr = event_ptr_tt<RoomEvent>;
using RoomEvents = EventsArray<RoomEvent>;
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 4a507ebd..634e5fb9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -7,8 +7,12 @@
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QNetworkReply>
-using namespace Quotient;
+#ifdef Quotient_E2EE_ENABLED
+# include <QtCore/QCryptographicHash>
+# include "events/encryptedfile.h"
+#endif
+using namespace Quotient;
class DownloadFileJob::Private {
public:
Private() : tempFile(new QTemporaryFile()) {}
@@ -20,6 +24,10 @@ public:
QScopedPointer<QFile> targetFile;
QScopedPointer<QFile> tempFile;
+
+#ifdef Quotient_E2EE_ENABLED
+ Omittable<EncryptedFile> encryptedFile;
+#endif
};
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
@@ -38,6 +46,19 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
setObjectName(QStringLiteral("DownloadFileJob"));
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const EncryptedFile& file,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
+{
+ setObjectName(QStringLiteral("DownloadFileJob"));
+ d->encryptedFile = file;
+}
+#endif
QString DownloadFileJob::targetFileName() const
{
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
@@ -52,7 +73,7 @@ void DownloadFileJob::doPrepare()
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) {
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) {
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
setStatus(FileError, "Could not open the temporary download file");
@@ -100,18 +121,46 @@ void DownloadFileJob::beforeAbandon()
BaseJob::Status DownloadFileJob::prepareResult()
{
if (d->targetFile) {
- d->targetFile->close();
- if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
- return { FileError, "Couldn't finalise the download" };
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFile.has_value()) {
+ d->tempFile->seek(0);
+ QByteArray encrypted = d->tempFile->readAll();
+
+ EncryptedFile file = *d->encryptedFile;
+ auto decrypted = file.decryptFile(encrypted);
+ d->targetFile->write(decrypted);
+ d->tempFile->remove();
+ } else {
+#endif
+ d->targetFile->close();
+ if (!d->targetFile->remove()) {
+ qCWarning(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()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+#ifdef Quotient_E2EE_ENABLED
}
- if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
- << "to" << d->targetFile->fileName();
- return { FileError, "Couldn't finalise the download" };
+#endif
+ } else {
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFile.has_value()) {
+ d->tempFile->seek(0);
+ auto encrypted = d->tempFile->readAll();
+
+ EncryptedFile file = *d->encryptedFile;
+ auto decrypted = file.decryptFile(encrypted);
+ d->tempFile->write(decrypted);
+ } else {
+#endif
+ d->tempFile->close();
+#ifdef Quotient_E2EE_ENABLED
}
- } else
- d->tempFile->close();
+#endif
+ }
qCDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index f8c62e4b..ffa3d055 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -4,6 +4,7 @@
#pragma once
#include "csapi/content-repo.h"
+#include "events/encryptedfile.h"
namespace Quotient {
class QUOTIENT_API DownloadFileJob : public GetContentJob {
@@ -14,6 +15,9 @@ public:
DownloadFileJob(const QString& serverName, const QString& mediaId,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {});
+#endif
QString targetFileName() const;
private:
diff --git a/lib/logging.cpp b/lib/logging.cpp
index 15eac69d..460caced 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -19,3 +19,4 @@ LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync")
LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail")
LOGGING_CATEGORY(NETWORK, "quotient.network")
LOGGING_CATEGORY(PROFILER, "quotient.profiler")
+LOGGING_CATEGORY(DATABASE, "quotient.database")
diff --git a/lib/logging.h b/lib/logging.h
index 5bf050a9..fc0a4c99 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -19,6 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB)
Q_DECLARE_LOGGING_CATEGORY(NETWORK)
Q_DECLARE_LOGGING_CATEGORY(PROFILER)
+Q_DECLARE_LOGGING_CATEGORY(DATABASE)
namespace Quotient {
// QDebug manipulators
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
index d3cc3c37..1d40c5e1 100644
--- a/lib/mxcreply.cpp
+++ b/lib/mxcreply.cpp
@@ -3,8 +3,15 @@
#include "mxcreply.h"
+#include <QtCore/QBuffer>
+#include "accountregistry.h"
+#include "connection.h"
#include "room.h"
+#ifdef Quotient_E2EE_ENABLED
+#include "events/encryptedfile.h"
+#endif
+
using namespace Quotient;
class MxcReply::Private
@@ -14,11 +21,14 @@ public:
: m_reply(r)
{}
QNetworkReply* m_reply;
+ Omittable<EncryptedFile> m_encryptedFile;
+ QIODevice* m_device = nullptr;
};
MxcReply::MxcReply(QNetworkReply* reply)
: d(makeImpl<Private>(reply))
{
+ d->m_device = d->m_reply;
reply->setParent(this);
connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
setError(d->m_reply->error(), d->m_reply->errorString());
@@ -31,11 +41,33 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
: d(makeImpl<Private>(reply))
{
reply->setParent(this);
- connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() {
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
setError(d->m_reply->error(), d->m_reply->errorString());
+
+#ifdef Quotient_E2EE_ENABLED
+ 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->open(ReadOnly);
+ d->m_device = buffer;
+ }
setOpenMode(ReadOnly);
emit finished();
+#else
+ d->m_device = d->m_reply;
+#endif
});
+
+#ifdef Quotient_E2EE_ENABLED
+ auto eventIt = room->findInTimeline(eventId);
+ if(eventIt != room->historyEdge()) {
+ auto event = eventIt->viewAs<RoomMessageEvent>();
+ d->m_encryptedFile = event->content()->fileInfo()->file;
+ }
+#endif
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
@@ -62,7 +94,7 @@ MxcReply::MxcReply()
qint64 MxcReply::readData(char *data, qint64 maxSize)
{
- return d->m_reply->read(data, maxSize);
+ return d->m_device->read(data, maxSize);
}
void MxcReply::abort()
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index 58c3cc3a..f4e7b1af 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -47,6 +47,17 @@ QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
return d->ignoredSslErrors;
}
+void NetworkAccessManager::ignoreSslErrors(bool ignore) const
+{
+ if (ignore) {
+ connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
+ reply->ignoreSslErrors();
+ });
+ } else {
+ disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr);
+ }
+}
+
void NetworkAccessManager::addIgnoredSslError(const QSslError& error)
{
d->ignoredSslErrors << error;
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
index 8ff1c6b5..01b0599d 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -17,6 +17,7 @@ public:
QList<QSslError> ignoredSslErrors() const;
void addIgnoredSslError(const QSslError& error);
void clearIgnoredSslErrors();
+ void ignoreSslErrors(bool ignore = true) const;
/** Get a pointer to the singleton */
static NetworkAccessManager* instance();
diff --git a/lib/room.cpp b/lib/room.cpp
index abd6110c..ea9915c3 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -12,7 +12,6 @@
#include "avatar.h"
#include "connection.h"
#include "converters.h"
-#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
#include "eventstats.h"
@@ -66,13 +65,15 @@
#include <functional>
#ifdef Quotient_E2EE_ENABLED
-#include <account.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <groupsession.h> // QtOlm
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolminboundsession.h"
+#include "database.h"
#endif // Quotient_E2EE_ENABLED
+
using namespace Quotient;
-using namespace QtOlm;
using namespace std::placeholders;
using std::move;
#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
@@ -138,6 +139,8 @@ public:
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
QPointer<GetMembersByRoomJob> allMembersJob;
+ // Map from megolm sessionId to set of eventIds
+ UnorderedMap<QString, QSet<QString>> undecryptedEvents;
struct FileTransferPrivateInfo {
FileTransferPrivateInfo() = default;
@@ -334,40 +337,27 @@ public:
bool isLocalUser(const User* u) const { return u == q->localUser(); }
#ifdef Quotient_E2EE_ENABLED
- // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
- QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
- groupSessionIndexRecord; // TODO: cache
- // A map from senderKey to a map of sessionId to InboundGroupSession
- // Not using QMultiHash, because we want to quickly return
- // a number of relations for a given event without enumerating them.
- QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO:
- // cache
+ // A map from (senderKey, sessionId) to InboundGroupSession
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> groupSessions;
+
bool addInboundGroupSession(QString senderKey, QString sessionId,
QString sessionKey)
{
- if (groupSessions.contains({ senderKey, sessionId })) {
- qCDebug(E2EE) << "Inbound Megolm session" << sessionId
+ if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) {
+ qCWarning(E2EE) << "Inbound Megolm session" << sessionId
<< "with senderKey" << senderKey << "already exists";
return false;
}
- InboundGroupSession* megolmSession;
- try {
- megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
- InboundGroupSession::Init,
- q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to create new InboundGroupSession"
- << e->what();
+ auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1());
+ if (megolmSession->sessionId() != sessionId) {
+ qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent "
+ "from sender with key" << senderKey;
return false;
}
- if (megolmSession->id() != sessionId) {
- qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent "
- "from sender with key"
- << senderKey;
- return false;
- }
- groupSessions.insert({ senderKey, sessionId }, megolmSession);
+ qCWarning(E2EE) << "Adding inbound session";
+ connection->saveMegolmSession(q, senderKey, megolmSession.get());
+ groupSessions[{senderKey, sessionId}] = std::move(megolmSession);
return true;
}
@@ -377,44 +367,31 @@ public:
const QString& eventId,
QDateTime timestamp)
{
- std::pair<QString, uint32_t> decrypted;
- QPair<QString, QString> senderSessionPairKey =
- qMakePair(senderKey, sessionId);
- if (!groupSessions.contains(senderSessionPairKey)) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "The sender's device has not sent us the keys for "
- "this message";
+ auto groupSessionIt = groupSessions.find({ senderKey, sessionId });
+ if (groupSessionIt == groupSessions.end()) {
+ // qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ // << "The sender's device has not sent us the keys for "
+ // "this message";
return QString();
}
- InboundGroupSession* senderSession =
- groupSessions.value(senderSessionPairKey);
- if (!senderSession) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "senderSessionPairKey:" << senderSessionPairKey;
+ auto& senderSession = groupSessionIt->second;
+ auto decryptResult = senderSession->decrypt(cipher);
+ if(std::holds_alternative<QOlmError>(decryptResult)) {
+ qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ << "with matching megolm session:" << std::get<QOlmError>(decryptResult);
return QString();
}
- try {
- decrypted = senderSession->decrypt(cipher);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "with matching megolm session:" << e->what();
- return QString();
- }
- QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(
- qMakePair(senderSession->id(), decrypted.second));
- if (properties.first.isEmpty()) {
- groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
- decrypted.second),
- qMakePair(eventId, timestamp));
+ const auto& [content, index] = std::get<std::pair<QString, uint32_t>>(decryptResult);
+ const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index);
+ if (recordEventId.isEmpty()) {
+ q->connection()->database()->addGroupSessionIndexRecord(q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch());
} else {
- if ((properties.first != eventId)
- || (properties.second != timestamp)) {
- qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
+ if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) {
+ qCWarning(E2EE) << "Detected a replay attack on event" << eventId;
return QString();
}
}
-
- return decrypted.first;
+ return content;
}
#endif // Quotient_E2EE_ENABLED
@@ -440,6 +417,21 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
+#ifdef Quotient_E2EE_ENABLED
+ connectSingleShot(this, &Room::encryption, this, [this, connection](){
+ connection->encryptionUpdate(this);
+ });
+ connect(this, &Room::userAdded, this, [this, connection](){
+ if(usesEncryption()) {
+ connection->encryptionUpdate(this);
+ }
+ });
+ d->groupSessions = connection->loadRoomMegolmSessions(this);
+
+ connect(this, &Room::beforeDestruction, this, [=](){
+ connection->database()->clearRoomData(id);
+ });
+#endif
qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
@@ -1482,20 +1474,20 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
#else // Quotient_E2EE_ENABLED
- if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
- QString decrypted = d->groupSessionDecryptMessage(
- encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
- encryptedEvent.sessionId(), encryptedEvent.id(),
- encryptedEvent.originTimestamp());
- if (decrypted.isEmpty()) {
- return {};
- }
- return makeEvent<RoomMessageEvent>(
- QJsonDocument::fromJson(decrypted.toUtf8()).object());
+ if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
+ qWarning(E2EE) << "Algorithm of the encrypted event with id"
+ << encryptedEvent.id() << "is not decryptable by the current device";
+ return {};
}
- qCDebug(E2EE) << "Algorithm of the encrypted event with id"
- << encryptedEvent.id() << "is not for the current device";
- return {};
+ QString decrypted = d->groupSessionDecryptMessage(
+ encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
+ encryptedEvent.sessionId(), encryptedEvent.id(),
+ encryptedEvent.originTimestamp());
+ if (decrypted.isEmpty()) {
+ // qCWarning(E2EE) << "Encrypted message is empty";
+ return {};
+ }
+ return encryptedEvent.createDecrypted(decrypted);
#endif // Quotient_E2EE_ENABLED
}
@@ -1513,8 +1505,25 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
}
if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(),
roomKeyEvent.sessionKey())) {
- qCDebug(E2EE) << "added new inboundGroupSession:"
- << d->groupSessions.count();
+ qCWarning(E2EE) << "added new inboundGroupSession:"
+ << d->groupSessions.size();
+ for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) {
+ const auto pIdx = d->eventsIndex.constFind(eventId);
+ if (pIdx == d->eventsIndex.cend())
+ continue;
+ auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())];
+ if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) {
+ auto decrypted = decryptMessage(*encryptedEvent);
+ if(decrypted) {
+ // The reference will survive the pointer being moved
+ auto& decryptedEvent = *decrypted;
+ auto oldEvent = ti.replaceEvent(std::move(decrypted));
+ decryptedEvent.setOriginalEvent(std::move(oldEvent));
+ emit replacedEvent(ti.event(), decrypted->originalEvent());
+ d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId;
+ }
+ }
+ }
}
#endif // Quotient_E2EE_ENABLED
}
@@ -1905,7 +1914,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
return;
}
it->setDeparted();
- qCDebug(EVENTS) << "Event txn" << txnId << "has departed";
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
Room::connect(call, &BaseJob::failure, q,
@@ -2343,7 +2351,17 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
filePath = QDir::tempPath() % '/' % filePath;
qDebug(MAIN) << "File path:" << filePath;
}
- auto job = connection()->downloadFile(fileUrl, filePath);
+ DownloadFileJob *job = nullptr;
+#ifdef Quotient_E2EE_ENABLED
+ if(fileInfo->file.has_value()) {
+ auto file = *fileInfo->file;
+ job = connection()->downloadFile(fileUrl, file, filePath);
+ } else {
+#endif
+ job = connection()->downloadFile(fileUrl, filePath);
+#ifdef Quotient_E2EE_ENABLED
+ }
+#endif
if (isJobPending(job)) {
// If there was a previous transfer (completed or failed), overwrite it.
d->fileTransfers[eventId] = { job, job->targetFileName() };
@@ -2595,6 +2613,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
QElapsedTimer et;
et.start();
+
+#ifdef Quotient_E2EE_ENABLED
+ for(long unsigned int i = 0; i < events.size(); i++) {
+ if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
+ auto decrypted = q->decryptMessage(*encrypted);
+ if(decrypted) {
+ auto oldEvent = std::exchange(events[i], std::move(decrypted));
+ events[i]->setOriginalEvent(std::move(oldEvent));
+ } else {
+ undecryptedEvents[encrypted->sessionId()] += encrypted->id();
+ }
+ }
+ }
+#endif
+
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2747,6 +2780,21 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
return;
Changes changes {};
+
+#ifdef Quotient_E2EE_ENABLED
+ for(long unsigned int i = 0; i < events.size(); i++) {
+ if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
+ auto decrypted = q->decryptMessage(*encrypted);
+ if(decrypted) {
+ auto oldEvent = std::exchange(events[i], std::move(decrypted));
+ events[i]->setOriginalEvent(std::move(oldEvent));
+ } else {
+ undecryptedEvents[encrypted->sessionId()] += encrypted->id();
+ }
+ }
+ }
+#endif
+
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 2491d89d..510d253c 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -138,18 +138,12 @@ void AccountSettings::clearAccessToken()
QByteArray AccountSettings::encryptionAccountPickle()
{
- QString passphrase = ""; // FIXME: add QtKeychain
return value("encryption_account_pickle", "").toByteArray();
}
void AccountSettings::setEncryptionAccountPickle(
const QByteArray& encryptionAccountPickle)
{
- qCWarning(MAIN)
- << "Saving encryption_account_pickle to QSettings is insecure."
- " Developers, do it manually or contribute to share QtKeychain "
- "logic to libQuotient.";
- QString passphrase = ""; // FIXME: add QtKeychain
setValue("encryption_account_pickle", encryptionAccountPickle);
}
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index b0cd8e4d..78957cbe 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -99,6 +99,34 @@ SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState,
fromJson(unreadJson.value(HighlightCountKey), highlightCount);
}
+QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList)
+{
+ QDebugStateSaver _(dbg);
+ QStringList sl;
+ if (!devicesList.changed.isEmpty())
+ sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", "));
+ if (!devicesList.left.isEmpty())
+ sl << QStringLiteral("left %1").arg(devicesList.left.join(", "));
+ dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
+ return dbg;
+}
+
+void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo,
+ const DevicesList& rs)
+{
+ addParam<IfNotEmpty>(jo, QStringLiteral("changed"),
+ rs.changed);
+ addParam<IfNotEmpty>(jo, QStringLiteral("left"),
+ rs.left);
+}
+
+void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo,
+ DevicesList& rs)
+{
+ fromJson(jo["changed"_ls], rs.changed);
+ fromJson(jo["left"_ls], rs.left);
+}
+
SyncData::SyncData(const QString& cacheFileName)
{
QFileInfo cacheFileInfo { cacheFileName };
@@ -133,6 +161,8 @@ std::pair<int, int> SyncData::cacheVersion()
return { MajorCacheVersion, 2 };
}
+DevicesList&& SyncData::takeDevicesList() { return std::move(devicesList); }
+
QJsonObject SyncData::loadJson(const QString& fileName)
{
QFile roomFile { fileName };
@@ -175,6 +205,10 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
fromJson(json.value("device_one_time_keys_count"_ls),
deviceOneTimeKeysCount_);
+ if(json.contains("device_lists")) {
+ fromJson(json.value("device_lists"), devicesList);
+ }
+
auto rooms = json.value("rooms"_ls).toObject();
auto totalRooms = 0;
auto totalEvents = 0;
diff --git a/lib/syncdata.h b/lib/syncdata.h
index e29540c2..633f4b52 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -41,6 +41,27 @@ struct JsonObjectConverter<RoomSummary> {
static void fillFrom(const QJsonObject& jo, RoomSummary& rs);
};
+/// Information on e2e device updates. Note: only present on an
+/// incremental sync.
+struct DevicesList {
+ /// List of users who have updated their device identity keys, or who
+ /// now share an encrypted room with the client since the previous
+ /// sync response.
+ QStringList changed;
+
+ /// List of users with whom we do not share any encrypted rooms
+ /// anymore since the previous sync response.
+ QStringList left;
+};
+
+QDebug operator<<(QDebug dhg, const DevicesList &devicesList);
+
+template <>
+struct JsonObjectConverter<DevicesList> {
+ static void dumpTo(QJsonObject &jo, const DevicesList &dev);
+ static void fillFrom(const QJsonObject& jo, DevicesList& rs);
+};
+
class SyncRoomData {
public:
QString roomId;
@@ -85,6 +106,7 @@ public:
return deviceOneTimeKeysCount_;
}
SyncDataList&& takeRoomData();
+ DevicesList&& takeDevicesList();
QString nextBatch() const { return nextBatch_; }
@@ -102,6 +124,7 @@ private:
SyncDataList roomData;
QStringList unresolvedRoomIds;
QHash<QString, int> deviceOneTimeKeysCount_;
+ DevicesList devicesList;
static QJsonObject loadJson(const QString& fileName);
};