diff options
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 }} @@ -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 ¬ification) +{ + 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 ¬ification); +#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); }; |