Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
177 KB
Referenced Files
None
Subscribers
None
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d5e4c94..23c03fe 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,153 +1,153 @@
# This file is part of libkazv.
# SPDX-FileCopyrightText: 2022-2023 tusooa <tusooa@kazv.moe>
# SPDX-License-Identifier: AGPL-3.0-or-later
stages:
- lint
- build
- doc
- pages
lint:
stage: lint
script: |
echo 'Verify that no source file contains tabs'
! egrep -R --exclude-dir=resources $'\t' src
rules: &build-rules
- changes: &src-chg
- .gitlab-ci.yml
- Dockerfile
- '**/*.cpp'
- '**/*.hpp'
- '**/CMakeLists.txt'
- '**/*.in'
- 'src/tests/resources/**'
.build:
stage: build
image:
name: quay.io/podman/stable
tags:
- podman
script:
# https://github.com/containers/podman/discussions/15612
- export TMPDIR="$CI_PROJECT_DIR"/cache
- mkdir -pv "$TMPDIR"
- chown -R podman:podman "$TMPDIR"
- sudo -u podman TMPDIR="$TMPDIR" podman login -u "$REGISTRY_USER" -p "$REGISTRY_PASSWORD" "$REGISTRY"
- - sudo -u podman TMPDIR="$TMPDIR" podman build -f "$CI_PROJECT_DIR"/Containerfile --build-arg JOBS=3 --build-arg BASE_IMG_TAG=$BASE_IMG_TAG --build-arg BUILD_TYPE=$BUILD_TYPE $BUILD_ARGS -t "$CANONICAL_TAG" "$CI_PROJECT_DIR"
+ - sudo -u podman TMPDIR="$TMPDIR" podman build -f "$CI_PROJECT_DIR"/Containerfile --build-arg JOBS=3 --build-arg BASE_IMG_TAG=$BASE_IMG_TAG --build-arg BUILD_TYPE=$BUILD_TYPE --build-arg LIBKAZV_ERROR_ON_WARNING=OFF $BUILD_ARGS -t "$CANONICAL_TAG" "$CI_PROJECT_DIR"
# Only branch and tag commits should trigger pushing
- |
if [ -z "$CI_COMMIT_BRANCH" -a -z "$CI_COMMIT_TAG" ]; then
echo 'Skipping pushing'
else
sudo -u podman TMPDIR="$TMPDIR" podman push "$CANONICAL_TAG"
if [ -n "$ADDITIONAL_TAGS" ]; then
for t in $ADDITIONAL_TAGS; do
sudo -u podman TMPDIR="$TMPDIR" podman push "$CANONICAL_TAG" "$t"
done
else
echo 'No additional tags to push'
fi
fi
rules: *build-rules
cache:
- key: $CI_JOB_NAME
paths:
- cache/buildah-cache-1000/
'build:debug:24.04':
extends: .build
before_script:
- BASE_IMG_TAG=servant-ubuntu24.04
- BUILD_TYPE=Debug
- REGISTRY_IMAGE=$REGISTRY/the-kazv-project/libkazv
- BASE_TAG="$REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG-none}"
- CANONICAL_TAG="$BASE_TAG-ubuntu24.04-debug"
- ADDITIONAL_TAGS="$BASE_TAG $BASE_TAG-debug $BASE_TAG-ubuntu24.04"
'build:production:24.04':
extends: .build
before_script:
- BASE_IMG_TAG=servant-ubuntu24.04
- BUILD_TYPE=Release
- REGISTRY_IMAGE=$REGISTRY/the-kazv-project/libkazv
- BASE_TAG="$REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG-none}"
- CANONICAL_TAG="$BASE_TAG-ubuntu24.04-prod"
- ADDITIONAL_TAGS="$BASE_TAG-prod"
'build:coverage':
stage: build
image:
name: r.lily-is.land/the-kazv-project/libkazv-deps/deps:servant-ubuntu24.04
entrypoint: [""]
# Do not set CCACHE_BASEDIR when building for coverage
# https://gitlab.kitware.com/cmake/cmake/-/issues/21138
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105238
# The build directory is always /builds/kazv/libkazv/ anyway
script: |
export BUILD_TYPE=Debug
export JOBS=3
export DEPS_INSTALL_DIR=/opt/libkazv-deps
export LIBKAZV_INSTALL_DIR=/opt/libkazv
export CCACHE_COMPILERCHECK=content
export CCACHE_DIR="$CI_PROJECT_DIR"/ccache
export PATH="/usr/lib/ccache:$PATH"
export CC=/usr/lib/ccache/gcc CXX=/usr/lib/ccache/g++
ccache --zero-stats || true
ccache --show-stats || true
mkdir build && cd build && \
cmake .. -DCMAKE_INSTALL_PREFIX="$LIBKAZV_INSTALL_DIR" -DCMAKE_PREFIX_PATH="$DEPS_INSTALL_DIR" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -Dlibkazv_BUILD_TESTS=ON \
- -Dlibkazv_BUILD_EXAMPLES=ON -Dlibkazv_BUILD_KAZVJOB=ON -Dlibkazv_ENABLE_COVERAGE=ON -DCMAKE_CXX_FLAGS=-fsanitize=address && \
+ -Dlibkazv_BUILD_EXAMPLES=ON -Dlibkazv_BUILD_KAZVJOB=ON -Dlibkazv_ENABLE_COVERAGE=ON -DCMAKE_CXX_FLAGS=-fsanitize=address -Dlibkazv_ERROR_ON_WARNING=OFF && \
make -j$JOBS && \
make CTEST_OUTPUT_ON_FAILURE=1 test && \
gcovr --xml-pretty --exclude-unreachable-branches --print-summary -o coverage.xml -r "${CI_PROJECT_DIR}" --object-directory src -e '.*/api/.*' -e '.*/tests/.*' -e '.*/testfixtures/.*' -e '.*/examples/.*'
after_script:
- ccache --show-stats
coverage: /^\s*lines:\s*\d+.\d+\%/
artifacts:
name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
paths:
- build/coverage.xml
reports:
junit: build/test-report.xml
coverage_report:
coverage_format: cobertura
path: build/coverage.xml
rules: *build-rules
cache:
- key: $CI_JOB_NAME
paths:
- ccache/
'doc:build':
stage: doc
image: r.lily-is.land/the-kazv-project/libkazv-deps/deps:servant
dependencies: []
script:
- doxygen Doxyfile
artifacts:
paths:
- doc/html/
rules:
- changes: *src-chg
- changes: &doc-chg
- '**/*.md'
- .gitlab-ci.yml
- public/**/*
pages:
stage: pages
image: alpine
dependencies:
- 'doc:build'
script:
- mv -v doc/html public/api
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH != "servant"
when: never
- changes: *src-chg
- changes: *doc-chg
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0a499e7..5abcfee 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,125 +1,131 @@
# Do not let the option()s override variables here
cmake_minimum_required(VERSION 3.13)
if(NOT DEFINED PROJECT_NAME)
if(NOT DEFINED libkazv_INSTALL_HEADERS)
set(libkazv_INSTALL_HEADERS ON)
endif()
endif()
project(libkazv)
set(libkazvSourceRoot ${CMAKE_CURRENT_SOURCE_DIR})
set(libkazv_VERSION_MAJOR 0)
set(libkazv_VERSION_MINOR 8)
set(libkazv_VERSION_PATCH 0)
set(libkazv_VERSION_STRING ${libkazv_VERSION_MAJOR}.${libkazv_VERSION_MINOR}.${libkazv_VERSION_PATCH})
set(libkazv_SOVERSION 7)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
option(libkazv_BUILD_TESTS "Build tests" ON)
option(libkazv_BUILD_EXAMPLES "Build examples" ON)
option(libkazv_BUILD_KAZVJOB "Build libkazvjob the async and networking library" ON)
option(libkazv_OUTPUT_LEVEL "Output level: Debug>=90, Info>=70, Quiet>=20, no output=1" 0)
option(libkazv_ENABLE_COVERAGE "Enable code coverage information" OFF)
+option(libkazv_ERROR_ON_WARNING "Turn compiler warnings to errors. Only use for development purposes." OFF)
if(libkazv_ENABLE_COVERAGE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage -fPIC -O0")
endif()
+if(libkazv_ERROR_ON_WARNING)
+ message(STATUS "Turning compiler warnings to errors. This should only be used for development purposes.")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
+endif()
+
if((libkazv_BUILD_TESTS OR libkazv_BUILD_EXAMPLES) AND NOT libkazv_BUILD_KAZVJOB)
message(FATAL_ERROR
"You asked kazvjob not to be built, but asked to build tests or examples. Tests and examples both depend on kazvjob. This is not possible.")
endif()
if(libkazv_BUILD_TESTS)
set(BUILD_TESTING ON)
include(CTest)
set(CMAKE_CTEST_ARGUMENTS --output-on-failure --output-junit test-report.xml)
endif()
# Build shared libraries by default
if(NOT DEFINED BUILD_SHARED_LIBS)
set(BUILD_SHARED_LIBS ON)
endif()
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(LIBKAZV_BUILT_WITH_DEBUG 1)
else()
set(LIBKAZV_BUILT_WITH_DEBUG 0)
endif()
find_package(Boost REQUIRED COMPONENTS serialization regex)
if(libkazv_BUILD_KAZVJOB)
find_package(cpr REQUIRED)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
endif()
if(libkazv_BUILD_TESTS)
find_package(Catch2 REQUIRED)
endif()
find_package(nlohmann_json REQUIRED)
find_package(Immer REQUIRED)
find_package(Zug REQUIRED)
find_package(Lager REQUIRED)
if(MINGW)
# On MinGW, disregard the Findcryptopp.cmake
find_package(cryptopp REQUIRED CONFIG)
else()
find_package(cryptopp REQUIRED)
endif()
# On MinGW we use the CMake build, and the target name is different
# for the shared and the static libraries.
if(MINGW)
set(CRYPTOPP_TARGET_NAME cryptopp-shared)
else()
set(CRYPTOPP_TARGET_NAME cryptopp)
endif()
find_package(vodozemac REQUIRED)
if(libkazv_BUILD_EXAMPLES)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBHTTPSERVER REQUIRED libhttpserver)
endif()
if(libkazv_OUTPUT_LEVEL)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DLIBKAZV_OUTPUT_LEVEL=${libkazv_OUTPUT_LEVEL}")
endif()
include(GNUInstallDirs)
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/libkazv)
add_subdirectory(src)
install(EXPORT libkazvTargets
NAMESPACE
libkazv::
DESTINATION
${ConfigPackageLocation}
)
install(
FILES cmake/libkazvConfig.cmake
DESTINATION ${ConfigPackageLocation})
# cpr does not install a good config file.
# We use the find module instead.
install(
FILES cmake/Findcpr.cmake
DESTINATION ${ConfigPackageLocation})
diff --git a/Containerfile b/Containerfile
index 2dc62ea..cd325f4 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,29 +1,30 @@
ARG BASE_IMG_TAG=servant
FROM r.lily-is.land/the-kazv-project/libkazv-deps/deps:$BASE_IMG_TAG
RUN mkdir -pv /build
COPY . /build/libkazv
WORKDIR /build/libkazv
ARG BUILD_TYPE=Debug
ARG JOBS=3
ARG DEPS_INSTALL_DIR=/opt/libkazv-deps
ARG LIBKAZV_INSTALL_DIR=/opt/libkazv
+ARG LIBKAZV_ERROR_ON_WARNING=OFF
RUN --mount=type=cache,id=ccache,target=/ccache \
export CCACHE_COMPILERCHECK=content \
CCACHE_BASEDIR=/build/libkazv \
CCACHE_DIR=/ccache \
PATH="/usr/lib/ccache:$PATH" \
CC=/usr/lib/ccache/gcc CXX=/usr/lib/ccache/g++ && \
ccache --zero-stats; ccache --show-stats; \
mkdir build && cd build && \
- cmake .. -DCMAKE_INSTALL_PREFIX="$LIBKAZV_INSTALL_DIR" -DCMAKE_PREFIX_PATH="$DEPS_INSTALL_DIR" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -Dlibkazv_BUILD_TESTS=ON -Dlibkazv_BUILD_EXAMPLES=ON -Dlibkazv_BUILD_KAZVJOB=ON && \
+ cmake .. -DCMAKE_INSTALL_PREFIX="$LIBKAZV_INSTALL_DIR" -DCMAKE_PREFIX_PATH="$DEPS_INSTALL_DIR" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -Dlibkazv_BUILD_TESTS=ON -Dlibkazv_BUILD_EXAMPLES=ON -Dlibkazv_BUILD_KAZVJOB=ON -Dlibkazv_ERROR_ON_WARNING="$LIBKAZV_ERROR_ON_WARNING" && \
make -j$JOBS && \
make test && \
make -j$JOBS install && \
cd ../.. && \
rm -rf /build/libkazv && \
ccache --show-stats
diff --git a/src/client/actions/auth.cpp b/src/client/actions/auth.cpp
index b24ce84..edc9fea 100644
--- a/src/client/actions/auth.cpp
+++ b/src/client/actions/auth.cpp
@@ -1,193 +1,193 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2022 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <debug.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
#include "auth.hpp"
#include "status-utils.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, LoginAction a)
{
m.addJob(LoginJob{a.serverUrl,
"m.login.password"s, // type
UserIdentifier{ "m.id.user"s, json{{"user", a.username}} }, // identifier
a.password,
{}, // token, not used
{}, // device id, not used
a.deviceName.value_or("libkazv")}
.withData(json{
{"serverUrl", a.serverUrl},
}));
return { m, lager::noop };
}
ClientResult updateClient(ClientModel m, MLoginTokenLoginAction a)
{
m.addJob(LoginJob{a.serverUrl,
"m.login.token"s, // type
std::nullopt, // identifier
std::nullopt, // password
a.loginToken, // token, not used
{}, // device id, not used
a.deviceName.value_or("libkazv")}
.withData(json{
{"serverUrl", a.serverUrl},
}));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, LoginResponse r)
{
if (! r.success()) {
m.addTrigger(LoginFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Job success" << std::endl;
auto jw = r.jsonBody();
auto const &j = jw.get();
// TODO: replace this with r.wellKnown()
std::string serverUrl = j.contains("well_known")
? j.at("well_known").at("m.homeserver").at("base_url").get<std::string>()
: r.dataStr("serverUrl");
// Synapse will return the server url with trailing slash
// and not recognize double slashes in the middle
while (serverUrl.back() == '/') {
serverUrl.pop_back();
}
m.serverUrl = serverUrl;
m.userId = r.userId().value_or(DEFVAL);
m.token = r.accessToken().value_or(DEFVAL);
m.deviceId = r.deviceId().value_or(DEFVAL);
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, TokenLoginAction a)
{
m.serverUrl = a.serverUrl;
m.userId = a.username;
m.token = a.token;
m.deviceId = a.deviceId;
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, LogoutAction)
{
// Note: this only performs a soft-logout.
m.serverUrl = "";
m.userId = "";
m.token = "";
m.deviceId = "";
m.loggedIn = false;
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, HardLogoutAction)
{
m.addJob(LogoutJob{m.serverUrl, m.token});
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, LogoutResponse r)
{
if (r.success()) {
m.token = "";
m.loggedIn = false;
return { std::move(m), lager::noop };
} else {
return { std::move(m), failWithResponse(std::move(r)) };
}
}
ClientResult updateClient(ClientModel m, GetWellknownAction a)
{
auto pos = a.userId.find(':');
if (pos == std::string::npos || pos == a.userId.size() - 1) {
return { std::move(m), simpleFail };
}
auto serverUrl = "https://" + a.userId.substr(pos + 1);
m.addJob(GetWellknownJob{serverUrl}
.withData(json{{"serverUrl", serverUrl}}));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, GetWellknownResponse r)
{
auto success = r.success() || r.statusCode == 404;
auto error = std::string();
std::string serverUrl = r.dataStr("serverUrl");
if (r.success()) {
auto data = r.data();
if (data.homeserver.baseUrl.empty()) {
success = false;
error = "FAIL_PROMPT";
} else {
serverUrl = data.homeserver.baseUrl;
}
} else {
error = "FAIL_PROMPT";
}
return {
std::move(m),
[success, serverUrl, error, r](auto &&) {
auto data = json{
{"homeserverUrl", serverUrl},
{"error", error},
{"errorCode", r.errorCode()},
};
return EffectStatus(success, data);
}
};
}
ClientResult updateClient(ClientModel m, GetVersionsAction a)
{
m.addJob(GetVersionsJob{a.serverUrl});
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetVersionsResponse r)
{
if (!r.success()) {
return {
std::move(m),
failWithResponse(r)
};
}
m.versions = r.versions();
return {
std::move(m),
- [r](auto &&ctx) {
+ [r]([[maybe_unused]] auto &&ctx) {
return EffectStatus(r.success(), json{
{"versions", r.versions()},
});
}
};
}
}
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
index abbbdbe..bacb651 100644
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -1,538 +1,538 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/algorithm.hpp>
#include <lager/util.hpp>
#include <lager/context.hpp>
#include <functional>
#include <zug/transducer/filter.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "immer-utils.hpp"
#include "json-utils.hpp"
#include "client-model.hpp"
#include "actions/states.hpp"
#include "actions/auth.hpp"
#include "actions/membership.hpp"
#include "actions/paginate.hpp"
#include "actions/send.hpp"
#include "actions/states.hpp"
#include "actions/account-data.hpp"
#include "actions/sync.hpp"
#include "actions/ephemeral.hpp"
#include "actions/content.hpp"
#include "actions/encryption.hpp"
#include "actions/profile.hpp"
#include "actions/storage.hpp"
namespace Kazv
{
auto ClientModel::update(ClientModel m, Action a) -> Result
{
auto oldClient = m;
auto oldDeviceLists = m.deviceLists;
auto actionIsStorage = std::holds_alternative<LoadEventsFromStorageAction>(a) || std::holds_alternative<PurgeRoomTimelineAction>(a);
auto [newClient, effect] = lager::match(std::move(a))(
[&](RoomListAction a) -> Result {
m.roomList = RoomListModel::update(std::move(m.roomList), a);
return {std::move(m), lager::noop};
},
[&](ResubmitJobAction a) -> Result {
m.addJob(std::move(a.job));
return { std::move(m), lager::noop };
},
[&](auto a) -> decltype(updateClient(m, a)) {
return updateClient(m, a);
},
#define RESPONSE_FOR(_jobId) \
if (r.jobId() == #_jobId) { \
return processResponse(m, _jobId##Response{std::move(r)}); \
}
[&](ProcessResponseAction a) -> Result {
auto r = std::move(a.response);
// auth
RESPONSE_FOR(Login);
RESPONSE_FOR(GetWellknown);
RESPONSE_FOR(GetVersions);
RESPONSE_FOR(Logout);
// paginate
RESPONSE_FOR(GetRoomEvents);
// sync
RESPONSE_FOR(Sync);
RESPONSE_FOR(DefineFilter);
// membership
RESPONSE_FOR(CreateRoom);
RESPONSE_FOR(InviteUser);
RESPONSE_FOR(JoinRoomById);
RESPONSE_FOR(JoinRoom);
RESPONSE_FOR(LeaveRoom);
RESPONSE_FOR(ForgetRoom);
RESPONSE_FOR(Kick);
RESPONSE_FOR(Ban);
RESPONSE_FOR(Unban);
// send
RESPONSE_FOR(SendMessage);
RESPONSE_FOR(SendToDevice);
RESPONSE_FOR(RedactEvent);
// states
RESPONSE_FOR(GetRoomState);
RESPONSE_FOR(SetRoomStateWithKey);
RESPONSE_FOR(GetRoomStateWithKey);
// account data
RESPONSE_FOR(SetAccountData);
RESPONSE_FOR(SetAccountDataPerRoom);
// ephemeral
RESPONSE_FOR(SetTyping);
RESPONSE_FOR(PostReceipt);
RESPONSE_FOR(SetReadMarker);
// content
RESPONSE_FOR(UploadContent);
RESPONSE_FOR(GetContent);
RESPONSE_FOR(GetContentThumbnail);
// encryption
RESPONSE_FOR(UploadKeys);
RESPONSE_FOR(QueryKeys);
RESPONSE_FOR(ClaimKeys);
// profile
RESPONSE_FOR(GetUserProfile);
RESPONSE_FOR(SetAvatarUrl);
RESPONSE_FOR(SetDisplayName);
m.addTrigger(UnrecognizedResponse{std::move(r)});
return { std::move(m), lager::noop };
}
#undef RESPONSE_FOR
);
newClient.maybeRotateSessions(oldClient);
// if it is an storage action, do not add it back because the things
// should be the same as the ones in storage
if (!actionIsStorage) {
newClient.maybeAddSaveEventsTrigger(oldClient);
}
return { std::move(newClient), std::move(effect) };
}
std::pair<Event, std::optional<std::string>> ClientModel::megOlmEncrypt(
Event e, std::string roomId, Timestamp timeMs, RandomData random)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return { e, std::nullopt };
}
auto j = e.originalJson().get();
auto r = roomList[roomId];
if (! r.encrypted) {
kzo.client.dbg() << "The room " << roomId
<< " is not encrypted, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
auto desc = r.sessionRotateDesc();
auto keyOpt = std::optional<std::string>{};
if (r.shouldRotateSessionKey) {
kzo.client.dbg() << "We should rotate this session." << std::endl;
keyOpt = withCrypto([&](auto &c) { return c.rotateMegOlmSessionWithRandom(random, timeMs, roomId); });
} else {
keyOpt = withCrypto([&](auto &c) { return c.rotateMegOlmSessionWithRandomIfNeeded(random, timeMs, roomId, desc); });
}
// we no longer need to rotate session
// until next time a device change happens
roomList.rooms = std::move(roomList.rooms)
.update(roomId, [](auto r) { r.shouldRotateSessionKey = false; return r; });
auto relation = hasAtThat(j["content"], "m.relates_to", &json::is_object) ? j["content"]["m.relates_to"] : json(nullptr);
// so that Crypto::encryptMegOlm() can find room id
j["room_id"] = roomId;
auto content = withCrypto([&](auto &c) { return c.encryptMegOlm(j); });
j["type"] = "m.room.encrypted";
j["content"] = std::move(content);
j["content"]["device_id"] = deviceId;
// add relationship to plaintext
if (relation.is_object()) {
j["content"]["m.relates_to"] = relation;
}
return { Event(JsonWrap(j)), keyOpt };
}
immer::map<std::string, immer::map<std::string, Event>> ClientModel::olmEncryptSplit(
Event e,
immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap,
RandomData random,
bool attachSenderDeviceKeys
)
{
using ResT = immer::map<std::string, immer::map<std::string, Event>>;
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return ResT{};
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return ResT{};
}
auto origJson = e.originalJson().get();
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", constCrypto().curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
if (attachSenderDeviceKeys) {
auto senderDeviceKeys = makeSelfDeviceKeys();
origJson["sender_device_keys"] = senderDeviceKeys;
}
ResT messages;
for (auto [userId, devices] : userIdToDeviceIdMap) {
messages = std::move(messages).set(userId, immer::map<std::string, Event>());
for (auto dev : devices) {
auto devInfoOpt = deviceLists.get(userId, dev);
if (! devInfoOpt) {
kzo.client.warn() << "no device info for " << userId << " / " << dev << std::endl;
continue;
}
auto devInfo = devInfoOpt.value();
auto jsonForThisDevice = origJson;
jsonForThisDevice["sender"] = this->userId;
jsonForThisDevice["recipient"] = userId;
jsonForThisDevice["recipient_keys"] = json{
{CryptoConstants::ed25519, devInfo.ed25519Key}
};
jsonForThisDevice["keys"] = json{
{CryptoConstants::ed25519, constCrypto().ed25519IdentityKey()}
};
auto thisEventJson = encJson;
thisEventJson["content"]["ciphertext"]
.merge_patch(withCrypto([&](auto &c) { return c.encryptOlmWithRandom(random, jsonForThisDevice, devInfo.curve25519Key); }));
random.erase(0, Crypto::encryptOlmMaxRandomSize());
messages = setIn(std::move(messages), Event(thisEventJson), userId, dev);
}
}
return messages;
}
DeviceKeys ClientModel::makeSelfDeviceKeys()
{
using namespace CryptoConstants;
auto keys =
immer::map<std::string, std::string>{}
.set(ed25519 + ":" + deviceId, constCrypto().ed25519IdentityKey())
.set(curve25519 + ":" + deviceId, constCrypto().curve25519IdentityKey());
DeviceKeys k {
userId,
deviceId,
{olmAlgo, megOlmAlgo},
keys,
{} // signatures to be added soon
};
auto j = json(k);
auto sig = withCrypto([&](auto &crypto) { return crypto.sign(j); });
k.signatures = convertSignature(sig);
return k;
}
json ClientModel::convertSignature(std::string signature) const
{
auto j = json::object();
j[userId] = json::object();
j[userId][CryptoConstants::ed25519 + ":" + deviceId] = signature;
return j;
};
immer::flex_vector<std::string /* deviceId */> ClientModel::devicesToSendKeys(std::string userId) const
{
auto trustLevelNeeded = this->trustLevelNeededToSendKeys;
// XXX: preliminary approach
auto shouldSendP = [=](auto deviceInfo, auto /* deviceMap */) {
return deviceInfo.trustLevel >= trustLevelNeeded;
};
auto devices = deviceLists.devicesFor(userId);
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([=](auto n) {
auto [id, dev] = n;
return shouldSendP(dev, devices);
})
| zug::map([=](auto n) {
return n.first;
}),
devices);
}
std::size_t ClientModel::numOneTimeKeysNeeded() const
{
const auto &crypto = constCrypto();
// Keep half of max supported number of keys
int numUploadedKeys = crypto.uploadedOneTimeKeysCount(CryptoConstants::signedCurve25519);
int numKeysNeeded = crypto.maxNumberOfOneTimeKeys() / 2
- numUploadedKeys;
// Subtract the number of existing one-time keys, in case
// the previous upload was not successful.
int numKeysToGenerate = numKeysNeeded - crypto.numUnpublishedOneTimeKeys();
if (numKeysToGenerate < 0) {
numKeysToGenerate = 0;
}
return numKeysToGenerate;
}
std::size_t EncryptMegOlmEventAction::maxRandomSize()
{
return Crypto::rotateMegOlmSessionRandomSize();
}
std::size_t EncryptMegOlmEventAction::minRandomSize()
{
return 0;
}
std::size_t PrepareForSharingRoomKeyAction::randomSize(PrepareForSharingRoomKeyAction::UserIdToDeviceIdMap devices)
{
auto singleRandomSize = Crypto::encryptOlmMaxRandomSize();
auto deviceNum = accumulate(devices, std::size_t{},
[](auto counter, auto pair) { return counter + pair.second.size(); });
return deviceNum * singleRandomSize;
}
std::size_t GenerateAndUploadOneTimeKeysAction::randomSize(std::size_t numToGen)
{
return Crypto::genOneTimeKeysRandomSize(numToGen);
}
std::size_t ClaimKeysAction::randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend)
{
auto singleRandomSize = Crypto::createOutboundSessionRandomSize();
auto deviceNum = accumulate(devicesToSend, std::size_t{},
[](auto counter, auto pair) { return counter + pair.second.size(); });
return deviceNum * singleRandomSize;
}
void ClientModel::maybeRotateSessions(ClientModel oldClient)
{
auto roomIds = intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](const auto &pair) {
return pair.second.encrypted && !pair.second.shouldRotateSessionKey;
})
| zug::map([](const auto &pair) { return pair.first; }),
roomList.rooms
);
auto markRotate = [this](const auto &roomId) {
roomList.rooms =
std::move(roomList.rooms)
.update(roomId, [](auto room) {
room.shouldRotateSessionKey = true;
return room;
});
};
// Rotate megolm keys for rooms whose users' device list has changed
auto changedUsers = deviceLists.diff(oldClient.deviceLists);
if (! changedUsers.empty()) {
for (auto roomId : roomIds) {
auto it = std::find_if(changedUsers.begin(), changedUsers.end(),
- [=](auto userId) { return roomList.rooms[roomId].hasUser(userId); });
+ [=, this](auto userId) { return roomList.rooms[roomId].hasUser(userId); });
if (it != changedUsers.end()) {
kzo.client.dbg() << "rotate keys for room " << roomId << std::endl;
markRotate(roomId);
}
}
}
roomIds = intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](const auto &pair) {
return pair.second.encrypted && !pair.second.shouldRotateSessionKey;
})
| zug::map([](const auto &pair) { return pair.first; }),
roomList.rooms
);
for (auto roomId : roomIds) {
auto userIds = roomList.rooms[roomId].joinedMemberIds();
auto devicesNotChanged = [oldClient, this](const auto &userId) {
return oldClient.devicesToSendKeys(userId) == devicesToSendKeys(userId);
};
// if any user has the device changes
if (!immer::all_of(userIds, devicesNotChanged)) {
kzo.client.dbg() << "rotate keys for room " << roomId << std::endl;
markRotate(roomId);
}
}
}
auto ClientModel::directRoomMap() const -> immer::map<std::string, std::string>
{
auto directs = accountData["m.direct"].content().get();
auto directItems = directs.items();
return std::accumulate(directItems.begin(), directItems.end(), immer::map<std::string, std::string>(),
[](auto acc, const auto &cur) {
auto [userId, roomIds] = cur;
if (!roomIds.is_array()) {
return acc;
}
for (auto roomId : roomIds) {
if (roomId.is_string()) {
acc = std::move(acc).set(roomId.template get<std::string>(), userId);
}
}
return acc;
}
);
}
auto ClientModel::roomIdsUnderTag(std::string tagId) const -> immer::map<std::string, double>
{
return std::accumulate(
roomList.rooms.begin(), roomList.rooms.end(),
immer::map<std::string, double>{},
[tagId](auto acc, auto cur) {
auto [roomId, room] = cur;
auto tags = room.tags();
if (tags.count(tagId)) {
acc = std::move(acc).set(roomId, tags[tagId]);
}
return acc;
}
);
}
auto ClientModel::roomIdsByTagId() const -> immer::map<std::string, immer::map<std::string, double>>
{
return std::accumulate(
roomList.rooms.begin(), roomList.rooms.end(),
immer::map<std::string, immer::map<std::string, double>>{},
[](auto acc, auto cur) {
auto [roomId, room] = cur;
auto tags = room.tags();
if (tags.empty()) {
acc = setIn(std::move(acc), ROOM_TAG_DEFAULT_ORDER, "", roomId);
} else {
for (const auto &[tagId, order] : tags) {
acc = setIn(std::move(acc), order, tagId, roomId);
}
}
return acc;
}
);
}
const Crypto &ClientModel::constCrypto() const
{
return crypto.value().get();
}
void ClientModel::maybeAddSaveEventsTrigger(const ClientModel &old)
{
SaveEventsRequested trigger;
using ELT = immer::flex_vector_transient<Event>;
auto addToTrigger = [&trigger](const std::string &roomId, ELT tl, ELT nonTl) {
if (!tl.empty()) {
trigger.timelineEvents = std::move(trigger.timelineEvents).set(roomId, std::move(tl).persistent());
}
if (!nonTl.empty()) {
trigger.nonTimelineEvents = std::move(trigger.nonTimelineEvents).set(roomId, std::move(nonTl).persistent());
}
};
immer::diff(old.roomList.rooms, roomList.rooms, immer::make_differ(
/* addedFn = */ [&addToTrigger](const auto &p) {
const auto &[roomId, room] = p;
ELT tl;
ELT nonTl;
for (const auto &[id, e] : room.messages) {
if (room.isInTimeline(id)) {
tl.push_back(e);
} else {
nonTl.push_back(e);
}
}
addToTrigger(roomId, std::move(tl), std::move(nonTl));
},
/* removedFn = */ [](auto &&) {},
/* changedFn = */ [&addToTrigger](const auto &p1, const auto &p2) {
const auto &roomId = p2.first;
const auto &newRoom = p2.second;
const auto &oldRoom = p1.second;
ELT tl;
ELT nonTl;
auto addEvent = [&tl, &nonTl, &newRoom](const auto &newPair) {
const auto &[eventId, event] = newPair;
if (newRoom.isInTimeline(eventId)) {
tl.push_back(event);
} else {
nonTl.push_back(event);
}
};
immer::diff(oldRoom.messages, newRoom.messages, immer::make_differ(
/* addedFn = */ addEvent,
/* removedFn = */ [](auto &&) {},
/* changedFn = */ [&addEvent](auto &&, auto &&newPair) {
addEvent(std::forward<decltype(newPair)>(newPair));
}
));
addToTrigger(roomId, std::move(tl), std::move(nonTl));
}
));
if (!trigger.timelineEvents.empty() || !trigger.nonTimelineEvents.empty()) {
nextTriggers = std::move(nextTriggers).push_back(trigger);
}
}
}
diff --git a/src/client/client.cpp b/src/client/client.cpp
index fa951fe..9e775fd 100644
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -1,772 +1,772 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <filesystem>
#include <algorithm>
#include <chrono>
#include <lager/constant.hpp>
#include "client.hpp"
#include "client-model.hpp"
#include "alias.hpp"
#include "immer-utils.hpp"
namespace Kazv
{
static Timestamp tsNow()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
}
static Client::PromiseT sendMultiVerificationEvents(Client::ContextT ctx, VerificationTracker::PendingEvents pendingEvents)
{
std::vector<Client::PromiseT> ps;
for (auto ed : pendingEvents) {
ps.push_back(ctx.dispatch(SendToDeviceMessageAction{
ed.event,
{{ed.toUserId, {ed.toDeviceId}}}
}));
}
return ctx.promiseInterface().all(ps);
}
static Client::PromiseT maybeMarkDevicesVerified(Client::ContextT ctx, VerificationTracker::PendingEvents pendingEvents)
{
immer::map<std::string, immer::map<std::string, DeviceTrustLevel>> trustLevelMap;
for (auto ed : pendingEvents) {
if (VerificationUtils::typeOf(ed.event) == VerificationEventTypes::tDone) {
// a process asking us to send an outbound done event means that we have verified them
trustLevelMap = setIn(trustLevelMap, DeviceTrustLevel::Verified, ed.toUserId, ed.toDeviceId);
kzo.client.dbg() << "maybeMarkDevicesVerified: verified " << ed.toUserId << "/" << ed.toDeviceId << std::endl;
}
}
if (!trustLevelMap.empty()) {
return ctx.dispatch(SetDevicesTrustLevelsAction{trustLevelMap});
}
return ctx.createResolvedPromise({});
}
static Client::PromiseT verificationChangePostProcess(Client::ContextT ctx, VerificationTracker::PendingEvents es)
{
return ctx.dispatch(NotifyVerificationTrackerModelAction{})
.then([ctx, es](auto &&) {
return maybeMarkDevicesVerified(ctx, es);
})
.then([ctx, es](auto &&) {
return sendMultiVerificationEvents(ctx, es);
});
}
Client::Client(lager::reader<SdkModel> sdk,
ContextT ctx, std::nullopt_t)
: m_sdk(sdk)
, m_client(sdk.map(&SdkModel::c))
, m_ctx(std::move(ctx))
{
}
Client::Client(lager::reader<SdkModel> sdk,
ContextWithDepsT ctx)
: m_sdk(sdk)
, m_client(sdk.map(&SdkModel::c))
, m_ctx(ctx)
, m_deps(std::move(ctx))
{
}
Client::Client(InEventLoopTag,
ContextWithDepsT ctx)
: m_sdk(std::nullopt)
, m_client(std::nullopt)
, m_ctx(ctx)
, m_deps(std::move(ctx))
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Client::Client(InEventLoopTag,
ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_client(std::nullopt)
, m_ctx(std::move(ctx))
, m_deps(std::move(deps))
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Client Client::toEventLoop() const
{
return Client(InEventLoopTag{}, m_ctx, m_deps.value());
}
Room Client::room(std::string id) const
{
if (m_deps.has_value()) {
return Room(sdkCursor(), lager::make_constant(id), m_ctx, m_deps.value());
} else {
return Room(sdkCursor(), lager::make_constant(id), m_ctx);
}
}
Room Client::roomByCursor(lager::reader<std::string> id) const
{
if (m_deps.has_value()) {
return Room(sdkCursor(), id, m_ctx, m_deps.value());
} else {
return Room(sdkCursor(), id, m_ctx);
}
}
auto Client::passwordLogin(std::string homeserver, std::string username,
std::string password, std::string deviceName) const
-> PromiseT
{
auto p1 = m_ctx.dispatch(LoginAction{
homeserver, username, password, deviceName});
p1
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
// It is meaningless to wait for it in a Promise
// that is never exposed to the user.
that.startSyncing();
});
return p1;
}
auto Client::mLoginTokenLogin(
std::string homeserver,
std::string loginToken,
std::optional<std::string> deviceName
) const -> PromiseT
{
auto p1 = m_ctx.dispatch(MLoginTokenLoginAction{
homeserver, loginToken, deviceName});
p1
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
that.startSyncing();
});
return p1;
}
auto Client::tokenLogin(std::string homeserver, std::string username,
std::string token, std::string deviceId) const
-> PromiseT
{
auto p1 = m_ctx.dispatch(TokenLoginAction{
homeserver, username, token, deviceId});
p1
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
that.startSyncing();
});
return p1;
}
auto Client::shouldSync() const -> lager::reader<bool> {
return this->clientCursor()[&ClientModel::shouldSync];
}
auto Client::logout() const
-> PromiseT
{
- return stopSyncing().then([ctx=m_ctx] (auto stat) {
+ return stopSyncing().then([ctx=m_ctx] ([[maybe_unused]] auto stat) {
return ctx.dispatch(HardLogoutAction{});
});
}
auto Client::autoDiscover(std::string userId) const
-> PromiseT
{
return m_ctx.dispatch(GetWellknownAction{userId})
.then([that=toEventLoop()](auto stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(GetVersionsAction{stat.dataStr("homeserverUrl")})
.then([that, stat](auto stat2) {
if (!stat2.success()) {
return stat2;
} else {
return stat;
}
});
});
}
auto Client::createRoom(
RoomVisibility v,
std::optional<std::string> name,
std::optional<std::string> alias,
immer::array<std::string> invite,
std::optional<bool> isDirect,
bool allowFederate,
std::optional<std::string> topic,
JsonWrap powerLevelContentOverride,
std::optional<CreateRoomPreset> preset,
immer::array<Event> initialState
) const
-> PromiseT
{
CreateRoomAction a;
a.visibility = v;
a.name = name;
a.roomAliasName = alias;
a.invite = invite;
a.isDirect = isDirect;
a.topic = topic;
a.powerLevelContentOverride = powerLevelContentOverride;
// Synapse won't buy it if we do not provide
// a creationContent object.
a.creationContent = json{
{"m.federate", allowFederate}
};
a.preset = preset;
a.initialState = initialState;
return m_ctx.dispatch(std::move(a));
}
auto Client::joinRoomById(std::string roomId) const -> PromiseT
{
return m_ctx.dispatch(JoinRoomByIdAction{roomId});
}
auto Client::joinRoom(std::string roomId, immer::array<std::string> serverName) const
-> PromiseT
{
return m_ctx.dispatch(JoinRoomAction{roomId, serverName});
}
auto Client::uploadContent(immer::box<Bytes> content,
std::string uploadId,
std::optional<std::string> filename,
std::optional<std::string> contentType) const
-> PromiseT
{
return m_ctx.dispatch(UploadContentAction{
FileDesc(FileContent{content.get().begin(), content.get().end()}),
filename, contentType, uploadId});
}
auto Client::uploadContent(FileDesc file) const
-> PromiseT
{
auto basename = file.name()
? std::optional(std::filesystem::path(file.name().value()).filename().string())
: std::nullopt;
return m_ctx.dispatch(UploadContentAction{
file,
// use only basename to prevent path info being leaked
basename,
file.contentType(),
// uploadId unused
std::string{}});
}
std::string Client::mxcUriToHttpV1(std::string mxcUri) const {
using namespace CursorOp;
auto [serverName, mediaId] = mxcUriToMediaDesc(mxcUri);
return (+clientCursor())
.template job<GetContentJobV1>()
.make(serverName, mediaId).url();
}
auto Client::downloadContent(std::string mxcUri, std::optional<FileDesc> downloadTo) const
-> PromiseT
{
return m_ctx.dispatch(DownloadContentAction{mxcUri, downloadTo});
}
auto Client::downloadThumbnail(
std::string mxcUri,
int width,
int height,
std::optional<ThumbnailResizingMethod> method,
std::optional<FileDesc> downloadTo) const
-> PromiseT
{
return m_ctx.dispatch(DownloadThumbnailAction{mxcUri, width, height, method, std::nullopt, downloadTo});
}
auto Client::startSyncing() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace Kazv::CursorOp;
if (+syncing()) {
return m_ctx.createResolvedPromise(true);
}
auto p1 = ensureReadyForSyncing();
p1
.then([m_ctx=m_ctx](auto stat) {
m_ctx.dispatch(SetShouldSyncAction{true});
return stat;
})
.then([that=toEventLoop()](auto stat) {
if (stat.success()) {
that.syncForever();
}
});
return p1;
}
auto Client::processVerificationEventsFromSync(EventList toDeviceEvents) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), ves=toDeviceEvents](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto now = tsNow();
auto es = vt.processIncoming(now, ves);
auto p = maybeMarkDevicesVerified(that.m_ctx, es);
for (auto ed : es) {
// We don't actually need to wait for these events to
// be sent before we proceed into next sync cycle.
that.m_ctx.dispatch(SendToDeviceMessageAction{
ed.event,
{{ed.toUserId, {ed.toDeviceId}}}
});
}
return p.then([ctx=that.m_ctx](const auto &) {
return ctx.dispatch(NotifyVerificationTrackerModelAction{});
});
});
}
auto Client::requestOutgoingToDeviceVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
{{userId, {deviceId}}},
});
})
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
return client.deviceLists.get(userId, deviceId);
}).make().get();
if (!deviceOpt) {
return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
{"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
{"error", "Cannot obtain device keys"},
})});
}
auto device = deviceOpt.value();
auto es = vt.requestOutgoingToDevice(VerificationUtils::DeviceIdentity{
userId,
deviceId,
device.ed25519Key,
}, tsNow());
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::readyForVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
{{userId, {deviceId}}},
});
})
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
return client.deviceLists.get(userId, deviceId);
}).make().get();
if (!deviceOpt) {
return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
{"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
{"error", "Cannot obtain device keys"},
})});
}
auto device = deviceOpt.value();
vt.setTheirIdentity(VerificationUtils::DeviceIdentity{
userId,
deviceId,
device.ed25519Key,
});
auto es = vt.userReady(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::cancelVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userCancel(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::confirmVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userConfirmMatch(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::denyVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userDenyMatch(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::ensureInitVerificationTracker() const -> PromiseT
{
if (!m_deps) {
return m_ctx.createResolvedPromise(false);
}
bool hasCrypto{clientCursor().map([](const auto &c) {
return c.crypto.has_value();
}).make().get()};
if (!hasCrypto) {
return m_ctx.createResolvedPromise(false);
}
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop()](auto) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
if (vt.identity.userId.empty()) {
auto client = that.clientCursor().get();
const auto &crypto = client.constCrypto();
vt.identity = {
client.userId,
client.deviceId,
crypto.ed25519IdentityKey(),
};
}
return EffectStatus{/* succ = */ true};
});
}
auto Client::syncForever(std::optional<int> retryTime) const -> void
{
KAZV_VERIFY_THREAD_ID();
// assert (m_deps);
using namespace CursorOp;
bool shouldSync = +clientCursor()[&ClientModel::shouldSync];
if (! shouldSync) {
return;
}
doOneSyncCycle()
.then([that=toEventLoop(), retryTime](auto stat) {
if (stat.success()) {
that.syncForever(); // reset retry time
} else {
auto firstRetryTime = +that.clientCursor()[&ClientModel::firstRetryMs];
auto retryTimeFactor = +that.clientCursor()[&ClientModel::retryTimeFactor];
auto maxRetryTime = +that.clientCursor()[&ClientModel::maxRetryMs];
auto curRetryTime = retryTime ? retryTime.value() : firstRetryTime;
if (curRetryTime > maxRetryTime) { curRetryTime = maxRetryTime; }
auto nextRetryTime = curRetryTime * retryTimeFactor;
kzo.client.warn() << "Sync failed, retrying in " << curRetryTime << "ms" << std::endl;
auto &jh = getJobHandler(that.m_deps.value());
jh.setTimeout([that=that.toEventLoop(), nextRetryTime]() { that.syncForever(nextRetryTime); },
curRetryTime);
}
});
}
auto Client::stopSyncing() const -> PromiseT
{
return m_ctx.dispatch(SetShouldSyncAction{false});
}
auto Client::ensureReadyForSyncing() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace Kazv::CursorOp;
return m_ctx.createResolvedPromise(true)
.then([that=toEventLoop()](auto) {
// post filters, if filters are incomplete
if ((+that.clientCursor()[&ClientModel::initialSyncFilterId]).empty()
|| (+that.clientCursor()[&ClientModel::incrementalSyncFilterId]).empty()) {
return that.m_ctx.dispatch(PostInitialFiltersAction{});
}
return that.m_ctx.createResolvedPromise(true);
})
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
// Upload identity keys if we need to
if (+that.clientCursor()[&ClientModel::crypto]
&& ! +that.clientCursor()[&ClientModel::identityKeysUploaded]) {
return that.m_ctx.dispatch(UploadIdentityKeysAction{});
} else {
return that.m_ctx.createResolvedPromise(true);
}
});
}
auto Client::doOneSyncCycle() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace CursorOp;
bool isInitialSync = !(+clientCursor()[&ClientModel::syncToken]).has_value();
auto syncRes = m_ctx.dispatch(SyncAction{});
auto uploadOneTimeKeysRes = syncRes
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &rg = lager::get<RandomInterface &>(that.m_deps.value());
bool hasCrypto{+that.clientCursor()[&ClientModel::crypto]};
if (! hasCrypto) {
return that.m_ctx.createResolvedPromise(true);
}
auto numKeysToGenerate = (+that.clientCursor()).numOneTimeKeysNeeded();
return that.m_ctx.dispatch(GenerateAndUploadOneTimeKeysAction{
numKeysToGenerate,
rg.generateRange<RandomData>(GenerateAndUploadOneTimeKeysAction::randomSize(numKeysToGenerate))
});
});
auto queryKeysRes = syncRes
.then([that=toEventLoop(), isInitialSync](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
bool hasCrypto{+that.clientCursor()[&ClientModel::crypto]};
return hasCrypto
? that.m_ctx.dispatch(QueryKeysAction{isInitialSync})
: that.m_ctx.createResolvedPromise(true);
});
auto processVerificationEventsRes = syncRes
.then([that=toEventLoop()](EffectStatus stat) {
if (!stat.success() || !that.m_deps ||
!that.clientCursor().map([](const auto &c) {
return c.crypto.has_value();
}).make().get()) {
return that.m_ctx.createResolvedPromise(stat);
}
kzo.client.dbg() << "processVerificationEvents: " << stat.data().get().dump() << std::endl;
EventList ves = stat.data().get().at("verificationEvents").at("toDevice").template get<EventList>();
return that.processVerificationEventsFromSync(ves);
});
return m_ctx.promiseInterface()
.all(std::vector<PromiseT>{uploadOneTimeKeysRes, queryKeysRes, processVerificationEventsRes});
}
lager::reader<ClientModel> Client::clientCursor() const
{
KAZV_VERIFY_THREAD_ID();
if (m_client.has_value()) {
return m_client.value();
} else {
assert(m_deps.has_value());
return lager::get<SdkModelCursorKey>(m_deps.value())->map(&SdkModel::c);
}
}
const lager::reader<SdkModel> &Client::sdkCursor() const
{
KAZV_VERIFY_THREAD_ID();
if (m_sdk.has_value()) {
return m_sdk.value();
} else {
assert(m_deps.has_value());
return *(lager::get<SdkModelCursorKey>(m_deps.value()));
}
}
auto Client::getProfile(std::string userId) const -> PromiseT
{
return m_ctx.dispatch(GetUserProfileAction{userId});
}
auto Client::setAvatarUrl(std::optional<std::string> avatarUrl) const -> PromiseT
{
return m_ctx.dispatch(SetAvatarUrlAction{avatarUrl});
}
auto Client::setDisplayName(std::optional<std::string> displayName) const -> PromiseT
{
return m_ctx.dispatch(SetDisplayNameAction{displayName});
}
auto Client::devicesOfUser(std::string userId) const -> lager::reader<immer::flex_vector<DeviceKeyInfo>>
{
return clientCursor()
[&ClientModel::deviceLists]
[&DeviceListTracker::deviceLists]
[userId]
[lager::lenses::or_default]
.xform(containerMap(immer::flex_vector<DeviceKeyInfo>{}, zug::map([](const auto &pair) {
const auto &[deviceId, info] = pair;
(void)deviceId;
return info;
})));
}
auto Client::setDeviceTrustLevel(std::string userId, std::string deviceId, DeviceTrustLevel trustLevel) const -> PromiseT
{
return m_ctx.dispatch(SetDeviceTrustLevelAction{userId, deviceId, trustLevel});
}
auto Client::trustLevelNeededToSendKeys() const -> lager::reader<DeviceTrustLevel>
{
return clientCursor()[&ClientModel::trustLevelNeededToSendKeys];
}
auto Client::setTrustLevelNeededToSendKeys(DeviceTrustLevel trustLevel) const -> PromiseT
{
return m_ctx.dispatch(SetTrustLevelNeededToSendKeysAction{trustLevel});
}
auto Client::directRoomMap() const -> lager::reader<immer::map<std::string, std::string>>
{
return clientCursor().map(&ClientModel::directRoomMap);
}
auto Client::roomIdsUnderTag(std::string tagId) const -> lager::reader<immer::map<std::string, double>>
{
return clientCursor().map([tagId](const auto &c) {
return c.roomIdsUnderTag(tagId);
});
}
auto Client::roomIdsByTagId() const -> lager::reader<immer::map<std::string, immer::map<std::string, double>>>
{
return clientCursor().map(&ClientModel::roomIdsByTagId);
}
auto Client::accountData() const -> lager::reader<immer::map<std::string, Event>>
{
return clientCursor()[&ClientModel::accountData];
}
auto Client::setAccountData(Event accountDataEvent) const -> PromiseT
{
return m_ctx.dispatch(SetAccountDataAction{accountDataEvent});
}
NotificationHandler Client::notificationHandler() const
{
return NotificationHandler(clientCursor());
}
auto Client::getVersions(std::string homeserver) const -> PromiseT
{
return m_ctx.dispatch(GetVersionsAction{homeserver});
}
auto Client::supportVersions() const -> lager::reader<immer::array<std::string>>
{
return clientCursor()[&ClientModel::versions];
}
auto Client::addDirectRoom(std::string userId, std::string roomId) const -> PromiseT
{
auto content = this->accountData().get()["m.direct"].content().get();
if (content.contains(userId)) {
auto& rooms = content[userId];
if (rooms.is_array()) {
if (std::find(rooms.begin(), rooms.end(), roomId) != rooms.end()) {
// The roomId is already in the m.direct, do nothing
return m_ctx.createResolvedPromise(true);
}
} else {
rooms = json::array({});
}
} else {
content.emplace(userId, json::array({}));
}
content[userId].push_back(roomId);
return Client::setAccountData(json{
{"type", "m.direct"},
{"content", std::move(content)}
});
}
auto Client::getRoomIdByAliasJob(std::string roomAlias) const -> BaseJob
{
return Kazv::getRoomIdByAliasJob(clientCursor().get(), roomAlias);
}
auto Client::purgeRoomEvents(immer::map<std::string, std::size_t> roomIdToMaxToKeepMap) const -> PromiseT
{
return m_ctx.dispatch(PurgeRoomTimelineAction{roomIdToMaxToKeepMap});
}
auto Client::loadEventsFromStorage(immer::map<std::string, EventList> timelineEvents, immer::map<std::string, EventList> relatedEvents) const -> PromiseT
{
return m_ctx.dispatch(LoadEventsFromStorageAction{
std::move(timelineEvents),
std::move(relatedEvents),
});
}
auto Client::importFromKeyBackupFile(std::string fileContent, std::string password) const -> PromiseT
{
return m_ctx.dispatch(ImportFromKeyBackupFileAction{
std::move(fileContent),
std::move(password),
});
}
}
diff --git a/src/client/push-rules-desc.cpp b/src/client/push-rules-desc.cpp
index 1e0db38..21a0c98 100644
--- a/src/client/push-rules-desc.cpp
+++ b/src/client/push-rules-desc.cpp
@@ -1,337 +1,337 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <charconv>
#include <boost/algorithm/string/regex.hpp>
#include <immer/array.hpp>
#include <event.hpp>
#include <json-utils.hpp>
#include <room/room-model.hpp>
#include <validator.hpp>
#include "push-rules-desc-p.hpp"
namespace Kazv
{
static immer::array<std::string> ruleSets = {
"override",
"content",
"room",
"sender",
"underride",
};
// https://spec.matrix.org/v1.8/appendices/#dot-separated-property-paths
static const auto splitRegex = boost::regex("(?<!\\\\)\\.");
static const auto unescapeRegex = boost::regex("\\\\(\\\\|\\.)");
std::vector<std::string> splitPath(std::string path)
{
std::vector<std::string> ret;
boost::algorithm::split_regex(ret, path, splitRegex);
for (auto &part : ret) {
part = boost::regex_replace(part, unescapeRegex, "$1");
}
return ret;
}
static const auto metaCharsRegex = boost::regex("([*])|([?])|([.^$|()\\[\\]{}+\\\\])");
bool matchGlob(std::string target, std::string pattern)
{
auto matchRegex = boost::regex_replace(pattern, metaCharsRegex, "(?1.*:)(?2.:)(?3\\\\$3:)", boost::regex_constants::format_all);
return boost::regex_match(target, boost::regex(matchRegex, boost::regex::icase));
}
static bool isConditionKind(const json &kind)
{
return kind == "event_match"
|| kind == "event_property_is"
|| kind == "event_property_contains"
|| kind == "room_member_count";
}
static const auto memberCountIsRegex = boost::regex("(>|==|<|>=|<=)?([0-9]+)");
static bool isMemberCountIsValid(const json &is)
{
auto isStr = is.template get<std::string>();
return is.is_string() && boost::regex_match(isStr, memberCountIsRegex);
}
static std::function<bool(int)> parseMemberCountIs(const json &is)
{
boost::smatch m;
auto isStr = is.template get<std::string>();
- auto res = boost::regex_match(isStr, m, memberCountIsRegex);
+ boost::regex_match(isStr, m, memberCountIsRegex);
auto targetStr = m[2].str();
int target;
std::from_chars(targetStr.data(), targetStr.data() + targetStr.size(), target);
if (m[1] == "" || m[1] == "==") {
return [target](auto memberCount) { return memberCount == target; };
} else if (m[1] == ">") {
return [target](auto memberCount) { return memberCount > target; };
} else if (m[1] == "<") {
return [target](auto memberCount) { return memberCount < target; };
} else if (m[1] == ">=") {
return [target](auto memberCount) { return memberCount >= target; };
} else if (m[1] == "<=") {
return [target](auto memberCount) { return memberCount <= target; };
} else {
// shouldn't be here
return [](auto) { return false; };
}
}
static std::pair<bool, json> validateCondition(const json &cond)
{
json validatedCond = json::object();
if (!(
cond.is_object()
&& cast(validatedCond, cond, "kind", identValidate(&isConditionKind))
)) {
return {false, json()};
}
if (validatedCond["kind"] == "event_match") {
if (!(
cast(validatedCond, cond, "key", identValidate(&json::is_string))
&& cast(validatedCond, cond, "pattern", identValidate(&json::is_string))
)) {
return {false, json()};
}
} else if (validatedCond["kind"] == "event_property_is" || validatedCond["kind"] == "event_property_contains") {
if (!(
cast(validatedCond, cond, "key", identValidate(&json::is_string))
&& cast(validatedCond, cond, "value", identValidate(&isNonCompoundCanonicalJsonValue))
)) {
return {false, json()};
}
} else if (validatedCond["kind"] == "room_member_count") {
if (!(cast(validatedCond, cond, "is", identValidate(&isMemberCountIsValid)))) {
return {false, json()};
}
} else {
return {false, json()};
}
return {true, validatedCond};
}
static std::pair<bool, json> validateAction(const json &act)
{
if (act == "notify") {
return {true, act};
} else if (act.is_object()) {
json validatedAct = json::object();
if (cast(validatedAct, act, "set_tweak", identValidate([](const auto &t) { return t == "sound"; }))) {
validatedAct["value"] = "default";
cast(validatedAct, act, "value", identValidate(&json::is_string));
} else if (cast(validatedAct, act, "set_tweak", identValidate([](const auto &t) { return t == "highlight"; }))) {
validatedAct["value"] = true;
cast(validatedAct, act, "value", identValidate(&json::is_boolean));
}
return {true, validatedAct};
}
return {false, json()};
}
std::pair<bool, json> validateRule(std::string ruleSetName, const json &rule)
{
json ret = json::object();
if (!(
cast(ret, rule, "rule_id", identValidate(&json::is_string))
&& cast(ret, rule, "enabled", identValidate(&json::is_boolean))
&& cast(ret, rule, "default", identValidate(&json::is_boolean))
&& (!(ruleSetName == "override" || ruleSetName == "underride")
|| castArray(
ret,
makeDefaultValue(rule, "conditions", json::array()),
"conditions",
validateCondition,
CastArrayStrategy::FailAll
))
&& castArray(ret, rule, "actions", validateAction, CastArrayStrategy::IgnoreInvalid)
)) {
return {false, json()};
}
return {true, ret};
}
Event validatePushRules(const Event &e)
{
json ret{
{"type", "m.push_rules"},
{"content", {
{"global", {
{"override", {}},
{"content", {}},
{"room", {}},
{"sender", {}},
{"underride", {}},
}},
}},
};
const auto content = e.content().get();
for (const auto &ruleSetName : ruleSets) {
auto rules = getInJson(content, std::array<std::string, 2>{"global", ruleSetName});
castArray(
ret["content"],
content,
nlohmann::json_pointer<std::string>("/global/" + ruleSetName),
[ruleSetName](const json &rule) {
return validateRule(ruleSetName, rule);
},
CastArrayStrategy::IgnoreInvalid
);
}
return Event(ret);
}
PushRulesDescPrivate::PushRulesDescPrivate(const Event &e)
: pushRulesEvent(validatePushRules(e))
{}
std::optional<std::pair<std::string, json>> PushRulesDescPrivate::matchRule(const Event &e, const RoomModel &room) const
{
for (const auto &ruleSetName : ruleSets) {
auto matched = matchRuleSet(ruleSetName, e, room);
if (matched.has_value()) {
return matched;
}
}
return std::nullopt;
}
std::optional<std::pair<std::string, json>> PushRulesDescPrivate::matchRuleSet(std::string ruleSetName, const Event &e, const RoomModel &room) const
{
auto rules = pushRulesEvent.content().get().at("global").at(ruleSetName);
for (const auto &rule : rules) {
if (matchP(ruleSetName, rule, e, room)) {
return std::make_pair(ruleSetName, rule);
}
}
return std::nullopt;
}
bool PushRulesDescPrivate::matchP(std::string ruleSetName, const json &rule, const Event &e, const RoomModel &room) const
{
if (!rule.at("enabled").template get<bool>()) {
return false;
}
if (ruleSetName == "override" || ruleSetName == "underride") {
return std::all_of(
rule.at("conditions").begin(),
rule.at("conditions").end(),
[&e, &room](const auto &condition) mutable {
if (condition.is_object()) {
if (condition.at("kind") == "event_property_is") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !isNonCompoundCanonicalJsonValue(valueInEvent.value())) {
return false;
}
return valueInEvent.value() == condition.at("value");
} else if (condition.at("kind") == "event_match") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !valueInEvent.value().is_string()) {
return false;
}
auto valueStr = valueInEvent.value().template get<std::string>();
auto patternStr = condition.at("pattern").template get<std::string>();
return matchGlob(valueStr, patternStr);
} else if (condition.at("kind") == "event_property_contains") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !valueInEvent.value().is_array()) {
return false;
}
auto vals = valueInEvent.value();
return std::any_of(vals.begin(), vals.end(), [cond=condition.at("value")](const auto &val) {
return isNonCompoundCanonicalJsonValue(val) && val == cond;
});
} else if (condition.at("kind") == "room_member_count") {
auto is = condition.at("is");
return parseMemberCountIs(is)(room.joinedMemberCount);
}
}
return false;
}
);
}
return false;
}
- PushAction PushRulesDescPrivate::handleRule(std::string ruleSetName, const json &rule, const Event &e, const RoomModel &room) const
+ PushAction PushRulesDescPrivate::handleRule([[maybe_unused]] std::string ruleSetName, const json &rule, [[maybe_unused]] const Event &e, [[maybe_unused]] const RoomModel &room) const
{
auto actions = rule.at("actions");
auto res = PushAction{
/* shouldNotify */ false,
/* sound */ std::nullopt,
/* shouldHighlight */ false,
};
if (std::find(actions.begin(), actions.end(), "notify") != actions.end()) {
res.shouldNotify = true;
}
if (auto it = std::find_if(
actions.begin(), actions.end(), [](const auto &a) {
return a.is_object()
&& a.contains("set_tweak")
&& a["set_tweak"] == "sound";
}); it != actions.end()) {
res.sound = (*it)["value"].template get<std::string>();
}
if (auto it = std::find_if(
actions.begin(), actions.end(), [](const auto &a) {
return a.is_object()
&& a.contains("set_tweak")
&& a["set_tweak"] == "highlight";
}); it != actions.end()) {
res.shouldHighlight = (*it)["value"].template get<bool>();
}
return res;
}
PushRulesDesc::PushRulesDesc()
: m_d()
{
}
PushRulesDesc::PushRulesDesc(const Event &pushRulesEvent)
: m_d(new PushRulesDescPrivate{pushRulesEvent})
{
}
PushRulesDesc::~PushRulesDesc() = default;
KAZV_DEFINE_COPYABLE_UNIQUE_PTR(PushRulesDesc, m_d);
bool PushRulesDesc::valid() const
{
return !!m_d;
}
PushAction PushRulesDesc::handle(const Event &e, const RoomModel &room) const
{
auto ruleOpt = m_d->matchRule(e, room);
if (ruleOpt.has_value()) {
auto [ruleSetName, rule] = ruleOpt.value();
return m_d->handleRule(ruleSetName, rule, e, room);
}
- return {false};
+ return {false, std::nullopt, false};
}
}
diff --git a/src/client/room/local-echo.hpp b/src/client/room/local-echo.hpp
index 136a516..505e677 100644
--- a/src/client/room/local-echo.hpp
+++ b/src/client/room/local-echo.hpp
@@ -1,69 +1,69 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <boost/serialization/split_free.hpp>
#include "event.hpp"
namespace Kazv
{
/**
* Describes a local echo.
*/
struct LocalEchoDesc
{
enum Status
{
Sending,
Failed,
};
std::string txnId;
Event event;
Status status{Sending};
friend bool operator==(const LocalEchoDesc &a, const LocalEchoDesc &b) = default;
friend bool operator!=(const LocalEchoDesc &a, const LocalEchoDesc &b) = default;
};
}
namespace boost::serialization
{
template<class Archive>
- void save(Archive &ar, const Kazv::LocalEchoDesc &d, std::uint32_t const version)
+ void save(Archive &ar, const Kazv::LocalEchoDesc &d, [[maybe_unused]] std::uint32_t const version)
{
using namespace Kazv;
LocalEchoDesc::Status dummyStatus{LocalEchoDesc::Failed};
ar
& d.txnId
& d.event
& dummyStatus
;
}
template<class Archive>
- void load(Archive &ar, Kazv::LocalEchoDesc &d, std::uint32_t const version)
+ void load(Archive &ar, Kazv::LocalEchoDesc &d, [[maybe_unused]] std::uint32_t const version)
{
using namespace Kazv;
LocalEchoDesc::Status dummyStatus{LocalEchoDesc::Failed};
ar
& d.txnId
& d.event
& dummyStatus
;
d.status = LocalEchoDesc::Failed;
}
template<class Archive>
void serialize(Archive &ar, Kazv::LocalEchoDesc &d, std::uint32_t const version)
{
using boost::serialization::split_free;
split_free(ar, d, version);
}
}
BOOST_CLASS_VERSION(Kazv::LocalEchoDesc, 0)
diff --git a/src/crypto/crypto.cpp b/src/crypto/crypto.cpp
index 6e2ab81..2559097 100644
--- a/src/crypto/crypto.cpp
+++ b/src/crypto/crypto.cpp
@@ -1,674 +1,674 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <vector>
#include <zug/transducer/filter.hpp>
#include <vodozemac.h>
#include <nlohmann/json.hpp>
#include <debug.hpp>
#include <event.hpp>
#include <cursorutil.hpp>
#include <types.hpp>
#include <validator.hpp>
#include "crypto-p.hpp"
#include "session-p.hpp"
#include "crypto-util-p.hpp"
#include "crypto-util.hpp"
#include "time-util.hpp"
namespace Kazv
{
using namespace CryptoConstants;
CryptoPrivate::CryptoPrivate()
: account(std::nullopt)
, valid(false)
{
}
CryptoPrivate::CryptoPrivate(RandomTag, [[maybe_unused]] RandomData data)
: account(std::nullopt)
, valid(true)
{
account = vodozemac::olm::new_account();
}
CryptoPrivate::~CryptoPrivate()
{
}
CryptoPrivate::CryptoPrivate(const CryptoPrivate &that)
: account(std::nullopt)
, uploadedOneTimeKeysCount(that.uploadedOneTimeKeysCount)
, numUnpublishedKeys(that.numUnpublishedKeys)
, knownSessions(that.knownSessions)
, inboundGroupSessions(that.inboundGroupSessions)
, outboundGroupSessions(that.outboundGroupSessions)
{
if (that.valid) {
valid = unpickle(that.pickle());
}
}
std::string CryptoPrivate::pickle() const
{
auto pickleData = account.value()->pickle(VODOZEMAC_PICKLE_KEY);
return static_cast<std::string>(pickleData);
}
bool CryptoPrivate::unpickle(std::string pickleData)
{
account = checkVodozemacError([&]() {
return vodozemac::olm::account_from_pickle(
rust::Str(pickleData),
VODOZEMAC_PICKLE_KEY
);
});
return account.has_value();
}
bool CryptoPrivate::unpickleFromLibolm(std::string pickleData)
{
account = checkVodozemacError([&]() {
return vodozemac::olm::account_from_libolm_pickle(
rust::Str(pickleData),
rust::Slice<const unsigned char>(OLM_PICKLE_KEY.data(), OLM_PICKLE_KEY.size())
);
});
return account.has_value();
}
MaybeString CryptoPrivate::decryptOlm(nlohmann::json content)
{
auto theirCurve25519IdentityKey = content.at("sender_key").get<std::string>();
auto ourCurve25519IdentityKey = curve25519IdentityKey();
if (! content.at("ciphertext").contains(ourCurve25519IdentityKey)) {
return NotBut("Message not intended for us");
}
auto type = content.at("ciphertext").at(ourCurve25519IdentityKey).at("type").get<int>();
auto body = content.at("ciphertext").at(ourCurve25519IdentityKey).at("body").get<std::string>();
auto hasKnownSession = knownSessions.find(theirCurve25519IdentityKey) != knownSessions.end();
if (type == 0) { // pre-key message
bool shouldCreateNewSession =
// there is no possible session
(! hasKnownSession)
// the possible session does not match this message
|| (! knownSessions.at(theirCurve25519IdentityKey).matches(body));
if (shouldCreateNewSession) {
auto created = createInboundSession(theirCurve25519IdentityKey, body);
if (! created) { // cannot create session, thus cannot decrypt
return NotBut("Cannot create session");
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.m_d->takeFirstDecrypted();
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
} else {
if (! hasKnownSession) {
return NotBut("No available session");
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
}
}
MaybeString CryptoPrivate::decryptMegOlm(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, sessionId};
if (inboundGroupSessions.find(k) == inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto msg = content.at("ciphertext").get<std::string>();
auto eventId = eventJson.at("event_id").get<std::string>();
auto originServerTs = eventJson.at("origin_server_ts").get<Timestamp>();
auto &session = inboundGroupSessions.at(k);
return session.decrypt(msg, eventId, originServerTs);
}
}
bool CryptoPrivate::createInboundSession(std::string theirCurve25519IdentityKey,
std::string message)
{
auto s = Session(InboundSessionTag{}, *this,
theirCurve25519IdentityKey, message);
if (s.valid()) {
knownSessions.insert_or_assign(theirCurve25519IdentityKey, std::move(s));
return true;
}
return false;
}
bool CryptoPrivate::reuseOrCreateOutboundGroupSession(
RandomData random, Timestamp timeMs,
std::string roomId, std::optional<MegOlmSessionRotateDesc> desc)
{
bool valid = true;
if (! desc.has_value()) { // force rotate
valid = false;
} else {
auto it = outboundGroupSessions.find(roomId);
if (it == outboundGroupSessions.end()) {
valid = false;
} else {
auto &session = it->second;
if (timeMs - session.creationTimeMs() >= desc.value().ms) {
valid = false;
} else if (session.messageIndex() >= desc.value().messages) {
valid = false;
}
}
}
if (! valid) {
outboundGroupSessions.insert_or_assign(roomId, OutboundGroupSession(RandomTag{}, random, timeMs));
auto &session = outboundGroupSessions.at(roomId);
auto sessionId = session.sessionId();
auto sessionKey = session.sessionKey();
auto k = KeyOfGroupSession{roomId, sessionId};
if (! createInboundGroupSession(k, sessionKey, ed25519IdentityKey())) {
kzo.client.warn() << "Create inbound group session from outbound group session failed. We may not be able to read our own messages." << std::endl;
}
}
return valid;
}
std::size_t Crypto::constructRandomSize()
{
return 0;
}
Crypto::Crypto()
: m_d(new CryptoPrivate{})
{
}
Crypto::Crypto(RandomTag, RandomData data)
: m_d(new CryptoPrivate(RandomTag{}, std::move(data)))
{
}
Crypto::~Crypto() = default;
Crypto::Crypto(const Crypto &that)
: m_d(new CryptoPrivate(*that.m_d))
{
}
Crypto::Crypto(Crypto &&that)
: m_d(std::move(that.m_d))
{
}
Crypto &Crypto::operator=(const Crypto &that)
{
m_d.reset(new CryptoPrivate(*that.m_d));
return *this;
}
Crypto &Crypto::operator=(Crypto &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool Crypto::operator==(const Crypto &that) const
{
return this->m_d == that.m_d;
}
bool Crypto::valid() const
{
return m_d->valid;
}
std::string CryptoPrivate::ed25519IdentityKey() const
{
auto key = account.value()->ed25519_key()->to_base64();
return static_cast<std::string>(key);
}
std::string CryptoPrivate::curve25519IdentityKey() const
{
auto key = account.value()->curve25519_key()->to_base64();
return static_cast<std::string>(key);
}
std::string Crypto::ed25519IdentityKey() const
{
return m_d->ed25519IdentityKey();
}
std::string Crypto::curve25519IdentityKey() const
{
return m_d->curve25519IdentityKey();
}
std::string Crypto::sign(nlohmann::json j)
{
j.erase("signatures");
j.erase("unsigned");
auto str = j.dump();
auto signature = checkVodozemacError([&]() {
return m_d->account.value()->sign(str);
});
if (!signature.has_value()) {
return "";
}
return static_cast<std::string>(signature.value()->to_base64());
}
void Crypto::setUploadedOneTimeKeysCount(immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount)
{
m_d->uploadedOneTimeKeysCount = uploadedOneTimeKeysCount;
}
std::size_t Crypto::maxNumberOfOneTimeKeys() const
{
return m_d->account.value()->max_number_of_one_time_keys();
}
- std::size_t Crypto::genOneTimeKeysRandomSize(int num)
+ std::size_t Crypto::genOneTimeKeysRandomSize([[maybe_unused]] int num)
{
return 0;
}
- void Crypto::genOneTimeKeysWithRandom(RandomData random, int num)
+ void Crypto::genOneTimeKeysWithRandom([[maybe_unused]] RandomData random, int num)
{
assert(random.size() >= genOneTimeKeysRandomSize(num));
m_d->account.value()->generate_one_time_keys(num);
m_d->numUnpublishedKeys += num;
}
nlohmann::json Crypto::unpublishedOneTimeKeys() const
{
auto keys = m_d->account.value()->one_time_keys();
auto ret = nlohmann::json{
{curve25519, nlohmann::json::object()},
};
for (const auto &k : keys) {
auto keyId = static_cast<std::string>(k.key_id);
auto key = static_cast<std::string>(k.key->to_base64());
ret[curve25519][keyId] = key;
}
return ret;
}
void Crypto::markOneTimeKeysAsPublished()
{
m_d->account.value()->mark_keys_as_published();
m_d->numUnpublishedKeys = 0;
}
int Crypto::numUnpublishedOneTimeKeys() const
{
return m_d->numUnpublishedKeys;
}
int Crypto::uploadedOneTimeKeysCount(std::string algorithm) const
{
return m_d->uploadedOneTimeKeysCount[algorithm];
}
MaybeString Crypto::decrypt(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto algo = content.contains("algorithm") ? content.at("algorithm").template get<std::string>() : std::string();
if (algo == olmAlgo) {
return m_d->decryptOlm(std::move(content));
} else if (algo == megOlmAlgo) {
return m_d->decryptMegOlm(eventJson);
}
return NotBut("Algorithm " + algo + " not supported");
}
bool Crypto::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
return m_d->createInboundGroupSession(std::move(k), std::move(sessionKey), std::move(ed25519Key));
}
std::size_t Crypto::importInboundGroupSessions(const nlohmann::json &keys)
{
if (!keys.is_array()) {
return 0;
}
auto validateStr = identValidate(&nlohmann::json::is_string);
std::size_t count = 0;
for (const auto &data : keys) {
if (!data.is_object()) {
continue;
}
auto key = nlohmann::json::object();
if (!(cast(key, data, "algorithm", identValidate([](const auto &j) {
return j == megOlmAlgo;
})) && cast(key, data, "room_id", validateStr)
&& cast(key, data, "session_key", validateStr)
&& cast(key, data, "session_id", validateStr)
&& cast(key, data, "/sender_claimed_keys/ed25519"_json_pointer, validateStr)
)) {
continue;
}
auto keyOfGroupSession = KeyOfGroupSession{
key["room_id"].template get<std::string>(),
key["session_id"].template get<std::string>(),
};
if (createInboundGroupSession(
keyOfGroupSession,
key["session_key"].template get<std::string>(),
key["sender_claimed_keys"]["ed25519"].template get<std::string>()
)) {
++count;
}
}
return count;
}
bool Crypto::hasInboundGroupSession(KeyOfGroupSession k) const
{
return m_d->inboundGroupSessions.find(k) != m_d->inboundGroupSessions.end();
}
bool CryptoPrivate::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
auto session = InboundGroupSession(sessionKey, ed25519Key);
if (!session.valid()) {
kzo.crypto.warn() << "Invalid session key for: " << k.roomId << ", " << k.sessionId << std::endl;
return false;
}
auto currentSessionIt = inboundGroupSessions.find(k);
if (currentSessionIt == inboundGroupSessions.end()) {
// the session is new, insert it
inboundGroupSessions.insert({k, std::move(session)});
return true;
}
// the session already exists, do some check
auto &currentSession = currentSessionIt->second;
if (currentSession.ed25519Key() != ed25519Key) {
return false;
}
return currentSession.merge(session);
}
bool Crypto::verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key)
{
if (! object.contains("signatures")) {
return false;
}
std::string signature;
try {
signature = object.at("signatures").at(userId).at(ed25519 + ":" + deviceId);
} catch(const std::exception &) {
return false;
}
object.erase("signatures");
object.erase("unsigned");
auto message = object.dump();
auto res = checkVodozemacError([&]() {
auto key = vodozemac::types::ed25519_key_from_base64(ed25519Key);
auto sig = vodozemac::types::ed25519_signature_from_base64(signature);
key->verify(message, *sig);
// It throws if the signature cannot be verified
return true;
});
return res.has_value() && res.value();
}
MaybeString Crypto::getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const
{
auto content = eventJson.at("content");
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, sessionId};
if (m_d->inboundGroupSessions.find(k) == m_d->inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto &session = m_d->inboundGroupSessions.at(k);
return session.ed25519Key();
}
}
std::size_t Crypto::encryptOlmRandomSize(std::string /* theirCurve25519IdentityKey */) const
{
// HACK: To prevent a possible race condition where we call
// encryptedOlmRandomSize() -> randomGenerator.generateRange() ~> [encryptOlm()]
//
// Here, encryptOlm() must be called in the reducer,
// as it changes the status of
// Crypto (so we should not risk any infomation lose in Crypto).
// Also, for the reducer to be pure, we may not call generateRange() in the reducer,
// as *that* is not a pure function. This means it can only be called in an effect,
// or .then() continuation. This means, the sequence from encryptedOlmRandomSize()
// to encryptOlm() can never be atomic. That is, there may be other encryptOlm()
// calls within, and that may increase the random data needed, and as a result,
// we will not have enough random data.
//
// According to the olm headers:
// https://gitlab.matrix.org/matrix-org/olm/-/blob/master/include/olm/ratchet.hh
// The maximum random size needed to encrypt is 32. We use this to ensure we
// will always have enough random data fot the encryption.
return encryptOlmMaxRandomSize();
}
std::size_t Crypto::encryptOlmMaxRandomSize()
{
return 0;
}
nlohmann::json Crypto::encryptOlmWithRandom(
RandomData random, nlohmann::json eventJson, std::string theirCurve25519IdentityKey)
{
assert(random.size() >= encryptOlmRandomSize(theirCurve25519IdentityKey));
try {
auto &session = m_d->knownSessions.at(theirCurve25519IdentityKey);
auto [type, body] = session.encryptWithRandom(random, eventJson.dump());
return nlohmann::json{
{
theirCurve25519IdentityKey, {
{"type", type},
{"body", body}
}
}
};
} catch (const std::exception &) {
return nlohmann::json::object();
}
}
nlohmann::json Crypto::encryptMegOlm(nlohmann::json eventJson)
{
auto roomId = eventJson.at("room_id").get<std::string>();
auto content = eventJson.at("content");
auto type = eventJson.at("type").get<std::string>();
auto jsonToEncrypt = nlohmann::json::object();
jsonToEncrypt["room_id"] = roomId;
jsonToEncrypt["content"] = std::move(content);
jsonToEncrypt["type"] = type;
auto textToEncrypt = std::move(jsonToEncrypt).dump();
auto &session = m_d->outboundGroupSessions.at(roomId);
auto ciphertext = session.encrypt(std::move(textToEncrypt));
return
json{
{"algorithm", CryptoConstants::megOlmAlgo},
// NOTE: we might stop sending sender_key in the future
// as per the Matrix spec
{"sender_key", curve25519IdentityKey()},
{"ciphertext", ciphertext},
{"session_id", session.sessionId()},
};
}
std::size_t Crypto::rotateMegOlmSessionRandomSize()
{
return OutboundGroupSession::constructRandomSize();
}
std::string Crypto::rotateMegOlmSessionWithRandom(RandomData random, Timestamp timeMs, std::string roomId)
{
m_d->reuseOrCreateOutboundGroupSession(
random, timeMs,
roomId, std::nullopt);
return outboundGroupSessionCurrentKey(roomId);
}
std::optional<std::string> Crypto::rotateMegOlmSessionWithRandomIfNeeded(
RandomData random, Timestamp timeMs,
std::string roomId, MegOlmSessionRotateDesc desc)
{
auto oldSessionValid = m_d->reuseOrCreateOutboundGroupSession(random, timeMs, roomId, std::move(desc));
return oldSessionValid ? std::nullopt : std::optional(outboundGroupSessionCurrentKey(roomId));
}
std::string Crypto::outboundGroupSessionInitialKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.initialSessionKey();
}
std::string Crypto::outboundGroupSessionCurrentKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.sessionKey();
}
auto Crypto::devicesMissingOutboundSessionKey(
immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>> keyMap) const -> UserIdToDeviceIdMap
{
auto ret = UserIdToDeviceIdMap{};
for (auto [userId, devices] : keyMap) {
auto unknownDevices =
intoImmer(immer::flex_vector<std::string>{},
- zug::filter([=](auto kv) {
+ zug::filter([this](auto kv) {
auto [deviceId, theirCurve25519IdentityKey] = kv;
return m_d->knownSessions.find(theirCurve25519IdentityKey)
== m_d->knownSessions.end();
})
- | zug::map([=](auto kv) {
+ | zug::map([](auto kv) {
auto [deviceId, key] = kv;
return deviceId;
}),
devices);
if (! unknownDevices.empty()) {
ret = std::move(ret).set(userId, std::move(unknownDevices));
}
}
return ret;
}
std::size_t Crypto::createOutboundSessionRandomSize()
{
return Session::constructOutboundRandomSize();
}
void Crypto::createOutboundSessionWithRandom(
RandomData random,
std::string theirIdentityKey,
std::string theirOneTimeKey)
{
assert(random.size() >= createOutboundSessionRandomSize());
auto session = Session(OutboundSessionTag{},
RandomTag{},
random,
*m_d,
theirIdentityKey,
theirOneTimeKey);
if (session.valid()) {
m_d->knownSessions.insert_or_assign(theirIdentityKey,
std::move(session));
}
}
nlohmann::json Crypto::toJson() const
{
std::string pickledData = m_d->valid ? m_d->pickle() : std::string();
auto j = nlohmann::json::object({
{"valid", m_d->valid},
{"version", 1},
{"account", std::move(pickledData)},
{"uploadedOneTimeKeysCount", m_d->uploadedOneTimeKeysCount},
{"numUnpublishedKeys", m_d->numUnpublishedKeys},
{"knownSessions", nlohmann::json(m_d->knownSessions)},
{"inboundGroupSessions", nlohmann::json(m_d->inboundGroupSessions)},
{"outboundGroupSessions", nlohmann::json(m_d->outboundGroupSessions)},
});
return j;
}
void Crypto::loadJson(const nlohmann::json &j)
{
m_d->valid = j.contains("valid") ? j["valid"].template get<bool>() : true;
const auto &pickledData = j.at("account").template get<std::string>();
if (m_d->valid) {
if (j.contains("version") && j["version"] == 1) {
m_d->valid = m_d->unpickle(pickledData);
} else {
m_d->valid = m_d->unpickleFromLibolm(pickledData);
}
}
m_d->uploadedOneTimeKeysCount = j.at("uploadedOneTimeKeysCount");
m_d->numUnpublishedKeys = j.at("numUnpublishedKeys");
m_d->knownSessions = j.at("knownSessions").template get<decltype(m_d->knownSessions)>();
m_d->inboundGroupSessions = j.at("inboundGroupSessions").template get<decltype(m_d->inboundGroupSessions)>();
m_d->outboundGroupSessions = j.at("outboundGroupSessions").template get<decltype(m_d->outboundGroupSessions)>();
}
}
diff --git a/src/crypto/session.cpp b/src/crypto/session.cpp
index 22a8c16..a89d299 100644
--- a/src/crypto/session.cpp
+++ b/src/crypto/session.cpp
@@ -1,256 +1,256 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <debug.hpp>
#include "crypto-p.hpp"
#include "session-p.hpp"
namespace Kazv
{
SessionPrivate::SessionPrivate()
: session(std::nullopt)
{
}
SessionPrivate::SessionPrivate(OutboundSessionTag,
- RandomTag,
- RandomData random,
- CryptoPrivate &cryptoD,
- std::string theirIdentityKey,
- std::string theirOneTimeKey)
+ RandomTag,
+ [[maybe_unused]] RandomData random,
+ CryptoPrivate &cryptoD,
+ std::string theirIdentityKey,
+ std::string theirOneTimeKey)
: SessionPrivate()
{
assert(random.size() >= Session::constructOutboundRandomSize());
session = checkVodozemacError([&]() {
auto identityKey = vodozemac::types::curve_key_from_base64(rust::Str(theirIdentityKey));
auto oneTimeKey = vodozemac::types::curve_key_from_base64(rust::Str(theirOneTimeKey));
return cryptoD.account.value()->create_outbound_session(
*identityKey,
*oneTimeKey
);
});
valid = session.has_value();
}
SessionPrivate::SessionPrivate(InboundSessionTag,
CryptoPrivate &cryptoD,
std::string theirIdentityKey,
std::string message)
: SessionPrivate()
{
auto res = checkVodozemacError([&]() {
auto identityKey = vodozemac::types::curve_key_from_base64(rust::Str(theirIdentityKey));
auto msg = vodozemac::olm::olm_message_from_parts(vodozemac::olm::OlmMessageParts{
0, // pre-key message
message,
});
return cryptoD.account.value()->create_inbound_session(
*identityKey,
*msg
);
});
valid = res.has_value();
if (valid) {
session = std::move(res->session);
firstDecrypted = std::string(res->plaintext.begin(), res->plaintext.end());
}
}
SessionPrivate::SessionPrivate(const SessionPrivate &that)
: SessionPrivate()
{
if (that.valid) {
valid = unpickle(that.pickle());
}
}
std::string SessionPrivate::pickle() const
{
auto pickleData = session.value()->pickle(VODOZEMAC_PICKLE_KEY);
return static_cast<std::string>(pickleData);
}
bool SessionPrivate::unpickle(std::string pickleData)
{
session = checkVodozemacError([&]() {
return vodozemac::olm::session_from_pickle(pickleData, VODOZEMAC_PICKLE_KEY);
});
return session.has_value();
}
bool SessionPrivate::unpickleFromLibolm(std::string pickleData)
{
session = checkVodozemacError([&]() {
return vodozemac::olm::session_from_libolm_pickle(
pickleData,
rust::Slice<const unsigned char>(OLM_PICKLE_KEY.data(), OLM_PICKLE_KEY.size()));
});
return session.has_value();
}
MaybeString SessionPrivate::takeFirstDecrypted()
{
auto res = std::move(firstDecrypted);
firstDecrypted = std::nullopt;
if (res.has_value()) {
return res.value();
} else {
return NotBut("No first decrypted available");
}
}
std::size_t Session::constructOutboundRandomSize()
{
return 0;
}
Session::Session()
: m_d(new SessionPrivate)
{
}
Session::Session(OutboundSessionTag,
RandomTag,
RandomData data,
CryptoPrivate &cryptoD,
std::string theirIdentityKey,
std::string theirOneTimeKey)
: m_d(new SessionPrivate{
OutboundSessionTag{},
RandomTag{},
std::move(data),
cryptoD,
theirIdentityKey,
theirOneTimeKey})
{
}
Session::Session(InboundSessionTag,
CryptoPrivate &cryptoD,
std::string theirIdentityKey,
std::string theirOneTimeKey)
: m_d(new SessionPrivate{
InboundSessionTag{},
cryptoD,
theirIdentityKey,
theirOneTimeKey})
{
}
Session::~Session() = default;
Session::Session(const Session &that)
: m_d(new SessionPrivate(*that.m_d))
{
}
Session::Session(Session &&that)
: m_d(std::move(that.m_d))
{
}
Session &Session::operator=(const Session &that)
{
m_d.reset(new SessionPrivate(*that.m_d));
return *this;
}
Session &Session::operator=(Session &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool Session::matches(std::string message)
{
auto res = checkVodozemacError([&]() {
auto msg = vodozemac::olm::olm_message_from_parts(vodozemac::olm::OlmMessageParts{
0, // pre-key
message,
});
return m_d->session.value()->session_matches(*msg);
});
return res.has_value() && res.value();
}
bool Session::valid() const
{
// maybe a moved-from state, so check m_d first
return m_d && m_d->valid;
}
MaybeString Session::decrypt(int type, std::string message)
{
auto res = checkVodozemacError([&]() {
auto msg = vodozemac::olm::olm_message_from_parts(vodozemac::olm::OlmMessageParts{
static_cast<std::size_t>(type),
message,
});
return m_d->session.value()->decrypt(*msg);
});
if (!res.has_value()) {
return NotBut(res.reason());
}
return std::string(res.value().begin(), res.value().end());
}
std::size_t Session::encryptRandomSize() const
{
return 0;
}
- std::pair<int, std::string> Session::encryptWithRandom(RandomData random, std::string plainText)
+ std::pair<int, std::string> Session::encryptWithRandom([[maybe_unused]] RandomData random, std::string plainText)
{
assert(random.size() >= encryptRandomSize());
auto res = checkVodozemacError([&]() {
return m_d->session.value()->encrypt(plainText);
});
if (!res.has_value()) {
return { -1, "" };
}
auto [type, msg] = res.value()->to_parts();
return { type, static_cast<std::string>(msg) };
}
void to_json(nlohmann::json &j, const Session &s)
{
j = nlohmann::json::object({
{"valid", s.m_d->valid},
{"version", 1},
{"data", s.m_d->valid ? s.m_d->pickle() : std::string()}
});
}
void from_json(const nlohmann::json &j, Session &s)
{
if (j.at("valid").template get<bool>()) {
if (j.contains("version") && j["version"] == 1) {
s.m_d->valid = s.m_d->unpickle(j.at("data"));
} else {
s.m_d->valid = s.m_d->unpickleFromLibolm(j.at("data"));
}
}
}
}
diff --git a/src/eventemitter/lagerstoreeventemitter.hpp b/src/eventemitter/lagerstoreeventemitter.hpp
index d392add..707fac3 100644
--- a/src/eventemitter/lagerstoreeventemitter.hpp
+++ b/src/eventemitter/lagerstoreeventemitter.hpp
@@ -1,174 +1,174 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <vector>
#include <memory>
#include <algorithm>
#include <lager/store.hpp>
#include <lager/reader.hpp>
#include <lager/event_loop/manual.hpp>
#include "types.hpp"
#include "eventinterface.hpp"
namespace Kazv
{
class LagerStoreEventEmitter : public EventInterface
{
struct Model { KazvTrigger curEvent; };
struct Action { KazvTrigger nextEvent; };
struct ListenerHolder;
using Result = std::pair<Model,
lager::effect<Action, lager::deps<ListenerHolder &>>>;
using SlotT = std::function<void(KazvTrigger)>;
struct Listener
{
void emit(KazvTrigger e) {
for (const auto &slot: m_slots) {
slot(e);
}
}
void connect(SlotT slot) {
m_slots.push_back(std::move(slot));
}
std::vector<SlotT> m_slots;
};
using ListenerSP = std::shared_ptr<Listener>;
using ListenerWSP = std::weak_ptr<Listener>;
struct ListenerHolder
{
void sendToListeners(KazvTrigger e) {
bool needsCleanup = false;
for (auto listener : m_listeners) {
auto strongListener = listener.lock();
if (strongListener) {
strongListener->emit(e);
} else {
needsCleanup = true;
}
}
if (needsCleanup) {
- std::remove_if(m_listeners.begin(),
+ m_listeners.erase(std::remove_if(m_listeners.begin(),
m_listeners.end(),
[](auto ptr) {
return ptr.expired();
- });
+ }), m_listeners.end());
}
}
std::vector<ListenerWSP> m_listeners;
};
inline static Result update(Model, Action a) {
return {
Model{a.nextEvent},
[=](auto &&ctx) {
auto &holder = lager::get<ListenerHolder &>(ctx);
holder.sendToListeners(a.nextEvent);
}
};
}
public:
template<class EventLoop>
LagerStoreEventEmitter(EventLoop loop)
: m_holder{}
, m_store(
lager::make_store<Action>(
Model{},
loop,
lager::with_reducer(&update),
lager::with_deps(std::ref(m_holder))))
, m_postingFunc(
[loop=loop](auto &&func) mutable {
loop.post(std::forward<decltype(func)>(func));
}) {}
~LagerStoreEventEmitter() override = default;
void emit(KazvTrigger e) override {
m_store.dispatch(Action{e});
}
class Watchable
{
public:
Watchable(LagerStoreEventEmitter &ee)
: m_listener(std::make_shared<Listener>()) {
ee.addListener(m_listener);
}
template<class EventType, class Func>
void after(Func &&func) {
m_listener->connect(
[f=std::forward<Func>(func)](KazvTrigger e) {
if (std::holds_alternative<EventType>(e)) {
f(std::get<EventType>(e));
}
});
}
template<class Func>
void afterAll(Func &&func) {
m_listener->connect(
[f=std::forward<Func>(func)](KazvTrigger e) {
f(e);
});
}
private:
ListenerSP m_listener;
};
/**
* An object you can watch for events.
* ```
* auto watchable = eventEmitter.watchable();
* watchable.after<ReceivingRoomTimelineEvent>(
* [](auto &&e) { ... });
* ```
*/
Watchable watchable() {
return Watchable(*this);
}
private:
void addListener(ListenerSP listener) {
- m_postingFunc([=]() {
+ m_postingFunc([=, this]() {
m_holder.m_listeners.push_back(listener);
});
}
using StoreT =
decltype(lager::make_store<Action>(
Model{},
lager::with_manual_event_loop{},
lager::with_reducer(&update),
lager::with_deps(std::ref(detail::declref<ListenerHolder>()))));
using PostingFunc = std::function<void(std::function<void()>)>;
ListenerHolder m_holder;
StoreT m_store;
PostingFunc m_postingFunc;
};
}
diff --git a/src/job/cprjobhandler.cpp b/src/job/cprjobhandler.cpp
index c3a139b..76df66b 100644
--- a/src/job/cprjobhandler.cpp
+++ b/src/job/cprjobhandler.cpp
@@ -1,519 +1,520 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <algorithm>
#include <deque>
#include <zug/into_vector.hpp>
#include <zug/transducer/map.hpp>
#include <cpr.h>
#include <callback.h>
#include <asio-std-file-handler.hpp>
#include "cprjobhandler.hpp"
#include "debug.hpp"
namespace {
using namespace Kazv;
struct SyncFileStream
{
using DataT = FileContent;
inline SyncFileStream(std::string filename, FileOpenMode mode)
: m_stream(filename,
(mode == FileOpenMode::Read
? std::ios_base::in
: std::ios_base::out)
| std::ios_base::binary)
{}
template<class Callback>
void read(int maxSize, Callback readCallback) {
auto buf = std::vector<char>(maxSize, '\0');
auto data = FileContent{};
auto actualSize = int{};
try {
m_stream.read(buf.data(), maxSize);
actualSize = m_stream.gcount();
data = FileContent(buf.begin(), buf.begin() + actualSize);
} catch (const std::exception &) {
readCallback(FileOpRetCode::Error, FileContent{});
return;
}
if (actualSize > 0) {
readCallback(FileOpRetCode::Success, data);
} else if (m_stream.eof()) {
readCallback(FileOpRetCode::Eof, FileContent{});
} else {
readCallback(FileOpRetCode::Error, FileContent{});
}
}
template<class Callback>
void write(DataT data, Callback writeCallback) {
for (auto c : data) {
m_stream.put(c);
if (m_stream.bad()) {
writeCallback(FileOpRetCode::Error, 0);
return;
}
}
writeCallback(FileOpRetCode::Success, data.size());
}
std::fstream m_stream;
};
struct SyncFileProvider
{
using FileStreamT = SyncFileStream;
std::string filename;
FileStreamT getStream(FileOpenMode mode) const {
return FileStreamT(filename, mode);
}
};
struct SyncFileHandler
{
using FileProviderT = SyncFileProvider;
FileProviderT getProviderFor(FileDesc desc) const {
// assert(desc.name())
return FileProviderT{desc.name().value()};
}
};
}
namespace Kazv
{
struct CprJobHandler::Private
{
CprJobHandler *q;
boost::asio::io_context::executor_type executor;
using TimerSP = std::shared_ptr<boost::asio::steady_timer>;
using TimerSPList = std::vector<TimerSP>;
using TimerMap = std::unordered_map<std::optional<std::string>, TimerSPList>;
TimerMap timerMap;
enum Status
{
Waiting,
Running,
};
using Callback = std::function<void(Response)>;
struct JobDesc
{
BaseJob job;
Callback callback;
Status status;
};
using JobQueue = std::deque<JobDesc>;
using JobMap = std::unordered_map<std::string, JobQueue>;
JobMap jobQueues;
void submitImpl(BaseJob job, std::function<void(Response)> userCallback);
void addToQueue(BaseJob job, Callback callback) {
boost::asio::post(
executor,
- [=] {
+ [=, this] {
// precondition: job has a queueId
auto queueId = job.queueId().value();
jobQueues[queueId].push_back(JobDesc{job, callback, Waiting});
});
}
void clearQueue(std::string queueId) {
boost::asio::post(
executor,
- [=] { clearQueueImpl(queueId); });
+ [=, this] { clearQueueImpl(queueId); });
}
void clearQueueImpl(std::string queueId) {
Response fakeResponse;
fakeResponse.statusCode = headFailureCancelledStatusCode;
fakeResponse.body = JsonBody(
json{ {"errcode", headFailureCancelledErrorCode},
{"error", headFailureCancelledErrorMsg} }
);
kzo.job.dbg() << "clearQueueImpl called with " << queueId << std::endl;
for (auto [job, callback, status] : jobQueues[queueId]) {
kzo.job.dbg() << "this job is " << (status == Waiting ? "Waiting" : "Running") << std::endl;
if (status == Waiting) {
// Run callback in a different thread, just as in submitImpl().
- q->async([=] { callback(job.genResponse(fakeResponse)); } );
+ q->async([=, this] { callback(job.genResponse(fakeResponse)); } );
}
// if status is Running, the callback is already called
}
jobQueues[queueId].clear();
}
void popJob(std::string queueId) {
boost::asio::post(
executor,
- [=] { popJobImpl(queueId); });
+ [=, this] { popJobImpl(queueId); });
}
void popJobImpl(std::string queueId) {
if (! jobQueues[queueId].empty()) {
jobQueues[queueId].pop_front();
}
}
void monitorQueues() {
boost::asio::post(
executor,
- [=] {
+ [=, this] {
// precondition: job has a queueId
for (auto &[queueId, queue] : jobQueues) { // need to change queue
if (! queue.empty()) {
auto [job, callback, status] = queue.front();
if (status == Waiting) {
queue.front().status = Running;
submitImpl(
job,
- [=](Response r) { // in new thread
+ [=, this](Response r) { // in new thread
callback(r);
if (! r.success() // should be enough for now
&& job.queuePolicy() == CancelFutureIfFailed) {
clearQueue(queueId); // in executor thread
} else {
popJob(queueId); // in executor thread
}
});
}
}
}
});
}
void addTimerToMap(TimerSP timer, std::optional<std::string> timerId) {
boost::asio::post(
executor,
- [=] {
+ [=, this] {
timerMap[timerId].push_back(timer);
});
}
void clearTimer(TimerSP timer, std::optional<std::string> timerId) {
boost::asio::post(
executor,
- [=] {
- std::remove(timerMap[timerId].begin(), timerMap[timerId].end(), timer);
+ [=, this] {
+ auto &timers = timerMap[timerId];
+ timers.erase(std::remove(timers.begin(), timers.end(), timer), timers.end());
});
}
void cancelAllTimers(std::optional<std::string> timerId) {
boost::asio::post(
executor,
- [=] {
+ [=, this] {
cancelAllTimersImpl(timerId);
});
}
void cancelAllTimersImpl(std::optional<std::string> timerId) {
auto timers = timerMap[timerId];
timerMap.erase(timerId);
for (auto timer : timers) {
timer->cancel();
}
}
void intervalTimerCallback(TimerSP timer,
std::function<void()> func,
int ms,
const boost::system::error_code &error) {
if (! error) {
func();
auto dur = boost::asio::chrono::milliseconds(ms);
timer->expires_at(timer->expiry() + dur);
timer->async_wait(
- [=](const boost::system::error_code &error) {
+ [=, this](const boost::system::error_code &error) {
intervalTimerCallback(timer, func, ms, error);
});
}
}
// This makes the callbacks work with both versions of cpr.
struct ReadCallback : public std::function<bool(char *, size_t &)>
{
using FuncT = std::function<bool(char *, size_t &)>;
using FuncT::FuncT;
using FuncT::operator();
bool operator()(char *buffer, size_t &length, intptr_t) const
{
return (*this)(buffer, length);
}
};
struct WriteCallback : public std::function<bool(std::string)>
{
using FuncT = std::function<bool(std::string)>;
using FuncT::FuncT;
using FuncT::operator();
bool operator()(std::string buffer, intptr_t) const
{
return (*this)(std::move(buffer));
}
bool operator()(std::string_view buffer, intptr_t) const
{
return (*this)(std::string(buffer));
}
};
};
CprJobHandler::CprJobHandler(boost::asio::io_context::executor_type executor)
: m_d(new Private{this, std::move(executor), Private::TimerMap{}, Private::JobMap{}})
{
setInterval(
- [=] {
+ [=, this] {
m_d->monitorQueues();
},
50, // ms
"-queue-monitor");
}
CprJobHandler::~CprJobHandler() = default;
void CprJobHandler::async(std::function<void()> func)
{
std::thread([func=std::move(func), guard=boost::asio::make_work_guard(m_d->executor)]() {
func();
}).detach();
}
void CprJobHandler::setTimeout(std::function<void()> func, int ms, std::optional<std::string> timerId)
{
auto timer=std::make_shared<boost::asio::steady_timer>(
m_d->executor, boost::asio::chrono::milliseconds(ms));
m_d->addTimerToMap(timer, timerId);
timer->async_wait(
- [=, timer=timer](const boost::system::error_code &error){
+ [=, this, timer=timer](const boost::system::error_code &error){
if (! error) {
func();
this->m_d->clearTimer(timer, timerId);
}
});
}
void CprJobHandler::setInterval(std::function<void()> func, int ms, std::optional<std::string> timerId)
{
auto dur = boost::asio::chrono::milliseconds(ms);
auto timer = std::make_shared<boost::asio::steady_timer>(m_d->executor, dur);
m_d->addTimerToMap(timer, timerId);
timer->async_wait(
- [=](const boost::system::error_code &error) {
+ [=, this](const boost::system::error_code &error) {
m_d->intervalTimerCallback(timer, func, ms, error);
});
}
void CprJobHandler::cancel(std::string timerId)
{
m_d->cancelAllTimers(timerId);
}
void CprJobHandler::submit(BaseJob job, std::function<void(Response)> userCallback)
{
if (job.queueId()) {
m_d->addToQueue(job, userCallback);
} else {
m_d->submitImpl(job, userCallback);
}
}
void CprJobHandler::Private::submitImpl(BaseJob job, std::function<void(Response)> userCallback)
{
cpr::Url url{job.url()};
cpr::Body body;
BaseJob::Header origHeader = job.requestHeader();
cpr::Header header(origHeader.get().begin(), origHeader.get().end());
auto readCallback = Private::ReadCallback();
auto writeCallback = Private::WriteCallback();
if (! std::holds_alternative<FileDesc>(job.requestBody())) {
body = std::get<BytesBody>(job.requestBody());
} else {
auto fileDesc = std::get<FileDesc>(job.requestBody());
auto typeOpt = fileDesc.contentType();
if (typeOpt) {
header.insert_or_assign("Content-Type", typeOpt.value());
kzo.job.dbg() << "Content-Type is " << header["Content-Type"] << std::endl;
}
auto fh = SyncFileHandler{};
auto provider = fileDesc.provider(fh);
auto stream = std::make_shared<FileStream>(provider.getStream());
readCallback =
[stream](char *buffer, size_t &length) -> bool {
bool retval = true;
kzo.job.dbg() << "A buffer of length " << length << " requested." << std::endl;
stream->read(length,
[&](FileOpRetCode code, FileContent data) {
if (code == FileOpRetCode::Success) {
std::copy_n(data.begin(), data.size(), buffer);
kzo.job.dbg() << "Read file successful, read " << data.size() << " bytes." << std::endl;
length = data.size();
} else if (code == FileOpRetCode::Eof) {
kzo.job.dbg() << "Got eof." << std::endl;
length = 0;
} else {
kzo.job.dbg() << "Got error reading file." << std::endl;
length = 0;
retval = false;
}
});
return retval;
};
}
if (job.responseFile()) {
auto fileDesc = job.responseFile().value();
auto fh = SyncFileHandler{};
auto provider = fileDesc.provider(fh);
auto stream = std::make_shared<FileStream>(provider.getStream(FileOpenMode::Write));
writeCallback =
[stream](std::string buffer) -> bool {
bool retval = true;
kzo.job.dbg() << "A buffer of length " << buffer.size() << " responded." << std::endl;
FileContent data(buffer.begin(), buffer.end());
stream->write(data,
[&](FileOpRetCode code, int num) {
if (code == FileOpRetCode::Success) {
kzo.job.dbg() << "Write file successful, wrote " << num << " bytes." << std::endl;
} else {
kzo.job.dbg() << "Got error writing file." << std::endl;
retval = false;
}
});
return retval;
};
}
cpr::Parameters params;
BaseJob::Query query = job.requestQuery();
BaseJob::ReturnType returnType = job.returnType();
BaseJob::Method method = job.requestMethod();
if (! query.empty()) {
// from cpr/parameters.cpp
cpr::CurlHolder holder;
for (const auto &kv : query) {
std::string key = kv.first;
std::string value = kv.second;
params.Add(cpr::Parameter(std::move(key), std::move(value)));
}
}
auto callback = [returnType, responseFile=job.responseFile()](cpr::Response r) -> Response {
Body body;
if (responseFile) {
body = responseFile.value();
} else if (returnType == BaseJob::ReturnType::Json) {
try {
body = BaseJob::JsonBody(std::move(json::parse(r.text)));
} catch (const json::exception &e) {
// the response is not valid json
body = r.text;
kzo.job.dbg() << "body is not correct json: " << e.what() << std::endl;
}
} else {
body = r.text;
}
return { static_cast<Response::StatusCode>(r.status_code),
body,
BaseJob::Header(r.header.begin(), r.header.end()),
{} // extraData, will be added in genResponse
};
};
std::shared_future<Response> res = std::visit(lager::visitor{
- [=](BaseJob::Get) {
+ [=, this](BaseJob::Get) {
if (readCallback) {
return cpr::GetCallback(callback, url, cpr::ReadCallback(readCallback), header, params);
} else if (writeCallback) {
return cpr::GetCallback(callback, url, cpr::WriteCallback(writeCallback), header, body, params);
} else {
return cpr::GetCallback(callback, url, header, body, params);
}
},
- [=](BaseJob::Post) {
+ [=, this](BaseJob::Post) {
if (readCallback) {
return cpr::PostCallback(callback, url, cpr::ReadCallback(readCallback), header, params);
} else if (writeCallback) {
return cpr::PostCallback(callback, url, cpr::WriteCallback(writeCallback), header, body, params);
} else {
return cpr::PostCallback(callback, url, header, body, params);
}
},
- [=](BaseJob::Put) {
+ [=, this](BaseJob::Put) {
if (readCallback) {
return cpr::PutCallback(callback, url, cpr::ReadCallback(readCallback), header, params);
} else if (writeCallback) {
return cpr::PutCallback(callback, url, cpr::WriteCallback(writeCallback), header, body, params);
} else {
return cpr::PutCallback(callback, url, header, body, params);
}
},
- [=](BaseJob::Delete) {
+ [=, this](BaseJob::Delete) {
if (readCallback) {
return cpr::DeleteCallback(callback, url, cpr::ReadCallback(readCallback), header, params);
} else if (writeCallback) {
return cpr::DeleteCallback(callback, url, cpr::WriteCallback(writeCallback), header, body, params);
} else {
return cpr::DeleteCallback(callback, url, header, body, params);
}
}
}, method).share();
- q->async([=]() {
+ q->async([=, this]() {
userCallback(job.genResponse(res.get()));
});
}
void CprJobHandler::stop()
{
boost::asio::post(
m_d->executor,
- [=] {
+ [=, this] {
auto ids = zug::into_vector(
zug::map([](auto i) { return i.first; }),
m_d->timerMap);
for (auto id : ids) {
m_d->cancelAllTimersImpl(id);
}
m_d->jobQueues.clear();
});
}
}
diff --git a/src/tests/crypto-test.cpp b/src/tests/crypto-test.cpp
index 2bfd84f..e58cc53 100644
--- a/src/tests/crypto-test.cpp
+++ b/src/tests/crypto-test.cpp
@@ -1,812 +1,815 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
#include <sstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <crypto/crypto.hpp>
#include <aes-256-ctr.hpp>
#include <base64.hpp>
#include <sha256.hpp>
#include "crypto/crypto-test-resource.hpp"
using namespace Kazv;
using namespace Kazv::CryptoConstants;
using IAr = boost::archive::text_iarchive;
using OAr = boost::archive::text_oarchive;
static const auto resource = cryptoDumpResource();
json makeEncryptedJson(json ciphertext, std::string senderKey)
{
return json{{"content", {
{"algorithm", olmAlgo},
{"ciphertext", std::move(ciphertext)},
{"sender_key", std::move(senderKey)},
}}};
}
template<class T>
static void serializeDup(const T &in, T &out)
{
std::stringstream stream;
{
auto ar = OAr(stream);
ar << in;
}
{
auto ar = IAr(stream);
ar >> out;
}
}
static bool doesDecryptTo(Crypto &crypto, const nlohmann::json &encryptedEvent, const nlohmann::json &plainText)
{
auto res = crypto.decrypt(encryptedEvent);
if (!res) {
return false;
}
auto decrypted = json::parse(res.value());
return decrypted == plainText;
}
TEST_CASE("Crypto constructors", "[crypto]")
{
Crypto crypto;
REQUIRE(!crypto.valid());
Crypto crypto2(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
REQUIRE(crypto2.valid());
}
TEST_CASE("Crypto conversion from libolm to vodozemac", "[crypto]")
{
Crypto a;
a.loadJson(resource["a"]);
Crypto b;
b.loadJson(resource["b"]);
// encrypt with existing sessions
auto aIdKey = a.curve25519IdentityKey();
auto origJson = json{{"test", "mew"}};
{
auto encryptedMsg = b.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto decryptedOpt = a.decrypt(makeEncryptedJson(encryptedMsg, b.curve25519IdentityKey()));
REQUIRE(decryptedOpt);
}
// encrypt/decrypt with new sessions
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
{
Crypto c(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
auto encryptedMsg = c.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto decryptedOpt = a.decrypt(makeEncryptedJson(encryptedMsg, c.curve25519IdentityKey()));
REQUIRE(decryptedOpt.reason() == "");
REQUIRE(decryptedOpt);
REQUIRE(decryptedOpt.value() == origJson.dump());
}
}
TEST_CASE("Crypto should be copyable", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto oneTimeKeys = crypto.unpublishedOneTimeKeys();
Crypto cryptoClone(crypto);
REQUIRE(crypto.ed25519IdentityKey() == cryptoClone.ed25519IdentityKey());
REQUIRE(crypto.curve25519IdentityKey() == cryptoClone.curve25519IdentityKey());
auto oneTimeKeys2 = cryptoClone.unpublishedOneTimeKeys();
REQUIRE(oneTimeKeys == oneTimeKeys2);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == cryptoClone.numUnpublishedOneTimeKeys());
}
TEST_CASE("Crypto should be serializable", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto oneTimeKeys = crypto.unpublishedOneTimeKeys();
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(crypto.ed25519IdentityKey() == cryptoClone.ed25519IdentityKey());
REQUIRE(crypto.curve25519IdentityKey() == cryptoClone.curve25519IdentityKey());
auto oneTimeKeys2 = cryptoClone.unpublishedOneTimeKeys();
REQUIRE(oneTimeKeys == oneTimeKeys2);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == cryptoClone.numUnpublishedOneTimeKeys());
}
TEST_CASE("Invalid Crypto should be serializable", "[crypto]")
{
Crypto crypto;
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(!cryptoClone.valid());
}
TEST_CASE("Serialize Crypto with an OutboundGroupSession", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
std::string roomId = "!example:example.org";
auto desc = MegOlmSessionRotateDesc{500000 /* ms */, 100 /* messages */};
crypto.rotateMegOlmSessionWithRandom(genRandomData(Crypto::rotateMegOlmSessionRandomSize()), currentTimeMs(), roomId);
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(! cryptoClone.rotateMegOlmSessionWithRandomIfNeeded(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()), currentTimeMs(),
roomId, desc).has_value());
}
TEST_CASE("Should handle unknown device sending Olm pre-key message", "[crypto][olm]")
{
// Bob: the current user, generates and publishes one-time keys
Crypto bob(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
bob.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto bobKeys = bob.unpublishedOneTimeKeys();
bob.markOneTimeKeysAsPublished();
auto bobIdentityKey = bob.curve25519IdentityKey();
auto bobOneTimeKey = std::string{};
for (auto [id, key] : bobKeys[curve25519].items()) {
bobOneTimeKey = key;
}
// Alice: a device that Bob does not know about
// (Bob has never queried her keys or sent her a message)
Crypto alice(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
auto aliceIdentityKey = alice.curve25519IdentityKey();
// Alice creates an outbound session to Bob and encrypts a message
alice.createOutboundSessionWithRandom(
genRandomData(Crypto::createOutboundSessionRandomSize()),
bobIdentityKey, bobOneTimeKey);
auto plainText = json{{"test", "mew"}};
auto encryptedMsg = alice.encryptOlmWithRandom(
genRandomData(Crypto::encryptOlmMaxRandomSize()),
plainText, bobIdentityKey);
// Bob receives the Olm message from Alice
auto encJson = makeEncryptedJson(encryptedMsg, aliceIdentityKey);
auto decryptedOpt = bob.decrypt(encJson);
REQUIRE(decryptedOpt);
auto decryptedJson = json::parse(decryptedOpt.value());
REQUIRE(decryptedJson == plainText);
}
TEST_CASE("Should reuse existing inbound session to encrypt after receiving from unknown device", "[crypto][olm]")
{
Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
a.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
auto aIdKey = a.curve25519IdentityKey();
// b (unknown to a) creates outbound session and sends a pre-key message
b.createOutboundSessionWithRandom(
genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
auto origJson = json{{"hello", "world"}};
auto encryptedMsg = b.encryptOlmWithRandom(
genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto encJson = makeEncryptedJson(encryptedMsg, b.curve25519IdentityKey());
// a decrypts - this creates an inbound session from the pre-key message
auto decryptedOpt = a.decrypt(encJson);
REQUIRE(decryptedOpt);
REQUIRE(json::parse(decryptedOpt.value()) == origJson);
// Now a can use the inbound session to encrypt back to b
using StrMap = immer::map<std::string, std::string>;
auto devMap = immer::map<std::string, StrMap>()
.set("b", StrMap().set("dev", b.curve25519IdentityKey()));
auto devices = a.devicesMissingOutboundSessionKey(devMap);
// The inbound session should be usable as an outbound session as well,
// so no device should be missing an olm session
auto expected = immer::map<std::string, immer::flex_vector<std::string>>();
REQUIRE(devices == expected);
}
TEST_CASE("Generating and publishing keys should work", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 1);
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 2);
crypto.markOneTimeKeysAsPublished();
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 0);
}
TEST_CASE("Should reuse existing inbound session to encrypt", "[crypto]")
{
Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
a.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
// Get A publish the key and send to B
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
auto aIdKey = a.curve25519IdentityKey();
b.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
auto origJson = json{{"test", "mew"}};
auto encryptedMsg = b.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto encJson = json{
{"content",
{
{"algorithm", olmAlgo},
{"ciphertext", encryptedMsg},
{"sender_key", b.curve25519IdentityKey()}
}
}
};
auto decryptedOpt = a.decrypt(encJson);
REQUIRE(decryptedOpt);
auto decryptedJson = json::parse(decryptedOpt.value());
REQUIRE(decryptedJson == origJson);
using StrMap = immer::map<std::string, std::string>;
auto devMap = immer::map<std::string, StrMap>()
.set("b", StrMap().set("dev", b.curve25519IdentityKey()));
Crypto aClone{a};
auto devices = a.devicesMissingOutboundSessionKey(devMap);
auto devicesAClone = aClone.devicesMissingOutboundSessionKey(devMap);
// No device should be missing an olm session, as A has received an
// inbound olm session before.
auto expected = immer::map<std::string, immer::flex_vector<std::string>>();
REQUIRE(devices == expected);
REQUIRE(devicesAClone == expected);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
std::string original = "test for aes-256-ctr";
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR with any sequence type", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
std::string oStr = "test for aes-256-ctr";
std::vector<unsigned char> original(oStr.begin(), oStr.end());
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR in a non-destructive way", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
std::string original = "test for aes-256-ctr";
auto [next, encrypted] = desc.process(original);
auto [next2, encrypted2] = desc.process(original);
REQUIRE(encrypted == encrypted2);
auto [next3, decrypted] = desc.process(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR in batches", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
std::string original = "test for aes-256-ctr";
std::string orig2 = "another test string...";
auto [next, encrypted] = desc.process(original);
auto [next2, encrypted2] = next.process(orig2);
auto [next3, decrypted] = desc.process(encrypted + encrypted2);
REQUIRE(original + orig2 == decrypted);
}
TEST_CASE("AES-256-CTR should be movable", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = std::move(desc);
REQUIRE(desc2.valid());
REQUIRE(! desc.valid());
std::string original = "test for aes-256-ctr";
std::string encrypted;
// Can be moved from itself
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wself-move"
desc2 = std::move(desc2);
+#pragma GCC diagnostic pop
REQUIRE(desc2.valid());
std::tie(desc2, encrypted) = std::move(desc2).process(original);
REQUIRE(desc2.valid());
}
TEST_CASE("AES-256-CTR should be copyable", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
REQUIRE(desc2.valid());
REQUIRE(desc.valid());
desc = AES256CTRDesc::fromRandom(RandomData{});
std::string original = "test for aes-256-ctr";
std::string encrypted;
REQUIRE(desc2.valid());
std::tie(desc2, encrypted) = desc2.process(original);
REQUIRE(desc2.valid());
}
TEST_CASE("Construct AES-256-CTR from known key and iv", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = AES256CTRDesc(desc.key(), desc.iv());
REQUIRE(desc2.valid());
REQUIRE(desc.key() == desc2.key());
REQUIRE(desc.iv() == desc2.iv());
}
TEST_CASE("AES-256-CTR validity check", "[crypto][aes256ctr]")
{
SECTION("Not enough random, should reject") {
ByteArray random = genRandom(AES256CTRDesc::randomSize - 1);
auto desc = AES256CTRDesc::fromRandom(random);
REQUIRE(! desc.valid());
}
SECTION("More than enough random, should accept") {
ByteArray random = genRandom(AES256CTRDesc::randomSize + 1);
auto desc = AES256CTRDesc::fromRandom(random);
REQUIRE(desc.valid());
}
}
TEST_CASE("AES256CTRDesc::fromRandom() should leave the lower 8 bytes as 0 for the counter", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto iv = decodeBase64(desc.iv());
REQUIRE(iv.size() == AES256CTRDesc::ivSize);
REQUIRE(std::all_of(iv.begin() + AES256CTRDesc::ivSizeInit, iv.end(), [](auto ch) { return ch == 0; }));
auto desc2 = desc;
std::string original = "test for aes-256-ctr";
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Base64 encoder and decoder", "[crypto][base64]")
{
std::string orig = "The Quick Brown Fox Jumps Over the Lazy Dog";
// no padding
std::string expected = "VGhlIFF1aWNrIEJyb3duIEZveCBKdW1wcyBPdmVyIHRoZSBMYXp5IERvZw";
std::string encoded = encodeBase64(orig);
REQUIRE(encoded == expected);
std::string decoded = decodeBase64(encoded);
REQUIRE(decoded == orig);
}
TEST_CASE("Urlsafe base64 encoder and decoder", "[crypto][base64]")
{
std::string orig = "The Quick Brown Fox Jumps Over the Lazy Dog";
// no padding
std::string expected = "VGhlIFF1aWNrIEJyb3duIEZveCBKdW1wcyBPdmVyIHRoZSBMYXp5IERvZw";
std::string encoded = encodeBase64(orig, Base64Opts::urlSafe);
REQUIRE(encoded == expected);
std::string decoded = decodeBase64(encoded, Base64Opts::urlSafe);
REQUIRE(decoded == orig);
}
TEST_CASE("Base64 encoder and decoder, example from Matrix specs", "[crypto][base64]")
{
std::string orig = "JGLn/yafz74HB2AbPLYJWIVGnKAtqECOBf11yyXac2Y";
std::string decoded = decodeBase64(orig);
std::string encoded = encodeBase64(decoded);
REQUIRE(encoded == orig);
}
TEST_CASE("Urlsafe base64 encoder and decoder, example from Matrix specs", "[crypto][base64]")
{
std::string orig = "JGLn_yafz74HB2AbPLYJWIVGnKAtqECOBf11yyXac2Y";
std::string decoded = decodeBase64(orig, Base64Opts::urlSafe);
std::string encoded = encodeBase64(decoded, Base64Opts::urlSafe);
REQUIRE(encoded == orig);
}
TEST_CASE("SHA256 hashing support", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
hash.processInPlace(message1);
auto res = hash.get();
auto expected = std::string("Y2QCZISah8kDVhKdmeoWXjeqX6vB/qRpBt8afKUNtJI");
REQUIRE(res == expected);
}
TEST_CASE("SHA256 hashing streaming", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
auto message2 = std::string("abcdefghijklmn");
hash.processInPlace(message1);
hash.processInPlace(message2);
auto res = hash.get();
auto hash2 = SHA256Desc{};
hash2.processInPlace(message1 + message2);
auto expected = hash2.get();
REQUIRE(res == expected);
}
TEST_CASE("SHA256Desc should be copyable", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
auto message2 = std::string("abcdefghijklmn");
hash.processInPlace(message1);
auto hash2 = hash;
hash.processInPlace(message2);
hash2.processInPlace(message2);
auto res = hash.get();
auto res2 = hash2.get();
REQUIRE(res == res2);
}
TEST_CASE("SHA256Desc should be self-copyable and -movable", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
hash = hash.process(message1);
auto message2 = std::string("abcdefghijklmn");
hash = std::move(hash).process(message2);
auto hash2 = SHA256Desc{};
hash2.processInPlace(message1 + message2);
auto res = hash.get();
auto res2 = hash2.get();
REQUIRE(res == res2);
}
TEST_CASE("SHA256 should accept any range type", "[crypto][sha256]")
{
std::string msg = "12345678910";
std::vector<char> arr(msg.begin(), msg.end());
auto hash = SHA256Desc{};
auto res1 = hash.process(arr).get();
auto res2 = std::move(hash).process(arr).get();
// after moving, hash is no longer valid, reset it here
hash = SHA256Desc{};
hash.processInPlace(arr);
auto res3 = hash.get();
hash = SHA256Desc{};
auto reference = hash.process(msg).get();
REQUIRE(res1 == res2);
REQUIRE(res1 == res3);
REQUIRE(res1 == reference);
}
TEST_CASE("Crypto::createInboundGroupSession should not allow session key replacement attacks", "[crypto][group-session]")
{
std::string roomId = "!someroom:example.com";
Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
// creating a outbound group session will add it to inbound group sessions
auto initialKey = a.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
roomId
);
// encrypt to get the session id
auto plainText = R"({
"content": {},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json;
auto encryptedContent = a.encryptMegOlm(plainText);
auto encryptedEvent = json{
{"event_id", "$some-event-id1"},
{"origin_server_ts", 1719196953000},
{"content", encryptedContent},
{"type", "m.room.encrypted"},
{"room_id", roomId},
};
// message index is currently at 1
auto currentKey = a.outboundGroupSessionCurrentKey(roomId);
auto sessionId = encryptedContent["session_id"].template get<std::string>();
auto plainText2 = R"({
"content": {"a": "b"},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json;
auto encryptedContent2 = a.encryptMegOlm(plainText2);
Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
Crypto malice(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
auto encryptedEvent2 = json{
{"event_id", "$some-event-id"},
{"origin_server_ts", 1719196953000},
{"content", encryptedContent2},
{"type", "m.room.encrypted"},
{"room_id", roomId},
};
auto keyOfSession = KeyOfGroupSession{roomId, sessionId};
auto created = b.createInboundGroupSession(keyOfSession, currentKey, a.ed25519IdentityKey());
REQUIRE(created);
REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
SECTION("it should reject if the identity key is not the same") {
auto updated = b.createInboundGroupSession(keyOfSession, initialKey, malice.ed25519IdentityKey());
REQUIRE(!updated);
}
SECTION("it should reject if the given session key does not belong to the same session") {
auto anotherSessionKey = a.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
roomId
);
auto updated = b.createInboundGroupSession(keyOfSession, anotherSessionKey, a.ed25519IdentityKey());
REQUIRE(!updated);
}
SECTION("it should prevent replay attack if merging with itself") {
auto updated = b.createInboundGroupSession(keyOfSession, currentKey, a.ed25519IdentityKey());
REQUIRE(updated);
REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
auto replay = encryptedEvent2;
replay["event_id"] = "$some-other-id";
REQUIRE(!b.decrypt(replay).has_value());
}
SECTION("it should prevent replay attack if session is updated") {
auto updated = b.createInboundGroupSession(keyOfSession, initialKey, a.ed25519IdentityKey());
REQUIRE(updated);
REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
REQUIRE(doesDecryptTo(b, encryptedEvent, plainText));
auto replay = encryptedEvent2;
replay["event_id"] = "$some-other-id";
REQUIRE(!b.decrypt(replay).has_value());
}
}
TEST_CASE("Crypto::hasInboundGroupSession", "[crypto][group-session]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
// creating a outbound group session will add it to inbound group sessions
crypto.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
"!someroom:example.com"
);
// encrypt to get the session id
auto encryptedContent = crypto.encryptMegOlm(R"({
"content": {},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json);
auto sessionId = encryptedContent["session_id"].template get<std::string>();
REQUIRE(crypto.hasInboundGroupSession(KeyOfGroupSession{
"!someroom:example.com",
sessionId,
}));
REQUIRE(!crypto.hasInboundGroupSession(KeyOfGroupSession{
"!someroom:example.com",
sessionId + "something something",
}));
}
TEST_CASE("Crypto::decrypt(MegOlmEvent)", "[crypto][group-session]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
"!someroom:example.com"
);
auto plainText = R"({
"content": {"body": "something"},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json;
// encrypt to get the session id
auto encryptedContent = crypto.encryptMegOlm(plainText);
auto encryptedEvent = json{
{"event_id", "$some-event-id"},
{"origin_server_ts", 1719196953000},
{"content", encryptedContent},
{"type", "m.room.encrypted"},
{"room_id", "!someroom:example.com"},
};
REQUIRE(doesDecryptTo(crypto, encryptedEvent, plainText));
}
TEST_CASE("KeyOfGroupSession serialization", "[crypto][group-session]")
{
{
auto k = KeyOfGroupSession{"!someroom:example.com", "some-session-id"};
json j = k;
REQUIRE(j == json{
{"roomId", "!someroom:example.com"},
{"sessionId", "some-session-id"},
});
}
{
json j{
{"roomId", "!someroom:example.com"},
{"senderKey", "some-key"}, // legacy version
{"sessionId", "some-session-id"},
};
auto k = j.template get<KeyOfGroupSession>();
REQUIRE(k == KeyOfGroupSession{"!someroom:example.com", "some-session-id"});
}
}
diff --git a/src/tests/event-emitter-test.cpp b/src/tests/event-emitter-test.cpp
index ccf91b5..0508a93 100644
--- a/src/tests/event-emitter-test.cpp
+++ b/src/tests/event-emitter-test.cpp
@@ -1,83 +1,169 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <chrono>
#include <catch2/catch_all.hpp>
#include <eventemitter/lagerstoreeventemitter.hpp>
#include <lager/event_loop/boost_asio.hpp>
using namespace Kazv;
TEST_CASE("Event emitter should work normally", "[eventemitter]")
{
boost::asio::io_context ioContext;
LagerStoreEventEmitter ee{lager::with_boost_asio_event_loop{ioContext.get_executor()}};
auto watchable = ee.watchable();
int counter = 0;
watchable.after<ReceivingPresenceEvent>(
[&](auto) {
++counter;
});
SECTION("Event handlers should be run properly") {
ee.emit(ReceivingPresenceEvent{Event()});
ee.emit(ReceivingRoomTimelineEvent{Event(), ""});
ioContext.run();
REQUIRE(counter == 1);
}
SECTION("Event handlers should be called when every event fires off") {
for (int i = 0; i < 100; ++i) {
ee.emit(ReceivingPresenceEvent{Event()});
}
ioContext.run();
REQUIRE(counter == 100);
}
SECTION("Handler should be disconnected once watchable is destroyed") {
int counter2 = 0;
auto guard = boost::asio::make_work_guard(ioContext.get_executor());
auto thread = std::thread([&] { ioContext.run(); });
ee.emit(ReceivingRoomTimelineEvent{});
std::this_thread::sleep_for(std::chrono::milliseconds{100});
{
auto watchable2 = ee.watchable();
watchable2.after<ReceivingRoomTimelineEvent>(
[&](auto) {
++counter2;
});
ee.emit(ReceivingRoomTimelineEvent{});
ee.emit(ReceivingPresenceEvent{});
std::this_thread::sleep_for(std::chrono::milliseconds{100});
}
ee.emit(ReceivingRoomTimelineEvent{});
guard.reset();
thread.join();
REQUIRE(counter == 1);
REQUIRE(counter2 == 1);
}
+
+ SECTION("Expired listeners should be removed from internal list") {
+ auto guard = boost::asio::make_work_guard(ioContext.get_executor());
+ auto thread = std::thread([&] { ioContext.run(); });
+
+ std::vector<int> results;
+
+ {
+ auto w = ee.watchable();
+ w.after<ReceivingPresenceEvent>(
+ [&](auto) {
+ results.push_back(1);
+ });
+ }
+ // w is destroyed, listener expired
+
+ // Emit several events to trigger cleanup of expired listener
+ // via sendToListeners -> erase-remove
+ for (int i = 0; i < 5; ++i) {
+ ee.emit(ReceivingPresenceEvent{});
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds{100});
+
+ // Now register a new listener - if erase-remove works, the old
+ // expired listener was removed and no stale references remain
+ int newCounter = 0;
+ {
+ auto w2 = ee.watchable();
+ w2.after<ReceivingPresenceEvent>(
+ [&](auto) {
+ ++newCounter;
+ });
+
+ ee.emit(ReceivingPresenceEvent{});
+ ee.emit(ReceivingPresenceEvent{});
+ std::this_thread::sleep_for(std::chrono::milliseconds{100});
+ }
+
+ guard.reset();
+ thread.join();
+
+ REQUIRE(results.empty());
+ REQUIRE(newCounter == 2);
+ }
+
+ SECTION("Many expired listeners should be cleaned without issue") {
+ auto guard = boost::asio::make_work_guard(ioContext.get_executor());
+ auto thread = std::thread([&] { ioContext.run(); });
+
+ // Create and destroy many watchables to build up expired listeners
+ for (int i = 0; i < 50; ++i) {
+ auto w = ee.watchable();
+ std::atomic<int> unused{0};
+ w.after<ReceivingPresenceEvent>(
+ [&unused](auto) {
+ ++unused;
+ });
+ }
+ // All watchables destroyed, all listeners expired
+
+ // Emit events to trigger cleanup via erase-remove
+ for (int i = 0; i < 10; ++i) {
+ ee.emit(ReceivingPresenceEvent{});
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds{100});
+
+ // Verify emitter still works after cleanup
+ int aliveCounter = 0;
+ {
+ auto w = ee.watchable();
+ w.after<ReceivingPresenceEvent>(
+ [&](auto) {
+ ++aliveCounter;
+ });
+
+ ee.emit(ReceivingPresenceEvent{});
+ std::this_thread::sleep_for(std::chrono::milliseconds{100});
+ }
+
+ guard.reset();
+ thread.join();
+
+ REQUIRE(aliveCounter == 1);
+ }
}
diff --git a/src/tests/kazvjobtest.cpp b/src/tests/kazvjobtest.cpp
index 62f4076..f099c77 100644
--- a/src/tests/kazvjobtest.cpp
+++ b/src/tests/kazvjobtest.cpp
@@ -1,243 +1,304 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <vector>
#include <iostream>
#include <filesystem>
#include <catch2/catch_all.hpp>
#include <cprjobhandler.hpp>
#include "tests.hpp"
#include "kazvtest-respath.hpp"
#include "file-guard.hpp"
using namespace Kazv;
TEST_CASE("setTimeout should behave properly", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
h.setTimeout(
[&v, &h] {
v.push_back(500);
h.stop();
}, 500);
h.setTimeout(
[&v] {
v.push_back(100);
}, 100);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 100 );
REQUIRE( v[1] == 500 );
}
TEST_CASE("setInterval should behave properly", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
h.setInterval(
[&v, &h] {
v.push_back(50);
std::cout << "timer executed" << std::endl;
if (v.size() >= 2) {
h.stop();
}
}, 100);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 50 );
REQUIRE( v[1] == 50 );
}
TEST_CASE("setInterval can be cancelled", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
std::string id{"testTimerId"};
h.setInterval(
- [&v, &h, id] {
+ [&v, &h, id]() {
v.push_back(50);
std::cout << "timer executed" << std::endl;
if (v.size() >= 2) {
h.cancel(id);
}
}, 100, id);
h.setTimeout(
- [&h] {
+ [&h]() {
h.stop();
}, 300);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 50 );
REQUIRE( v[1] == 50 );
}
+TEST_CASE("clearTimer should erase timer from map when timer fires", "[kazvjob]")
+{
+ boost::asio::io_context ioContext;
+ CprJobHandler h(ioContext.get_executor());
+
+ std::vector<int> firedWithIds;
+
+ // Set two timers with the same timerId - each fires once and
+ // is cleared via clearTimer -> erase-remove, then a new timer
+ // with the same ID is set
+ h.setTimeout(
+ [&firedWithIds, &h]() {
+ firedWithIds.push_back(1);
+ // Set a new timer with the same ID after this one fires
+ h.setTimeout(
+ [&firedWithIds, &h]() {
+ firedWithIds.push_back(2);
+ h.stop();
+ }, 50, "shared-id");
+ }, 50, "shared-id");
+
+ ioContext.run();
+
+ REQUIRE(firedWithIds.size() == 2);
+ REQUIRE(firedWithIds[0] == 1);
+ REQUIRE(firedWithIds[1] == 2);
+}
+
+TEST_CASE("Multiple timers with same id should be independently clearable", "[kazvjob]")
+{
+ boost::asio::io_context ioContext;
+ CprJobHandler h(ioContext.get_executor());
+
+ std::vector<int> fired;
+
+ // After each timer fires, clearTimer via erase-remove should
+ // remove only that specific timer, not affect others
+ h.setTimeout(
+ [&fired]() {
+ fired.push_back(1);
+ }, 50, "multi-id");
+
+ h.setTimeout(
+ [&fired]() {
+ fired.push_back(2);
+ }, 100, "multi-id");
+
+ h.setTimeout(
+ [&fired, &h]() {
+ fired.push_back(3);
+ h.stop();
+ }, 150, "multi-id");
+
+ ioContext.run();
+
+ REQUIRE(fired.size() == 3);
+ REQUIRE(fired[0] == 1);
+ REQUIRE(fired[1] == 2);
+ REQUIRE(fired[2] == 3);
+}
+
static BaseJob succJob =
BaseJob(TEST_SERVER_URL, "/.well-known/matrix/client", BaseJob::Get{}, "TestJob")
.withQueue("testjob");
static BaseJob failJob =
BaseJob(TEST_SERVER_URL, "/.well-known/jfasjewfn/awejioaewjgjaad/fawre", BaseJob::Get{}, "AnotherTestJob")
.withQueue("testjob");
static BaseJob failJobWithCancel =
failJob.withQueue("testjob", CancelFutureIfFailed);
TEST_CASE("Job queue should behave properly", "[kazvjob][needs-internet]")
{
std::vector<bool> v;
std::function<void(Response)> callback =
[&v](Response r) {
std::cout << "callback called" << std::endl;
v.push_back(r.success());
};
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
h.submit(succJob, callback); // true
h.submit(failJob, callback); // false
h.submit(failJobWithCancel, callback); // false
h.submit(succJob, callback); // false (because this is cancelled)
h.submit(succJob, [&h](Response) {
h.stop();
}); // nothing added
ioContext.run();
REQUIRE( v.size() == 4 );
REQUIRE( v[0] == true );
REQUIRE( v[1] == false );
REQUIRE( v[2] == false );
REQUIRE( v[3] == false );
}
static const std::string httpbinServer = "http://www.httpbin.org";
static const std::string httpbinEndpoint = "/post";
TEST_CASE("Stream uploads should work properly", "[kazvjob][needs-internet]")
{
auto filename = std::filesystem::path(resPath) / "kazvjob-test-res1";
auto desc = FileDesc(filename.native(), "text/plain");
BaseJob job(httpbinServer,
httpbinEndpoint,
BaseJob::POST,
"TestJob",
std::string{}, // token
BaseJob::ReturnType::Json,
desc);
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
h.submit(job, [&h](Response r) {
REQUIRE(isBodyJson(r.body));
auto body = r.jsonBody();
std::cout << "Ret= " << body.get().dump() << std::endl;
REQUIRE(body.get().at("data").template get<std::string>() == "foobar");
REQUIRE(body.get().at("headers").at("Content-Type").template get<std::string>() == "text/plain");
h.stop();
});
ioContext.run();
}
TEST_CASE("Streaming binary data should be ok", "[kazvjob][needs-internet]")
{
auto filename = std::filesystem::path(resPath) / "kazvjob-test-res2";
auto desc = FileDesc(filename.native(), "application/octet-stream");
BaseJob job(httpbinServer,
httpbinEndpoint,
BaseJob::POST,
"TestJob",
std::string{}, // token
BaseJob::ReturnType::Json,
desc);
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
h.submit(job, [&h](Response r) {
REQUIRE(isBodyJson(r.body));
auto body = r.jsonBody();
std::cout << "Ret= " << body.get().dump() << std::endl;
REQUIRE(body.get().at("data").template get<std::string>() == "\n\r\n\0\t\n"s);
REQUIRE(body.get().at("headers").at("Content-Type").template get<std::string>() == "application/octet-stream");
h.stop();
});
ioContext.run();
}
TEST_CASE("Stream downloads should work properly", "[kazvjob][needs-internet]")
{
auto filename = std::filesystem::path(resPath) / "kazvjob-test-tmp1";
auto desc = FileDesc(filename.native());
auto guard = FileGuard{filename};
BaseJob job(httpbinServer,
httpbinEndpoint,
BaseJob::POST,
"TestJob",
std::string{}, // token
BaseJob::ReturnType::File,
"foobar"s,
{}, // query
Header(Header::value_type{{"Content-Type", "text/plain"}}), // header
desc);
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
h.submit(job, [&h, filename](Response r) {
REQUIRE(std::holds_alternative<FileDesc>(r.body));
auto body = std::get<FileDesc>(r.body);
REQUIRE(body.name().has_value());
REQUIRE(body.name().value() == filename);
auto fh = DumbFileInterface{};
auto stream = body.provider(fh).getStream();
stream.read(1024, [](auto retCode, auto data) {
REQUIRE(retCode == FileOpRetCode::Success);
auto j = json::parse(data.begin(), data.end());
std::cout << j.dump() << std::endl;
REQUIRE(j.at("data") == "foobar");
REQUIRE(j.at("headers").at("Content-Type") == "text/plain");
});
h.stop();
});
ioContext.run();
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 29, 11:30 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798824
Default Alt Text
(177 KB)

Event Timeline