Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F52257518
D295.1774573732.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
58 KB
Referenced Files
None
Subscribers
None
D295.1774573732.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D295: Implement Client integration for verification
Attached
Detach File
Event Timeline
Log In to Comment