Page MenuHomePhorge

D295.1774522514.diff
No OneTemporary

Size
27 KB
Referenced Files
None
Subscribers
None

D295.1774522514.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,28 @@
* 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 (readyForVerification(),
+ * cancelVerification(), confirmVerificationSasMatch(),
+ * denyVerificationSasMatch()) will also automatically send outbound
+ * events according to the result returned from the VerificationTracker.
+ *
+ * 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 +84,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 +680,64 @@
*/
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);
+
+ /**
+ * 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);
+
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"
@@ -309,6 +309,36 @@
return p1;
}
+ auto Client::processVerificationEventsFromSync(EventList toDeviceEvents) 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(true)
+ .then([that=toEventLoop(), ves=toDeviceEvents](auto &&) {
+ auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
+ auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::system_clock::now().time_since_epoch()
+ ).count();
+ 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::syncForever(std::optional<int> retryTime) const -> void
{
KAZV_VERIFY_THREAD_ID();
@@ -355,8 +385,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/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();
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Mar 26, 3:55 AM (35 m, 35 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1232276
Default Alt Text
D295.1774522514.diff (27 KB)

Event Timeline