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