Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F50709541
D294.1774265727.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
D294.1774265727.diff
View Options
diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt
--- a/src/crypto/CMakeLists.txt
+++ b/src/crypto/CMakeLists.txt
@@ -10,6 +10,7 @@
key-export.cpp
verification-process.cpp
verification-utils.cpp
+ verification-tracker.cpp
sas-desc.cpp
)
diff --git a/src/crypto/verification-process.hpp b/src/crypto/verification-process.hpp
--- a/src/crypto/verification-process.hpp
+++ b/src/crypto/verification-process.hpp
@@ -82,6 +82,7 @@
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"};
@@ -143,11 +144,21 @@
*/
[[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
{
diff --git a/src/crypto/verification-process.cpp b/src/crypto/verification-process.cpp
--- a/src/crypto/verification-process.cpp
+++ b/src/crypto/verification-process.cpp
@@ -318,9 +318,41 @@
};
}
+ 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},
+ })};
+ return {std::move(requestEvent)};
+ }
+
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
diff --git a/src/crypto/verification-tracker.hpp b/src/crypto/verification-tracker.hpp
new file mode 100644
--- /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-tracker.cpp b/src/crypto/verification-tracker.cpp
new file mode 100644
--- /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-utils.hpp b/src/crypto/verification-utils.hpp
--- a/src/crypto/verification-utils.hpp
+++ b/src/crypto/verification-utils.hpp
@@ -24,6 +24,15 @@
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.
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -122,6 +122,7 @@
crypto/key-export-test.cpp
crypto/verification-process-test.cpp
crypto/verification-utils-test.cpp
+ crypto/verification-tracker-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
diff --git a/src/tests/crypto/verification-tracker-test.cpp b/src/tests/crypto/verification-tracker-test.cpp
new file mode 100644
--- /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/plain
Expires
Mon, Mar 23, 4:35 AM (14 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1223370
Default Alt Text
D294.1774265727.diff (22 KB)
Attached To
Mode
D294: Add verification tracker
Attached
Detach File
Event Timeline
Log In to Comment