Page MenuHomePhorge

D295.1774573732.diff
No OneTemporary

Size
58 KB
Referenced Files
None
Subscribers
None

D295.1774573732.diff

diff --git a/src/base/kazv-triggers.hpp b/src/base/kazv-triggers.hpp
--- a/src/base/kazv-triggers.hpp
+++ b/src/base/kazv-triggers.hpp
@@ -75,6 +75,8 @@
Response response;
};
+ struct VerificationTrackerModelChanged {};
+
using KazvTrigger = std::variant<
// use this for placeholder of "no events yet"
// otherwise the first LoginSuccessful event cannot be detected
@@ -93,6 +95,8 @@
LoginSuccessful, LoginFailed,
// storage
SaveEventsRequested,
+ // encryption
+ VerificationTrackerModelChanged,
// general
UnrecognizedResponse
diff --git a/src/client/actions/encryption.hpp b/src/client/actions/encryption.hpp
--- a/src/client/actions/encryption.hpp
+++ b/src/client/actions/encryption.hpp
@@ -21,6 +21,7 @@
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a);
ClientResult updateClient(ClientModel m, QueryKeysAction a);
+ ClientResult updateClient(ClientModel m, EnsureKeysFromDevicesAction a);
ClientResult processResponse(ClientModel m, QueryKeysResponse r);
ClientResult updateClient(ClientModel m, ClaimKeysAction a);
@@ -35,4 +36,6 @@
ClientResult updateClient(ClientModel m, PrepareForSharingRoomKeyAction a);
ClientResult updateClient(ClientModel m, ImportFromKeyBackupFileAction a);
+
+ ClientResult updateClient(ClientModel m, NotifyVerificationTrackerModelAction a);
}
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -435,10 +435,13 @@
kzo.client.dbg() << "^" << std::endl;
auto job = m.job<QueryKeysJob>()
- .make(std::move(deviceKeys),
+ .make(deviceKeys,
std::nullopt, // timeout
a.isInitialSync ? std::nullopt : m.syncToken
- );
+ )
+ .withData(json::object({
+ {"deviceKeys", deviceKeys},
+ }));
return job;
}
@@ -452,6 +455,42 @@
return { std::move(m), lager::noop };
}
+ ClientResult updateClient(ClientModel m, EnsureKeysFromDevicesAction a)
+ {
+ immer::map<std::string, immer::array<std::string>> deviceKeys;
+ for (auto [userId, deviceIds] : a.userIdToDeviceIdsMap) {
+ if (deviceIds.empty()) {
+ deviceKeys = std::move(deviceKeys).set(userId, {});
+ } else {
+ auto devicesToFetch = intoImmer(
+ immer::array<std::string>{},
+ zug::filter([&m, userId](const auto &deviceId) {
+ return !m.deviceLists.get(userId, deviceId).has_value();
+ }),
+ deviceIds
+ );
+ if (!devicesToFetch.empty()) {
+ // Originally we want to ensure a subset of the devices
+ // of some user, but we are still missing some
+ deviceKeys = std::move(deviceKeys).set(userId, devicesToFetch);
+ }
+ // Otherwise, we already have all the keys we need.
+ }
+ }
+ if (!deviceKeys.empty()) {
+ auto job = m.job<QueryKeysJob>()
+ .make(deviceKeys,
+ std::nullopt, // timeout
+ std::nullopt // sync token
+ )
+ .withData(json::object({
+ {"deviceKeys", deviceKeys},
+ }));
+ m.addJob(job);
+ }
+ return { std::move(m), lager::noop };
+ }
+
ClientResult processResponse(ClientModel m, QueryKeysResponse r)
{
if (! m.crypto) {
@@ -466,7 +505,41 @@
kzo.client.dbg() << "Received a query key response" << std::endl;
+ auto requested = r.dataJson("deviceKeys").template get<immer::map<std::string, immer::array<std::string>>>();
+ auto wantedToFetchAllForUser = [&requested](const std::string &userId) {
+ return requested.count(userId) && requested[userId].empty();
+ };
auto usersMap = r.deviceKeys();
+ auto unsatisfied = json::object({
+ {"users", zug::into(
+ json::array(), zug::filter([usersMap](const auto &p) {
+ return !usersMap.count(p.first);
+ }),
+ requested
+ )},
+ {"devices", zug::into(
+ json::array(),
+ zug::filter([usersMap](const auto &p) {
+ return p.second.size() && usersMap.count(p.first);
+ })
+ | zug::map([usersMap](const auto &p) {
+ auto [userId, deviceIds] = p;
+ auto deviceMap = usersMap[p.first];
+ return zug::into(
+ std::vector<json>(),
+ zug::filter([deviceMap](const auto &deviceId) {
+ return !deviceMap.count(deviceId);
+ })
+ | zug::map([userId](const auto &deviceId) {
+ return json::array({userId, deviceId});
+ }),
+ deviceIds
+ );
+ })
+ | zug::cat,
+ requested
+ )},
+ });
for (auto [userId, deviceMap] : usersMap) {
for (auto [deviceId, deviceInfo] : deviceMap) {
@@ -478,10 +551,16 @@
m.deviceLists.addDevice(userId, deviceId, deviceInfo, c);
});
}
- m.deviceLists.markUpToDate(userId);
+ if (wantedToFetchAllForUser(userId)) {
+ m.deviceLists.markUpToDate(userId);
+ }
}
- return { std::move(m), lager::noop };
+ return { std::move(m), detail::ReturnEffectStatusT{
+ EffectStatus{/* succ = */ true, json::object({
+ {"unsatisfied", unsatisfied}
+ })}
+ } };
}
ClientResult updateClient(ClientModel m, ClaimKeysAction a)
@@ -709,4 +788,10 @@
},
}}};
};
+
+ ClientResult updateClient(ClientModel m, [[maybe_unused]] NotifyVerificationTrackerModelAction a)
+ {
+ m.addTrigger(VerificationTrackerModelChanged{});
+ return {std::move(m), lager::noop};
+ }
}
diff --git a/src/client/actions/sync.cpp b/src/client/actions/sync.cpp
--- a/src/client/actions/sync.cpp
+++ b/src/client/actions/sync.cpp
@@ -21,6 +21,7 @@
#include "encryption.hpp"
#include "status-utils.hpp"
+#include "verification-tracker.hpp"
namespace Kazv
{
@@ -241,6 +242,26 @@
return {};
}
+ [[nodiscard]] static json popVerificationEvents(ClientModel &m)
+ {
+ auto oldToDevice = std::move(m.toDevice);
+ auto toDeviceVerificationEvents = intoImmer(
+ EventList{},
+ zug::filter([](const auto &e) {
+ return VerificationTracker::isVerificationEvent(e);
+ }),
+ oldToDevice);
+ m.toDevice = intoImmer(
+ EventList{},
+ zug::filter([](const auto &e) {
+ return !VerificationTracker::isVerificationEvent(e);
+ }),
+ std::move(oldToDevice));
+ return json::object({
+ {"toDevice", toDeviceVerificationEvents}
+ });
+ }
+
ClientResult processResponse(ClientModel m, SyncResponse r)
{
if (! r.success()) {
@@ -284,6 +305,9 @@
auto is = r.dataStr("is");
auto isInitialSync = is == "initial";
+ json ve = json::object({
+ {"toDevice", json::array()},
+ });
if (m.crypto) {
kzo.client.dbg() << "E2EE is on. Processing device lists and one-time key counts." << std::endl;
@@ -318,9 +342,14 @@
auto model = tryDecryptEvents(std::move(m));
m = std::move(model);
+ ve = popVerificationEvents(m);
}
- return { std::move(m), lager::noop };
+ return { std::move(m), [ve=std::move(ve)](auto &&) {
+ return EffectStatus(true, json{
+ {"verificationEvents", ve},
+ });
+ } };
}
ClientResult updateClient(ClientModel m, SetShouldSyncAction a)
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -22,7 +22,7 @@
#include <file-desc.hpp>
#include <crypto.hpp>
-
+#include <verification-tracker.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
@@ -501,6 +501,46 @@
bool isInitialSync;
};
+ /**
+ * Ensure keys from devices of a user.
+ *
+ * After the reducer for this action completes,
+ * the ClientModel will contain information about the devices'
+ * keys, in the DeviceListTracker (ClientModel::deviceLists).
+ *
+ * The after receiving a response, the resulting EffectStatus will contain
+ * a data property `unsatisfied`. It is in the format:
+ *
+ * ```
+ * {
+ * "users": [userIds...], "devices": [[userId, deviceId]...]
+ * }
+ * ```
+ *
+ * If we requested all devices of a user `@foo:example.org` and the user is
+ * not available, then `data.at("unsatisfied").at("users")` will contain
+ * `@foo:example.org`.
+ *
+ * If we requested a device `Device1` of a user `@foo:example.org` and the
+ * device is not available, then `data.at("unsatisfied").at("devices")` will
+ * contain `["@foo:example.org", "Device1"]`.
+ */
+ struct EnsureKeysFromDevicesAction
+ {
+ /**
+ * The map detailing the devices of which the keys to be fetched.
+ *
+ * This follows the same semantics as the query keys endpoint
+ * (/_matrix/client/v3/keys/query): if the device id list is
+ * empty, it will query all keys of that user. In this case,
+ * after receiving the response, we will also mark the device lists
+ * for that user as up-to-date.
+ */
+ immer::map<
+ std::string /* userId */,
+ immer::flex_vector<std::string /* deviceId */>> userIdToDeviceIdsMap;
+ };
+
struct ClaimKeysAction
{
static std::size_t randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend);
@@ -592,6 +632,11 @@
std::string password;
};
+ /**
+ * Notify that the verification tracker model has been changed.
+ */
+ struct NotifyVerificationTrackerModelAction {};
+
struct GetUserProfileAction
{
std::string userId;
diff --git a/src/client/client.hpp b/src/client/client.hpp
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -21,6 +21,7 @@
#include "room/room.hpp"
#include "notification-handler.hpp"
+#include <verification-tracker.hpp>
namespace Kazv
{
@@ -43,6 +44,32 @@
* If the Client is constructed from a cursor, copy-constructing another
* Client is safe only from the same thread as this Client.
*
+ * ## Device verification integration
+ *
+ * The `startSyncing()` function will automatically feed verification events
+ * received from sync into the VerificationTracker, and send outbound events
+ * according to the result returned from the VerificationTracker.
+ *
+ * The verification processing functions in this class (
+ * requestOutgoingToDeviceVerification(),
+ * readyForVerification(),
+ * cancelVerification(), confirmVerificationSasMatch(),
+ * denyVerificationSasMatch()) will also automatically send outbound
+ * events according to the result returned from the VerificationTracker.
+ * They will also ensure the device keys of the devices to be verified
+ * is available during the verification process.
+ *
+ * Additionally, this class will cause the VerificationTrackerModelChanged
+ * trigger to be emitted when appropriate.
+ * If you use a `lager::sensor` to observe the `VerificationTracker::model`,
+ * you should call `lager::commit()` on the `lager::sensor` **in the event loop
+ * thread** after you received VerificationTrackerModelChanged.
+ *
+ * You must always access VerificationTracker::model from the event loop thread,
+ * because the modifications to VerificationTracker always happen in the event
+ * loop thread. However, once you have a copy of the VerificationTrackerModel,
+ * you are free to copy it and pass it onto other threads.
+ *
* ## Error handling
*
* A lot of functions in Client and Room are asynchronous actions.
@@ -61,7 +88,7 @@
public:
using ActionT = ClientAction;
- using DepsT = lager::deps<JobInterface &, EventInterface &, SdkModelCursorKey, RandomInterface &
+ using DepsT = lager::deps<JobInterface &, EventInterface &, SdkModelCursorKey, RandomInterface &, VerificationTracker &
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, EventLoopThreadIdKeeper &
#endif
@@ -657,6 +684,69 @@
*/
PromiseT importFromKeyBackupFile(std::string fileContent, std::string password) const;
+ /**
+ * Process verification events from a sync result.
+ *
+ * This will be called automatically after a sync.
+ *
+ * @param toDeviceEvents The list of to-device verification events received from sync.
+ * @return A Promise that resolves when the processing is done. After it resolves,
+ * actions will be dispatched to send any pending to-device events in the
+ * VerificationTracker's model.
+ */
+ PromiseT processVerificationEventsFromSync(EventList toDeviceEvents) const;
+
+ /**
+ * Request an outgoing verification using to-device message.
+ *
+ * This will automatically fetch the device keys if we do not yet have
+ * them.
+ *
+ * @return A Promise that resolves when the outgoing request is sent,
+ * or when there is an error.
+ */
+ PromiseT requestOutgoingToDeviceVerification(std::string userId, std::string deviceId) const;
+
+ /**
+ * Signal that the user is ready for an incoming verification request.
+ *
+ * This will automatically fetch the device keys if we do not yet have
+ * them.
+ *
+ * @return A Promise that resolves when the ready event is sent,
+ * or when there is an error.
+ */
+ PromiseT readyForVerification(std::string userId, std::string deviceId) const;
+
+ /**
+ * Cancel a verification process.
+ *
+ * @return A Promise that resolves when the cancel event is sent,
+ * or when there is an error.
+ */
+ PromiseT cancelVerification(std::string userId, std::string deviceId) const;
+
+ /**
+ * Confirm an sas match for a verification process.
+ *
+ * @return A Promise that resolves when the next event is sent,
+ * or when there is an error.
+ */
+ PromiseT confirmVerificationSasMatch(std::string userId, std::string deviceId) const;
+
+ /**
+ * Deny an sas match for a verification process.
+ *
+ * @return A Promise that resolves when the next event is sent,
+ * or when there is an error.
+ */
+ PromiseT denyVerificationSasMatch(std::string userId, std::string deviceId) const;
+
+ /**
+ * Ensure the VerificationTracker is initialized.
+ */
+ PromiseT ensureInitVerificationTracker() const;
+
private:
void syncForever(std::optional<int> retryTime = std::nullopt) const;
diff --git a/src/client/client.cpp b/src/client/client.cpp
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -8,7 +8,7 @@
#include <filesystem>
#include <algorithm>
-
+#include <chrono>
#include <lager/constant.hpp>
#include "client.hpp"
@@ -17,6 +17,25 @@
namespace Kazv
{
+ static Timestamp tsNow()
+ {
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::system_clock::now().time_since_epoch()
+ ).count();
+ }
+
+ static Client::PromiseT sendMultiVerificationEvents(Client::ContextT ctx, VerificationTracker::PendingEvents pendingEvents)
+ {
+ std::vector<Client::PromiseT> ps;
+ for (auto ed : pendingEvents) {
+ ps.push_back(ctx.dispatch(SendToDeviceMessageAction{
+ ed.event,
+ {{ed.toUserId, {ed.toDeviceId}}}
+ }));
+ }
+ return ctx.promiseInterface().all(ps);
+ }
+
Client::Client(lager::reader<SdkModel> sdk,
ContextT ctx, std::nullopt_t)
: m_sdk(sdk)
@@ -309,6 +328,156 @@
return p1;
}
+ auto Client::processVerificationEventsFromSync(EventList toDeviceEvents) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), ves=toDeviceEvents](auto &&stat) {
+ if (!stat.success()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto now = tsNow();
+ auto es = vt.processIncoming(now, ves);
+ for (auto ed : es) {
+ // We don't actually need to wait for these events to
+ // be sent before we proceed into next sync cycle.
+ that.m_ctx.dispatch(SendToDeviceMessageAction{
+ ed.event,
+ {{ed.toUserId, {ed.toDeviceId}}}
+ });
+ }
+ return that.m_ctx.dispatch(NotifyVerificationTrackerModelAction{});
+ });
+ }
+
+ auto Client::requestOutgoingToDeviceVerification(std::string userId, std::string deviceId) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), userId, deviceId](auto &&stat) {
+ if (!stat.success()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
+ {{userId, {deviceId}}},
+ });
+ })
+ .then([that=toEventLoop(), userId, deviceId](auto &&stat) {
+ if (!stat.success()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
+ return client.deviceLists.get(userId, deviceId);
+ }).make().get();
+ if (!deviceOpt) {
+ return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
+ {"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
+ {"error", "Cannot obtain device keys"},
+ })});
+ }
+ auto device = deviceOpt.value();
+ auto es = vt.requestOutgoingToDevice(VerificationUtils::DeviceIdentity{
+ userId,
+ deviceId,
+ device.ed25519Key,
+ }, tsNow());
+ return sendMultiVerificationEvents(that.m_ctx, es);
+ });
+ }
+
+ auto Client::readyForVerification(std::string userId, std::string deviceId) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), userId, deviceId](auto &&stat) {
+ if (!stat.success()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
+ {{userId, {deviceId}}},
+ });
+ })
+ .then([that=toEventLoop(), userId, deviceId](auto &&stat) {
+ if (!stat.success()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
+ return client.deviceLists.get(userId, deviceId);
+ }).make().get();
+ if (!deviceOpt) {
+ return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
+ {"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
+ {"error", "Cannot obtain device keys"},
+ })});
+ }
+ auto device = deviceOpt.value();
+ vt.setTheirIdentity(VerificationUtils::DeviceIdentity{
+ userId,
+ deviceId,
+ device.ed25519Key,
+ });
+ auto es = vt.userReady(userId, deviceId);
+ return sendMultiVerificationEvents(that.m_ctx, es);
+ });
+ }
+
+ auto Client::cancelVerification(std::string userId, std::string deviceId) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), userId, deviceId](auto &&) {
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto es = vt.userCancel(userId, deviceId);
+ return sendMultiVerificationEvents(that.m_ctx, es);
+ });
+ }
+
+ auto Client::confirmVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), userId, deviceId](auto &&) {
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto es = vt.userConfirmMatch(userId, deviceId);
+ return sendMultiVerificationEvents(that.m_ctx, es);
+ });
+ }
+
+ auto Client::denyVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
+ {
+ return ensureInitVerificationTracker()
+ .then([that=toEventLoop(), userId, deviceId](auto &&) {
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto es = vt.userDenyMatch(userId, deviceId);
+ return sendMultiVerificationEvents(that.m_ctx, es);
+ });
+ }
+
+ auto Client::ensureInitVerificationTracker() const -> PromiseT
+ {
+ if (!m_deps) {
+ return m_ctx.createResolvedPromise(false);
+ }
+ bool hasCrypto{clientCursor().map([](const auto &c) {
+ return c.crypto.has_value();
+ }).make().get()};
+ if (!hasCrypto) {
+ return m_ctx.createResolvedPromise(false);
+ }
+ return m_ctx.createResolvedPromise({})
+ .then([that=toEventLoop()](auto) {
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ if (vt.identity.userId.empty()) {
+ auto client = that.clientCursor().get();
+ const auto &crypto = client.constCrypto();
+ vt.identity = {
+ client.userId,
+ client.deviceId,
+ crypto.ed25519IdentityKey(),
+ };
+ }
+ return EffectStatus{/* succ = */ true};
+ });
+ }
+
auto Client::syncForever(std::optional<int> retryTime) const -> void
{
KAZV_VERIFY_THREAD_ID();
@@ -355,8 +524,21 @@
: that.m_ctx.createResolvedPromise(true);
});
+ auto processVerificationEventsRes = syncRes
+ .then([that=toEventLoop()](EffectStatus stat) {
+ if (!stat.success() || !that.m_deps ||
+ !that.clientCursor().map([](const auto &c) {
+ return c.crypto.has_value();
+ }).make().get()) {
+ return that.m_ctx.createResolvedPromise(stat);
+ }
+ kzo.client.dbg() << "processVerificationEvents: " << stat.data().get().dump() << std::endl;
+ EventList ves = stat.data().get().at("verificationEvents").at("toDevice").template get<EventList>();
+ return that.processVerificationEventsFromSync(ves);
+ });
+
m_ctx.promiseInterface()
- .all(std::vector<PromiseT>{uploadOneTimeKeysRes, queryKeysRes})
+ .all(std::vector<PromiseT>{uploadOneTimeKeysRes, queryKeysRes, processVerificationEventsRes})
.then([that=toEventLoop(), retryTime](auto stat) {
if (stat.success()) {
that.syncForever(); // reset retry time
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -66,12 +66,14 @@
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
+ struct EnsureKeysFromDevicesAction;
struct ClaimKeysAction;
struct EncryptMegOlmEventAction;
struct SetDeviceTrustLevelAction;
struct SetTrustLevelNeededToSendKeysAction;
struct PrepareForSharingRoomKeyAction;
struct ImportFromKeyBackupFileAction;
+ struct NotifyVerificationTrackerModelAction;
struct GetUserProfileAction;
struct SetAvatarUrlAction;
@@ -136,12 +138,14 @@
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
+ EnsureKeysFromDevicesAction,
ClaimKeysAction,
EncryptMegOlmEventAction,
SetDeviceTrustLevelAction,
SetTrustLevelNeededToSendKeysAction,
PrepareForSharingRoomKeyAction,
ImportFromKeyBackupFileAction,
+ NotifyVerificationTrackerModelAction,
GetUserProfileAction,
SetAvatarUrlAction,
diff --git a/src/client/sdk.hpp b/src/client/sdk.hpp
--- a/src/client/sdk.hpp
+++ b/src/client/sdk.hpp
@@ -16,7 +16,7 @@
#include "sdk-model-cursor-tag.hpp"
#include "client.hpp"
#include "thread-safety-helper.hpp"
-
+#include "verification-tracker.hpp"
#include "random-generator.hpp"
@@ -43,7 +43,8 @@
std::ref(detail::declref<JobInterface>()),
std::ref(detail::declref<EventInterface>()),
lager::dep::as<SdkModelCursorKey>(std::declval<std::function<CursorTSP()>>()),
- std::ref(detail::declref<RandomInterface>())
+ std::ref(detail::declref<RandomInterface>()),
+ std::ref(detail::declref<VerificationTracker>())
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, std::ref(detail::declref<EventLoopThreadIdKeeper>())
#endif
@@ -51,7 +52,7 @@
std::declval<Enhancers>()...)
);
- using DepsT = lager::deps<JobInterface &, EventInterface &, SdkModelCursorKey, RandomInterface &
+ using DepsT = lager::deps<JobInterface &, EventInterface &, SdkModelCursorKey, RandomInterface &, VerificationTracker &
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, EventLoopThreadIdKeeper &
#endif
@@ -143,6 +144,7 @@
Xform &&xform,
Enhancers &&...enhancers)
: rg(RandomInterface{RandomDeviceGenerator{}})
+ , vt(VerificationUtils::DeviceIdentity())
, store(makeStore<ActionT>(
std::move(model),
&ModelT::update,
@@ -152,7 +154,8 @@
std::ref(eventEmitter),
lager::dep::as<SdkModelCursorKey>(
std::function<CursorTSP()>([this] { return sdk; })),
- std::ref(rg.value())
+ std::ref(rg.value()),
+ std::ref(vt)
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, std::ref(keeper)
#endif
@@ -171,6 +174,7 @@
EventLoopThreadIdKeeper keeper;
#endif
std::optional<RandomInterface> rg;
+ VerificationTracker vt;
StoreT store;
CursorTSP sdk;
};
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -93,6 +93,7 @@
client/alias-test.cpp
client/encode-test.cpp
client/maybe-add-save-events-trigger-benchmark-test.cpp
+ client/verification-processing-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
diff --git a/src/tests/client/action-mock-utils.hpp b/src/tests/client/action-mock-utils.hpp
--- a/src/tests/client/action-mock-utils.hpp
+++ b/src/tests/client/action-mock-utils.hpp
@@ -17,6 +17,7 @@
#include <client/sdk.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
+#include <promise-interface.hpp>
#include <asio-promise-handler.hpp>
#include <lager/event_loop/boost_asio.hpp>
@@ -328,6 +329,17 @@
, jh{io.get_executor()}
, ee(lager::with_boost_asio_event_loop{io.get_executor()})
, ph{io.get_executor()}
+ , sgph(ph)
+ , sdk(Kazv::makeSdk(Kazv::SdkModel{m}, jh, ee, ph, zug::identity))
+ , ctx(sdk.context())
+ {}
+
+ MockSdkUtil(boost::asio::io_context::executor_type ex, Kazv::ClientModel m)
+ : io()
+ , jh{ex}
+ , ee(lager::with_boost_asio_event_loop{ex})
+ , ph{ex}
+ , sgph(ph)
, sdk(Kazv::makeSdk(Kazv::SdkModel{m}, jh, ee, ph, zug::identity))
, ctx(sdk.context())
{}
@@ -335,7 +347,7 @@
template<class ...Handlers>
auto getMockDispatcher(Handlers &&...handlers)
{
- return ::getMockDispatcher(ph, ctx, std::forward<Handlers>(handlers)...);
+ return ::getMockDispatcher(sgph, ctx, std::forward<Handlers>(handlers)...);
}
template<class MD>
@@ -349,6 +361,7 @@
Kazv::CprJobHandler jh;
Kazv::LagerStoreEventEmitter ee;
PH ph;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph;
SdkT sdk;
ContextT ctx;
};
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -33,13 +33,14 @@
};
}
-static CreateE2EESessionResult createE2EESession()
+static json makeDeviceInfo(const ClientModel &client)
{
- auto makeDeviceInfo = [](const ClientModel &client) {
- auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
- return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
- };
+ auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
+ return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
+}
+static CreateE2EESessionResult createE2EESession()
+{
auto r1Crypto = makeCrypto();
r1Crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto r1 = makeClient(withCrypto(r1Crypto));
@@ -74,13 +75,16 @@
};
std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse(
- makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonSender))
+ makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonSender)
+ | withResponseDataKV("deviceKeys", json::object({{"@receiver:example.com", json::array()}})))
));
std::tie(r1, std::ignore) = processResponse(r1, QueryKeysResponse(
- makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonReceiver))
+ makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonReceiver)
+ | withResponseDataKV("deviceKeys", json::object({{"@sender:example.com", json::array()}})))
));
std::tie(r2, std::ignore) = processResponse(r2, QueryKeysResponse(
- makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonReceiver))
+ makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonReceiver)
+ | withResponseDataKV("deviceKeys", json::object({{"@sender:example.com", json::array()}})))
));
// Claim keys
@@ -439,3 +443,69 @@
u.io.run();
}
}
+
+TEST_CASE("EnsureKeysFromDevicesAction", "[client][encryption]")
+{
+ auto client = makeClient(
+ withCrypto(makeCrypto())
+ );
+
+ auto u1Client = makeClient(withCrypto(makeCrypto()));
+ u1Client.userId = "@user:example.com";
+ u1Client.deviceId = "U1Device1";
+
+ auto [next, _] = updateClient(client, EnsureKeysFromDevicesAction{
+ {{"@user:example.com", {"U1Device1"}}},
+ });
+ assert1Job(next);
+ auto job = next.nextJobs.front();
+ next.nextJobs = {};
+ REQUIRE(job.jobId() == "QueryKeys");
+ auto body = json::parse(std::get<BytesBody>(job.requestBody()));
+ REQUIRE(body.at("device_keys") == json::object({
+ {"@user:example.com", json::array({"U1Device1"})},
+ }));
+
+ auto u = makeMockSdkUtil(next);
+ auto md = u.getMockDispatcher(passDown<ProcessResponseAction>());
+ auto ctx = getMockContext(u.ph, md);
+ WHEN("good response") {
+ auto resp = makeResponse("QueryKeys", withResponseJsonBody(json::object({
+ {"device_keys", {
+ {"@user:example.com", {
+ {"U1Device1", makeDeviceInfo(u1Client)},
+ }},
+ }},
+ })) | withResponseDataKV("deviceKeys", job.dataJson("deviceKeys")));
+
+ ctx.dispatch(ProcessResponseAction{resp})
+ .then([&u](const EffectStatus &s) {
+ REQUIRE(s.success());
+ REQUIRE(s.dataJson("unsatisfied") == json::object({
+ {"users", json::array()},
+ {"devices", json::array()},
+ }));
+ u.io.stop();
+ });
+ u.io.run();
+ }
+
+ WHEN("missing device") {
+ auto resp = makeResponse("QueryKeys", withResponseJsonBody(json::object({
+ {"device_keys", {
+ {"@user:example.com", json::object()},
+ }},
+ })) | withResponseDataKV("deviceKeys", job.dataJson("deviceKeys")));
+
+ ctx.dispatch(ProcessResponseAction{resp})
+ .then([&u](const EffectStatus &s) {
+ REQUIRE(s.success());
+ REQUIRE(s.dataJson("unsatisfied") == json::object({
+ {"users", json::array()},
+ {"devices", json::array({{"@user:example.com", "U1Device1"}})},
+ }));
+ u.io.stop();
+ });
+ u.io.run();
+ }
+}
diff --git a/src/tests/client/sync-test.cpp b/src/tests/client/sync-test.cpp
--- a/src/tests/client/sync-test.cpp
+++ b/src/tests/client/sync-test.cpp
@@ -18,8 +18,10 @@
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <client/actions/sync.hpp>
+#include <client/actions/encryption.hpp>
#include <outbound-group-session.hpp>
#include "client-test-util.hpp"
+#include "action-mock-utils.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
@@ -764,3 +766,33 @@
auto t = std::get<SaveEventsRequested>(*it);
REQUIRE(t.timelineEvents["!726s6s6q:example.com"].size() == 2);
}
+
+TEST_CASE("Sync pops verification events", "[client][sync][encryption]")
+{
+ auto u = makeMockSdkUtil(makeClient(withCrypto(makeCrypto())));
+ auto md = u.getMockDispatcher(passDown<ProcessResponseAction>());
+ auto ctx = getMockContext(u.ph, md);
+ auto resp = makeResponse("Sync", withResponseJsonBody(R"({
+ "next_batch": "something",
+ "to_device": {"events": [{
+ "content": {
+ "from_device": "AliceDevice2",
+ "methods": [
+ "m.sas.v1"
+ ],
+ "timestamp": 1559598944869,
+ "transaction_id": "S0meUniqueAndOpaqueString"
+ },
+ "sender": "@alice:example.com",
+ "type": "m.key.verification.request"
+ }]}
+})"_json) | withResponseDataKV("is", "incremental"));
+ ctx.dispatch(ProcessResponseAction{resp})
+ .then([&u](const EffectStatus &s) {
+ REQUIRE(s.success());
+ REQUIRE(s.dataJson("verificationEvents").at("toDevice").size() == 1);
+ REQUIRE(u.sdk.client().toDevice().make().get().empty());
+ u.io.stop();
+ });
+ u.io.run();
+}
diff --git a/src/tests/client/verification-processing-test.cpp b/src/tests/client/verification-processing-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/client/verification-processing-test.cpp
@@ -0,0 +1,552 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <boost/asio.hpp>
+
+#include <client/client.hpp>
+#include <client/client-model.hpp>
+#include <client/actions/encryption.hpp>
+#include <crypto/verification-tracker.hpp>
+#include <crypto/verification-utils.hpp>
+#include <crypto.hpp>
+
+#include "client-test-util.hpp"
+#include "action-mock-utils.hpp"
+#include "factory.hpp"
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+namespace VCC = Kazv::VerificationCancelCodes;
+namespace VPS = Kazv::VerificationProcessStates;
+using namespace Kazv::VerificationEventTypes;
+using VTM = VerificationTrackerModel;
+
+namespace
+{
+ static Event withSender(Event e, std::string userId)
+ {
+ auto j = e.originalJson().get();
+ j["sender"] = userId;
+ return Event(j);
+ }
+
+ static ComposedModifier<ClientModel> withClientInfo(std::string userId, std::string deviceId)
+ {
+ return [userId, deviceId](ClientModel &m) {
+ m.userId = userId;
+ m.deviceId = deviceId;
+ };
+ }
+
+ static json makeDeviceInfo(const ClientModel &client)
+ {
+ auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
+ return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
+ }
+
+ struct VerificationTestSetup
+ {
+ std::string userIdA;
+ std::string userIdB;
+ std::string deviceIdA;
+ std::string deviceIdB;
+ MockSdkUtil utilA;
+ MockSdkUtil utilB;
+ json deviceInfoA;
+ json deviceInfoB;
+
+ VerificationTestSetup(boost::asio::io_context::executor_type ex)
+ : userIdA("@alice:example.com")
+ , userIdB("@bob:example.com")
+ , deviceIdA("AliceDevice")
+ , deviceIdB("BobDevice")
+ , utilA(ex, makeClient(withCrypto(makeCrypto()) | withClientInfo(userIdA, deviceIdA)))
+ , utilB(ex, makeClient(withCrypto(makeCrypto()) | withClientInfo(userIdB, deviceIdB)))
+ , deviceInfoA(makeDeviceInfo(lager::get<SdkModelCursorKey>(utilA.sdk.context())->get().client))
+ , deviceInfoB(makeDeviceInfo(lager::get<SdkModelCursorKey>(utilB.sdk.context())->get().client))
+ {
+ }
+
+ template<class PH, class Ctx>
+ auto handleKeyRequest([[maybe_unused]] PH &ph, Ctx &ctx, EnsureKeysFromDevicesAction a) -> typename MockSdkUtil::ContextT::PromiseT
+ {
+ if (!a.userIdToDeviceIdsMap.size()) {
+ return ctx.createResolvedPromise({});
+ }
+ auto userId = a.userIdToDeviceIdsMap.begin()->first;
+ auto deviceId = userId == userIdA ? deviceIdA : deviceIdB;
+ auto deviceInfo = userId == userIdA ? deviceInfoA : deviceInfoB;
+ auto body = json::object({
+ {"device_keys", {{userId, {
+ {deviceId, deviceInfo},
+ }}}},
+ });
+ auto resp = makeResponse(
+ "QueryKeys",
+ withResponseJsonBody(body)
+ | withResponseDataKV("deviceKeys", json(a.userIdToDeviceIdsMap)));
+ return ctx.dispatch(ProcessResponseAction{resp});
+ }
+ };
+}
+
+TEST_CASE("Client verification processing - complete verification flow", "[client][verification]")
+{
+ boost::asio::io_context io;
+
+ VerificationTestSetup setup(io.get_executor());
+
+ auto dispatcherA = setup.utilA.getMockDispatcher(
+ makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
+ return setup.handleKeyRequest(ph, ctx, action);
+ }),
+ returnEmpty<SendToDeviceMessageAction>(),
+ passDown<NotifyVerificationTrackerModelAction>()
+ );
+ auto dispatcherB = setup.utilB.getMockDispatcher(
+ makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
+ return setup.handleKeyRequest(ph, ctx, action);
+ }),
+ returnEmpty<SendToDeviceMessageAction>(),
+ passDown<NotifyVerificationTrackerModelAction>()
+ );
+
+ auto clientA = setup.utilA.getClient(dispatcherA);
+ auto clientB = setup.utilB.getClient(dispatcherB);
+
+ SECTION("Alice initiates, Bob accepts, both confirm")
+ {
+ // Alice initiates verification
+ clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 1);
+
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
+ dispatcherA.clear();
+
+ // Bob receives the request
+ return clientB.processVerificationEventsFromSync({requestEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
+ dispatcherB.clear();
+
+ // Bob signals ready
+ return clientB.readyForVerification(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
+
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ auto readyEvent = withSender(sentEvents.at(0).event, setup.userIdB);
+ dispatcherB.clear();
+
+ // Alice receives the ready event
+ return clientA.processVerificationEventsFromSync({readyEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
+
+ // Alice should have sent a start event
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ Event startEvent = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(startEvent.type() == tStart);
+ dispatcherA.clear();
+
+ // Bob receives the start event
+ return clientB.processVerificationEventsFromSync({startEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ // Bob should have sent an accept event
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ Event acceptEvent = withSender(sentEvents.at(0).event, setup.userIdB);
+ REQUIRE(acceptEvent.type() == tAccept);
+ dispatcherB.clear();
+
+ // Alice receives accept event
+ return clientA.processVerificationEventsFromSync({acceptEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ // Alice should have sent a key event
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ // Find key event from Alice
+ Event keyEventA = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(keyEventA.type() == tKey);
+ dispatcherA.clear();
+
+ // Bob receives Alice's key event
+ return clientB.processVerificationEventsFromSync({keyEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ // Bob should have sent a key event
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ // Find key event from Bob
+ Event keyEventB = withSender(sentEvents.at(0).event, setup.userIdB);
+ REQUIRE(keyEventB.type() == tKey);
+ dispatcherB.clear();
+
+ // Alice receives Bob's key event
+ return clientA.processVerificationEventsFromSync({keyEventB});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
+
+ auto stateA = lager::get<VerificationTracker>(setup.utilA.sdk.context()).model.processes.at(0).state;
+ auto stateB = lager::get<VerificationTracker>(setup.utilB.sdk.context()).model.processes.at(0).state;
+ REQUIRE(std::holds_alternative<VTM::ProcessCodeDisplayed>(stateA));
+ REQUIRE(std::holds_alternative<VTM::ProcessCodeDisplayed>(stateB));
+ REQUIRE(std::get<VTM::ProcessCodeDisplayed>(stateA).emojiIndices == std::get<VTM::ProcessCodeDisplayed>(stateB).emojiIndices);
+
+ // Alice confirms the SAS matches
+ return clientA.confirmVerificationSasMatch(setup.userIdB, setup.deviceIdB);
+ }).then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ auto macEventA = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(macEventA.type() == tMac);
+ dispatcherA.clear();
+
+ // Bob receives Alice's MAC
+ return clientB.processVerificationEventsFromSync({macEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ dispatcherB.clear();
+
+ // Bob confirms the SAS match
+ return clientB.confirmVerificationSasMatch(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 2);
+
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+
+ // Find MAC and done events from Bob
+ Event macEventB;
+ Event doneEventB;
+ for (auto se : sentEvents) {
+ if (se.event.type() == tMac) {
+ macEventB = withSender(se.event, setup.userIdB);
+ }
+ if (se.event.type() == tDone) {
+ doneEventB = withSender(se.event, setup.userIdB);
+ }
+ }
+ REQUIRE(macEventB.type() == tMac);
+ REQUIRE(doneEventB.type() == tDone);
+ dispatcherB.clear();
+
+ // Alice receives Bob's MAC and done events
+ return clientA.processVerificationEventsFromSync({macEventB, doneEventB});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ // Alice should send a done event
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ Event doneEventA = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(doneEventA.type() == tDone);
+ dispatcherA.clear();
+
+ // Bob receives Alice's done event
+ return clientB.processVerificationEventsFromSync({doneEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ io.stop();
+ });
+ io.run();
+ }
+
+ SECTION("Alice initiates, Bob cancels")
+ {
+ // Alice initiates verification
+ clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
+ dispatcherA.clear();
+
+ // Bob receives the request
+ return clientB.processVerificationEventsFromSync({requestEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ dispatcherB.clear();
+
+ // Bob cancels
+ return clientB.cancelVerification(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
+
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ auto cancelEvent = withSender(sentEvents.at(0).event, setup.userIdB);
+ REQUIRE(cancelEvent.type() == tCancel);
+ REQUIRE(cancelEvent.content().get().at("code") == VCC::userCancel);
+ dispatcherB.clear();
+
+ // Alice receives the cancel event
+ return clientA.processVerificationEventsFromSync({cancelEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ // After receiving a cancel, Alice should not send another cancel
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
+ io.stop();
+ });
+ io.run();
+ }
+
+ SECTION("Alice initiates, Bob accepts, both deny")
+ {
+ // Alice initiates verification
+ clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 1);
+
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
+ dispatcherA.clear();
+
+ // Bob receives the request
+ return clientB.processVerificationEventsFromSync({requestEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
+ dispatcherB.clear();
+
+ // Bob signals ready
+ return clientB.readyForVerification(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
+
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ auto readyEvent = withSender(sentEvents.at(0).event, setup.userIdB);
+ dispatcherB.clear();
+
+ // Alice receives the ready event
+ return clientA.processVerificationEventsFromSync({readyEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
+
+ // Alice should have sent a start event
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ Event startEvent = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(startEvent.type() == tStart);
+ dispatcherA.clear();
+
+ // Bob receives the start event
+ return clientB.processVerificationEventsFromSync({startEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ // Bob should have sent an accept event
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ Event acceptEvent = withSender(sentEvents.at(0).event, setup.userIdB);
+ REQUIRE(acceptEvent.type() == tAccept);
+ dispatcherB.clear();
+
+ // Alice receives accept event
+ return clientA.processVerificationEventsFromSync({acceptEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+
+ // Alice should have sent a key event
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ // Find key event from Alice
+ Event keyEventA = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(keyEventA.type() == tKey);
+ dispatcherA.clear();
+
+ // Bob receives Alice's key event
+ return clientB.processVerificationEventsFromSync({keyEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ // Bob should have sent a key event
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ REQUIRE(sentEvents.size() == 1);
+
+ // Find key event from Bob
+ Event keyEventB = withSender(sentEvents.at(0).event, setup.userIdB);
+ REQUIRE(keyEventB.type() == tKey);
+ dispatcherB.clear();
+
+ // Alice receives Bob's key event
+ return clientA.processVerificationEventsFromSync({keyEventB});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
+
+ // Alice denies the SAS matches
+ return clientA.denyVerificationSasMatch(setup.userIdB, setup.deviceIdB);
+ }).then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
+ auto cancelEventA = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(cancelEventA.type() == tCancel);
+ dispatcherA.clear();
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ dispatcherB.clear();
+
+ // Bob denies the SAS match
+ return clientB.denyVerificationSasMatch(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() >= 1);
+
+ auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
+ auto cancelEventB = withSender(sentEvents.at(0).event, setup.userIdA);
+ REQUIRE(cancelEventB.type() == tCancel);
+ dispatcherB.clear();
+ io.stop();
+ });
+ io.run();
+ }
+}
+
+TEST_CASE("Client verification processing - tracker model updates", "[client][verification]")
+{
+ boost::asio::io_context io;
+
+ VerificationTestSetup setup(io.get_executor());
+
+ auto dispatcherA = setup.utilA.getMockDispatcher(
+ returnEmpty<EnsureKeysFromDevicesAction>(),
+ returnEmpty<SendToDeviceMessageAction>(),
+ returnEmpty<NotifyVerificationTrackerModelAction>()
+ );
+ auto clientA = setup.utilA.getClient(dispatcherA);
+
+ // Process a verification request
+ Event requestEvent = R"({
+ "content": {
+ "from_device": "BobDevice",
+ "methods": ["m.sas.v1"],
+ "timestamp": 0,
+ "transaction_id": "testTxnId"
+ },
+ "type": "m.key.verification.request",
+ "sender": "@bob:example.com"
+ })"_json;
+
+ clientA.processVerificationEventsFromSync({requestEvent})
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
+ io.stop();
+ });
+ io.run();
+}
+
+TEST_CASE("Client verification processing - edge cases", "[client][verification]")
+{
+ boost::asio::io_context io;
+ VerificationTestSetup setup(io.get_executor());
+
+ SECTION("Multiple verification events processed together")
+ {
+ auto dispatcherA = setup.utilA.getMockDispatcher(
+ returnEmpty<SendToDeviceMessageAction>(),
+ returnEmpty<NotifyVerificationTrackerModelAction>()
+ );
+ auto clientA = setup.utilA.getClient(dispatcherA);
+
+ Event requestEvent1 = R"({
+ "content": {
+ "from_device": "BobDevice",
+ "methods": ["m.sas.v1"],
+ "timestamp": 0,
+ "transaction_id": "txn1"
+ },
+ "type": "m.key.verification.request",
+ "sender": "@bob:example.com"
+ })"_json;
+
+ Event requestEvent2 = R"({
+ "content": {
+ "from_device": "BobDevice2",
+ "methods": ["m.sas.v1"],
+ "timestamp": 0,
+ "transaction_id": "txn1"
+ },
+ "type": "m.key.verification.request",
+ "sender": "@bob:example.com"
+ })"_json;
+
+ clientA.processVerificationEventsFromSync({requestEvent1, requestEvent2})
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ io.stop();
+ });
+ io.run();
+ }
+
+ SECTION("Cancel non-existent verification process")
+ {
+ auto dispatcherA = setup.utilA.getMockDispatcher(
+ returnEmpty<SendToDeviceMessageAction>()
+ );
+ auto clientA = setup.utilA.getClient(dispatcherA);
+
+ // Try to cancel a verification that doesn't exist
+ clientA.cancelVerification(setup.userIdB, setup.deviceIdB)
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ // Should still succeed, but no events sent
+ REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
+ io.stop();
+ });
+ io.run();
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Mar 26, 6:08 PM (5 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1232106
Default Alt Text
D295.1774573732.diff (58 KB)

Event Timeline