Page MenuHomePhorge

No OneTemporary

Size
42 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
index 2c9ab3c..21d0386 100644
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -1,417 +1,473 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/algorithm.hpp>
#include <lager/util.hpp>
#include <lager/context.hpp>
#include <functional>
#include <zug/transducer/filter.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "immer-utils.hpp"
#include "json-utils.hpp"
#include "client-model.hpp"
#include "actions/states.hpp"
#include "actions/auth.hpp"
#include "actions/membership.hpp"
#include "actions/paginate.hpp"
#include "actions/send.hpp"
#include "actions/states.hpp"
#include "actions/account-data.hpp"
#include "actions/sync.hpp"
#include "actions/ephemeral.hpp"
#include "actions/content.hpp"
#include "actions/encryption.hpp"
#include "actions/profile.hpp"
namespace Kazv
{
auto ClientModel::update(ClientModel m, Action a) -> Result
{
auto oldClient = m;
auto oldDeviceLists = m.deviceLists;
auto [newClient, effect] = lager::match(std::move(a))(
[&](RoomListAction a) -> Result {
m.roomList = RoomListModel::update(std::move(m.roomList), a);
return {std::move(m), lager::noop};
},
[&](ResubmitJobAction a) -> Result {
m.addJob(std::move(a.job));
return { std::move(m), lager::noop };
},
[&](auto a) -> decltype(updateClient(m, a)) {
return updateClient(m, a);
},
#define RESPONSE_FOR(_jobId) \
if (r.jobId() == #_jobId) { \
return processResponse(m, _jobId##Response{std::move(r)}); \
}
[&](ProcessResponseAction a) -> Result {
auto r = std::move(a.response);
// auth
RESPONSE_FOR(Login);
RESPONSE_FOR(GetWellknown);
RESPONSE_FOR(GetVersions);
// paginate
RESPONSE_FOR(GetRoomEvents);
// sync
RESPONSE_FOR(Sync);
RESPONSE_FOR(DefineFilter);
// membership
RESPONSE_FOR(CreateRoom);
RESPONSE_FOR(InviteUser);
RESPONSE_FOR(JoinRoomById);
RESPONSE_FOR(JoinRoom);
RESPONSE_FOR(LeaveRoom);
RESPONSE_FOR(ForgetRoom);
RESPONSE_FOR(Kick);
RESPONSE_FOR(Ban);
RESPONSE_FOR(Unban);
// send
RESPONSE_FOR(SendMessage);
RESPONSE_FOR(SendToDevice);
RESPONSE_FOR(RedactEvent);
// states
RESPONSE_FOR(GetRoomState);
RESPONSE_FOR(SetRoomStateWithKey);
RESPONSE_FOR(GetRoomStateWithKey);
// account data
RESPONSE_FOR(SetAccountData);
RESPONSE_FOR(SetAccountDataPerRoom);
// ephemeral
RESPONSE_FOR(SetTyping);
RESPONSE_FOR(PostReceipt);
RESPONSE_FOR(SetReadMarker);
// content
RESPONSE_FOR(UploadContent);
RESPONSE_FOR(GetContent);
RESPONSE_FOR(GetContentThumbnail);
// encryption
RESPONSE_FOR(UploadKeys);
RESPONSE_FOR(QueryKeys);
RESPONSE_FOR(ClaimKeys);
// profile
RESPONSE_FOR(GetUserProfile);
RESPONSE_FOR(SetAvatarUrl);
RESPONSE_FOR(SetDisplayName);
m.addTrigger(UnrecognizedResponse{std::move(r)});
return { std::move(m), lager::noop };
}
#undef RESPONSE_FOR
);
newClient.maybeRotateSessions(oldClient);
return { std::move(newClient), std::move(effect) };
}
std::pair<Event, std::optional<std::string>> ClientModel::megOlmEncrypt(
Event e, std::string roomId, Timestamp timeMs, RandomData random)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return { e, std::nullopt };
}
auto j = e.originalJson().get();
auto r = roomList[roomId];
if (! r.encrypted) {
kzo.client.dbg() << "The room " << roomId
<< " is not encrypted, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
auto desc = r.sessionRotateDesc();
auto keyOpt = std::optional<std::string>{};
if (r.shouldRotateSessionKey) {
kzo.client.dbg() << "We should rotate this session." << std::endl;
keyOpt = withCrypto([&](auto &c) { return c.rotateMegOlmSessionWithRandom(random, timeMs, roomId); });
} else {
keyOpt = withCrypto([&](auto &c) { return c.rotateMegOlmSessionWithRandomIfNeeded(random, timeMs, roomId, desc); });
}
// we no longer need to rotate session
// until next time a device change happens
roomList.rooms = std::move(roomList.rooms)
.update(roomId, [](auto r) { r.shouldRotateSessionKey = false; return r; });
auto relation = hasAtThat(j["content"], "m.relates_to", &json::is_object) ? j["content"]["m.relates_to"] : json(nullptr);
// so that Crypto::encryptMegOlm() can find room id
j["room_id"] = roomId;
auto content = withCrypto([&](auto &c) { return c.encryptMegOlm(j); });
j["type"] = "m.room.encrypted";
j["content"] = std::move(content);
j["content"]["device_id"] = deviceId;
// add relationship to plaintext
if (relation.is_object()) {
j["content"]["m.relates_to"] = relation;
}
kzo.client.dbg() << "Encrypted json is " << j.dump() << std::endl;
kzo.client.dbg() << "Session key is " << (keyOpt ? keyOpt.value() : "<not rotated>") << std::endl;
return { Event(JsonWrap(j)), keyOpt };
}
Event ClientModel::olmEncrypt(Event e,
immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return e;
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return e;
}
auto origJson = e.originalJson().get();
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", constCrypto().curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
for (auto [userId, devices] : userIdToDeviceIdMap) {
for (auto dev : devices) {
auto devInfoOpt = deviceLists.get(userId, dev);
if (! devInfoOpt) {
continue;
}
auto devInfo = devInfoOpt.value();
auto jsonForThisDevice = origJson;
jsonForThisDevice["sender"] = this->userId;
jsonForThisDevice["recipient"] = userId;
jsonForThisDevice["recipient_keys"] = json{
{CryptoConstants::ed25519, devInfo.ed25519Key}
};
jsonForThisDevice["keys"] = json{
{CryptoConstants::ed25519, constCrypto().ed25519IdentityKey()}
};
encJson["content"]["ciphertext"]
.merge_patch(withCrypto([&](auto &c) { return c.encryptOlmWithRandom(random, jsonForThisDevice, devInfo.curve25519Key); }));
random.erase(0, Crypto::encryptOlmMaxRandomSize());
}
}
return Event(JsonWrap(encJson));
}
+ immer::map<std::string, immer::map<std::string, Event>> ClientModel::olmEncryptSplit(
+ Event e,
+ immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random)
+ {
+ using ResT = immer::map<std::string, immer::map<std::string, Event>>;
+ if (!crypto) {
+ kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
+ return ResT{};
+ }
+
+ if (e.encrypted()) {
+ kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
+ return ResT{};
+ }
+
+ auto origJson = e.originalJson().get();
+
+ auto encJson = json::object();
+ encJson["content"] = json{
+ {"algorithm", CryptoConstants::olmAlgo},
+ {"ciphertext", json::object()},
+ {"sender_key", constCrypto().curve25519IdentityKey()},
+ };
+
+ encJson["type"] = "m.room.encrypted";
+
+ ResT messages;
+
+ for (auto [userId, devices] : userIdToDeviceIdMap) {
+ messages = std::move(messages).set(userId, immer::map<std::string, Event>());
+ for (auto dev : devices) {
+ auto devInfoOpt = deviceLists.get(userId, dev);
+ if (! devInfoOpt) {
+ continue;
+ }
+ auto devInfo = devInfoOpt.value();
+ auto jsonForThisDevice = origJson;
+ jsonForThisDevice["sender"] = this->userId;
+ jsonForThisDevice["recipient"] = userId;
+ jsonForThisDevice["recipient_keys"] = json{
+ {CryptoConstants::ed25519, devInfo.ed25519Key}
+ };
+ jsonForThisDevice["keys"] = json{
+ {CryptoConstants::ed25519, constCrypto().ed25519IdentityKey()}
+ };
+ auto thisEventJson = encJson;
+ thisEventJson["content"]["ciphertext"]
+ .merge_patch(withCrypto([&](auto &c) { return c.encryptOlmWithRandom(random, jsonForThisDevice, devInfo.curve25519Key); }));
+ random.erase(0, Crypto::encryptOlmMaxRandomSize());
+ messages = setIn(std::move(messages), Event(thisEventJson), userId, dev);
+ }
+ }
+
+ return messages;
+ }
+
immer::flex_vector<std::string /* deviceId */> ClientModel::devicesToSendKeys(std::string userId) const
{
auto trustLevelNeeded = this->trustLevelNeededToSendKeys;
// XXX: preliminary approach
auto shouldSendP = [=](auto deviceInfo, auto /* deviceMap */) {
return deviceInfo.trustLevel >= trustLevelNeeded;
};
auto devices = deviceLists.devicesFor(userId);
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([=](auto n) {
auto [id, dev] = n;
return shouldSendP(dev, devices);
})
| zug::map([=](auto n) {
return n.first;
}),
devices);
}
std::size_t ClientModel::numOneTimeKeysNeeded() const
{
const auto &crypto = constCrypto();
// Keep half of max supported number of keys
int numUploadedKeys = crypto.uploadedOneTimeKeysCount(CryptoConstants::signedCurve25519);
int numKeysNeeded = crypto.maxNumberOfOneTimeKeys() / 2
- numUploadedKeys;
// Subtract the number of existing one-time keys, in case
// the previous upload was not successful.
int numKeysToGenerate = numKeysNeeded - crypto.numUnpublishedOneTimeKeys();
return numKeysToGenerate;
}
std::size_t EncryptMegOlmEventAction::maxRandomSize()
{
return Crypto::rotateMegOlmSessionRandomSize();
}
std::size_t EncryptMegOlmEventAction::minRandomSize()
{
return 0;
}
std::size_t EncryptOlmEventAction::randomSize(EncryptOlmEventAction::UserIdToDeviceIdMap devices)
{
auto singleRandomSize = Crypto::encryptOlmMaxRandomSize();
auto deviceNum = accumulate(devices, std::size_t{},
[](auto counter, auto pair) { return counter + pair.second.size(); });
return deviceNum * singleRandomSize;
}
std::size_t GenerateAndUploadOneTimeKeysAction::randomSize(std::size_t numToGen)
{
return Crypto::genOneTimeKeysRandomSize(numToGen);
}
std::size_t ClaimKeysAction::randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend)
{
auto singleRandomSize = Crypto::createOutboundSessionRandomSize();
auto deviceNum = accumulate(devicesToSend, std::size_t{},
[](auto counter, auto pair) { return counter + pair.second.size(); });
return deviceNum * singleRandomSize;
}
void ClientModel::maybeRotateSessions(ClientModel oldClient)
{
auto roomIds = intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](const auto &pair) {
return pair.second.encrypted && !pair.second.shouldRotateSessionKey;
})
| zug::map([](const auto &pair) { return pair.first; }),
roomList.rooms
);
auto markRotate = [this](const auto &roomId) {
roomList.rooms =
std::move(roomList.rooms)
.update(roomId, [](auto room) {
room.shouldRotateSessionKey = true;
return room;
});
};
// Rotate megolm keys for rooms whose users' device list has changed
auto changedUsers = deviceLists.diff(oldClient.deviceLists);
if (! changedUsers.empty()) {
for (auto roomId : roomIds) {
auto it = std::find_if(changedUsers.begin(), changedUsers.end(),
[=](auto userId) { return roomList.rooms[roomId].hasUser(userId); });
if (it != changedUsers.end()) {
kzo.client.dbg() << "rotate keys for room " << roomId << std::endl;
markRotate(roomId);
}
}
}
roomIds = intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](const auto &pair) {
return pair.second.encrypted && !pair.second.shouldRotateSessionKey;
})
| zug::map([](const auto &pair) { return pair.first; }),
roomList.rooms
);
for (auto roomId : roomIds) {
auto userIds = roomList.rooms[roomId].joinedMemberIds();
auto devicesNotChanged = [oldClient, this](const auto &userId) {
return oldClient.devicesToSendKeys(userId) == devicesToSendKeys(userId);
};
// if any user has the device changes
if (!immer::all_of(userIds, devicesNotChanged)) {
kzo.client.dbg() << "rotate keys for room " << roomId << std::endl;
markRotate(roomId);
}
}
}
auto ClientModel::directRoomMap() const -> immer::map<std::string, std::string>
{
auto directs = accountData["m.direct"].content().get();
auto directItems = directs.items();
return std::accumulate(directItems.begin(), directItems.end(), immer::map<std::string, std::string>(),
[](auto acc, const auto &cur) {
auto [userId, roomIds] = cur;
if (!roomIds.is_array()) {
return acc;
}
for (auto roomId : roomIds) {
if (roomId.is_string()) {
acc = std::move(acc).set(roomId.template get<std::string>(), userId);
}
}
return acc;
}
);
}
auto ClientModel::roomIdsUnderTag(std::string tagId) const -> immer::map<std::string, double>
{
return std::accumulate(
roomList.rooms.begin(), roomList.rooms.end(),
immer::map<std::string, double>{},
[tagId](auto acc, auto cur) {
auto [roomId, room] = cur;
auto tags = room.tags();
if (tags.count(tagId)) {
acc = std::move(acc).set(roomId, tags[tagId]);
}
return acc;
}
);
}
auto ClientModel::roomIdsByTagId() const -> immer::map<std::string, immer::map<std::string, double>>
{
return std::accumulate(
roomList.rooms.begin(), roomList.rooms.end(),
immer::map<std::string, immer::map<std::string, double>>{},
[](auto acc, auto cur) {
auto [roomId, room] = cur;
auto tags = room.tags();
if (tags.empty()) {
acc = setIn(std::move(acc), ROOM_TAG_DEFAULT_ORDER, "", roomId);
} else {
for (const auto &[tagId, order] : tags) {
acc = setIn(std::move(acc), order, tagId, roomId);
}
}
return acc;
}
);
}
const Crypto &ClientModel::constCrypto() const
{
return crypto.value().get();
}
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index ca952cc..2f6bfaf 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,651 +1,656 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <string>
#include <optional>
#include <lager/context.hpp>
#include <boost/hana.hpp>
#include <serialization/std-optional.hpp>
#include <csapi/sync.hpp>
#include <file-desc.hpp>
#include <crypto.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
#include <serialization/immer-array.hpp>
#include "clientfwd.hpp"
#include "device-list-tracker.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
inline const std::string DEFTXNID{"0"};
enum RoomVisibility
{
Private,
Public,
};
enum CreateRoomPreset
{
PrivateChat,
PublicChat,
TrustedPrivateChat,
};
enum ThumbnailResizingMethod
{
Crop,
Scale,
};
struct ClientModel
{
std::string serverUrl;
std::string userId;
std::string token;
std::string deviceId;
bool loggedIn{false};
bool syncing{false};
bool shouldSync{true};
int firstRetryMs{1000};
int retryTimeFactor{2};
int maxRetryMs{30 * 1000};
int syncTimeoutMs{20000};
std::string initialSyncFilterId;
std::string incrementalSyncFilterId;
std::optional<std::string> syncToken;
RoomListModel roomList;
immer::map<std::string /* sender */, Event> presence;
immer::map<std::string /* type */, Event> accountData;
std::string nextTxnId{DEFTXNID};
immer::flex_vector<BaseJob> nextJobs;
immer::flex_vector<KazvEvent> nextTriggers;
EventList toDevice;
std::optional<immer::box<Crypto>> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
DeviceTrustLevel trustLevelNeededToSendKeys{DeviceTrustLevel::Unseen};
immer::flex_vector<std::string /* deviceId */> devicesToSendKeys(std::string userId) const;
/// rotate sessions for a room if there is a user in the room with
/// devicesToSendKeys changes
void maybeRotateSessions(ClientModel oldClient);
std::pair<Event, std::optional<std::string> /* sessionKey */>
megOlmEncrypt(Event e, std::string roomId, Timestamp timeMs, RandomData random);
/// precondition: the one-time keys for those devices must already be claimed
+ [[deprecated("Use olmEncryptSplit instead")]]
Event olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random);
+ /// precondition: the one-time keys for those devices must already be claimed
+ /// @return A map from user id to device id to encrypted event for that device
+ immer::map<std::string, immer::map<std::string, Event>> olmEncryptSplit(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random);
+
/// @return number of one-time keys we need to generate
std::size_t numOneTimeKeysNeeded() const;
/// @return the mapping from room id to user id of direct rooms
auto directRoomMap() const -> immer::map<std::string, std::string>;
auto roomIdsUnderTag(std::string tagId) const -> immer::map<std::string, double>;
auto roomIdsByTagId() const -> immer::map<std::string, immer::map<std::string, double>>;
/// Get the const reference of crypto of this client.
///
/// `crypto.has_value()` must be true.
const Crypto &constCrypto() const;
/// Do func with crypto, returning its return value.
///
/// `crypto.has_value()` must be true.
template<class Func>
auto withCrypto(Func &&func) -> std::decay_t<std::invoke_result_t<Func &&, Crypto &>>
{
using ResT = std::decay_t<std::invoke_result_t<Func &&, Crypto &>>;
if constexpr (std::is_same_v<ResT, void>) {
crypto = std::move(crypto).value()
.update([f=std::forward<Func>(func)](Crypto c) mutable {
std::forward<Func>(f)(c);
return c;
});
} else {
std::optional<ResT> res;
crypto = std::move(crypto).value()
.update([f=std::forward<Func>(func), &res](Crypto c) mutable {
res = std::forward<Func>(f)(c);
return c;
});
return std::move(res).value();
}
}
// helpers
template<class Job>
struct MakeJobT
{
template<class ...Args>
constexpr auto make(Args &&...args) const {
if constexpr (Job::needsAuth()) {
return Job(
serverUrl,
token,
std::forward<Args>(args)...);
} else {
return Job(
serverUrl,
std::forward<Args>(args)...);
}
}
std::string serverUrl;
std::string token;
};
template<class Job>
constexpr auto job() const {
return MakeJobT<Job>{serverUrl, token};
}
inline void addJob(BaseJob j) {
nextJobs = std::move(nextJobs).push_back(std::move(j));
}
inline auto popAllJobs() {
auto jobs = std::move(nextJobs);
nextJobs = DEFVAL;
return jobs;
};
inline void addTrigger(KazvEvent t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvEvent> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
static Result update(ClientModel m, Action a);
};
// actions:
struct LoginAction {
std::string serverUrl;
std::string username;
std::string password;
std::optional<std::string> deviceName;
};
struct TokenLoginAction
{
std::string serverUrl;
std::string username;
std::string token;
std::string deviceId;
};
struct LogoutAction {};
struct GetWellknownAction
{
std::string userId;
};
struct GetVersionsAction
{
std::string serverUrl;
};
struct SyncAction {};
struct SetShouldSyncAction
{
bool shouldSync;
};
struct PaginateTimelineAction
{
std::string roomId;
/// Must be where the Gap is
std::string fromEventId;
std::optional<int> limit;
};
struct SendMessageAction
{
std::string roomId;
Event event;
std::optional<std::string> txnId{std::nullopt};
};
struct SendStateEventAction
{
std::string roomId;
Event event;
};
/**
* Saves an local echo.
*
* After dispatching this action, the result should be such that
* `result.dataStr("txnId")` contains the transaction id to be used
* in SendMessageAction.
*/
struct SaveLocalEchoAction
{
/// The room id
std::string roomId;
/// The event to send
Event event;
/// The chosen txnId for this event. If not specified, generate from the current ClientModel.
std::optional<std::string> txnId{std::nullopt};
};
/**
* Updates the status of an local echo.
*
* After dispatching this action, the local echo's status will be
* set to the one described in the action.
*/
struct UpdateLocalEchoStatusAction
{
/// The room id.
std::string roomId;
/// The chosen txnId for this event.
std::string txnId;
/// The updated status of this local echo.
LocalEchoDesc::Status status;
};
struct RedactEventAction
{
std::string roomId;
std::string eventId;
std::optional<std::string> reason;
};
struct CreateRoomAction
{
using Visibility = RoomVisibility;
using Preset = CreateRoomPreset;
Visibility visibility;
std::optional<std::string> roomAliasName;
std::optional<std::string> name;
std::optional<std::string> topic;
immer::array<std::string> invite;
//immer::array<Invite3pid> invite3pid;
std::optional<std::string> roomVersion;
JsonWrap creationContent;
immer::array<Event> initialState;
std::optional<Preset> preset;
std::optional<bool> isDirect;
JsonWrap powerLevelContentOverride;
};
struct GetRoomStatesAction
{
std::string roomId;
};
struct GetStateEventAction
{
std::string roomId;
std::string type;
std::string stateKey;
};
struct InviteToRoomAction
{
std::string roomId;
std::string userId;
};
struct JoinRoomByIdAction
{
std::string roomId;
};
struct JoinRoomAction
{
std::string roomIdOrAlias;
immer::array<std::string> serverName;
};
struct LeaveRoomAction
{
std::string roomId;
};
struct ForgetRoomAction
{
std::string roomId;
};
struct KickAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct BanAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct UnbanAction
{
std::string roomId;
std::string userId;
};
struct SetAccountDataPerRoomAction
{
std::string roomId;
Event accountDataEvent;
};
struct SetTypingAction
{
std::string roomId;
bool typing;
std::optional<int> timeoutMs;
};
struct PostReceiptAction
{
std::string roomId;
std::string eventId;
};
struct SetReadMarkerAction
{
std::string roomId;
std::string eventId;
};
struct UploadContentAction
{
FileDesc content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
std::string uploadId; // to be used by library users
};
struct DownloadContentAction
{
std::string mxcUri;
std::optional<FileDesc> downloadTo;
};
struct DownloadThumbnailAction
{
std::string mxcUri;
int width;
int height;
std::optional<ThumbnailResizingMethod> method;
std::optional<bool> allowRemote;
std::optional<FileDesc> downloadTo;
};
struct ResubmitJobAction
{
BaseJob job;
};
struct ProcessResponseAction
{
Response response;
};
struct PostInitialFiltersAction
{
};
struct SetAccountDataAction
{
Event accountDataEvent;
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
std::optional<std::string> txnId{std::nullopt};
};
/**
* Send multiple to device messages.
*
* Due to limitations of the spec, the type of the Events must be the same.
*/
struct SendMultipleToDeviceMessagesAction
{
/// A map from user id to device id to the event.
immer::map<std::string, immer::map<std::string, Event>> userToDeviceToEventMap;
/// An optional transaction id. Will be generated if not provided.
std::optional<std::string> txnId{std::nullopt};
};
struct UploadIdentityKeysAction
{
};
/**
* The action to generate one-time keys.
*
* `random.size()` must be at least `randomSize(numToGen)`.
*
* This action will not generate keys exceeding the local limit of olm.
*/
struct GenerateAndUploadOneTimeKeysAction
{
/// @return The size of random needed to generate
/// `numToGen` one-time keys
static std::size_t randomSize(std::size_t numToGen);
/// The number of keys to generate
std::size_t numToGen;
/// The random data used to generate keys
RandomData random;
};
struct QueryKeysAction
{
bool isInitialSync;
};
struct ClaimKeysAction
{
static std::size_t randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend);
std::string roomId;
std::string sessionId;
std::string sessionKey;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
RandomData random;
};
/**
* The action to encrypt an megolm event for a room.
*
* If the action is successful, the result `r` will
* be such that `r.dataJson("encrypted")` contains the encrypted event *json*.
*
* If the megolm session is rotated, `r.dataStr("key")` will contain the key
* of the megolm session. Otherwise, `r.data().contains("key")` will be false.
*
* The Action may fail due to insufficient random data,
* when the megolm session needs to be rotated.
* In this case, the reducer for the Action will fail,
* and its result `r` will be such that
* `r.dataStr("reason") == "NotEnoughRandom"`.
* The user needs to provide random data of
* at least size `maxRandomSize()`.
*
*/
struct EncryptMegOlmEventAction
{
static std::size_t maxRandomSize();
static std::size_t minRandomSize();
/// The id of the room to encrypt for.
std::string roomId;
/// The event to encrypt.
Event e;
/// The timestamp, to determine whether the session should expire.
Timestamp timeMs;
/// Random data for the operation. Must be of at least size
/// `minRandomSize()`. If this is a retry of the previous operation
/// due to NotEnoughRandom, it must be of at least size `maxRandomSize()`.
RandomData random;
};
/**
* The action to encrypt events with olm for multiple devices.
*
* If the action is successful,
* The result `r` will be such that `r.dataJson("encrypted")`
* contains the json of the encrypted event.
*/
struct EncryptOlmEventAction
{
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
static std::size_t randomSize(UserIdToDeviceIdMap devices);
/// Devices to encrypt for.
UserIdToDeviceIdMap devices;
/// The event to encrypt.
Event e;
/// The random data for the encryption. Must be of at least
/// size `randomSize(devices)`.
RandomData random;
};
struct SetDeviceTrustLevelAction
{
std::string userId;
std::string deviceId;
DeviceTrustLevel trustLevel;
};
struct SetTrustLevelNeededToSendKeysAction
{
DeviceTrustLevel trustLevel;
};
/// Encrypt room key as olm and add it to the room's
/// pending keyshare slots.
/// This is to ensure atomicity and that we do not lose an olm-encrypted event.
struct PrepareForSharingRoomKeyAction
{
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
/// The room to share the key event in.
std::string roomId;
/// Devices to encrypt for.
UserIdToDeviceIdMap devices;
/// The key event to encrypt.
Event e;
/// The random data for the encryption. Must be of at least
/// size `EncryptOlmEventAction::randomSize(devices)`.
RandomData random;
};
struct GetUserProfileAction
{
std::string userId;
};
struct SetAvatarUrlAction
{
std::optional<std::string> avatarUrl;
};
struct SetDisplayNameAction
{
std::optional<std::string> displayName;
};
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const version)
{
bool dummySyncing{false};
ar
& m.serverUrl
& m.userId
& m.token
& m.deviceId
& m.loggedIn
& dummySyncing
& m.firstRetryMs
& m.retryTimeFactor
& m.maxRetryMs
& m.syncTimeoutMs
& m.initialSyncFilterId
& m.incrementalSyncFilterId
& m.syncToken
& m.roomList
& m.presence
& m.accountData
& m.nextTxnId
& m.toDevice;
// version <= 1 uses std::optional<Crypto>
// while version >= 2 uses std::optional<immer::box<Crypto>>
if (version >= 2) {
ar & m.crypto;
} else {
if constexpr (typename Archive::is_loading()) {
std::optional<Crypto> crypto;
ar >> crypto;
if (crypto.has_value()) {
m.crypto = immer::box<Crypto>(std::move(crypto).value());
}
}
// otherwise is_saving, which will always use the latest version
// this is unreachable
}
ar
& m.identityKeysUploaded
& m.deviceLists
;
if (version >= 1) { ar & m.trustLevelNeededToSendKeys; }
}
}
BOOST_CLASS_VERSION(Kazv::ClientModel, 2)
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
index f25b061..e4a3fd8 100644
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -1,93 +1,142 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
+#include <client/actions/encryption.hpp>
#include <client-model.hpp>
#include "client-test-util.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
TEST_CASE("PrepareForSharingRoomKeyAction: adds the encrypted event to pending events", "[client][encryption]")
{
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto event = Event{json{
{"type", "m.room_key"},
{"content", {{"some", "thing"}}},
}};
auto [next, dontCareEffect] = ClientModel::update(m, PrepareForSharingRoomKeyAction{"!exampleroomid:example.com", {}, event, {}});
auto nextRoom = next.roomList.rooms.at("!exampleroomid:example.com");
REQUIRE(nextRoom.pendingRoomKeyEvents.size() == 1);
REQUIRE(nextRoom.pendingRoomKeyEvents[0].event.encrypted());
}
TEST_CASE("encrypted event will keep a copy of m.relates_to in plaintext", "[client][encryption]")
{
auto room = makeRoom(withRoomEncrypted(true));
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto eventToEncrypt = makeEvent(
withEventType("m.room.message")
| withEventRelationship("moe.kazv.mxc.custom-rel-type", "$some-event-id")
);
auto [encryptedEvent, maybeKey] = client.megOlmEncrypt(
eventToEncrypt,
room.roomId,
0,
genRandomData(EncryptMegOlmEventAction::maxRandomSize())
);
// because we do not have session key yet, it should always be rotated
REQUIRE(maybeKey.has_value());
// check we can still access relationship
REQUIRE(encryptedEvent.relationship() == std::pair<std::string, std::string>{"moe.kazv.mxc.custom-rel-type", "$some-event-id"});
// check that the relationship is also in plaintext
REQUIRE(encryptedEvent.originalJson().get()["content"]["m.relates_to"] == json{
{"rel_type", "moe.kazv.mxc.custom-rel-type"},
{"event_id", "$some-event-id"},
});
}
TEST_CASE("encrypting event without relationship should not put m.relates_to key in plaintext", "[client][encryption]")
{
auto room = makeRoom(withRoomEncrypted(true));
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto eventToEncrypt = makeEvent(
withEventType("m.room.message")
);
auto [encryptedEvent, maybeKey] = client.megOlmEncrypt(
eventToEncrypt,
room.roomId,
0,
genRandomData(EncryptMegOlmEventAction::maxRandomSize())
);
REQUIRE(maybeKey.has_value());
REQUIRE(!encryptedEvent.originalJson().get()["content"].contains("m.relates_to"));
}
+
+TEST_CASE("ClientModel::olmEncryptSplit()", "[client][encryption]")
+{
+ auto makeDeviceInfo = [](const Crypto &crypto, std::string userId, std::string deviceId) {
+ auto client = makeClient(withCrypto(crypto));
+ client.userId = userId;
+ client.deviceId = deviceId;
+ auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
+ return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
+ };
+
+ auto receiver1 = makeCrypto();
+ receiver1.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
+ auto receiver2 = makeCrypto();
+ receiver2.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
+
+ auto oneTimeKeys1 = receiver1.unpublishedOneTimeKeys();
+ auto cv25519Key1 = oneTimeKeys1["curve25519"].items().begin().value().template get<std::string>();
+ auto oneTimeKeys2 = receiver2.unpublishedOneTimeKeys();
+ auto cv25519Key2 = oneTimeKeys2["curve25519"].items().begin().value().template get<std::string>();
+
+ auto queryKeysRespJson = json{
+ {"device_keys", {{"@receiver:example.com", {
+ {"device1", makeDeviceInfo(receiver1, "@receiver:example.com", "device1")},
+ {"device2", makeDeviceInfo(receiver2, "@receiver:example.com", "device2")},
+ }}}},
+ };
+
+ // Query keys
+ auto client = makeClient(withCrypto(makeCrypto()));
+ std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse(
+ makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJson))
+ ));
+
+ // Claim keys
+ client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), receiver1.curve25519IdentityKey(), cv25519Key1); });
+ client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), receiver2.curve25519IdentityKey(), cv25519Key2); });
+
+ // encrypt
+ auto res = client.olmEncryptSplit(Event(json::object()),
+ {{"@receiver:example.com", {"device1", "device2"}}},
+ genRandomData(Crypto::encryptOlmMaxRandomSize() * 2));
+
+ REQUIRE(res["@receiver:example.com"]["device1"].originalJson().get().at("content").at("ciphertext").size() == 1);
+ REQUIRE(res["@receiver:example.com"]["device1"].originalJson().get().at("content").at("ciphertext").contains(receiver1.curve25519IdentityKey()));
+ REQUIRE(res["@receiver:example.com"]["device2"].originalJson().get().at("content").at("ciphertext").size() == 1);
+ REQUIRE(res["@receiver:example.com"]["device2"].originalJson().get().at("content").at("ciphertext").contains(receiver2.curve25519IdentityKey()));
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 11:25 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785503
Default Alt Text
(42 KB)

Event Timeline