Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85710119
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
60 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt
index de05546..cd3dd3f 100644
--- a/src/crypto/CMakeLists.txt
+++ b/src/crypto/CMakeLists.txt
@@ -1,31 +1,32 @@
set(kazvcrypto_SRCS
crypto.cpp
session.cpp
inbound-group-session.cpp
outbound-group-session.cpp
aes-256-ctr.cpp
base64.cpp
sha256.cpp
key-export.cpp
verification-process.cpp
verification-utils.cpp
+ verification-tracker.cpp
sas-desc.cpp
)
add_library(kazvcrypto ${kazvcrypto_SRCS})
add_library(libkazv::kazvcrypto ALIAS kazvcrypto)
set_target_properties(kazvcrypto PROPERTIES VERSION ${libkazv_VERSION_STRING} SOVERSION ${libkazv_SOVERSION})
target_link_libraries(kazvcrypto PUBLIC kazvbase)
target_link_libraries(kazvcrypto PRIVATE vodozemac::vodozemac ${CRYPTOPP_TARGET_NAME})
target_include_directories(kazvcrypto PRIVATE .)
target_include_directories(kazvcrypto
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include/kazv/crypto>
)
install(TARGETS kazvcrypto EXPORT libkazvTargets LIBRARY)
diff --git a/src/crypto/verification-process.cpp b/src/crypto/verification-process.cpp
index 3ada28a..12f7eba 100644
--- a/src/crypto/verification-process.cpp
+++ b/src/crypto/verification-process.cpp
@@ -1,606 +1,641 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include "verification-process.hpp"
#include "verification-utils.hpp"
#include "sha256.hpp"
#include <validator.hpp>
#include <cursorutil.hpp>
#include <debug.hpp>
#include <lager/util.hpp>
#include <zug/transducer/filter.hpp>
namespace Kazv
{
namespace VCC = VerificationCancelCodes;
namespace VPS = VerificationProcessStates;
namespace VU = VerificationUtils;
using namespace VerificationEventTypes;
inline const immer::map<std::string, std::string> codeToMessage = {
{VCC::unexpectedMessage, "Unexpected message"},
{VCC::userCancel, "User cancelled"},
{VCC::keyMismatch, "Key mismatch"},
{VCC::timeout, "Timeout"},
{VCC::unknownMethod, "Unknown method"},
{VCC::invalidMessage, "Invalid message"},
{VCC::acceptedElsewhere, "Accepted elsewhere"},
{VCC::mismatchedCommitment, "Mismatched commitment hash"},
{VCC::mismatchedSas, "Mismatched SAS"},
};
static VPS::Cancelled makeCancelState(std::string code)
{
return {
code,
codeToMessage.count(code) ? codeToMessage[code] : code,
};
}
template<class Cont1, class Cont2, class Conv>
static auto firstIn(Cont1 &&wanted, Cont2 &&supported, Conv &&conv) -> std::optional<std::decay_t<decltype(supported[0])>>
{
auto it = std::find_if(supported.begin(), supported.end(), [&conv, &wanted](const auto &v) {
return std::find(wanted.begin(), wanted.end(), conv(v)) != wanted.end();
});
if (it == supported.end()) {
return std::nullopt;
} else {
return *it;
}
}
static json strToJsonValue(const std::string &s)
{
return json(s);
}
template<class Cont1, class Cont2>
static auto firstIn(Cont1 &&wanted, Cont2 &&supported)
{
return firstIn(std::forward<Cont1>(wanted), std::forward<Cont2>(supported), strToJsonValue);
}
template<class Cont, class T>
static auto contHas(Cont &&cont, T &&value)
{
return std::find(cont.begin(), cont.end(), std::forward<T>(value)) != cont.end();
}
VerificationProcess::VerificationProcess(std::string ourUserId, std::string ourDeviceId, std::string theirUserId, std::string theirDeviceId, std::string ourDeviceKey)
: ourUserId(ourUserId)
, ourDeviceId(ourDeviceId)
, theirUserId(theirUserId)
, theirDeviceId(theirDeviceId)
, ourDeviceKey(ourDeviceKey)
{}
void VerificationProcess::setTheirDeviceKey(std::string key)
{
theirDeviceKey = key;
}
auto VerificationProcess::ValidateResult::ok() -> ValidateResult
{
return {/* valid = */ true, std::monostate{}, {}};
}
auto VerificationProcess::ValidateResult::error(VerificationProcessState state, EventList msgs) -> ValidateResult
{
return {/* valid = */ false, std::move(state), std::move(msgs)};
}
auto VerificationProcess::validateEvent(Event incomingEvent) const -> ValidateResult
{
auto cancel = [this](std::string code) {
return ValidateResult::error(
makeCancelState(code),
{makeCancelEvent(code)}
);
};
auto unexpected = [cancel]() {
return cancel(VCC::unexpectedMessage);
};
auto type = VU::typeOf(incomingEvent);
auto content = incomingEvent.content().get();
auto r = json::object();
if (type == tRequest) {
// double request
if (!std::holds_alternative<std::monostate>(state)) {
return unexpected();
}
if (!firstIn(content.at("methods"), supportedMethods).has_value()) {
return cancel(VCC::unknownMethod);
}
return ValidateResult::ok();
} else if (type == tReady) {
// [request, ready]
if (!std::holds_alternative<VPS::WeRequested>(state)) {
return unexpected();
}
if (!firstIn(content.at("methods"), supportedMethods).has_value()) {
return cancel(VCC::unknownMethod);
}
return ValidateResult::ok();
} else if (type == tStart) {
// request -> ready -> start
// must have ready (from either side)
// only allow double start if we sent a start as well
if (!(std::holds_alternative<VPS::WeReady>(state)
|| std::holds_alternative<VPS::WeStartedSas>(state)
)) {
return unexpected();
}
auto method = content.at("method").template get<std::string>();
if (std::find(supportedMethods.begin(), supportedMethods.end(), method) == supportedMethods.end()) {
return cancel(VCC::unknownMethod);
} else if (!(firstIn(content.at("hashes"), supportedHashes).has_value()
&& firstIn(content.at("key_agreement_protocols"), supportedKeyAgreementProtocols).has_value()
&& firstIn(content.at("message_authentication_codes"), supportedMessageAuthenticationCodes)
&& firstIn(content.at("short_authentication_string"), defaultShortAuthenticationString)
)) {
return cancel(VCC::unknownMethod);
}
return ValidateResult::ok();
} else if (type == tAccept) {
if (!std::holds_alternative<VPS::WeStartedSas>(state)) {
return unexpected();
}
if (!contHas(supportedHashes, content.at("hash").template get<std::string>())
|| !contHas(supportedKeyAgreementProtocols, content.at("key_agreement_protocol").template get<std::string>())
|| !contHas(supportedMessageAuthenticationCodes, content.at("message_authentication_code").template get<std::string>())
|| !firstIn(content.at("short_authentication_string"), defaultShortAuthenticationString).has_value()
) {
return cancel(VCC::unknownMethod);
}
} else if (type == tKey) {
if (!(std::holds_alternative<VPS::WeAcceptedSas>(state)
|| std::holds_alternative<VPS::TheyAcceptedSas>(state))) {
return unexpected();
}
} else if (type == tMac) {
if (!std::holds_alternative<VPS::ReceivedSasKey>(state)) {
return unexpected();
}
} else if (type == tDone) {
if (!std::holds_alternative<VPS::VerifiedThem>(state)) {
return unexpected();
}
}
return ValidateResult::ok();
}
EventList VerificationProcess::processIncoming(Event e)
{
events = std::move(events).push_back({Them, e});
auto type = VU::typeOf(e);
kzo.crypto.dbg() << "VerificationProcess::processIncoming: Handling event:" << e.raw().get().dump() << std::endl;
auto validateRes = validateEvent(e);
if (!validateRes.valid) {
kzo.crypto.dbg() << "VerificationProcess::processIncoming: failed validation" << std::endl;
state = validateRes.state;
addOutgoingEvents(validateRes.msgs);
return validateRes.msgs;
}
try {
if (type == tRequest) {
state = VPS::TheyRequested{};
return {};
} else if (type == tReady) {
state = VPS::TheyReady{};
auto methods = e.content().get().at("methods");
auto selected = firstIn(methods, VerificationProcess::supportedMethods, strToJsonValue).value();
state = VPS::WeStartedSas{};
EventList r{makeStartEvent(selected)};
addOutgoingEvents(r);
return r;
} else if (type == tStart) {
auto startingParty = getStartingParty();
if (startingParty == Us) {
// double start, but we take over: do nothing
return {};
}
auto method = e.content().get().at("method").template get<std::string>();
if (method == "m.sas.v1") {
state = VPS::TheyStartedSas{};
EventList r{makeSasAcceptEvent()};
addOutgoingEvents(r);
return r;
}
// unreachable
} else if (type == tAccept) {
auto method = e.content().get().at("method").template get<std::string>();
if (method == "m.sas.v1") {
state = VPS::TheyAcceptedSas{};
EventList r{makeSasKeyEvent()};
addOutgoingEvents(r);
return r;
}
// unreachable
} else if (type == tKey) {
state = VPS::ReceivedSasKey{};
auto starter = getStartingParty();
if (starter == Them) {
// us <-start-- them
// -accept->
// <- key --
auto keyEvent = makeSasKeyEvent();
codes = makeDisplayCodes();
if (codes.decimalCode.empty()) {
state = makeCancelState(VCC::invalidMessage);
return {makeCancelEvent(VCC::invalidMessage)};
}
EventList r{keyEvent};
addOutgoingEvents(r);
return r;
} else {
// us --start-> them
// <-accept-
// -- key ->
// <- key --
// From this point, ask the user to verify the codes match.
// Only after the user confirmed the match
// do we begin to send mac events.
if (verifySasCommitment()) {
codes = makeDisplayCodes();
if (codes.decimalCode.empty()) {
state = makeCancelState(VCC::invalidMessage);
EventList r{makeCancelEvent(VCC::invalidMessage)};
addOutgoingEvents(r);
return r;
}
return {};
} else {
state = makeCancelState(VCC::mismatchedCommitment);
EventList r{makeCancelEvent(VCC::mismatchedCommitment)};
addOutgoingEvents(r);
return r;
}
}
} else if (type == "m.key.verification.mac") {
auto [nextState, msgs] = verifyKeyMac(e);
state = std::move(nextState);
addOutgoingEvents(msgs);
return msgs;
} else if (type == "m.key.verification.done") {
state = VPS::VerifiedBoth{};
return {};
} else if (type == "m.key.verification.cancel") {
auto content = e.content().get();
state = VPS::Cancelled{
content.at("code").template get<std::string>(),
content.at("reason").template get<std::string>()
};
return {};
}
} catch (const std::exception &e) {
kzo.crypto.warn() << "Error handling key event:" << e.what() << std::endl;
kzo.crypto.warn() << "It is likely a bug in libkazv." << std::endl;
}
return {};
}
void VerificationProcess::addOutgoing(Event e)
{
events = std::move(events).push_back({Us, std::move(e)});
}
void VerificationProcess::addOutgoingEvents(EventList el)
{
for (auto e : el) {
addOutgoing(e);
}
}
EventList VerificationProcess::userReady()
{
if (!std::holds_alternative<VPS::TheyRequested>(state)) {
return {};
}
state = VPS::WeReady{};
EventList r{makeReadyEvent()};
addOutgoingEvents(r);
return r;
}
EventList VerificationProcess::userCancel()
{
if (std::holds_alternative<VPS::Cancelled>(state)) {
return {};
}
state = makeCancelState(VCC::userCancel);
EventList r{makeCancelEvent(VCC::userCancel)};
addOutgoingEvents(r);
return r;
}
EventList VerificationProcess::userConfirmMatch()
{
if ((!std::holds_alternative<VPS::ReceivedSasMac>(state)
&& !std::holds_alternative<VPS::ReceivedSasKey>(state)
) || confirmedMatch) {
return {};
}
confirmedMatch = true;
EventList msgs;
state = lager::match(state)(
[&msgs, this](VPS::ReceivedSasMac) -> VerificationProcessState {
msgs = std::move(msgs).push_back(makeEvent(tDone, json::object()));
return VPS::VerifiedThem{};
},
[](auto &&v) -> VerificationProcessState {
return VerificationProcessState(std::forward<decltype(v)>(v));
}
);
auto r = EventList{
makeSasMacEvent(),
} + msgs;
addOutgoingEvents(r);
return r;
}
EventList VerificationProcess::userDenyMatch()
{
if ((!std::holds_alternative<VPS::ReceivedSasMac>(state)
&& !std::holds_alternative<VPS::ReceivedSasKey>(state)
) || confirmedMatch) {
return {};
}
confirmedMatch = false;
state = makeCancelState(VCC::mismatchedSas);
EventList r{makeCancelEvent(VCC::mismatchedSas)};
addOutgoingEvents(r);
return r;
}
+ EventList VerificationProcess::makeRequest(Timestamp now)
+ {
+ auto txnId = "v" + std::to_string(now);
+ auto requestEvent = Event{json::object({
+ {"content", {
+ {"from_device", ourDeviceId},
+ {"transaction_id", txnId},
+ {"timestamp", now},
+ {"methods", VerificationProcess::supportedMethods},
+ }},
+ {"type", tRequest},
+ })};
+ state = VPS::WeRequested{};
+ EventList r{std::move(requestEvent)};
+ addOutgoingEvents(r);
+ return r;
+ }
+
std::string VerificationProcess::txnId() const
{
- return VU::txnId(events[0].second);
+ if (!events.empty()) {
+ return VU::txnId(events.at(0).second);
+ } else {
+ return "";
+ }
+ }
+
+ Timestamp VerificationProcess::requestTimestamp() const
+ {
+ if (events.empty()) {
+ return {};
+ }
+ Event requestEvent = events.at(0).second;
+ if (VU::isToDevice(requestEvent)) {
+ return requestEvent.content().get().at("timestamp").template get<Timestamp>();
+ } else {
+ return requestEvent.originServerTs();
+ }
}
Event VerificationProcess::makeEvent(std::string type, json content) const
{
content["transaction_id"] = txnId();
return Event{json{
{"type", type},
{"content", std::move(content)},
}};
}
Event VerificationProcess::makeCancelEvent(std::string code) const
{
return makeEvent("m.key.verification.cancel", json::object({
{"code", code},
{"reason", codeToMessage.count(code) ? codeToMessage[code] : code},
}));
}
Event VerificationProcess::makeReadyEvent() const
{
return makeEvent("m.key.verification.ready", json{
{"from_device", ourDeviceId},
{"methods", supportedMethods},
});
}
Event VerificationProcess::makeStartEvent(std::string method) const
{
if (method == "m.sas.v1") {
return makeEvent("m.key.verification.start", json{
{"from_device", ourDeviceId},
{"method", std::move(method)},
{"hashes", supportedHashes},
{"key_agreement_protocols", supportedKeyAgreementProtocols},
{"message_authentication_codes", supportedMessageAuthenticationCodes},
{"short_authentication_string", defaultShortAuthenticationString},
});
} else {
return Event();
}
}
Event VerificationProcess::makeSasAcceptEvent()
{
auto [party, lastEvent] = getStartEvent();
auto lastContent = lastEvent.content().get();
auto hash = firstIn(lastContent.at("hashes"), supportedHashes, strToJsonValue);
auto kap = firstIn(lastContent.at("key_agreement_protocols"), supportedKeyAgreementProtocols, strToJsonValue);
auto mac = firstIn(lastContent.at("message_authentication_codes"), supportedMessageAuthenticationCodes, strToJsonValue);
auto content = addCommitmentToAcceptContent(json::object({
{"from_device", ourDeviceId},
{"method", "m.sas.v1"},
{"hash", hash.value()},
{"key_agreement_protocol", kap.value()},
{"message_authentication_code", mac.value()},
{"short_authentication_string", defaultShortAuthenticationString},
}), lastEvent);
state = VPS::WeAcceptedSas{};
return makeEvent("m.key.verification.accept", content);
}
Event VerificationProcess::makeSasKeyEvent()
{
auto [lastSender, lastEvent] = events.back();
auto type = VU::typeOf(lastEvent);
if (type == "m.key.verification.accept" || type == "m.key.verification.key") {
if (!sas.valid()) {
sas.emplace(RandomTag{}, {});
}
auto publicKey = sas.publicKey();
auto content = json{
{"key", publicKey},
};
return makeEvent("m.key.verification.key", content);
} else {
return Event();
}
}
Event VerificationProcess::makeSasMacEvent()
{
auto makeMac = [this](auto &&macFunc, auto k, auto &&...as) {
return std::invoke(macFunc,
sas, k,
ourUserId, ourDeviceId, theirUserId, theirDeviceId,
txnId(),
std::forward<decltype(as)>(as)...
);
};
if (confirmedMatch) {
auto deviceKeyId = "ed25519:" + ourDeviceId;
auto keyList = std::set<std::string>{deviceKeyId};
auto content = json{
{"mac", {
{deviceKeyId, makeMac(&SasDesc::getKeyMacHkdfHMacSha256V2, ourDeviceKey, deviceKeyId)},
}},
{"keys", makeMac(&SasDesc::getKeyListMacHkdfHMacSha256V2, keyList)},
};
return makeEvent("m.key.verification.mac", content);
} else {
return Event();
}
}
auto VerificationProcess::getStartEvent() const -> std::pair<Party, Event>
{
auto startEvents = intoImmer(immer::flex_vector<std::pair<Party, Event>>{}, zug::filter([](const auto &p) {
return VU::typeOf(p.second) == "m.key.verification.start";
}), events);
if (startEvents.empty()) {
return std::make_pair(Us, Event());
}
auto minPair = std::min_element(startEvents.begin(), startEvents.end(), [this](const auto &a, const auto &b) {
auto aKey = a.first == Us ? std::make_pair(ourUserId, ourDeviceId) : std::make_pair(theirUserId, theirDeviceId);
auto bKey = b.first == Us ? std::make_pair(ourUserId, ourDeviceId) : std::make_pair(theirUserId, theirDeviceId);
return aKey < bKey;
});
return *minPair;
}
auto VerificationProcess::getStartingParty() const -> Party
{
return getStartEvent().first;
}
json VerificationProcess::addCommitmentToAcceptContent(json content, Event startEvent)
{
SHA256Desc hash;
sas.emplace(RandomTag{}, {});
hash.processInPlace(sas.publicKey());
hash.processInPlace(startEvent.content().get().dump());
content["commitment"] = hash.get();
return content;
}
bool VerificationProcess::verifySasCommitment()
{
auto theirAcceptEventIt = std::find_if(events.begin(), events.end(), [](const auto &p) {
return p.first == Them && VU::typeOf(p.second) == "m.key.verification.accept";
});
if (theirAcceptEventIt == events.end()) {
kzo.crypto.warn() << "No accept event found" << std::endl;
return false;
}
auto acceptEvent = theirAcceptEventIt->second;
SHA256Desc hash;
hash.processInPlace(getTheirKey());
hash.processInPlace(getStartEvent().second.content().get().dump());
auto commitment = hash.get();
return commitment == acceptEvent.content().get().at("commitment").template get<std::string>();
}
SasDisplayCodes VerificationProcess::makeDisplayCodes()
{
auto starter = getStartingParty();
auto starterUserId = starter == Us ? ourUserId : theirUserId;
auto starterDeviceId = starter == Us ? ourDeviceId : theirDeviceId;
auto starterKey = starter == Us ? sas.publicKey() : getTheirKey();
auto accepterUserId = starter == Us ? theirUserId : ourUserId;
auto accepterDeviceId = starter == Us ? theirDeviceId : ourDeviceId;
auto accepterKey = starter == Us ? getTheirKey() : sas.publicKey();
sas.setTheirKey(getTheirKey());
auto [emoji, decimal] = sas.getDisplayCodesCurve25519HkdfSha256(
starterUserId,
starterDeviceId,
starterKey,
accepterUserId,
accepterDeviceId,
accepterKey,
txnId()
);
return {emoji, decimal};
}
std::string VerificationProcess::getTheirKey() const
{
auto it = std::find_if(events.begin(), events.end(), [](const auto &p) {
return p.first == Them && VU::typeOf(p.second) == "m.key.verification.key";
});
if (it == events.end()) {
return std::string();
}
return it->second.content().get().at("key").template get<std::string>();
}
std::pair<VerificationProcessState, EventList> VerificationProcess::verifyKeyMac(Event macEvent)
{
auto checkMac = [this](auto &&macFunc, auto mac, auto k, auto &&...as) {
return std::invoke(macFunc,
sas, mac, k,
ourUserId, ourDeviceId, theirUserId, theirDeviceId,
txnId(),
std::forward<decltype(as)>(as)...
);
};
auto macFailed = [this]() {
return std::make_pair(
makeCancelState(VCC::keyMismatch),
EventList{makeCancelEvent(VCC::keyMismatch)}
);
};
auto content = macEvent.content().get();
std::set<std::string> keyList;
for (auto [k, v]: content.at("mac").items()) {
keyList.insert(k);
}
if (!checkMac(
&SasDesc::verifyKeyListMacHkdfHMacSha256V2,
content.at("keys").template get<std::string>(),
keyList
)) {
return macFailed();
}
auto keyId = "ed25519:" + theirDeviceId;
if (!checkMac(
&SasDesc::verifyKeyMacHkdfHMacSha256V2,
content.at("mac").at(keyId).template get<std::string>(),
theirDeviceKey,
keyId
)) {
return macFailed();
}
if (confirmedMatch) {
EventList msgs = {
makeEvent(tDone, json::object())
};
return {
// If the user has confirmed the SAS matches,
// then we verified their mac to be valid.
VPS::VerifiedThem{},
msgs
};
} else {
// if user has not confirmed match, do not send anything.
return {
VPS::ReceivedSasMac{},
{},
};
}
}
}
diff --git a/src/crypto/verification-process.hpp b/src/crypto/verification-process.hpp
index 91cd20a..0412900 100644
--- a/src/crypto/verification-process.hpp
+++ b/src/crypto/verification-process.hpp
@@ -1,261 +1,272 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include "sas-desc.hpp"
#include <crypto-util.hpp>
#include <types.hpp>
#include <immer/array.hpp>
namespace Kazv
{
namespace VerificationCancelCodes
{
inline const std::string unexpectedMessage = "m.unexpected_message";
inline const std::string userCancel = "m.user";
inline const std::string timeout = "m.timeout";
inline const std::string unknownMethod = "m.unknown_method";
inline const std::string keyMismatch = "m.key_mismatch";
inline const std::string invalidMessage = "m.invalid_message";
inline const std::string acceptedElsewhere = "m.accepted";
inline const std::string mismatchedCommitment = "m.mismatched_commitment";
inline const std::string mismatchedSas = "m.mismatched_sas";
}
struct SasDisplayCodes
{
/// The indices of emojis to display
/// They can be converted to emojis using the table at
/// https://spec.matrix.org/v1.17/client-server-api/#sas-method-emoji
immer::array<int> emojiIndices;
/// The numbers to display
immer::array<int> decimalCode;
friend bool operator==(const SasDisplayCodes &a, const SasDisplayCodes &b) = default;
};
namespace VerificationProcessStates
{
struct WeRequested {};
struct TheyRequested {};
struct WeReady {};
struct TheyReady {};
struct WeStartedSas {};
struct TheyStartedSas {};
struct WeAcceptedSas {};
struct TheyAcceptedSas {};
// from ReceivedSasKey to VerifiedBoth, we need to also check confirmedMatch
struct ReceivedSasKey {};
struct ReceivedSasMac {};
struct VerifiedThem {};
struct VerifiedBoth {};
struct Cancelled
{
std::string reasonCode;
std::string reasonString;
};
}
using VerificationProcessState = std::variant<
std::monostate,
VerificationProcessStates::WeRequested,
VerificationProcessStates::TheyRequested,
VerificationProcessStates::WeReady,
VerificationProcessStates::TheyReady,
VerificationProcessStates::WeStartedSas,
VerificationProcessStates::TheyStartedSas,
VerificationProcessStates::WeAcceptedSas,
VerificationProcessStates::TheyAcceptedSas,
VerificationProcessStates::ReceivedSasKey,
VerificationProcessStates::ReceivedSasMac,
VerificationProcessStates::VerifiedThem,
VerificationProcessStates::VerifiedBoth,
VerificationProcessStates::Cancelled
>;
struct VerificationProcess
{
enum Party {
Us,
Them,
};
+ struct RequestTag {};
inline static const immer::flex_vector<std::string> supportedMethods = {"m.sas.v1"};
inline static const immer::flex_vector<std::string> supportedHashes = {"sha256"};
inline static const immer::flex_vector<std::string> supportedKeyAgreementProtocols = {"curve25519-hkdf-sha256"};
inline static const immer::flex_vector<std::string> supportedMessageAuthenticationCodes = {"hkdf-hmac-sha256.v2"};
inline static const immer::flex_vector<std::string> defaultShortAuthenticationString = {"emoji", "decimal"};
VerificationProcess(std::string ourUserId, std::string ourDeviceId, std::string theirUserId, std::string theirDeviceId, std::string ourDeviceKey);
std::string ourUserId;
std::string ourDeviceId;
std::string theirUserId;
std::string theirDeviceId;
/// A list of events that have been transmitted between the parties.
immer::flex_vector<std::pair<Party, Event>> events;
bool confirmedMatch{false};
std::string ourDeviceKey;
std::string theirDeviceKey;
SasDesc sas;
VerificationProcessState state;
SasDisplayCodes codes;
/// @return Whether the process is finished.
bool finished() const;
// Modification functions
/**
* Process an incoming event.
*/
[[nodiscard]] EventList processIncoming(Event e);
/**
* Add an outgoing event to the process.
*/
void addOutgoing(Event e);
/**
* Set the device key of the other party.
*/
void setTheirDeviceKey(std::string key);
/**
* Signal that the user is ready for an incoming verification.
*/
[[nodiscard]] EventList userReady();
/**
* Signal that the user wants to cancel the verification.
*/
[[nodiscard]] EventList userCancel();
/**
* Signal that the user has confirmed that the codes match.
*/
[[nodiscard]] EventList userConfirmMatch();
/**
* Signal that the user has noticed that the codes do not match.
*/
[[nodiscard]] EventList userDenyMatch();
+ /**
+ * Make an outgoing verification request to the other party.
+ */
+ [[nodiscard]] EventList makeRequest(Timestamp now);
+
/**
* Get the transaction id for this process.
*/
[[nodiscard]] std::string txnId() const;
+ /**
+ * Get the request timestamp for this process.
+ */
+ [[nodiscard]] Timestamp requestTimestamp() const;
+
private:
struct ValidateResult
{
/// Whether the event can be processed normally
bool valid;
/// State to set to if the event cannot be processed
VerificationProcessState state;
/// Messages to send if the event cannot be processed
EventList msgs;
static ValidateResult ok();
static ValidateResult error(VerificationProcessState state, EventList msgs);
};
/**
* Validate an incoming event.
*
* Assumes the event is already pushed to the back of `events`.
*/
[[nodiscard]] ValidateResult validateEvent(Event incomingEvent) const;
/**
* Process an incoming event.
*/
[[nodiscard]] EventList toNextState(Event e);
/**
* Make an outgoing event for this transaction.
*/
[[nodiscard]] Event makeEvent(std::string type, json content) const;
/**
* Make an outgoing cancel event for this transaction.
*/
[[nodiscard]] Event makeCancelEvent(std::string code) const;
/**
* Make a m.key.verification.ready event for this verification process.
*
* It is the caller's responsibility to use AddOutgoingVerificationEventAction
* to add the event after it has been sent.
*/
[[nodiscard]] Event makeReadyEvent() const;
/**
* Make a m.key.verification.start event for this verification process.
*
* It is the caller's responsibility to use AddOutgoingVerificationEventAction
* to add the event after it has been sent.
*
* @param method The method to use for verification.
*/
[[nodiscard]] Event makeStartEvent(std::string method) const;
/**
* Make a m.key.verification.accept event with method equal to `m.sas.v1`
* for this verification process.
*
* This function assumes that the last event is a m.key.verification.start
* event.
*/
[[nodiscard]] Event makeSasAcceptEvent();
/**
* Make a m.key.verification.key event for sas.
*
* This function assumes that the last event is a m.key.verification.accept
* event if we started the process, or a m.key.verification.key event
* if they started the process.
*/
[[nodiscard]] Event makeSasKeyEvent();
/**
* Make a m.key.verification.mac event for sas.
*
* This function assumes that the last event is a m.key.verification.key
* event if we started the process, or a m.key.verification.mac event
* if they started the process.
*/
[[nodiscard]] Event makeSasMacEvent();
/**
* Get the start event to use for this process.
*
* If two start events are present, the one to be used is resolved
* through the algorithm as per the spec.
*/
[[nodiscard]] std::pair<Party, Event> getStartEvent() const;
/// Get the party whose start event is used.
[[nodiscard]] Party getStartingParty() const;
/// Add the commitment to the content of the accept event.
[[nodiscard]] json addCommitmentToAcceptContent(json content, Event startEvent);
/// Verify the commitment in the accept event.
[[nodiscard]] bool verifySasCommitment();
/// Get the codes to display to the user.
/// This function assumes that the SasDesc already has keys from both parties.
[[nodiscard]] SasDisplayCodes makeDisplayCodes();
/// Get the key from the other party from a m.key.verification.key event.
[[nodiscard]] std::string getTheirKey() const;
/// Verify the key mac and return the corresponding done/cancel event.
[[nodiscard]] std::pair<VerificationProcessState, EventList> verifyKeyMac(Event macEvent);
/// Add outgoing events to `events`.
void addOutgoingEvents(EventList el);
};
}
diff --git a/src/crypto/verification-tracker.cpp b/src/crypto/verification-tracker.cpp
new file mode 100644
index 0000000..a1fdfd7
--- /dev/null
+++ b/src/crypto/verification-tracker.cpp
@@ -0,0 +1,300 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "verification-tracker.hpp"
+#include <immer-utils.hpp>
+#include <cursorutil.hpp>
+#include <zug/into_vector.hpp>
+#include <zug/transducer/map.hpp>
+
+namespace Kazv
+{
+ namespace VU = VerificationUtils;
+ using namespace VerificationEventTypes;
+
+ static VerificationTracker::PendingEvents sendAllToDevice(
+ std::string userId,
+ std::string deviceId,
+ EventList events
+ )
+ {
+ return intoImmer(
+ VerificationTracker::PendingEvents{},
+ zug::map([userId, deviceId](auto e) {
+ return VerificationTracker::PendingEventDesc{
+ std::move(e), userId, deviceId
+ };
+ }),
+ std::move(events)
+ );
+ }
+
+ VerificationTracker::VerificationTracker(VerificationUtils::DeviceIdentity identity)
+ : identity(identity)
+ , processes()
+ , model()
+ {}
+
+ auto VerificationTracker::requestOutgoingToDevice(VU::DeviceIdentity theirIdentity, Timestamp now) -> PendingEvents
+ {
+ auto userId = theirIdentity.userId;
+ auto deviceId = theirIdentity.deviceId;
+ if (!processes.contains(userId)) {
+ processes[userId] = std::unordered_map<std::string, VerificationProcess>();
+ }
+ auto &u = processes[userId];
+ u.insert_or_assign(deviceId, VerificationProcess(
+ identity.userId,
+ identity.deviceId,
+ userId,
+ deviceId,
+ identity.deviceKey
+ ));
+ auto &p = u.at(deviceId);
+ p.setTheirDeviceKey(theirIdentity.deviceKey);
+
+ auto r = p.makeRequest(now);
+ updateModel();
+ return sendAllToDevice(userId, deviceId, r);
+ }
+
+ static int dismissDelayMs = 1000 * 60 * 10; // 10 mins
+
+ template<class Func>
+ void withProcess(
+ VerificationTracker &t,
+ std::string userId,
+ std::string deviceId,
+ Func &&func
+ )
+ {
+ if (!t.processes.contains(userId)) {
+ return;
+ }
+ auto &u = t.processes.at(userId);
+ if (!u.contains(deviceId)) {
+ return;
+ }
+ auto &p = u.at(deviceId);
+ std::forward<Func>(func)(p);
+ }
+
+ auto VerificationTracker::processIncoming(Timestamp now, EventList toDevice) -> PendingEvents
+ {
+ if (toDevice.empty()) {
+ return {};
+ }
+ PendingEvents res;
+ for (auto e : toDevice) {
+ res = std::move(res) + processOne(e, now);
+ }
+ updateModel();
+ return res;
+ }
+
+ auto VerificationTracker::processOne(const Event &e, Timestamp now) -> PendingEvents
+ {
+ auto valid = VU::validateFormat(e);
+ if (!valid) {
+ return {};
+ }
+
+ auto txnId = VU::txnId(e);
+ auto type = VU::typeOf(e);
+ auto sender = e.sender();
+ auto isToDevice = VU::isToDevice(e);
+ if (type == tRequest) {
+ auto content = e.content().get();
+ auto timestamp = isToDevice ? content.at("timestamp").template get<Timestamp>() : e.originServerTs();
+ if (now > timestamp + dismissDelayMs) {
+ // timeout dismiss
+ return {};
+ }
+ if (!processes.contains(sender)) {
+ processes[sender] = std::unordered_map<std::string, VerificationProcess>();
+ }
+ auto deviceId = e.content().get().at("from_device").template get<std::string>();
+ processes.at(sender).insert_or_assign(deviceId, VerificationProcess(
+ identity.userId,
+ identity.deviceId,
+ sender,
+ deviceId,
+ identity.deviceKey
+ ));
+ auto &p = processes.at(sender).at(deviceId);
+ return sendAllToDevice(sender, deviceId, p.processIncoming(e));
+ } else {
+ if (!processes.contains(sender)) {
+ return {};
+ }
+ auto &u = processes.at(sender);
+ auto it = std::find_if(u.begin(), u.end(), [txnId](const auto &p) {
+ return p.second.txnId() == txnId;
+ });
+ if (it == u.end()) {
+ return {};
+ }
+ auto deviceId = it->first;
+ auto &p = it->second;
+ if (isExpired(p, now)) {
+ return {};
+ }
+ return sendAllToDevice(sender, deviceId, p.processIncoming(e));
+ }
+ }
+
+ static VerificationTrackerModel::ProcessState getState(const VerificationProcess &p)
+ {
+ using M = VerificationTrackerModel;
+ using S = M::ProcessState;
+ using namespace VerificationProcessStates;
+ return lager::match(p.state)(
+ [](WeRequested) -> S { return M::ProcessWaiting{}; },
+ [](TheyRequested) -> S { return M::ProcessTheyRequested{}; },
+ [&p](ReceivedSasKey) -> S {
+ if (p.confirmedMatch) {
+ return M::ProcessWaiting{};
+ }
+ return M::ProcessCodeDisplayed{p.codes.emojiIndices, p.codes.decimalCode};
+ },
+ [&p](ReceivedSasMac) -> S {
+ if (p.confirmedMatch) {
+ // unreachable
+ return M::ProcessWaiting{};
+ }
+ return M::ProcessCodeDisplayed{p.codes.emojiIndices, p.codes.decimalCode};
+ },
+ [](VerifiedThem) -> S { return M::ProcessVerifiedThem{}; },
+ [](VerifiedBoth) -> S { return M::ProcessDone{}; },
+ [](Cancelled c) -> S {
+ return M::ProcessCancelled(std::move(c.reasonCode), std::move(c.reasonString));
+ },
+ [](const auto &) -> S {
+ return M::ProcessWaiting{};
+ }
+ );
+ }
+
+ void VerificationTracker::updateModel()
+ {
+ VerificationTrackerModel next;
+ for (const auto &[uid, m] : processes) {
+ for (const auto &[did, p] : m) {
+ auto newProcess = VerificationTrackerModel::Process{
+ uid,
+ did,
+ p.requestTimestamp(),
+ getState(p)
+ };
+ next.processes = std::move(next.processes).push_back(
+ std::move(newProcess)
+ );
+ }
+ }
+ model = std::move(next);
+ }
+
+ bool VerificationTracker::isVerificationEvent(const Event &e)
+ {
+ auto type = VU::typeOf(e);
+ return type.starts_with("m.key.verification.");
+ }
+
+ void VerificationTracker::setTheirIdentity(VerificationUtils::DeviceIdentity theirId)
+ {
+ bool changed = false;
+ withProcess(*this, theirId.userId, theirId.deviceId, [theirId, &changed](auto &p) {
+ p.setTheirDeviceKey(theirId.deviceKey);
+ changed = true;
+ });
+ if (changed) { updateModel(); }
+ }
+
+ auto VerificationTracker::userReady(std::string userId, std::string deviceId) -> PendingEvents
+ {
+ bool changed = false;
+ EventList ret;
+ withProcess(*this, userId, deviceId, [&changed, &ret](auto &p) {
+ ret = p.userReady();
+ changed = true;
+ });
+ if (changed) { updateModel(); }
+ return sendAllToDevice(userId, deviceId, ret);
+ }
+
+ auto VerificationTracker::userCancel(std::string userId, std::string deviceId) -> PendingEvents
+ {
+ bool changed = false;
+ EventList ret;
+ withProcess(*this, userId, deviceId, [&changed, &ret](auto &p) {
+ ret = p.userCancel();
+ changed = true;
+ });
+ if (changed) { updateModel(); }
+ return sendAllToDevice(userId, deviceId, ret);
+ }
+
+ auto VerificationTracker::userConfirmMatch(std::string userId, std::string deviceId) -> PendingEvents
+ {
+ bool changed = false;
+ EventList ret;
+ withProcess(*this, userId, deviceId, [&changed, &ret](auto &p) {
+ ret = p.userConfirmMatch();
+ changed = true;
+ });
+ if (changed) { updateModel(); }
+ return sendAllToDevice(userId, deviceId, ret);
+ }
+
+ auto VerificationTracker::userDenyMatch(std::string userId, std::string deviceId) -> PendingEvents
+ {
+ bool changed = false;
+ EventList ret;
+ withProcess(*this, userId, deviceId, [&changed, &ret](auto &p) {
+ ret = p.userDenyMatch();
+ changed = true;
+ });
+ if (changed) { updateModel(); }
+ return sendAllToDevice(userId, deviceId, ret);
+ }
+
+ void VerificationTracker::collect(Timestamp now)
+ {
+ bool changed = false;
+ auto userIds = zug::into_vector(
+ zug::map([](const auto &p) { return p.first; }),
+ processes
+ );
+ // don't iterate over processes directly because we may call erase()
+ // and invalidate the iterators
+ for (const auto &userId : userIds) {
+ auto &u = processes.at(userId);
+ auto deviceIds = zug::into_vector(
+ zug::map([](const auto &p) { return p.first; }),
+ u
+ );
+ for (const auto &deviceId : deviceIds) {
+ if (isExpired(u.at(deviceId), now)) {
+ changed = true;
+ u.erase(deviceId);
+ }
+ }
+ if (u.empty()) {
+ changed = true;
+ processes.erase(userId);
+ }
+ }
+ if (changed) {
+ updateModel();
+ }
+ }
+
+ bool VerificationTracker::isExpired(const VerificationProcess &process, Timestamp now)
+ {
+ auto timestamp = process.requestTimestamp();
+ return now > timestamp + dismissDelayMs;
+ }
+}
diff --git a/src/crypto/verification-tracker.hpp b/src/crypto/verification-tracker.hpp
new file mode 100644
index 0000000..e5005a2
--- /dev/null
+++ b/src/crypto/verification-tracker.hpp
@@ -0,0 +1,192 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+#include "verification-process.hpp"
+#include "verification-utils.hpp"
+
+namespace Kazv
+{
+ /**
+ * A model that is suitable for displaying to the user.
+ *
+ * @sa VerificationTracker
+ */
+ struct VerificationTrackerModel
+ {
+ struct ProcessTheyRequested
+ {
+ friend bool operator==(const ProcessTheyRequested &a, const ProcessTheyRequested &b) = default;
+ };
+
+ struct ProcessWaiting
+ {
+ friend bool operator==(const ProcessWaiting &a, const ProcessWaiting &b) = default;
+ };
+
+ struct ProcessCodeDisplayed
+ {
+ /// The indices of emojis to display
+ immer::array<int> emojiIndices;
+
+ /// The numbers to display
+ immer::array<int> decimalCode;
+
+ friend bool operator==(const ProcessCodeDisplayed &a, const ProcessCodeDisplayed &b) = default;
+ };
+
+ struct ProcessVerifiedThem
+ {
+ friend bool operator==(const ProcessVerifiedThem &a, const ProcessVerifiedThem &b) = default;
+ };
+
+ struct ProcessDone
+ {
+ friend bool operator==(const ProcessDone &a, const ProcessDone &b) = default;
+ };
+
+ struct ProcessCancelled
+ {
+ std::string reasonCode;
+ std::string reasonString;
+ friend bool operator==(const ProcessCancelled &a, const ProcessCancelled &b) = default;
+ };
+
+ using ProcessState = std::variant<
+ ProcessTheyRequested,
+ ProcessWaiting,
+ ProcessCodeDisplayed,
+ ProcessVerifiedThem,
+ ProcessDone,
+ ProcessCancelled
+ >;
+
+ struct Process
+ {
+ std::string theirUserId;
+ std::string theirDeviceId;
+ Timestamp requestedTs;
+ ProcessState state;
+ friend bool operator==(const Process &a, const Process &b) = default;
+ };
+
+ immer::flex_vector<Process> processes;
+ };
+
+ /**
+ * A stateful tracker for all verification processes.
+ *
+ * If you subscribe to the `model` attribute of this class using
+ * `lager::sensor`, you should call `lager::commit()` on the sensor
+ * each time you execute a change function.
+ *
+ * `this` will not modify itself unless you execute a change function.
+ *
+ * This class is not thread-safe, and you should serialize all
+ * function calls on one object.
+ *
+ * Unfortunately, the producer of the model cannot be made a value type,
+ * because how vodozemac dictates the no-copy nature of Sas.
+ * This sadly means that we cannot continue with a verification process
+ * if the program has been closed.
+ */
+ struct VerificationTracker
+ {
+ struct PendingEventDesc
+ {
+ Event event;
+ std::string toUserId;
+ std::string toDeviceId;
+ };
+
+ using PendingEvents = immer::flex_vector<PendingEventDesc>;
+
+ VerificationUtils::DeviceIdentity identity;
+ std::unordered_map<
+ std::string /* userId */,
+ std::unordered_map<
+ std::string /* deviceId */,
+ VerificationProcess
+ >
+ > processes;
+ /**
+ * The model that library user should subscribe to using `lager::sensor`.
+ */
+ VerificationTrackerModel model;
+
+ VerificationTracker(VerificationUtils::DeviceIdentity identity);
+
+ // Change functions:
+ /**
+ * Request an outgoing verification for a device using to-device message.
+ *
+ * After this function returns, `model` will contain a new process.
+ * `model.processes` will contain the created process `p`
+ * with `p.theirUserId == theirIdentity.userId && p.theirDeviceId == theirIdentity.theirDeviceId`.
+ */
+ [[nodiscard]] PendingEvents requestOutgoingToDevice(VerificationUtils::DeviceIdentity theirIdentity, Timestamp now);
+
+ /**
+ * Process incoming verification events.
+ */
+ [[nodiscard]] PendingEvents processIncoming(Timestamp now, EventList toDevice);
+
+ /**
+ * Check if the event is an verification event.
+ *
+ * When you receive an event from sync, you should call
+ * this function after decrypting it. If it returns true,
+ * you should then call processIncoming().
+ */
+ [[nodiscard]] static bool isVerificationEvent(const Event &e);
+
+ /**
+ * Set the key(s) for the other party in a process.
+ *
+ * When there is a process in the `model` with a
+ * ProcessTheyRequested state, you should, after the user is
+ * ready for the incoming request, fetch their device keys
+ * and call this function.
+ */
+ void setTheirIdentity(VerificationUtils::DeviceIdentity theirId);
+
+ /**
+ * Clean up all expired verification processes.
+ */
+ void collect(Timestamp now);
+
+ /**
+ * Mark ourselves ready for a process.
+ */
+ [[nodiscard]] PendingEvents userReady(std::string userId, std::string deviceId);
+
+ /**
+ * Cancel a verification process.
+ */
+ [[nodiscard]] PendingEvents userCancel(std::string userId, std::string deviceId);
+
+ /**
+ * Confirm an sas match for a process.
+ */
+ [[nodiscard]] PendingEvents userConfirmMatch(std::string userId, std::string deviceId);
+
+ /**
+ * Deny an sas match for a process.
+ */
+ [[nodiscard]] PendingEvents userDenyMatch(std::string userId, std::string deviceId);
+
+ private:
+ /**
+ * Generate `model` from `processes`.
+ */
+ void updateModel();
+
+ [[nodiscard]] static bool isExpired(const VerificationProcess &process, Timestamp now);
+
+ [[nodiscard]] PendingEvents processOne(const Event &e, Timestamp now);
+ };
+}
diff --git a/src/crypto/verification-utils.hpp b/src/crypto/verification-utils.hpp
index c9f3e26..22a893e 100644
--- a/src/crypto/verification-utils.hpp
+++ b/src/crypto/verification-utils.hpp
@@ -1,38 +1,47 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <event.hpp>
namespace Kazv
{
namespace VerificationEventTypes
{
inline const std::string tRequest = "m.key.verification.request";
inline const std::string tReady = "m.key.verification.ready";
inline const std::string tStart = "m.key.verification.start";
inline const std::string tAccept = "m.key.verification.accept";
inline const std::string tKey = "m.key.verification.key";
inline const std::string tMac = "m.key.verification.mac";
inline const std::string tDone = "m.key.verification.done";
inline const std::string tCancel = "m.key.verification.cancel";
}
namespace VerificationUtils
{
+ struct DeviceIdentity
+ {
+ std::string userId;
+ std::string deviceId;
+ std::string deviceKey;
+
+ friend bool operator==(const DeviceIdentity &a, const DeviceIdentity &b) = default;
+ };
+
/// Validate the format of the verification event.
bool validateFormat(const Event &e);
/// @return iff the verification event is to device.
bool isToDevice(const Event &e);
/// Get the type of the verification event.
std::string typeOf(const Event &e);
/// @return iff the verification event has a transaction id.
bool hasTxnId(const Event &e);
/// Get the transaction id of the verification event.
std::string txnId(const Event &e);
}
}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index c31f0c1..04d076d 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,131 +1,132 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
function(libkazv_add_tests)
set(options "")
set(oneValueArgs "")
set(multiValueArgs EXTRA_LINK_LIBRARIES EXTRA_INCLUDE_DIRECTORIES)
cmake_parse_arguments(PARSE_ARGV 0 libkazv_add_tests "${options}" "${oneValueArgs}" "${multiValueArgs}")
foreach(test_source ${libkazv_add_tests_UNPARSED_ARGUMENTS})
string(REGEX REPLACE "\\.cpp$" "" test_executable "${test_source}")
string(REGEX REPLACE "/|\\\\" "--" test_executable "${test_executable}")
message(STATUS "Test ${test_executable} added")
add_executable("${test_executable}" "${test_source}")
target_link_libraries("${test_executable}"
PRIVATE Catch2::Catch2WithMain
Threads::Threads
${libkazv_add_tests_EXTRA_LINK_LIBRARIES}
)
target_include_directories(
"${test_executable}"
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${libkazv_add_tests_EXTRA_INCLUDE_DIRECTORIES}
)
target_compile_definitions("${test_executable}" PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
add_test(NAME "${test_executable}" COMMAND "${test_executable}" "--allow-running-no-tests" "~[needs-internet]")
endforeach()
endfunction()
libkazv_add_tests(
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
EXTRA_LINK_LIBRARIES kazvbase
)
add_library(client-test-lib SHARED client/client-test-util.cpp)
target_link_libraries(client-test-lib PUBLIC kazvjob kazvclient)
libkazv_add_tests(
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/storage-actions-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
client/join-test.cpp
client/keys-test.cpp
client/device-ops-test.cpp
client/send-test.cpp
client/encryption-test.cpp
client/redact-test.cpp
client/tagging-test.cpp
client/account-data-test.cpp
client/room/room-actions-test.cpp
client/room/local-echo-test.cpp
client/room/event-relationships-test.cpp
client/room/member-membership-test.cpp
client/room/purge-test.cpp
client/push-rules-desc-test.cpp
client/notification-handler-test.cpp
client/validator-test.cpp
client/power-levels-desc-test.cpp
client/client-test.cpp
client/create-room-test.cpp
client/device-list-tracker-test.cpp
client/device-list-tracker-benchmark-test.cpp
client/room/read-receipt-test.cpp
client/room/undecrypted-events-test.cpp
client/encryption-benchmark-test.cpp
client/login-test.cpp
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
client/alias-test.cpp
client/encode-test.cpp
client/maybe-add-save-events-trigger-benchmark-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
libkazv_add_tests(
basejobtest.cpp
kazvjobtest.cpp
file-desc-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob
)
libkazv_add_tests(
promise-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob kazvstore
)
libkazv_add_tests(
event-emitter-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazveventemitter
)
libkazv_add_tests(
crypto-test.cpp
crypto/inbound-group-session-test.cpp
crypto/outbound-group-session-test.cpp
crypto/session-test.cpp
crypto/key-export-test.cpp
crypto/verification-process-test.cpp
crypto/verification-utils-test.cpp
+ crypto/verification-tracker-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
libkazv_add_tests(
store-test.cpp
EXTRA_LINK_LIBRARIES kazvstore kazvjob
)
diff --git a/src/tests/crypto/verification-tracker-test.cpp b/src/tests/crypto/verification-tracker-test.cpp
new file mode 100644
index 0000000..733aa38
--- /dev/null
+++ b/src/tests/crypto/verification-tracker-test.cpp
@@ -0,0 +1,81 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "verification-tracker.hpp"
+#include <crypto.hpp>
+#include <catch2/catch_test_macros.hpp>
+
+#define KT_CONTINUE(_name) AND_WHEN(_name) {
+#define KT_END }
+
+using namespace Kazv;
+namespace VU = Kazv::VerificationUtils;
+using namespace Kazv::VerificationEventTypes;
+
+const Timestamp ts = 1559598944869;
+using VTM = VerificationTrackerModel;
+
+static Event withSender(Event e, std::string userId)
+{
+ auto j = e.originalJson().get();
+ j["sender"] = userId;
+ return Event(j);
+}
+
+TEST_CASE("VerificationTracker", "[client][verification]")
+{
+ Crypto ca(RandomTag{}, {});
+ Crypto cb(RandomTag{}, {});
+ VU::DeviceIdentity aId{
+ "@alice:example.com",
+ "AliceDevice2",
+ ca.ed25519IdentityKey(),
+ };
+ VU::DeviceIdentity bId{
+ "@bob:example.com",
+ "BobDevice1",
+ cb.ed25519IdentityKey(),
+ };
+ auto ta = VerificationTracker(aId);
+ auto tb = VerificationTracker(bId);
+ auto es = ta.requestOutgoingToDevice(bId, ts);
+ const auto procA = [&ta, bId]() {
+ return *std::find_if(
+ ta.model.processes.begin(),
+ ta.model.processes.end(),
+ [bId](const auto &p) { return p.theirUserId == bId.userId && p.theirDeviceId == bId.deviceId; }
+ );
+ };
+ const auto procB = [&tb, aId]() {
+ return *std::find_if(
+ tb.model.processes.begin(),
+ tb.model.processes.end(),
+ [aId](const auto &p) { return p.theirUserId == aId.userId && p.theirDeviceId == aId.deviceId; }
+ );
+ };
+ REQUIRE(std::holds_alternative<VTM::ProcessWaiting>(procA().state));
+ auto requestEvent = withSender(es.at(0).event, aId.userId);
+ REQUIRE(requestEvent.type() == tRequest);
+
+ WHEN("too late") {
+ es = tb.processIncoming(ts + 10 * 60 * 1000 + 1, {requestEvent});
+ REQUIRE(tb.model.processes.empty());
+ }
+
+ KT_CONTINUE("ok state");
+
+ es = tb.processIncoming(ts, {requestEvent});
+ REQUIRE(std::holds_alternative<VTM::ProcessTheyRequested>(procB().state));
+ REQUIRE(es.empty());
+
+ es = tb.userReady(aId.userId, aId.deviceId);
+ REQUIRE(std::holds_alternative<VTM::ProcessWaiting>(procB().state));
+ REQUIRE(es.size() == 1);
+ auto readyEvent = withSender(es.at(0).event, bId.userId);
+ REQUIRE(readyEvent.type() == tReady);
+
+ KT_END;
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Sep 18, 10:21 PM (14 m, 53 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768329
Default Alt Text
(60 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment