Page MenuHomePhorge

No OneTemporary

Size
60 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Fri, Sep 18, 10:21 PM (1 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768329
Default Alt Text
(60 KB)

Event Timeline