Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85628240
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
62 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
index 70e9b84..f51b08d 100644
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -1,829 +1,841 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/transducer/cat.hpp>
#include "encryption.hpp"
#include <immer-utils.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "status-utils.hpp"
#include "key-export.hpp"
namespace Kazv
{
using namespace CryptoConstants;
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto k = m.makeSelfDeviceKeys();
auto job = m.job<UploadKeysJob>()
.make(k)
.withData(json{{"is", "identityKeys"}});
kzo.client.dbg() << "Uploading identity keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), simpleFail };
}
kzo.client.dbg() << "Generating " << a.numToGen << " one-time keys..." << std::endl;
auto maxNumKeys = m.constCrypto().maxNumberOfOneTimeKeys();
auto numLocalKeys = m.constCrypto().numUnpublishedOneTimeKeys();
auto numStoredKeys = m.constCrypto().uploadedOneTimeKeysCount(signedCurve25519) + numLocalKeys;
auto numKeysToGenerate = a.numToGen;
auto genKeysLimit = maxNumKeys - numStoredKeys;
if (numKeysToGenerate > genKeysLimit) {
numKeysToGenerate = genKeysLimit;
}
if (numLocalKeys <= 0 && numKeysToGenerate <= 0) { // we have enough already
kzo.client.dbg() << "We have enough one-time keys. Ignoring this." << std::endl;
return { std::move(m), lager::noop };
}
if (numKeysToGenerate > 0) {
m.withCrypto([&](auto &c) { c.genOneTimeKeysWithRandom(a.random, numKeysToGenerate); });
}
kzo.client.dbg() << "Generating done." << std::endl;
auto keys = m.constCrypto().unpublishedOneTimeKeys();
auto cv25519Keys = keys.at(curve25519);
json oneTimeKeys = json::object();
for (auto [id, keyStr] : cv25519Keys.items()) {
json keyObject = json::object();
keyObject["key"] = keyStr;
keyObject["signatures"] = m.convertSignature(m.withCrypto([&](auto &c) { return c.sign(keyObject); }));
oneTimeKeys[signedCurve25519 + ":" + id] = keyObject;
}
auto job = m.job<UploadKeysJob>()
.make(
std::nullopt, // deviceKeys
oneTimeKeys)
.withData(json{{"is", "oneTimeKeys"}});
kzo.client.dbg() << "Uploading one time keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
};
ClientResult processResponse(ClientModel m, UploadKeysResponse r)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto is = r.dataStr("is");
if (is == "identityKeys") {
if (! r.success()) {
kzo.client.dbg() << "Uploading identity keys failed" << std::endl;
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading identity keys successful" << std::endl;
m.identityKeysUploaded = true;
} else {
if (! r.success()) {
kzo.client.dbg() << "Uploading one-time keys failed" << std::endl;
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading one-time keys successful" << std::endl;
m.withCrypto([&](auto &c) { c.markOneTimeKeysAsPublished(); });
}
m.withCrypto([&](auto &c) { c.setUploadedOneTimeKeysCount(r.oneTimeKeyCounts()); });
return { std::move(m), lager::noop };
}
static JsonWrap cannotDecryptEvent(
const std::string &reason,
const std::string &errcode,
const json &raw)
{
return json{
{"type", "m.room.message"},
{"content", {
{"msgtype","moe.kazv.mxc.cannot.decrypt"},
{"body", "**This message cannot be decrypted due to " + reason + ".**"},
{"moe.kazv.mxc.error", reason},
{"moe.kazv.mxc.errcode", errcode},
{"moe.kazv.mxc.raw", raw},
}},
};
}
// returns std::nullopt on success, and an error event on failure
static std::optional<JsonWrap> verifyEvent(ClientModel &m, Event e, const json &plainJson)
{
try {
std::string algo = e.originalJson().get().at("content").at("algorithm");
if (algo == olmAlgo) {
// Perform checks described in
// https://spec.matrix.org/v1.18/client-server-api/#validation-of-incoming-decrypted-events
// (5) Where sender_device_keys is present in the decrypted content:
auto [status, deviceInfo] = m.deviceLists.findByOlmEvent(e.setDecryptedJson(plainJson, Event::Decrypted));
if (status == DeviceListTracker::NotFound) {
kzo.client.dbg() << "Device key unknown, thus invalid" << std::endl;
return cannotDecryptEvent(
"device key unknown",
"MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN",
plainJson
);
}
// (1) The sender property in the decrypted content must match the sender of the event.
if (! (plainJson.at("sender") == e.sender())) {
kzo.client.dbg() << "Sender does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
"sender does not match",
"MOE.KAZV.MXC_BAD_SENDER",
plainJson
);
}
// (3) The recipient property in the decrypted content must match the user ID of the local user.
if (! (plainJson.at("recipient") == m.userId)) {
kzo.client.dbg() << "Recipient does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
"recipient does not match",
"MOE.KAZV.MXC_BAD_RECIPIENT",
plainJson
);
}
// (4) The recipient_keys.ed25519 property in the decrypted content must match the client
// device's Ed25519 signing key.
if (! (plainJson.at("recipient_keys").at(ed25519) == m.constCrypto().ed25519IdentityKey())) {
kzo.client.dbg() << "Recipient key does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
"recipient keys do not match",
"MOE.KAZV.MXC_BAD_RECIPIENT_KEYS",
plainJson
);
}
// (2) The keys.ed25519 property in the decrypted content must match the
// [CORRECTED: ed25519 identity key of the sending device]
auto thisEd25519Key = plainJson.at("keys").at(ed25519).get<std::string>();
// If `sender_device_keys` is present, this also checks:
// (3) [CORRECTED: sender_device_keys.keys.ed25519:<device_id> must be the same as the `keys.ed25519` property in the decrypted content.]
if (thisEd25519Key != deviceInfo.ed25519Key) {
kzo.client.dbg() << "Sender ed25519 key does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
"sender keys do not match",
"MOE.KAZV.MXC_BAD_SENDER_KEYS",
plainJson
);
}
// if everything looks good, add the deviceInfo if it is not yet in tracker
if (status == DeviceListTracker::InEvent) {
m.deviceLists.addVerifiedDeviceKeyInfo(
e.sender(),
deviceInfo.deviceId,
deviceInfo
);
}
} else if (algo == megOlmAlgo) {
auto roomId = plainJson.at("room_id").get<std::string>();
if (roomId.empty() ||
roomId != e.originalJson().get().at("room_id").template get<std::string>()) {
kzo.client.dbg() << "Room id does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
"room id does not match",
"MOE.KAZV.MXC_BAD_ROOM_ID",
plainJson
);
}
} else {
kzo.client.dbg() << "Unknown algorithm, thus invalid" << std::endl;
return cannotDecryptEvent(
"unknown algorithm",
"MOE.KAZV.MXC_UNKNOWN_ALGORITHM",
plainJson
);
}
} catch (const std::exception &exception) {
kzo.client.dbg() << "json format is not correct, thus invalid" << std::endl;
return cannotDecryptEvent(
exception.what(),
"M_BAD_JSON",
plainJson
);
}
return std::nullopt;
}
Event decryptEvent(ClientModel &m, Event e)
{
// no need for decryption
if (e.decrypted() || (! e.encrypted())) {
return e;
}
kzo.client.dbg() << "About to decrypt event: "
<< e.id() << std::endl;
auto maybePlainText = m.withCrypto([&](Crypto &c) {
return c.decrypt(e.originalJson().get());
});
if (! maybePlainText) {
kzo.client.dbg() << "Cannot decrypt: " << maybePlainText.reason() << std::endl;
return e.setDecryptedJson(
cannotDecryptEvent(
maybePlainText.reason(),
"MOE.KAZV.MXC_DECRYPT_ERROR",
json(nullptr)
),
Event::NotDecrypted);
} else {
try {
auto plainJson = json::parse(maybePlainText.value());
auto error = verifyEvent(m, e, plainJson);
auto valid = !error.has_value();
if (valid) {
kzo.client.dbg() << "The decrypted event is valid." << std::endl;
}
return valid
? e.setDecryptedJson(plainJson, Event::Decrypted)
: e.setDecryptedJson(
error.value(),
Event::NotDecrypted);
} catch (const std::exception &exception) {
return e.setDecryptedJson(
cannotDecryptEvent(
exception.what(),
"M_NOT_JSON",
maybePlainText.value()
),
Event::NotDecrypted
);
}
}
}
ClientModel tryDecryptEvents(ClientModel m)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring decryption request" << std::endl;
return m;
}
kzo.client.dbg() << "Trying to decrypt events..." << std::endl;
auto decryptFunc = [&](auto e) { return decryptEvent(m, e); };
auto takeOutRoomKeyEvents =
[&](auto e) {
if (e.type() != "m.room_key") {
// Leave it as it is
return true;
}
// It is a room key event, but unencrypted.
// Per matrix spec, we should not trust it as a E2EE key.
// matrix spec also says we should make sure it's Olm-encrypted.
// This is realized by verifying all MegOlm-encrypted events have
// a room_id.
if (!e.encrypted()) {
kzo.client.warn() << "Received an unencrypted room key event. Ignoring." << std::endl;
return false;
}
try {
auto content = e.content();
std::string roomId = content.get().at("room_id");
std::string sessionId = content.get().at("session_id");
kzo.client.dbg() << "Got a room key for room " << roomId
<< ", session id: " << sessionId << std::endl;
std::string sessionKey = content.get().at("session_key");
auto k = KeyOfGroupSession{roomId, sessionId};
std::string ed25519Key = e.decryptedJson().get().at("keys").at(ed25519);
if (m.withCrypto([&](auto &c) { return c.createInboundGroupSession(k, sessionKey, ed25519Key); })) {
return false; // such that this event is removed
} else {
kzo.client.warn() << "The session exists and cannot be merged. Someone is trying to do a session-replace attack." << std::endl;
kzo.client.dbg() << "sender key is " << ed25519Key << std::endl;
return true;
}
} catch (...) {
kzo.client.dbg() << "cannot create group session";
return false;
}
return true;
};
m.toDevice = intoImmer(
EventList{},
zug::map(decryptFunc)
| zug::filter(takeOutRoomKeyEvents),
std::move(m.toDevice));
auto decryptEventInRoom =
[&](auto id, auto room) {
if (! room.encrypted) {
return;
} else {
auto messages = room.messages;
auto undecryptedEvents = room.undecryptedEvents;
for (auto [sessionId, eventIds] : undecryptedEvents) {
if (m.constCrypto().hasInboundGroupSession({
room.roomId,
sessionId,
})) {
auto nextEventIds = intoImmer(
immer::flex_vector<std::string>{},
zug::filter([&](auto eventId) {
auto event = room.messages[eventId];
auto decrypted = decryptFunc(event);
room.messages = std::move(room.messages)
.set(eventId, decrypted);
return !decrypted.decrypted();
}),
eventIds
);
if (nextEventIds.empty()) {
room.undecryptedEvents = std::move(room.undecryptedEvents).erase(sessionId);
} else {
room.undecryptedEvents = std::move(room.undecryptedEvents).set(sessionId, nextEventIds);
}
}
}
m.roomList.rooms = std::move(m.roomList.rooms).set(id, room);
}
};
auto rooms = m.roomList.rooms;
for (auto [id, room]: rooms) {
decryptEventInRoom(id, room);
}
return m;
}
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return std::nullopt;
}
immer::map<std::string, immer::array<std::string>> deviceKeys;
auto encryptedUsers = m.deviceLists.outdatedUsers();
- if (encryptedUsers.empty()) {
- kzo.client.dbg() << "Keys are up-to-date." << std::endl;
- return std::nullopt;
- }
-
kzo.client.dbg() << "We need to query keys for: " << std::endl;
for (auto userId: encryptedUsers) {
kzo.client.dbg() << userId << std::endl;
deviceKeys = std::move(deviceKeys).set(userId, {});
}
kzo.client.dbg() << "^" << std::endl;
+ for (const auto &msg: m.toDevice) {
+ // If message cannot be verified because device key is unknown, try to fetch corresponding device key
+ if (msg.encrypted()
+ && !msg.decrypted()
+ && msg.decryptedJson().get().contains("/content/moe.kazv.mxc.errcode"_json_pointer)
+ && msg.decryptedJson().get().at("/content/moe.kazv.mxc.errcode"_json_pointer) == "MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN"
+ // do not fetch if the (invalid) sender device keys is embedded
+ && !msg.decryptedJson().get().contains("/content/moe.kazv.mxc.raw/sender_device_keys"_json_pointer)) {
+ // the event does not include the device id that it is sent from, so query all devices for the sender
+ deviceKeys = std::move(deviceKeys).set(msg.sender(), {});
+ }
+ }
+
+ if (deviceKeys.empty()) {
+ return std::nullopt;
+ }
+
auto job = m.job<QueryKeysJob>()
.make(deviceKeys,
std::nullopt, // timeout
a.isInitialSync ? std::nullopt : m.syncToken
)
.withData(json::object({
{"deviceKeys", deviceKeys},
}));
return job;
}
ClientResult updateClient(ClientModel m, QueryKeysAction a)
{
auto jobOpt = clientPerform(m, a);
if (jobOpt) {
m.addJob(jobOpt.value());
}
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) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "query keys failed: " << r.errorCode() << r.errorMessage() << std::endl;
return { std::move(m), failWithResponse(r) };
}
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) {
kzo.client.dbg() << "Key for " << userId
<< "/" << deviceId
<< ": " << json(deviceInfo).dump()
<< std::endl;
m.deviceLists.addDevice(userId, deviceId, deviceInfo);
}
if (wantedToFetchAllForUser(userId)) {
// We requested all devices for this user, so any
// device currently tracked but absent from the
// response has been deleted by its owner. Mark it
// as deleted rather than removing it, so we can still
// verify past decrypted events from that device.
auto existingDevices = m.deviceLists.devicesFor(userId);
for (auto [deviceId, info] : existingDevices) {
if (!deviceMap.count(deviceId)) {
kzo.client.dbg() << "Marking deleted device " << userId
<< "/" << deviceId << std::endl;
m.deviceLists.markDeviceAsDeleted(userId, deviceId);
}
}
m.deviceLists.markUpToDate(userId);
}
}
return { std::move(m), detail::ReturnEffectStatusT{
EffectStatus{/* succ = */ true, json::object({
{"unsatisfied", unsatisfied}
})}
} };
}
ClientResult updateClient(ClientModel m, ClaimKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "claim keys for: " << json(a.devicesToSend).dump() << std::endl;
auto keyMap = immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>>{};
for (auto [userId, devices] : a.devicesToSend) {
kzo.client.dbg() << "Iterating through user " << userId << std::endl;
auto deviceToKey = immer::map<std::string, std::string>{};
for (auto deviceId : devices) {
kzo.client.dbg() << "Device: " << deviceId << std::endl;
auto infoOpt = m.deviceLists.get(userId, deviceId);
if (infoOpt) {
kzo.client.dbg() << "Got device info, curve25519 key is: " << infoOpt.value().curve25519Key << std::endl;
deviceToKey = std::move(deviceToKey)
.set(deviceId, infoOpt.value().curve25519Key);
} else {
kzo.client.dbg() << "Did not get device info" << std::endl;
}
}
keyMap = std::move(keyMap).set(userId, deviceToKey);
}
auto devicesToClaimKeys = m.withCrypto([&](auto &c) { return c.devicesMissingOutboundSessionKey(keyMap); });
kzo.client.dbg() << "Really claim keys for: " << json(devicesToClaimKeys).dump() << std::endl;
auto oneTimeKeys = immer::map<std::string, immer::map<std::string, std::string>>{};
for (auto [userId, devices] : devicesToClaimKeys) {
auto devKeys = immer::map<std::string, std::string>{};
for (auto deviceId: devices) {
devKeys = std::move(devKeys).set(deviceId, signedCurve25519);
}
oneTimeKeys = std::move(oneTimeKeys).set(userId, devKeys);
}
auto job = m.job<ClaimKeysJob>()
.make(std::move(oneTimeKeys))
.withData(json{
{"roomId", a.roomId},
{"sessionId", a.sessionId},
{"sessionKey", a.sessionKey},
{"devicesToSend", a.devicesToSend},
{"random", a.random}
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, ClaimKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "claim keys failed" << std::endl;
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "claim keys successful" << std::endl;
kzo.client.dbg() << "Json body: " << r.jsonBody().get().dump() << std::endl;
auto roomId = r.dataStr("roomId");
auto sessionKey = r.dataStr("sessionKey");
auto sessionId = r.dataStr("sessionId");
auto devicesToSend =
immer::map<std::string, immer::flex_vector<std::string>>(r.dataJson("devicesToSend"));
auto random = r.dataJson("random").template get<RandomData>();
// create outbound sessions for those devices
auto oneTimeKeys = r.oneTimeKeys();
for (auto [userId, deviceMap] : oneTimeKeys) {
for (auto [deviceId, keyVar] : deviceMap) {
auto keys = keyVar.get();
for (auto [keyId, key] : keys.items()) {
auto deviceInfoOpt = m.deviceLists.get(userId, deviceId);
if (deviceInfoOpt) {
auto deviceInfo = deviceInfoOpt.value();
kzo.client.dbg() << "Verifying key for " << userId
<< "/" << deviceId
<< key.dump()
<< " with ed25519 key "
<< deviceInfo.ed25519Key << std::endl;
auto verified = m.withCrypto([&](auto &c) { return c.verify(key, userId, deviceId, deviceInfo.ed25519Key); });
kzo.client.dbg() << (verified ? "passed" : "did not pass") << std::endl;
if (verified && key.contains("key")) {
auto theirOneTimeKey = key.at("key");
kzo.client.dbg() << "creating outbound session for it" << std::endl;
m.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(random, deviceInfo.curve25519Key, theirOneTimeKey); });
random.erase(0, Crypto::createOutboundSessionRandomSize());
kzo.client.dbg() << "done" << std::endl;
}
}
}
}
}
auto eventJson = json{
{"content", {{"algorithm", megOlmAlgo},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.room_key"}
};
auto event = Event(JsonWrap(eventJson));
return {
std::move(m),
[event](auto &&) { return EffectStatus{ /* success = */ true, json{{ "keyEvent", event.originalJson() }} }; }
};
}
ClientResult updateClient(ClientModel m, EncryptMegOlmEventAction a)
{
auto [encryptedEvent, maybeKey] = m.megOlmEncrypt(a.e, a.roomId, a.timeMs, a.random);
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"encrypted", encryptedEvent.originalJson()},
});
if (maybeKey.has_value()) {
retJson["key"] = maybeKey.value();
}
return EffectStatus(/* succ = */ true, retJson);
}
};
}
ClientResult updateClient(ClientModel m, SetDeviceTrustLevelAction a)
{
auto maybeOldInfo = m.deviceLists.get(a.userId, a.deviceId);
if (!maybeOldInfo) {
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"error", "No such device"},
{"errorCode", "MOE_KAZV_MXC_KAZV_NO_SUCH_DEVICE"},
});
return EffectStatus(/* succ = */ false, retJson);
}
};
}
m.deviceLists.deviceLists = updateIn(
std::move(m.deviceLists.deviceLists),
[a](auto device) {
device.trustLevel = a.trustLevel;
return device;
},
a.userId,
a.deviceId
);
return { m, lager::noop };
}
ClientResult updateClient(ClientModel m, SetDevicesTrustLevelsAction a)
{
auto res = json::object({
{"notFound", json::array()},
});
auto succ = true;
for (const auto &[userId, devicesMap] : a.trustLevelMap) {
for (const auto &[deviceId, trustLevel] : devicesMap) {
kzo.client.dbg() << "SetDevicesTrustLevelsAction: user " << userId << ", device " << deviceId << ", trust level " << trustLevel << std::endl;
auto maybeOldInfo = m.deviceLists.get(userId, deviceId);
if (!maybeOldInfo) {
res.at("notFound").push_back(json::array({userId, deviceId}));
succ = false;
continue;
}
m.deviceLists.deviceLists = updateIn(
std::move(m.deviceLists.deviceLists),
[trustLevel](auto device) {
device.trustLevel = trustLevel;
return device;
},
userId,
deviceId
);
}
}
return {std::move(m), detail::ReturnEffectStatusT{{succ, res}}};
}
ClientResult updateClient(ClientModel m, SetTrustLevelNeededToSendKeysAction a)
{
m.trustLevelNeededToSendKeys = a.trustLevel;
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PrepareForSharingRoomKeyAction a)
{
auto messages = m.olmEncryptSplit(a.e, a.devices, a.random);
auto txnId = getTxnId(Event(), m);
m.roomList = RoomListModel::update(
std::move(m.roomList),
UpdateRoomAction{
a.roomId,
AddPendingRoomKeyAction{
PendingRoomKeyEvent{txnId, messages}
}
}
);
return { std::move(m), [txnId](auto &&) {
return EffectStatus(/* succ = */ true, json::object({{"txnId", txnId}}));
} };
}
ClientResult updateClient(ClientModel m, ImportFromKeyBackupFileAction a)
{
auto maybeExportFile = decryptKeyExport(std::move(a.fileContent), std::move(a.password));
if (!maybeExportFile) {
return {std::move(m), Kazv::detail::ReturnEffectStatusT{{
/* succ = */ false,
json{
{"errorCode", maybeExportFile.reason()},
{"error", maybeExportFile.reason()},
},
}}};
}
std::size_t imported = 0;
m.withCrypto([&maybeExportFile, &imported](Crypto &c) {
imported = c.importInboundGroupSessions(std::move(maybeExportFile).value());
});
return {std::move(m), Kazv::detail::ReturnEffectStatusT{{
/* succ = */ true,
json{
{"imported", imported},
},
}}};
};
ClientResult updateClient(ClientModel m, [[maybe_unused]] NotifyVerificationTrackerModelAction a)
{
m.addTrigger(VerificationTrackerModelChanged{});
return {std::move(m), lager::noop};
}
}
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
index 1328942..8b3bf27 100644
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -1,718 +1,729 @@
/*
* 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 "key-export.hpp"
#include "client-test-util.hpp"
#include "action-mock-utils.hpp"
#include "encryption-test-utils.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
namespace
{
struct CreateE2EESessionResult
{
ClientModel receiver1;
ClientModel receiver2;
ClientModel sender;
};
struct CreateMegOlmSessionResult
{
Event encryptedRoomEvent;
Event encryptedKeyEvent;
Event unencryptedKeyEvent;
};
}
static json makeDeviceInfo(const ClientModel &client)
{
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));
r1.userId = "@receiver:example.com";
r1.deviceId = "device1";
auto r2Crypto = makeCrypto();
r2Crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto r2 = makeClient(withCrypto(r2Crypto));
r2.userId = "@receiver:example.com";
r2.deviceId = "device2";
auto oneTimeKeys1 = r1Crypto.unpublishedOneTimeKeys();
auto cv25519Key1 = oneTimeKeys1["curve25519"].items().begin().value().template get<std::string>();
auto oneTimeKeys2 = r2Crypto.unpublishedOneTimeKeys();
auto cv25519Key2 = oneTimeKeys2["curve25519"].items().begin().value().template get<std::string>();
auto queryKeysRespJsonSender = json{
{"device_keys", {{"@receiver:example.com", {
{"device1", makeDeviceInfo(r1)},
{"device2", makeDeviceInfo(r2)},
}}}},
};
// Query keys
auto client = makeClient(withCrypto(makeCrypto()));
client.userId = "@sender:example.com";
client.deviceId = "device1";
auto queryKeysRespJsonReceiver = json{
{"device_keys", {{"@sender:example.com", {
{"device1", makeDeviceInfo(client)}
}}}},
};
std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse(
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)
| withResponseDataKV("deviceKeys", json::object({{"@sender:example.com", json::array()}})))
));
std::tie(r2, std::ignore) = processResponse(r2, QueryKeysResponse(
makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJsonReceiver)
| withResponseDataKV("deviceKeys", json::object({{"@sender:example.com", json::array()}})))
));
// Claim keys
client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), r1Crypto.curve25519IdentityKey(), cv25519Key1); });
client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), r2Crypto.curve25519IdentityKey(), cv25519Key2); });
return {
r1,
r2,
client,
};
}
static CreateMegOlmSessionResult createMegOlmSession(ClientModel &sender, ClientModel &receiver, std::string roomId, Event plainText)
{
auto sessionKey = sender.withCrypto([&](auto &c) {
return c.rotateMegOlmSessionWithRandom(genRandomData(Crypto::rotateMegOlmSessionRandomSize()), 0, roomId);
});
auto mod = withRoom(makeRoom(withRoomId(roomId) | withRoomEncrypted(true)));
mod(sender);
mod(receiver);
auto [encrypted, _noSessionKey] = sender.megOlmEncrypt(plainText, roomId, 0,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto sessionId = encrypted.originalJson().get()["content"]["session_id"].template get<std::string>();
auto keyEventJson = json{
{"content", {{"algorithm", CryptoConstants::megOlmAlgo},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.room_key"}
};
auto res = sender.olmEncryptSplit(Event(keyEventJson),
{{receiver.userId, {receiver.deviceId}}},
genRandomData(Crypto::encryptOlmMaxRandomSize() * 2));
auto encryptedKeyEventJson = res[receiver.userId][receiver.deviceId].originalJson().get();
encryptedKeyEventJson["sender"] = sender.userId;
auto encryptedKeyEvent = Event(encryptedKeyEventJson);
std::cerr << "room event" << encrypted.originalJson().get().dump() << std::endl;
std::cerr << "key event" << encryptedKeyEvent.originalJson().get().dump() << std::endl;
keyEventJson["keys"] = json{
{CryptoConstants::ed25519, sender.constCrypto().ed25519IdentityKey()},
};
keyEventJson["sender"] = sender.userId;
return {encrypted, encryptedKeyEvent, Event(keyEventJson)};
}
static Response syncResponseFromToDevice(Event toDevice)
{
auto j = json{
{"next_batch", "something"},
{"to_device", {
{"events", json::array({toDevice.originalJson().get()})},
}},
};
return makeResponse(
"Sync",
withResponseJsonBody(j)
| withResponseDataKV("is", "incremental"));
}
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);
}
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 r = createE2EESession();
auto client = r.sender;
auto receiver1 = r.receiver1.constCrypto();
auto receiver2 = r.receiver2.constCrypto();
// 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()));
}
TEST_CASE("tryDecryptEvents()", "[client][encryption]")
{
auto roomId = "!someroom:example.com";
auto room = makeRoom(
withRoomEncrypted(true)
| withRoomId(roomId)
);
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto plainText = makeEvent();
auto [encrypted, sessionId] = client.megOlmEncrypt(plainText, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto plainText2 = makeEvent();
// verify that we can decrypt events without sender_key or device_id
auto [encrypted2, sessionId2] = client.megOlmEncrypt(plainText2, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto j = encrypted2.originalJson().get();
j["content"].erase("sender_key");
j["content"].erase("device_id");
encrypted2 = Event(j);
auto events = EventList{
makeEvent(),
makeEvent(),
encrypted,
encrypted2,
};
withRoomTimeline(events)(room);
withRoom(room)(client);
auto nextClient = tryDecryptEvents(client);
auto decryptedEvent = nextClient.roomList.rooms[roomId].messages[encrypted.id()];
REQUIRE(decryptedEvent.encrypted());
REQUIRE(decryptedEvent.decrypted());
REQUIRE(decryptedEvent.type() == plainText.type());
REQUIRE(decryptedEvent.content() == plainText.content());
auto decryptedEvent2 = nextClient.roomList.rooms[roomId].messages[encrypted2.id()];
REQUIRE(decryptedEvent2.encrypted());
REQUIRE(decryptedEvent2.decrypted());
REQUIRE(decryptedEvent2.type() == plainText2.type());
REQUIRE(decryptedEvent2.content() == plainText2.content());
REQUIRE(nextClient.roomList.rooms[roomId].undecryptedEvents
==
immer::map<std::string, immer::flex_vector<std::string>>{});
}
TEST_CASE("tryDecryptEvents() will decrypt to-device events and add group session key", "[client][encryption]")
{
auto r = createE2EESession();
auto sender = r.sender;
auto receiver = r.receiver1;
std::string roomId = "!someroom:example.com";
Event plainText = json{
{"type", "m.room.message"},
{"content", {
{"body", "mew"},
}},
{"room_id", roomId},
};
auto [encryptedRoomEvent, encryptedKeyEvent, unencryptedKeyEvent] = createMegOlmSession(sender, receiver, roomId, plainText);
auto sessionId = encryptedRoomEvent.originalJson().get()["content"]["session_id"].template get<std::string>();
SECTION("Process key event") {
auto resp = syncResponseFromToDevice(encryptedKeyEvent);
auto [next, _dontCareEffect] = ClientModel::update(receiver, ProcessResponseAction{resp});
REQUIRE(next.constCrypto().hasInboundGroupSession(KeyOfGroupSession{roomId, sessionId}));
REQUIRE(next.toDevice.size() == 0);
}
SECTION("Reject unencrypted key event") {
auto resp = syncResponseFromToDevice(unencryptedKeyEvent);
auto [next, _dontCareEffect] = ClientModel::update(receiver, ProcessResponseAction{resp});
REQUIRE(!next.constCrypto().hasInboundGroupSession(KeyOfGroupSession{roomId, sessionId}));
REQUIRE(next.toDevice.size() == 0);
}
}
TEST_CASE("tryDecryptEvents() will update room.undecryptedEvents", "[client][encryption]")
{
auto roomId = "!someroom:example.com";
auto room = makeRoom(
withRoomEncrypted(true)
| withRoomId(roomId)
);
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto plainText = makeEvent();
auto [encrypted, sessionId] = client.megOlmEncrypt(plainText, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto plainText2 = makeEvent();
auto [encrypted2, sessionId2] = client.megOlmEncrypt(plainText2, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto j = encrypted2.originalJson().get();
// simulate an undecryptable event with a known session id
j["content"]["/////"];
encrypted2 = Event(j);
// simulate an undecryptable event with an unknown session id
auto plainText3 = makeEvent();
auto [encrypted3, sessionId3] = client.megOlmEncrypt(plainText3, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
j["content"]["session_id"] = "some-session-id";
encrypted3 = Event(j);
auto events = EventList{
makeEvent(),
makeEvent(),
encrypted,
encrypted2,
encrypted3,
};
withRoomTimeline(events)(room);
withRoom(room)(client);
auto nextClient = tryDecryptEvents(client);
auto decryptedEvent = nextClient.roomList.rooms[roomId].messages[encrypted.id()];
REQUIRE(decryptedEvent.encrypted());
REQUIRE(decryptedEvent.decrypted());
REQUIRE(decryptedEvent.type() == plainText.type());
REQUIRE(decryptedEvent.content() == plainText.content());
auto decryptedEvent2 = nextClient.roomList.rooms[roomId].messages[encrypted2.id()];
REQUIRE(decryptedEvent2.encrypted());
REQUIRE(!decryptedEvent2.decrypted());
auto decryptedEvent3 = nextClient.roomList.rooms[roomId].messages[encrypted2.id()];
REQUIRE(decryptedEvent3.encrypted());
REQUIRE(!decryptedEvent3.decrypted());
REQUIRE(nextClient.roomList.rooms[roomId].undecryptedEvents
==
immer::map<std::string, immer::flex_vector<std::string>>{
{encrypted.originalJson().get()["content"]["session_id"], {encrypted2.id()}},
{encrypted3.originalJson().get()["content"]["session_id"], {encrypted3.id()}},
});
}
static const std::string password = "test";
static const std::string backupFile = R"(-----BEGIN MEGOLM SESSION DATA-----
AV6R43XMe68Ekf7jB4lYHLgAAAAAAAAAAAAAAAAAAAAAAA9CQG628kByLP7LApTtbvhnpgFnUUJ
+tRMkpw4zcGoTOJya9/lawfRWKjd8LZeuHKdNLkEhfIAE16Xmqv+uU8oEASPxjLDOMjsgBKLMRx
/iwUR7Aoe4wjuwEcdEEOW+T6ffjUz5LmEJcI14qZ1wXUPk1pnNmz+4nX8+a9UxgEpAN7vsmilwz
P4PXNubhvGsqtZpy44pP6Td0alYgwVfTXqWB1KokMjuQE+2q6/Jb6U/z5D5nv8ArcJL04cD0U6r
ySsRWI9Jra2OcKFQxgLeVpRAiP6/sRyl9k1n6eiSOfmGkZ+qnvOfZsQh7Wupgh6zRe8LNEtrYZh
FpSaCE+0U8I5hZJrWNBDFfHg+rtzB4BEk0YwpD3rVcWEsk8kKqHmEulEqIXckd1SbSG7y7H1ADB
7mjAY7qWetMizPXD+I8MDUnU1TF3Jv3CIZfZY7BHh2WukmiORlpN4H5s/Wwq2oCIk7qXhCHFvaF
uj+XytIz6TmkEVZfXK9zqUCwCU+VYSGl9GVAezO8CZ6aEJes95yYqRxfADdJG2Vtd0oXwrpR0xV
1GO+0JJ3xKicVX6U77iMtJbL1Lge32QvbAcv8o6mcaW28xeeYPrccMIRa3vLtuSDDqKC79S9bIP
2U5F+MHn+5dqMeXcG9K2hS91gsBQMAvX6
-----END MEGOLM SESSION DATA-----)";
TEST_CASE("import keys", "[client][encryption]")
{
auto u = makeMockSdkUtil(makeClient(
withCrypto(makeCrypto())
));
auto c = u.sdk.client();
SECTION("success")
{
c.importFromKeyBackupFile(backupFile, password)
.then([&u](auto stat) {
REQUIRE(stat);
// the example json in the spec is not a valid key
REQUIRE(stat.dataJson("imported") == 0);
u.io.stop();
});
u.io.run();
}
SECTION("failure")
{
c.importFromKeyBackupFile(backupFile, "wrongpass")
.then([&u](auto stat) {
REQUIRE(!stat);
REQUIRE(stat.dataStr("error") == DecryptKeyExportErrorCodes::HMAC_FAILED);
u.io.stop();
});
u.io.run();
}
}
-TEST_CASE("tryDecryptEvents() rejects Olm-encrypted to-device event from unknown device", "[client][encryption][olm]")
+TEST_CASE("tryDecryptEvents() rejects Olm-encrypted to-device event from unknown device, which causes QueryKeysAction to query relevant keys", "[client][encryption][olm]")
{
// Bob: the current user, with crypto enabled
auto bobCrypto = makeCrypto();
bobCrypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto bobOneTimeKeys = bobCrypto.unpublishedOneTimeKeys();
bobCrypto.markOneTimeKeysAsPublished();
auto bobClient = makeClient(withCrypto(bobCrypto));
bobClient.userId = "@bob:example.com";
bobClient.deviceId = "bobdevice";
auto bobIdentityKey = bobCrypto.curve25519IdentityKey();
auto bobOneTimeKey = std::string{};
for (auto [id, key] : bobOneTimeKeys[CryptoConstants::curve25519].items()) {
bobOneTimeKey = key;
}
// Alice: a device NOT known to Bob (not in Bob's device list)
auto aliceCrypto = makeCrypto();
auto aliceIdentityKey = aliceCrypto.curve25519IdentityKey();
auto aliceEdKey = aliceCrypto.ed25519IdentityKey();
// Alice creates an outbound session to Bob (using Bob's published one-time key)
aliceCrypto.createOutboundSessionWithRandom(
genRandomData(Crypto::createOutboundSessionRandomSize()),
bobIdentityKey, bobOneTimeKey);
// Alice encrypts an m.room_key event for Bob
auto plainJson = json{
{"content", {
{"algorithm", CryptoConstants::megOlmAlgo},
{"room_id", "!someroom:example.com"},
{"session_id", "somesessionid"},
{"session_key", "somesessionkey"},
}},
{"keys", {
{CryptoConstants::ed25519, aliceEdKey},
}},
{"sender", "@alice:example.com"},
{"recipient", "@bob:example.com"},
{"recipient_keys", {
{CryptoConstants::ed25519, bobCrypto.ed25519IdentityKey()},
}},
{"type", "m.room_key"},
};
auto encryptedCiphertext = aliceCrypto.encryptOlmWithRandom(
genRandomData(Crypto::encryptOlmMaxRandomSize()),
plainJson, bobIdentityKey);
auto toDeviceJson = json{
{"sender", "@alice:example.com"},
{"type", "m.room.encrypted"},
{"content", {
{"algorithm", CryptoConstants::olmAlgo},
{"sender_key", aliceIdentityKey},
{"ciphertext", encryptedCiphertext},
}},
};
auto toDeviceEvent = Event(toDeviceJson);
// Feed the to-device event to Bob's client via a sync response
auto resp = syncResponseFromToDevice(toDeviceEvent);
auto [next, _] = ClientModel::update(bobClient, ProcessResponseAction{resp});
// The event should be marked as NOT decrypted because Alice's device
// is not in Bob's device list
REQUIRE(next.toDevice.size() == 1);
auto processedEvent = next.toDevice[0];
REQUIRE(processedEvent.encrypted());
REQUIRE(!processedEvent.decrypted());
// Verify the error indicates the device key is unknown
auto decryptedContent = processedEvent.content().get();
REQUIRE(decryptedContent.contains("moe.kazv.mxc.error"));
REQUIRE(decryptedContent["moe.kazv.mxc.error"] == "device key unknown");
REQUIRE(decryptedContent["moe.kazv.mxc.errcode"] == "MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN");
+
+ REQUIRE(next.deviceLists.outdatedUsers().empty());
+ std::tie(next, std::ignore) = ClientModel::update(next, QueryKeysAction{/* isInitialSync = */ false});
+ assert1Job(next);
+ auto job = next.nextJobs.at(0);
+ REQUIRE(job.jobId() == "QueryKeys");
+ auto jsonBody = json::parse(std::get<BytesBody>(job.requestBody()));
+ auto expected = json{
+ {"@alice:example.com", json::array()},
+ };
+ REQUIRE(jsonBody["device_keys"] == expected);
}
TEST_CASE("decryptEvent() handles Olm-encrypted to-device event", "[client][encryption][olm]")
{
auto s = OlmFirstTimeDecryptTestSetup();
auto plainText = json{
{"content", json::object()},
{"type", "moe.kazv.mxc.xxx"},
};
SECTION("good keys, with sender_device_keys") {
auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ true);
auto ev = decryptEvent(s.clientNoDevice, encrypted);
REQUIRE(ev.decrypted());
// Then, after the decryption, device info should be added to the tracker
auto devOpt = s.clientNoDevice.deviceLists.findByCurve25519Key(
ev.sender(),
ev.originalJson().get().at("/content/sender_key"_json_pointer).template get<std::string>()
);
REQUIRE(devOpt.has_value());
}
SECTION("good keys, without sender_device_keys") {
auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ false);
auto ev = decryptEvent(s.clientWithDevice, encrypted);
REQUIRE(ev.decrypted());
}
SECTION("sender mismatch") {
s.clientA.userId = "@bad:example.com";
auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ false);
s.clientA.userId = s.sender;
withEventKV("/sender"_json_pointer, s.sender)(encrypted);
auto ev = decryptEvent(s.clientWithDevice, encrypted);
REQUIRE(!ev.decrypted());
REQUIRE(ev.content().get().at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_BAD_SENDER");
}
SECTION("recipient mismatch") {
auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ false);
auto clientRec = s.clientWithDevice;
clientRec.userId = "@bad:example.com";
auto ev = decryptEvent(clientRec, encrypted);
REQUIRE(!ev.decrypted());
REQUIRE(ev.content().get().at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_BAD_RECIPIENT");
}
SECTION("recipient keys mismatch") {
using namespace CryptoConstants;
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", s.clientA.constCrypto().curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
auto c = makeCrypto();
auto toEncrypt = plainText;
toEncrypt["sender"] = s.sender;
toEncrypt["recipient"] = s.recipient;
toEncrypt["recipient_keys"] = json{
{ed25519, c.ed25519IdentityKey()},
};
toEncrypt["keys"] = json{
{ed25519, s.clientA.constCrypto().ed25519IdentityKey()},
};
encJson["content"]["ciphertext"] = s.clientA.withCrypto([&toEncrypt, &s](auto &c) {
auto key = s.clientNoDevice.constCrypto().curve25519IdentityKey();
return c.encryptOlmWithRandom(
genRandomData(c.encryptOlmRandomSize(key)),
toEncrypt,
key
);
});
encJson["sender"] = s.sender;
auto encrypted = Event(encJson);
auto ev = decryptEvent(s.clientWithDevice, encrypted);
REQUIRE(!ev.decrypted());
REQUIRE(ev.content().get().at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_BAD_RECIPIENT_KEYS");
}
SECTION("bad sender ed25519 keys") {
using namespace CryptoConstants;
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", s.clientA.constCrypto().curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
auto c = makeCrypto();
auto toEncrypt = plainText;
toEncrypt["sender"] = s.sender;
toEncrypt["recipient"] = s.recipient;
toEncrypt["recipient_keys"] = json{
{ed25519, s.clientNoDevice.constCrypto().ed25519IdentityKey()},
};
toEncrypt["keys"] = json{
{ed25519, c.ed25519IdentityKey()},
};
encJson["content"]["ciphertext"] = s.clientA.withCrypto([&toEncrypt, &s](auto &c) {
auto key = s.clientNoDevice.constCrypto().curve25519IdentityKey();
return c.encryptOlmWithRandom(
genRandomData(c.encryptOlmRandomSize(key)),
toEncrypt,
key
);
});
encJson["sender"] = s.sender;
auto encrypted = Event(encJson);
auto ev = decryptEvent(s.clientWithDevice, encrypted);
REQUIRE(!ev.decrypted());
REQUIRE(ev.content().get().at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_BAD_SENDER_KEYS");
}
SECTION("if verify event fails, device info is not added to tracker") {
auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ true);
auto clientRec = s.clientNoDevice;
clientRec.userId = "@bad:example.com";
auto ev = decryptEvent(clientRec, encrypted);
REQUIRE(!ev.decrypted());
REQUIRE(ev.content().get().at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_BAD_RECIPIENT");
REQUIRE(s.clientNoDevice.deviceLists.deviceLists.empty());
}
}
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();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Aug 8, 11:44 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722634
Default Alt Text
(62 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment