Page MenuHomePhorge

D294.1774265727.diff
No OneTemporary

Size
22 KB
Referenced Files
None
Subscribers
None

D294.1774265727.diff

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

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)

Event Timeline