From 8c3395145dbaa20a5aba8e0081c80b183c5d0ff1 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 22 Sep 2024 17:08:03 +0200 Subject: [PATCH] Port to matrix-rust-sdk-crypto --- .github/workflows/ci.yml | 51 +- .gitignore | 3 + CMakeLists.txt | 60 +- Quotient/connection.cpp | 734 ++++-- Quotient/connection.h | 80 +- Quotient/connection_p.h | 47 +- Quotient/connectionencryptiondata_p.cpp | 1025 -------- Quotient/connectionencryptiondata_p.h | 128 - Quotient/crypto-sdk/Cargo.lock | 2551 ++++++++++++++++++++ Quotient/crypto-sdk/Cargo.toml | 41 + Quotient/crypto-sdk/build.rs | 8 + Quotient/crypto-sdk/src/cryptomachine.rs | 1109 +++++++++ Quotient/crypto-sdk/src/encryption_info.rs | 11 + Quotient/crypto-sdk/src/file_crypto.rs | 71 + Quotient/crypto-sdk/src/lib.rs | 300 +++ Quotient/crypto-sdk/src/request.rs | 318 +++ Quotient/crypto-sdk/src/verification.rs | 46 + Quotient/database.cpp | 667 ----- Quotient/database.h | 102 - Quotient/e2ee/cryptoutils.cpp | 338 --- Quotient/e2ee/cryptoutils.h | 121 - Quotient/e2ee/e2ee_common.h | 6 +- Quotient/e2ee/qolmaccount.cpp | 302 --- Quotient/e2ee/qolmaccount.h | 127 - Quotient/e2ee/qolminboundsession.cpp | 169 -- Quotient/e2ee/qolminboundsession.h | 61 - Quotient/e2ee/qolmmessage.cpp | 31 - Quotient/e2ee/qolmmessage.h | 42 - Quotient/e2ee/qolmoutboundsession.cpp | 125 - Quotient/e2ee/qolmoutboundsession.h | 60 - Quotient/e2ee/qolmsession.cpp | 146 -- Quotient/e2ee/qolmsession.h | 63 - Quotient/e2ee/qolmutility.cpp | 49 - Quotient/e2ee/qolmutility.h | 42 - Quotient/e2ee/sssshandler.cpp | 294 --- Quotient/e2ee/sssshandler.h | 62 - Quotient/events/filesourceinfo.cpp | 67 +- Quotient/keyimport.cpp | 145 +- Quotient/keyverificationsession.cpp | 735 +----- Quotient/keyverificationsession.h | 203 +- Quotient/logging_categories_p.h | 1 - Quotient/room.cpp | 451 ++-- Quotient/room.h | 7 +- autotests/CMakeLists.txt | 11 - autotests/cross_signing_data.json | 328 --- autotests/key-export.data | 15 - autotests/testcrosssigning.cpp | 52 - autotests/testcryptoutils.cpp | 112 - autotests/testgroupsession.cpp | 59 - autotests/testgroupsession.h | 14 - autotests/testkeyimport.cpp | 64 - autotests/testkeyverification.cpp | 64 - autotests/testolmaccount.h | 32 - autotests/testolmsession.cpp | 85 - autotests/testolmsession.h | 14 - autotests/testolmutility.cpp | 118 - autotests/testolmutility.h | 15 - quotest/quotest.cpp | 28 +- 58 files changed, 5520 insertions(+), 6490 deletions(-) delete mode 100644 Quotient/connectionencryptiondata_p.cpp delete mode 100644 Quotient/connectionencryptiondata_p.h create mode 100644 Quotient/crypto-sdk/Cargo.lock create mode 100644 Quotient/crypto-sdk/Cargo.toml create mode 100644 Quotient/crypto-sdk/build.rs create mode 100644 Quotient/crypto-sdk/src/cryptomachine.rs create mode 100644 Quotient/crypto-sdk/src/encryption_info.rs create mode 100644 Quotient/crypto-sdk/src/file_crypto.rs create mode 100644 Quotient/crypto-sdk/src/lib.rs create mode 100644 Quotient/crypto-sdk/src/request.rs create mode 100644 Quotient/crypto-sdk/src/verification.rs delete mode 100644 Quotient/database.cpp delete mode 100644 Quotient/database.h delete mode 100644 Quotient/e2ee/cryptoutils.cpp delete mode 100644 Quotient/e2ee/cryptoutils.h delete mode 100644 Quotient/e2ee/qolmaccount.cpp delete mode 100644 Quotient/e2ee/qolmaccount.h delete mode 100644 Quotient/e2ee/qolminboundsession.cpp delete mode 100644 Quotient/e2ee/qolminboundsession.h delete mode 100644 Quotient/e2ee/qolmmessage.cpp delete mode 100644 Quotient/e2ee/qolmmessage.h delete mode 100644 Quotient/e2ee/qolmoutboundsession.cpp delete mode 100644 Quotient/e2ee/qolmoutboundsession.h delete mode 100644 Quotient/e2ee/qolmsession.cpp delete mode 100644 Quotient/e2ee/qolmsession.h delete mode 100644 Quotient/e2ee/qolmutility.cpp delete mode 100644 Quotient/e2ee/qolmutility.h delete mode 100644 Quotient/e2ee/sssshandler.cpp delete mode 100644 Quotient/e2ee/sssshandler.h delete mode 100644 autotests/cross_signing_data.json delete mode 100644 autotests/key-export.data delete mode 100644 autotests/testcrosssigning.cpp delete mode 100644 autotests/testcryptoutils.cpp delete mode 100644 autotests/testgroupsession.cpp delete mode 100644 autotests/testgroupsession.h delete mode 100644 autotests/testkeyimport.cpp delete mode 100644 autotests/testkeyverification.cpp delete mode 100644 autotests/testolmaccount.h delete mode 100644 autotests/testolmsession.cpp delete mode 100644 autotests/testolmsession.h delete mode 100644 autotests/testolmutility.cpp delete mode 100644 autotests/testolmutility.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d2136d0..e6540b5a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: - name: Install dependencies (Linux) if: startsWith(matrix.os, 'ubuntu') run: | - COMMON_PKGS="libolm-dev ninja-build gnome-keyring g++$GCC_VERSION clang$CLANG_VERSION" + COMMON_PKGS="libolm-dev ninja-build gnome-keyring rustc cargo g++$GCC_VERSION clang$CLANG_VERSION" # See https://github.com/actions/runner-images/issues/9679 sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y # Add LLVM repo for newer Clang @@ -123,6 +123,25 @@ jobs: cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV + - name: Install Rustup using win.rustup.rs + if: startsWith(matrix.os, 'windows') + run: | + # Disable the download progress bar which can cause perf issues + $ProgressPreference = "SilentlyContinue" + Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe + .\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc + del rustup-init.exe + rustup target add x86_64-pc-windows-msvc + rustup --version + shell: powershell + + - name: Install Rustup + if: startsWith(matrix.os, 'macos') + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh + sh rustup-init.sh -y --default-toolchain none + rustup target add "x86_64-apple-darwin" + - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 if: startsWith(matrix.os, 'windows') @@ -141,12 +160,12 @@ jobs: cmake -S qtkeychain -B qtkeychain/build -DBUILD_WITH_QT6=ON $CMAKE_ARGS cmake --build qtkeychain/build --target install - - name: Build and install Olm + - name: Build and install Corrosion run: | cd .. - git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B olm/build $CMAKE_ARGS - cmake --build olm/build --target install + git clone https://github.com/corrosion-rs/corrosion + cmake -S corrosion -B corrosion/build $CMAKE_ARGS + cmake --build corrosion/build --target install - name: Get CS API definitions; clone and build GTAD if: matrix.update-api @@ -172,20 +191,20 @@ jobs: - name: Configure libQuotient run: | - cmake -S $GITHUB_WORKSPACE -B $BUILD_PATH $CMAKE_ARGS -DQuotient_INSTALL_TESTS=ON + cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_INSTALL_TESTS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=1 - name: Regenerate API code if: matrix.update-api - run: cmake --build ../build/libQuotient --target update-api + run: cmake --build build --target update-api - name: Build and install libQuotient + shell: pwsh run: | - if [[ '${{ matrix.static-analysis }}' == 'sonar' ]]; then - BUILD_WRAPPER="${{ steps.sonar.outputs.build-wrapper-binary }} --out-dir $BUILD_PATH/sonar" - fi - $BUILD_WRAPPER cmake --build $BUILD_PATH --target all - cmake --build $BUILD_PATH --target install - ls ~/.local$BIN_DIR/quotest + # if [[ '${{ matrix.static-analysis }}' == 'sonar' ]]; then + # BUILD_WRAPPER="${{ steps.sonar.outputs.build-wrapper-binary }} --out-dir build/sonar" + # fi + cmake --build build --target all + cmake --build build --target install - name: Run tests env: @@ -195,7 +214,7 @@ jobs: QT_LOGGING_RULES: 'quotient.*.debug=true;quotient.jobs.sync.debug=false;quotient.events.ephemeral.debug=false;quotient.events.state.debug=false;quotient.events.members.debug=false' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | - CTEST_ARGS="--test-dir $BUILD_PATH --output-on-failure" + CTEST_ARGS="--test-dir build --output-on-failure" if [[ '${{ runner.os }}' != 'Linux' ]]; then CTEST_ARGS="$CTEST_ARGS -E testolmaccount" else @@ -219,7 +238,7 @@ jobs: SONAR_SERVER_URL: 'https://sonarcloud.io' run: | mkdir .coverage && pushd .coverage - find $BUILD_PATH -name '*.gcda' -print0 \ + find ../build -name '*.gcda' -print0 \ | xargs -0 gcov$GCC_VERSION -s $GITHUB_WORKSPACE -pr # Coverage of the test source code is not tracked, as it is always 100% # (if not, some tests failed and broke the build at an earlier stage) @@ -227,6 +246,6 @@ jobs: popd ${{ steps.sonar.outputs.sonar-scanner-binary }} \ -Dsonar.host.url="$SONAR_SERVER_URL" \ - -Dsonar.cfamily.compile-commands="$BUILD_PATH/sonar/compile_commands.json" \ + -Dsonar.cfamily.compile-commands="build/compile_commands.json" \ -Dsonar.cfamily.threads=2 \ -Dsonar.cfamily.gcov.reportsPath=.coverage diff --git a/.gitignore b/.gitignore index f86acb26d..20b7118f9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ compile_commands.json # Created by doxygen html/ latex/ + +Quotient/crypto-sdk/target +.kateproject.build diff --git a/CMakeLists.txt b/CMakeLists.txt index 47482a79f..4c0adadf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,20 +95,14 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test Sql) get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) find_package(${Qt}Keychain REQUIRED) +find_package(Corrosion REQUIRED) -find_package(Olm 3.2.5 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(NOT WIN32) + find_package(PkgConfig REQUIRED) + pkg_check_modules(SQLITE sqlite3 REQUIRED IMPORTED_TARGET) +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 -) +corrosion_import_crate(MANIFEST_PATH Quotient/crypto-sdk/Cargo.toml) add_library(${QUOTIENT_LIB_NAME}) @@ -174,19 +168,8 @@ target_sources(${QUOTIENT_LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS . Quotient/jobs/syncjob.h Quotient/jobs/mediathumbnailjob.h Quotient/jobs/downloadfilejob.h - Quotient/database.h - Quotient/connectionencryptiondata_p.h Quotient/keyverificationsession.h Quotient/e2ee/e2ee_common.h - Quotient/e2ee/qolmaccount.h - Quotient/e2ee/qolmsession.h - Quotient/e2ee/qolminboundsession.h - Quotient/e2ee/qolmoutboundsession.h - Quotient/e2ee/qolmutility.h - Quotient/e2ee/qolmsession.h - Quotient/e2ee/qolmmessage.h - Quotient/e2ee/cryptoutils.h - Quotient/e2ee/sssshandler.h Quotient/events/keyverificationevent.h Quotient/keyimport.h Quotient/qt_connection_util.h @@ -233,19 +216,8 @@ target_sources(${QUOTIENT_LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS . Quotient/jobs/syncjob.cpp Quotient/jobs/mediathumbnailjob.cpp Quotient/jobs/downloadfilejob.cpp - Quotient/database.cpp - Quotient/connectionencryptiondata_p.cpp Quotient/keyverificationsession.cpp Quotient/e2ee/e2ee_common.cpp - Quotient/e2ee/qolmaccount.cpp - Quotient/e2ee/qolmsession.cpp - Quotient/e2ee/qolminboundsession.cpp - Quotient/e2ee/qolmoutboundsession.cpp - Quotient/e2ee/qolmutility.cpp - Quotient/e2ee/qolmsession.cpp - Quotient/e2ee/qolmmessage.cpp - Quotient/e2ee/cryptoutils.cpp - Quotient/e2ee/sssshandler.cpp Quotient/keyimport.cpp libquotientemojis.qrc ) @@ -362,9 +334,22 @@ target_include_directories(${QUOTIENT_LIB_NAME} PUBLIC $>:${CMAKE_CURRENT_SOURCE_DIR}/Quotient>> ) +find_package(OpenSSL REQUIRED) + target_link_libraries(${QUOTIENT_LIB_NAME} - PUBLIC ${Qt}::Core ${Qt}::Network ${Qt}::Gui qt${${Qt}Core_VERSION_MAJOR}keychain Olm::Olm ${Qt}::Sql - PRIVATE OpenSSL::Crypto ${Qt}::CorePrivate) + PUBLIC ${Qt}::Core ${Qt}::Network ${Qt}::Gui qt${${Qt}Core_VERSION_MAJOR}keychain ${Qt}::Sql + PRIVATE ${Qt}::CorePrivate matrix_rust_sdk_crypto_cpp OpenSSL::Crypto) + +if(NOT WIN32) + target_link_libraries(${QUOTIENT_LIB_NAME} PRIVATE PkgConfig::SQLITE) +else() + target_link_libraries(${QUOTIENT_LIB_NAME} PRIVATE Bcrypt.lib) +endif() + +target_include_directories(${QUOTIENT_LIB_NAME} PRIVATE + ${CMAKE_BINARY_DIR}/cargo/build/${Rust_CARGO_TARGET}/cxxbridge/matrix-rust-sdk-crypto/src/) + + configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${QUOTIENT_LIB_NAME}.pc @ONLY NEWLINE_STYLE UNIX) @@ -389,6 +374,7 @@ write_basic_package_version_file( COMPATIBILITY SameMajorVersion ) +install(TARGETS matrix_rust_sdk_crypto_cpp EXPORT ${QUOTIENT_LIB_NAME}Targets) export(PACKAGE ${QUOTIENT_LIB_NAME}) export(EXPORT ${QUOTIENT_LIB_NAME}Targets FILE "${CMAKE_CURRENT_BINARY_DIR}/${QUOTIENT_LIB_NAME}/${QUOTIENT_LIB_NAME}Targets.cmake") @@ -432,8 +418,6 @@ message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}") message(STATUS " Header files install prefix: ${CMAKE_INSTALL_PREFIX}/${${PROJECT_NAME}_INSTALL_INCLUDEDIR}") message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") message(STATUS "Using QtKeychain ${${Qt}Keychain_VERSION} at ${${Qt}Keychain_DIR}") -message(STATUS "Using libOlm ${Olm_VERSION} at ${Olm_DIR}") -message(STATUS "Using OpenSSL libcrypto ${OPENSSL_VERSION} at ${OPENSSL_CRYPTO_LIBRARY}") message(STATUS) feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/Quotient/connection.cpp b/Quotient/connection.cpp index 4c2839cf0..eea4d78b4 100644 --- a/Quotient/connection.cpp +++ b/Quotient/connection.cpp @@ -8,8 +8,6 @@ #include "connection_p.h" #include "connectiondata.h" -#include "connectionencryptiondata_p.h" -#include "database.h" #include "logging_categories_p.h" #include "qt_connection_util.h" #include "room.h" @@ -17,6 +15,7 @@ #include "user.h" #include "csapi/account-data.h" +#include "csapi/cross_signing.h" #include "csapi/joining.h" #include "csapi/leaving.h" #include "csapi/logout.h" @@ -25,8 +24,9 @@ #include "csapi/voip.h" #include "csapi/wellknown.h" #include "csapi/whoami.h" - -#include "e2ee/qolminboundsession.h" +#include "csapi/keys.h" +#include +#include "csapi/key_backup.h" #include "events/directchatevent.h" #include "events/encryptionevent.h" @@ -51,6 +51,28 @@ using namespace Quotient; namespace { +static QByteArray bytesFromRust(const rust::String &bytes) { + return {bytes.data(), (int) bytes.size()}; +} + +static QString stringFromRust(const rust::String& string) { + return QString::fromUtf8(bytesFromRust(string)); +} + +static QJsonObject jsonFromRust(const rust::String& string) { + return QJsonDocument::fromJson(bytesFromRust(string)).object(); +} + +static rust::String bytesToRust(const QByteArray& bytes) +{ + return rust::String(bytes.data(), bytes.size()); +} + +static rust::String stringToRust(const QString& string) +{ + return bytesToRust(string.toUtf8()); +} + // This is very much Qt-specific; STL iterators don't have key() and value() template HashT remove_if(HashT& hashMap, @@ -87,6 +109,7 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); + } void Connection::resolveServer(const QString& mxid) @@ -179,6 +202,8 @@ void Connection::assumeIdentity(const QString& mxId, const QString& deviceId, const QString& accessToken) { d->completeSetup(mxId, false, deviceId, accessToken); + d->processOutgoingRequests(); + d->ensureHomeserver(mxId).then([this, mxId] { callApi().onResult([this, mxId](const GetTokenOwnerJob* job) { switch (job->error()) { @@ -303,15 +328,82 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) }); } +inline QFuture runKeychainJob(QKeychain::Job* j, const QString& keychainId) +{ + j->setAutoDelete(true); + j->setKey(keychainId); + auto ft = QtFuture::connect(j, &QKeychain::Job::finished); + j->start(); + return ft; +} + +QFuture Connection::Private::setupPicklingKey() +{ + using namespace QKeychain; + const auto keychainId = q->userId() + "-Pickle"_L1; + qCInfo(MAIN) << "Keychain request: app" << qAppName() << "id" << keychainId; + + return runKeychainJob(new ReadPasswordJob(qAppName()), keychainId) + .then([keychainId, this](const Job* j) -> QFuture { + // The future will hold nullptr if the existing pickling key was found and no write is + // pending; a pointer to the write job if if a new key was made and is being written; + // be cancelled in case of an error. + switch (const auto readJob = static_cast(j); readJob->error()) { + case Error::NoError: { + auto&& data = readJob->binaryData(); + qDebug(E2EE) << "Successfully loaded pickling key from keychain"; + + setupCryptoMachine(data); + return QtFuture::makeReadyFuture(nullptr); + qCritical(E2EE) + << "The pickling key loaded from" << keychainId << "has length" + << data.size() << "but the library expected" << PicklingKey::extent; + return {}; + } + case Error::EntryNotFound: { + auto&& picklingKey = PicklingKey::generate(); + auto writeJob = new WritePasswordJob(qAppName()); + setupCryptoMachine(picklingKey.viewAsByteArray().toBase64()); + writeJob->setBinaryData(picklingKey.viewAsByteArray().toBase64()); + qDebug(E2EE) << "Saving a new pickling key to the keychain"; + return runKeychainJob(writeJob, keychainId); + } + default: + qWarning(E2EE) << "Error loading pickling key - please fix your keychain:" + << readJob->errorString(); + //TODO: We probably want to fail entirely here. + } + return {}; + }) + .unwrap() + .then([](QFuture writeFuture) { + if (const Job* const writeJob = writeFuture.result(); + writeJob && writeJob->error() != Error::NoError) // + { + qCritical(E2EE) << "Could not save pickling key to keychain: " + << writeJob->errorString(); + writeFuture.cancel(); + } + }); +} + +void Connection::Private::setupCryptoMachine(const QByteArray& picklingKey) +{ + auto mxIdForDb = q->userId(); + mxIdForDb.replace(u':', u'_'); + const QString databasePath{ QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % u'/' % mxIdForDb % u'/' % q->deviceId() }; + cryptoMachine = crypto::init(stringToRust(q->userId()), stringToRust(q->deviceId()), stringToRust(databasePath), bytesToRust(picklingKey)); +} + void Connection::Private::completeSetup(const QString& mxId, bool newLogin, const std::optional& deviceId, const std::optional& accessToken) { - data->setIdentity(mxId, deviceId.value_or(u""_s), accessToken.value_or(u""_s).toLatin1()); q->setObjectName(data->userId() % u'/' % data->deviceId()); + data->setIdentity(mxId, deviceId.value_or(u""_s), accessToken.value_or(u""_s).toLatin1()); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() - << "by user" << data->userId() - << "from device" << data->deviceId(); + << "by user" << data->userId() + << "from device" << data->deviceId(); connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState); if (newLogin) { @@ -323,31 +415,32 @@ void Connection::Private::completeSetup(const QString& mxId, bool newLogin, q->loadCapabilities(); q->user()->load(); // Load the local user's profile } + auto doCompleteSetup = [this, mxId, deviceId, accessToken](){ + setupPicklingKey(); - emit q->stateChanged(); // Technically connected to the homeserver but no E2EE yet + emit q->stateChanged(); - if (useEncryption) { - using _impl::ConnectionEncryptionData; - if (!accessToken) { - // Mock connection; initialise bare bones necessary for testing - qInfo(E2EE) << "Using a mock pickling key"; - encryptionData = std::make_unique(q, PicklingKey::generate()); - encryptionData->database.clear(); - encryptionData->olmAccount.setupNewAccount(); - } else - ConnectionEncryptionData::setup(q, encryptionData, newLogin).then([this](bool successful) { - if (!successful || !encryptionData) - useEncryption = false; - - emit q->encryptionChanged(useEncryption); - emit q->stateChanged(); - emit q->ready(); - emit q->connected(); - }); + if (useEncryption) { + emit q->encryptionChanged(useEncryption); + emit q->stateChanged(); + emit q->ready(); + emit q->connected(); + } else { + qCInfo(E2EE) << "End-to-end encryption (E2EE) support is off for" << q->objectName(); + emit q->ready(); + emit q->connected(); + } + }; + if (newLogin) { + auto mxIdForDb = q->userId(); + mxIdForDb.replace(u':', u'_'); + const QString databasePath{ QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % u'/' % mxIdForDb % u'/' % q->deviceId() }; + QDir(databasePath).removeRecursively(); + runKeychainJob(new QKeychain::DeletePasswordJob(qAppName()), q->userId() % u"-Pickle"_s).then([doCompleteSetup](const auto &) { + doCompleteSetup(); + }); } else { - qCInfo(E2EE) << "End-to-end encryption (E2EE) support is off for" << q->objectName(); - emit q->ready(); - emit q->connected(); + doCompleteSetup(); } } @@ -442,7 +535,33 @@ void Connection::sync(int timeout) callApi(BackgroundRequest, d->data->lastEvent(), filter, timeout); connect(job, &SyncJob::success, this, [this, job] { + auto syncChanges = (*d->cryptoMachine)->receive_sync_changes(bytesToRust(job->rawData())); + + for (const auto& key : syncChanges->keys()) { + if (const auto& room = this->room(stringFromRust(key.room_id()))) { + room->newSession(stringFromRust(key.session_id())); + } + } + + for (auto &session : syncChanges->sessions()) { + auto keyVerificationSession = new KeyVerificationSession(stringFromRust(session.remote_user_id()), stringFromRust(session.verification_id()), stringFromRust(session.remote_device_id()), this); + emit newKeyVerificationSession(keyVerificationSession); + } onSyncSuccess(job->takeData()); + + if ((*d->cryptoMachine)->has_pending_backup_key()) { + d->initializeExistingBackup(); + } + + const auto &request = (*d->cryptoMachine)->backup_keys(); + if (request->has_request()) { + //TODO make sure that this is only run once simultaneously + auto transaction_id = request->transaction_id(); + callApi(stringFromRust(request->version()), fromJson>(jsonFromRust(request->rooms()))).onResult([this, transaction_id](const auto& job){ + (*d->cryptoMachine)->mark_keys_backup_as_sent(bytesToRust(job->rawData()), transaction_id); + }); + } + d->syncJob = nullptr; d->lastSyncSuccessful = true; emit isOnlineChanged(); @@ -512,19 +631,85 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { - if (d->encryptionData) { - d->encryptionData->onSyncSuccess(data); - } - d->consumeToDeviceEvents(data.takeToDeviceEvents()); d->data->setLastEvent(data.nextBatch()); d->consumeRoomData(data.takeRoomData(), fromCache); d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); - if(d->encryptionData && d->encryptionData->encryptionUpdateRequired) { - d->encryptionData->loadOutdatedUserDevices(); - d->encryptionData->encryptionUpdateRequired = false; - } + Q_UNUSED(std::move(data)) // Tell static analysers `data` is consumed now + + d->processOutgoingRequests(); +} + +void Connection::Private::processOutgoingRequests() +{ + if (!cryptoMachine) { + qWarning() << "Crypto machine not loaded yet"; + return; + } + auto requests = (*cryptoMachine)->outgoing_requests(); + for (const auto &request : requests) { + auto id = stringFromRust(request.id()); + if (requestsInFlight.contains(id)) { + continue; + } + requestsInFlight += id; + if (request.request_type() == 0) { // Keys upload + auto deviceKeys = jsonFromRust(request.keys_upload_device_keys()); + q->callApi(deviceKeys.isEmpty() ? std::nullopt : std::make_optional(fromJson(deviceKeys)), fromJson(jsonFromRust(request.keys_upload_one_time_keys())), fromJson(jsonFromRust(request.keys_upload_fallback_keys()))).then([id, this](const auto& job) { + (*cryptoMachine)->mark_keys_upload_as_sent(bytesToRust(job->rawData()), stringToRust(id)); + requestsInFlight.removeAll(id); + }, [this, id](const auto& job){ + requestsInFlight.removeAll(id); + }); + } else if (request.request_type() == 1) { // Keys query + q->callApi(fromJson>(jsonFromRust(request.keys_query_device_keys())), request.keys_query_timeout() /*TODO check correctness*/).then([id, this](const auto &job){ + (*cryptoMachine)->mark_keys_query_as_sent(bytesToRust(job->rawData()), stringToRust(id)); + requestsInFlight.removeAll(id); + }, [this, id](const auto& job){ + requestsInFlight.removeAll(id); + }); + } else if (request.request_type() == 2) { // keys claim + q->callApi(fromJson>>(jsonFromRust(request.keys_claim_one_time_keys())), std::nullopt /*TODO: timeout */).then([id, this](const auto& job){ + (*cryptoMachine)->mark_keys_claim_as_sent(bytesToRust(job->rawData()), stringToRust(id)); + requestsInFlight.removeAll(id); + }, [this, id](const auto& job){ + requestsInFlight.removeAll(id); + }); + } else if (request.request_type() == 3) { // to device + q->callApi(stringFromRust(request.to_device_event_type()), stringFromRust(request.to_device_txn_id()), fromJson>>(jsonFromRust(request.to_device_messages()))).then([id, this](const auto& job){ + (*cryptoMachine)->mark_to_device_as_sent(bytesToRust(job->rawData()), stringToRust(id)); + requestsInFlight.removeAll(id); + }, [this, id](const auto& job){ + requestsInFlight.removeAll(id); + }); + } else if (request.request_type() == 4) { // signature upload + q->callApi(fromJson>>(jsonFromRust(request.upload_signature_signed_keys()))).then([this, id](const auto& job) { + (*cryptoMachine)->mark_signature_upload_as_sent(bytesToRust(job->rawData()), stringToRust(id)); + requestsInFlight.removeAll(id); + }, [this, id](){ + requestsInFlight.removeAll(id); + }); + } else if (request.request_type() == 5) { // room message + auto room = q->room(stringFromRust(request.room_msg_room_id())); + auto json = jsonFromRust(request.room_msg_content()); + auto transactionId = stringFromRust(request.room_msg_txn_id()); + //TODO: this is wrong (content <-> full), but i also think we don't need it json[u"unsigned"_s] = QJsonObject { {u"transaction_id"_s, transactionId} }; + auto actualTransactionId = room->postJson(stringFromRust(request.room_msg_matrix_type()), json); + connectUntil(room, &Room::messageSent, q, [this, actualTransactionId, id](const auto& txnId, const auto &event) { + if (txnId != actualTransactionId) { + return false; + } + requestsInFlight.removeAll(id); + (*cryptoMachine)->mark_room_message_as_sent(stringToRust(event), stringToRust(id)); + return true; + }); + } else if (request.request_type() == 6) { // keys backup + // Afaict these requests are never actually used by sdk + qWarning() << "keys";// << stringFromRust(request.json()); + //TODO + } + } } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -550,8 +735,18 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, // Update rooms one by one, giving time to update the UI. QMetaObject::invokeMethod( r, - [r, rd = std::move(roomData), fromCache] () mutable { + [r, rd = std::move(roomData), fromCache, this] () mutable { r->updateData(std::move(rd), fromCache); + if (r->usesEncryption()) { + rust::Vec userIds; + const auto ids = r->memberIds(); + for (const auto &id : ids) { + userIds.push_back(stringToRust(id)); + } + if (cryptoMachine) { + (*cryptoMachine)->update_tracked_users(userIds); + } + } }, Qt::QueuedConnection); } @@ -635,18 +830,6 @@ void Connection::Private::consumePresenceData(Events&& presenceData) // To be implemented } -void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) -{ - if (toDeviceEvents.empty()) - return; - - qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; - for (auto&& tdEvt : std::move(toDeviceEvents)) { - if (encryptionData) - encryptionData->consumeToDeviceEvent(std::move(tdEvt)); - } -} - void Connection::stopSync() { // If there's a sync loop, break it @@ -1107,11 +1290,6 @@ bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } bool Connection::isOnline() const { return d->lastSyncSuccessful; } -QOlmAccount* Connection::olmAccount() const -{ - return d->encryptionData ? &d->encryptionData->olmAccount : nullptr; -} - SyncJob* Connection::syncJob() const { return d->syncJob; } int Connection::millisToReconnect() const @@ -1507,11 +1685,6 @@ void Connection::saveState() const rootObj.insert("account_data"_L1, QJsonObject{ { u"events"_s, accountDataEvents } }); } - if (d->encryptionData) { - QJsonObject keysJson = toJson(d->encryptionData->oneTimeKeysCount); - rootObj.insert("device_one_time_keys_count"_L1, keysJson); - } - const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); @@ -1685,51 +1858,6 @@ QVector Connection::availableRoomVersions() co return result; } -bool Connection::isQueryingKeys() const -{ - return d->encryptionData - && d->encryptionData->currentQueryKeysJob != nullptr; -} - -void Connection::encryptionUpdate(const Room* room, const QStringList& invitedIds) -{ - if (d->encryptionData) { - d->encryptionData->encryptionUpdate(room->joinedMemberIds() + invitedIds); - } -} - -QFuture Connection::requestKeyFromDevices(event_type_t name) -{ - QPromise keyPromise; - keyPromise.setProgressRange(0, 10); - keyPromise.start(); - - UsersToDevicesToContent content; - const auto& requestId = generateTxnId(); - const QJsonObject eventContent{ { "action"_L1, "request"_L1 }, - { "name"_L1, name }, - { "request_id"_L1, requestId }, - { "requesting_device_id"_L1, deviceId() } }; - for (const auto& deviceId : devicesForUser(userId())) { - content[userId()][deviceId] = eventContent; - } - sendToDevices("m.secret.request"_L1, content); - auto futureKey = keyPromise.future(); - keyPromise.setProgressValue(5); // Already sent the request, now it's only to get the result - connectUntil(this, &Connection::secretReceived, this, - [this, requestId, name, kp = std::move(keyPromise)]( - const QString& receivedRequestId, const QString& secret) mutable { - if (requestId != receivedRequestId) { - return false; - } - const auto& key = QByteArray::fromBase64(secret.toLatin1()); - database()->storeEncrypted(name, key); - kp.addResult(key); - kp.finish(); - return true; - }); - return futureKey; -} QJsonObject Connection::decryptNotification(const QJsonObject& notification) { @@ -1741,221 +1869,327 @@ QJsonObject Connection::decryptNotification(const QJsonObject& notification) return {}; } -Database* Connection::database() const +void Connection::sendToDevice(const QString& targetUserId, + const QString& targetDeviceId, const Event& event, + bool encrypted) { - return d->encryptionData ? &d->encryptionData->database : nullptr; } -std::unordered_map Connection::loadRoomMegolmSessions(const Room* room) const +Quotient::KeyVerificationSession* Connection::startKeyVerificationSession(const QString& userId, + const QString& deviceId) { - return database()->loadMegolmSessions(room->id()); + auto session = KeyVerificationSession::requestDeviceVerification(userId, deviceId, this); + Q_EMIT newKeyVerificationSession(session); + return session; } -void Connection::saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session, const QByteArray& senderKey, const QByteArray& senderEdKey) const +Connection* Connection::makeMockConnection(const QString& mxId, bool enableEncryption) { - database()->saveMegolmSession(room->id(), session, senderKey, senderEdKey); + auto* c = new Connection; + c->enableEncryption(enableEncryption); + c->d->completeSetup(mxId); + return c; } -QStringList Connection::devicesForUser(const QString& userId) const +QStringList Connection::accountDataEventTypes() const { - return d->encryptionData->deviceKeys.value(userId).keys(); + QStringList events; + events.reserve(d->accountData.size()); + for (const auto& [key, value] : std::as_const(d->accountData)) { + events += key; + } + return events; } -QString Connection::edKeyForUserDevice(const QString& userId, - const QString& deviceId) const +void Connection::startSelfVerification() { - return d->encryptionData->deviceKeys[userId][deviceId] - .keys["ed25519:"_L1 + deviceId]; + auto request = (*d->cryptoMachine)->request_self_verification(); + callApi(stringFromRust(request->to_device_event_type()), stringFromRust(request->to_device_txn_id()), fromJson>>(jsonFromRust(request->to_device_messages()))); + Q_EMIT newKeyVerificationSession(KeyVerificationSession::selfVerification(stringFromRust(request->verification_id()), this)); } -QString Connection::curveKeyForUserDevice( - const QString& userId, const QString& device) const +bool Connection::allSessionsSelfVerified(const QString& userId) const { - return d->encryptionData->curveKeyForUserDevice(userId, device); + return (*d->cryptoMachine)->all_sessions_verified(stringToRust(userId)); } -bool Connection::hasOlmSession(const QString& user, - const QString& deviceId) const +QFuture Connection::shareRoomKey(Room* room) { - return d->encryptionData && d->encryptionData->hasOlmSession(user, deviceId); + QPromise promise; + auto result = promise.future(); + promise.start(); + + rust::Vec ids; + for (const auto& id : room->joinedMemberIds()) { //TODO + invited + ids.push_back(stringToRust(id)); + } + + auto missing = (*d->cryptoMachine)->get_missing_sessions(ids); + auto id = missing->id(); + callApi(fromJson>>(jsonFromRust(missing->one_time_keys()))).then([this, id, room, ids, p = std::move(promise)](const auto& job) mutable { + (*d->cryptoMachine)->mark_keys_claim_as_sent(bytesToRust(job->rawData()), id); + //TODO: Wire up visibility + only_trusted + //TODO: if only_trusted, we do somehow need to send keys for verification sessions? + auto requests = (*d->cryptoMachine)->share_room_key(stringToRust(room->id()), ids, false, 1); + p.finish(); + for (const auto& request : requests) { + auto id = request.txn_id(); + + //TODO type from event + callApi(u"m.room.encrypted"_s, stringFromRust(id), fromJson>>(jsonFromRust(request.messages()))).then([this, id](const auto& job) { + (*d->cryptoMachine)->mark_to_device_as_sent(bytesToRust(job->rawData()), id); + }, [=](const auto& job){}); + } + }, [=](const auto& job){}); + + return result; } -void Connection::sendSessionKeyToDevices( - const QString& roomId, const QOlmOutboundGroupSession& outboundSession, - const QMultiHash& devices) +QString Connection::encryptRoomEvent(Room* room, const QByteArray& content, const QString& type) { - Q_ASSERT(d->encryptionData != nullptr); - d->encryptionData->sendSessionKeyToDevices(roomId, outboundSession, devices); + return stringFromRust((*d->cryptoMachine)->encrypt_room_event(stringToRust(room->id()), bytesToRust(content), stringToRust(type))); } -KeyVerificationSession* Connection::startKeyVerificationSession(const QString& userId, - const QString& deviceId) +QString Connection::decryptRoomEvent(Room* room, const QByteArray& event) { - if (!d->encryptionData) { - qWarning(E2EE) << "E2EE is switched off on" << objectName() - << "- you can't start a verification session on it"; - return nullptr; - } - return d->encryptionData->setupKeyVerificationSession(userId, deviceId, - this); + return stringFromRust((*d->cryptoMachine)->decrypt_room_event(stringToRust(room->id()), bytesToRust(event))); } -void Connection::sendToDevice(const QString& targetUserId, - const QString& targetDeviceId, const Event& event, - bool encrypted) +void Connection::Private::acceptKeyVerification(KeyVerificationSession* session) { - if (encrypted && !d->encryptionData) { - qWarning(E2EE) << "E2EE is off for" << objectName() - << "- no encrypted to-device message will be sent"; - return; + auto outgoing = (*cryptoMachine)->accept_verification(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); + if (!session->room()) { + q->callApi(stringFromRust(outgoing->to_device_event_type()), stringFromRust(outgoing->to_device_txn_id()), fromJson>>(jsonFromRust(outgoing->to_device_messages()))); + } else { + auto json = jsonFromRust(outgoing->in_room_content()); + auto transactionId = stringFromRust(outgoing->in_room_txn_id()); + //TODO: this is wrong (content <-> full), but i also think we don't need it json[u"unsigned"_s] = QJsonObject { {u"transaction_id"_s, transactionId} }; + session->room()->postJson(u"m.key.verification.ready"_s, json); } - - const auto contentJson = - encrypted - ? d->encryptionData->assembleEncryptedContent(event.fullJson(), - targetUserId, - targetDeviceId) - : event.contentJson(); - sendToDevices(encrypted ? EncryptedEvent::TypeId : event.matrixType(), - { { targetUserId, { { targetDeviceId, contentJson } } } }); + session->setState(keyVerificationSessionState(session)); + session->setSasState(sasState(session)); } -bool Connection::isVerifiedSession(const QByteArray& megolmSessionId) const +void Connection::Private::startKeyVerification(KeyVerificationSession* session) { - auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_L1); - query.bindValue(":sessionId"_L1, megolmSessionId); - database()->execute(query); - if (!query.next()) { - return false; - } - const auto olmSessionId = query.value("olmSessionId"_L1).toString(); - if (olmSessionId == "BACKUP_VERIFIED"_L1) { - return true; + auto startSas = (*cryptoMachine)->start_sas(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); + if (!session->room()) { + q->callApi(stringFromRust(startSas->to_device_event_type()), stringFromRust(startSas->to_device_txn_id()), fromJson>>(jsonFromRust(startSas->to_device_messages()))); + } else { + auto json = jsonFromRust(startSas->in_room_content()); + auto transactionId = stringFromRust(startSas->in_room_txn_id()); + //TODO: this is wrong (content <-> full), but i also think we don't need it json[u"unsigned"_s] = QJsonObject { {u"transaction_id"_s, transactionId} }; + session->room()->postJson(u"m.key.verification.start"_s, json); } - if (olmSessionId == "SELF"_L1) { - return true; +} + +void Connection::Private::confirmKeyVerification(KeyVerificationSession* session) +{ + auto requests = (*cryptoMachine)->confirm_verification(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); + for (const auto& request : requests->verification_requests()) { + if (!session->room()) { + const auto& type = stringFromRust(request.to_device_event_type()); + q->callApi(type, stringFromRust(request.to_device_txn_id()), fromJson>>(jsonFromRust(request.to_device_messages()))); + if (type == u"m.key.verification.done"_s) { + session->setState(KeyVerificationSession::DONE); + } + if (type == u"m.key.verification.cancel"_s) { + session->setState(KeyVerificationSession::CANCELLED); + } + } else { + const auto& type = stringFromRust(request.in_room_event_type()); + session->room()->postJson(stringFromRust(request.in_room_event_type()), jsonFromRust(request.in_room_content())); + if (type == u"m.key.verification.done"_s) { + session->setState(KeyVerificationSession::DONE); + } + if (type == u"m.key.verification.cancel"_s) { + session->setState(KeyVerificationSession::CANCELLED); + } + } } - query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_L1); - query.bindValue(":sessionId"_L1, olmSessionId.toLatin1()); - database()->execute(query); - if (!query.next()) { - return false; + if (requests->has_signature_request()) { + q->callApi(fromJson>>(jsonFromRust(requests->signature_request_content()))); } - const auto curveKey = query.value("senderKey"_L1).toString(); + session->setSasState(sasState(session)); +} - query.prepare("SELECT matrixId, selfVerified, verified FROM tracked_devices WHERE curveKey=:curveKey;"_L1); - query.bindValue(":curveKey"_L1, curveKey); - database()->execute(query); - if (!query.next()) { - return false; +void Connection::Private::acceptSas(KeyVerificationSession* session) +{ + const auto& request = (*cryptoMachine)->accept_sas(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); + if (!session->room()) { + q->callApi(stringFromRust(request->to_device_event_type()), stringFromRust(request->to_device_txn_id()), fromJson>>(jsonFromRust(request->to_device_messages()))); + } else { + session->room()->postJson(u"m.key.verification.accept"_s, jsonFromRust(request->in_room_content())); } - const auto userId = query.value("matrixId"_L1).toString(); - return query.value("verified"_L1).toBool() || (isUserVerified(userId) && query.value("selfVerified"_L1).toBool()); + session->setSasState(sasState(session)); } -QString Connection::masterKeyForUser(const QString& userId) const + +KeyVerificationSession::State Connection::Private::keyVerificationSessionState(KeyVerificationSession* session) { - auto query = database()->prepareQuery("SELECT key FROM master_keys WHERE userId=:userId"_L1); - query.bindValue(":userId"_L1, userId); - database()->execute(query); - return query.next() ? query.value("key"_L1).toString() : QString(); + return (KeyVerificationSession::State) (*cryptoMachine)->verification_get_state(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); } -bool Connection::isUserVerified(const QString& userId) const +KeyVerificationSession::SasState Connection::Private::sasState(KeyVerificationSession* session) { - auto query = database()->prepareQuery("SELECT verified FROM master_keys WHERE userId=:userId"_L1); - query.bindValue(":userId"_L1, userId); - database()->execute(query); - return query.next() && query.value("verified"_L1).toBool(); + return (KeyVerificationSession::SasState) (*cryptoMachine)->sas_get_state(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); } -bool Connection::isVerifiedDevice(const QString& userId, const QString& deviceId) const +QList> Connection::Private::keyVerificationSasEmoji(KeyVerificationSession* session) { - auto query = database()->prepareQuery("SELECT verified, selfVerified FROM tracked_devices WHERE deviceId=:deviceId AND matrixId=:matrixId;"_L1); - query.bindValue(":deviceId"_L1, deviceId); - query.bindValue(":matrixId"_L1, userId); - database()->execute(query); - if (!query.next()) { - return false; + auto e = (*cryptoMachine)->sas_emoji(stringToRust(session->remoteUser()), stringToRust(session->verificationId())); + + QList> out; + + for (const auto& emoji : e) { + out += {stringFromRust(emoji.symbol()), stringFromRust(emoji.description())}; } - return query.value("verified"_L1).toBool() || (isUserVerified(userId) && query.value("selfVerified"_L1).toBool()); + return out; } -bool Connection::isKnownE2eeCapableDevice(const QString& userId, const QString& deviceId) const +void Connection::Private::requestDeviceVerification(KeyVerificationSession* session) { - auto query = database()->prepareQuery("SELECT verified FROM tracked_devices WHERE deviceId=:deviceId AND matrixId=:matrixId;"_L1); - query.bindValue(":deviceId"_L1, deviceId); - query.bindValue(":matrixId"_L1, userId); - database()->execute(query); - return query.next(); + auto request = (*cryptoMachine)->request_device_verification(stringToRust(session->remoteUser()), stringToRust(session->remoteDevice())); + q->callApi(stringFromRust(request->to_device_event_type()), stringFromRust(request->to_device_txn_id()), fromJson>>(jsonFromRust(request->to_device_messages()))); + session->setVerificationId(stringFromRust(request->verification_id())); } -bool Connection::hasConflictingDeviceIdsAndCrossSigningKeys(const QString& userId) +bool Connection::isVerifiedEvent(const QString& eventId, Room* room) { - if (d->encryptionData) { - return d->encryptionData->hasConflictingDeviceIdsAndCrossSigningKeys(userId); + if (eventId.isEmpty()) { + return false; } - return true; + + const auto timelineIt = room->findInTimeline(eventId); + if (timelineIt == room->historyEdge()) { + return false; + } + + auto event = timelineIt->get(); + + QJsonObject json; + + if (event->is()) { + json = event->fullJson(); + } else if (const auto& originalEvent = event->originalEvent()) { + json = originalEvent->fullJson(); + } + auto rustJson = bytesToRust(QJsonDocument(json).toJson(QJsonDocument::Compact)); + auto info = (*d->cryptoMachine)->get_room_event_encryption_info(rustJson, stringToRust(room->id())); + return info->is_verified(); } -void Connection::reloadDevices() +Quotient::KeyVerificationSession* Connection::requestUserVerification(Room* room) { - if (d->encryptionData) { - d->encryptionData->reloadDevices(); - } + auto session = KeyVerificationSession::requestUserVerification(room, this); + Q_EMIT newKeyVerificationSession(session); + return session; } -Connection* Connection::makeMockConnection(const QString& mxId, bool enableEncryption) +void Connection::Private::requestUserVerification(KeyVerificationSession* session) { - auto* c = new Connection; - c->enableEncryption(enableEncryption); - c->d->completeSetup(mxId); - return c; + auto request = (*cryptoMachine)->request_user_verification_content(stringToRust(session->remoteUser())); + auto transactionId = session->room()->postJson(u"m.room.message"_s, jsonFromRust(request)); + connectUntil(session->room(), &Room::pendingEventAboutToMerge, q, [this, transactionId, session](const auto &event) { + if (event->transactionId() != transactionId) { + return false; + } + + auto rustSession = (*cryptoMachine)->request_user_verification(stringToRust(session->remoteUser()), stringToRust(session->room()->id()), stringToRust(event->id())); + session->setVerificationId(stringFromRust(rustSession->verification_id())); + return true; + }); } -QStringList Connection::accountDataEventTypes() const +void Connection::receiveVerificationEvent(const QByteArray& fullJson) +{ + (*d->cryptoMachine)->receive_verification_event(bytesToRust(fullJson)); + Q_EMIT verificationEventProcessed(); +} + +void Connection::loadFromBackup(const QString& passphrase) +{ + auto versionJob = callApi(); + connect(versionJob, &BaseJob::finished, this, [this, versionJob, passphrase] { + const auto &version = versionJob->version(); + auto keysJob = callApi(version); + connect(keysJob, &BaseJob::finished, this, [this, keysJob, passphrase, version, versionJob](){ + const auto& defaultKeyEvent = accountData("m.secret_storage.default_key"_L1); + auto defaultKey = defaultKeyEvent->contentPart("key"_L1); + const auto keyName = "m.secret_storage.key."_L1 + defaultKey; + const auto &storageKeyEvent = accountData(keyName); + const auto &backupKeyEvent = accountData(u"m.megolm_backup.v1"_s); + const auto &masterKeyEvent = accountData(u"m.cross_signing.master"_s); + const auto &selfKeyEvent = accountData(u"m.cross_signing.self_signing"_s); + const auto &userKeyEvent = accountData(u"m.cross_signing.user_signing"_s); + + auto request = (*d->cryptoMachine)->load_secrets( + stringToRust(passphrase), + stringToRust(defaultKey), + storageKeyEvent->contentPart(u"passphrase"_s)[u"iterations"_s].toInt(), + stringToRust(storageKeyEvent->contentPart(u"passphrase"_s)[u"salt"_s].toString()), + stringToRust(storageKeyEvent->contentPart(u"iv"_s)), + stringToRust(storageKeyEvent->contentPart(u"mac"_s)), + stringToRust(backupKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"iv"_s].toString()), + stringToRust(backupKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"ciphertext"_s].toString()), + stringToRust(backupKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"mac"_s].toString()), + + stringToRust(masterKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"iv"_s].toString()), + stringToRust(masterKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"ciphertext"_s].toString()), + stringToRust(masterKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"mac"_s].toString()), + + stringToRust(selfKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"iv"_s].toString()), + stringToRust(selfKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"ciphertext"_s].toString()), + stringToRust(selfKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"mac"_s].toString()), + + stringToRust(userKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"iv"_s].toString()), + stringToRust(userKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"ciphertext"_s].toString()), + stringToRust(userKeyEvent->contentPart(u"encrypted"_s)[defaultKey][u"mac"_s].toString()), + + stringToRust(version) + ); + callApi(fromJson>>(jsonFromRust(request))); + (*d->cryptoMachine)->import_from_backup(bytesToRust(keysJob->rawData())); + }); + }); +} + +void Connection::requestSecretsFromDevices() { - QStringList events; - events.reserve(d->accountData.size()); - for (const auto& [key, value] : std::as_const(d->accountData)) { - events += key; + (*d->cryptoMachine)->request_secrets_from_devices(); +} + +void Connection::Private::initializeExistingBackup() +{ + if (isInitializingBackup) { + return; } - return events; + isInitializingBackup = true; + + q->callApi().onResult([this](const auto& job){ + (*cryptoMachine)->initialize_existing_backup(bytesToRust(job->rawData())); + }); } -void Connection::startSelfVerification() +void Connection::importKeys(const QString& passphrase, const QString& data) { - auto query = database()->prepareQuery("SELECT deviceId FROM tracked_devices WHERE matrixId=:matrixId AND selfVerified=1;"_L1); - query.bindValue(":matrixId"_L1, userId()); - database()->execute(query); - QStringList devices; - while(query.next()) { - auto id = query.value("deviceId"_L1).toString(); - if (id != deviceId()) { - devices += id; + auto keys = (*d->cryptoMachine)->import_keys(stringToRust(passphrase), stringToRust(data)); + + for (const auto& key : keys) { + if (const auto& room = this->room(stringFromRust(key.room_id()))) { + room->newSession(stringFromRust(key.session_id())); } } - for (const auto &device : devices) { - auto session = new KeyVerificationSession(userId(), device, this); - d->encryptionData->verificationSessions[session->transactionId()] = session; - connect(session, &QObject::destroyed, this, [this, session] { - d->encryptionData->verificationSessions.remove(session->transactionId()); - }); - connectUntil(this, &Connection::keyVerificationStateChanged, this, [session, this](const auto &changedSession, const auto state){ - if (changedSession->transactionId() == session->transactionId() && state != KeyVerificationSession::CANCELED) { - emit newKeyVerificationSession(session); - return true; - } - return state == KeyVerificationSession::CANCELED; - }); - } } -bool Connection::allSessionsSelfVerified(const QString& userId) const +QString Connection::exportKeys(const QString& passphrase) +{ + return stringFromRust((*d->cryptoMachine)->export_keys(stringToRust(passphrase))); +} + +bool Connection::isUserVerified(const QString& userId) const { - auto query = database()->prepareQuery("SELECT deviceId FROM tracked_devices WHERE matrixId=:matrixId AND selfVerified=0;"_L1); - query.bindValue(":matrixId"_L1, userId); - database()->execute(query); - return !query.next(); + return (*d->cryptoMachine)->is_user_verified(stringToRust(userId)); } + +//TODO limit query key jobs to 1 diff --git a/Quotient/connection.h b/Quotient/connection.h index f515aea63..8b71c2df0 100644 --- a/Quotient/connection.h +++ b/Quotient/connection.h @@ -14,8 +14,6 @@ #include "csapi/login.h" #include "csapi/content-repo.h" -#include "e2ee/qolmoutboundsession.h" - #include "events/accountdataevents.h" #include "jobs/jobhandle.h" @@ -51,12 +49,8 @@ class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; -class Database; struct EncryptedFileMetadata; -class QOlmAccount; -class QOlmInboundGroupSession; - using LoginFlow = GetLoginFlowsJob::LoginFlow; //! Predefined login flows @@ -338,53 +332,36 @@ class QUOTIENT_API Connection : public QObject { //! //! \return true, if the last sync was successful, false otherwise. bool isOnline() const; - QOlmAccount* olmAccount() const; - Database* database() const; - - std::unordered_map loadRoomMegolmSessions( - const Room* room) const; - void saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session, const QByteArray &senderKey, const QByteArray& senderEdKey) const; - - QString edKeyForUserDevice(const QString& userId, - const QString& deviceId) const; - QString curveKeyForUserDevice(const QString& userId, - const QString& device) const; - bool hasOlmSession(const QString& user, const QString& deviceId) const; // This assumes that an olm session already exists. If it doesn't, no message is sent. void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, const Event& event, bool encrypted); - //! Returns true if this megolm session comes from a verified device - bool isVerifiedSession(const QByteArray& megolmSessionId) const; - //! Returns whether the device is verified - bool isVerifiedDevice(const QString& userId, const QString& deviceId) const; + //! \brief Returns whether this event comes from a verified device + bool isVerifiedEvent(const QString& eventId, Room* room); - //! \brief Returns whether the device is known and supports end-to-end encryption. - //! - //! This might give unexpected results for users we're not tracking, - //! i.e., users that we don't share an encrypted room with - bool isKnownE2eeCapableDevice(const QString& userId, const QString& deviceId) const; + // //! Returns whether the device is verified + // bool isVerifiedDevice(const QString& userId, const QString& deviceId) const; + // //! \brief Returns whether the device is known and supports end-to-end encryption. + // //! + // //! This might give unexpected results for users we're not tracking, + // //! i.e., users that we don't share an encrypted room with + // bool isKnownE2eeCapableDevice(const QString& userId, const QString& deviceId) const; + // - void sendSessionKeyToDevices(const QString& roomId, - const QOlmOutboundGroupSession& outboundSession, - const QMultiHash& devices); + //TODO + // void sendSessionKeyToDevices(const QString& roomId, + // const QOlmOutboundGroupSession& outboundSession, + // const QMultiHash& devices); QJsonObject decryptNotification(const QJsonObject ¬ification); QStringList devicesForUser(const QString& userId) const; - Q_INVOKABLE bool isQueryingKeys() const; - QFuture requestKeyFromDevices(event_type_t name); - QString masterKeyForUser(const QString& userId) const; Q_INVOKABLE bool isUserVerified(const QString& userId) const; Q_INVOKABLE bool allSessionsSelfVerified(const QString& userId) const; - bool hasConflictingDeviceIdsAndCrossSigningKeys(const QString& userId); - - void reloadDevices(); Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE QString nextBatchToken() const; @@ -766,12 +743,25 @@ public Q_SLOTS: Quotient::KeyVerificationSession* startKeyVerificationSession(const QString& userId, const QString& deviceId); + Quotient::KeyVerificationSession* requestUserVerification(Room* room); + Q_INVOKABLE void startSelfVerification(); - void encryptionUpdate(const Room* room, const QStringList& invitedIds = {}); static Connection* makeMockConnection(const QString& mxId, bool enableEncryption = true); + QFuture shareRoomKey(Room* room); + QString encryptRoomEvent(Room* room, const QByteArray& content, const QString& type); + QString decryptRoomEvent(Room* room, const QByteArray& event); + + void receiveVerificationEvent(const QByteArray& fullJson); + + Q_INVOKABLE void loadFromBackup(const QString& passphrase); + Q_INVOKABLE void requestSecretsFromDevices(); + + Q_INVOKABLE void importKeys(const QString& passphrase, const QString& data); + Q_INVOKABLE QString exportKeys(const QString& passphrase); + Q_SIGNALS: //! \brief Initial server resolution has failed //! @@ -924,20 +914,15 @@ public Q_SLOTS: void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); - void devicesListLoaded(); //! Encryption has been enabled or disabled void encryptionChanged(bool enabled); void directChatsEncryptionChanged(bool enabled); void newKeyVerificationSession(Quotient::KeyVerificationSession* session); - void keyVerificationStateChanged( - const Quotient::KeyVerificationSession* session, - Quotient::KeyVerificationSession::State state); - void sessionVerified(const QString& userId, const QString& deviceId); - void finishedQueryingKeys(); - void secretReceived(const QString& requestId, const QString& secret); + // TODO: make sure these still work + void sessionVerified(const QString& userId, const QString& deviceId); void userVerified(const QString& userId); //! The account does not yet have cross-signing keys. The client should ask the user @@ -948,7 +933,12 @@ public Q_SLOTS: //! This does not mean that the server was reached, a sync was performed, or the state cache was loaded. void ready(); + //! \brief Emitted after the crypto machine has processed the verification events for a sync. + //! Usually not relevant to clients. + void verificationEventProcessed(); + friend class ::TestCrossSigning; + friend class KeyVerificationSession; protected: //! Access the underlying ConnectionData class const ConnectionData* connectionData() const; diff --git a/Quotient/connection_p.h b/Quotient/connection_p.h index 126775bc0..29e227ac1 100644 --- a/Quotient/connection_p.h +++ b/Quotient/connection_p.h @@ -7,9 +7,9 @@ #include "avatar.h" #include "connection.h" #include "connectiondata.h" -#include "connectionencryptiondata_p.h" #include "settings.h" #include "syncdata.h" +#include "e2ee/e2ee_common.h" #include "csapi/account-data.h" #include "csapi/capabilities.h" @@ -19,6 +19,8 @@ #include +#include + namespace Quotient { class Q_DECL_HIDDEN Quotient::Connection::Private { @@ -51,17 +53,20 @@ class Q_DECL_HIDDEN Quotient::Connection::Private { std::unordered_map accountData; QMetaObject::Connection syncLoopConnection {}; int syncTimeout = -1; + std::optional> cryptoMachine; + QStringList requestsInFlight; GetVersionsJob::Response apiVersions{}; GetCapabilitiesJob::Capabilities capabilities{}; + bool isInitializingBackup = false; + QVector loginFlows; static inline bool encryptionDefault = false; bool useEncryption = encryptionDefault; static inline bool directChatEncryptionDefault = false; bool encryptDirectChats = directChatEncryptionDefault; - std::unique_ptr<_impl::ConnectionEncryptionData> encryptionData; JobHandle resolverJob = nullptr; JobHandle loginFlowsJob = nullptr; @@ -126,5 +131,43 @@ class Q_DECL_HIDDEN Quotient::Connection::Private { void saveAccessTokenToKeychain() const; void dropAccessToken(); + void processOutgoingRequests(); + + //! \brief Send an m.key.verification.accept event to the session + //! \param session The session to send the event to + void acceptKeyVerification(KeyVerificationSession* session); + + //! \brief Send an m.key.verification.start event to the session + //! This does not start a new verification session + //! \param session The session to send the event to + void startKeyVerification(KeyVerificationSession* session); + + //! \brief Send an m.key.verification.mac event to the session + //! This is sent after the user confirms that the sas emoji match + //! \param session The session to send the event to + void confirmKeyVerification(KeyVerificationSession* session); + + void acceptSas(KeyVerificationSession* session); + + //! \brief Query the state of a key verification session + //! \param session The session to query the state for + KeyVerificationSession::State keyVerificationSessionState(KeyVerificationSession* session); + + //! \brief Query the state of a sas verification + //! \param session The session to query the state for + KeyVerificationSession::SasState sasState(KeyVerificationSession* session); + + //! \brief Query the sas emoji of a session + //! If the session is not in a state to show emoji, the return value + //! \param session The session to query the emoji for + QList> keyVerificationSasEmoji(KeyVerificationSession* session); + + void requestDeviceVerification(KeyVerificationSession* session); + void requestUserVerification(KeyVerificationSession* session); + + void initializeExistingBackup(); + + QFuture setupPicklingKey(); + void setupCryptoMachine(const QByteArray& picklingKey); }; } // namespace Quotient diff --git a/Quotient/connectionencryptiondata_p.cpp b/Quotient/connectionencryptiondata_p.cpp deleted file mode 100644 index 4e362eb40..000000000 --- a/Quotient/connectionencryptiondata_p.cpp +++ /dev/null @@ -1,1025 +0,0 @@ -#include "connectionencryptiondata_p.h" - -#include "logging_categories_p.h" -#include "qt_connection_util.h" -#include "room.h" -#include "syncdata.h" -#include "user.h" - -#include "e2ee/qolmutility.h" - -#include "events/encryptedevent.h" -#include "events/roomkeyevent.h" - -#include - -#include -#include - -using namespace Quotient; -using namespace Quotient::_impl; - -// Below, encryptionData gets filled inside setupPicklingKey() instead of returning the future for -// a pickling key and then, in CED::setup(), another future for ConnectionEncryptionData because -// Qt versions before 6.5.2 don't handle QFutures with move-only data quite well (see QTBUG-112513). -// Oh, and unwrap() doesn't work with move-only types at all (QTBUG-127423). So it is a bit more -// verbose and repetitive than it should be. - -inline QFuture runKeychainJob(QKeychain::Job* j, const QString& keychainId) -{ - j->setAutoDelete(true); - j->setKey(keychainId); - auto ft = QtFuture::connect(j, &QKeychain::Job::finished); - j->start(); - return ft; -} - -QFuture setupPicklingKey(Connection* connection, - std::unique_ptr& encryptionData) -{ - using namespace QKeychain; - const auto keychainId = connection->userId() + "-Pickle"_L1; - qCInfo(MAIN) << "Keychain request: app" << qAppName() << "id" << keychainId; - - return runKeychainJob(new ReadPasswordJob(qAppName()), keychainId) - .then([keychainId, &encryptionData, connection](const Job* j) -> QFuture { - // The future will hold nullptr if the existing pickling key was found and no write is - // pending; a pointer to the write job if if a new key was made and is being written; - // be cancelled in case of an error. - switch (const auto readJob = static_cast(j); readJob->error()) { - case Error::NoError: { - auto&& data = readJob->binaryData(); - if (data.size() == PicklingKey::extent) { - qDebug(E2EE) << "Successfully loaded pickling key from keychain"; - encryptionData = std::make_unique( - connection, PicklingKey::fromByteArray(std::move(data))); - return QtFuture::makeReadyFuture(nullptr); - } - qCritical(E2EE) - << "The pickling key loaded from" << keychainId << "has length" - << data.size() << "but the library expected" << PicklingKey::extent; - return {}; - } - case Error::EntryNotFound: { - auto&& picklingKey = PicklingKey::generate(); - auto writeJob = new WritePasswordJob(qAppName()); - writeJob->setBinaryData(picklingKey.viewAsByteArray()); - encryptionData = std::make_unique( - connection, std::move(picklingKey)); // the future may still get cancelled - qDebug(E2EE) << "Saving a new pickling key to the keychain"; - return runKeychainJob(writeJob, keychainId); - } - default: - qWarning(E2EE) << "Error loading pickling key - please fix your keychain:" - << readJob->errorString(); - } - return {}; - }) - .unwrap() - .then([](QFuture writeFuture) { - if (const Job* const writeJob = writeFuture.result(); - writeJob && writeJob->error() != Error::NoError) // - { - qCritical(E2EE) << "Could not save pickling key to keychain: " - << writeJob->errorString(); - writeFuture.cancel(); - } - }); -} - -QFuture ConnectionEncryptionData::setup(Connection* connection, - std::unique_ptr& result, - bool clearDatabase) -{ - return setupPicklingKey(connection, result) - .then([connection, &result, clearDatabase] { - if (clearDatabase) { - qCInfo(E2EE) << "Clearing the database for account" << connection->objectName(); - result->database.clear(); - } - if (const auto outcome = result->database.setupOlmAccount(result->olmAccount)) { - if (outcome == OLM_SUCCESS) { - qCDebug(E2EE) << "The existing Olm account successfully unpickled"; - return true; - } - - qCritical(E2EE) << "Could not unpickle Olm account for" << connection->objectName(); - return false; - } - qCDebug(E2EE) << "A new Olm account has been created, uploading device keys"; - connection->callApi(result->olmAccount.deviceKeys()) - .then(connection, - [connection, &result] { - result->trackedUsers += connection->userId(); - result->outdatedUsers += connection->userId(); - result->encryptionUpdateRequired = true; - }, - [](auto* job) { - qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); - }); - return true; - }) - .onCanceled([connection] { - qCritical(E2EE) << "Could not setup E2EE for" << connection->objectName(); - return false; - }); -} - -void ConnectionEncryptionData::saveDevicesList() -{ - database.transaction(); - auto query = database.prepareQuery(u"DELETE FROM tracked_users"_s); - database.execute(query); - query.prepare(u"INSERT INTO tracked_users(matrixId) VALUES(:matrixId);"_s); - for (const auto& user : std::as_const(trackedUsers)) { - query.bindValue(u":matrixId"_s, user); - database.execute(query); - } - - query.prepare(u"DELETE FROM outdated_users"_s); - database.execute(query); - query.prepare(u"INSERT INTO outdated_users(matrixId) VALUES(:matrixId);"_s); - for (const auto& user : std::as_const(outdatedUsers)) { - query.bindValue(u":matrixId"_s, user); - database.execute(query); - } - - query.prepare( - u"INSERT INTO tracked_devices" - "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified, selfVerified) " - "VALUES (:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified, :selfVerified);"_s); - for (const auto& [user, devices] : deviceKeys.asKeyValueRange()) { - auto deleteQuery = - database.prepareQuery(u"DELETE FROM tracked_devices WHERE matrixId=:matrixId;"_s); - deleteQuery.bindValue(u":matrixId"_s, user); - database.execute(deleteQuery); - for (const auto& device : std::as_const(devices)) { - const auto keys = device.keys.asKeyValueRange(); - deleteQuery.prepare( - u"DELETE FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId;"_s); - deleteQuery.bindValue(u":matrixId"_s, user); - deleteQuery.bindValue(u":deviceId"_s, device.deviceId); - database.execute(deleteQuery); - - if (device.deviceId.isEmpty()) { - qCCritical(E2EE) << "Clearing an invalid tracked device record with empty deviceId"; - continue; - } - const auto curveKeyIt = std::ranges::find_if(keys, [](const auto& p) { - return p.first.startsWith("curve"_L1); - }); - const auto edKeyIt = std::ranges::find_if(keys, [](const auto& p) { - return p.first.startsWith("ed"_L1); - }); - if (curveKeyIt == keys.end() || edKeyIt == keys.end()) { - qCCritical(E2EE) << "Clearing an invalid tracked device record due to keys missing"; - continue; - } - - query.bindValue(u":matrixId"_s, user); - query.bindValue(u":deviceId"_s, device.deviceId); - query.bindValue(u":curveKeyId"_s, curveKeyIt->first); - query.bindValue(u":curveKey"_s, curveKeyIt->second); - query.bindValue(u":edKeyId"_s, edKeyIt->first); - query.bindValue(u":edKey"_s, edKeyIt->second); - // If the device gets saved here, it can't be verified - query.bindValue(u":verified"_s, verifiedDevices[user][device.deviceId]); - query.bindValue(u":selfVerified"_s, selfVerifiedDevices[user][device.deviceId]); - - database.execute(query); - } - } - database.commit(); -} - -void ConnectionEncryptionData::loadDevicesList() -{ - auto query = - database.prepareQuery(QStringLiteral("SELECT * FROM tracked_users;")); - database.execute(query); - while (query.next()) { - trackedUsers += query.value(0).toString(); - } - - query = - database.prepareQuery(QStringLiteral("SELECT * FROM outdated_users;")); - database.execute(query); - while (query.next()) { - outdatedUsers += query.value(0).toString(); - } - - static const QStringList Algorithms{ SupportedAlgorithms.cbegin(), - SupportedAlgorithms.cend() }; - query = - database.prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;")); - database.execute(query); - while (query.next()) { - deviceKeys[query.value("matrixId"_L1).toString()].insert( - query.value("deviceId"_L1).toString(), - { - .userId = query.value("matrixId"_L1).toString(), - .deviceId = query.value("deviceId"_L1).toString(), - .algorithms = Algorithms, - .keys{ { query.value("curveKeyId"_L1).toString(), - query.value("curveKey"_L1).toString() }, - { query.value("edKeyId"_L1).toString(), - query.value("edKey"_L1).toString() } }, - .signatures{} // not needed after initial validation so not saved - }); - selfVerifiedDevices[query.value("matrixId"_L1).toString()][query.value("deviceId"_L1).toString()] = query.value("selfVerified"_L1).toBool(); - verifiedDevices[query.value("matrixId"_L1).toString()][query.value("deviceId"_L1).toString()] = query.value("verified"_L1).toBool(); - } -} - -QString ConnectionEncryptionData::curveKeyForUserDevice( - const QString& userId, const QString& device) const -{ - return deviceKeys[userId][device].keys["curve25519:"_L1 + device]; -} - -bool ConnectionEncryptionData::isKnownCurveKey(const QString& userId, - const QString& curveKey) const -{ - auto query = database.prepareQuery( - QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId " - "AND curveKey=:curveKey")); - query.bindValue(":matrixId"_L1, userId); - query.bindValue(":curveKey"_L1, curveKey); - database.execute(query); - return query.next(); -} - -bool ConnectionEncryptionData::hasOlmSession(const QString& user, - const QString& deviceId) const -{ - const auto& curveKey = curveKeyForUserDevice(user, deviceId).toLatin1(); - const auto sessionIt = olmSessions.find(curveKey); - return sessionIt != olmSessions.cend() && !sessionIt->second.empty(); -} - -void ConnectionEncryptionData::onSyncSuccess(SyncData& syncResponse) -{ - oneTimeKeysCount = syncResponse.deviceOneTimeKeysCount(); - if (oneTimeKeysCount[SignedCurve25519Key] - < 0.4 * olmAccount.maxNumberOfOneTimeKeys() - && !isUploadingKeys) { - isUploadingKeys = true; - olmAccount.generateOneTimeKeys(olmAccount.maxNumberOfOneTimeKeys() / 2 - - oneTimeKeysCount[SignedCurve25519Key]); - auto keys = olmAccount.oneTimeKeys(); - auto job = olmAccount.createUploadKeyRequest(keys); - q->run(job, ForegroundRequest); - QObject::connect(job, &BaseJob::success, q, - [this] { olmAccount.markKeysAsPublished(); }); - QObject::connect(job, &BaseJob::result, q, - [this] { isUploadingKeys = false; }); - } - if(firstSync) { - loadDevicesList(); - firstSync = false; - } - - consumeDevicesList(syncResponse.takeDevicesList()); - - auto checkQuery = database.prepareQuery("SELECT * FROM master_keys WHERE userId=:userId"_L1); - checkQuery.bindValue(":userId"_L1, q->userId()); - database.execute(checkQuery); - const auto haveMasterKey = checkQuery.next(); - if (trackedUsers.contains(q->userId()) && !outdatedUsers.contains(q->userId()) && !haveMasterKey) { - emit q->crossSigningSetupRequired(); - } - -} - -void ConnectionEncryptionData::consumeDevicesList(const DevicesList& devicesList) -{ - 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(); -} - -void ConnectionEncryptionData::loadOutdatedUserDevices() -{ - QHash users; - for(const auto &user : outdatedUsers) { - users[user] += QStringList(); - } - currentQueryKeysJob.abandon(); // Cancel network request explicitly - currentQueryKeysJob = q->callApi(users).onResult(q, [this](QueryKeysJob* job) { - if (job->status().good()) - handleQueryKeys(collectResponse(job)); - - emit q->finishedQueryingKeys(); - }); -} - -void ConnectionEncryptionData::consumeToDeviceEvent(EventPtr toDeviceEvent) -{ - if (processIfVerificationEvent(*toDeviceEvent, false)) - return; - if (auto&& event = eventCast(std::move(toDeviceEvent))) { - if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event->id() - << "for event" << event->algorithm(); - return; - } - if (isKnownCurveKey(event->senderId(), event->senderKey())) { - handleEncryptedToDeviceEvent(*event); - return; - } - trackedUsers += event->senderId(); - outdatedUsers += event->senderId(); - encryptionUpdateRequired = true; - pendingEncryptedEvents.push_back(std::move(event)); - } -} - -bool ConnectionEncryptionData::processIfVerificationEvent(const Event& evt, - bool encrypted) -{ - return switchOnType( - evt, - [this, encrypted](const KeyVerificationRequestEvent& reqEvt) { - setupKeyVerificationSession(reqEvt.fullJson()[SenderKey].toString(), - reqEvt, q, encrypted); - return true; - }, - [](const KeyVerificationDoneEvent&) { - qCDebug(E2EE) << "Ignoring m.key.verification.done"; - return true; - }, - [this](const KeyVerificationEvent& kvEvt) { - if (auto* const session = - verificationSessions.value(kvEvt.transactionId())) { - qCDebug(E2EE) << "Handling" << kvEvt.matrixType(); - session->handleEvent(kvEvt); - emit q->keyVerificationStateChanged(session, session->state()); - } - return true; - }, - false); -} - -class SecretSendEvent : public Event { -public: - using Event::Event; - QUO_EVENT(SecretSendEvent, "m.secret.send") - QUO_CONTENT_GETTER(QString, requestId) - QUO_CONTENT_GETTER(QString, secret) -}; - -void ConnectionEncryptionData::handleEncryptedToDeviceEvent(const EncryptedEvent& event) -{ - const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); - if (!decryptedEvent) { - qCWarning(E2EE) << "Failed to decrypt to-device event from device" - << event.deviceId(); - return; - } - - if (processIfVerificationEvent(*decryptedEvent, true)) - return; - decryptedEvent->switchOnType( - [this, &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), - olmSessionId, event.senderKey().toLatin1(), q->edKeyForUserDevice(event.senderId(), event.deviceId()).toLatin1()); - } else { - qCDebug(E2EE) - << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - } - }, - [this](const SecretSendEvent& sse) { - emit q->secretReceived(sse.requestId(), sse.secret()); - }, - [](const Event& evt) { - qCWarning(E2EE) << "Skipping encrypted to_device event, type" << evt.matrixType(); - }); -} - -void ConnectionEncryptionData::handleMasterKeys(const QHash& masterKeys) -{ - for (const auto &[userId, key] : asKeyValueRange(masterKeys)) { - if (key.userId != userId) { - qCWarning(E2EE) << "Master key: userId mismatch" << key.userId << userId; - continue; - } - if (!key.usage.contains("master"_L1)) { - qCWarning(E2EE) << "Master key: invalid usage" << key.usage; - continue; - } - auto checkQuery = database.prepareQuery("SELECT * FROM master_keys WHERE userId=:userId"_L1); - checkQuery.bindValue(":userId"_L1, key.userId); - database.execute(checkQuery); - if (checkQuery.next()) { - if (checkQuery.value("key"_L1).toString() == key.keys.values()[0]) { - continue; - } - qCWarning(E2EE) << "New master key for" << key.userId; - database.transaction(); - auto query = database.prepareQuery( - "UPDATE tracked_devices SET verified=0, selfVerified=0 WHERE matrixId=:matrixId;"_L1); - query.bindValue(":matrixId"_L1, userId); - database.execute(query); - query = database.prepareQuery("DELETE FROM self_signing_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, userId); - database.execute(query); - database.commit(); - } - - auto query = database.prepareQuery("DELETE FROM master_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, userId); - database.execute(query); - query = database.prepareQuery("INSERT INTO master_keys(userId, key, verified) VALUES(:userId, :key, false);"_L1); - query.bindValue(":userId"_L1, userId); - query.bindValue(":key"_L1, key.keys.values()[0]); - database.execute(query); - } -} - -namespace { -QString getEd25519Signature(const CrossSigningKey& keyObject, const QString& userId, - const QString& masterKey) -{ - return keyObject.signatures[userId]["ed25519:"_L1 + masterKey].toString(); -} -} - -void ConnectionEncryptionData::handleSelfSigningKeys(const QHash& selfSigningKeys) -{ - for (const auto &[userId, key] : asKeyValueRange(selfSigningKeys)) { - if (key.userId != userId) { - qCWarning(E2EE) << "Self signing key: userId mismatch"<< key.userId << userId; - continue; - } - if (!key.usage.contains("self_signing"_L1)) { - qCWarning(E2EE) << "Self signing key: invalid usage" << key.usage; - continue; - } - const auto masterKey = q->masterKeyForUser(userId); - if (masterKey.isEmpty()) - continue; - - auto checkQuery = database.prepareQuery("SELECT key FROM self_signing_keys WHERE userId=:userId;"_L1); - checkQuery.bindValue(":userId"_L1, userId); - database.execute(checkQuery); - if (checkQuery.next()) { - auto oldKey = checkQuery.value("key"_L1).toString(); - if (oldKey != key.keys.values()[0]) { - qCWarning(E2EE) << "New self-signing key for" << userId << ". Marking all devices as unverified."; - database.transaction(); - auto query = database.prepareQuery( - "UPDATE tracked_devices SET verified=0, selfVerified=0 WHERE matrixId=:matrixId;"_L1); - query.bindValue(":matrixId"_L1, userId); - database.execute(query); - database.commit(); - } - } - - if (!ed25519VerifySignature(masterKey, toJson(key), - getEd25519Signature(key, userId, masterKey))) { - qCWarning(E2EE) << "Self signing key: failed signature verification" << userId; - continue; - } - auto query = database.prepareQuery("DELETE FROM self_signing_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, userId); - database.execute(query); - query = database.prepareQuery("INSERT INTO self_signing_keys(userId, key) VALUES(:userId, :key);"_L1); - query.bindValue(":userId"_L1, userId); - query.bindValue(":key"_L1, key.keys.values()[0]); - database.execute(query); - } -} - -void ConnectionEncryptionData::handleUserSigningKeys(const QHash& userSigningKeys) -{ - for (const auto &[userId, key] : asKeyValueRange(userSigningKeys)) { - if (key.userId != userId) { - qWarning() << "User signing key: userId mismatch" << key.userId << userId; - continue; - } - if (!key.usage.contains("user_signing"_L1)) { - qWarning() << "User signing key: invalid usage" << key.usage; - continue; - } - const auto masterKey = q->masterKeyForUser(userId); - if (masterKey.isEmpty()) - continue; - - auto checkQuery = database.prepareQuery("SELECT key FROM user_signing_keys WHERE userId=:userId"_L1); - checkQuery.bindValue(":userId"_L1, userId); - database.execute(checkQuery); - if (checkQuery.next()) { - auto oldKey = checkQuery.value("key"_L1).toString(); - if (oldKey != key.keys.values()[0]) { - qCWarning(E2EE) << "New user signing key; marking all master signing keys as unverified" << userId; - database.transaction(); - auto query = database.prepareQuery( - "UPDATE master_keys SET verified=0;"_L1); - database.execute(query); - database.commit(); - } - } - - if (!ed25519VerifySignature(masterKey, toJson(key), - getEd25519Signature(key, userId, masterKey))) { - qWarning() << "User signing key: failed signature verification" << userId; - continue; - } - auto query = database.prepareQuery("DELETE FROM user_signing_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, userId); - database.execute(query); - query = database.prepareQuery("INSERT INTO user_signing_keys(userId, key) VALUES(:userId, :key);"_L1); - query.bindValue(":userId"_L1, userId); - query.bindValue(":key"_L1, key.keys.values()[0]); - database.execute(query); - } -} - -void ConnectionEncryptionData::checkVerifiedMasterKeys(const QHash& masterKeys) -{ - if (!q->isUserVerified(q->userId())) { - return; - } - auto query = database.prepareQuery("SELECT key FROM user_signing_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, q->userId()); - database.execute(query); - if (!query.next()) { - return; - } - const auto userSigningKey = query.value("key"_L1).toString(); - for (const auto& masterKey : masterKeys) { - auto signature = getEd25519Signature(masterKey, q->userId(), userSigningKey); - if (signature.isEmpty()) { - continue; - } - if (ed25519VerifySignature(userSigningKey, toJson(masterKey), signature)) { - database.setMasterKeyVerified(masterKey.keys.values()[0]); - emit q->userVerified(masterKey.userId); - } else { - qCWarning(E2EE) << "Master key signature verification failed" << masterKey.userId; - } - } -} - -void ConnectionEncryptionData::handleDevicesList( - const QHash>& newDeviceKeys) -{ - for(const auto &[user, keys] : newDeviceKeys.asKeyValueRange()) { - const auto oldDevices = deviceKeys[user]; - auto query = database.prepareQuery("SELECT * FROM self_signing_keys WHERE userId=:userId;"_L1); - query.bindValue(":userId"_L1, user); - database.execute(query); - const auto selfSigningKey = query.next() ? query.value("key"_L1).toString() : QString(); - deviceKeys[user].clear(); - selfVerifiedDevices[user].clear(); - for (const auto &device : keys) { - if (device.userId != user) { - qWarning(E2EE) - << "mxId mismatch during device key verification:" - << device.userId << user; - continue; - } - if (!std::all_of(device.algorithms.cbegin(), - device.algorithms.cend(), isSupportedAlgorithm)) { - qWarning(E2EE) << "Unsupported encryption algorithms found" - << device.algorithms; - continue; - } - if (!verifyIdentitySignature(device, device.deviceId, - device.userId)) { - qWarning(E2EE) << "Failed to verify device keys signature. " - "Skipping device" << device.userId << device.deviceId; - continue; - } - if (const auto oldDeviceKeys = oldDevices.value(device.deviceId); - !oldDeviceKeys.deviceId.isEmpty()) // We've seen this device... - { - if (const auto keyId = "ed25519:"_L1 + device.deviceId; - oldDeviceKeys.keys[keyId] != device.keys[keyId]) - // ...but its keys that came now are not the same - { - qDebug(E2EE) - << "Device reuse detected. Skipping device" << device.userId << device.deviceId; - continue; - } - } - if (!selfSigningKey.isEmpty() && !device.signatures[user]["ed25519:"_L1 + selfSigningKey].isEmpty()) { - if (ed25519VerifySignature(selfSigningKey, toJson(static_cast(device)), device.signatures[user]["ed25519:"_L1 + selfSigningKey])) { - selfVerifiedDevices[user][device.deviceId] = true; - emit q->sessionVerified(user, device.deviceId); - } else { - qCWarning(E2EE) << "failed self signing signature check" << user << device.deviceId; - } - } - deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys); - } - outdatedUsers -= user; - } -} - -void ConnectionEncryptionData::handleQueryKeys(const QueryKeysJob::Response& keys) -{ - database.transaction(); - handleMasterKeys(keys.masterKeys); - handleSelfSigningKeys(keys.selfSigningKeys); - handleUserSigningKeys(keys.userSigningKeys); - checkVerifiedMasterKeys(keys.masterKeys); - handleDevicesList(keys.deviceKeys); - database.commit(); - - saveDevicesList(); - - // A completely faithful code would call std::partition() with bare - // isKnownCurveKey(), then handleEncryptedToDeviceEvent() on each event - // with the known key, and then std::erase()... but - // handleEncryptedToDeviceEvent() doesn't have side effects on the handled - // events so a small corner-cutting should be fine. - std::erase_if(pendingEncryptedEvents, - [this](const event_ptr_tt& pendingEvent) { - if (!isKnownCurveKey(pendingEvent->senderId(), - pendingEvent->senderKey())) - return false; - handleEncryptedToDeviceEvent(*pendingEvent); - return true; - }); -} - -void ConnectionEncryptionData::encryptionUpdate(const QList& forUsers) -{ - for (const auto& userId : forUsers) - if (!trackedUsers.contains(userId)) { - trackedUsers += userId; - outdatedUsers += userId; - encryptionUpdateRequired = true; - } -} - -bool ConnectionEncryptionData::createOlmSession( - const QString& targetUserId, const QString& targetDeviceId, - const OneTimeKeys& oneTimeKeyObject) -{ - static QOlmUtility verifier; - qDebug(E2EE) << "Creating a new session for" << targetUserId - << targetDeviceId; - if (oneTimeKeyObject.isEmpty()) { - qWarning(E2EE) << "No one time key for" << targetUserId - << targetDeviceId; - return false; - } - auto* signedOneTimeKey = - std::get_if(&*oneTimeKeyObject.begin()); - if (!signedOneTimeKey) { - qWarning(E2EE) << "No signed one time key for" << targetUserId - << targetDeviceId; - return false; - } - // Verify contents of signedOneTimeKey - for that, drop `signatures` and - // `unsigned` and then verify the object against the respective signature - const auto signature = - signedOneTimeKey->signature(targetUserId, targetDeviceId); - if (!verifier.ed25519Verify( - q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), - signedOneTimeKey->toJsonForVerification(), signature)) { - qWarning(E2EE) << "Failed to verify one-time-key signature for" - << targetUserId << targetDeviceId - << ". Skipping this device."; - return false; - } - const auto recipientCurveKey = - curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(); - auto session = olmAccount.createOutboundSession(recipientCurveKey, - signedOneTimeKey->key()); - if (!session) { - qCWarning(E2EE) << "Failed to create olm session for " - << recipientCurveKey << session.error(); - return false; - } - saveSession(*session, recipientCurveKey); - olmSessions[recipientCurveKey].push_back(std::move(*session)); - return true; -} - -std::pair -ConnectionEncryptionData::olmEncryptMessage(const QString& userId, - const QString& device, - const QByteArray& message) const -{ - const auto& curveKey = curveKeyForUserDevice(userId, device).toLatin1(); - const auto& olmSession = olmSessions.at(curveKey).front(); - const auto result = olmSession.encrypt(message); - database.updateOlmSession(curveKey, olmSession); - return { result.type(), result.toCiphertext() }; -} - -QJsonObject ConnectionEncryptionData::assembleEncryptedContent( - QJsonObject payloadJson, const QString& targetUserId, - const QString& targetDeviceId) const -{ - payloadJson.insert(SenderKey, q->userId()); - payloadJson.insert("keys"_L1, - QJsonObject{ - { Ed25519Key, olmAccount.identityKeys().ed25519 } }); - payloadJson.insert("recipient"_L1, targetUserId); - payloadJson.insert( - "recipient_keys"_L1, - QJsonObject{ { Ed25519Key, - q->edKeyForUserDevice(targetUserId, targetDeviceId) } }); - const auto [type, cipherText] = olmEncryptMessage( - targetUserId, targetDeviceId, - QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); - QJsonObject encrypted{ - { curveKeyForUserDevice(targetUserId, targetDeviceId), - QJsonObject{ { "type"_L1, type }, - { "body"_L1, QString::fromLatin1(cipherText) } } } - }; - return EncryptedEvent(encrypted, olmAccount.identityKeys().curve25519) - .contentJson(); -} - -std::pair doDecryptMessage(const QOlmSession& session, - const QOlmMessage& message, - auto&& andThen) -{ - const auto expectedMessage = session.decrypt(message); - if (expectedMessage) { - const auto result = - std::make_pair(*expectedMessage, session.sessionId()); - andThen(); - return result; - } - const auto errorLine = message.type() == QOlmMessage::PreKey - ? "Failed to decrypt prekey message:" - : "Failed to decrypt message:"; - qCDebug(E2EE) << errorLine << expectedMessage.error(); - return {}; -} - -std::pair ConnectionEncryptionData::sessionDecryptMessage( - const QJsonObject& personalCipherObject, const QByteArray& senderKey) -{ - const auto msgType = static_cast( - personalCipherObject.value(TypeKey).toInt(-1)); - if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) { - qCWarning(E2EE) << "Olm message has incorrect type" << msgType; - return {}; - } - const QOlmMessage message{ - personalCipherObject.value(BodyKey).toString().toLatin1(), msgType - }; - for (const auto& session : olmSessions[senderKey]) - if (msgType == QOlmMessage::General - || session.matchesInboundSessionFrom(senderKey, message)) { - return doDecryptMessage(session, message, [this, &session] { - q->database()->setOlmSessionLastReceived( - session.sessionId(), QDateTime::currentDateTime()); - }); - } - - if (msgType == QOlmMessage::General) { - qCWarning(E2EE) << "Failed to decrypt message"; - return {}; - } - - qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only - auto newSessionResult = - olmAccount.createInboundSessionFrom(senderKey, message); - if (!newSessionResult) { - qCWarning(E2EE) << "Failed to create inbound session for" << senderKey; - return {}; - } - auto&& newSession = std::move(*newSessionResult); - if (olmAccount.removeOneTimeKeys(newSession) != OLM_SUCCESS) { - qWarning(E2EE) << "Failed to remove one time key for session" - << newSession.sessionId(); - // Keep going though - } - return doDecryptMessage(newSession, message, [this, &senderKey, &newSession] { - saveSession(newSession, senderKey); - olmSessions[senderKey].push_back(std::move(newSession)); - }); -} - -std::pair ConnectionEncryptionData::sessionDecryptMessage( - const EncryptedEvent& encryptedEvent) -{ - if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) - return {}; - - const auto identityKey = olmAccount.identityKeys().curve25519; - const auto personalCipherObject = encryptedEvent.ciphertext(identityKey); - if (personalCipherObject.isEmpty()) { - qDebug(E2EE) << "Encrypted event is not for the current device"; - return {}; - } - const auto [decrypted, olmSessionId] = - sessionDecryptMessage(personalCipherObject, - encryptedEvent.senderKey().toLatin1()); - if (decrypted.isEmpty()) { - qDebug(E2EE) << "Problem with new session from senderKey:" - << encryptedEvent.senderKey() - << olmAccount.oneTimeKeys().keys; - - auto query = database.prepareQuery( - "SELECT deviceId FROM tracked_devices WHERE curveKey=:curveKey;"_L1); - query.bindValue(":curveKey"_L1, encryptedEvent.senderKey()); - database.execute(query); - if (!query.next()) { - qCWarning(E2EE) << "Unknown device while trying to recover from broken olm session"; - return {}; - } - auto senderId = encryptedEvent.senderId(); - auto deviceId = query.value("deviceId"_L1).toString(); - QHash> hash{ { encryptedEvent.senderId(), - { { deviceId, "signed_curve25519"_L1 } } } }; - q->callApi(hash).then(q, [this, deviceId, senderId](const auto* job) { - if (triedDevices.contains({ senderId, deviceId })) { - return; - } - triedDevices += { senderId, deviceId }; - qDebug(E2EE) << "Sending dummy event to" << senderId << deviceId; - createOlmSession(senderId, deviceId, job->oneTimeKeys()[senderId][deviceId]); - q->sendToDevice(senderId, deviceId, DummyEvent(), true); - }); - return {}; - } - - auto&& decryptedEvent = - fromJson(QJsonDocument::fromJson(decrypted)); - - if (auto sender = decryptedEvent->fullJson()[SenderKey].toString(); - sender != encryptedEvent.senderId()) { - qWarning(E2EE) << "Found user" << sender << "instead of sender" << encryptedEvent.senderId() - << "in Olm plaintext"; - return {}; - } - - auto query = database.prepareQuery(QStringLiteral( - "SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;")); - const auto senderKey = encryptedEvent.contentPart(SenderKeyKey); - query.bindValue(":curveKey"_L1, senderKey); - database.execute(query); - if (!query.next()) { - qWarning(E2EE) << "Received olm message from unknown device" << senderKey; - return {}; - } - if (auto edKey = decryptedEvent->fullJson()["keys"_L1][Ed25519Key].toString(); - edKey.isEmpty() || query.value("edKey"_L1).toString() != edKey) // - { - qDebug(E2EE) << "Received olm message with invalid ed key"; - return {}; - } - - // TODO: keys to constants - const auto decryptedEventObject = decryptedEvent->fullJson(); - if (const auto recipient = - decryptedEventObject.value("recipient"_L1).toString(); - recipient != q->userId()) // - { - qDebug(E2EE) << "Found user" << recipient << "instead of" << q->userId() - << "in Olm plaintext"; - return {}; - } - if (const auto ourKey = - decryptedEventObject["recipient_keys"_L1][Ed25519Key].toString(); - ourKey != olmAccount.identityKeys().ed25519) // - { - qDebug(E2EE) << "Found key" << ourKey - << "instead of our own ed25519 key in Olm plaintext"; - return {}; - } - - return { std::move(decryptedEvent), olmSessionId }; -} - -void ConnectionEncryptionData::doSendSessionKeyToDevices( - const QString& roomId, const QByteArray& sessionId, - const QByteArray& sessionKey, uint32_t messageIndex, - const QMultiHash& devices) -{ - qDebug(E2EE) << "Sending room key to devices:" << sessionId << messageIndex; - QHash> hash; - for (const auto& [userId, deviceId] : devices.asKeyValueRange()) - if (!hasOlmSession(userId, deviceId)) { - hash[userId].insert(deviceId, "signed_curve25519"_L1); - qDebug(E2EE) << "Adding" << userId << deviceId - << "to keys to claim"; - } - - const auto sendKey = [devices, this, sessionId, messageIndex, sessionKey, - roomId] { - QHash> usersToDevicesToContent; - for (const auto& [targetUserId, targetDeviceId] : devices.asKeyValueRange()) { - if (!hasOlmSession(targetUserId, targetDeviceId)) - continue; - - // Noisy and leaks the key to logs but nice for debugging -// qDebug(E2EE) << "Creating the payload for" << targetUserId -// << targetDeviceId << sessionId << sessionKey.toHex(); - const auto keyEventJson = - RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, - QString::fromLatin1(sessionId), - QString::fromLatin1(sessionKey)) - .fullJson(); - - usersToDevicesToContent[targetUserId][targetDeviceId] = - assembleEncryptedContent(keyEventJson, targetUserId, - targetDeviceId); - } - if (!usersToDevicesToContent.empty()) { - q->sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); - QVector> receivedDevices; - receivedDevices.reserve(devices.size()); - for (const auto& [user, device] : devices.asKeyValueRange()) - receivedDevices.push_back( - { user, device, curveKeyForUserDevice(user, device) }); - - database.setDevicesReceivedKey(roomId, receivedDevices, - sessionId, messageIndex); - } - }; - - if (hash.isEmpty()) { - sendKey(); - return; - } - - q->callApi(hash).then(q, [this, sendKey](const ClaimKeysJob* job) { - for (const auto& [userId, userDevices] : job->oneTimeKeys().asKeyValueRange()) - for (const auto& [deviceId, keys] : userDevices.asKeyValueRange()) - createOlmSession(userId, deviceId, keys); - - sendKey(); - }); -} - -void ConnectionEncryptionData::sendSessionKeyToDevices( - const QString& roomId, const QOlmOutboundGroupSession& outboundSession, - const QMultiHash& devices) -{ - const auto& sessionId = outboundSession.sessionId(); - const auto& sessionKey = outboundSession.sessionKey(); - const auto& index = outboundSession.sessionMessageIndex(); - - const auto closure = [this, roomId, sessionId, sessionKey, index, devices] { - doSendSessionKeyToDevices(roomId, sessionId, sessionKey, index, devices); - }; - if (currentQueryKeysJob != nullptr) { - currentQueryKeysJob = currentQueryKeysJob.onResult(q, closure); - } else - closure(); -} - -ConnectionEncryptionData::ConnectionEncryptionData(Connection* connection, - PicklingKey&& picklingKey) - : q(connection) - , olmAccount(q->userId(), q->deviceId()) - , database(q->userId(), q->deviceId(), std::move(picklingKey)) - , olmSessions(database.loadOlmSessions()) -{ - QObject::connect(&olmAccount, &QOlmAccount::needsSave, q, - [this] { saveOlmAccount(); }); -} - -void ConnectionEncryptionData::saveOlmAccount() -{ - qCDebug(E2EE) << "Saving olm account"; - database.storeOlmAccount(olmAccount); -} - -void ConnectionEncryptionData::reloadDevices() -{ - outdatedUsers = trackedUsers; - loadOutdatedUserDevices(); -} - -bool ConnectionEncryptionData::hasConflictingDeviceIdsAndCrossSigningKeys(const QString& userId) -{ - auto devices = q->devicesForUser(userId); - - auto selfQuery = database.prepareQuery("SELECT key FROM self_signing_keys WHERE userId=:userId;"_L1); - selfQuery.bindValue(":userId"_L1, userId); - database.execute(selfQuery); - if (selfQuery.next() && devices.contains(selfQuery.value("key"_L1).toString())) - return true; - - if (devices.contains(q->masterKeyForUser(userId))) - return true; - - auto userQuery = database.prepareQuery("SELECT key FROM user_signing_keys WHERE userId=:userId;"_L1); - userQuery.bindValue(":userId"_L1, userId); - database.execute(userQuery); - return userQuery.next() && devices.contains(userQuery.value("key"_L1).toString()); -} diff --git a/Quotient/connectionencryptiondata_p.h b/Quotient/connectionencryptiondata_p.h deleted file mode 100644 index 2dcc67996..000000000 --- a/Quotient/connectionencryptiondata_p.h +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include "connection.h" -#include "database.h" -#include "logging_categories_p.h" - -#include "e2ee/qolmaccount.h" -#include "e2ee/qolmsession.h" - -#include "events/encryptedevent.h" - -namespace Quotient { - -struct DevicesList; - -namespace _impl { - class ConnectionEncryptionData { - public: - static QFuture setup(Connection* connection, - std::unique_ptr& result, - bool clearDatabase = false); - - Connection* q; - QOlmAccount olmAccount; - // No easy way in C++ to discern between SQL SELECT from UPDATE, too bad - mutable Database database; - std::unordered_map> olmSessions; - //! A map from SenderKey to vector of InboundSession - QHash verificationSessions{}; - QSet trackedUsers{}; - QSet outdatedUsers{}; - QHash> deviceKeys{}; - JobHandle currentQueryKeysJob{}; - QSet> triedDevices{}; - //! An update of internal tracking structures (trackedUsers, e.g.) is - //! needed - bool encryptionUpdateRequired = false; - QHash oneTimeKeysCount{}; - std::vector> pendingEncryptedEvents{}; - bool isUploadingKeys = false; - bool firstSync = true; - QHash> selfVerifiedDevices; - QHash> verifiedDevices; - - void saveDevicesList(); - void loadDevicesList(); - QString curveKeyForUserDevice(const QString& userId, - const QString& device) const; - bool isKnownCurveKey(const QString& userId, - const QString& curveKey) const; - bool hasOlmSession(const QString &user, const QString &deviceId) const; - - void onSyncSuccess(SyncData &syncResponse); - void loadOutdatedUserDevices(); - void consumeToDeviceEvent(EventPtr toDeviceEvent); - void encryptionUpdate(const QList& forUsers); - - bool createOlmSession(const QString& targetUserId, - const QString& targetDeviceId, - const OneTimeKeys& oneTimeKeyObject); - void saveSession(const QOlmSession& session, const QByteArray& senderKey) - { - database.saveOlmSession(senderKey, session, - QDateTime::currentDateTime()); - } - void saveOlmAccount(); - void reloadDevices(); - - std::pair sessionDecryptMessage( - const QJsonObject& personalCipherObject, - const QByteArray& senderKey); - std::pair sessionDecryptMessage(const EncryptedEvent& encryptedEvent); - - QJsonObject assembleEncryptedContent( - QJsonObject payloadJson, const QString& targetUserId, - const QString& targetDeviceId) const; - void sendSessionKeyToDevices( - const QString& roomId, - const QOlmOutboundGroupSession& outboundSession, - const QMultiHash& devices); - - template - KeyVerificationSession* setupKeyVerificationSession( - ArgTs&&... sessionArgs) - { - auto session = - new KeyVerificationSession(std::forward(sessionArgs)...); - qCDebug(E2EE) << "Incoming key verification session from" << session->remoteDeviceId(); - verificationSessions.insert(session->transactionId(), session); - QObject::connect(session, &QObject::destroyed, q, - [this, txnId = session->transactionId()] { - verificationSessions.remove(txnId); - }); - emit q->newKeyVerificationSession(session); - return session; - } - - // This is only public to enable std::make_unique; do not use directly, - // get an instance from setup() instead - ConnectionEncryptionData(Connection* connection, - PicklingKey&& picklingKey); - bool hasConflictingDeviceIdsAndCrossSigningKeys(const QString& userId); - - void handleQueryKeys(const QueryKeysJob::Response& keys); - - void handleMasterKeys(const QHash& masterKeys); - void handleSelfSigningKeys(const QHash& selfSigningKeys); - void handleUserSigningKeys(const QHash& userSigningKeys); - void handleDevicesList( - const QHash>& newDeviceKeys); - void checkVerifiedMasterKeys(const QHash& masterKeys); - - private: - void consumeDevicesList(const DevicesList &devicesList); - bool processIfVerificationEvent(const Event& evt, bool encrypted); - void handleEncryptedToDeviceEvent(const EncryptedEvent& event); - - // This function assumes that an olm session with (user, device) exists - std::pair olmEncryptMessage( - const QString& userId, const QString& device, - const QByteArray& message) const; - - void doSendSessionKeyToDevices(const QString& roomId, const QByteArray& sessionId, - const QByteArray &sessionKey, uint32_t messageIndex, - const QMultiHash& devices); - }; -} // namespace _impl -} // namespace Quotient diff --git a/Quotient/crypto-sdk/Cargo.lock b/Quotient/crypto-sdk/Cargo.lock new file mode 100644 index 000000000..da532eb15 --- /dev/null +++ b/Quotient/crypto-sdk/Cargo.lock @@ -0,0 +1,2551 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anyhow" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "as_variant" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38fa22307249f86fb7fad906fcae77f2564caeb56d7209103c551cd1cf4798f" + +[[package]] +name = "assign" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_panic" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxx" +version = "1.0.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sqlite" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8010e36e12f3be22543a5e478b4af20aeead9a700dd69581a5e050a070fc22c" +dependencies = [ + "deadpool", + "deadpool-sync", + "rusqlite", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "eyeball" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f" +dependencies = [ + "futures-core", + "readlock", + "tracing", +] + +[[package]] +name = "eyeball-im" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9326c8d9f6d59d18935412608b4514cc661e4e068011bb2f523f6c8b1cfa3bd4" +dependencies = [ + "futures-core", + "imbl", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "imbl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978d142c8028edf52095703af2fad11d6f611af1246685725d6b850634647085" +dependencies = [ + "bitmaps", + "imbl-sized-chunks", + "rand_core", + "rand_xoshiro", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "js_int" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" +dependencies = [ + "serde", +] + +[[package]] +name = "js_option" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" +dependencies = [ + "serde", +] + +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matrix-pickle" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2551de3bba2cc65b52dc6b268df6114011fe118ac24870fbcf1b35537bd721" +dependencies = [ + "matrix-pickle-derive", + "thiserror", +] + +[[package]] +name = "matrix-pickle-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75de44c3120d78e978adbcf6d453b20ba011f3c46363e52d1dbbc72f545e9fb" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matrix-rust-sdk-crypto-cpp" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-build", + "matrix-sdk-common", + "matrix-sdk-crypto", + "matrix-sdk-sqlite", + "ruma 0.11.1", + "ruma-common 0.13.0", + "serde", + "serde_json", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00891954d0826a94f1d130f46cbca64176003a234c1be5d9d282970d31cf0c87" +dependencies = [ + "as_variant", + "async-trait", + "bitflags", + "eyeball", + "eyeball-im", + "futures-util", + "matrix-sdk-common", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "once_cell", + "ruma 0.9.4", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "matrix-sdk-common" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb365a626ab6f6c6a2422cfe2565522f19accb06706c6d04bca8f0f71df29c9f" +dependencies = [ + "async-trait", + "futures-core", + "futures-util", + "gloo-timers", + "instant", + "ruma 0.9.4", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "matrix-sdk-crypto" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03a64d12a83ebe33bb55de9b77faef4ce27006f946f8468c3311f311f39ab8b" +dependencies = [ + "aes", + "as_variant", + "async-trait", + "bs58", + "byteorder", + "cbc", + "cfg-if", + "ctr", + "eyeball", + "futures-core", + "futures-util", + "hkdf", + "hmac", + "itertools 0.12.1", + "matrix-sdk-common", + "pbkdf2", + "rand", + "rmp-serde", + "ruma 0.9.4", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-sqlite" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a98d034dd5aa85b4da6500e60c7d5b9328a37632cf40ba38bbe2c77cea3f14" +dependencies = [ + "async-trait", + "deadpool-sqlite", + "itertools 0.12.1", + "matrix-sdk-base", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "rmp-serde", + "ruma 0.9.4", + "rusqlite", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7e3162e9f982a4c57ab46df01a4775f697dec8899738bf62d7e97b63faa61c" +dependencies = [ + "blake3", + "chacha20poly1305", + "displaydoc", + "hmac", + "pbkdf2", + "rand", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "thiserror", + "zeroize", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs7" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d79178be066405e0602bf3035946edef6b11b3f9dde46dfe5f8bfd7dea4b77e7" +dependencies = [ + "der", + "spki", + "x509-cert", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "readlock" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072cfe5b1d2dcd38d20e18f85e9c9978b6cc08f0b373e9f1fff1541335622974" + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "ruma" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2779c38df072964c63476259d9300efb07d0d1a7178c6469893636ce0c547a36" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client-api", + "ruma-common 0.12.1", + "ruma-events", +] + +[[package]] +name = "ruma" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94984418ae8a5e1160e6c87608141330e9ae26330abf22e3d15416efa96d48a" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-common 0.14.1", + "web-time", +] + +[[package]] +name = "ruma-client-api" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "641837258fa214a70823477514954ef0f5d3bc6ae8e1d5d85081856a33103386" +dependencies = [ + "assign", + "bytes", + "http", + "js_int", + "js_option", + "maplit", + "ruma-common 0.12.1", + "ruma-events", + "serde", + "serde_html_form", + "serde_json", +] + +[[package]] +name = "ruma-common" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bca4c33c50e47b4cdceeac71bdef0c04153b0e29aa992d9030ec14a62323e85" +dependencies = [ + "as_variant", + "base64 0.21.7", + "bytes", + "form_urlencoded", + "http", + "indexmap", + "js_int", + "konst", + "percent-encoding", + "rand", + "regex", + "ruma-identifiers-validation 0.9.5", + "ruma-macros 0.12.0", + "serde", + "serde_html_form", + "serde_json", + "thiserror", + "tracing", + "url", + "uuid", + "wildmatch", +] + +[[package]] +name = "ruma-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba97203cc4cab8dc10e62fe8156ae5c61d2553f37c3037759fbae601982fb7b" +dependencies = [ + "as_variant", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "indexmap", + "js_int", + "percent-encoding", + "regex", + "ruma-identifiers-validation 0.9.5", + "ruma-macros 0.13.0", + "serde", + "serde_html_form", + "serde_json", + "thiserror", + "time", + "tracing", + "url", + "web-time", + "wildmatch", +] + +[[package]] +name = "ruma-common" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad71c7f49abaa047ba228339d34f9aaefa4d8b50ebeb8e859d0340cc2138bda8" +dependencies = [ + "as_variant", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "indexmap", + "js_int", + "percent-encoding", + "regex", + "ruma-identifiers-validation 0.10.0", + "ruma-macros 0.14.0", + "serde", + "serde_html_form", + "serde_json", + "thiserror", + "time", + "tracing", + "url", + "web-time", + "wildmatch", +] + +[[package]] +name = "ruma-events" +version = "0.27.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20a52770e5a9fb30b7a1c14ba8b3dcf76dadc01674e58e40094f78e6bd5e3f1" +dependencies = [ + "as_variant", + "indexmap", + "js_int", + "js_option", + "percent-encoding", + "regex", + "ruma-common 0.12.1", + "ruma-identifiers-validation 0.9.5", + "ruma-macros 0.12.0", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wildmatch", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa38974f5901ed4e00e10aec57b9ad3b4d6d6c1a1ae683c51b88700b9f4ffba" +dependencies = [ + "js_int", + "thiserror", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" +dependencies = [ + "js_int", + "thiserror", +] + +[[package]] +name = "ruma-macros" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0280534a4b3e34416f883285fac4f9c408cd0b737890ae66f3e7a7056d14be80" +dependencies = [ + "once_cell", + "proc-macro-crate 2.0.0", + "proc-macro2", + "quote", + "ruma-identifiers-validation 0.9.5", + "serde", + "syn", + "toml", +] + +[[package]] +name = "ruma-macros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36857a3350ea611ecc9968dcc4f3d5a03227a6c3fcbb446e8530e3be8852282" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "ruma-identifiers-validation 0.9.5", + "serde", + "syn", + "toml", +] + +[[package]] +name = "ruma-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d57d3cb20e8e758e8f7c5e408ce831d46758003b615100099852e468631934" +dependencies = [ + "cfg-if", + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "ruma-identifiers-validation 0.10.0", + "serde", + "syn", + "toml", +] + +[[package]] +name = "rusqlite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_html_form" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.22", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.20", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vodozemac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051d4af70b53b42adf2aac459a305851b8d754f210aaf11ab509e1065beff422" +dependencies = [ + "aes", + "arrayvec", + "base64 0.22.1", + "base64ct", + "cbc", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "getrandom", + "hkdf", + "hmac", + "matrix-pickle", + "pkcs7", + "prost", + "rand", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "subtle", + "thiserror", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Quotient/crypto-sdk/Cargo.toml b/Quotient/crypto-sdk/Cargo.toml new file mode 100644 index 000000000..5a764d79d --- /dev/null +++ b/Quotient/crypto-sdk/Cargo.toml @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2024 Tobias Fella +# SPDX-License-Identifier: LGPL-2.0-or-later + +[package] +name = "matrix-rust-sdk-crypto-cpp" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +cxx = "1.0" +matrix-sdk-common = "0.7" +matrix-sdk-crypto = "0.7" +ruma = "0.11.1" +ruma-common = "0.13.0" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +tokio = { version = "1.40.0", features = ["full"] } +tracing-subscriber = "0.3.18" + +[target.'cfg(target_os = "windows")'.dependencies] +matrix-sdk-sqlite = { version = "0.7", default-features = false, features = [ + "bundled", + "crypto-store", +] } + +[target.'cfg(not(target_os = "windows"))'.dependencies] +matrix-sdk-sqlite = { version = "0.7", default-features = false, features = [ + "crypto-store", +] } + +[build-dependencies] +cxx-build = "1.0" + +[profile.release] +#strip = true +#opt-level = "z" +#lto = true +#codegen-units = 1 diff --git a/Quotient/crypto-sdk/build.rs b/Quotient/crypto-sdk/build.rs new file mode 100644 index 000000000..156626a3b --- /dev/null +++ b/Quotient/crypto-sdk/build.rs @@ -0,0 +1,8 @@ +use cxx_build::CFG; + +fn main() { + CFG.include_prefix = "matrix-rust-sdk-crypto"; + cxx_build::bridge("src/lib.rs") + .std("c++20") + .compile("matrix-rust-sdk-crypto"); +} diff --git a/Quotient/crypto-sdk/src/cryptomachine.rs b/Quotient/crypto-sdk/src/cryptomachine.rs new file mode 100644 index 000000000..3241a3113 --- /dev/null +++ b/Quotient/crypto-sdk/src/cryptomachine.rs @@ -0,0 +1,1109 @@ +use std::{collections::BTreeMap, mem::ManuallyDrop, ops::Deref}; + +use matrix_sdk_common::ruma::{ + api::client::{ + backup::{add_backup_keys, RoomKeyBackup}, + keys::{ + claim_keys::{self, v3::OneTimeKeys}, + get_keys, upload_keys, + upload_signatures::{self, v3::Failure}, + }, + message::send_message_event, + sync::sync_events::DeviceLists, + to_device, + }, + encryption::{CrossSigningKey, DeviceKeys}, + events::{ + secret::request::SecretName, + secret_storage::key::{ + PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent, + SecretStorageV1AesHmacSha2Properties, + }, + AnyToDeviceEvent, + }, + serde::{base64::Standard, Raw}, + DeviceId, DeviceKeyAlgorithm, EventId, OwnedDeviceId, OwnedUserId, RoomId, UserId, +}; + +use matrix_sdk_crypto::types::RoomKeyBackupInfo; +use matrix_sdk_crypto::{ + secret_storage::{AesHmacSha2EncryptedData, SecretStorageKey}, + store::BackupDecryptionKey, + CrossSigningKeyExport, EncryptionSettings, EncryptionSyncChanges, IncomingResponse, OlmMachine, + SasState, UserIdentities, Verification, VerificationRequestState, KeysBackupRequest, +}; +use ruma::UInt; +use serde::Deserialize; +use matrix_sdk_common::ruma::OwnedTransactionId; + +use crate::{ + encryption_info::EncryptionInfo, + request::{ + ConfirmRequests, KeyVerificationRequest, KeysClaimRequest, OutgoingKeyVerificationRequest, + ToDeviceRequest, + }, + verification::CreatedSession, + verification::Emoji, +}; + +pub(crate) struct CryptoMachine { + pub(crate) runtime: tokio::runtime::Runtime, + pub(crate) machine: Option>, +} + +impl Drop for CryptoMachine { + fn drop(&mut self) { + self.runtime.block_on(async { + let machine = self.machine.take().unwrap(); + drop(ManuallyDrop::into_inner(machine)); + }) + } +} + +#[derive(Clone)] +pub(crate) struct Key { + session_id: String, + room_id: String, +} + +impl Key { + pub(crate) fn session_id(&self) -> String { + self.session_id.clone() + } + + pub(crate) fn room_id(&self) -> String { + self.room_id.clone() + } +} + +pub(crate) struct SyncChanges { + sessions: Vec, + keys: Vec, +} + +impl SyncChanges { + pub(crate) fn sessions(&self) -> Vec { + self.sessions.clone() + } + + pub(crate) fn keys(&self) -> Vec { + self.keys.clone() + } +} + +impl CryptoMachine { + pub(crate) fn outgoing_requests(&self) -> Vec { + self.runtime.block_on(async { + match self + .machine + .as_ref() + .expect("Should not happen") + .outgoing_requests() + .await + { + Ok(requests) => requests + .iter() + .map(|it| crate::request::OutgoingRequest(it.clone())) + .collect(), + Err(err) => { + println!("Failed to load outgoing requests"); + println!("{:?}", err); + Default::default() + } + } + }) + } + + pub(crate) fn mark_keys_upload_as_sent(&mut self, response: String, request_id: String) { + self.runtime.block_on(async { + #[derive(serde::Deserialize)] + struct Response { + one_time_key_counts: BTreeMap, + } + + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::KeysUpload(&upload_keys::v3::Response::new( + serde_json::from_str::(response.as_str()) + .unwrap() + .one_time_key_counts, + )), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_keys_query_as_sent(&mut self, response: String, request_id: String) { + self.runtime.block_on(async { + #[derive(Deserialize)] + struct Response { + failures: Option>, + device_keys: + Option>>>, + master_keys: Option>>, + self_signing_keys: Option>>, + user_signing_keys: Option>>, + } + let r: Response = serde_json::from_str(response.as_str()).unwrap(); + let mut response = get_keys::v3::Response::new(); + response.failures = r.failures.unwrap_or_default(); + response.device_keys = r.device_keys.unwrap_or_default(); + response.master_keys = r.master_keys.unwrap_or_default(); + response.self_signing_keys = r.self_signing_keys.unwrap_or_default(); + response.user_signing_keys = r.user_signing_keys.unwrap_or_default(); + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::KeysQuery(&response), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_keys_claim_as_sent(&mut self, response: String, request_id: String) { + self.runtime.block_on(async { + #[derive(Deserialize)] + struct Response { + failures: Option>, + one_time_keys: Option>, + } + let r: Response = serde_json::from_str(response.as_str()).unwrap(); + let mut request = claim_keys::v3::Response::new(r.one_time_keys.unwrap_or_default()); + request.failures = r.failures.unwrap_or_default(); + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::KeysClaim(&request), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_to_device_as_sent(&mut self, _: String, request_id: String) { + self.runtime.block_on(async { + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::ToDevice( + &to_device::send_event_to_device::v3::Response::new(/* no content */), + ), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_signature_upload_as_sent(&mut self, response: String, request_id: String) { + self.runtime.block_on(async { + #[derive(Deserialize)] + struct Response { + failures: Option>>, + } + let r: Response = serde_json::from_str(response.as_str()).unwrap(); + let mut request = upload_signatures::v3::Response::new(); + request.failures = r.failures.unwrap_or_default(); + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::SignatureUpload(&request), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_room_message_as_sent(&mut self, event_id: String, request_id: String) { + self.runtime.block_on(async { + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::RoomMessage(&send_message_event::v3::Response::new( + EventId::parse(event_id).unwrap(), + )), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn mark_keys_backup_as_sent(&mut self, response: String, request_id: String) { + self.runtime.block_on(async { + #[derive(Deserialize)] + struct Response { + etag: String, + count: UInt, + } + let r: Response = serde_json::from_str(response.as_str()).unwrap(); + self.machine + .as_ref() + .unwrap() + .mark_request_as_sent( + request_id.as_str().into(), + IncomingResponse::KeysBackup(&add_backup_keys::v3::Response::new( + r.etag, r.count, + )), + ) + .await + .unwrap(); + }); + } + + pub(crate) fn receive_sync_changes( + &mut self, + sync_json: String, + ) -> Box { + self.runtime.block_on(async { + #[derive(serde::Deserialize)] + struct Changes { + to_device: Option>>>, + device_lists: Option, + device_one_time_keys_count: BTreeMap, + next_batch: Option, + device_unused_fallback_key_types: Option>, + } + + let changes = serde_json::from_str::(sync_json.as_str()).unwrap(); + + let changes = self + .machine + .as_ref() + .unwrap() + .receive_sync_changes(EncryptionSyncChanges { + to_device_events: if let Some(events) = changes.to_device { + events["events"].clone() + } else { + Default::default() + }, + changed_devices: &changes.device_lists.unwrap_or_default(), + one_time_keys_counts: &changes.device_one_time_keys_count, + unused_fallback_keys: changes.device_unused_fallback_key_types.as_deref(), + next_batch_token: changes.next_batch, + }) + .await + .unwrap(); + + let mut events = vec![]; + for to_device_event in changes.0 { + if let AnyToDeviceEvent::KeyVerificationRequest(request) = + to_device_event.deserialize().unwrap() + { + events.push(KeyVerificationRequest( + self.machine + .as_ref() + .unwrap() + .get_verification_request( + &request.sender, + request.content.transaction_id, + ) + .unwrap(), + )); + } + } + Box::new(SyncChanges{sessions: events, keys: changes.1.iter().map(|it| Key {session_id: it.session_id.clone(), room_id: it.room_id.to_string()}).collect()}) + }) + } + + pub(crate) fn share_room_key( + &mut self, + room_id: String, + user_ids: Vec, + only_trusted: bool, + visibility: u8, + ) -> Vec { + use matrix_sdk_common::ruma::events::room::history_visibility::HistoryVisibility; + self.runtime + .block_on(async { + let room_id = RoomId::parse(room_id).unwrap(); + let user_ids: Vec = user_ids + .iter() + .map(|it| UserId::parse(it).unwrap()) + .collect(); + self.machine + .as_ref() + .unwrap() + .share_room_key( + &room_id, + user_ids.iter().map(Deref::deref), + EncryptionSettings { + history_visibility: match visibility { + 0 => HistoryVisibility::Invited, + 1 => HistoryVisibility::Joined, + 2 => HistoryVisibility::Shared, + 3 => HistoryVisibility::WorldReadable, + _ => panic!("Invalid value for visibility"), + }, + only_allow_trusted_devices: only_trusted, + .. Default::default() + }, + ) + .await + .unwrap() //TODO settings? + }) + .iter() + .map(|it| ToDeviceRequest { + txn_id: it.txn_id.to_string(), + event_type: it.event_type.to_string(), + messages: it.messages.clone(), + }) + .collect() + } + + //TODO lock + pub(crate) fn get_missing_sessions(&mut self, user_ids: Vec) -> Box { + self.runtime.block_on(async { + let user_ids: Vec = user_ids + .iter() + .map(|it| UserId::parse(it).unwrap()) + .collect(); + if let Some((id, request)) = self + .machine + .as_ref() + .unwrap() + .get_missing_sessions(user_ids.iter().map(Deref::deref)) + .await + .unwrap() + { + Box::new(KeysClaimRequest { + id: id.to_string(), + timeout: request.timeout, + one_time_keys: request.one_time_keys, + }) + } else { + Box::new(KeysClaimRequest { + id: "".to_string(), + timeout: None, + one_time_keys: Default::default(), + }) + } + }) + } + + pub(crate) fn encrypt_room_event( + &mut self, + room_id: String, + content: String, + matrix_type: String, + ) -> String { + self.runtime.block_on(async { + let room_id = RoomId::parse(room_id).unwrap(); + serde_json::to_string( + &self + .machine + .as_ref() + .unwrap() + //TODO: Don't hardcode this + .encrypt_room_event_raw( + &room_id, + &matrix_type, + &serde_json::from_str(content.as_str()).unwrap(), + ) + .await + .unwrap(), + ) + .unwrap() + }) + } + + pub(crate) fn decrypt_room_event(&mut self, room_id: String, json: String) -> String { + self.runtime.block_on(async { + let room_id = RoomId::parse(room_id).unwrap(); + if let Ok(event) = self + .machine + .as_ref() + .unwrap() + .decrypt_room_event(&serde_json::from_str(json.as_str()).unwrap(), &room_id) + .await + { + serde_json::to_string(&event.event).unwrap() + } else { + Default::default() + } + }) + } + + pub(crate) fn update_tracked_users(&mut self, user_ids: Vec) { + self.runtime.block_on(async { + let user_ids: Vec = user_ids + .iter() + .map(|it| UserId::parse(it).unwrap()) + .collect(); + self.machine + .as_ref() + .unwrap() + .update_tracked_users(user_ids.iter().map(Deref::deref)) + .await + .unwrap(); + }); + } + + pub(crate) fn accept_verification( + &mut self, + remote_user: String, + verification_id: String, + ) -> Box { + let user_id = UserId::parse(remote_user).unwrap(); + //TODO: why is there sometimes no request? + Box::new(OutgoingKeyVerificationRequest( + self.machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + .unwrap() + .accept() + .unwrap(), + )) + } + + pub(crate) fn confirm_verification( + &mut self, + remote_user: String, + verification_id: String, + ) -> Box { + self.runtime.block_on(async { + //TODO signature upload + let user_id = UserId::parse(remote_user).unwrap(); + let VerificationRequestState::Transitioned { verification } = self + .machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + .unwrap() + .state() + else { + panic!(); + }; + let Verification::SasV1(sas) = verification else { + panic!(); + }; + let (outgoing_verification_requests, signature_upload_request) = + sas.confirm().await.unwrap(); + + let outgoing_verification_requests = outgoing_verification_requests + .iter() + .map(|it| OutgoingKeyVerificationRequest(it.clone())) + .collect(); + + Box::new(ConfirmRequests { + verification: outgoing_verification_requests, + signature: signature_upload_request, + }) + }) + } + + pub(crate) fn start_sas( + &mut self, + remote_user: String, + verification_id: String, + ) -> Box { + self.runtime.block_on(async { + let user_id = UserId::parse(&remote_user).unwrap(); + let result = self + .machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + .unwrap() + .start_sas() + .await + .unwrap() + .unwrap(); + Box::new(OutgoingKeyVerificationRequest(result.1)) + }) + } + + pub(crate) fn accept_sas( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Box { + let user_id = UserId::parse(remote_user).unwrap(); + let Some(session) = self + .machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + else { + panic!() + }; + + if let VerificationRequestState::Transitioned { verification } = session.state() { + if let Verification::SasV1(sas) = verification { + Box::new(OutgoingKeyVerificationRequest(sas.accept().unwrap())) + } else { + panic!() + } + } else { + panic!() + } + } + + pub(crate) fn verification_get_state( + &mut self, + remote_user: String, + verification_id: String, + ) -> u8 { + let user_id = UserId::parse(remote_user).unwrap(); + match self + .machine + .as_ref() + .expect("Should not happen") + .get_verification_request(&user_id, &verification_id) + { + Some(session) => match session.state() { + VerificationRequestState::Created { .. } => 0, + VerificationRequestState::Requested { .. } => 1, + VerificationRequestState::Ready { .. } => 2, + VerificationRequestState::Transitioned { .. } => 3, + VerificationRequestState::Done => 4, + VerificationRequestState::Cancelled(_) => 5, + }, + None => 6, + } + } + + pub(crate) fn sas_get_state(&mut self, remote_user: String, verification_id: String) -> u8 { + let user_id = UserId::parse(remote_user).unwrap(); + let Some(session) = self + .machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + else { + return 6; + }; + + if let VerificationRequestState::Transitioned { verification } = session.state() { + if let Verification::SasV1(sas) = verification { + match sas.state() { + SasState::Started { .. } => 0, + SasState::Accepted { .. } => 1, + SasState::KeysExchanged { .. } => 2, + SasState::Confirmed => 3, + SasState::Done { .. } => 4, + SasState::Cancelled(_) => 5, + } + } else { + panic!() + } + } else { + // this is (in the current setup) mostly normal, since we're always querying sas state. + 6 + } + } + + pub(crate) fn sas_emoji(&self, remote_user: String, verification_id: String) -> Vec { + let user_id = UserId::parse(remote_user).unwrap(); + if let VerificationRequestState::Transitioned { verification } = self + .machine + .as_ref() + .unwrap() + .get_verification_request(&user_id, &verification_id) + .unwrap() + .state() + { + if let Verification::SasV1(sas) = verification { + sas.emoji() + .expect("Emoji can't be presented yet") + .iter() + .map(|e| crate::verification::Emoji(e.clone())) + .collect() + } else { + panic!() + } + } else { + panic!() + } + } + + pub(crate) fn request_device_verification( + &mut self, + user_id: String, + device_id: String, + ) -> Box { + self.runtime.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let device_id: Box = device_id.into(); + let device = self + .machine + .as_ref() + .unwrap() + .get_device(&user_id, &device_id, None) + .await + .unwrap() + .unwrap(); + let (session, outgoing) = device.request_verification().await; + Box::new(CreatedSession(session, outgoing)) + }) + } + + pub(crate) fn request_user_verification( + &mut self, + user_id: String, + room_id: String, + request_event_id: String, + ) -> Box { + self.runtime.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let room_id = RoomId::parse(room_id).unwrap(); + let event_id = EventId::parse(request_event_id).unwrap(); + let identity = self + .machine + .as_ref() + .unwrap() + .get_identity(&user_id, None) + .await + .unwrap() + .unwrap(); + if let UserIdentities::Other(other) = identity { + Box::new(KeyVerificationRequest( + other + .request_verification(&room_id, &event_id, None /*TODO?*/) + .await, + )) + } else { + panic!() + } + }) + } + + pub(crate) fn request_user_verification_content(&mut self, user_id: String) -> String { + self.runtime.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let identity = self + .machine + .as_ref() + .unwrap() + .get_identity(&user_id, None) + .await + .unwrap() + .unwrap(); + if let UserIdentities::Other(other) = identity { + serde_json::to_string(&other.verification_request_content(None /*TODO ?*/).await) + .unwrap() + } else { + Default::default() + } + }) + } + + pub(crate) fn get_room_event_encryption_info( + &self, + event: String, + room_id: String, + ) -> Box { + self.runtime.block_on(async { + let room_id = RoomId::parse(room_id).unwrap(); + let info = self + .machine + .as_ref() + .unwrap() + .get_room_event_encryption_info(&serde_json::from_str(&event).unwrap(), &room_id) + .await + .unwrap(); + Box::new(EncryptionInfo(info)) + }) + } + + pub(crate) fn receive_verification_event(&mut self, full_json: String) { + self.runtime.block_on(async { + self.machine + .as_ref() + .unwrap() + .receive_verification_event(&serde_json::from_str(&full_json).unwrap()) + .await + .unwrap(); + }) + } + + pub(crate) fn load_secrets( + &mut self, + passphrase: String, + key_id: String, + iterations: u32, + salt: String, + iv: String, + mac: String, + backup_key_iv: String, + backup_key_cipher: String, + backup_key_mac: String, + master_key_iv: String, + master_key_cipher: String, + master_key_mac: String, + self_key_iv: String, + self_key_cipher: String, + self_key_mac: String, + user_key_iv: String, + user_key_cipher: String, + user_key_mac: String, + version: String + ) -> String { + self.runtime.block_on(async { + let mut content = SecretStorageKeyEventContent::new( + key_id, + SecretStorageEncryptionAlgorithm::V1AesHmacSha2( + SecretStorageV1AesHmacSha2Properties::new( + matrix_sdk_common::ruma::serde::Base64::parse(iv).unwrap(), + matrix_sdk_common::ruma::serde::Base64::parse(mac).unwrap(), + ), + ), + ); + content.passphrase = Some(PassPhrase::new(salt, UInt::new_wrapping(iterations as u64))); + + let ss_key = SecretStorageKey::from_account_data(&passphrase, content).unwrap(); + let Ok(decryption_key) = BackupDecryptionKey::from_base64( + &String::from_utf8( + ss_key + .decrypt( + &AesHmacSha2EncryptedData { + iv: matrix_sdk_common::ruma::serde::Base64::::parse( + backup_key_iv, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + ciphertext: matrix_sdk_common::ruma::serde::Base64::parse( + backup_key_cipher, + ) + .unwrap(), + mac: matrix_sdk_common::ruma::serde::Base64::::parse( + backup_key_mac, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + }, + &SecretName::RecoveryKey, + ) + .unwrap(), + ) + .unwrap(), + ) else { + return Default::default(); + }; + + let master_private = String::from_utf8( + ss_key + .decrypt( + &AesHmacSha2EncryptedData { + iv: matrix_sdk_common::ruma::serde::Base64::::parse( + master_key_iv, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + ciphertext: matrix_sdk_common::ruma::serde::Base64::parse( + master_key_cipher, + ) + .unwrap(), + mac: matrix_sdk_common::ruma::serde::Base64::::parse( + master_key_mac, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + }, + &SecretName::CrossSigningMasterKey, + ) + .unwrap(), + ) + .unwrap(); + + let self_private = String::from_utf8( + ss_key + .decrypt( + &AesHmacSha2EncryptedData { + iv: matrix_sdk_common::ruma::serde::Base64::::parse( + self_key_iv, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + ciphertext: matrix_sdk_common::ruma::serde::Base64::parse( + self_key_cipher, + ) + .unwrap(), + mac: matrix_sdk_common::ruma::serde::Base64::::parse( + self_key_mac, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + }, + &SecretName::CrossSigningSelfSigningKey, + ) + .unwrap(), + ) + .unwrap(); + + let user_private = String::from_utf8( + ss_key + .decrypt( + &AesHmacSha2EncryptedData { + iv: matrix_sdk_common::ruma::serde::Base64::::parse( + user_key_iv, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + ciphertext: matrix_sdk_common::ruma::serde::Base64::parse( + user_key_cipher, + ) + .unwrap(), + mac: matrix_sdk_common::ruma::serde::Base64::::parse( + user_key_mac, + ) + .unwrap() + .as_bytes() + .try_into() + .unwrap(), + }, + &SecretName::CrossSigningUserSigningKey, + ) + .unwrap(), + ) + .unwrap(); + + self.machine + .as_ref() + .unwrap() + .backup_machine() + .save_decryption_key(Some(decryption_key.clone()), Some(version.clone())) + .await + .unwrap(); + + let backup_key = decryption_key.megolm_v1_public_key(); + backup_key.set_version(version); + self.machine.as_ref().unwrap().backup_machine().enable_backup_v1(backup_key).await.unwrap(); + + self.machine + .as_ref() + .unwrap() + .import_cross_signing_keys(CrossSigningKeyExport { + master_key: Some(master_private), + self_signing_key: Some(self_private), + user_signing_key: Some(user_private), + }) + .await + .unwrap(); + + let machine = self.machine.as_ref().unwrap(); + let upload_request = if let Some(own_device) = machine + .get_device(machine.user_id(), machine.device_id(), None) + .await + .unwrap() + { + Some(own_device.verify().await.unwrap()) + } else { + None + }; + serde_json::to_string(&upload_request.unwrap().signed_keys).unwrap() + }) + } + + pub(crate) fn import_from_backup(&mut self, response: String) { + self.runtime.block_on(async { + #[derive(Deserialize)] + struct Response { + rooms: BTreeMap, + } + let backed_up_keys: Response = serde_json::from_str(&response).unwrap(); + let mut decrypted_room_keys: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new(); + + let decryption_key = self.machine.as_ref().unwrap().backup_machine().get_backup_keys().await.unwrap().decryption_key; + + if let Some(decryption_key) = decryption_key { + for (room_id, room_keys) in backed_up_keys.rooms { + for (session_id, room_key) in room_keys.sessions { + let Ok(room_key) = room_key.deserialize() else { + continue; + }; + + let Ok(room_key) = decryption_key.decrypt_session_data(room_key.session_data) + else { + continue; + }; + + decrypted_room_keys + .entry(room_id.to_owned()) + .or_default() + .insert(session_id, room_key); + } + } + + self.machine + .as_ref() + .unwrap() + .backup_machine() + .import_backed_up_room_keys(decrypted_room_keys, |_, _| {}) + .await + .unwrap(); + } + }) + } + pub(crate) fn request_self_verification(&mut self) -> Box { + self.runtime.block_on(async { + if let UserIdentities::Own(own) = self + .machine + .as_ref() + .unwrap() + .get_identity(self.machine.as_ref().unwrap().user_id(), None) + .await + .unwrap() + .unwrap() + { + let session = own.request_verification().await.unwrap(); + Box::new(CreatedSession(session.0, session.1)) + } else { + panic!() + } + }) + } + + pub(crate) fn request_secrets_from_devices(&mut self) { + self.runtime.block_on(async { + self.machine + .as_ref() + .unwrap() + .query_missing_secrets_from_other_sessions() + .await + .unwrap(); + }); + } + + pub(crate) fn has_pending_backup_key(&self) -> bool { + self.runtime.block_on(async { + let machine = self.machine.as_ref().unwrap(); + machine.store().get_secrets_from_inbox(&SecretName::RecoveryKey).await.unwrap().len() > 0 && machine.backup_machine().get_backup_keys().await.unwrap().decryption_key.is_none() + }) + } + + pub(crate) fn initialize_existing_backup(&mut self, version_reply: String) { + self.runtime.block_on(async { + let machine = self.machine.as_ref().unwrap(); + for secret in machine.store().get_secrets_from_inbox(&SecretName::RecoveryKey).await.unwrap() { + let decryption_key = BackupDecryptionKey::from_base64(&secret.event.content.secret).unwrap(); + #[derive(Deserialize)] + struct Response { + version: String, + } + let current_version: Response = serde_json::from_str(&version_reply).unwrap(); + let backup_info: RoomKeyBackupInfo = serde_json::from_str::(&version_reply).unwrap().into(); + if decryption_key.backup_key_matches(&backup_info) { + machine.backup_machine().disable_backup().await.unwrap(); + let backup_key = decryption_key.megolm_v1_public_key(); + backup_key.set_version(current_version.version.clone()); + + machine.backup_machine().save_decryption_key(Some(decryption_key.to_owned()), Some(current_version.version)).await.unwrap(); + machine.backup_machine().enable_backup_v1(backup_key).await.unwrap(); + break; + } + } + machine.store().delete_secrets_from_inbox(&SecretName::RecoveryKey).await.unwrap(); + }) + } + pub(crate) fn backup_keys(&mut self) -> Box { + self.runtime.block_on(async { + let backup_request = self.machine.as_ref().unwrap().backup_machine().backup().await.unwrap(); + if let Some(backup_request) = backup_request { + Box::new(BackupRequest { + transaction_id: Some(backup_request.0), + request: Some(backup_request.1), + }) + } else { + Box::new(BackupRequest { + transaction_id: None, + request: None, + }) + } + }) + } + + pub(crate) fn export_keys(&self, passphrase: String) -> String { + use matrix_sdk_crypto::encrypt_room_key_export; + self.runtime.block_on(async { + encrypt_room_key_export(&self.machine.as_ref().unwrap().export_room_keys(|_| true).await.unwrap(), &passphrase, 500_000).unwrap() + }) + } + + pub(crate) fn import_keys(&self, passphrase: String, ciphertext: String) -> Vec { + use matrix_sdk_crypto::decrypt_room_key_export; + self.runtime.block_on(async { + let keys = decrypt_room_key_export(ciphertext.as_bytes(), &passphrase).unwrap(); + let keys = self.machine.as_ref().unwrap().store().import_exported_room_keys(keys, |_, _| {}).await.unwrap().keys; + let mut keys_vec = vec!(); + for room_id in keys.keys() { + for sender in keys[room_id].keys() { + for session_id in &keys[room_id][sender] { + keys_vec.push(Key { + room_id: room_id.to_string(), + session_id: session_id.to_owned(), + }); + } + } + } + keys_vec + }) + } + pub(crate) fn all_sessions_verified(&self, user_id: String) -> bool { + self.runtime.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let devices = self.machine.as_ref().unwrap().get_user_devices(&user_id, None).await.unwrap(); + let all_verified = devices.devices().all(|it| it.is_cross_signed_by_owner()); + all_verified + }) + } + + pub(crate) fn is_user_verified(&self, user_id: String) -> bool { + self.runtime.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let identity = self.machine.as_ref().unwrap().get_identity(&user_id, None).await.unwrap().unwrap(); + match identity { + UserIdentities::Own(own) => own.is_verified(), + UserIdentities::Other(other) => other.is_verified(), + } + }) + } +} + +pub(crate) struct BackupRequest { + transaction_id: Option, + request: Option, +} + +impl BackupRequest { + pub(crate) fn transaction_id(&self) -> String { + self.transaction_id.clone().unwrap().to_string() + } + pub(crate) fn version(&self) -> String { + self.request.clone().unwrap().version + } + pub(crate) fn rooms(&self) -> String { + serde_json::to_string(&self.request.clone().unwrap().rooms).unwrap() + } + pub(crate) fn has_request(&self) -> bool { + self.request.is_some() + } +} diff --git a/Quotient/crypto-sdk/src/encryption_info.rs b/Quotient/crypto-sdk/src/encryption_info.rs new file mode 100644 index 000000000..f2a117749 --- /dev/null +++ b/Quotient/crypto-sdk/src/encryption_info.rs @@ -0,0 +1,11 @@ +use matrix_sdk_common::deserialized_responses::VerificationState; + +pub(crate) struct EncryptionInfo( + pub(crate) matrix_sdk_common::deserialized_responses::EncryptionInfo, +); + +impl EncryptionInfo { + pub(crate) fn is_verified(&self) -> bool { + matches!(self.0.verification_state, VerificationState::Verified) + } +} diff --git a/Quotient/crypto-sdk/src/file_crypto.rs b/Quotient/crypto-sdk/src/file_crypto.rs new file mode 100644 index 000000000..ea0e4beeb --- /dev/null +++ b/Quotient/crypto-sdk/src/file_crypto.rs @@ -0,0 +1,71 @@ +use std::collections::BTreeMap; + +pub(crate) struct MediaEncryptionInfo { + pub(crate) info: matrix_sdk_crypto::MediaEncryptionInfo, + pub(crate) ciphertext: Vec, +} + +impl MediaEncryptionInfo { + pub(crate) fn media_encryption_info_bytes(&self) -> Vec { + self.ciphertext.clone() + } + + pub(crate) fn media_encryption_info_iv(&self) -> String { + self.info.iv.to_string() + } + + pub(crate) fn media_encryption_info_hash(&self) -> String { + self.info.hashes["sha256"].to_string() + } + + pub(crate) fn media_encryption_info_key(&self) -> String { + self.info.key.k.to_string() + } +} + +pub(crate) fn encrypt_file(bytes: &mut [u8]) -> Box { + use matrix_sdk_crypto::AttachmentEncryptor; + use std::io::Read; + let mut binding = bytes.as_ref(); + let mut encryptor = AttachmentEncryptor::new(&mut binding); + let mut ciphertext = vec![]; + let _ = encryptor.read_to_end(&mut ciphertext).unwrap(); + Box::new(MediaEncryptionInfo { + info: encryptor.finish(), + ciphertext, + }) +} + +pub(crate) fn decrypt_file(bytes: &mut [u8], iv: String, key: String, hash: String) -> Vec { + use matrix_sdk_common::ruma::events::room::JsonWebKey; + use matrix_sdk_common::ruma::events::room::JsonWebKeyInit; + use matrix_sdk_common::ruma::serde::Base64; + use matrix_sdk_crypto::AttachmentDecryptor; + use std::io::Read; + let mut binding = bytes.as_ref(); + if let Ok(mut decryptor) = AttachmentDecryptor::new( + &mut binding, + matrix_sdk_crypto::MediaEncryptionInfo { + version: "v2".to_string(), + key: JsonWebKey::from(JsonWebKeyInit { + alg: "A256CTR".to_string(), + ext: true, + k: Base64::parse(key).unwrap(), + key_ops: vec!["encrypt".to_string(), "decrypt".to_string()], + kty: "oct".to_string(), + }), + iv: Base64::parse(iv).unwrap(), + hashes: { + let mut hashes: BTreeMap = BTreeMap::new(); + hashes.insert("sha256".to_string(), Base64::parse(hash).unwrap()); + hashes + }, + }, + ) { + let mut data = vec![]; + decryptor.read_to_end(&mut data).unwrap(); + data + } else { + vec![] + } +} diff --git a/Quotient/crypto-sdk/src/lib.rs b/Quotient/crypto-sdk/src/lib.rs new file mode 100644 index 000000000..a548535c3 --- /dev/null +++ b/Quotient/crypto-sdk/src/lib.rs @@ -0,0 +1,300 @@ +use std::{mem::ManuallyDrop, path::Path}; + +use matrix_sdk_common::ruma::{DeviceId, UserId}; + +use matrix_sdk_crypto::OlmMachine; + +use matrix_sdk_sqlite::SqliteCryptoStore; + +mod cryptomachine; +mod encryption_info; +mod file_crypto; +mod request; +mod verification; + +use cryptomachine::CryptoMachine; +use cryptomachine::SyncChanges; +use cryptomachine::Key; + +fn init( + user_id: String, + device_id: String, + path: String, + pickle_key: String, +) -> Box { + // This will fail when initializing a second account, but that's ok + let _ = tracing_subscriber::fmt().try_init(); + let rt = tokio::runtime::Runtime::new().unwrap(); + let _ = rt.enter(); + + let machine = rt.block_on(async { + let user_id = UserId::parse(user_id).unwrap(); + let device_id: Box = device_id.into(); + + let store = SqliteCryptoStore::open(Path::new(&path), Some(&pickle_key)) + .await + .unwrap(); + + let machine = OlmMachine::with_store(&user_id, &device_id, store) + .await + .expect("Failed to load crypto database"); + + let state = machine.cross_signing_status().await; + + println!("Has: {} {} {} {}", state.has_master, state.has_self_signing, state.has_user_signing, machine.backup_machine().get_backup_keys().await.unwrap().decryption_key.is_some()); + + machine + }); + + Box::new(CryptoMachine { + runtime: rt, + machine: Some(ManuallyDrop::new(machine)), + }) +} + +use encryption_info::EncryptionInfo; +use file_crypto::decrypt_file; +use file_crypto::encrypt_file; +use file_crypto::MediaEncryptionInfo; +use request::{ + ConfirmRequests, KeyVerificationRequest, KeysClaimRequest, OutgoingKeyVerificationRequest, + OutgoingRequest, ToDeviceRequest, +}; +use verification::{CreatedSession, Emoji}; +use cryptomachine::BackupRequest; + +#[cxx::bridge] +mod ffi { + #[namespace = "crypto"] + extern "Rust" { + type CryptoMachine; + type OutgoingRequest; + type ToDeviceRequest; + type KeysClaimRequest; + type KeyVerificationRequest; + type OutgoingKeyVerificationRequest; + type Emoji; + type CreatedSession; + type EncryptionInfo; + type MediaEncryptionInfo; + type ConfirmRequests; + type BackupRequest; + type SyncChanges; + type Key; + + // General CryptoMachine functions + fn init( + user_id: String, + device_id: String, + path: String, + pickle_key: String, + ) -> Box; + fn outgoing_requests(self: &CryptoMachine) -> Vec; + fn receive_sync_changes( + self: &mut CryptoMachine, + sync_json: String, + ) -> Box; + fn receive_verification_event(self: &mut CryptoMachine, full_json: String); + + // Mark requests as sent + fn mark_keys_upload_as_sent(self: &mut CryptoMachine, request_id: String, response: String); + fn mark_keys_query_as_sent(self: &mut CryptoMachine, request_id: String, response: String); + fn mark_keys_claim_as_sent(self: &mut CryptoMachine, request_id: String, response: String); + fn mark_to_device_as_sent(self: &mut CryptoMachine, request_id: String, response: String); + fn mark_room_message_as_sent( + self: &mut CryptoMachine, + request_id: String, + response: String, + ); + fn mark_signature_upload_as_sent( + self: &mut CryptoMachine, + request_id: String, + response: String, + ); + fn mark_keys_backup_as_sent(self: &mut CryptoMachine, response: String, request_id: String); + + // Specific CryptoMachine functions + fn share_room_key( + self: &mut CryptoMachine, + room_id: String, + user_ids: Vec, + only_trusted: bool, + visibility: u8, + ) -> Vec; + fn get_missing_sessions( + self: &mut CryptoMachine, + user_ids: Vec, + ) -> Box; + fn encrypt_room_event( + self: &mut CryptoMachine, + room_id: String, + content: String, + matrix_type: String, + ) -> String; + fn decrypt_room_event(self: &mut CryptoMachine, room_id: String, json: String) -> String; + fn update_tracked_users(self: &mut CryptoMachine, user_ids: Vec); + fn request_device_verification( + self: &mut CryptoMachine, + user_id: String, + device_id: String, + ) -> Box; + fn request_user_verification( + self: &mut CryptoMachine, + user_id: String, + room_id: String, + event_id: String, + ) -> Box; + fn request_user_verification_content(self: &mut CryptoMachine, user_id: String) -> String; + fn get_room_event_encryption_info( + self: &CryptoMachine, + event: String, + room_id: String, + ) -> Box; + + // Gettings parts of an OutgoingRequest + fn id(self: &OutgoingRequest) -> String; + fn keys_upload_device_keys(self: &OutgoingRequest) -> String; + fn keys_upload_one_time_keys(self: &OutgoingRequest) -> String; + fn keys_upload_fallback_keys(self: &OutgoingRequest) -> String; + fn keys_query_device_keys(self: &OutgoingRequest) -> String; + fn keys_query_timeout(self: &OutgoingRequest) -> usize; + fn keys_claim_one_time_keys(self: &OutgoingRequest) -> String; + fn to_device_event_type(self: &OutgoingRequest) -> String; + fn to_device_messages(self: &OutgoingRequest) -> String; + fn upload_signature_signed_keys(self: &OutgoingRequest) -> String; + fn request_type(self: &OutgoingRequest) -> u8; + fn room_msg_content(self: &OutgoingRequest) -> String; + fn room_msg_room_id(self: &OutgoingRequest) -> String; + fn room_msg_matrix_type(self: &OutgoingRequest) -> String; + fn to_device_txn_id(self: &OutgoingRequest) -> String; + fn room_msg_txn_id(self: &OutgoingRequest) -> String; + + fn timeout(self: &KeysClaimRequest) -> usize; + fn id(self: &KeysClaimRequest) -> String; + fn one_time_keys(self: &KeysClaimRequest) -> String; + + fn event_type(self: &ToDeviceRequest) -> String; + fn txn_id(self: &ToDeviceRequest) -> String; + fn messages(self: &ToDeviceRequest) -> String; + + fn accept_verification( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Box; + fn confirm_verification( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Box; + fn start_sas( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Box; + fn accept_sas( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Box; + fn verification_get_state( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> u8; + fn sas_get_state( + self: &mut CryptoMachine, + remote_user: String, + verification_id: String, + ) -> u8; + fn remote_user_id(self: &KeyVerificationRequest) -> String; + fn remote_device_id(self: &KeyVerificationRequest) -> String; + fn verification_id(self: &KeyVerificationRequest) -> String; + + fn to_device_event_type(self: &OutgoingKeyVerificationRequest) -> String; + fn to_device_messages(self: &OutgoingKeyVerificationRequest) -> String; + fn in_room_room_id(self: &OutgoingKeyVerificationRequest) -> String; + fn to_device_txn_id(self: &OutgoingKeyVerificationRequest) -> String; + fn in_room_txn_id(self: &OutgoingKeyVerificationRequest) -> String; + fn in_room_content(self: &OutgoingKeyVerificationRequest) -> String; + fn in_room_event_type(self: &OutgoingKeyVerificationRequest) -> String; + + fn sas_emoji( + self: &CryptoMachine, + remote_user: String, + verification_id: String, + ) -> Vec; + fn symbol(self: &Emoji) -> String; + fn description(self: &Emoji) -> String; + + fn to_device_event_type(self: &CreatedSession) -> String; + fn to_device_txn_id(self: &CreatedSession) -> String; + fn to_device_messages(self: &CreatedSession) -> String; + fn verification_id(self: &CreatedSession) -> String; + + fn is_verified(self: &EncryptionInfo) -> bool; + + fn encrypt_file(bytes: &mut [u8]) -> Box; + fn media_encryption_info_bytes(self: &MediaEncryptionInfo) -> Vec; + fn media_encryption_info_iv(self: &MediaEncryptionInfo) -> String; + fn media_encryption_info_key(self: &MediaEncryptionInfo) -> String; + fn media_encryption_info_hash(self: &MediaEncryptionInfo) -> String; + + fn decrypt_file(bytes: &mut [u8], iv: String, key: String, hash: String) -> Vec; + + fn import_from_backup( + self: &mut CryptoMachine, + response: String, + ); + + fn load_secrets( + self: &mut CryptoMachine, + passphrase: String, + key_id: String, + iterations: u32, + salt: String, + iv: String, + mac: String, + backup_key_iv: String, + backup_key_cipher: String, + backup_key_mac: String, + master_key_iv: String, + master_key_cipher: String, + master_key_mac: String, + self_key_iv: String, + self_key_cipher: String, + self_key_mac: String, + user_key_iv: String, + user_key_cipher: String, + user_key_mac: String, + version: String + ) -> String; + + fn verification_requests(self: &ConfirmRequests) -> Vec; + fn has_signature_request(self: &ConfirmRequests) -> bool; + fn signature_request_content(self: &ConfirmRequests) -> String; + fn request_self_verification(self: &mut CryptoMachine) -> Box; + fn request_secrets_from_devices(self: &mut CryptoMachine); + + fn has_pending_backup_key(self: &CryptoMachine) -> bool; + fn initialize_existing_backup(self: &mut CryptoMachine, version_reply: String); + fn backup_keys(self: &mut CryptoMachine) -> Box; + + fn has_request(self: &BackupRequest) -> bool; + fn rooms(self: &BackupRequest) -> String; + fn transaction_id(self: &BackupRequest) -> String; + fn version(self: &BackupRequest) -> String; + + fn sessions(self: &SyncChanges) -> Vec; + fn keys(self: &SyncChanges) -> Vec; + + fn session_id(self: &Key) -> String; + fn room_id(self: &Key) -> String; + + fn export_keys(self: &CryptoMachine, passphrase: String) -> String; + fn import_keys(self: &CryptoMachine, passphrase: String, ciphertext: String) -> Vec; + fn all_sessions_verified(self: &CryptoMachine, user_id: String) -> bool; + fn is_user_verified(self: &CryptoMachine, user_id: String) -> bool; + + } +} diff --git a/Quotient/crypto-sdk/src/request.rs b/Quotient/crypto-sdk/src/request.rs new file mode 100644 index 000000000..a3b881850 --- /dev/null +++ b/Quotient/crypto-sdk/src/request.rs @@ -0,0 +1,318 @@ +use std::{collections::BTreeMap, time::Duration}; + +use matrix_sdk_common::ruma::{ + api::client::keys::upload_signatures, + events::{AnyMessageLikeEventContent, AnyToDeviceEventContent}, + serde::Raw, + to_device::DeviceIdOrAllDevices, + DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, +}; +use matrix_sdk_crypto::{OutgoingRequests, OutgoingVerificationRequest, VerificationRequest}; + +#[derive(Clone)] +pub(crate) struct OutgoingKeyVerificationRequest(pub(crate) OutgoingVerificationRequest); + +impl OutgoingKeyVerificationRequest { + //TODO type + + //TODO: deduplicate with other things + pub(crate) fn to_device_event_type(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.0 { + request.event_type.to_string() + } else { + panic!() + } + } + + pub(crate) fn to_device_txn_id(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.0 { + request.txn_id.to_string() + } else { + panic!() + } + } + + pub(crate) fn to_device_messages(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.0 { + serde_json::to_string(&request.messages).unwrap() + } else { + panic!() + } + } + + pub(crate) fn in_room_room_id(&self) -> String { + if let OutgoingVerificationRequest::InRoom(request) = &self.0 { + request.room_id.to_string() + } else { + panic!() + } + } + + pub(crate) fn in_room_txn_id(&self) -> String { + if let OutgoingVerificationRequest::InRoom(request) = &self.0 { + request.txn_id.to_string() + } else { + panic!() + } + } + + pub(crate) fn in_room_content(&self) -> String { + if let OutgoingVerificationRequest::InRoom(request) = &self.0 { + serde_json::to_string(&request.content).unwrap() + } else { + panic!() + } + } + + pub(crate) fn in_room_event_type(&self) -> String { + if let OutgoingVerificationRequest::InRoom(request) = &self.0 { + match request.content { + AnyMessageLikeEventContent::KeyVerificationReady(_) => "m.key.verification.ready", + AnyMessageLikeEventContent::KeyVerificationStart(_) => "m.key.verification.start", + AnyMessageLikeEventContent::KeyVerificationCancel(_) => "m.key.verification.cancel", + AnyMessageLikeEventContent::KeyVerificationAccept(_) => "m.key.verification.accept", + AnyMessageLikeEventContent::KeyVerificationKey(_) => "m.key.verification.key", + AnyMessageLikeEventContent::KeyVerificationMac(_) => "m.key.verification.mac", + AnyMessageLikeEventContent::KeyVerificationDone(_) => "m.key.verification.done", + AnyMessageLikeEventContent::RoomEncrypted(_) => "m.room.encrypted", + _ => { + println!( + "Requesting to send unexpected event type {:?}", + request.content + ); + Default::default() + } + } + .to_string() + } else { + panic!() + } + } +} + +pub(crate) struct KeysClaimRequest { + pub(crate) id: String, + pub(crate) timeout: Option, + pub(crate) one_time_keys: BTreeMap>, +} + +impl KeysClaimRequest { + pub(crate) fn id(&self) -> String { + self.id.clone() + } + pub(crate) fn timeout(&self) -> usize { + self.timeout.unwrap_or(Duration::from_millis(0)).as_millis() as usize + } + pub(crate) fn one_time_keys(&self) -> String { + serde_json::to_string(&self.one_time_keys).unwrap() + } +} + +pub(crate) struct ToDeviceRequest { + pub(crate) event_type: String, + pub(crate) txn_id: String, + pub(crate) messages: + BTreeMap>>, +} + +impl ToDeviceRequest { + pub(crate) fn event_type(&self) -> String { + self.event_type.clone() + } + pub(crate) fn txn_id(&self) -> String { + self.txn_id.clone() + } + pub(crate) fn messages(&self) -> String { + serde_json::to_string(&self.messages).unwrap() + } +} + +pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::requests::OutgoingRequest); + +impl OutgoingRequest { + pub(crate) fn id(&self) -> String { + self.0.request_id().to_string() + } + + pub(crate) fn request_type(&self) -> u8 { + match self.0.request() { + OutgoingRequests::KeysUpload(_) => 0, + OutgoingRequests::KeysQuery(_) => 1, + OutgoingRequests::KeysClaim(_) => 2, + OutgoingRequests::ToDeviceRequest(_) => 3, + OutgoingRequests::SignatureUpload(_) => 4, + OutgoingRequests::RoomMessage(_) => 5, + OutgoingRequests::KeysBackup(_) => 6, + } + } + + pub(crate) fn keys_upload_device_keys(&self) -> String { + if let OutgoingRequests::KeysUpload(request) = self.0.request() { + serde_json::to_string(&request.device_keys).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn keys_upload_fallback_keys(&self) -> String { + if let OutgoingRequests::KeysUpload(request) = self.0.request() { + serde_json::to_string(&request.fallback_keys).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn keys_upload_one_time_keys(&self) -> String { + if let OutgoingRequests::KeysUpload(request) = self.0.request() { + serde_json::to_string(&request.one_time_keys).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn keys_query_device_keys(&self) -> String { + if let OutgoingRequests::KeysQuery(request) = self.0.request() { + serde_json::to_string(&request.device_keys).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn keys_query_timeout(&self) -> usize { + if let OutgoingRequests::KeysQuery(request) = self.0.request() { + request + .timeout + .unwrap_or(Duration::from_millis(0)) + .as_millis() as usize + } else { + Default::default() + } + } + + pub(crate) fn keys_claim_one_time_keys(&self) -> String { + if let OutgoingRequests::KeysClaim(request) = self.0.request() { + serde_json::to_string(&request.one_time_keys).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn to_device_event_type(&self) -> String { + if let OutgoingRequests::ToDeviceRequest(request) = self.0.request() { + request.event_type.to_string() + } else { + Default::default() + } + } + + pub(crate) fn to_device_txn_id(&self) -> String { + if let OutgoingRequests::ToDeviceRequest(request) = self.0.request() { + request.txn_id.to_string() + } else { + Default::default() + } + } + + pub(crate) fn to_device_messages(&self) -> String { + if let OutgoingRequests::ToDeviceRequest(request) = self.0.request() { + serde_json::to_string(&request.messages).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn room_msg_room_id(&self) -> String { + if let OutgoingRequests::RoomMessage(request) = self.0.request() { + request.room_id.to_string() + } else { + Default::default() + } + } + + pub(crate) fn room_msg_txn_id(&self) -> String { + if let OutgoingRequests::RoomMessage(request) = self.0.request() { + request.txn_id.to_string() + } else { + Default::default() + } + } + + pub(crate) fn room_msg_content(&self) -> String { + if let OutgoingRequests::RoomMessage(request) = self.0.request() { + serde_json::to_string(&request.content).unwrap() + } else { + Default::default() + } + } + + pub(crate) fn room_msg_matrix_type(&self) -> String { + if let OutgoingRequests::RoomMessage(request) = self.0.request() { + match request.content { + AnyMessageLikeEventContent::KeyVerificationReady(_) => "m.key.verification.ready", + AnyMessageLikeEventContent::KeyVerificationStart(_) => "m.key.verification.start", + AnyMessageLikeEventContent::KeyVerificationCancel(_) => "m.key.verification.cancel", + AnyMessageLikeEventContent::KeyVerificationAccept(_) => "m.key.verification.accept", + AnyMessageLikeEventContent::KeyVerificationKey(_) => "m.key.verification.key", + AnyMessageLikeEventContent::KeyVerificationMac(_) => "m.key.verification.mac", + AnyMessageLikeEventContent::KeyVerificationDone(_) => "m.key.verification.done", + AnyMessageLikeEventContent::RoomEncrypted(_) => "m.room.encrypted", + _ => { + println!( + "Requesting to send unexpected event type {:?}", + request.content + ); + Default::default() + } + } + .to_string() + } else { + Default::default() + } + } + + pub(crate) fn upload_signature_signed_keys(&self) -> String { + if let OutgoingRequests::SignatureUpload(request) = self.0.request() { + serde_json::to_string(&request.signed_keys).unwrap() + } else { + Default::default() + } + } +} + +impl ConfirmRequests { + pub(crate) fn has_signature_request(&self) -> bool { + self.signature.is_some() + } + + pub(crate) fn verification_requests(&self) -> Vec { + self.verification.clone() + } + + pub(crate) fn signature_request_content(&self) -> String { + serde_json::to_string(&self.signature.as_ref().unwrap().signed_keys).unwrap() + } +} + +pub(crate) struct ConfirmRequests { + pub(crate) verification: Vec, + pub(crate) signature: Option, +} + +#[derive(Clone)] +pub(crate) struct KeyVerificationRequest(pub(crate) VerificationRequest); + +impl KeyVerificationRequest { + pub(crate) fn remote_user_id(&self) -> String { + self.0.other_user().to_string() + } + pub(crate) fn remote_device_id(&self) -> String { + self.0 + .other_device_id() + .map(|it| it.to_string()) + .unwrap_or(Default::default()) //TODO: or panic? + } + + pub(crate) fn verification_id(&self) -> String { + self.0.flow_id().as_str().into() + } +} diff --git a/Quotient/crypto-sdk/src/verification.rs b/Quotient/crypto-sdk/src/verification.rs new file mode 100644 index 000000000..a6fd8c38a --- /dev/null +++ b/Quotient/crypto-sdk/src/verification.rs @@ -0,0 +1,46 @@ +use matrix_sdk_crypto::{OutgoingVerificationRequest, VerificationRequest}; + +pub(crate) struct CreatedSession( + pub(crate) VerificationRequest, + pub(crate) OutgoingVerificationRequest, +); + +impl CreatedSession { + pub(crate) fn verification_id(&self) -> String { + self.0.flow_id().as_str().to_string() + } + pub(crate) fn to_device_event_type(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.1 { + request.event_type.to_string() + } else { + Default::default() + } + } + + pub(crate) fn to_device_txn_id(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.1 { + request.txn_id.to_string() + } else { + Default::default() + } + } + + pub(crate) fn to_device_messages(&self) -> String { + if let OutgoingVerificationRequest::ToDevice(request) = &self.1 { + serde_json::to_string(&request.messages).unwrap() + } else { + Default::default() + } + } +} + +pub(crate) struct Emoji(pub(crate) matrix_sdk_crypto::Emoji); + +impl Emoji { + pub(crate) fn symbol(&self) -> String { + self.0.symbol.to_string() + } + pub(crate) fn description(&self) -> String { + self.0.description.to_string() + } +} diff --git a/Quotient/database.cpp b/Quotient/database.cpp deleted file mode 100644 index 5a2266d11..000000000 --- a/Quotient/database.cpp +++ /dev/null @@ -1,667 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Tobias Fella -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "database.h" - -#include "connection.h" -#include "logging_categories_p.h" - -#include "e2ee/qolmaccount.h" -#include "e2ee/qolminboundsession.h" -#include "e2ee/qolmoutboundsession.h" -#include "e2ee/qolmsession.h" -#include "e2ee/cryptoutils.h" - -#include -#include -#include -#include -#include - -using namespace Quotient; - -Database::Database(const QString& userId, const QString& deviceId, - PicklingKey&& picklingKey) - : m_userId(userId) - , m_deviceId(deviceId) - , m_picklingKey(std::move(picklingKey)) -{ - auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, "Quotient_"_L1 + m_userId); - auto dbDir = m_userId; - dbDir.replace(u':', u'_'); - const QString databasePath{ QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) - % u'/' % dbDir }; - QDir(databasePath).mkpath("."_L1); - db.setDatabaseName(databasePath + "/quotient_%1.db3"_L1.arg(m_deviceId)); - db.open(); // Further accessed via database() - - switch(version()) { - case 0: migrateTo1(); [[fallthrough]]; - case 1: migrateTo2(); [[fallthrough]]; - case 2: migrateTo3(); [[fallthrough]]; - case 3: migrateTo4(); [[fallthrough]]; - case 4: migrateTo5(); [[fallthrough]]; - case 5: migrateTo6(); [[fallthrough]]; - case 6: migrateTo7(); [[fallthrough]]; - case 7: migrateTo8(); [[fallthrough]]; - case 8: migrateTo9(); [[fallthrough]]; - case 9: migrateTo10(); - } -} - -int Database::version() -{ - auto query = execute(u"PRAGMA user_version;"_s); - if (query.next()) { - bool ok = false; - int value = query.value(0).toInt(&ok); - qCDebug(DATABASE) << "Database version" << value; - if (ok) - return value; - } else { - qCritical(DATABASE) << "Failed to check database version"; - } - return -1; -} - -QSqlQuery Database::execute(const QString& queryString) -{ - QSqlQuery query(queryString, database()); - if (query.lastError().type() != QSqlError::NoError) { - qCritical(DATABASE) << "Failed to execute query"; - qCritical(DATABASE) << query.lastQuery(); - qCritical(DATABASE) << query.lastError(); - } - return query; -} - -void Database::execute(QSqlQuery& query) -{ - if (!query.exec()) { - qCritical(DATABASE) << "Failed to execute query"; - qCritical(DATABASE) << query.lastQuery(); - qCritical(DATABASE) << query.lastError(); - } -} - -void Database::transaction() -{ - database().transaction(); -} - -void Database::commit() -{ - database().commit(); -} - -void Database::migrateTo1() -{ - qCDebug(DATABASE) << "Migrating database to version 1"; - transaction(); - execute(u"CREATE TABLE accounts (pickle TEXT);"_s); - execute(u"CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);"_s); - execute(u"CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"_s); - execute(u"CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"_s); - execute(u"CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);"_s); - execute(u"CREATE TABLE tracked_users (matrixId TEXT);"_s); - execute(u"CREATE TABLE outdated_users (matrixId TEXT);"_s); - execute(u"CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);"_s); - - execute(u"PRAGMA user_version = 1;"_s); - commit(); -} - -void Database::migrateTo2() -{ - qCDebug(DATABASE) << "Migrating database to version 2"; - transaction(); - - execute(u"ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT"_s); - execute(u"ALTER TABLE olm_sessions ADD lastReceived TEXT"_s); - - // Add indexes for improving queries speed on larger database - execute(u"CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)"_s); - execute(u"CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)"_s); - execute(u"CREATE INDEX inbound_room_idx ON inbound_megolm_sessions(roomId)"_s); - execute(u"CREATE INDEX group_session_idx ON group_session_record_index(roomId, sessionId, i)"_s); - execute(u"PRAGMA user_version = 2;"_s); - commit(); -} - -void Database::migrateTo3() -{ - qCDebug(DATABASE) << "Migrating database to version 3"; - transaction(); - - execute(u"CREATE TABLE inbound_megolm_sessions_temp AS SELECT roomId, sessionId, pickle FROM inbound_megolm_sessions;"_s); - execute(u"DROP TABLE inbound_megolm_sessions;"_s); - execute(u"ALTER TABLE inbound_megolm_sessions_temp RENAME TO inbound_megolm_sessions;"_s); - execute(u"ALTER TABLE inbound_megolm_sessions ADD olmSessionId TEXT;"_s); - execute(u"ALTER TABLE inbound_megolm_sessions ADD senderId TEXT;"_s); - execute(u"PRAGMA user_version = 3;"_s); - commit(); -} - -void Database::migrateTo4() -{ - qCDebug(DATABASE) << "Migrating database to version 4"; - transaction(); - - execute(u"CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);"_s); - execute(u"ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;"_s); - execute(u"ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;"_s); - execute(u"PRAGMA user_version = 4;"_s); - commit(); -} - -void Database::migrateTo5() -{ - qCDebug(DATABASE) << "Migrating database to version 5"; - transaction(); - - execute(u"ALTER TABLE tracked_devices ADD verified BOOL;"_s); - execute(u"PRAGMA user_version = 5"_s); - commit(); -} - -void Database::migrateTo6() -{ - qCDebug(DATABASE) << "Migrating database to version 6"; - transaction(); - - execute(u"CREATE TABLE encrypted (name TEXT, cipher TEXT, iv TEXT);"_s); - execute(u"PRAGMA user_version = 6"_s); - commit(); -} - -void Database::migrateTo7() -{ - qCDebug(DATABASE) << "Migrating database to version 7"; - transaction(); - execute(u"CREATE TABLE master_keys (userId TEXT, key TEXT, verified INTEGER);"_s); - execute(u"CREATE TABLE self_signing_keys (userId TEXT, key TEXT);"_s); - execute(u"CREATE TABLE user_signing_keys (userId TEXT, key TEXT);"_s); - execute(u"INSERT INTO outdated_users SELECT * FROM tracked_users;"_s); - execute(u"ALTER TABLE tracked_devices ADD selfVerified INTEGER;"_s); - execute(u"PRAGMA user_version = 7;"_s); - - commit(); -} - -void Database::migrateTo8() -{ - qCDebug(DATABASE) << "Migrating database to version 8"; - transaction(); - - execute(u"ALTER TABLE inbound_megolm_sessions ADD senderKey TEXT;"_s); - auto query = prepareQuery(u"SELECT sessionId, olmSessionId FROM inbound_megolm_sessions;"_s); - execute(query); - while (query.next()) { - if (query.value(u"olmSessionId"_s).toString().startsWith(u"BACKUP")) { - continue; - } - auto senderKeyQuery = prepareQuery(u"SELECT senderKey FROM olm_sessions WHERE sessionId=:olmSessionId;"_s); - senderKeyQuery.bindValue(u":olmSessionId"_s, query.value(u"olmSessionId"_s).toByteArray()); - execute(senderKeyQuery); - if (!senderKeyQuery.next()) { - continue; - } - auto updateQuery = prepareQuery(u"UPDATE inbound_megolm_sessions SET senderKey=:senderKey WHERE sessionId=:sessionId;"_s); - updateQuery.bindValue(u":sessionId"_s, query.value(u"sessionId"_s).toByteArray()); - updateQuery.bindValue(u":senderKey"_s, senderKeyQuery.value(u"senderKey"_s).toByteArray()); - - execute(updateQuery); - } - execute(u"PRAGMA user_version = 8;"_s); - commit(); -} - -void Database::migrateTo9() -{ - qCDebug(DATABASE) << "Migrating database to version 9"; - transaction(); - - auto query = prepareQuery(u"SELECT curveKey FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId;"_s); - query.bindValue(u":matrixId"_s, m_userId); - query.bindValue(u":deviceId"_s, m_deviceId); - execute(query); - if (!query.next()) { - return; - } - auto curveKey = query.value(u"curveKey"_s).toByteArray(); - query = prepareQuery(u"UPDATE inbound_megolm_sessions SET senderKey=:senderKey WHERE olmSessionId=:self;"_s); - query.bindValue(u":senderKey"_s, curveKey); - query.bindValue(u":self"_s, "SELF"_ba); - execute(u"PRAGMA user_version = 9;"_s); - execute(query); - commit(); -} - -void Database::migrateTo10() -{ - qCDebug(DATABASE) << "Migrating database to version 10"; - - transaction(); - - execute(u"ALTER TABLE inbound_megolm_sessions ADD senderClaimedEd25519Key TEXT;"_s); - - auto query = prepareQuery(u"SELECT DISTINCT senderKey FROM inbound_megolm_sessions;"_s); - execute(query); - - QStringList keys; - while (query.next()) { - keys += query.value(u"senderKey"_s).toString(); - } - for (const auto& key : keys) { - auto edKeyQuery = prepareQuery(u"SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"_s); - edKeyQuery.bindValue(u":curveKey"_s, key); - execute(edKeyQuery); - if (!edKeyQuery.next()) { - continue; - } - const auto &edKey = edKeyQuery.value(u"edKey"_s).toByteArray(); - - auto updateQuery = prepareQuery(u"UPDATE inbound_megolm_sessions SET senderClaimedEd25519Key=:senderClaimedEd25519Key WHERE senderKey=:senderKey;"_s); - updateQuery.bindValue(u":senderKey"_s, key.toLatin1()); - updateQuery.bindValue(u":senderClaimedEd25519Key"_s, edKey); - execute(updateQuery); - } - - execute(u"pragma user_version = 10"_s); - commit(); - -} - -void Database::storeOlmAccount(const QOlmAccount& olmAccount) -{ - auto deleteQuery = prepareQuery(u"DELETE FROM accounts;"_s); - auto query = prepareQuery(u"INSERT INTO accounts(pickle) VALUES(:pickle);"_s); - query.bindValue(u":pickle"_s, olmAccount.pickle(m_picklingKey)); - transaction(); - execute(deleteQuery); - execute(query); - commit(); -} - -std::optional Database::setupOlmAccount(QOlmAccount& olmAccount) -{ - auto query = prepareQuery(u"SELECT pickle FROM accounts;"_s); - execute(query); - if (query.next()) - return olmAccount.unpickle(query.value(u"pickle"_s).toByteArray(), m_picklingKey); - - olmAccount.setupNewAccount(); - return {}; -} - -void Database::clear() -{ - // SQLite driver only supports one query at a time, so feed them one by one - transaction(); - for (auto&& q : { u"DELETE FROM accounts;"_s, // @clang-format: one per line, plz - u"DELETE FROM olm_sessions;"_s, // - u"DELETE FROM inbound_megolm_sessions;"_s, // - u"DELETE FROM group_session_record_index;"_s, // - u"DELETE FROM master_keys;"_s, // - u"DELETE FROM self_signing_keys;"_s, // - u"DELETE FROM user_signing_keys;"_s }) - execute(q); - commit(); - -} - -void Database::saveOlmSession(const QByteArray& senderKey, - const QOlmSession& session, - const QDateTime& timestamp) -{ - auto query = prepareQuery(u"INSERT INTO olm_sessions(senderKey, sessionId, pickle, lastReceived) VALUES(:senderKey, :sessionId, :pickle, :lastReceived);"_s); - query.bindValue(u":senderKey"_s, senderKey); - query.bindValue(u":sessionId"_s, session.sessionId()); - query.bindValue(u":pickle"_s, session.pickle(m_picklingKey)); - query.bindValue(u":lastReceived"_s, timestamp); - transaction(); - execute(query); - commit(); -} - -std::unordered_map > Database::loadOlmSessions() -{ - auto query = prepareQuery(u"SELECT * FROM olm_sessions ORDER BY lastReceived DESC;"_s); - transaction(); - execute(query); - commit(); - std::unordered_map> sessions; - while (query.next()) { - if (auto&& expectedSession = - QOlmSession::unpickle(query.value(u"pickle"_s).toByteArray(), m_picklingKey)) { - sessions[query.value(u"senderKey"_s).toByteArray()].emplace_back( - std::move(*expectedSession)); - } else - qCWarning(E2EE) - << "Failed to unpickle olm session:" << expectedSession.error(); - } - return sessions; -} - -std::unordered_map Database::loadMegolmSessions( - const QString& roomId) -{ - auto query = prepareQuery(u"SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;"_s); - query.bindValue(u":roomId"_s, roomId); - transaction(); - execute(query); - commit(); - decltype(Database::loadMegolmSessions({})) sessions; - while (query.next()) { - if (auto&& expectedSession = QOlmInboundGroupSession::unpickle( - query.value(u"pickle"_s).toByteArray(), m_picklingKey)) { - const auto sessionId = query.value(u"sessionId"_s).toByteArray(); - if (const auto it = sessions.find(sessionId); it != sessions.end()) { - qCritical(DATABASE) << "More than one inbound group session " - "with the same session id" - << sessionId << "in the database"; - Q_ASSERT(false); - // For Release builds, take the last session found - qCritical(DATABASE) - << "The database is intact but only one session will be " - "used so some messages will be undecryptable"; - sessions.erase(it); - } - expectedSession->setOlmSessionId( - query.value(u"olmSessionId"_s).toByteArray()); - expectedSession->setSenderId(query.value(u"senderId"_s).toString()); - sessions.try_emplace(query.value(u"sessionId"_s).toByteArray(), - std::move(*expectedSession)); - } else - qCWarning(E2EE) << "Failed to unpickle megolm session:" - << expectedSession.error(); - } - return sessions; -} - -void Database::saveMegolmSession(const QString& roomId, - const QOlmInboundGroupSession& session, const QByteArray &senderKey, const QByteArray& senderClaimedEdKey) -{ - auto deleteQuery = prepareQuery(u"DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"_s); - deleteQuery.bindValue(u":roomId"_s, roomId); - deleteQuery.bindValue(u":sessionId"_s, session.sessionId()); - auto query = prepareQuery( - u"INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId, senderKey, senderClaimedEd25519Key) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId, :senderKey, :senderClaimedEd25519Key);"_s); - query.bindValue(u":roomId"_s, roomId); - query.bindValue(u":sessionId"_s, session.sessionId()); - query.bindValue(u":pickle"_s, session.pickle(m_picklingKey)); - query.bindValue(u":senderId"_s, session.senderId()); - query.bindValue(u":olmSessionId"_s, session.olmSessionId()); - query.bindValue(u":senderKey"_s, senderKey); - query.bindValue(u":senderClaimedEd25519Key"_s, senderClaimedEdKey); - transaction(); - execute(deleteQuery); - execute(query); - commit(); -} - -void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) -{ - auto query = prepareQuery(u"INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"_s); - query.bindValue(u":roomId"_s, roomId); - query.bindValue(u":sessionId"_s, sessionId); - query.bindValue(u":index"_s, index); - query.bindValue(u":eventId"_s, eventId); - query.bindValue(u":ts"_s, ts); - transaction(); - execute(query); - commit(); -} - -std::pair Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) -{ - auto query = prepareQuery(u"SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;"_s); - query.bindValue(u":roomId"_s, roomId); - query.bindValue(u":sessionId"_s, sessionId); - query.bindValue(u":index"_s, index); - transaction(); - execute(query); - commit(); - if (!query.next()) { - return {}; - } - return {query.value(u"eventId"_s).toString(), query.value(u"ts"_s).toLongLong()}; -} - -QSqlDatabase Database::database() const -{ - return QSqlDatabase::database("Quotient_"_L1 + m_userId); -} - -QSqlQuery Database::prepareQuery(const QString& queryString) const -{ - QSqlQuery query(database()); - query.prepare(queryString); - return query; -} - -void Database::clearRoomData(const QString& roomId) -{ - transaction(); - for (const auto& queryText : - { u"DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;"_s, - u"DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;"_s, - u"DELETE FROM group_session_record_index WHERE roomId=:roomId;"_s }) { - auto q = prepareQuery(queryText); - q.bindValue(u":roomId"_s, roomId); - execute(q); - } - commit(); -} - -void Database::setOlmSessionLastReceived(const QByteArray& sessionId, const QDateTime& timestamp) -{ - auto query = prepareQuery(u"UPDATE olm_sessions SET lastReceived=:lastReceived WHERE sessionId=:sessionId;"_s); - query.bindValue(u":lastReceived"_s, timestamp); - query.bindValue(u":sessionId"_s, sessionId); - transaction(); - execute(query); - commit(); -} - -void Database::saveCurrentOutboundMegolmSession(const QString& roomId, - const QOlmOutboundGroupSession& session) -{ - const auto pickle = session.pickle(m_picklingKey); - auto deleteQuery = prepareQuery( - u"DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"_s); - deleteQuery.bindValue(u":roomId"_s, roomId); - deleteQuery.bindValue(u":sessionId"_s, session.sessionId()); - - auto insertQuery = prepareQuery( - u"INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);"_s); - insertQuery.bindValue(u":roomId"_s, roomId); - insertQuery.bindValue(u":sessionId"_s, session.sessionId()); - insertQuery.bindValue(u":pickle"_s, pickle); - insertQuery.bindValue(u":creationTime"_s, session.creationTime()); - insertQuery.bindValue(u":messageCount"_s, session.messageCount()); - - transaction(); - execute(deleteQuery); - execute(insertQuery); - commit(); -} - -std::optional Database::loadCurrentOutboundMegolmSession( - const QString& roomId) -{ - auto query = prepareQuery( - u"SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;"_s); - query.bindValue(u":roomId"_s, roomId); - execute(query); - if (query.next()) { - if (auto&& session = - QOlmOutboundGroupSession::unpickle(query.value(u"pickle"_s).toByteArray(), - m_picklingKey)) { - session->setCreationTime(query.value(u"creationTime"_s).toDateTime()); - session->setMessageCount(query.value(u"messageCount"_s).toInt()); - return std::move(*session); - } - } - return {}; -} - -void Database::setDevicesReceivedKey( - const QString& roomId, - const QVector>& devices, - const QByteArray& sessionId, uint32_t index) -{ - transaction(); - for (const auto& [user, device, curveKey] : devices) { - auto query = prepareQuery(u"INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);"_s); - query.bindValue(u":roomId"_s, roomId); - query.bindValue(u":userId"_s, user); - query.bindValue(u":deviceId"_s, device); - query.bindValue(u":identityKey"_s, curveKey); - query.bindValue(u":sessionId"_s, sessionId); - query.bindValue(u":i"_s, index); - execute(query); - } - commit(); -} - -QMultiHash Database::devicesWithoutKey( - const QString& roomId, QMultiHash devices, - const QByteArray& sessionId) -{ - auto query = prepareQuery(u"SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId"_s); - query.bindValue(u":roomId"_s, roomId); - query.bindValue(u":sessionId"_s, sessionId); - transaction(); - execute(query); - commit(); - while (query.next()) - devices.remove(query.value(u"userId"_s).toString(), query.value(u"deviceId"_s).toString()); - - return devices; -} - -void Database::updateOlmSession(const QByteArray& senderKey, - const QOlmSession& session) -{ - auto query = prepareQuery( - u"UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;"_s); - query.bindValue(u":pickle"_s, session.pickle(m_picklingKey)); - query.bindValue(u":senderKey"_s, senderKey); - query.bindValue(u":sessionId"_s, session.sessionId()); - transaction(); - execute(query); - commit(); -} - -void Database::setSessionVerified(const QString& edKeyId) -{ - auto query = prepareQuery(u"UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;"_s); - query.bindValue(u":edKeyId"_s, edKeyId); - transaction(); - execute(query); - commit(); -} - -bool Database::isSessionVerified(const QString& edKey) -{ - auto query = prepareQuery(u"SELECT verified FROM tracked_devices WHERE edKey=:edKey"_s); - query.bindValue(u":edKey"_s, edKey); - execute(query); - return query.next() && query.value(u"verified"_s).toBool(); -} - -QString Database::edKeyForKeyId(const QString& userId, const QString& edKeyId) -{ - auto query = prepareQuery(u"SELECT edKey FROM tracked_devices WHERE matrixId=:userId and edKeyId=:edKeyId;"_s); - query.bindValue(u":matrixId"_s, userId); - query.bindValue(u":edKeyId"_s, edKeyId); - execute(query); - if (!query.next()) { - return {}; - } - return query.value(u"edKey"_s).toString(); -} - -void Database::storeEncrypted(const QString& name, const QByteArray& key) -{ - auto iv = getRandom(); - auto result = - aesCtr256Encrypt(key, asCBytes(m_picklingKey).first(), - iv); - if (!result.has_value()) - return; - - auto cipher = result.value().toBase64(); - auto query = prepareQuery(u"INSERT INTO encrypted(name, cipher, iv) VALUES(:name, :cipher, :iv);"_s); - auto deleteQuery = prepareQuery(u"DELETE FROM encrypted WHERE name=:name;"_s); - deleteQuery.bindValue(u":name"_s, name); - query.bindValue(u":name"_s, name); - query.bindValue(u":cipher"_s, cipher); - query.bindValue(u":iv"_s, iv.toBase64()); - transaction(); - execute(deleteQuery); - execute(query); - commit(); -} - -QByteArray Database::loadEncrypted(const QString& name) -{ - auto query = prepareQuery(u"SELECT cipher, iv FROM encrypted WHERE name=:name;"_s); - query.bindValue(u":name"_s, name); - execute(query); - if (!query.next()) { - return {}; - } - auto cipher = QByteArray::fromBase64(query.value(u"cipher"_s).toString().toLatin1()); - auto iv = QByteArray::fromBase64(query.value(u"iv"_s).toString().toLatin1()); - if (iv.size() < AesBlockSize) { - qCWarning(E2EE) << "Corrupt iv at the database record for" << name; - return {}; - } - - return aesCtr256Decrypt(cipher, asCBytes(m_picklingKey).first(), - asCBytes(iv)) - .move_value_or({}); -} - -void Database::setMasterKeyVerified(const QString& masterKey) -{ - auto query = prepareQuery(u"UPDATE master_keys SET verified=true WHERE key=:key;"_s); - query.bindValue(u":key"_s, masterKey); - transaction(); - execute(query); - commit(); -} - -QString Database::userSigningPublicKey() -{ - auto query = prepareQuery(u"SELECT key FROM user_signing_keys WHERE userId=:userId;"_s); - query.bindValue(u":userId"_s, m_userId); - execute(query); - return query.next() ? query.value(u"key"_s).toString() : QString(); -} - -QString Database::selfSigningPublicKey() -{ - auto query = prepareQuery(u"SELECT key FROM self_signing_keys WHERE userId=:userId;"_s); - query.bindValue(u":userId"_s, m_userId); - execute(query); - return query.next() ? query.value(u"key"_s).toString() : QString(); -} - -QString Database::edKeyForMegolmSession(const QString& sessionId) -{ - auto query = prepareQuery(u"SELECT senderClaimedEd25519Key FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_s); - query.bindValue(u":sessionId"_s, sessionId.toLatin1()); - execute(query); - return query.next() ? query.value(u"senderClaimedEd25519Key"_s).toString() : QString(); -} - -QString Database::senderKeyForMegolmSession(const QString& sessionId) -{ - auto query = prepareQuery(u"SELECT senderKey FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_s); - query.bindValue(u":sessionId"_s, sessionId.toLatin1()); - execute(query); - return query.next() ? query.value(u"senderKey"_s).toString() : QString(); -} diff --git a/Quotient/database.h b/Quotient/database.h deleted file mode 100644 index 4f23289ab..000000000 --- a/Quotient/database.h +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Tobias Fella -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include -#include - -#include - -#include "e2ee/e2ee_common.h" - -namespace Quotient { - -class QOlmAccount; -class QOlmSession; -class QOlmInboundGroupSession; -class QOlmOutboundGroupSession; - -class QUOTIENT_API Database -{ -public: - Database(const QString& userId, const QString& deviceId, - PicklingKey&& picklingKey); - - int version(); - void transaction(); - void commit(); - QSqlQuery execute(const QString &queryString); - void execute(QSqlQuery &query); - QSqlDatabase database() const; - QSqlQuery prepareQuery(const QString& queryString) const; - - void storeOlmAccount(const QOlmAccount& olmAccount); - std::optional setupOlmAccount(QOlmAccount &olmAccount); - void clear(); - void saveOlmSession(const QByteArray& senderKey, const QOlmSession& session, - const QDateTime& timestamp); - std::unordered_map> loadOlmSessions(); - std::unordered_map loadMegolmSessions( - const QString& roomId); - void saveMegolmSession(const QString& roomId, - const QOlmInboundGroupSession& session, - const QByteArray& senderKey, - const QByteArray& senderClaimedEdKey); - void addGroupSessionIndexRecord(const QString& roomId, - const QString& sessionId, uint32_t index, - const QString& eventId, qint64 ts); - std::pair groupSessionIndexRecord(const QString& roomId, - const QString& sessionId, - qint64 index); - void clearRoomData(const QString& roomId); - void setOlmSessionLastReceived(const QByteArray& sessionId, - const QDateTime& timestamp); - std::optional loadCurrentOutboundMegolmSession(const QString& roomId); - void saveCurrentOutboundMegolmSession( - const QString& roomId, const QOlmOutboundGroupSession& session); - void updateOlmSession(const QByteArray& senderKey, - const QOlmSession& session); - - // Returns a map UserId -> [DeviceId] that have not received key yet - QMultiHash devicesWithoutKey( - const QString& roomId, QMultiHash devices, - const QByteArray& sessionId); - // 'devices' contains tuples {userId, deviceId, curveKey} - void setDevicesReceivedKey( - const QString& roomId, - const QVector>& devices, - const QByteArray& sessionId, uint32_t index); - - bool isSessionVerified(const QString& edKey); - void setSessionVerified(const QString& edKeyId); - void setMasterKeyVerified(const QString& masterKey); - - QString edKeyForKeyId(const QString& userId, const QString& edKeyId); - void storeEncrypted(const QString& name, const QByteArray& key); - QByteArray loadEncrypted(const QString& name); - - QString userSigningPublicKey(); - QString selfSigningPublicKey(); - - QString edKeyForMegolmSession(const QString& sessionId); - QString senderKeyForMegolmSession(const QString& sessionId); - -private: - void migrateTo1(); - void migrateTo2(); - void migrateTo3(); - void migrateTo4(); - void migrateTo5(); - void migrateTo6(); - void migrateTo7(); - void migrateTo8(); - void migrateTo9(); - void migrateTo10(); - - QString m_userId; - QString m_deviceId; - PicklingKey m_picklingKey; -}; -} // namespace Quotient diff --git a/Quotient/e2ee/cryptoutils.cpp b/Quotient/e2ee/cryptoutils.cpp deleted file mode 100644 index 36741b02c..000000000 --- a/Quotient/e2ee/cryptoutils.cpp +++ /dev/null @@ -1,338 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later - -#include "cryptoutils.h" -#include "e2ee_common.h" - -#include "../logging_categories_p.h" -#include "../util.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -using namespace Quotient; - -// The checks below make sure the definitions in cryptoutils.h match those in -// OpenSSL headers - -static_assert(AesBlockSize == AES_BLOCK_SIZE); -static_assert(std::is_same_v); -static_assert(SslErrorUserOffset == ERR_LIB_USER); - -//! \brief A wrapper for `std::unique_ptr` for use with OpenSSL context functions -//! -//! This class and the deduction guide for it are merely to remove -//! the boilerplate necessary to pass custom deleter to `std::unique_ptr`. -//! Usage: `const ContextHolder ctx(CTX_new(), &CTX_free);`, where `CTX_new` and -//! `CTX_free` are the matching allocation and deallocation functions from -//! OpenSSL API. You can pass additional parameters to the allocation function -//! as needed; the deallocation function is assumed to take exactly one -//! parameter of the same type that is returned by the allocation function. -template -class ContextHolder : public std::unique_ptr { -public: - using std::unique_ptr::unique_ptr; -}; -template -ContextHolder(CryptoContext*, Deleter) -> ContextHolder; - -template - requires (sizeof(SizeT) >= sizeof(int)) -inline std::pair checkedSize( - SizeT uncheckedSize, - std::type_identity_t maxSize = std::numeric_limits::max()) -// ^ NB: usage of type_identity_t disables type deduction -{ - Q_ASSERT(uncheckedSize >= 0 && maxSize >= 0); - if (uncheckedSize <= maxSize) [[likely]] - return { static_cast(uncheckedSize), false }; - - qCCritical(E2EE) << "Cryptoutils:" << uncheckedSize - << "bytes is too many for OpenSSL, first" << maxSize - << "bytes will be taken"; - return { maxSize, true }; -} - -// NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay) -// TODO: remove NOLINT brackets once we're on clang-tidy 18 -#define CLAMP_SIZE(SizeVar_, ByteArray_, ...) \ - const auto [SizeVar_, ByteArray_##Clamped] = \ - checkedSize((ByteArray_).size() __VA_OPT__(, ) __VA_ARGS__); \ - if (QUO_ALARM_X(ByteArray_##Clamped, \ - u"" #ByteArray_ \ - " is %1 bytes long, too much for OpenSSL and overall suspicious"_s.arg( \ - (ByteArray_).size()))) \ - return SslPayloadTooLong; \ - do {} while (false) \ -// End of macro - -#define CALL_OPENSSL(Call_) \ - do { \ - if ((Call_) <= 0) { \ - qCWarning(E2EE) << std::source_location::current().function_name() \ - << "failed to call OpenSSL API:" \ - << ERR_error_string(ERR_get_error(), nullptr); \ - return ERR_get_error(); \ - } \ - } while (false) \ - // End of macro -// NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay) - -SslErrorCode Quotient::_impl::pbkdf2HmacSha512(const QByteArray& passphrase, const QByteArray& salt, - int iterations, byte_span_t<> output) -{ - CLAMP_SIZE(passphraseSize, passphrase); - CLAMP_SIZE(saltSize, salt); - CLAMP_SIZE(outputSize, output); - CALL_OPENSSL(PKCS5_PBKDF2_HMAC(passphrase.data(), passphraseSize, asCBytes(salt).data(), - saltSize, iterations, EVP_sha512(), outputSize, output.data())); - return 0; // OpenSSL doesn't have a special constant for success code :/ -} - -SslExpected Quotient::aesCtr256Encrypt(const QByteArray& plaintext, - byte_view_t key, - byte_view_t iv) -{ - CLAMP_SIZE(plaintextSize, plaintext); - - const ContextHolder ctx(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free); - if (QUO_ALARM_X(!ctx, QByteArrayLiteral("failed to create SSL context: ") - + ERR_error_string(ERR_get_error(), nullptr))) - return ERR_get_error(); - - QByteArray encrypted(plaintextSize + static_cast(iv.size()), - Qt::Uninitialized); - int encryptedLength = 0; - { - // Working with `encrypted` the span adaptor in this scope, avoiding reinterpret_casts - auto encryptedSpan = asWritableCBytes(encrypted); - fillFromSecureRng(encryptedSpan); // Now `encrypted` is initialised - constexpr auto mask = static_cast(~(1U << (63 / 8))); - encryptedSpan[15 - 63 % 8] &= mask; - - CALL_OPENSSL(EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, - key.data(), iv.data())); - - CALL_OPENSSL( - EVP_EncryptUpdate(ctx.get(), encryptedSpan.data(), &encryptedLength, - asCBytes(plaintext).data(), plaintextSize)); - Q_ASSERT(encryptedLength >= 0); - - int tailLength = -1; - CALL_OPENSSL(EVP_EncryptFinal_ex( - ctx.get(), - encryptedSpan.subspan(static_cast(encryptedLength)).data(), - &tailLength)); - Q_ASSERT_X(tailLength == 0, std::source_location::current().function_name(), - "Encryption finalizer returned non-zero-size tail - this should not happen with " - "AES CTR algorithm."); - encryptedLength += tailLength; // Recovery for Release builds - } - encrypted.resize(encryptedLength); - return encrypted; -} - -SslExpected Quotient::hkdfSha256(byte_view_t key, - byte_view_t<32> salt, byte_view_t<> info) -{ - CLAMP_SIZE(infoSize, info); - - HkdfKeys result; - const ContextHolder context(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), - &EVP_PKEY_CTX_free); - - CALL_OPENSSL(EVP_PKEY_derive_init(context.get())); - CALL_OPENSSL(EVP_PKEY_CTX_set_hkdf_md(context.get(), EVP_sha256())); - CALL_OPENSSL(EVP_PKEY_CTX_set1_hkdf_salt(context.get(), salt.data(), salt.size())); - CALL_OPENSSL(EVP_PKEY_CTX_set1_hkdf_key(context.get(), key.data(), key.size())); - CALL_OPENSSL(EVP_PKEY_CTX_add1_hkdf_info(context.get(), info.data(), infoSize)); - size_t outputLength = result.size(); - CALL_OPENSSL(EVP_PKEY_derive(context.get(), result.data(), &outputLength)); - if (outputLength != result.size()) { - qCCritical(E2EE) << "hkdfSha256: the shared secret is" << outputLength - << "bytes instead of" << result.size(); - Q_ASSERT(false); - return WrongDerivedKeyLength; - } - - return result; -} - -SslExpected Quotient::hmacSha256(byte_view_t hmacKey, - const QByteArray& data) -{ - unsigned int len = SHA256_DIGEST_LENGTH; - auto output = zeroedByteArray(SHA256_DIGEST_LENGTH); - if (HMAC(EVP_sha256(), hmacKey.data(), hmacKey.size(), asCBytes(data).data(), - unsignedSize(data), asWritableCBytes(output).data(), &len) - == nullptr) { - qWarning() << ERR_error_string(ERR_get_error(), nullptr); - return ERR_get_error(); - } - return output; -} - -SslExpected Quotient::aesCtr256Decrypt(const QByteArray& ciphertext, - byte_view_t key, - byte_view_t iv) -{ - CLAMP_SIZE(ciphertextSize, ciphertext); - - const ContextHolder context(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free); - if (!context) { - qCCritical(E2EE) - << "aesCtr256Decrypt() failed to create cipher context:" - << ERR_error_string(ERR_get_error(), nullptr); - Q_ASSERT(context); - return ERR_get_error(); - } - - auto decrypted = zeroedByteArray(ciphertextSize); - int decryptedLength = 0; - { - const auto decryptedSpan = asWritableCBytes(decrypted); - CALL_OPENSSL(EVP_DecryptInit_ex(context.get(), EVP_aes_256_ctr(), - nullptr, key.data(), iv.data())); - CALL_OPENSSL(EVP_DecryptUpdate(context.get(), decryptedSpan.data(), - &decryptedLength, - asCBytes(ciphertext).data(), - ciphertextSize)); - int tailLength = -1; - CALL_OPENSSL( - EVP_DecryptFinal_ex(context.get(), - decryptedSpan.subspan(static_cast(decryptedLength)).data(), - &tailLength)); - Q_ASSERT_X(tailLength == 0, std::source_location::current().function_name(), - "Decrypt operation finalizer returned non-zero-size tail - this should not " - "happen with AES CTR algorithm."); - decryptedLength += tailLength; - } - decrypted.resize(decryptedLength); - return decrypted; -} - -QOlmExpected Quotient::curve25519AesSha2Decrypt( - QByteArray ciphertext, const QByteArray& privateKey, - const QByteArray& ephemeral, const QByteArray& mac) -{ - auto context = makeCStruct(olm_pk_decryption, olm_pk_decryption_size, olm_clear_pk_decryption); - Q_ASSERT(context); - - // NB: The produced public key is not actually used, the call is only - // to fill the context with the private key for olm_pk_decrypt() - if (std::vector publicKey(olm_pk_key_length()); - olm_pk_key_from_private(context.get(), publicKey.data(), - publicKey.size(), privateKey.data(), - unsignedSize(privateKey)) - == olm_error()) - return olm_pk_decryption_last_error_code(context.get()); - - auto plaintext = byteArrayForOlm(olm_pk_max_plaintext_length(context.get(), unsignedSize(ciphertext))); - const auto resultSize = olm_pk_decrypt(context.get(), ephemeral.data(), unsignedSize(ephemeral), mac.data(), unsignedSize(mac), ciphertext.data(), unsignedSize(ciphertext), plaintext.data(), unsignedSize(plaintext)); - if (resultSize == olm_error()) - return olm_pk_decryption_last_error_code(context.get()); - - const auto checkedResultSize = checkedSize(resultSize).first; - plaintext.resize(checkedResultSize); - return plaintext; -} - -QOlmExpected Quotient::curve25519AesSha2Encrypt( - const QByteArray& plaintext, const QByteArray& publicKey) -{ - auto context = makeCStruct(olm_pk_encryption, olm_pk_encryption_size, - olm_clear_pk_encryption); - - if (olm_pk_encryption_set_recipient_key(context.get(), publicKey.data(), - unsignedSize(publicKey)) - == olm_error()) - return olm_pk_encryption_last_error_code(context.get()); - - auto ephemeral = byteArrayForOlm(olm_pk_key_length()); - auto mac = byteArrayForOlm(olm_pk_mac_length(context.get())); - auto ciphertext = byteArrayForOlm( - olm_pk_ciphertext_length(context.get(), unsignedSize(plaintext))); - - const auto randomLength = olm_pk_encrypt_random_length(context.get()); - if (olm_pk_encrypt(context.get(), plaintext.data(), unsignedSize(plaintext), - ciphertext.data(), unsignedSize(ciphertext), mac.data(), - unsignedSize(mac), ephemeral.data(), - unsignedSize(ephemeral), getRandom(randomLength).data(), - randomLength) - == olm_error()) - return olm_pk_encryption_last_error_code(context.get()); - - return Curve25519Encrypted { - .ciphertext = ciphertext, - .mac = mac, - .ephemeral = ephemeral, - }; -} - -std::vector Quotient::base58Decode(const QByteArray& encoded) -{ - // See https://spec.matrix.org/latest/client-server-api/#recovery-key - constexpr auto reverse_alphabet = []() consteval { - std::array init; // NOLINT(cppcoreguidelines-pro-type-member-init) - init.fill(0xFF); - const std::array alphabet{ - "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - }; - // NOLINTNEXTLINE(bugprone-too-small-loop-variable) - for (uint8_t i = 0; i < std::size(alphabet) - 1; ++i) { - init[alphabet[i]] = i; - } - return init; - }(); - - std::vector result; - result.reserve(unsignedSize(encoded) * 733 / 1000 + 1); - - for (const auto b : encoded) { - uint32_t carry = reverse_alphabet[static_cast(b)]; - for (auto& j : result) { - carry += j * 58; - j = carry & 0xFF; - carry /= 0x100; - } - while (carry > 0) { - result.push_back(carry & 0xFF); - carry /= 0x100; - } - } - - for (auto i = 0; i < encoded.size() && encoded[i] == '1'; ++i) { - result.push_back(0x0); - } - - std::reverse(result.begin(), result.end()); - return result; -} - -QByteArray Quotient::sign(const QByteArray& key, const QByteArray& data) -{ - auto context = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing); - QByteArray pubKey(olm_pk_signing_public_key_length(), 0); - olm_pk_signing_key_from_seed(context.get(), pubKey.data(), unsignedSize(pubKey), key.data(), - unsignedSize(key)); - Q_ASSERT(context); - - const auto signatureLength = olm_pk_signature_length(); - auto signatureBuffer = byteArrayForOlm(signatureLength); - - if (olm_pk_sign(context.get(), asCBytes(data).data(), unsignedSize(data), asWritableCBytes(signatureBuffer).data(), signatureLength) - == olm_error()) - QOLM_INTERNAL_ERROR_X("Failed to sign a message", olm_pk_signing_last_error(context.get())); - - return signatureBuffer; -} diff --git a/Quotient/e2ee/cryptoutils.h b/Quotient/e2ee/cryptoutils.h deleted file mode 100644 index b02687d81..000000000 --- a/Quotient/e2ee/cryptoutils.h +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later - -#pragma once - -#include "e2ee_common.h" -#include "../expected.h" - -#include "../quotient_export.h" - -#include -#include - -namespace Quotient { - -// Common remark: OpenSSL is a private dependency of libQuotient, meaning that -// headers of libQuotient can't include OpenSSL headers. Instead, alias -// the value or type along with a comment (see SslErrorCode e.g.) and add -// static_assert in the .cpp file to check it against the OpenSSL definition. - -constexpr auto DefaultPbkdf2KeyLength = 32u; -constexpr auto Aes256KeySize = 32u; -constexpr auto AesBlockSize = 16u; // AES_BLOCK_SIZE -constexpr auto HmacKeySize = 32u; - -struct QUOTIENT_API HkdfKeys - : public FixedBuffer { - //! \brief Key to be used for AES encryption / decryption - auto aes() const { return asCBytes(*this).first(); } - //! \brief Key to be used for MAC creation / verification - auto mac() const { return asCBytes(*this).last(); } -}; - -struct QUOTIENT_API Curve25519Encrypted { - QByteArray ciphertext; - QByteArray mac; - QByteArray ephemeral; -}; - -// NOLINTNEXTLINE(google-runtime-int): the type is copied from OpenSSL -using SslErrorCode = unsigned long; // decltype(ERR_get_error()) - -template -using key_material_t = std::array; -using key_view_t = byte_view_t; - -enum SslErrorCodes : SslErrorCode { - SslErrorUserOffset = 128, // ERR_LIB_USER; never use this bare - WrongDerivedKeyLength = SslErrorUserOffset + 1, - SslPayloadTooLong = SslErrorUserOffset + 2 -}; - -//! Same as QOlmExpected but for wrapping OpenSSL instead of Olm calls -template -using SslExpected = Expected; - -// TODO, 0.9: merge zeroedByteArray() into zeroes() and replace const QByteArray& with -// QByteArrayView where OpenSSL/Olm expect an array of signed chars - -inline QByteArray zeroedByteArray(QByteArray::size_type n = 32) { return { n, '\0' }; } - -template consteval std::array zeroes() { return {}; } - -namespace _impl { - QUOTIENT_API SslErrorCode pbkdf2HmacSha512(const QByteArray& passphrase, const QByteArray& salt, - int iterations, byte_span_t<> output); -} - -//! Generate a key out of the given passphrase -template -QUOTIENT_API inline SslExpected> pbkdf2HmacSha512(const QByteArray& passphrase, - const QByteArray& salt, - int iterations) -{ - key_material_t result; - if (auto code = _impl::pbkdf2HmacSha512(passphrase, salt, iterations, result); code != 0) - return code; - return result; -} - -//! \brief Derive a key from the input data using HKDF-SHA256 -//! -//! The info parameter should be either 0 or 32 bytes long -QUOTIENT_API SslExpected hkdfSha256(key_view_t key, byte_view_t<32> salt, - byte_view_t<> info); - -//! Calculate a MAC from the given key and data -QUOTIENT_API SslExpected hmacSha256( - byte_view_t hmacKey, - const QByteArray& data); - -//! \brief Decrypt the data using Curve25519-AES-Sha256 -//! \note ciphertext must be given as base64 -QUOTIENT_API QOlmExpected curve25519AesSha2Decrypt( - QByteArray ciphertext, const QByteArray& privateKey, - const QByteArray& ephemeral, const QByteArray& mac); - -//! \brief Encrypt the data using Curve25519-AES-Sha256 -//! \note publicKey must be given as base64 -QUOTIENT_API QOlmExpected curve25519AesSha2Encrypt( - const QByteArray& plaintext, const QByteArray& publicKey); - -//! \brief Encrypt data using AES-CTR-256 -//! -//! key and iv have a length of 32 bytes -QUOTIENT_API SslExpected aesCtr256Encrypt( - const QByteArray& plaintext, byte_view_t key, - byte_view_t iv); - -//! \brief Decrypt data using AES-CTR-256 -//! -//! key and iv have a length of 32 bytes -QUOTIENT_API SslExpected aesCtr256Decrypt( - const QByteArray& ciphertext, byte_view_t key, - byte_view_t iv); - -QUOTIENT_API std::vector base58Decode(const QByteArray& encoded); - -QUOTIENT_API QByteArray sign(const QByteArray &key, const QByteArray &data); - -} // namespace Quotient diff --git a/Quotient/e2ee/e2ee_common.h b/Quotient/e2ee/e2ee_common.h index 64bfb7161..c3521c2cd 100644 --- a/Quotient/e2ee/e2ee_common.h +++ b/Quotient/e2ee/e2ee_common.h @@ -15,8 +15,6 @@ #include #include -#include - namespace Quotient { constexpr inline auto AlgorithmKeyL = "algorithm"_L1; @@ -60,8 +58,8 @@ inline bool isSupportedAlgorithm(const QString& algorithm) #define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \ QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), lastError()) -template -using QOlmExpected = Expected; +// template +// using QOlmExpected = Expected; //! \brief Initialise a buffer object for use with Olm calls //! diff --git a/Quotient/e2ee/qolmaccount.cpp b/Quotient/e2ee/qolmaccount.cpp deleted file mode 100644 index 98303430d..000000000 --- a/Quotient/e2ee/qolmaccount.cpp +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmaccount.h" - -#include "../logging_categories_p.h" -#include "qolmsession.h" -#include "qolmutility.h" - -#include "../csapi/keys.h" - -#include - -#include - -#include - -using namespace Quotient; - -// Convert olm error to enum -OlmErrorCode QOlmAccount::lastErrorCode() const { - return olm_account_last_error_code(olmData); -} - -const char* QOlmAccount::lastError() const -{ - return olm_account_last_error(olmData); -} - -QOlmExpected QOlmAccount::createInbound( - QOlmMessage preKeyMessage, const QByteArray& theirIdentityKey) const -{ - if (preKeyMessage.type() != QOlmMessage::PreKey) { - qCCritical(E2EE) << "The message is not a pre-key; will try to create " - "the inbound session anyway"; - } - - QOlmSession session{}; - - // std::span has size_type that fits standard library and Olm, avoiding - // the warning noise about integer signedness/precision - const std::span oneTimeKeyMessageBuf{ preKeyMessage.begin(), - preKeyMessage.end() }; - const std::span theirIdentityKeyBuf{ theirIdentityKey.cbegin(), - theirIdentityKey.cend() }; - const auto error = - theirIdentityKey.isEmpty() - ? olm_create_inbound_session(session.olmData, olmData, - oneTimeKeyMessageBuf.data(), - oneTimeKeyMessageBuf.size()) - : olm_create_inbound_session_from(session.olmData, olmData, - theirIdentityKeyBuf.data(), - theirIdentityKeyBuf.size(), - oneTimeKeyMessageBuf.data(), - oneTimeKeyMessageBuf.size()); - - if (error == olm_error()) { - qCWarning(E2EE) << "Error when creating inbound session" - << session.lastError(); - return session.lastErrorCode(); - } - - return session; -} - -QOlmAccount::QOlmAccount(QString userId, QString deviceId, QObject* parent) - : QObject(parent) - , olmDataHolder(makeCStruct(olm_account, olm_account_size, olm_clear_account)) - , m_userId(std::move(userId)) - , m_deviceId(std::move(deviceId)) -{} - -void QOlmAccount::setupNewAccount() -{ - if (const auto randomLength = olm_create_account_random_length(olmData); - olm_create_account(olmData, getRandom(randomLength).data(), randomLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to setup a new account"); - - emit needsSave(); -} - -OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, const PicklingKey& key) -{ - if (olm_unpickle_account(olmData, key.data(), key.size(), pickled.data(), - unsignedSize(pickled)) - == olm_error()) { - // Probably log the user out since we have no way of getting to the keys - return lastErrorCode(); - } - return OLM_SUCCESS; -} - -QByteArray QOlmAccount::pickle(const PicklingKey& key) const -{ - const auto pickleLength = olm_pickle_account_length(olmData); - auto pickleBuffer = byteArrayForOlm(pickleLength); - if (olm_pickle_account(olmData, key.data(), key.size(), - pickleBuffer.data(), pickleLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to pickle Olm account "_L1 + accountId()); - - return pickleBuffer; -} - -IdentityKeys QOlmAccount::identityKeys() const -{ - const auto keyLength = olm_account_identity_keys_length(olmData); - auto keyBuffer = byteArrayForOlm(keyLength); - if (olm_account_identity_keys(olmData, keyBuffer.data(), keyLength) - == olm_error()) { - QOLM_INTERNAL_ERROR("Failed to get "_L1 % accountId() % " identity keys"_L1); - } - const auto key = QJsonDocument::fromJson(keyBuffer).object(); - return { key.value("curve25519"_L1).toString(), key.value("ed25519"_L1).toString() }; -} - -QByteArray QOlmAccount::sign(const QByteArray &message) const -{ - const auto signatureLength = olm_account_signature_length(olmData); - auto signatureBuffer = byteArrayForOlm(signatureLength); - - if (olm_account_sign(olmData, message.data(), unsignedSize(message), - signatureBuffer.data(), signatureLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to sign a message"); - - return signatureBuffer; -} - -QByteArray QOlmAccount::sign(const QJsonObject &message) const -{ - return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); -} - -QByteArray QOlmAccount::signIdentityKeys() const -{ - const auto keys = identityKeys(); - static const auto& Algorithms = toJson(SupportedAlgorithms); - return sign( - QJsonObject{ { u"algorithms"_s, Algorithms }, - { u"user_id"_s, m_userId }, - { u"device_id"_s, m_deviceId }, - { u"keys"_s, QJsonObject{ { "curve25519:"_L1 + m_deviceId, keys.curve25519 }, - { "ed25519:"_L1 + m_deviceId, keys.ed25519 } } } }); -} - -size_t QOlmAccount::maxNumberOfOneTimeKeys() const -{ - return olm_account_max_number_of_one_time_keys(olmData); -} - -size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) -{ - const auto randomLength = - olm_account_generate_one_time_keys_random_length(olmData, numberOfKeys); - const auto result = olm_account_generate_one_time_keys( - olmData, numberOfKeys, getRandom(randomLength).data(), randomLength); - - if (result == olm_error()) - QOLM_INTERNAL_ERROR("Failed to generate one-time keys for account "_L1 + accountId()); - - emit needsSave(); - return result; -} - -UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const -{ - const auto oneTimeKeyLength = olm_account_one_time_keys_length(olmData); - QByteArray oneTimeKeysBuffer(static_cast(oneTimeKeyLength), '\0'); - - if (olm_account_one_time_keys(olmData, oneTimeKeysBuffer.data(), - oneTimeKeyLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to obtain one-time keys for account"_L1 % accountId()); - - const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); - UnsignedOneTimeKeys oneTimeKeys; - fromJson(json, oneTimeKeys.keys); - return oneTimeKeys; -} - -OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const -{ - OneTimeKeys signedOneTimeKeys; - for (const auto& [keyId, key] : keys.curve25519().asKeyValueRange()) - signedOneTimeKeys.insert("signed_curve25519:"_L1 % keyId, - SignedOneTimeKey { - key, m_userId, m_deviceId, - sign(QJsonObject { { "key"_L1, key } }) }); - return signedOneTimeKeys; -} - -OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session) -{ - if (olm_remove_one_time_keys(olmData, session.olmData) == olm_error()) { - qWarning(E2EE).nospace() - << "Failed to remove one-time keys for session " - << session.sessionId() << ": "_L1 << lastError(); - return lastErrorCode(); - } - emit needsSave(); - return OLM_SUCCESS; -} - -DeviceKeys QOlmAccount::deviceKeys() const -{ - static const QStringList Algorithms(SupportedAlgorithms.cbegin(), - SupportedAlgorithms.cend()); - - const auto idKeys = identityKeys(); - return DeviceKeys{ - .userId = m_userId, - .deviceId = m_deviceId, - .algorithms = Algorithms, - .keys{ { "curve25519:"_L1 + m_deviceId, idKeys.curve25519 }, - { "ed25519:"_L1 + m_deviceId, idKeys.ed25519 } }, - .signatures{ { m_userId, - { { "ed25519:"_L1 + m_deviceId, - QString::fromLatin1(signIdentityKeys()) } } } } - }; -} - -UploadKeysJob* QOlmAccount::createUploadKeyRequest( - const UnsignedOneTimeKeys& oneTimeKeys) const -{ - return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys)); -} - -QOlmExpected QOlmAccount::createInboundSession( - const QOlmMessage& preKeyMessage) const -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return createInbound(preKeyMessage); -} - -QOlmExpected QOlmAccount::createInboundSessionFrom( - const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage) const -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return createInbound(preKeyMessage, theirIdentityKey); -} - -QOlmExpected QOlmAccount::createOutboundSession( - const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey) const -{ - QOlmSession olmOutboundSession{}; - if (const auto randomLength = - olm_create_outbound_session_random_length(olmOutboundSession.olmData); - olm_create_outbound_session(olmOutboundSession.olmData, olmData, theirIdentityKey.data(), - unsignedSize(theirIdentityKey), theirOneTimeKey.data(), - unsignedSize(theirOneTimeKey), getRandom(randomLength).data(), - randomLength) - == olm_error()) { - const auto errorCode = olmOutboundSession.lastErrorCode(); - QOLM_FAIL_OR_LOG_X(errorCode == OLM_NOT_ENOUGH_RANDOM, - "Failed to create an outbound Olm session"_L1, - olmOutboundSession.lastError()); - return errorCode; - } - return olmOutboundSession; -} - -void QOlmAccount::markKeysAsPublished() -{ - olm_account_mark_keys_as_published(olmData); - emit needsSave(); -} - -bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys, - const QString& deviceId, - const QString& userId) -{ - const auto signKeyId = "ed25519:"_L1 + deviceId; - const auto signingKey = deviceKeys.keys[signKeyId]; - const auto signature = deviceKeys.signatures[userId][signKeyId]; - - 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"_L1); - obj1.remove("signatures"_L1); - - auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); - - QByteArray signingKeyBuf = signingKey.toUtf8(); - QOlmUtility utility; - auto signatureBuf = signature.toUtf8(); - return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); -} - -QString QOlmAccount::accountId() const { return m_userId % u'/' % m_deviceId; } diff --git a/Quotient/e2ee/qolmaccount.h b/Quotient/e2ee/qolmaccount.h deleted file mode 100644 index 1a40ee352..000000000 --- a/Quotient/e2ee/qolmaccount.h +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#pragma once - -#include -#include - -#include - -#include - -struct OlmAccount; - -namespace Quotient { - -class QOlmSession; - -//! An olm account manages all cryptographic keys used on a device. -//! \code{.cpp} -//! const auto olmAccount = new QOlmAccount(this); -//! \endcode -class QUOTIENT_API QOlmAccount : public QObject -{ - Q_OBJECT -public: - QOlmAccount(QString userId, QString deviceId, QObject* parent = nullptr); - - //! Creates a new instance of OlmAccount. During the instantiation - //! the Ed25519 fingerprint key pair and the Curve25519 identity key - //! pair are generated. - //! \sa https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption - //! \note This needs to be called before any other action or use unpickle() instead. - void setupNewAccount(); - - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. - //! \note This needs to be called before any other action or use setupNewAccount() instead. - [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled, - const PicklingKey& key); - - //! Serialises an OlmAccount to encrypted Base64. - QByteArray pickle(const PicklingKey& key) const; - - //! 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); - - //! Gets the OlmAccount's one time keys formatted as JSON. - UnsignedOneTimeKeys oneTimeKeys() const; - - //! Sign all one time keys. - OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; - - UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; - - DeviceKeys deviceKeys() const; - - //! Remove the one time key used to create the supplied session. - [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session); - - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - //! - //! \param preKeyMessage An Olm pre-key message that was encrypted for this account. - QOlmExpected createInboundSession( - const QOlmMessage& preKeyMessage) const; - - //! 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. - QOlmExpected createInboundSessionFrom( - const QByteArray& theirIdentityKey, - const QOlmMessage& preKeyMessage) const; - - //! Creates an outbound session for sending messages to a specific - /// identity and one time key. - QOlmExpected createOutboundSession( - const QByteArray& theirIdentityKey, - const QByteArray& theirOneTimeKey) const; - - void markKeysAsPublished(); - - OlmErrorCode lastErrorCode() const; - const char* lastError() const; - -Q_SIGNALS: - void needsSave(); - -private: - CStructPtr olmDataHolder; - QString m_userId; - QString m_deviceId; - OlmAccount* olmData = olmDataHolder.get(); - - QOlmExpected createInbound(QOlmMessage preKeyMessage, - const QByteArray &theirIdentityKey = "") const; - - QString accountId() const; -}; - -// TODO, 0.9: Move the two below to qolmutility.h - -QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, - const QString& deviceId, - const QString& userId); - -//! checks if the signature is signed by the signing_key -QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey, - const QJsonObject& obj, - const QString& signature); - -} // namespace Quotient diff --git a/Quotient/e2ee/qolminboundsession.cpp b/Quotient/e2ee/qolminboundsession.cpp deleted file mode 100644 index 8d53ccee5..000000000 --- a/Quotient/e2ee/qolminboundsession.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolminboundsession.h" - -#include "../logging_categories_p.h" - -#include - -#include -#include - -using namespace Quotient; - -OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const { - return olm_inbound_group_session_last_error_code(olmData); -} - -const char* QOlmInboundGroupSession::lastError() const -{ - return olm_inbound_group_session_last_error(olmData); -} - -QOlmInboundGroupSession::QOlmInboundGroupSession() - : m_groupSession(makeCStruct(olm_inbound_group_session, - olm_inbound_group_session_size, - olm_clear_inbound_group_session)) -{} - -QOlmExpected QOlmInboundGroupSession::create(const QByteArray& key) -{ - QOlmInboundGroupSession groupSession{}; - if (olm_init_inbound_group_session(groupSession.olmData, - std::bit_cast(key.constData()), - unsignedSize(key)) - == olm_error()) { - qWarning(E2EE) << "Failed to create an inbound group session:" - << groupSession.lastError(); - return groupSession.lastErrorCode(); - } - - return groupSession; -} - -QOlmExpected QOlmInboundGroupSession::importSession(const QByteArray& key) -{ - QOlmInboundGroupSession groupSession{}; - if (olm_import_inbound_group_session(groupSession.olmData, - std::bit_cast(key.data()), - unsignedSize(key)) - == olm_error()) { - qWarning(E2EE) << "Failed to import an inbound group session:" - << groupSession.lastError(); - return groupSession.lastErrorCode(); - } - - return groupSession; -} - -QByteArray QOlmInboundGroupSession::pickle(const PicklingKey& key) const -{ - const auto pickleLength = olm_pickle_inbound_group_session_length(olmData); - auto pickledBuf = byteArrayForOlm(pickleLength); - if (olm_pickle_inbound_group_session(olmData, key.data(), key.size(), - pickledBuf.data(), pickleLength) - == olm_error()) { - QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session"); - } - return pickledBuf; -} - -QOlmExpected QOlmInboundGroupSession::unpickle( - QByteArray&& pickled, const PicklingKey& key) -{ - QOlmInboundGroupSession groupSession{}; - if (olm_unpickle_inbound_group_session(groupSession.olmData, key.data(), - key.size(), pickled.data(), - unsignedSize(pickled)) - == olm_error()) { - qWarning(E2EE) << "Failed to unpickle an inbound group session:" - << groupSession.lastError(); - return groupSession.lastErrorCode(); - } - - return groupSession; -} - -QOlmExpected> 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 - const auto plaintextLength = olm_group_decrypt_max_plaintext_length( - olmData, std::bit_cast(QByteArray(message).data()), unsignedSize(message)); - auto plaintextBuf = byteArrayForOlm(plaintextLength); - - const auto actualLength = - olm_group_decrypt(olmData, std::bit_cast(QByteArray(message).data()), - unsignedSize(message), std::bit_cast(plaintextBuf.data()), - plaintextLength, &messageIndex); - if (actualLength == olm_error()) { - qWarning(E2EE) << "Failed to decrypt the message:" << lastError(); - return lastErrorCode(); - } - - // actualLength cannot be more than plainTextLength because the resulting - // text would overflow the allocated memory; but it can be less, in theory - plaintextBuf.truncate(static_cast(actualLength)); - return std::pair{ plaintextBuf, messageIndex }; -} - -QOlmExpected QOlmInboundGroupSession::exportSession( - uint32_t messageIndex) -{ - const auto keyLength = olm_export_inbound_group_session_length(olmData); - auto keyBuf = byteArrayForOlm(keyLength); - if (olm_export_inbound_group_session(olmData, std::bit_cast(keyBuf.data()), keyLength, - messageIndex) - == olm_error()) { - QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, - "Failed to export the inbound group session"_L1); - return lastErrorCode(); - } - return keyBuf; -} - -uint32_t QOlmInboundGroupSession::firstKnownIndex() const -{ - return olm_inbound_group_session_first_known_index(olmData); -} - -QByteArray QOlmInboundGroupSession::sessionId() const -{ - const auto sessionIdLength = olm_inbound_group_session_id_length(olmData); - auto sessionIdBuf = byteArrayForOlm(sessionIdLength); - if (olm_inbound_group_session_id(olmData, std::bit_cast(sessionIdBuf.data()), - sessionIdLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to obtain the group session id"); - - return sessionIdBuf; -} - -bool QOlmInboundGroupSession::isVerified() const -{ - return olm_inbound_group_session_is_verified(olmData) != 0; -} - -QByteArray QOlmInboundGroupSession::olmSessionId() const -{ - return m_olmSessionId; -} -void QOlmInboundGroupSession::setOlmSessionId(const QByteArray& newOlmSessionId) -{ - m_olmSessionId = newOlmSessionId; -} - -QString QOlmInboundGroupSession::senderId() const -{ - return m_senderId; -} -void QOlmInboundGroupSession::setSenderId(const QString& senderId) -{ - m_senderId = senderId; -} diff --git a/Quotient/e2ee/qolminboundsession.h b/Quotient/e2ee/qolminboundsession.h deleted file mode 100644 index 6aa3cd5e4..000000000 --- a/Quotient/e2ee/qolminboundsession.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -struct OlmInboundGroupSession; - -namespace Quotient { - -//! An in-bound group session is responsible for decrypting incoming -//! communication in a Megolm session. -class QUOTIENT_API QOlmInboundGroupSession -{ -public: - //! Creates a new instance of `OlmInboundGroupSession`. - static QOlmExpected create(const QByteArray& key); - //! Import an inbound group session, from a previous export. - static QOlmExpected importSession( - const QByteArray& key); - //! Serialises an `OlmInboundGroupSession` to encrypted Base64. - QByteArray pickle(const PicklingKey& key) const; - //! Deserialises from encrypted Base64 that was previously obtained by pickling - //! an `OlmInboundGroupSession`. - static QOlmExpected unpickle( - QByteArray&& pickled, const PicklingKey& key); - //! Decrypts ciphertext received for this group session. - QOlmExpected > 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. - QOlmExpected 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; - - //! The olm session that this session was received from. - //! Required to get the device this session is from. - QByteArray olmSessionId() const; - void setOlmSessionId(const QByteArray& newOlmSessionId); - - //! The sender of this session. - QString senderId() const; - void setSenderId(const QString& senderId); - - OlmErrorCode lastErrorCode() const; - const char* lastError() const; - -private: - QOlmInboundGroupSession(); - CStructPtr m_groupSession; - QByteArray m_olmSessionId; - QString m_senderId; - OlmInboundGroupSession* olmData = m_groupSession.get(); -}; - -using QOlmInboundGroupSessionPtr = std::unique_ptr; -} // namespace Quotient diff --git a/Quotient/e2ee/qolmmessage.cpp b/Quotient/e2ee/qolmmessage.cpp deleted file mode 100644 index 9618626a7..000000000 --- a/Quotient/e2ee/qolmmessage.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmmessage.h" - -#include - -using namespace Quotient; - -QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) - : QByteArray(std::move(ciphertext)) - , m_messageType(type) -{ - Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); -} - -QOlmMessage::Type QOlmMessage::type() const -{ - return m_messageType; -} - -QByteArray QOlmMessage::toCiphertext() const -{ - return SLICE(*this, QByteArray); -} - -QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) -{ - return QOlmMessage(ciphertext, QOlmMessage::General); -} diff --git a/Quotient/e2ee/qolmmessage.h b/Quotient/e2ee/qolmmessage.h deleted file mode 100644 index d2640b93d..000000000 --- a/Quotient/e2ee/qolmmessage.h +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -#include -#include -#include - -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 QUOTIENT_API QOlmMessage : public QByteArray { - Q_GADGET -public: - enum Type { - PreKey = OLM_MESSAGE_TYPE_PRE_KEY, - General = OLM_MESSAGE_TYPE_MESSAGE, - }; - Q_ENUM(Type) - - explicit QOlmMessage(QByteArray ciphertext, Type type = General); - - 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/Quotient/e2ee/qolmoutboundsession.cpp b/Quotient/e2ee/qolmoutboundsession.cpp deleted file mode 100644 index 8d8320c22..000000000 --- a/Quotient/e2ee/qolmoutboundsession.cpp +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmoutboundsession.h" - -#include "../logging_categories_p.h" - -#include - -using namespace Quotient; - -OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const { - return olm_outbound_group_session_last_error_code(olmData); -} - -const char* QOlmOutboundGroupSession::lastError() const -{ - return olm_outbound_group_session_last_error(olmData); -} - -QOlmOutboundGroupSession::QOlmOutboundGroupSession() - : m_groupSession(makeCStruct(olm_outbound_group_session, - olm_outbound_group_session_size, - olm_clear_outbound_group_session)) -{ - if (const auto randomLength = - olm_init_outbound_group_session_random_length(olmData); - olm_init_outbound_group_session(olmData, getRandom(randomLength).data(), - randomLength) - == olm_error()) { - QOLM_INTERNAL_ERROR("Failed to initialise an outbound group session"); - } -} - -QByteArray QOlmOutboundGroupSession::pickle(const PicklingKey& key) const -{ - const auto pickleLength = - olm_pickle_outbound_group_session_length(olmData); - auto pickledBuf = byteArrayForOlm(pickleLength); - if (olm_pickle_outbound_group_session(olmData, key.data(), key.size(), - pickledBuf.data(), pickleLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session"); - - return pickledBuf; -} - -QOlmExpected QOlmOutboundGroupSession::unpickle( - QByteArray&& pickled, const PicklingKey& key) -{ - QOlmOutboundGroupSession groupSession{}; - if (olm_unpickle_outbound_group_session(groupSession.olmData, key.data(), - key.size(), pickled.data(), - unsignedSize(pickled)) - == olm_error()) { - qWarning(E2EE) << "Failed to unpickle an outbound group session:" - << groupSession.lastError(); - return groupSession.lastErrorCode(); - } - - return groupSession; -} - -QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const -{ - const auto messageMaxLength = - olm_group_encrypt_message_length(olmData, unsignedSize(plaintext)); - auto messageBuf = byteArrayForOlm(messageMaxLength); - if (olm_group_encrypt(olmData, std::bit_cast(plaintext.data()), - unsignedSize(plaintext), std::bit_cast(messageBuf.data()), - messageMaxLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to encrypt a message"); - - return messageBuf; -} - -uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const -{ - return olm_outbound_group_session_message_index(olmData); -} - -QByteArray QOlmOutboundGroupSession::sessionId() const -{ - const auto idMaxLength = olm_outbound_group_session_id_length(olmData); - auto idBuffer = byteArrayForOlm(idMaxLength); - if (olm_outbound_group_session_id(olmData, std::bit_cast(idBuffer.data()), idMaxLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to obtain group session id"); - - return idBuffer; -} - -QByteArray QOlmOutboundGroupSession::sessionKey() const -{ - const auto keyMaxLength = olm_outbound_group_session_key_length(olmData); - auto keyBuffer = byteArrayForOlm(keyMaxLength); - if (olm_outbound_group_session_key(olmData, std::bit_cast(keyBuffer.data()), - keyMaxLength) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to obtain group session key"); - - return keyBuffer; -} - -int QOlmOutboundGroupSession::messageCount() const -{ - return m_messageCount; -} - -void QOlmOutboundGroupSession::setMessageCount(int messageCount) -{ - m_messageCount = messageCount; -} - -QDateTime QOlmOutboundGroupSession::creationTime() const -{ - return m_creationTime; -} - -void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) -{ - m_creationTime = creationTime; -} diff --git a/Quotient/e2ee/qolmoutboundsession.h b/Quotient/e2ee/qolmoutboundsession.h deleted file mode 100644 index 807671521..000000000 --- a/Quotient/e2ee/qolmoutboundsession.h +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -struct OlmOutboundGroupSession; - -namespace Quotient { - -//! An out-bound group session is responsible for encrypting outgoing -//! communication in a Megolm session. -class QUOTIENT_API QOlmOutboundGroupSession -{ -public: - QOlmOutboundGroupSession(); - - //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - QByteArray pickle(const PicklingKey &key) const; - //! Deserialises from encrypted Base64 that was previously obtained by - //! pickling a `QOlmOutboundGroupSession`. - static QOlmExpected unpickle(QByteArray&& pickled, const PicklingKey& key); - - //! Encrypts a plaintext message using the session. - QByteArray encrypt(const QByteArray& plaintext) const; - - //! 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. - QByteArray sessionKey() const; - - int messageCount() const; - void setMessageCount(int messageCount); - - QDateTime creationTime() const; - void setCreationTime(const QDateTime& creationTime); - - OlmErrorCode lastErrorCode() const; - const char* lastError() const; - -private: - CStructPtr m_groupSession; - int m_messageCount = 0; - QDateTime m_creationTime = QDateTime::currentDateTime(); - OlmOutboundGroupSession* olmData = m_groupSession.get(); -}; - -} // namespace Quotient diff --git a/Quotient/e2ee/qolmsession.cpp b/Quotient/e2ee/qolmsession.cpp deleted file mode 100644 index 85a79cd0d..000000000 --- a/Quotient/e2ee/qolmsession.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmsession.h" - -#include "../logging_categories_p.h" - -#include - -using namespace Quotient; - -OlmErrorCode QOlmSession::lastErrorCode() const { - return olm_session_last_error_code(olmData); -} - -const char* QOlmSession::lastError() const -{ - return olm_session_last_error(olmData); -} - -QByteArray QOlmSession::pickle(const PicklingKey &key) const -{ - const auto pickleLength = olm_pickle_session_length(olmData); - auto pickledBuf = byteArrayForOlm(pickleLength); - if (olm_pickle_session(olmData, key.data(), key.size(), - pickledBuf.data(), unsignedSize(pickledBuf)) - == olm_error()) - QOLM_INTERNAL_ERROR("Failed to pickle an Olm session"); - - return pickledBuf; -} - -QOlmExpected QOlmSession::unpickle(QByteArray&& pickled, - const PicklingKey &key) -{ - QOlmSession olmSession{}; - if (olm_unpickle_session(olmSession.olmData, key.data(), key.size(), - pickled.data(), unsignedSize(pickled)) - == olm_error()) { - const auto errorCode = olmSession.lastErrorCode(); - QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL, - "Failed to unpickle an Olm session"_L1, - olmSession.lastError()); - return errorCode; - } - - return olmSession; -} - -QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) const -{ - const auto messageMaxLength = - olm_encrypt_message_length(olmData, unsignedSize(plaintext)); - auto messageBuf = byteArrayForOlm(messageMaxLength); - // NB: The type has to be calculated before calling olm_encrypt() - const auto messageType = olm_encrypt_message_type(olmData); - if (const auto randomLength = olm_encrypt_random_length(olmData); - olm_encrypt(olmData, plaintext.data(), unsignedSize(plaintext), - getRandom(randomLength).data(), randomLength, - messageBuf.data(), messageMaxLength) - == olm_error()) { - QOLM_INTERNAL_ERROR("Failed to encrypt the message"); - } - - return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); -} - -QOlmExpected QOlmSession::decrypt(const QOlmMessage& message) const -{ - const auto ciphertext = message.toCiphertext(); - const auto messageTypeValue = message.type(); - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - const auto plaintextMaxLen = - olm_decrypt_max_plaintext_length(olmData, messageTypeValue, - QByteArray(ciphertext).data(), - unsignedSize(ciphertext)); - if (plaintextMaxLen == olm_error()) { - qWarning(E2EE) << "Couldn't calculate decrypted message length:" - << lastError(); - return lastErrorCode(); - } - - auto plaintextBuf = byteArrayForOlm(plaintextMaxLen); - const auto actualLength = olm_decrypt(olmData, messageTypeValue, - QByteArray(ciphertext).data(), - unsignedSize(ciphertext), - plaintextBuf.data(), plaintextMaxLen); - if (actualLength == olm_error()) { - QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, "Failed to decrypt the message"_L1); - return lastErrorCode(); - } - // actualLength cannot be more than plainTextLength because the resulting - // text would overflow the allocated memory; but it can be less, in theory - plaintextBuf.truncate(static_cast(actualLength)); - return plaintextBuf; -} - -QByteArray QOlmSession::sessionId() const -{ - const auto idMaxLength = olm_session_id_length(olmData); - auto idBuffer = byteArrayForOlm(idMaxLength); - if (olm_session_id(olmData, idBuffer.data(), idMaxLength) == olm_error()) - QOLM_INTERNAL_ERROR("Failed to obtain Olm session id"); - - return idBuffer; -} - -bool QOlmSession::hasReceivedMessage() const -{ - return olm_session_has_received_message(olmData) != 0; -} - -bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); - QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto maybeMatches = - olm_matches_inbound_session(olmData, oneTimeKeyBuf.data(), - unsignedSize(oneTimeKeyBuf)); - if (maybeMatches == olm_error()) - qWarning(E2EE) << "Error matching an inbound session:" << lastError(); - - return maybeMatches == 1; // Any errors are treated as non-match -} - -bool QOlmSession::matchesInboundSessionFrom( - QByteArray theirIdentityKey, const QOlmMessage& preKeyMessage) const -{ - auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - const auto maybeMatches = olm_matches_inbound_session_from( - olmData, theirIdentityKey.data(), unsignedSize(theirIdentityKey), - oneTimeKeyMessageBuf.data(), unsignedSize(oneTimeKeyMessageBuf)); - - if (maybeMatches == olm_error()) - qCWarning(E2EE) << "Error matching an inbound session:" << lastError(); - - return maybeMatches == 1; -} - -QOlmSession::QOlmSession() - : olmDataHolder( - makeCStruct(olm_session, olm_session_size, olm_clear_session)) -{} diff --git a/Quotient/e2ee/qolmsession.h b/Quotient/e2ee/qolmsession.h deleted file mode 100644 index ccc6be930..000000000 --- a/Quotient/e2ee/qolmsession.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include - -struct OlmSession; - -namespace Quotient { - -class QOlmAccount; - -//! Either an outbound or inbound session for secure communication. -class QUOTIENT_API QOlmSession -{ -public: - //! Serialises an `QOlmSession` to encrypted Base64. - QByteArray pickle(const PicklingKey& key) const; - - //! Deserialises from encrypted Base64 previously made with pickle() - static QOlmExpected unpickle(QByteArray&& pickled, - const PicklingKey& key); - - //! Encrypts a plaintext message using the session. - QOlmMessage encrypt(const QByteArray& plaintext) const; - - //! Decrypts a message using this session. Decoding is lossy, meaning if - //! the decrypted plaintext contains invalid UTF-8 symbols, they will - //! be returned as `U+FFFD`. - QOlmExpected decrypt(const QOlmMessage &message) const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! Checker for any received messages for this session. - bool hasReceivedMessage() const; - - //! Checks if the 'prekey' message is for this in-bound session. - bool matchesInboundSession(const QOlmMessage& preKeyMessage) const; - - //! Checks if the 'prekey' message is for this in-bound session. - bool matchesInboundSessionFrom(QByteArray theirIdentityKey, - const QOlmMessage& preKeyMessage) const; - - friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) - { - return lhs.sessionId() < rhs.sessionId(); - } - - OlmErrorCode lastErrorCode() const; - const char* lastError() const; - -private: - QOlmSession(); - CStructPtr olmDataHolder; - OlmSession* olmData = olmDataHolder.get(); - - friend class QOlmAccount; -}; -} //namespace Quotient diff --git a/Quotient/e2ee/qolmutility.cpp b/Quotient/e2ee/qolmutility.cpp deleted file mode 100644 index a1cb0d066..000000000 --- a/Quotient/e2ee/qolmutility.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmutility.h" - -#include "e2ee_common.h" - -#include - -using namespace Quotient; - -OlmErrorCode QOlmUtility::lastErrorCode() const { - return olm_utility_last_error_code(olmDataHolder.get()); -} - -const char* QOlmUtility::lastError() const -{ - return olm_utility_last_error(olmDataHolder.get()); -} - -QOlmUtility::QOlmUtility() - : olmDataHolder( - makeCStruct(olm_utility, olm_utility_size, olm_clear_utility)) -{} - -QString QOlmUtility::sha256Bytes(const QByteArray& inputBuf) const -{ - const auto outputLength = olm_sha256_length(olmDataHolder.get()); - auto outputBuf = byteArrayForOlm(outputLength); - olm_sha256(olmDataHolder.get(), inputBuf.data(), unsignedSize(inputBuf), - outputBuf.data(), outputLength); - - return QString::fromUtf8(outputBuf); -} - -QString QOlmUtility::sha256Utf8Msg(const QString& message) const -{ - return sha256Bytes(message.toUtf8()); -} - -bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, - QByteArray signature) const -{ - return olm_ed25519_verify(olmDataHolder.get(), key.data(), unsignedSize(key), - message.data(), unsignedSize(message), - signature.data(), unsignedSize(signature)) - == 0; -} diff --git a/Quotient/e2ee/qolmutility.h b/Quotient/e2ee/qolmutility.h deleted file mode 100644 index 48c59b26c..000000000 --- a/Quotient/e2ee/qolmutility.h +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -#include - -struct OlmUtility; - -namespace Quotient { - -//! Allows you to make use of crytographic hashing via SHA-2 and -//! verifying ed25519 signatures. -class QUOTIENT_API QOlmUtility -{ -public: - 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. - bool ed25519Verify(const QByteArray& key, const QByteArray& message, - QByteArray signature) const; - - OlmErrorCode lastErrorCode() const; - const char* lastError() const; - -private: - CStructPtr olmDataHolder; -}; -} // namespace Quotient diff --git a/Quotient/e2ee/sssshandler.cpp b/Quotient/e2ee/sssshandler.cpp deleted file mode 100644 index 83417cb52..000000000 --- a/Quotient/e2ee/sssshandler.cpp +++ /dev/null @@ -1,294 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later - -#include "sssshandler.h" - -#include "qolmaccount.h" - -#include "../csapi/key_backup.h" - -#include "../events/event.h" - -#include "../database.h" -#include "../logging_categories_p.h" -#include "../room.h" - -using namespace Quotient; - -namespace { -constexpr inline auto MegolmBackupKey = "m.megolm_backup.v1"_L1; -constexpr inline auto CrossSigningMasterKey = "m.cross_signing.master"_L1; -constexpr inline auto CrossSigningSelfSigningKey = "m.cross_signing.self_signing"_L1; -constexpr inline auto CrossSigningUserSigningKey = "m.cross_signing.user_signing"_L1; -} - -QByteArray SSSSHandler::decryptKey(event_type_t keyType, const QString& defaultKey, - key_view_t decryptionKey) -{ - Q_ASSERT(m_connection); - const auto& encryptedKeyObject = m_connection->accountData(keyType); - if (!encryptedKeyObject) { - qWarning() << "No account data for key" << keyType; - emit error(NoKeyError); - return {}; - } - const auto& encrypted = - encryptedKeyObject->contentPart("encrypted"_L1).value(defaultKey).toObject(); - - auto hkdfResult = hkdfSha256(decryptionKey, zeroes<32>(), asCBytes<>(keyType)); - if (!hkdfResult.has_value()) { - qCWarning(E2EE) << "Failed to calculate HKDF for" << keyType; - emit error(DecryptionError); - } - const auto& keys = hkdfResult.value(); - - auto rawCipher = QByteArray::fromBase64(encrypted["ciphertext"_L1].toString().toLatin1()); - auto result = hmacSha256(keys.mac(), rawCipher); - if (!result.has_value()) { - qCWarning(E2EE) << "Failed to calculate HMAC for" << keyType; - emit error(DecryptionError); - } - if (QString::fromLatin1(result.value().toBase64()) != encrypted["mac"_L1].toString()) { - qCWarning(E2EE) << "MAC mismatch for" << keyType; - emit error(DecryptionError); - return {}; - } - auto decryptResult = - aesCtr256Decrypt(rawCipher, keys.aes(), - asCBytes(QByteArray::fromBase64( - encrypted["iv"_L1].toString().toLatin1()))); - if (!decryptResult.has_value()) { - qCWarning(E2EE) << "Failed to decrypt for" << keyType; - emit error(DecryptionError); - } - auto key = QByteArray::fromBase64(decryptResult.value()); - m_connection->database()->storeEncrypted(keyType, key); - return key; -} - -class AesHmacSha2KeyDescription : public Event { -public: - // No QUO_EVENT because the account data "type id" is variable - QUO_CONTENT_GETTER(QString, algorithm) - QUO_CONTENT_GETTER(QJsonObject, passphrase) - QUO_CONTENT_GETTER(QString, iv) - QUO_CONTENT_GETTER(QString, mac) -}; - -struct SSSSHandler::UnlockData { - static Expected prepare(const Connection* c) - { - Q_ASSERT(c); - - const auto& defaultKeyEvent = c->accountData("m.secret_storage.default_key"_L1); - if (!defaultKeyEvent) { - qCWarning(E2EE) << "SSSS: No default secret storage key"; - return NoKeyError; - } - auto defaultKey = defaultKeyEvent->contentPart("key"_L1); - const auto keyName = "m.secret_storage.key."_L1 + defaultKey; - auto* const keyDescription = c->accountData(keyName); - if (!keyDescription) { - qCWarning(E2EE) << "SSSS: No account data for key" << keyName; - return NoKeyError; - } - - if (keyDescription->algorithm() != "m.secret_storage.v1.aes-hmac-sha2"_L1) { - qCWarning(E2EE) << "Unsupported SSSS key algorithm" << keyDescription->algorithm() - << " - aborting."; - return UnsupportedAlgorithmError; - } - auto iv = QByteArray::fromBase64Encoding(keyDescription->iv().toLatin1()); - if (!iv || iv.decoded.isEmpty() || iv.decoded.size() != AesBlockSize) { - qCWarning(E2EE) << "SSSS: Malformed or empty IV"; - return DecryptionError; - } - auto&& mac = QByteArray::fromBase64Encoding(keyDescription->mac().toLatin1()); - if (!mac || mac.decoded.isEmpty()) { - qCWarning(E2EE) << "SSSS: Failed to decode expected MAC or it is empty"; - return DecryptionError; - } - return UnlockData{ std::move(defaultKey), keyDescription->passphrase(), std::move(*iv), - std::move(*mac) }; - } - - QString defaultKey; - QJsonObject passphraseInfo; - QByteArray decodedIV; - QByteArray decodedMac; -}; - -void SSSSHandler::unlockSSSSWithPassphrase(const QString& passphrase) -{ - const auto unlockData = UnlockData::prepare(m_connection); - if (!unlockData) { - emit error(unlockData.error()); - return; - } - if (unlockData->passphraseInfo["algorithm"_L1].toString() != "m.pbkdf2"_L1) { - qCWarning(E2EE) << "Unsupported SSSS passphrase algorithm" - << unlockData->passphraseInfo["algorithm"_L1].toString() << " - aborting."; - emit error(UnsupportedAlgorithmError); - return; - } - if (const auto& result = - pbkdf2HmacSha512(viewAsByteArray(passphrase.toUtf8()), - unlockData->passphraseInfo["salt"_L1].toString().toLatin1(), - unlockData->passphraseInfo["iterations"_L1].toInt())) { - unlockAndLoad(*unlockData, *result); - return; - } - qCWarning(E2EE) << "Failed to calculate PBKDF"; - emit error(DecryptionError); - return; -} - -void SSSSHandler::unlockSSSSFromCrossSigning() -{ - Q_ASSERT(m_connection); - m_connection->requestKeyFromDevices(MegolmBackupKey).then([this](const QByteArray& key) { - loadMegolmBackup(key); - }); - for (auto k : {CrossSigningUserSigningKey, CrossSigningSelfSigningKey, CrossSigningMasterKey}) - m_connection->requestKeyFromDevices(k); -} - -Connection* SSSSHandler::connection() const -{ - return m_connection; -} - -void SSSSHandler::setConnection(Connection* connection) -{ - if (connection == m_connection) { - return; - } - m_connection = connection; - emit connectionChanged(); -} - -void SSSSHandler::loadMegolmBackup(const QByteArray& megolmDecryptionKey) -{ - auto job = m_connection->callApi(); - connect(job, &BaseJob::finished, this, [this, job, megolmDecryptionKey] { - m_connection->database()->storeEncrypted("etag"_L1, job->etag().toLatin1()); - auto authData = job->authData(); - const auto& userSignaturesObject = - authData["signatures"_L1].toObject().value(m_connection->userId()).toObject(); - for (auto it = userSignaturesObject.constBegin(); it != userSignaturesObject.constEnd(); - ++it) { - const auto& edKey = - m_connection->database()->edKeyForKeyId(m_connection->userId(), it.key()); - if (edKey.isEmpty()) { - continue; - } - const auto& signature = it.value().toString(); - if (!ed25519VerifySignature(edKey, authData, signature)) { - qCWarning(E2EE) << "Signature mismatch for" << edKey; - emit error(InvalidSignatureError); - return; - } - } - qCDebug(E2EE) << "Loading key backup" << job->version(); - auto keysJob = m_connection->callApi(job->version()); - connect(keysJob, &BaseJob::finished, this, [this, keysJob, megolmDecryptionKey](){ - const auto &rooms = keysJob->rooms(); - qCDebug(E2EE) << rooms.size() << "rooms in the backup"; - for (const auto& [roomId, roomKeyBackup] : rooms.asKeyValueRange()) { - if (!m_connection->room(roomId)) - continue; - - for (const auto& [sessionId, backupData] : - roomKeyBackup.sessions.asKeyValueRange()) { - const auto& sessionData = backupData.sessionData; - const auto result = - curve25519AesSha2Decrypt(sessionData["ciphertext"_L1].toString().toLatin1(), - megolmDecryptionKey, - sessionData["ephemeral"_L1].toString().toLatin1(), - sessionData["mac"_L1].toString().toLatin1()); - if (!result.has_value()) { - qCWarning(E2EE) << "Failed to decrypt session" << sessionId; - emit error(DecryptionError); - continue; - } - const auto data = QJsonDocument::fromJson(result.value()).object(); - m_connection->room(roomId)->addMegolmSessionFromBackup( - sessionId.toLatin1(), data["session_key"_L1].toString().toLatin1(), - static_cast(backupData.firstMessageIndex), data["sender_key"_L1].toVariant().toByteArray(), data["sender_claimed_keys"_L1]["ed25519"_L1].toVariant().toByteArray()); - } - } - emit finished(); - }); - }); -} - -void SSSSHandler::unlockAndLoad(const UnlockData& unlockData, key_view_t decryptingKey) -{ - const auto& testKeys = hkdfSha256(decryptingKey, zeroes<32>(), {}); - if (!testKeys.has_value()) { - qCWarning(E2EE) << "SSSS: Failed to calculate HKDF"; - emit error(DecryptionError); - return; - } - const auto& encrypted = aesCtr256Encrypt(zeroedByteArray(), testKeys.value().aes(), - asCBytes(unlockData.decodedIV)); - if (!encrypted.has_value()) { - qCWarning(E2EE) << "SSSS: Failed to encrypt test keys"; - emit error(DecryptionError); - return; - } - const auto &result = hmacSha256(testKeys.value().mac(), encrypted.value()); - if (!result.has_value()) { - qCWarning(E2EE) << "SSSS: Failed to calculate HMAC"; - emit error(DecryptionError); - return; - } - if (result.value() != unlockData.decodedMac) { - qCWarning(E2EE) << "SSSS: MAC mismatch for secret storage test key"; - emit error(WrongKeyError); - return; - } - - emit keyBackupUnlocked(); - - auto megolmDecryptionKey = decryptKey(MegolmBackupKey, unlockData.defaultKey, decryptingKey); - if (megolmDecryptionKey.isEmpty()) { - qCWarning(E2EE) << "SSSS: No megolm decryption key"; - emit error(NoKeyError); - return; - } - loadMegolmBackup(megolmDecryptionKey); - - // These keys are only decrypted since this will automatically store them locally - decryptKey(CrossSigningSelfSigningKey, unlockData.defaultKey, decryptingKey); - decryptKey(CrossSigningUserSigningKey, unlockData.defaultKey, decryptingKey); - decryptKey(CrossSigningMasterKey, unlockData.defaultKey, decryptingKey); -} - -void SSSSHandler::unlockSSSSFromSecurityKey(const QString& encodedKey) -{ - auto securityKey = encodedKey; - securityKey.remove(u' '); - auto&& decoded = base58Decode(securityKey.toLatin1()); - if (decoded.size() != DefaultPbkdf2KeyLength + 3) { - qCWarning(E2EE) << "SSSS: Incorrect decryption key length"; - emit error(WrongKeyError); - return; - } - if (decoded.front() != 0x8B || decoded[1] != 0x01) { - qCWarning(E2EE) << "SSSS: invalid prefix in the decryption key"; - emit error(WrongKeyError); - return; - } - if (std::accumulate(decoded.cbegin(), decoded.cend(), uint8_t{ 0 }, std::bit_xor<>()) != 0) { - qCWarning(E2EE) << "SSSS: invalid parity byte in the decryption key"; - emit error(WrongKeyError); - return; - } - const auto unlockData = UnlockData::prepare(m_connection); - if (!unlockData) { - emit error(unlockData.error()); - return; - } - unlockAndLoad(*unlockData, byte_view_t<>(decoded).subspan<2, DefaultPbkdf2KeyLength>()); -} diff --git a/Quotient/e2ee/sssshandler.h b/Quotient/e2ee/sssshandler.h deleted file mode 100644 index 0d32d1d67..000000000 --- a/Quotient/e2ee/sssshandler.h +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later - -#pragma once - -#include "cryptoutils.h" - -#include "../connection.h" - -#include -#include - -namespace Quotient { -class QUOTIENT_API SSSSHandler : public QObject -{ - Q_OBJECT - Q_PROPERTY(Quotient::Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) - -public: - enum Error - { - WrongKeyError, - NoKeyError, - DecryptionError, - InvalidSignatureError, - UnsupportedAlgorithmError, - }; - Q_ENUM(Error) - - using QObject::QObject; - - //! \brief Unlock the secret backup from the given passprhase - Q_INVOKABLE void unlockSSSSWithPassphrase(const QString& passphrase); - - //! \brief Unlock the secret backup by requesting the decryption keys from other devices - Q_INVOKABLE void unlockSSSSFromCrossSigning(); - - //! \brief Unlock the secret backup from the given security key - Q_INVOKABLE void unlockSSSSFromSecurityKey(const QString& encodedKey); - - Connection* connection() const; - void setConnection(Connection* connection); - -Q_SIGNALS: - void keyBackupUnlocked(); - void error(Error error); - void connectionChanged(); - - //! \brief Emitted after keys are loaded - void finished(); - -private: - QPointer m_connection; - - //! \brief Decrypt the key with this name from the account data - QByteArray decryptKey(event_type_t keyType, const QString& defaultKey, key_view_t decryptionKey); - - void loadMegolmBackup(const QByteArray& megolmDecryptionKey); - struct UnlockData; - void unlockAndLoad(const UnlockData& unlockData, key_view_t decryptingKey); -}; -} // namespace Quotient diff --git a/Quotient/events/filesourceinfo.cpp b/Quotient/events/filesourceinfo.cpp index 3b10af512..f6c4f3f59 100644 --- a/Quotient/events/filesourceinfo.cpp +++ b/Quotient/events/filesourceinfo.cpp @@ -4,16 +4,26 @@ #include "filesourceinfo.h" -#include "../e2ee/cryptoutils.h" #include "../e2ee/e2ee_common.h" #include "../logging_categories_p.h" #include "../util.h" +#include "lib.rs.h" #include #include using namespace Quotient; +static rust::String bytesToRust(const QByteArray& bytes) +{ + return rust::String(bytes.data(), bytes.size()); +} + +static rust::String stringToRust(const QString& string) +{ + return bytesToRust(string.toUtf8()); +} + QByteArray Quotient::decryptFile(const QByteArray& ciphertext, const EncryptedFileMetadata& metadata) { @@ -22,46 +32,35 @@ QByteArray Quotient::decryptFile(const QByteArray& ciphertext, qCWarning(E2EE) << "Hash verification failed for file"; return {}; } - const auto key = QByteArray::fromBase64(metadata.key.k.toLatin1(), - QByteArray::Base64UrlEncoding); - if (key.size() < Aes256KeySize) { - qCWarning(E2EE) << "Decoded key is too short for AES, need" - << Aes256KeySize << "bytes, got" << key.size(); - return {}; - } - const auto iv = QByteArray::fromBase64(metadata.iv.toLatin1()); - if (iv.size() < AesBlockSize) { - qCWarning(E2EE) << "Decoded iv is too short for AES, need" - << AesBlockSize << "bytes, got" << iv.size(); - return {}; - } - return aesCtr256Decrypt(ciphertext, asCBytes<32>(key), asCBytes<16>(iv)) - .move_value_or({}); + const auto plaintext = crypto::decrypt_file(rust::Slice((std::uint8_t *) ciphertext.data(), ciphertext.size()), stringToRust(metadata.iv), stringToRust(metadata.key.k), stringToRust(metadata.hashes["sha256"_L1])); + return QByteArray((const char *) plaintext.data(), plaintext.size()); +} + +static QByteArray bytesFromRust(const rust::String &bytes) { + return {bytes.data(), (int) bytes.size()}; +} + +static QString stringFromRust(const rust::String& string) { + return QString::fromUtf8(bytesFromRust(string)); } std::pair Quotient::encryptFile( const QByteArray& plainText) { - auto k = getRandom(); - auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding - | QByteArray::OmitTrailingEquals); - auto iv = getRandom(); - const JWK key = { - "oct"_L1, { "encrypt"_L1, "decrypt"_L1 }, "A256CTR"_L1, QString::fromLatin1(kBase64), true - }; - auto result = aesCtr256Encrypt(plainText, k, iv); - if (!result.has_value()) - return {}; - - auto hash = QCryptographicHash::hash(result.value(), QCryptographicHash::Sha256) - .toBase64(QByteArray::OmitTrailingEquals); - auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals); - const EncryptedFileMetadata efm = { - {}, key, QString::fromLatin1(ivBase64), - { { "sha256"_L1, QString::fromLatin1(hash) } }, "v2"_L1 + auto info = crypto::encrypt_file(rust::Slice((std::uint8_t *) plainText.data(), plainText.size())); + auto bytes = info->media_encryption_info_bytes(); + QByteArray ciphertext((const char *) bytes.data(), bytes.size()); + EncryptedFileMetadata metadata { + .url = {}, + .key = { + "oct"_L1, { "encrypt"_L1, "decrypt"_L1 }, "A256CTR"_L1, stringFromRust(info->media_encryption_info_key()), true + }, + .iv = stringFromRust(info->media_encryption_info_iv()), + .hashes = {{u"sha256"_s, stringFromRust(info->media_encryption_info_hash())}}, + .v = u"v2"_s, }; - return { efm, result.value() }; + return { metadata, ciphertext }; } void JsonObjectConverter::dumpTo( diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index e13a30c18..2ea7aada9 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -8,7 +8,6 @@ #include #include -#include "e2ee/cryptoutils.h" #include "connection.h" #include "room.h" @@ -17,14 +16,14 @@ using namespace Quotient; using namespace Qt::Literals::StringLiterals; -const auto VersionLength = 1; -const auto SaltOffset = VersionLength; -const auto IvOffset = SaltOffset + AesBlockSize; -const auto RoundsOffset = IvOffset + AesBlockSize; -const auto RoundsLength = 4; -const auto PayloadOffset = RoundsOffset + RoundsLength; -const auto MacLength = 32; -const auto HeaderLength = VersionLength + AesBlockSize + AesBlockSize + RoundsLength + MacLength; +// const auto VersionLength = 1; +// const auto SaltOffset = VersionLength; +// const auto IvOffset = SaltOffset + AesBlockSize; +// const auto RoundsOffset = IvOffset + AesBlockSize; +// const auto RoundsLength = 4; +// const auto PayloadOffset = RoundsOffset + RoundsLength; +// const auto MacLength = 32; +// const auto HeaderLength = VersionLength + AesBlockSize + AesBlockSize + RoundsLength + MacLength; Expected KeyImport::decrypt(QString data, const QString& passphrase) { @@ -37,40 +36,41 @@ Expected KeyImport::decrypt(QString data, const QS return InvalidData; } - if (decoded.size() < HeaderLength) { - qCWarning(E2EE) << "Data not long enough"; - return InvalidData; - } - + // if (decoded.size() < HeaderLength) { + // qCWarning(E2EE) << "Data not long enough"; + // return InvalidData; + // } +/* const auto salt = decoded.mid(SaltOffset, AesBlockSize); const auto iv = decoded.mid(IvOffset, AesBlockSize); const auto rounds = qFromBigEndian(decoded.mid(RoundsOffset, RoundsLength).data()); const auto payload = decoded.mid(PayloadOffset, decoded.size() - HeaderLength); - const auto expectedMac = decoded.right(MacLength); - - auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt, rounds); - if (!keys.has_value()) { - qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error(); - return OtherError; - } - - auto actualMac = hmacSha256(key_view_t(keys.value().begin() + 32, 32), decoded.left(decoded.size() - MacLength)); - if (!actualMac.has_value()) { - qCWarning(E2EE) << "Failed to calculate hmac:" << actualMac.error(); - return OtherError; - } - + const auto expectedMac = decoded.right(MacLength);*/ + + // auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt, rounds); + // if (!keys.has_value()) { + // qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error(); + // return OtherError; + // } + // + // auto actualMac = hmacSha256(key_view_t(keys.value().begin() + 32, 32), decoded.left(decoded.size() - MacLength)); + // if (!actualMac.has_value()) { + // qCWarning(E2EE) << "Failed to calculate hmac:" << actualMac.error(); + // return OtherError; + // } +/* if (actualMac.value() != expectedMac) { qCWarning(E2EE) << "Mac incorrect"; return InvalidPassphrase; - } - + }*/ +/* auto plain = aesCtr256Decrypt(payload, byte_view_t(keys.value().begin(), Aes256KeySize), asCBytes(iv)); if (!plain.has_value()) { qCWarning(E2EE) << "Failed to decrypt data"; return OtherError; } - return QJsonDocument::fromJson(plain.value()).array(); + return QJsonDocument::fromJson(plain.value()).array();*/ + return {}; } KeyImport::Error KeyImport::importKeys(QString data, const QString& passphrase, const Connection* connection) @@ -87,12 +87,12 @@ KeyImport::Error KeyImport::importKeys(QString data, const QString& passphrase, continue; } // We don't know the session index for these sessions here. We just pretend it's 0, it's not terribly important. - room->addMegolmSessionFromBackup( - keyObject["session_id"_L1].toString().toLatin1(), - keyObject["session_key"_L1].toString().toLatin1(), 0, - keyObject[SenderKeyKey].toVariant().toByteArray(), - keyObject["sender_claimed_keys"_L1]["ed25519"_L1].toString().toLatin1() - ); + // room->addMegolmSessionFromBackup( + // keyObject["session_id"_L1].toString().toLatin1(), + // keyObject["session_key"_L1].toString().toLatin1(), 0, + // keyObject[SenderKeyKey].toVariant().toByteArray(), + // keyObject["sender_claimed_keys"_L1]["ed25519"_L1].toString().toLatin1() + // ); } return Success; } @@ -116,41 +116,42 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s { auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact); - auto salt = getRandom(); - auto iv = getRandom(); - quint32 rounds = 200'000; // spec: "N should be at least 100,000"; - - auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt.viewAsByteArray(), rounds); - if (!keys.has_value()) { - qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error(); - return OtherError; - } - - auto result = aesCtr256Encrypt(plainText, byte_view_t(keys.value().begin(), Aes256KeySize), asCBytes(iv.viewAsByteArray())); - - if (!result.has_value()) { - qCWarning(E2EE) << "Failed to encrypt export" << result.error(); - return OtherError; - } - - QByteArray data; - data.append("\x01"); - data.append(salt.viewAsByteArray()); - data.append(iv.viewAsByteArray()); - QByteArray roundsData(4, u'\x0'); - qToBigEndian(rounds, roundsData.data()); - data.append(roundsData); - data.append(result.value()); - auto mac = hmacSha256(key_view_t(keys.value().begin() + 32, 32), data); - if (!mac.has_value()) { - qCWarning(E2EE) << "Failed to calculate MAC" << mac.error(); - return OtherError; - } - data.append(mac.value()); - - // TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with - return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba % lineWrapped(data.toBase64(), 96) - % "\n-----END MEGOLM SESSION DATA-----\n"_ba; + // auto salt = getRandom(); + // auto iv = getRandom(); + // quint32 rounds = 200'000; // spec: "N should be at least 100,000"; + // + // auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt.viewAsByteArray(), rounds); + // if (!keys.has_value()) { + // qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error(); + // return OtherError; + // } + + // auto result = aesCtr256Encrypt(plainText, byte_view_t(keys.value().begin(), Aes256KeySize), asCBytes(iv.viewAsByteArray())); + // + // if (!result.has_value()) { + // qCWarning(E2EE) << "Failed to encrypt export" << result.error(); + // return OtherError; + // } + // + // QByteArray data; + // data.append("\x01"); + // data.append(salt.viewAsByteArray()); + // data.append(iv.viewAsByteArray()); + // QByteArray roundsData(4, u'\x0'); + // qToBigEndian(rounds, roundsData.data()); + // data.append(roundsData); + // data.append(result.value()); + // auto mac = hmacSha256(key_view_t(keys.value().begin() + 32, 32), data); + // if (!mac.has_value()) { + // qCWarning(E2EE) << "Failed to calculate MAC" << mac.error(); + // return OtherError; + // } + // data.append(mac.value()); + // + // // TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with + // return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba % lineWrapped(data.toBase64(), 96) + // % "\n-----END MEGOLM SESSION DATA-----\n"_ba; + return {}; } diff --git a/Quotient/keyverificationsession.cpp b/Quotient/keyverificationsession.cpp index 7755cc9b4..bc683707e 100644 --- a/Quotient/keyverificationsession.cpp +++ b/Quotient/keyverificationsession.cpp @@ -3,719 +3,196 @@ #include "keyverificationsession.h" +#include + #include "connection.h" -#include "database.h" -#include "logging_categories_p.h" -#include "csapi/cross_signing.h" +#include "connection_p.h" #include "room.h" -#include "e2ee/cryptoutils.h" -#include "e2ee/sssshandler.h" -#include "e2ee/qolmaccount.h" - -#include "events/event.h" - -#include -#include -#include - -#include - -#include - using namespace Quotient; -using namespace std::chrono; - -const QStringList supportedMethods = { SasV1Method }; - -QStringList commonSupportedMethods(const QStringList& remoteMethods) -{ - QStringList result; - for (const auto& method : remoteMethods) { - if (supportedMethods.contains(method)) { - result += method; - } - } - return result; -} - -CStructPtr KeyVerificationSession::makeOlmData() -{ - auto data = makeCStruct(olm_sas, olm_sas_size, olm_clear_sas); - - const auto randomLength = olm_create_sas_random_length(data.get()); - olm_create_sas(data.get(), getRandom(randomLength).data(), randomLength); - return data; -} -KeyVerificationSession::KeyVerificationSession(QString remoteUserId, - const KeyVerificationRequestEvent& event, - Connection* connection, bool encrypted) - : KeyVerificationSession(std::move(remoteUserId), connection, event.fromDevice(), encrypted, - event.methods(), event.timestamp(), event.transactionId()) -{} - -KeyVerificationSession::KeyVerificationSession(const RoomMessageEvent* event, Room* room) - : KeyVerificationSession(event->senderId(), room->connection(), - event->contentPart("from_device"_L1), room->usesEncryption(), - event->contentPart("methods"_L1), - event->originTimestamp(), {}, room, event->id()) -{} - -KeyVerificationSession::KeyVerificationSession(QString remoteUserId, Connection* connection, - QString remoteDeviceId, bool encrypted, - QStringList methods, QDateTime startTimestamp, - QString transactionId, Room* room, - QString requestEventId) +//TODO: separate constructors +KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const QString& verificationId, const QString& remoteDeviceId, Connection* connection) : QObject(connection) + , m_remoteUserId(remoteUserId) + , m_verificationId(verificationId) + , m_remoteDeviceId(remoteDeviceId) , m_connection(connection) - , m_room(room) - , m_remoteUserId(std::move(remoteUserId)) - , m_remoteDeviceId(std::move(remoteDeviceId)) - , m_transactionId(std::move(transactionId)) - , m_encrypted(encrypted) - , m_remoteSupportedMethods(std::move(methods)) - , m_requestEventId(std::move(requestEventId)) // TODO: Consider merging with transactionId + , m_processTimer(new QTimer) { - if (m_connection->hasConflictingDeviceIdsAndCrossSigningKeys(m_remoteUserId)) { - qCWarning(E2EE) << "Remote user has conflicting device ids and cross signing keys; refusing to verify."; - return; + connect(connection, &Connection::syncDone, this, [this](){ + setState(m_connection->d->keyVerificationSessionState(this)); + setSasState(m_connection->d->sasState(this)); + }); + if (verificationId.isEmpty()) { + setState(CREATED); + m_connection->d->requestDeviceVerification(this); } - const auto& currentTime = QDateTime::currentDateTime(); - const auto timeoutTime = - std::min(startTimestamp.addSecs(600), currentTime.addSecs(120)); - const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; - if (timeout > 5s) { - setupTimeout(timeout); - } else { - // Otherwise don't even bother starting up - deleteLater(); + m_processTimer->setInterval(250); + m_processTimer->setSingleShot(false); + m_processTimer->start(); + connect(m_processTimer, &QTimer::timeout, this, [this](){ + m_connection->d->processOutgoingRequests(); + }); + //TODO make sure that objects are deleted when finished + + // This happens when starting a "broadcast" self verification + if (remoteDeviceId.isEmpty()) { + setState(CREATED); } } -KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, - Connection* connection) - : KeyVerificationSession(std::move(userId), connection, nullptr, std::move(deviceId), - QUuid::createUuid().toString()) -{} - -KeyVerificationSession::KeyVerificationSession(Room* room) - : KeyVerificationSession(room->members()[room->members()[0].isLocalMember() ? 1 : 0].id(), - room->connection(), - room) -{} - -KeyVerificationSession::KeyVerificationSession(QString remoteUserId, Connection* connection, - Room* room, QString remoteDeviceId, - QString transactionId) +KeyVerificationSession::KeyVerificationSession(Room* room, Connection* connection, const QString& verificationId) : QObject(connection) + , m_verificationId(verificationId) , m_connection(connection) , m_room(room) - , m_remoteUserId(std::move(remoteUserId)) - , m_remoteDeviceId(std::move(remoteDeviceId)) - , m_transactionId(std::move(transactionId)) + , m_processTimer(new QTimer) { - if (m_connection->hasConflictingDeviceIdsAndCrossSigningKeys(m_remoteUserId)) { - qCWarning(E2EE) << "Remote user has conflicting device ids and cross signing keys; refusing to verify."; + const auto& ids = room->joinedMemberIds(); + if (ids.size() != 2) { + //TODO: error return; } - setupTimeout(600s); - QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); -} - -void KeyVerificationSession::setupTimeout(milliseconds timeout) -{ - QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); -} - -void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent) -{ - if (!switchOnType( - baseEvent, - [this](const KeyVerificationCancelEvent& event) { - setError(stringToError(event.code())); - setState(CANCELED); - return true; - }, - [this](const KeyVerificationStartEvent& event) { - if (state() != WAITINGFORREADY && state() != READY && state() != WAITINGFORACCEPT) - return false; - handleStart(event); - return true; - }, - [this](const KeyVerificationReadyEvent& event) { - if (state() == WAITINGFORREADY) - handleReady(event); - // ACCEPTED is also fine here because it's possible to receive - // ready and start in the same sync, in which case start might - // be handled before ready. - return state() == READY || state() == WAITINGFORACCEPT || state() == ACCEPTED; - }, - [this](const KeyVerificationAcceptEvent& event) { - if (state() != WAITINGFORACCEPT) - return false; - const auto& theirMac = event.messageAuthenticationCode(); - for (const auto& mac : SupportedMacs) { - if (mac == theirMac) { - m_commonMacCodes.push_back(theirMac); - } - } - if (m_commonMacCodes.isEmpty()) { - cancelVerification(UNKNOWN_METHOD); - return false; - } - m_commitment = event.commitment(); - sendKey(); - setState(WAITINGFORKEY); - return true; - }, - [this](const KeyVerificationKeyEvent& event) { - if (state() != ACCEPTED && state() != WAITINGFORKEY) - return false; - handleKey(event); - return true; - }, - [this](const KeyVerificationMacEvent& event) { - if (state() != WAITINGFORMAC && state() != WAITINGFORVERIFICATION) - return false; - handleMac(event); - return true; - }, - [this](const KeyVerificationDoneEvent&) { return state() == DONE; })) - cancelVerification(UNEXPECTED_MESSAGE); -} - -struct EmojiStoreEntry : EmojiEntry { - QHash translatedDescriptions; + m_remoteUserId = ids[0] == connection->userId() ? ids[1] : ids[0]; - explicit EmojiStoreEntry(const QJsonObject& json) - : EmojiEntry{ fromJson(json["emoji"_L1]), - fromJson(json["description"_L1]) } - , translatedDescriptions{ fromJson>( - json["translated_descriptions"_L1]) } - {} -}; - -using EmojiStore = QVector; - -EmojiStore loadEmojiStore() -{ - Q_INIT_RESOURCE(libquotientemojis); - QFile dataFile(":/sas-emoji.json"_L1); - dataFile.open(QFile::ReadOnly); - auto data = dataFile.readAll(); - Q_CLEANUP_RESOURCE(libquotientemojis); - return fromJson( - QJsonDocument::fromJson(data).array()); -} + connect(connection, &Connection::syncDone, this, [this](){ + setState(m_connection->d->keyVerificationSessionState(this)); + setSasState(m_connection->d->sasState(this)); + }); -EmojiEntry emojiForCode(int code, const QString& language) -{ - static const EmojiStore emojiStore = loadEmojiStore(); - const auto& entry = emojiStore[code]; - if (!language.isEmpty()) - if (const auto translatedDescription = - emojiStore[code].translatedDescriptions.value(language); - !translatedDescription.isNull()) - return { entry.emoji, translatedDescription }; - - return SLICE(entry, EmojiEntry); -} + connect(connection, &Connection::verificationEventProcessed, this, [this](){ + setState(m_connection->d->keyVerificationSessionState(this)); + setSasState(m_connection->d->sasState(this)); + }); -void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) -{ - auto eventKey = event.key().toLatin1(); - olm_sas_set_their_key(olmData, eventKey.data(), unsignedSize(eventKey)); - - if (startSentByUs) { - const auto paddedCommitment = - QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), - QCryptographicHash::Sha256) - .toBase64(); - const QLatin1String unpaddedCommitment(paddedCommitment.constData(), - QString::fromLatin1(paddedCommitment).indexOf(u'=')); - if (unpaddedCommitment != m_commitment) { - qCWarning(E2EE) << "Commitment mismatch; aborting verification"; - cancelVerification(MISMATCHED_COMMITMENT); - return; - } + if (m_verificationId.isEmpty()) { //TODO this check seems pointless + setState(CREATED); + m_connection->d->requestUserVerification(this); } else { - sendKey(); + setState(REQUESTED); } - - std::string key(olm_sas_pubkey_length(olmData), '\0'); - olm_sas_get_pubkey(olmData, key.data(), key.size()); - - std::array output{}; - const auto infoTemplate = - startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_L1 - : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_L1; - - const auto info = infoTemplate - .arg(m_connection->userId(), m_connection->deviceId(), - QString::fromLatin1(key.data()), m_remoteUserId, m_remoteDeviceId, - event.key(), m_room ? m_requestEventId : m_transactionId) - .toLatin1(); - - olm_sas_generate_bytes(olmData, info.data(), unsignedSize(info), - output.data(), output.size()); - - static constexpr auto x3f = std::byte{ 0x3f }; - const std::array code{ - output[0] >> 2, - (output[0] << 4 & x3f) | output[1] >> 4, - (output[1] << 2 & x3f) | output[2] >> 6, - output[2] & x3f, - output[3] >> 2, - (output[3] << 4 & x3f) | output[4] >> 4, - (output[4] << 2 & x3f) | output[5] >> 6 - }; - - const auto uiLanguages = QLocale().uiLanguages(); - const auto preferredLanguage = uiLanguages.isEmpty() - ? QString() - : uiLanguages.front().section(u'-', 0, 0); - for (const auto& c : code) - m_sasEmojis += emojiForCode(std::to_integer(c), preferredLanguage); - - emit sasEmojisChanged(); - emit keyReceived(); - setState(WAITINGFORVERIFICATION); + m_processTimer->setInterval(250); + m_processTimer->setSingleShot(false); + m_processTimer->start(); + connect(m_processTimer, &QTimer::timeout, this, [this](){ + m_connection->d->processOutgoingRequests(); + }); } -QString KeyVerificationSession::calculateMac(const QString& input, - bool verifying, - const QString& keyId) +KeyVerificationSession* KeyVerificationSession::requestDeviceVerification(const QString& userId, const QString& deviceId, Connection* connection) { - const auto inputBytes = input.toLatin1(); - const auto macLength = olm_sas_mac_length(olmData); - auto macChars = byteArrayForOlm(macLength); - const auto macInfo = (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_L1 - : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_L1) - .arg(m_connection->userId(), m_connection->deviceId(), m_remoteUserId, - m_remoteDeviceId, m_room ? m_requestEventId : m_transactionId, - input.contains(u',') ? u"KEY_IDS"_s : keyId) - .toLatin1(); - if (m_commonMacCodes.contains(HmacSha256V2Code)) - olm_sas_calculate_mac_fixed_base64(olmData, inputBytes.data(), - unsignedSize(inputBytes), - macInfo.data(), unsignedSize(macInfo), - macChars.data(), macLength); - else - olm_sas_calculate_mac(olmData, inputBytes.data(), - unsignedSize(inputBytes), macInfo.data(), - unsignedSize(macInfo), macChars.data(), macLength); - return QString::fromLatin1(macChars.data(), macChars.indexOf('=')); + return new KeyVerificationSession(userId, {}, deviceId, connection); } -void KeyVerificationSession::sendMac() +KeyVerificationSession* KeyVerificationSession::requestUserVerification(Room* room, Connection* connection) { - QString keyId{ "ed25519:"_L1 % m_connection->deviceId() }; - - const auto &masterKey = m_connection->isUserVerified(m_connection->userId()) ? m_connection->masterKeyForUser(m_connection->userId()) : QString(); - - QStringList keyList{keyId}; - if (!masterKey.isEmpty()) { - keyList += "ed25519:"_L1 + masterKey; - } - keyList.sort(); - - auto keys = calculateMac(keyList.join(u','), false); - - QJsonObject mac; - auto key = m_connection->olmAccount()->deviceKeys().keys.value(keyId); - mac[keyId] = calculateMac(key, false, keyId); - if (!masterKey.isEmpty()) { - mac["ed25519:"_L1 + masterKey] = calculateMac(masterKey, false, "ed25519:"_L1 + masterKey); - } - - sendEvent(m_remoteUserId, m_remoteDeviceId, - KeyVerificationMacEvent(m_transactionId, keys, - mac), - m_encrypted); - setState (macReceived ? DONE : WAITINGFORMAC); - m_verified = true; - if (!m_pendingEdKeyId.isEmpty()) { - trustKeys(); - } + return new KeyVerificationSession(room, connection); } -void KeyVerificationSession::sendDone() -{ - sendEvent(m_remoteUserId, m_remoteDeviceId, - KeyVerificationDoneEvent(m_transactionId), - m_encrypted); -} -void KeyVerificationSession::sendKey() +void KeyVerificationSession::accept() { - const auto pubkeyLength = olm_sas_pubkey_length(olmData); - auto keyBytes = byteArrayForOlm(pubkeyLength); - olm_sas_get_pubkey(olmData, keyBytes.data(), pubkeyLength); - sendEvent(m_remoteUserId, m_remoteDeviceId, - KeyVerificationKeyEvent(m_transactionId, - QString::fromLatin1(keyBytes)), - m_encrypted); + m_connection->d->acceptKeyVerification(this); } - -void KeyVerificationSession::cancelVerification(Error error) +void KeyVerificationSession::confirm() { - sendEvent(m_remoteUserId, m_remoteDeviceId, KeyVerificationCancelEvent(m_transactionId, - errorToString(error)), m_encrypted); - setState(CANCELED); - setError(error); - emit finished(); - deleteLater(); + m_connection->d->confirmKeyVerification(this); } -void KeyVerificationSession::sendReady() +void KeyVerificationSession::setState(State state) { - auto methods = commonSupportedMethods(m_remoteSupportedMethods); - - if (methods.isEmpty()) { - cancelVerification(UNKNOWN_METHOD); + if (m_state == state) { return; } - sendEvent( - m_remoteUserId, m_remoteDeviceId, - KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), - methods), - m_encrypted); - setState(READY); - - if (methods.size() == 1) { - sendStartSas(); - } -} - -void KeyVerificationSession::sendStartSas() -{ - startSentByUs = true; - KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); - auto fixedJson = event.contentJson(); - if (m_room) { - fixedJson.remove("transaction_id"_L1); - fixedJson["m.relates_to"_L1] = QJsonObject { - {"event_id"_L1, m_requestEventId}, - {"rel_type"_L1, "m.reference"_L1}, - }; + if (state == NOTFOUND) { + return; } - m_startEvent = QString::fromUtf8( - QJsonDocument(fixedJson).toJson(QJsonDocument::Compact)); - sendEvent(m_remoteUserId, m_remoteDeviceId, event, - m_encrypted); - setState(WAITINGFORACCEPT); -} - -void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) -{ - setState(READY); - m_remoteSupportedMethods = event.methods(); - auto methods = commonSupportedMethods(m_remoteSupportedMethods); - // This happens for outgoing user verification - if (m_remoteDeviceId.isEmpty()) { - m_remoteDeviceId = event.fromDevice(); - } - if (methods.isEmpty()) - cancelVerification(UNKNOWN_METHOD); - else if (methods.size() == 1) - sendStartSas(); // -> WAITINGFORACCEPT -} + m_state = state; + Q_EMIT stateChanged(); -void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) -{ - if (startSentByUs) { - if (m_remoteUserId > m_connection->userId() - || (m_remoteUserId == m_connection->userId() - && m_remoteDeviceId > m_connection->deviceId())) { - return; - } - startSentByUs = false; - } - const auto& theirMacs = event.messageAuthenticationCodes(); - for (const auto& macCode : SupportedMacs) - if (theirMacs.contains(macCode)) - m_commonMacCodes.push_back(macCode); - if (m_commonMacCodes.isEmpty()) { - cancelVerification(UNKNOWN_METHOD); - return; - } - const auto pubkeyLength = olm_sas_pubkey_length(olmData); - auto publicKey = byteArrayForOlm(pubkeyLength); - olm_sas_get_pubkey(olmData, publicKey.data(), pubkeyLength); - const auto canonicalEvent = QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); - const auto commitment = - QString::fromLatin1(QCryptographicHash::hash(publicKey + canonicalEvent, - QCryptographicHash::Sha256) - .toBase64(QByteArray::OmitTrailingEquals)); - - sendEvent(m_remoteUserId, m_remoteDeviceId, - KeyVerificationAcceptEvent(m_transactionId, - commitment), - m_encrypted); - setState(ACCEPTED); + qWarning() << "Verification state" << m_state; } -void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +void KeyVerificationSession::setSasState(SasState state) { - QStringList keys = event.mac().keys(); - keys.sort(); - const auto& key = keys.join(","_L1); - const QString edKeyId = "ed25519:"_L1 % m_remoteDeviceId; - - if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, - m_remoteDeviceId), - true, edKeyId) - != event.mac().value(edKeyId)) { - cancelVerification(KEY_MISMATCH); + if (m_sasState == state || state == SASNOTFOUND) { return; } - auto masterKey = m_connection->masterKeyForUser(m_remoteUserId); - if (event.mac().contains("ed25519:"_L1 % masterKey) - && calculateMac(masterKey, true, "ed25519:"_L1 % masterKey) - != event.mac().value("ed25519:"_L1 % masterKey)) { - cancelVerification(KEY_MISMATCH); - return; - } + m_sasState = state; + Q_EMIT sasStateChanged(); - if (calculateMac(key, true) != event.keys()) { - cancelVerification(KEY_MISMATCH); - return; + if (m_sasState == KEYSEXCHANGED) { //TODO move to sasstate + emit sasEmojisChanged(); } + qWarning() << "Sas state" << m_sasState; - m_pendingEdKeyId = edKeyId; - m_pendingMasterKey = masterKey; - - if (m_verified) { - trustKeys(); + // TODO: only accept if the verification started from a request, otherwise we're just auto-accepting things. + // I'm not sure if unexpected m.key.verification.start's will even work, though. And i honestly don't care. + if (m_sasState == STARTED && !weStarted) { + m_connection->d->acceptSas(this); } } -void KeyVerificationSession::trustKeys() +QVector KeyVerificationSession::sasEmojis() { - m_connection->database()->setSessionVerified(m_pendingEdKeyId); - m_connection->database()->setMasterKeyVerified(m_pendingMasterKey); - if (m_remoteUserId == m_connection->userId()) { - m_connection->reloadDevices(); - } - - if (!m_pendingMasterKey.isEmpty()) { - if (m_remoteUserId == m_connection->userId()) { - const auto selfSigningKey = m_connection->database()->loadEncrypted("m.cross_signing.self_signing"_L1); - if (!selfSigningKey.isEmpty()) { - QHash> signatures; - auto json = QJsonObject { - {"keys"_L1, QJsonObject { - {"ed25519:"_L1 + m_remoteDeviceId, m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId)}, - {"curve25519:"_L1 + m_remoteDeviceId, m_connection->curveKeyForUserDevice(m_remoteUserId, m_remoteDeviceId)}, - }}, - {"algorithms"_L1, QJsonArray {"m.olm.v1.curve25519-aes-sha2"_L1, "m.megolm.v1.aes-sha2"_L1}}, - {"device_id"_L1, m_remoteDeviceId}, - {"user_id"_L1, m_remoteUserId}, - }; - auto signature = sign(selfSigningKey, QJsonDocument(json).toJson(QJsonDocument::Compact)); - json["signatures"_L1] = QJsonObject { - {m_connection->userId(), QJsonObject { - {"ed25519:"_L1 + m_connection->database()->selfSigningPublicKey(), QString::fromLatin1(signature)}, - }}, - }; - signatures[m_remoteUserId][m_remoteDeviceId] = json; - auto uploadSignatureJob = m_connection->callApi(signatures); - connect(uploadSignatureJob, &BaseJob::finished, m_connection, [uploadSignatureJob]() { - if (uploadSignatureJob->error() != BaseJob::Success) { - qCWarning(E2EE) << "Failed to upload self-signing signature" << uploadSignatureJob << uploadSignatureJob->error() << uploadSignatureJob->errorString(); - } - }); - } else { - // Not parenting to this since the session is going to be destroyed soon - auto handler = new SSSSHandler(m_connection); - handler->setConnection(m_connection); - handler->unlockSSSSFromCrossSigning(); - connect(handler, &SSSSHandler::finished, handler, &QObject::deleteLater); - } - } else { - const auto userSigningKey = m_connection->database()->loadEncrypted("m.cross_signing.user_signing"_L1); - if (!userSigningKey.isEmpty()) { - QHash> signatures; - auto json = QJsonObject { - {"keys"_L1, QJsonObject { - {"ed25519:"_L1 + m_pendingMasterKey, m_pendingMasterKey}, - }}, - {"usage"_L1, QJsonArray {"master"_L1}}, - {"user_id"_L1, m_remoteUserId}, - }; - auto signature = sign(userSigningKey, QJsonDocument(json).toJson(QJsonDocument::Compact)); - json["signatures"_L1] = QJsonObject { - {m_connection->userId(), QJsonObject { - {"ed25519:"_L1 + m_connection->database()->userSigningPublicKey(), QString::fromLatin1(signature)}, - }}, - }; - signatures[m_remoteUserId][m_pendingMasterKey] = json; - auto uploadSignatureJob = m_connection->callApi(signatures); - connect(uploadSignatureJob, &BaseJob::finished, m_connection, [uploadSignatureJob, userId = m_remoteUserId](){ - if (uploadSignatureJob->error() != BaseJob::Success) { - qCWarning(E2EE) << "Failed to upload user-signing signature for" << userId << uploadSignatureJob << uploadSignatureJob->error() << uploadSignatureJob->errorString(); - } - }); - } - } - emit m_connection->userVerified(m_remoteUserId); - } - - emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); - macReceived = true; + auto raw = m_connection->d->keyVerificationSasEmoji(this); - if (state() == WAITINGFORMAC) { - setState(DONE); - sendDone(); - emit finished(); - deleteLater(); + QVector out; + for (const auto& [symbol, description] : raw) { + out += { + symbol, description + }; } + return out; } -QVector KeyVerificationSession::sasEmojis() const -{ - return m_sasEmojis; -} - -void KeyVerificationSession::sendRequest() -{ - sendEvent( - m_remoteUserId, m_remoteDeviceId, - KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), - supportedMethods, - QDateTime::currentDateTime()), - m_encrypted); - setState(WAITINGFORREADY); -} - -KeyVerificationSession::State KeyVerificationSession::state() const -{ - return m_state; -} - -void KeyVerificationSession::setState(KeyVerificationSession::State state) -{ - qCDebug(E2EE) << "KeyVerificationSession state" << m_state << "->" << state; - m_state = state; - emit stateChanged(); -} - -KeyVerificationSession::Error KeyVerificationSession::error() const +QString KeyVerificationSession::remoteDevice() const { - return m_error; + return m_remoteDeviceId; } -void KeyVerificationSession::setError(Error error) +void KeyVerificationSession::setVerificationId(const QString& verificationId) { - m_error = error; - emit errorChanged(); + m_verificationId = verificationId; } -QString KeyVerificationSession::errorToString(Error error) +QString KeyVerificationSession::remoteUser() const { - switch(error) { - case NONE: - return "none"_L1; - case TIMEOUT: - return "m.timeout"_L1; - case USER: - return "m.user"_L1; - case UNEXPECTED_MESSAGE: - return "m.unexpected_message"_L1; - case UNKNOWN_TRANSACTION: - return "m.unknown_transaction"_L1; - case UNKNOWN_METHOD: - return "m.unknown_method"_L1; - case KEY_MISMATCH: - return "m.key_mismatch"_L1; - case USER_MISMATCH: - return "m.user_mismatch"_L1; - case INVALID_MESSAGE: - return "m.invalid_message"_L1; - case SESSION_ACCEPTED: - return "m.accepted"_L1; - case MISMATCHED_COMMITMENT: - return "m.mismatched_commitment"_L1; - case MISMATCHED_SAS: - return "m.mismatched_sas"_L1; - default: - return "m.user"_L1; - } + return m_remoteUserId; } -KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) +QString KeyVerificationSession::verificationId() const { - if (error == "m.timeout"_L1) - return REMOTE_TIMEOUT; - if (error == "m.user"_L1) - return REMOTE_USER; - if (error == "m.unexpected_message"_L1) - return REMOTE_UNEXPECTED_MESSAGE; - if (error == "m.unknown_message"_L1) - return REMOTE_UNEXPECTED_MESSAGE; - if (error == "m.unknown_transaction"_L1) - return REMOTE_UNKNOWN_TRANSACTION; - if (error == "m.unknown_method"_L1) - return REMOTE_UNKNOWN_METHOD; - if (error == "m.key_mismatch"_L1) - return REMOTE_KEY_MISMATCH; - if (error == "m.user_mismatch"_L1) - return REMOTE_USER_MISMATCH; - if (error == "m.invalid_message"_L1) - return REMOTE_INVALID_MESSAGE; - if (error == "m.accepted"_L1) - return REMOTE_SESSION_ACCEPTED; - if (error == "m.mismatched_commitment"_L1) - return REMOTE_MISMATCHED_COMMITMENT; - if (error == "m.mismatched_sas"_L1) - return REMOTE_MISMATCHED_SAS; - return NONE; + return m_verificationId; } -QString KeyVerificationSession::remoteDeviceId() const +Room* KeyVerificationSession::room() const { - return m_remoteDeviceId; + return m_room; } -QString KeyVerificationSession::transactionId() const +KeyVerificationSession* KeyVerificationSession::processIncomingUserVerification(Room* room, const QString& eventId) { - return m_transactionId; + return new KeyVerificationSession(room, room->connection(), eventId); } -void KeyVerificationSession::sendEvent(const QString &userId, const QString &deviceId, const KeyVerificationEvent &event, bool encrypted) +void KeyVerificationSession::startSas() { - if (m_room) { - auto json = event.contentJson(); - json.remove("transaction_id"_L1); - if (event.metaType().matrixId == KeyVerificationRequestEvent::TypeId) { - json["msgtype"_L1] = event.matrixType(); - json["body"_L1] = m_connection->userId() + " sent a verification request"_L1; - json["to"_L1] = m_remoteUserId; - m_room->postJson("m.room.message"_L1, json); - } else { - json["m.relates_to"_L1] = QJsonObject { - {"event_id"_L1, m_requestEventId}, - {"rel_type"_L1, "m.reference"_L1} - }; - m_room->postJson(event.matrixType(), json); - } - } else { - m_connection->sendToDevice(userId, deviceId, event, encrypted); + if (m_state != READY) { + return; } + //TODO: resolve glare + weStarted = true; + m_connection->d->startKeyVerification(this); } -bool KeyVerificationSession::userVerification() const -{ - return m_room; -} - -void KeyVerificationSession::setRequestEventId(const QString& eventId) +KeyVerificationSession* KeyVerificationSession::selfVerification(const QString& verificationId, Connection* connection) { - m_requestEventId = eventId; + return new KeyVerificationSession(connection->userId(), verificationId, {}, connection); } diff --git a/Quotient/keyverificationsession.h b/Quotient/keyverificationsession.h index a28db2ecb..f14c9386d 100644 --- a/Quotient/keyverificationsession.h +++ b/Quotient/keyverificationsession.h @@ -3,27 +3,28 @@ #pragma once -#include "events/keyverificationevent.h" -#include "events/roommessageevent.h" +#include "quotient_export.h" #include #include -struct OlmSAS; +class QTimer; namespace Quotient { class Connection; class Room; struct QUOTIENT_API EmojiEntry { - QString emoji; - QString description; - Q_GADGET + //! \brief Unicode of the emoji Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) + //! \brief Textual description of the emoji + //! This follows https://spec.matrix.org/v1.11/client-server-api/#sas-method-emoji Q_PROPERTY(QString description MEMBER description CONSTANT) public: + QString emoji; + QString description; friend bool operator==(const EmojiEntry&, const EmojiEntry&) = default; }; @@ -34,152 +35,96 @@ struct QUOTIENT_API EmojiEntry { class QUOTIENT_API KeyVerificationSession : public QObject { Q_OBJECT - public: enum State { - INCOMING, ///< There is a request for verification incoming - //! We sent a request for verification and are waiting for ready - WAITINGFORREADY, - //! Either party sent a ready as a response to a request; the user - //! selects a method - READY, - WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept - ACCEPTED, ///< The other party sent an accept and is waiting for a key - WAITINGFORKEY, ///< We're waiting for a key - //! We're waiting for the *user* to verify the emojis - WAITINGFORVERIFICATION, - WAITINGFORMAC, ///< We're waiting for the mac - CANCELED, ///< The session has been canceled - DONE, ///< The verification is done + CREATED, //! The verification request has been newly created by us. + REQUESTED, //! The verification request was received from the other party. + READY, //! The verification request is ready to start a verification flow. + TRANSITIONED, //! The verification request has transitioned into a concrete verification flow. For example it transitioned into the emoji based SAS verification. + DONE, //! The verification flow that was started with this request has finished. + CANCELLED, //! The verification process has been cancelled. + NOTFOUND, }; Q_ENUM(State) - enum Error { - NONE, - TIMEOUT, - REMOTE_TIMEOUT, - USER, - REMOTE_USER, - UNEXPECTED_MESSAGE, - REMOTE_UNEXPECTED_MESSAGE, - UNKNOWN_TRANSACTION, - REMOTE_UNKNOWN_TRANSACTION, - UNKNOWN_METHOD, - REMOTE_UNKNOWN_METHOD, - KEY_MISMATCH, - REMOTE_KEY_MISMATCH, - USER_MISMATCH, - REMOTE_USER_MISMATCH, - INVALID_MESSAGE, - REMOTE_INVALID_MESSAGE, - SESSION_ACCEPTED, - REMOTE_SESSION_ACCEPTED, - MISMATCHED_COMMITMENT, - REMOTE_MISMATCHED_COMMITMENT, - MISMATCHED_SAS, - REMOTE_MISMATCHED_SAS, + enum SasState { + STARTED, //! The verification has been started, the protocols that should be used have been proposed and can be accepted. + ACCEPTED, //! The verification has been accepted and both sides agreed to a set of protocols that will be used for the verification process. + KEYSEXCHANGED, //! The public keys have been exchanged and the short auth string can be presented to the user. + CONFIRMED, //! The verification process has been confirmed from our side, we’re waiting for the other side to confirm as well. + SASDONE, //! The verification process has been successfully concluded. + SASCANCELLED, //! The verification process has been cancelled. + SASNOTFOUND, }; - Q_ENUM(Error) + Q_ENUM(SasState) - Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + //! \brief The matrix id of the user we're verifying. + //! For device verification this is our own id. Q_PROPERTY(QString remoteUserId MEMBER m_remoteUserId CONSTANT) - Q_PROPERTY(QVector sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) - Q_PROPERTY(State state READ state NOTIFY stateChanged) - Q_PROPERTY(Error error READ error NOTIFY errorChanged) - // Whether this is a user verification (in contrast to a device verification) - Q_PROPERTY(bool userVerification READ userVerification CONSTANT) - // Incoming device verification - KeyVerificationSession(QString remoteUserId, - const KeyVerificationRequestEvent& event, - Connection* connection, bool encrypted); + //! \brief The device id of the device we're verifying + //! Does not have a specified value when verifying a different user + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) - // Outgoing device verification - KeyVerificationSession(QString userId, QString deviceId, - Connection* connection); + //! \brief The current state of the verification session + //! Clients should use this to adapt their UI to the current stage of the verification + Q_PROPERTY(State state MEMBER m_state NOTIFY stateChanged) - // Incoming user verification - KeyVerificationSession(const RoomMessageEvent *event, Room *room); + //! \brief The current state of the sas verification + //! Clients should use this to adapt their UI to the current stage of the verification + Q_PROPERTY(SasState sasState MEMBER m_sasState NOTIFY sasStateChanged) - // Outgoing user verification - explicit KeyVerificationSession(Room *room); + //! \brief The sas emoji that should be shown to the user. + //! Only has a specified value when the session is in TRANSITIONED state + Q_PROPERTY(QVector sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) - void handleEvent(const KeyVerificationEvent& baseEvent); + KeyVerificationSession(const QString& remoteUserId, const QString& verificationId, const QString& remoteDeviceId, Quotient::Connection* connection); + KeyVerificationSession(Room* room, Quotient::Connection* connection, const QString& verificationId = {}); - QVector sasEmojis() const; - State state() const; + //! \brief Accept an incoming verification session + Q_INVOKABLE void accept(); - Error error() const; + //! \brief Confirm that the emojis shown to the user match + //! This will mark the remote session / user as verified and send an m.key.verification.mac event + //! Only call this after the user has confirmed the correctness of the emoji! + Q_INVOKABLE void confirm(); - QString remoteDeviceId() const; - QString transactionId() const; - bool userVerification() const; + //! \brief Start a SAS verification + Q_INVOKABLE void startSas(); - void setRequestEventId(const QString &eventId); + QVector sasEmojis(); + QString remoteUser() const; + QString remoteDevice() const; + QString verificationId() const; -public Q_SLOTS: - void sendRequest(); - void sendReady(); - void sendMac(); - void sendStartSas(); - void sendKey(); - void sendDone(); - void cancelVerification(Error error); + static KeyVerificationSession* requestDeviceVerification(const QString& userId, const QString& deviceId, Connection* connection); + static KeyVerificationSession* requestUserVerification(Room* room, Connection* connection); + static KeyVerificationSession* selfVerification(const QString& verificationId, Connection* connection); -Q_SIGNALS: - void keyReceived(); - void sasEmojisChanged(); - void stateChanged(); - void errorChanged(); - void finished(); + static KeyVerificationSession* processIncomingUserVerification(Room* room, const QString& eventId); -private: - // Internal delegating constructors + Quotient::Room* room() const; - KeyVerificationSession(QString remoteUserId, Connection* connection, QString remoteDeviceId, - bool encrypted, QStringList methods, QDateTime startTimestamp, - QString transactionId, Room* room = nullptr, QString requestEventId = {}); - KeyVerificationSession(QString remoteUserId, Connection* connection, Room* room, - QString remoteDeviceId = {}, QString transactionId = {}); - - Connection* const m_connection; - QPointer m_room; - const QString m_remoteUserId; +private: + QString m_remoteUserId; + QString m_verificationId; QString m_remoteDeviceId; - QString m_transactionId; - bool m_encrypted = false; - QStringList m_remoteSupportedMethods{}; - QStringList m_commonMacCodes{}; - - CStructPtr olmDataHolder = makeOlmData(); - OlmSAS* olmData = olmDataHolder.get(); - QVector m_sasEmojis; - bool startSentByUs = false; - State m_state = INCOMING; - Error m_error = NONE; - QString m_startEvent{}; - QString m_commitment{}; - bool macReceived = false; - bool m_verified = false; - QString m_pendingEdKeyId{}; - QString m_pendingMasterKey{}; - QString m_requestEventId{}; - - static CStructPtr makeOlmData(); - void handleReady(const KeyVerificationReadyEvent& event); - void handleStart(const KeyVerificationStartEvent& event); - void handleKey(const KeyVerificationKeyEvent& event); - void handleMac(const KeyVerificationMacEvent& event); - void setupTimeout(std::chrono::milliseconds timeout); + QPointer m_connection; + QPointer m_room; + State m_state = REQUESTED; + SasState m_sasState = SASNOTFOUND; + bool weStarted = false; + QTimer *m_processTimer = nullptr; + void setState(State state); - void setError(Error error); - static QString errorToString(Error error); - static Error stringToError(const QString& error); - void trustKeys(); - void sendEvent(const QString &userId, const QString &deviceId, const KeyVerificationEvent &event, bool encrypted); - - QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_L1); - QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_L1); + void setSasState(SasState state); + void setVerificationId(const QString& verificationId); + + friend class Quotient::Connection; +Q_SIGNALS: + void stateChanged(); + void sasStateChanged(); + void sasEmojisChanged(); }; } // namespace Quotient diff --git a/Quotient/logging_categories_p.h b/Quotient/logging_categories_p.h index 4919a769a..0a9cccaa7 100644 --- a/Quotient/logging_categories_p.h +++ b/Quotient/logging_categories_p.h @@ -23,7 +23,6 @@ QUO_LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") QUO_LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") QUO_LOGGING_CATEGORY(NETWORK, "quotient.network") QUO_LOGGING_CATEGORY(PROFILER, "quotient.profiler") -QUO_LOGGING_CATEGORY(DATABASE, "quotient.database") } // namespace Quotient diff --git a/Quotient/room.cpp b/Quotient/room.cpp index e48e20736..82424a66b 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -12,7 +12,6 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "database.h" #include "eventstats.h" #include "keyverificationsession.h" #include "logging_categories_p.h" @@ -39,8 +38,6 @@ #include "csapi/tags.h" #include "e2ee/e2ee_common.h" -#include "e2ee/qolmaccount.h" -#include "e2ee/qolminboundsession.h" #include "events/callevents.h" #include "events/encryptionevent.h" @@ -56,6 +53,7 @@ #include "events/roomtombstoneevent.h" #include "events/simplestateevents.h" #include "events/typingevent.h" +#include "events/keyverificationevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" @@ -150,9 +148,6 @@ class Q_DECL_HIDDEN Room::Private { JobHandle allMembersJob; //! Map from megolm sessionId to set of eventIds std::unordered_map> undecryptedEvents; - //! Map from event id of the request event to the session object - QHash keyVerificationSessions; - QPointer pendingKeyVerificationSession; struct FileTransferPrivateInfo { FileTransferPrivateInfo() = default; @@ -337,121 +332,6 @@ class Q_DECL_HIDDEN Room::Private { bool isLocalMember(const QString& memberId) const { return memberId == connection->userId(); } - std::unordered_map groupSessions; - std::optional currentOutboundMegolmSession = {}; - - bool addInboundGroupSession(QByteArray sessionId, QByteArray sessionKey, - const QString& senderId, - const QByteArray& olmSessionId, const QByteArray& senderKey, const QByteArray& senderEdKey) - { - if (groupSessions.contains(sessionId)) { - qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; - return false; - } - - auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey); - Q_ASSERT(expectedMegolmSession.has_value()); - auto&& megolmSession = *expectedMegolmSession; - if (megolmSession.sessionId() != sessionId) { - qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; - return false; - } - megolmSession.setSenderId(senderId); - megolmSession.setOlmSessionId(olmSessionId); - qCWarning(E2EE) << "Adding inbound session" << sessionId; - connection->saveMegolmSession(q, megolmSession, senderKey, senderEdKey); - groupSessions.try_emplace(sessionId, std::move(megolmSession)); - return true; - } - - QString groupSessionDecryptMessage(const QByteArray& ciphertext, - const QByteArray& sessionId, - const QString& eventId, - const QDateTime& timestamp, - const QString& senderId) - { - auto groupSessionIt = groupSessions.find(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"; - // TODO: request the keys - return {}; - } - auto& senderSession = groupSessionIt->second; - if (senderSession.senderId() != "BACKUP"_L1 && senderSession.senderId() != senderId) { - qCWarning(E2EE) << "Sender from event does not match sender from session"; - return {}; - } - auto decryptResult = senderSession.decrypt(ciphertext); - if(!decryptResult) { - qCWarning(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << decryptResult.error(); - return {}; - } - const auto& [content, index] = *decryptResult; - const auto& [recordEventId, ts] = - q->connection()->database()->groupSessionIndexRecord( - q->id(), QString::fromLatin1(senderSession.sessionId()), index); - if (recordEventId.isEmpty()) { - q->connection()->database()->addGroupSessionIndexRecord( - q->id(), QString::fromLatin1(senderSession.sessionId()), index, eventId, - timestamp.toMSecsSinceEpoch()); - } else { - if ((eventId != recordEventId) - || (ts != timestamp.toMSecsSinceEpoch())) { - qCWarning(E2EE) << "Detected a replay attack on event" << eventId; - return {}; - } - } - return QString::fromUtf8(content); - } - - bool shouldRotateMegolmSession() const - { - const auto* encryptionConfig = currentState.get(); - if (!encryptionConfig || !encryptionConfig->useEncryption()) - return false; - - const auto rotationInterval = encryptionConfig->rotationPeriodMs(); - const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs(); - return currentOutboundMegolmSession->messageCount() - >= rotationMessageCount - || currentOutboundMegolmSession->creationTime().addMSecs( - rotationInterval) - < QDateTime::currentDateTime(); - } - - bool hasValidMegolmSession() const - { - return q->usesEncryption() && currentOutboundMegolmSession.has_value(); - } - - void createMegolmSession() { - qCDebug(E2EE) << "Creating new outbound megolm session for room " - << q->objectName(); - currentOutboundMegolmSession.emplace(); - connection->database()->saveCurrentOutboundMegolmSession( - id, *currentOutboundMegolmSession); - - addInboundGroupSession(currentOutboundMegolmSession->sessionId(), - currentOutboundMegolmSession->sessionKey(), - q->localMember().id(), QByteArrayLiteral("SELF"), - connection->curveKeyForUserDevice(connection->userId(), connection->deviceId()).toLatin1(), - connection->edKeyForUserDevice(connection->userId(), connection->deviceId()).toLatin1()); - } - - QMultiHash getDevicesWithoutKey() const - { - QMultiHash devices; - for (const auto& user : memberNameMap.values() + membersInvited) - for (const auto& deviceId : connection->devicesForUser(user)) - devices.insert(user, deviceId); - - return connection->database()->devicesWithoutKey( - id, devices, currentOutboundMegolmSession->sessionId()); - } - private: Room::Timeline::size_type mergePendingEvent(PendingEvents::iterator localEchoIt, RoomEvents::iterator remoteEchoIt); @@ -469,30 +349,8 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name if (connection->encryptionEnabled()) { - connect(this, &Room::encryption, this, - [this, connection] { connection->encryptionUpdate(this); }); - connect(this, &Room::memberListChanged, this, [this, connection] { - if(usesEncryption()) { - connection->encryptionUpdate(this, d->membersInvited); - } - }); - d->groupSessions = connection->loadRoomMegolmSessions(this); - d->currentOutboundMegolmSession = - connection->database()->loadCurrentOutboundMegolmSession(id); - if (d->currentOutboundMegolmSession - && d->shouldRotateMegolmSession()) { - d->currentOutboundMegolmSession.reset(); - } - connect(this, &Room::memberLeft, this, [this] { - if (d->hasValidMegolmSession()) { - qCDebug(E2EE) - << "Rotating the megolm session because a user left"; - d->createMegolmSession(); - } - }); - - connect(this, &Room::beforeDestruction, this, [id, connection] { - connection->database()->clearRoomData(id); + connect(this, &Room::memberLeft, this, [] { + //TODO do we need to manually rotate the megolm session? i think this is done through the members that are passed to the encrypt call }); } qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; @@ -1548,10 +1406,12 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) << encryptedEvent.id() << "is not supported"; return {}; } - QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.sessionId().toLatin1(), - encryptedEvent.id(), encryptedEvent.originTimestamp(), - encryptedEvent.senderId()); + + auto decrypted = connection()->decryptRoomEvent(this, QJsonDocument(encryptedEvent.fullJson()).toJson()); + // QString decrypted = d->groupSessionDecryptMessage( + // encryptedEvent.ciphertext(), encryptedEvent.sessionId().toLatin1(), + // encryptedEvent.id(), encryptedEvent.originTimestamp(), + // encryptedEvent.senderId()); if (decrypted.isEmpty()) { // qCWarning(E2EE) << "Encrypted message is empty"; return {}; @@ -1565,41 +1425,6 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) return {}; } -void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, - const QString& senderId, - const QByteArray& olmSessionId, - const QByteArray& senderKey, - const QByteArray& senderEdKey) -{ - if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { - qCWarning(E2EE) << "Ignoring unsupported algorithm" - << roomKeyEvent.algorithm() << "in m.room_key event"; - } - if (d->addInboundGroupSession(roomKeyEvent.sessionId().toLatin1(), - roomKeyEvent.sessionKey(), senderId, - olmSessionId, senderKey, senderEdKey)) { - qCWarning(E2EE) << "added new inboundGroupSession:" - << d->groupSessions.size(); - const auto undecryptedEvents = - d->undecryptedEvents[roomKeyEvent.sessionId()]; - for (const auto& eventId : undecryptedEvents) { - 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()) { - if (auto decrypted = decryptMessage(*encryptedEvent)) { - auto&& oldEvent = eventCast( - ti.replaceEvent(std::move(decrypted))); - ti->setOriginalEvent(std::move(oldEvent)); - emit replacedEvent(ti.event(), ti->originalEvent()); - d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; - } - } - } - } -} - int Room::joinedCount() const { return d->summary.joinedMemberCount.value_or(0); @@ -1941,14 +1766,13 @@ const PendingEventItem& Room::Private::sendEvent(RoomEventPtr&& event) return doSendEvent(addAsPending(std::move(event))); } +//TODO encrypted files / images / videos const PendingEventItem& Room::Private::doSendEvent(PendingEvents::iterator eventItemIter) { Q_ASSERT(eventItemIter != unsyncedEvents.end()); const auto& eventItem = *eventItemIter; const auto txnId = eventItem->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. - const RoomEvent* _event = eventItemIter->event(); - std::unique_ptr encryptedEvent; if (!q->successorId().isEmpty()) { // TODO: replace with a proper power levels check qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; @@ -1965,64 +1789,89 @@ const PendingEventItem& Room::Private::doSendEvent(PendingEvents::iterator event onEventSendingFailure(eventItemIter); return eventItem; } - if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { - createMegolmSession(); - } - // Send the session to other people - connection->sendSessionKeyToDevices(id, *currentOutboundMegolmSession, - getDevicesWithoutKey()); - - const auto encrypted = currentOutboundMegolmSession->encrypt( - QJsonDocument(eventItem->fullJson()).toJson()); - currentOutboundMegolmSession->setMessageCount( - currentOutboundMegolmSession->messageCount() + 1); - connection->database()->saveCurrentOutboundMegolmSession( - id, *currentOutboundMegolmSession); - encryptedEvent = makeEvent( - encrypted, connection->olmAccount()->identityKeys().curve25519, - connection->deviceId(), QString::fromLatin1(currentOutboundMegolmSession->sessionId())); - encryptedEvent->setTransactionId(connection->generateTxnId()); - encryptedEvent->setRoomId(id); - encryptedEvent->setSender(connection->userId()); - if (eventItem->contentJson().contains(RelatesToKey)) { - encryptedEvent->setRelation(eventItem->contentJson()[RelatesToKey].toObject()); - } - // We show the unencrypted event locally while pending. The echo - // check will throw the encrypted version out - _event = encryptedEvent.get(); - } - - if (auto call = connection->callApi(BackgroundRequest, id, _event->matrixType(), - txnId, _event->contentJson())) { - // Below - find pending events by txnIds again because PendingEventItems may move around - // as unsyncedEvents vector grows. - Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { - auto it = q->findPendingEvent(txnId); - if (it == unsyncedEvents.end()) { - qWarning(EVENTS) << "Pending event for transaction" << txnId - << "not found - got synced so soon?"; - return; + auto type = eventItem->matrixType(); + auto contentJson = eventItem->contentJson(); + + connection->shareRoomKey(q).then([this, txnId, eventItemIter, type, contentJson](){ + const RoomEvent* _event = eventItemIter->event(); + std::unique_ptr encryptedEvent; + auto content = QJsonDocument::fromJson(connection->encryptRoomEvent(q, QJsonDocument(contentJson).toJson(), type).toUtf8()).object(); + encryptedEvent = makeEvent(content[u"ciphertext"_s].toString().toUtf8(), content[u"sender_key"_s].toString(), content[u"device_id"_s].toString(), content[u"session_id"_s].toString()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + if (contentJson.contains(RelatesToKey)) { + encryptedEvent->setRelation(contentJson[RelatesToKey].toObject()); } - it->setDeparted(); - emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); - }); - call.onResult(q, [this, txnId, call] { - auto it = q->findPendingEvent(txnId); - if (!call->status().good()) { - onEventSendingFailure(it, call); - return; - } - if (it != unsyncedEvents.end()) - onEventReachedServer(it, call->eventId()); - else - qDebug(EVENTS) << "Pending event for transaction" << txnId - << "already merged"; + // We show the unencrypted event locally while pending. The echo + // check will throw the encrypted version out + _event = encryptedEvent.get(); + if (auto call = connection->callApi(BackgroundRequest, id, _event->matrixType(), + txnId, _event->contentJson())) { + // Below - find pending events by txnIds again because PendingEventItems may move around + // as unsyncedEvents vector grows. + Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { + auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) { + qWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + }); + call.onResult(q, [this, txnId, call] { + auto it = q->findPendingEvent(txnId); + if (!call->status().good()) { + onEventSendingFailure(it, call); + return; + } + if (it != unsyncedEvents.end()) + onEventReachedServer(it, call->eventId()); + else + qDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; - emit q->messageSent(txnId, call->eventId()); + emit q->messageSent(txnId, call->eventId()); + }); + } else + onEventSendingFailure(eventItemIter); }); - } else - onEventSendingFailure(eventItemIter); + } else { + if (auto call = connection->callApi(BackgroundRequest, id, eventItem->matrixType(), + txnId, eventItem->contentJson())) { + // Below - find pending events by txnIds again because PendingEventItems may move around + // as unsyncedEvents vector grows. + Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { + auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) { + qWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + }); + call.onResult(q, [this, txnId, call] { + auto it = q->findPendingEvent(txnId); + if (!call->status().good()) { + onEventSendingFailure(it, call); + return; + } + if (it != unsyncedEvents.end()) + onEventReachedServer(it, call->eventId()); + else + qDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; + + emit q->messageSent(txnId, call->eventId()); + }); + } else + onEventSendingFailure(eventItemIter); + } + + //TODO return future! return eventItem; } @@ -2134,9 +1983,9 @@ QString Room::postPlainText(const QString& plainText) QString Room::postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type) { - return post(plainText, type, - std::make_unique(html, u"text/html"_s)) - ->transactionId(); + post(plainText, type, + std::make_unique(html, u"text/html"_s)); + return {}; //TODO } QString Room::postHtmlText(const QString& plainText, const QString& html) @@ -2899,40 +2748,41 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) emit q->callEvent(q, evt); for (auto it = from; it != syncEdge(); ++it) { - if (it->event()->senderId() == connection->userId()) { - if (const auto* evt = it->viewAs()) { - if (evt->rawMsgtype() == "m.key.verification.request"_L1 && pendingKeyVerificationSession && evt->senderId() == q->localMember().id()) { - keyVerificationSessions[evt->id()] = pendingKeyVerificationSession; - connect(pendingKeyVerificationSession.get(), &QObject::destroyed, q, [this, evt] { - keyVerificationSessions.remove(evt->id()); - }); - pendingKeyVerificationSession->setRequestEventId(evt->id()); - pendingKeyVerificationSession.clear(); - } - } - continue; - } + // if (it->event()->senderId() == connection->userId()) { + // if (const auto* evt = it->viewAs()) { + // if (evt->rawMsgtype() == "m.key.verification.request"_L1 && pendingKeyVerificationSession && evt->senderId() == q->localMember().id()) { + // keyVerificationSessions[evt->id()] = pendingKeyVerificationSession; + // connect(pendingKeyVerificationSession.get(), &QObject::destroyed, q, [this, evt] { + // keyVerificationSessions.remove(evt->id()); + // }); + // //pendingKeyVerificationSession->setRequestEventId(evt->id()); + // pendingKeyVerificationSession.clear(); + // } + // } + // continue; + // } if (const auto* evt = it->viewAs()) { - if (evt->rawMsgtype() == "m.key.verification.request"_L1) { + if (evt->rawMsgtype() == "m.key.verification.request"_L1 && evt->senderId() != q->connection()->userId()) { if (evt->originTimestamp() > QDateTime::currentDateTime().addSecs(-60)) { - auto session = new KeyVerificationSession(evt, q); + auto json = evt->fullJson(); + if (!json.contains(u"room_id"_s)) { + json[u"room_id"_s] = q->id(); //TODO ?????????????????????????????????? + } + q->connection()->receiveVerificationEvent(QJsonDocument(json).toJson(QJsonDocument::Compact)); + auto session = KeyVerificationSession::processIncomingUserVerification(q, evt->id()); emit connection->newKeyVerificationSession(session); - keyVerificationSessions[evt->id()] = session; - connect(session, &QObject::destroyed, q, [this, evt] { - keyVerificationSessions.remove(evt->id()); - }); } } } if (auto event = it->viewAs()) { - const auto &baseEvent = event->contentJson()["m.relates_to"_L1]["event_id"_L1].toString(); - if (event->matrixType() == "m.key.verification.done"_L1) { + if (event->senderId() == connection->userId()) { continue; } - if (keyVerificationSessions.contains(baseEvent)) { - keyVerificationSessions[baseEvent]->handleEvent(*event); - } else - qCWarning(E2EE) << "Unknown verification session, id" << baseEvent; + auto json = event->fullJson(); + if (!json.contains(u"room_id"_s)) { + json[u"room_id"_s] = q->id(); //TODO ?????????????????????????????????? + } + q->connection()->receiveVerificationEvent(QJsonDocument(json).toJson(QJsonDocument::Compact)); } } @@ -3475,65 +3325,38 @@ void Room::activateEncryption() setState(EncryptionType::MegolmV1AesSha2); } -void Room::addMegolmSessionFromBackup(const QByteArray& sessionId, const QByteArray& sessionKey, uint32_t index, const QByteArray& senderKey, const QByteArray& senderEdKey) -{ - const auto sessionIt = d->groupSessions.find(sessionId); - if (sessionIt != d->groupSessions.end() && sessionIt->second.firstKnownIndex() <= index) - return; - - auto&& importResult = QOlmInboundGroupSession::importSession(sessionKey); - if (!importResult) - return; - // NB: after the next line, sessionIt can be invalid. - auto& session = d->groupSessions - .insert_or_assign(sessionIt, sessionId, - std::move(importResult.value())) - ->second; - session.setOlmSessionId(d->connection->isVerifiedSession(sessionId) - ? QByteArrayLiteral("BACKUP_VERIFIED") - : QByteArrayLiteral("BACKUP")); - session.setSenderId("BACKUP"_L1); - d->connection->saveMegolmSession(this, session, senderKey, senderEdKey); -} - void Room::startVerification() { if (joinedMembers().count() != 2) { return; } - d->pendingKeyVerificationSession = new KeyVerificationSession(this); - emit d->connection->newKeyVerificationSession(d->pendingKeyVerificationSession); + connection()->requestUserVerification(this); } QJsonArray Room::exportMegolmSessions() { QJsonArray sessions; - for (auto& [key, value] : d->groupSessions) { - auto session = value.exportSession(value.firstKnownIndex()); - if (!session.has_value()) { - qCWarning(E2EE) << "Failed to export session" << session.error(); - continue; - } + return sessions; +} - const auto senderClaimedKey = connection()->database()->edKeyForMegolmSession(QString::fromLatin1(value.sessionId())); - const auto senderKey = connection()->database()->senderKeyForMegolmSession(QString::fromLatin1(value.sessionId())); - const auto json = QJsonObject { - {"algorithm"_L1, "m.megolm.v1.aes-sha2"_L1}, - {"forwarding_curve25519_key_chain"_L1, QJsonArray()}, - {"room_id"_L1, id()}, - {"sender_claimed_keys"_L1, QJsonObject{ {"ed25519"_L1, senderClaimedKey} }}, - {"sender_key"_L1, senderKey}, - {"session_id"_L1, QString::fromLatin1(value.sessionId())}, - {"session_key"_L1, QString::fromLatin1(session.value())}, - }; - if (senderClaimedKey.isEmpty() || senderKey.isEmpty()) { - // These are edge-cases for some sessions that were added before libquotient started storing these fields. - // Some clients refuse to the entire file if this is missing for one key, so we shouldn't export the session in this case. - qCWarning(E2EE) << "Session" << value.sessionId() << "has unknown sender key."; +void Room::newSession(const QString& session) +{ + const auto undecryptedEvents = + d->undecryptedEvents[session]; + for (const auto& eventId : undecryptedEvents) { + 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()) { + if (auto decrypted = decryptMessage(*encryptedEvent)) { + auto&& oldEvent = eventCast( + ti.replaceEvent(std::move(decrypted))); + ti->setOriginalEvent(std::move(oldEvent)); + emit replacedEvent(ti.event(), ti->originalEvent()); + d->undecryptedEvents[session] -= eventId; + } } - sessions.append(json); } - return sessions; } diff --git a/Quotient/room.h b/Quotient/room.h index 1059266e5..100df95b0 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -264,11 +264,6 @@ class QUOTIENT_API Room : public QObject { int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); - void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, - const QString& senderId, - const QByteArray& olmSessionId, - const QByteArray& senderKey, - const QByteArray& senderEdKey); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; @@ -745,6 +740,8 @@ class QUOTIENT_API Room : public QObject { QJsonArray exportMegolmSessions(); + void newSession(const QString& newSession); + public Q_SLOTS: /** Check whether the room should be upgraded */ void checkVersion(); diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 24f6b7623..7db6e0fd7 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -16,14 +16,3 @@ endfunction() quotient_add_test(NAME callcandidateseventtest) quotient_add_test(NAME utiltests) -quotient_add_test(NAME testolmaccount) -quotient_add_test(NAME testgroupsession) -quotient_add_test(NAME testolmsession) -if (NOT MSVC) - target_compile_options(testolmsession PRIVATE -fexceptions) -endif() -quotient_add_test(NAME testolmutility) -quotient_add_test(NAME testcryptoutils) -quotient_add_test(NAME testkeyverification) -quotient_add_test(NAME testcrosssigning) -quotient_add_test(NAME testkeyimport) diff --git a/autotests/cross_signing_data.json b/autotests/cross_signing_data.json deleted file mode 100644 index 2108b6a0e..000000000 --- a/autotests/cross_signing_data.json +++ /dev/null @@ -1,328 +0,0 @@ -{ - "device_keys": { - "@aloy:kde.org": { - "YPZRNLXRIR": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "YPZRNLXRIR", - "keys": { - "curve25519:YPZRNLXRIR": "okfX8OH+9OycY9/pgeQDAsDa/dACUxKlkMn+sQj2+mo", - "ed25519:YPZRNLXRIR": "aqLUt997mMPmTQmDsO9t9p1mrH/pNpn6b+cv9ZV/Swk" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:YPZRNLXRIR": "ULDVIsMQYga95KiHLy8s3yJQekmkowCgOBVbFbC9tFhAnxOhqnjgPg7ZWFRbX8Favf1Lp0WppxzlcMySXbZfAA" - } - }, - "user_id": "@aloy:kde.org", - "unsigned": { - "device_display_name": "NeoChat" - } - }, - "VJIBVDKIST": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "VJIBVDKIST", - "keys": { - "curve25519:VJIBVDKIST": "DYzCv4qTnVnPoCX+N6tfwose2l5bDRd2KBhgtajwqn0", - "ed25519:VJIBVDKIST": "9v/k6vPwRMYT1eV16HtrTToC3QoXnMrsdZLJSf950gE" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:VJIBVDKIST": "joYictTxgm0Th6zdi//2v86+WnOVoyKGACZ9flWs59XY6sGd7uOQUwmFYA8B+GSLCvUTMPMrtbPhxs7kvO60CQ" - } - }, - "user_id": "@aloy:kde.org", - "unsigned": { - "device_display_name": "Device" - } - }, - "MOQGQXXGLF": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "MOQGQXXGLF", - "keys": { - "curve25519:MOQGQXXGLF": "yt94oV8k/EBBs3u3y/KuDW9OwEIlbX+MjbxKfw+G/3Y", - "ed25519:MOQGQXXGLF": "8SVRJ8Ctxn89UCXLq92xP4jzftSQfbfHFSgimtV/j8s" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:MOQGQXXGLF": "5PeT5Fp/3KxQYLQWXBu7ldUdaCs5et4O56wB27TjqUUrYzt620Pr+E5uUuaPDve7gYXXtncYCNY5PtFbE6+iBA" - } - }, - "user_id": "@aloy:kde.org", - "unsigned": { - "device_display_name": "NeoChat" - } - }, - "BUJGIJTVJB": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "BUJGIJTVJB", - "keys": { - "curve25519:BUJGIJTVJB": "OX6PkrugavB52qb2VMzjboYZIP+2rdOE/IztVwPzZwg", - "ed25519:BUJGIJTVJB": "6WFva6hGAnE6ADRSriK00c1cePQnZ8ldxQCyJs4grdk" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:BUJGIJTVJB": "mZ2jT7g9ES/FIffg70uDQH1Y8pkhXM8dpW9s5dMWRNxXDwT4ti8krfCLOrbn47LWUwfF/BPnm8mOlCH6k+vWDg", - "ed25519:YC9mscNFes+PkztOT401XdPNez2712Q4B9Pq7gXOG1M": "5Sx9lsCuyESpefcrS1NkPxfZ4G1LeT7UllShKJn6vlLsQ2qPdVQd7w5MrTlJdIhOAoYeHxeTy5rpFBqxb9f3Bw" - } - }, - "user_id": "@aloy:kde.org", - "unsigned": { - "device_display_name": "app.element.io" - } - } - }, - "@carl:kde.org": { - "DCVUBIMZJJ": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "DCVUBIMZJJ", - "keys": { - "curve25519:DCVUBIMZJJ": "S1RGu1yRn6Q+bw6tjo3fUlOOiwsOp6Hn5kMkjL3z8iY", - "ed25519:DCVUBIMZJJ": "E6K06lkSutNqOKCJA3tzRy4jKsuCl8Kzl2xNXBxHKAw" - }, - "signatures": { - "@carl:kde.org": { - "ed25519:DCVUBIMZJJ": "VzERmB+jWTYa1t46YGdStnnIglVNrLP5eqyXlVIznr9xEmOTdGWZdVmixHnDN0KYLM6VAL3DDXJ5J15TSjsnBw", - "ed25519:JVHbEwSheI3ZYscn48PlrItknkE4ZsCxLDMmGBJITTo": "0Qkqz6Z3s5V+FLaxYLPB5zAErxZbENMdr6KEdbA6gvqGgADbWDRN9O6+9XZVRvVE0nO9qUY0JPx8f6uCMerLAQ" - } - }, - "user_id": "@carl:kde.org", - "unsigned": { - "device_display_name": "A random device" - } - } - }, - "@tobiasfella:kde.org": { - "LTLVYDIVMO": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "LTLVYDIVMO", - "keys": { - "curve25519:LTLVYDIVMO": "pwCBZ5qGHMSUE1PsfoTD+vJHJPBg9tFV7tZvC3C8G0k", - "ed25519:LTLVYDIVMO": "KsOuSdoUX2vNP1N3IwYXudB7+SE/qHhR0Q7sD+zNomM" - }, - "signatures": { - "@tobiasfella:kde.org": { - "ed25519:LTLVYDIVMO": "/mBxlsd0M//iGPEgLhsxc058mNkY7KmIMEFatmcdm9VW3G77PXVo40Hddgzzs6UtcTJ6971+7Osqws3Q5My/Cg", - "ed25519:66xWQ3zub5AMn0u/929opxaawM3HO2mWLTY5OMR5y1I": "NEhuiGlrc0SP30vpUypwEUzx/kRypjLUmTWnbGdnwMJwa1/3JdK86DfLbsz65ymd93md4Av4KckANRMiCN4wDQ" - } - }, - "user_id": "@tobiasfella:kde.org", - "unsigned": { - "device_display_name": "NeoChat fedora 37 x86_64" - } - }, - "IDEJTUJQAF": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "IDEJTUJQAF", - "keys": { - "curve25519:IDEJTUJQAF": "wSiK3dt563r64IxM1DHd++DsjHzi08c0fCxtrjsuBRA", - "ed25519:IDEJTUJQAF": "9j1s53XIxD/bj71QoAzrY6lkmpHbnJt57al6MFwWmSg" - }, - "signatures": { - "@tobiasfella:kde.org": { - "ed25519:IDEJTUJQAF": "4J1Gx/rxOMwqA0sR9qRhReAbUc42Nu89oKkJ9D+MaWeoLO0d98Ml4NPh21YwEZjFVGaC5oeE/jjqsMZOa+gfAQ" - } - }, - "user_id": "@tobiasfella:kde.org", - "unsigned": { - "device_display_name": "NeoChat Android" - } - }, - "VGXPAZVBQO": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "VGXPAZVBQO", - "keys": { - "curve25519:VGXPAZVBQO": "j1WBlKNBOSQmxGztg0blNq1tBZQUYbZWSwgw/cyCJiQ", - "ed25519:VGXPAZVBQO": "LsNqFP6iwwNKZuwQzTtSiEqgsvDWPbFhMVjY1FOT8e8" - }, - "signatures": { - "@tobiasfella:kde.org": { - "ed25519:VGXPAZVBQO": "moZc9uiOpfRgQ/01SZkfUWUT+CT864xcHIJDDaikbB+bMIM5Nx7nTi2CTWOGP3Rl9zuaN8pXmKdowXiQH+hlCw" - } - }, - "user_id": "@tobiasfella:kde.org", - "unsigned": { - "device_display_name": "NeoChat android 12.0 arm64" - } - } - }, - "@tobias:kryptons.eu": { - "YJHFONRPEA": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "YJHFONRPEA", - "keys": { - "curve25519:YJHFONRPEA": "oUFVpFSze7S6A4J49Z7GUrnxta+QIafyfDKegQ3tTAM", - "ed25519:YJHFONRPEA": "r9OFtYn7drAR/tBZ6aIGVVVC/wvrN3FAXBb+MKfLA7w" - }, - "signatures": { - "@tobias:kryptons.eu": { - "ed25519:YJHFONRPEA": "eWPpQbeKI4CEvzSfANm0NzRqTSERk1V5K5i8DF8p6BGUuF0KB7yKqTU/HKnu5LdJIBH4bdf16ap8c8A9VgZOAw" - } - }, - "user_id": "@tobias:kryptons.eu", - "unsigned": { - "device_display_name": "Another random client" - } - } - } - }, - "master_keys": { - "@aloy:kde.org": { - "user_id": "@aloy:kde.org", - "usage": [ - "master" - ], - "keys": { - "ed25519:TAZ6oJMa3xtEObuxgvFMxnAMwzNzyuZ0/A0dPjueYsI": "TAZ6oJMa3xtEObuxgvFMxnAMwzNzyuZ0/A0dPjueYsI" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:BUJGIJTVJB": "hkoB+KVxKOev0cJuTTA4k8TQVh1ePgrA4/wyN2xDWGKGYkEXxoVzui7V7NVI9koZ/DtNOLhfySOG8AeziyNUAg" - }, - "@tobiasfella:kde.org": { - "ed25519:I1t81HEgBNNiDnA3Q5X8ig5IHEngwcUe0/ErjHS7dtU": "9JaiiLNsXncqsKup9Bic4RyjPa6ijXofWAGrQvdibaNuqdfQjsCq4gxTAo+/2HmGw9JBZhIxBrNt+S68obXlDA" - } - } - }, - "@carl:kde.org": { - "user_id": "@carl:kde.org", - "usage": [ - "master" - ], - "keys": { - "ed25519:FAZpIXm2112PIRgeY7oK+5capRD5WKy0bYPQW9Ldnos": "FAZpIXm2112PIRgeY7oK+5capRD5WKy0bYPQW9Ldnos" - }, - "signatures": { - "@carl:kde.org": { - "ed25519:PQUEKFGTFX": "PuJj3YNNz66Pnc7AAj3N4JKKzFfsT6mwmwjcnFxFPiyC4y9j0AgEctWpDbvYOVgCe97a9FEPovEtzag60nWJDw" - } - } - }, - "@tobiasfella:kde.org": { - "user_id": "@tobiasfella:kde.org", - "usage": [ - "master" - ], - "keys": { - "ed25519:iiNvK2+mJtBXj6t+FVnaPBZ4e/M/n84wPJBfUVN38OE": "iiNvK2+mJtBXj6t+FVnaPBZ4e/M/n84wPJBfUVN38OE" - }, - "signatures": { - "@tobiasfella:kde.org": {} - } - }, - "@tobias:kryptons.eu": { - "user_id": "@tobias:kryptons.eu", - "usage": [ - "master" - ], - "keys": { - "ed25519:opHk6rsSV6FYk4VM5eN/AojNqCIwiN7fyAWw2hNshcc": "opHk6rsSV6FYk4VM5eN/AojNqCIwiN7fyAWw2hNshcc" - }, - "signatures": { - "@tobias:kryptons.eu": { - "ed25519:HUGZTAXEBQ": "ihSXn4p9Np4yF9UzyZmjFD67KGUHR27l6eKHj3+8ThZBiRrwZF6BRQgUWgWmBPqZsxBVRM3SXRu1/YS9Nje4Bg" - } - } - } - }, - "self_signing_keys": { - "@aloy:kde.org": { - "user_id": "@aloy:kde.org", - "usage": [ - "self_signing" - ], - "keys": { - "ed25519:YC9mscNFes+PkztOT401XdPNez2712Q4B9Pq7gXOG1M": "YC9mscNFes+PkztOT401XdPNez2712Q4B9Pq7gXOG1M" - }, - "signatures": { - "@aloy:kde.org": { - "ed25519:TAZ6oJMa3xtEObuxgvFMxnAMwzNzyuZ0/A0dPjueYsI": "x6DaIWjfNSc4wxgxn6xsTRlLv6/tZtEGaaLWpH9ytt1KJmtSsE6cNd+NBcUAQmC49NIRbGVEh+bP2JO4q6F7DQ" - } - } - }, - "@carl:kde.org": { - "user_id": "@carl:kde.org", - "usage": [ - "self_signing" - ], - "keys": { - "ed25519:JVHbEwSheI3ZYscn48PlrItknkE4ZsCxLDMmGBJITTo": "JVHbEwSheI3ZYscn48PlrItknkE4ZsCxLDMmGBJITTo" - }, - "signatures": { - "@carl:kde.org": { - "ed25519:FAZpIXm2112PIRgeY7oK+5capRD5WKy0bYPQW9Ldnos": "bNTb4wnv7P5+tETSqWgSoJmDTpyD7f4P+xYI967MEf+VlbuWhBOHIvMVbbXkvi129CPaq6FVCfJ7dXkht4VDBw" - } - } - }, - "@tobiasfella:kde.org": { - "user_id": "@tobiasfella:kde.org", - "usage": [ - "self_signing" - ], - "keys": { - "ed25519:66xWQ3zub5AMn0u/929opxaawM3HO2mWLTY5OMR5y1I": "66xWQ3zub5AMn0u/929opxaawM3HO2mWLTY5OMR5y1I" - }, - "signatures": { - "@tobiasfella:kde.org": { - "ed25519:iiNvK2+mJtBXj6t+FVnaPBZ4e/M/n84wPJBfUVN38OE": "vksGLKo0GKAoNIhMAGR3OxLE0QhNPEIk62VIg1Y5lVpEuoOn5DixMhVKEjC9TVuMesAyxwNk1ntCqdAKlIbtDQ" - } - } - }, - "@tobias:kryptons.eu": { - "user_id": "@tobias:kryptons.eu", - "usage": [ - "self_signing" - ], - "keys": { - "ed25519:J529eerbyjgT5XHCA1qec+V6HQqQlrNYugGheSBuJFA": "J529eerbyjgT5XHCA1qec+V6HQqQlrNYugGheSBuJFA" - }, - "signatures": { - "@tobias:kryptons.eu": { - "ed25519:opHk6rsSV6FYk4VM5eN/AojNqCIwiN7fyAWw2hNshcc": "2lVuMFF7SY0JB4XOfvOU4mQccITjsWRaPuIA8YVIAu6ZnO/MQ2+XOaE81yI3Dbys8/8VPM4reCNRBv/A1lVdCA" - } - } - } - }, - "user_signing_keys": { - "@tobiasfella:kde.org": { - "user_id": "@tobiasfella:kde.org", - "usage": [ - "user_signing" - ], - "keys": { - "ed25519:I1t81HEgBNNiDnA3Q5X8ig5IHEngwcUe0/ErjHS7dtU": "I1t81HEgBNNiDnA3Q5X8ig5IHEngwcUe0/ErjHS7dtU" - }, - "signatures": { - "@tobiasfella:kde.org": { - "ed25519:iiNvK2+mJtBXj6t+FVnaPBZ4e/M/n84wPJBfUVN38OE": "AdLeRPeo5zBGKq0cB2/pxANB4aqzXPmMDt2UWZVwJwyOM5ntaxuKlh3hgN58PhnjiWlFgBeEy7rn0MSeGRkdCQ" - } - } - } - } -} diff --git a/autotests/key-export.data b/autotests/key-export.data deleted file mode 100644 index 9f093b08d..000000000 --- a/autotests/key-export.data +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN MEGOLM SESSION DATA----- -ATQrLZT5CO0z1mJGtJcBaa4mabBB0pRII3RsKeXzvtKSAAehIFJ3016VcwFNTWDHPXC6cixX6u9YXWP7rTk7Z5vMVkIPRkUND2SbGmdrXYDWJgN/Ueg++VQZKkTWbiIQ -6l/h9Xo3Jp8Quskrb1g9kqerF0s9m+z5xfp2plwCzsYwEtvm7mFbS2v5yvY8Gc9c4oaMRU/+W1ZqBB0AznubfcvmU+EbWXtCKk+7EhnCbeD12ve9YLL4G/gfiE9afeRk -T318G8xYYT1eCMt/wxXAiSSmjwXc+Vp1n6qbg7KIGHWmu8eNvnj55aRu9Kk0+jtBXGhrMCGnjiJ3XDyPnjZnBJV81Cb3FIE8o3AAZGZ1aGgxK/PSBScaCI4aoWURd1oP -4SXNgehJdahqhLNfulagns9Cxx3lo/IlRG9tf28gDE6Bb3IFj5GbH8SUICylHZsJSD+vTSXb7aUNNqjcqowNS4evGFJCzOtfDqkoHyfqiln+8rMQ1OL0MuqEcX/2G6Tt -encqwrJN/6dMgdInABXArSgM1sqiBe59ZRVoml2cSDGekvpsq6sufmS/tg/25tvLVDRs7nwXLRSELAEo3WuWeBugUuJsYWn0b7Nb+ye3TW4ep7tvGh3v1TIhV4UQ9nbv -c6xPJnKS7CHw+xXUEXDg+4o20wY9+6w/lb59549/86GLaa0jPNr4AuV6QcSFy0kXXt2E2y0uzfRUEiUJmMT/YcgiSrgQR4Hc+lVMIDIPhhY5bzgCPytt7u78Y2faX+ef -9gU+1vHHvc5WeVt3TlVoEx2nDeCMSR/a/waBIV0bkSq0WwgIrYioNNTQ0SchAGHXN5eERJKAuXMd4n6khhXAI0+XCRQFgNn9b20hCCjEq4yb6GQ4cRHmvjtY12Z11VDH -4L7rmGFN0bvcmvjvrirZXj7Myi0engMHUhvd3zNhVKMpHzPghyDTClUWfnA/amZe1VN12jrkSAxSoLmQYALCBooIu7z/G8jZAZrgr72OmesYg4QE8A0UR+Y2cEWoOKvg -QvmBMnnu/kBIOqhdFfAcoJlWbTcYH/AJJsbqMsb9RmOhmBch8kSXDq7oTuV4iGT6xp47ZejI1St40oAXRjYEL4uJkXyZTvK/jkdjkFPbxHH8zAIcrOWWnczs8sfE7OMa -ASeiPjD0UWNEm8vXJIXE50zvHg0tDoIHlWSrt3khW+UlEEvK4J5oJYRTAUfKzXZrbZaq+C6Q0z30ksuE45aLtSdODlsKAc1fksscqycmVU22/SfvQje5Dv/uD9i8J86Y -+6OuXTzVw0H21+2tIBAjKaCM4jRFDruc/stok5Ixhxepl7184blfhZ4uRbt1hYGjcPfl6viB7lcboN4+i2WyB0xcpaZpQD6UBrglmFO05XKmSfjegR00YhmfS8yFXygs -fBJPNYihZoMmyRJjXqkETS7YVUSRPruMA+y8tyrwRO199LXaBbWORBI4EbFeVZlndGeileYQ6JNB7JdFu+RkeUkvkJPAyM8YD7NxR/5t+vNToJGUshy41CGbnVs30gxz -OPV+SCzfqD8qIjFiDWjsIGj7Hqm0qA== ------END MEGOLM SESSION DATA----- diff --git a/autotests/testcrosssigning.cpp b/autotests/testcrosssigning.cpp deleted file mode 100644 index 5c1e7fbd6..000000000 --- a/autotests/testcrosssigning.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#include -#include -#include "testutils.h" - -using namespace Quotient; - -class TestCrossSigning : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void test1() - { - auto path = QString::fromUtf8(__FILE__); - path = path.left(path.lastIndexOf(QDir::separator())); - path += "/cross_signing_data.json"_L1; - QFile file(path); - file.open(QIODevice::ReadOnly); - auto data = file.readAll(); - QVERIFY(!data.isEmpty()); - auto jobMock = Mocked(QHash{}); - jobMock.setResult(QJsonDocument::fromJson(data)); - auto mockKeys = collectResponse(&jobMock); - - auto connection = Connection::makeMockConnection("@tobiasfella:kde.org"_L1, true); - connection->d->encryptionData->handleQueryKeys(mockKeys); - - QVERIFY(!connection->isUserVerified("@tobiasfella:kde.org"_L1)); - QVERIFY(!connection->isUserVerified("@carl:kde.org"_L1)); - QVERIFY(!connection->isUserVerified("@eve:foo.bar"_L1)); - QVERIFY(!connection->isUserVerified("@aloy:kde.org"_L1)); - QVERIFY(!connection->isVerifiedDevice("@tobiasfella:kde.org"_L1, "LTLVYDIVMO"_L1)); - connection->database()->setMasterKeyVerified("iiNvK2+mJtBXj6t+FVnaPBZ4e/M/n84wPJBfUVN38OE"_L1); - QVERIFY(connection->isUserVerified("@tobiasfella:kde.org"_L1)); - connection->d->encryptionData->handleQueryKeys(mockKeys); - - QVERIFY(connection->isUserVerified("@tobiasfella:kde.org"_L1)); - QVERIFY(connection->isUserVerified("@aloy:kde.org"_L1)); - QVERIFY(!connection->isVerifiedDevice("@tobiasfella:kde.org"_L1, "IDEJTUJQAF"_L1)); - QVERIFY(!connection->isVerifiedDevice("@tobiasfella:kde.org"_L1, "DEADBEEF"_L1)); - QVERIFY(connection->isVerifiedDevice("@tobiasfella:kde.org"_L1, "LTLVYDIVMO"_L1)); - QVERIFY(connection->isVerifiedDevice("@aloy:kde.org"_L1, "BUJGIJTVJB"_L1)); - QVERIFY(!connection->isVerifiedDevice("@aloy:kde.org"_L1, "VJIBVDKIST"_L1)); - } -}; -QTEST_GUILESS_MAIN(TestCrossSigning) -#include "testcrosssigning.moc" diff --git a/autotests/testcryptoutils.cpp b/autotests/testcryptoutils.cpp deleted file mode 100644 index 3fcfdd123..000000000 --- a/autotests/testcryptoutils.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Tobias Fella -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include -#include -#include -#include - -#include - -#include - -#include - -class TestCryptoUtils : public QObject -{ - Q_OBJECT -private slots: - void aesCtrEncryptDecryptData(); - void hkdfSha256ExpandKeys(); - void encryptDecryptFile(); - void pbkdfGenerateKey(); - void hmac(); - void curve25519AesEncryptDecrypt(); - void decodeBase58(); - void testEncrypted(); -}; - -using namespace Quotient; - -void TestCryptoUtils::aesCtrEncryptDecryptData() -{ - const QByteArray plain = "ABCDEF"; - const FixedBuffer key{}; - const FixedBuffer iv{}; - auto cipher = aesCtr256Encrypt(plain, key, iv); - QVERIFY(cipher.has_value()); - auto decrypted = aesCtr256Decrypt(cipher.value(), key, iv); - QVERIFY(decrypted.has_value()); - QCOMPARE(plain, decrypted.value()); -} - -void TestCryptoUtils::encryptDecryptFile() -{ - const QByteArray data = "ABCDEF"; - auto [file, cipherText] = encryptFile(data); - auto decrypted = decryptFile(cipherText, file); - // AES CTR produces ciphertext of the same size as the original - QCOMPARE(cipherText.size(), data.size()); - QCOMPARE(decrypted.size(), data.size()); - QCOMPARE(decrypted, data); -} - -void TestCryptoUtils::hkdfSha256ExpandKeys() -{ - auto result = hkdfSha256(zeroes<32>(), zeroes<32>(), zeroes<32>()); - QVERIFY(result.has_value()); - auto&& keys = result.value(); - QCOMPARE(viewAsByteArray(keys.aes()), QByteArray::fromBase64("WQvd7OvHEaSFkO5nPBLDHK9F0UW5r11S6MS83AjhHx8=")); - QCOMPARE(viewAsByteArray(keys.mac()), QByteArray::fromBase64("hZhUYGZQRYj4src+HzLcKRruQQ0wSr9kC/g105lej+s=")); -} - -void TestCryptoUtils::pbkdfGenerateKey() -{ - auto key = pbkdf2HmacSha512(QByteArrayLiteral("PASSWORD"), zeroedByteArray(32), 50000); - QVERIFY(key.has_value()); - QCOMPARE(viewAsByteArray(key.value()), QByteArray::fromBase64("ejq90XW/J2J+cgi1ASgBj94M/YrEtWRKAPnsG+rdG4w=")); -} - -void TestCryptoUtils::hmac() -{ - auto result = hmacSha256(FixedBuffer{}, QByteArray(64, 1)); - QVERIFY(result.has_value()); - QCOMPARE(result.value(), QByteArray::fromBase64("GfJTpEMByWSMA/NXBYH/KHW2qlKxSZu4r//jRsUuz24=")); -} - -void TestCryptoUtils::curve25519AesEncryptDecrypt() -{ - const auto plain = QByteArrayLiteral("ABCDEF"); - auto privateKey = zeroedByteArray(); - - auto context = makeCStruct(olm_pk_decryption, olm_pk_decryption_size, - olm_clear_pk_decryption); - const auto publicKeySize = olm_pk_key_length(); - auto publicKey = byteArrayForOlm(publicKeySize); - olm_pk_key_from_private(context.get(), publicKey.data(), publicKeySize, privateKey.data(), unsignedSize(privateKey)); - - auto encrypted = curve25519AesSha2Encrypt(plain, publicKey); - QVERIFY(encrypted.has_value()); - auto decrypted = curve25519AesSha2Decrypt(encrypted.value().ciphertext, privateKey, encrypted.value().ephemeral, encrypted.value().mac); - QVERIFY(decrypted.has_value()); - QCOMPARE(plain, decrypted.value()); -} - -void TestCryptoUtils::decodeBase58() -{ - QCOMPARE(viewAsByteArray(base58Decode(QByteArrayLiteral("ABCDEFabcdef"))).toBase64(), QByteArrayLiteral("DG3GmkxFR1TQ")); -} - -void TestCryptoUtils::testEncrypted() -{ - QByteArray key(32, '\0'); - auto text = QByteArrayLiteral("This is a message"); - auto connection = Connection::makeMockConnection("@foo:bar.com"_L1, true); - connection->database()->storeEncrypted("testKey"_L1, text); - auto decrypted = connection->database()->loadEncrypted("testKey"_L1); - QCOMPARE(text, decrypted); -} - -QTEST_GUILESS_MAIN(TestCryptoUtils) -#include "testcryptoutils.moc" diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp deleted file mode 100644 index 6b812258a..000000000 --- a/autotests/testgroupsession.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "testgroupsession.h" -#include -#include - -using namespace Quotient; - -void TestGroupSession::groupSessionPicklingValid() -{ - QOlmOutboundGroupSession ogs{}; - const auto ogsId = ogs.sessionId(); - QVERIFY(QByteArray::fromBase64(ogsId).size() > 0); - QCOMPARE(0, ogs.sessionMessageIndex()); - - auto&& ogsPickled = ogs.pickle(PicklingKey::mock()); - auto ogs2 = QOlmOutboundGroupSession::unpickle(std::move(ogsPickled), - PicklingKey::mock()) - .value(); - QCOMPARE(ogsId, ogs2.sessionId()); - - auto igs = QOlmInboundGroupSession::create(ogs.sessionKey()); - QVERIFY(igs.has_value()); - const auto igsId = igs->sessionId(); - // ID is valid base64? - QVERIFY(QByteArray::fromBase64(igsId).size() > 0); - - //// no messages have been sent yet - QCOMPARE(0, igs->firstKnownIndex()); - - auto igsPickled = igs->pickle(PicklingKey::mock()); - igs = QOlmInboundGroupSession::unpickle(std::move(igsPickled), - PicklingKey::mock()).value(); - QVERIFY(igs.has_value()); - QCOMPARE(igsId, igs->sessionId()); -} - -void TestGroupSession::groupSessionCryptoValid() -{ - QOlmOutboundGroupSession ogs{}; - auto igs = QOlmInboundGroupSession::create(ogs.sessionKey()); - QVERIFY(igs.has_value()); - QCOMPARE(ogs.sessionId(), igs->sessionId()); - - const auto plainText = "Hello world!"; - const auto ciphertext = ogs.encrypt(plainText); - // ciphertext valid base64? - QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); - - const auto decryptionResult = igs->decrypt(ciphertext).value(); - - //// correct plaintext? - QCOMPARE(plainText, decryptionResult.first); - - QCOMPARE(0, decryptionResult.second); -} -QTEST_GUILESS_MAIN(TestGroupSession) diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h deleted file mode 100644 index 6edf0d160..000000000 --- a/autotests/testgroupsession.h +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -class TestGroupSession : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void groupSessionPicklingValid(); - void groupSessionCryptoValid(); -}; diff --git a/autotests/testkeyimport.cpp b/autotests/testkeyimport.cpp deleted file mode 100644 index 2bafbdd3b..000000000 --- a/autotests/testkeyimport.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Tobias Fella -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -#include - -#include - -class TestKeyImport : public QObject -{ - Q_OBJECT -private slots: - void testImport(); - void testExport(); -}; - -using namespace Quotient; - -void TestKeyImport::testImport() -{ - KeyImport keyImport; - - auto path = QString::fromUtf8(__FILE__); - path = path.left(path.lastIndexOf(QDir::separator())); - path += "/key-export.data"_L1; - QFile file(path); - file.open(QIODevice::ReadOnly); - auto data = file.readAll(); - QVERIFY(!data.isEmpty()); - const auto result = keyImport.decrypt(QString::fromUtf8(data), u"123passphrase"_s); - QVERIFY(result.has_value()); - const auto &json = result.value(); - QCOMPARE(json.size(), 2); -} - -void TestKeyImport::testExport() -{ - KeyImport keyImport; - - QJsonArray sessions; - sessions += QJsonObject{ - { "algorithm"_L1, "m.megolm.v1.aes-sha2"_L1 }, - { "forwarding_curve25519_key_chain"_L1, QJsonArray() }, - { "room_id"_L1, "!asdf:foo.bar"_L1 }, - { "sender_claimed_keys"_L1, QJsonObject{ { "ed25519"_L1, "asdfkey"_L1 } } }, - { "sender_key"_L1, "senderkey"_L1 }, - { "session_id"_L1, "sessionidasdf"_L1 }, - { "session_key"_L1, "sessionkeyfoo"_L1 }, - - }; - auto result = keyImport.encrypt(sessions, u"a passphrase"_s); - QVERIFY(result.has_value()); - QVERIFY(result.value().size() > 0); - - auto plain = keyImport.decrypt(QString::fromLatin1(result.value()), u"a passphrase"_s); - QVERIFY(plain.has_value()); - auto value = plain.value(); - QCOMPARE(value.size(), 1); - QCOMPARE(value[0]["algorithm"_L1].toString(), "m.megolm.v1.aes-sha2"_L1); -} - -QTEST_GUILESS_MAIN(TestKeyImport) -#include "testkeyimport.moc" diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp deleted file mode 100644 index 9e8fabb74..000000000 --- a/autotests/testkeyverification.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Tobias Fella -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "olm/sas.h" - -#include -#include - -#include -#include - -using namespace Quotient; - -class TestKeyVerificationSession : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void testOutgoing1() - { - auto connection = Connection::makeMockConnection("@carl:localhost"_L1); - const auto transactionId = "other_transaction_id"_L1; - auto session = connection->startKeyVerificationSession("@alice:localhost"_L1, "ABCDEF"_L1); - session->sendRequest(); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); - session->handleEvent(KeyVerificationReadyEvent(transactionId, "ABCDEF"_L1, {SasV1Method})); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); - session->handleEvent(KeyVerificationAcceptEvent(transactionId, "commitment_TODO"_L1)); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY); - // Since we can't get the events sent by the session, we're limited by what we can test. This means that continuing here would force us to test - // the exact same path as the other test, which is useless. - // TODO: Test some other path once we're able to. - } - - void testIncoming1() - { - auto userId = u"@bob:localhost"_s; - auto deviceId = u"DEFABC"_s; - const auto transactionId = "trans123action123id"_L1; - auto connection = Connection::makeMockConnection("@carl:localhost"_L1); - auto session = new KeyVerificationSession(userId, KeyVerificationRequestEvent(transactionId, deviceId, {SasV1Method}, QDateTime::currentDateTime()), connection, false); - QVERIFY(session->state() == KeyVerificationSession::INCOMING); - session->sendReady(); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); - session->handleEvent(KeyVerificationStartEvent(transactionId, deviceId)); - QVERIFY(session->state() == KeyVerificationSession::ACCEPTED); - auto account = new QOlmAccount(userId, deviceId); - account->setupNewAccount(); - - auto sas = olm_sas(new std::byte[olm_sas_size()]); - const auto randomLength = olm_create_sas_random_length(sas); - olm_create_sas(sas, getRandom(randomLength).data(), randomLength); - QByteArray keyBytes(olm_sas_pubkey_length(sas), '\0'); - olm_sas_get_pubkey(sas, keyBytes.data(), keyBytes.size()); - session->handleEvent(KeyVerificationKeyEvent(transactionId, QString::fromLatin1(keyBytes))); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); - session->sendMac(); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC); - //TODO: Send and verify the mac once we have a way of getting the KeyVerificationKeyEvent sent by the session. - } -}; -QTEST_GUILESS_MAIN(TestKeyVerificationSession) -#include "testkeyverification.moc" diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h deleted file mode 100644 index 367092f63..000000000 --- a/autotests/testolmaccount.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include -#include - -namespace Quotient { - class Connection; -} - -class TestOlmAccount : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void pickleUnpickledTest(); - void identityKeysValid(); - void signatureValid(); - void oneTimeKeysValid(); - //void removeOneTimeKeys(); - void deviceKeys(); - void encryptedFile(); - void uploadIdentityKey(); - void uploadOneTimeKeys(); - void uploadSignedOneTimeKeys(); - void uploadKeys(); - void queryTest(); - void claimKeys(); - void claimMultipleKeys(); - void enableEncryption(); -}; diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp deleted file mode 100644 index 46049393e..000000000 --- a/autotests/testolmsession.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include -#include -#include "testolmsession.h" - -using namespace Quotient; - -std::pair createSessionPair() -{ - QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); - QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); - auto accountA = QOlmAccount(u"accountA:foo.com"_s, u"Device1UserA"_s); - if (accountA.unpickle(std::move(pickledAccountA), PicklingKey::mock()) - != OLM_SUCCESS) - qFatal("Failed to unpickle account A: %s", accountA.lastError()); - - auto accountB = QOlmAccount(u"accountB:foo.com"_s, u"Device1UserB"_s); - if (accountB.unpickle(std::move(pickledAccountB), PicklingKey::mock()) - != OLM_SUCCESS) - qFatal("Failed to unpickle account B: %s", accountB.lastError()); - - //const auto identityKeyA = "qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o"_ba; - //const auto oneTimeKeyA = "WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"_ba; - const auto identityKeyB = "q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg"_ba; - const auto oneTimeKeyB = "oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ"_ba; - auto outbound = accountA.createOutboundSession(identityKeyB, oneTimeKeyB).value(); - - const auto preKey = outbound.encrypt(""); // Payload does not matter for PreKey - - if (preKey.type() != QOlmMessage::PreKey) { - // We can't call QFail here because it's an helper function returning a value - throw "Wrong first message type received, can't create session"; - } - auto inbound = accountB.createInboundSession(preKey).value(); - return { std::move(inbound), std::move(outbound) }; -} - -void TestOlmSession::olmOutboundSessionCreation() -{ - const auto [_, outboundSession] = createSessionPair(); - QCOMPARE(0, outboundSession.hasReceivedMessage()); -} - -void TestOlmSession::olmEncryptDecrypt() -{ - const auto [inboundSession, outboundSession] = createSessionPair(); - const auto encrypted = outboundSession.encrypt("Hello world!"); - if (encrypted.type() == QOlmMessage::PreKey) { - QOlmMessage m(encrypted); // clone - QVERIFY(inboundSession.matchesInboundSession(m)); - } - - const auto decrypted = inboundSession.decrypt(encrypted).value(); - - QCOMPARE(decrypted, "Hello world!"); -} - -void TestOlmSession::correctSessionOrdering() -{ - // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM - auto session1 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", PicklingKey::mock()).value(); - // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 - auto session2 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", PicklingKey::mock()).value(); - // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME - auto session3 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", PicklingKey::mock()).value(); - - const auto session1Id = session1.sessionId(); - const auto session2Id = session2.sessionId(); - const auto session3Id = session3.sessionId(); - - std::vector sessionList; - sessionList.push_back(std::move(session1)); - sessionList.push_back(std::move(session2)); - sessionList.push_back(std::move(session3)); - - std::sort(sessionList.begin(), sessionList.end()); - QCOMPARE(sessionList[0].sessionId(), session2Id); - QCOMPARE(sessionList[1].sessionId(), session3Id); - QCOMPARE(sessionList[2].sessionId(), session1Id); -} - -QTEST_GUILESS_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h deleted file mode 100644 index 9a5798fa3..000000000 --- a/autotests/testolmsession.h +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -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 deleted file mode 100644 index f15eb7816..000000000 --- a/autotests/testolmutility.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "testolmutility.h" -#include -#include - -#include - -using namespace Quotient; - -void TestOlmUtility::canonicalJSON() -{ - // Examples taken from - // https://matrix.org/docs/spec/appendices.html#canonical-json - auto data = QJsonDocument::fromJson(QByteArrayLiteral(R"({ - "auth": { - "success": true, - "mxid": "@john.doe:example.com", - "profile": { - "display_name": "John Doe", - "three_pids": [{ - "medium": "email", - "address": "john.doe@example.org" - }, { - "medium": "msisdn", - "address": "123456789" - }] - }}})")); - - QCOMPARE(data.toJson(QJsonDocument::Compact), - "{\"auth\":{\"mxid\":\"@john.doe:example.com\",\"profile\":{\"display_name\":\"John " - "Doe\",\"three_pids\":[{\"address\":\"john.doe@example.org\",\"medium\":\"email\"},{" - "\"address\":\"123456789\",\"medium\":\"msisdn\"}]},\"success\":true}}"); - - auto data0 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"b":"2","a":"1"})")); - QCOMPARE(data0.toJson(QJsonDocument::Compact), "{\"a\":\"1\",\"b\":\"2\"}"); - - auto data1 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "本": 2, "日": 1 })")); - QCOMPARE(data1.toJson(QJsonDocument::Compact), "{\"日\":1,\"本\":2}"); - - auto data2 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"a": "\u65E5"})")); - QCOMPARE(data2.toJson(QJsonDocument::Compact), "{\"a\":\"日\"}"); - - auto data3 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "a": null })")); - QCOMPARE(data3.toJson(QJsonDocument::Compact), "{\"a\":null}"); -} - -void TestOlmUtility::verifySignedOneTimeKey() -{ - QOlmAccount aliceOlm(u"@alice:matrix.org"_s, u"aliceDevice"_s); - aliceOlm.setupNewAccount(); - aliceOlm.generateOneTimeKeys(1); - auto keys = aliceOlm.oneTimeKeys(); - - auto firstKey = *keys.curve25519().begin(); - auto msgObj = QJsonObject({{"key"_L1, 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.size(), '\0'); - std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); - - auto res = - olm_ed25519_verify(utility, - aliceOlm.identityKeys().ed25519.toLatin1().data(), - aliceOlm.identityKeys().ed25519.size(), msg.data(), - msg.size(), sig.data(), sig.size()); - - QCOMPARE(olm_utility_last_error_code(utility), OLM_SUCCESS); - QCOMPARE(res, 0); - - delete[](reinterpret_cast(utility)); - - QOlmUtility utility2; - auto res2 = - utility2.ed25519Verify(aliceOlm.identityKeys().ed25519.toLatin1(), msg, - signatureBuf1); - - //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); - QVERIFY(res2); -} - -void TestOlmUtility::validUploadKeysRequest() -{ - const auto userId = u"@alice:matrix.org"_s; - const auto deviceId = u"FKALSOCCC"_s; - - QOlmAccount alice { userId, deviceId }; - alice.setupNewAccount(); - alice.generateOneTimeKeys(1); - - auto idSig = alice.signIdentityKeys(); - - const QJsonObject body{ - { "algorithms"_L1, toJson(SupportedAlgorithms) }, - { "user_id"_L1, userId }, - { "device_id"_L1, deviceId }, - { "keys"_L1, QJsonObject{ { "curve25519:"_L1 + deviceId, alice.identityKeys().curve25519 }, - { "ed25519:"_L1 + deviceId, alice.identityKeys().ed25519 } } }, - { "signatures"_L1, QJsonObject{ { userId, QJsonObject{ { "ed25519:"_L1 + deviceId, - QString::fromLatin1(idSig) } } } } } - }; - - const auto deviceKeys = alice.deviceKeys(); - QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact), - QJsonDocument(body).toJson(QJsonDocument::Compact)); - - QVERIFY(verifyIdentitySignature(fromJson(body), deviceId, userId)); - QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId)); -} -QTEST_GUILESS_MAIN(TestOlmUtility) diff --git a/autotests/testolmutility.h b/autotests/testolmutility.h deleted file mode 100644 index f2a3ca45e..000000000 --- a/autotests/testolmutility.h +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -class TestOlmUtility : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void canonicalJSON(); - void verifySignedOneTimeKey(); - void validUploadKeysRequest(); -}; diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 2c982c92c..5ffa96f14 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -187,7 +187,7 @@ TestManager::TestManager(int& argc, char** argv) Q_ASSERT(argc >= 5); // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) clog << "Connecting to Matrix as " << argv[1] << endl; - c->loginWithPassword(QString::fromUtf8(argv[1]), QString::fromUtf8(argv[2]), + c->loginWithPassword(QString::fromUtf8(argv[1]), QString::fromUtf8(argv[2]), {}, QString::fromUtf8(argv[3])); targetRoomName = QString::fromUtf8(argv[4]); clog << "Test room name: " << argv[4] << '\n'; @@ -209,20 +209,20 @@ TestManager::TestManager(int& argc, char** argv) clog << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << '\n' << "Access token: " << c->accessToken().toStdString() << endl; + // TODO: we can't do this, as the crypto machine is already initialized and explodes when this is run // Test Connection::assumeIdentity() while we can replace connection objects - auto* newC = new Connection(c->homeserver(), this); - newC->assumeIdentity(c->userId(), c->deviceId(), QString::fromLatin1(c->accessToken())); - // NB: this will need to change when we switch E2EE on in quotest because encryption - // data is initialised asynchronously - if (QUO_ALARM(newC->homeserver() != c->homeserver()) - || QUO_ALARM(newC->userId() != c->userId()) || QUO_ALARM(!newC->isLoggedIn())) { - clog << "Connection::assumeIdentity() is broken" << endl; - QCoreApplication::exit(-2); - return; - } - - c->deleteLater(); - c = newC; + // auto* newC = new Connection(c->homeserver(), this); + // newC->assumeIdentity(c->userId(), c->deviceId(), QString::fromLatin1(c->accessToken())); + // // NB: this will need to change when we switch E2EE on in quotest because encryption + // // data is initialised asynchronously + // if (QUO_ALARM(newC->homeserver() != c->homeserver()) + // || QUO_ALARM(newC->userId() != c->userId()) || QUO_ALARM(!newC->isLoggedIn())) { + // clog << "Connection::assumeIdentity() is broken" << endl; + // QCoreApplication::exit(-2); + // return; + // } + // + // c = newC; setupAndRun(); }); connect(c, &Connection::resolveError, this,