Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85825193
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
177 KB
Referenced Files
None
Subscribers
None
View Options
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 ¤tSession = 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
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 29, 11:30 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798824
Default Alt Text
(177 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline