Page MenuHomePhorge

No OneTemporary

Size
85 KB
Referenced Files
None
Subscribers
None
diff --git a/design-docs/undecryptable-event-fallback.md b/design-docs/undecryptable-event-fallback.md
index a63d4bc..673152e 100644
--- a/design-docs/undecryptable-event-fallback.md
+++ b/design-docs/undecryptable-event-fallback.md
@@ -1,85 +1,86 @@
# Undecryptable event fallbacks
For events that cannot be decrypted, `event.decrypted()` will be false, `event.type()` will be `m.room.message`, and `event.content().get().at("msgtype")` can have the following values:
## xyz.tusooa.kazv.not.yet.decrypted
This means we haven't even tried to decrypt this event. The most probable case is that we do not have the corresponding session key.
## moe.kazv.mxc.cannot.decrypt
This means that the corresponding session is known, but there are errors in the decryption.
The event content will have the following properties:
- `moe.kazv.mxc.errcode` The error code.
- `moe.kazv.mxc.error` The error message.
- `moe.kazv.mxc.raw` The raw data associated with the error.
+- For to-device events only, `moe.kazv.mxc.failed_times` The number of failed trials of decryption. After a certain amount of failed trials, libkazv will no longer attempt to decrypt the event by itself.
### Possible error codes
#### `MOE.KAZV.MXC_DECRYPT_ERROR`
There is an error from libkazvcrypto when decrypting the event.
`moe.kazv.mxc.error` contains the error message.
`moe.kazv.mxc.raw` is always `null`.
#### `M_NOT_JSON`
The decrypted message is not valid json.
`moe.kazv.mxc.error` contains the error message from the json library when parsing the decrypted message.
`moe.kazv.mxc.raw` contains the decrypted message as string.
#### `MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN`
The sender's device key is unknown. Only for olm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_BAD_SENDER`
The sender specified in the decrypted event does not match the sender in the original event. Only for olm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_BAD_RECIPIENT`
The recipient specified in the decrypted event does not match the current user. Only for olm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_BAD_RECIPIENT_KEYS`
The recipient keys specified in the decrypted event do not match the current user. Only for olm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_BAD_SENDER_KEYS`
The sender keys specified in the decrypted event do not match the sender keys we received from the homeserver. Only for olm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_BAD_ROOM_ID`
The room id specified in the decrypted event do not match the one we received from the homeserver. Only for megolm-encrypted events.
`moe.kazv.mxc.raw` is the decrypted json.
#### `MOE.KAZV.MXC_UNKNOWN_ALGORITHM`
The encryption algorithm is unknown.
`moe.kazv.mxc.raw` is the decrypted json.
#### `M_BAD_JSON`
The decrypted json does not have a valid format.
`moe.kazv.mxc.error` contains the error message from the json library when accessing required properties (e.g. `at()` or `template get()`).
`moe.kazv.mxc.raw` is the decrypted json.
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
index 5904002..eff7f00 100644
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -1,888 +1,994 @@
/*
* 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 "send.hpp"
#include <immer-utils.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "status-utils.hpp"
#include "key-export.hpp"
#include "clientutil.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 const std::string CANNOT_DECRYPT_MSGTYPE = "moe.kazv.mxc.cannot.decrypt";
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"},
+ {"msgtype", CANNOT_DECRYPT_MSGTYPE},
{"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;
}
+ static constexpr int TO_DEVICE_EVENT_MAX_RETRY_COUNT = 10;
+
+ static int getEventFailedTimes(const Event &e)
+ {
+ // Safety guarantee: if the event is decrypted, then e.decrypted() will be true.
+ // So if someone sends us an encrypted m.room.message event with msgtype
+ // of <CANNOT_DECRYPT_MSGTYPE>, we will not consider it to be a failed-
+ // to-decrypt event.
+ if (!e.decrypted()
+ && e.encrypted()
+ && e.decryptedJson().get().contains("/content/msgtype"_json_pointer)
+ && e.decryptedJson().get()["content"]["msgtype"] == CANNOT_DECRYPT_MSGTYPE
+ && e.decryptedJson().get().contains("/content/moe.kazv.mxc.failed_times"_json_pointer)
+ && e.decryptedJson().get()["content"]["moe.kazv.mxc.failed_times"].is_number()) {
+ return e.decryptedJson().get()["content"]["moe.kazv.mxc.failed_times"].template get<int>();
+ } else {
+ return 0;
+ }
+ }
+
+ namespace
+ {
+ struct ReuseLastError {};
+
+ using GetPlainTextResult = std::variant<
+ // result from Crypto::decrypt()
+ MaybeString,
+ // result from cached decrypted (in events that cannot be verified)
+ json,
+ // ask to reuse last error (i.e. return e as-is)
+ ReuseLastError
+ >;
+ }
+
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());
- });
+ auto getPlainText = [&m](Event e) -> GetPlainTextResult {
+ auto fallbackDecrypt = [&m](Event ev) {
+ return m.withCrypto([&](Crypto &c) {
+ return c.decrypt(ev.originalJson().get());
+ });
+ };
+ if (e.decryptedJson().get().contains("/content/msgtype"_json_pointer)
+ && e.decryptedJson().get()["content"]["msgtype"] == CANNOT_DECRYPT_MSGTYPE
+ && e.decryptedJson().get().contains("/content/moe.kazv.mxc.errcode"_json_pointer)) {
+ auto errCode = e.decryptedJson().get()["content"]["moe.kazv.mxc.errcode"];
+ // We do not want to decrypt an event unnecessarily too many times
+ // olm events cannot be decrypted more than once
+ if (errCode == "MOE.KAZV.MXC_DECRYPT_ERROR"
+ || errCode == "MOE.KAZV.MXC_UNKNOWN_ALGORITHM") {
+ // Last time, this was unable to decrypt. We can try again.
+ return fallbackDecrypt(e);
+ } else if (errCode == "M_NOT_JSON") {
+ // Decrypted content is not json. Cannot recover.
+ return ReuseLastError{};
+ } else if (errCode == "MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN"
+ || errCode == "MOE.KAZV.MXC_BAD_SENDER"
+ || errCode == "MOE.KAZV.MXC_BAD_RECIPIENT"
+ || errCode == "MOE.KAZV.MXC_BAD_RECIPIENT_KEYS"
+ || errCode == "MOE.KAZV.MXC_BAD_SENDER_KEYS"
+ || errCode == "MOE.KAZV.MXC_BAD_ROOM_ID"
+ || errCode == "M_BAD_JSON") {
+ // Last time the event was decrypted, but did not pass verification.
+ // Try to verify it again without re-decrypting it.
+ if (e.decryptedJson().get()["content"].contains("moe.kazv.mxc.raw")) {
+ return e.decryptedJson().get()["content"]["moe.kazv.mxc.raw"];
+ } else {
+ return fallbackDecrypt(e);
+ }
+ }
+ }
- 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 {
+ return fallbackDecrypt(e);
+ };
+
+ auto r = getPlainText(e);
+ if (std::holds_alternative<ReuseLastError>(r)) {
+ return e;
+ }
+
+ json plainJson;
+
+ if (std::holds_alternative<MaybeString>(r)) {
+ auto maybePlainText = std::get<MaybeString>(r);
+ 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);
+ }
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);
+ plainJson = json::parse(maybePlainText.value());
} catch (const std::exception &exception) {
return e.setDecryptedJson(
cannotDecryptEvent(
exception.what(),
"M_NOT_JSON",
maybePlainText.value()
),
Event::NotDecrypted
);
}
+ } else {
+ // json
+ plainJson = std::get<json>(r);
}
+
+ 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);
}
- ClientModel tryDecryptEvents(ClientModel m)
+ [[nodiscard]] 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 decryptOneInRoom = [&](Event e) {
+ return decryptEvent(m, e);
+ };
+ auto decryptOneToDevice = [&](Event e) {
+ auto failedTimes = getEventFailedTimes(e);
+ // Only try to decrypt to-device events for limited times
+ if (failedTimes >= TO_DEVICE_EVENT_MAX_RETRY_COUNT) {
+ return e;
+ }
+ auto res = decryptEvent(m, e);
+ // if it still fails, add failed times
+ if (!res.decrypted()
+ && res.decryptedJson().get().contains("/content/msgtype"_json_pointer)
+ && res.decryptedJson().get()["content"]["msgtype"] == CANNOT_DECRYPT_MSGTYPE) {
+ auto decryptedJson = res.decryptedJson().get();
+ decryptedJson["content"]["moe.kazv.mxc.failed_times"] = failedTimes + 1;
+ return res.setDecryptedJson(
+ std::move(decryptedJson),
+ Event::NotDecrypted
+ );
+ } else {
+ return res;
+ }
+ };
auto takeOutRoomKeyEvents =
[&](auto e) {
if (e.type() != "m.room_key" && e.type() != "m.forwarded_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;
}
if (e.type() == "m.forwarded_room_key") {
// only accept forwarded room keys of verified devices of the same user
auto [st, sendingDev] = m.deviceLists.findByOlmEvent(e);
if (!(st == DeviceListTracker::InTracker
&& e.sender() == m.userId
&& sendingDev.trustLevel >= Verified)) {
kzo.client.info() << "Forwarded key is not from verified devices of the same user. 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.type() == "m.forwarded_room_key" ? e.decryptedJson().get().at("content").at("sender_claimed_ed25519_key") : 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::map(decryptOneToDevice)
| zug::filter(takeOutRoomKeyEvents),
std::move(m.toDevice));
- auto decryptEventInRoom =
+ auto decryptAllInRoom =
[&](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);
+ auto decrypted = decryptOneInRoom(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);
+ decryptAllInRoom(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();
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};
}
ClientResult updateClient(ClientModel m, RequestShareRoomSessionKeyAction a)
{
auto devices = a.devices.empty() ? m.devicesToRequestKeys() : a.devices;
auto origEventJson = a.event.originalJson().get();
if (!(
origEventJson.contains("/content/algorithm"_json_pointer)
&& origEventJson.contains("/content/session_id"_json_pointer)
)) {
return { std::move(m), failEffect("MOE.KAZV.MXC_BAD_EVENT", "Event is malformed") };
}
auto requestId = getTxnId(Event(), m);
auto requestJson = json{
{"type", "m.room_key_request"},
{"content", {
{"action", "request"},
{"body", {
{"algorithm", origEventJson.at("/content/algorithm"_json_pointer)},
{"room_id", a.roomId},
{"session_id", origEventJson.at("/content/session_id"_json_pointer)},
}},
{"request_id", requestId},
{"requesting_device_id", m.deviceId},
}},
};
if (origEventJson.contains("/content/sender_key"_json_pointer)) {
requestJson["content"]["body"]["sender_key"] = origEventJson["/content/sender_key"_json_pointer];
}
return updateClient(std::move(m), SendToDeviceMessageAction{
Event(std::move(requestJson)),
devices,
});
}
}
diff --git a/src/client/actions/encryption.hpp b/src/client/actions/encryption.hpp
index 9792507..9e8200b 100644
--- a/src/client/actions/encryption.hpp
+++ b/src/client/actions/encryption.hpp
@@ -1,46 +1,46 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include "client-model.hpp"
#include "csapi/keys.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction a);
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a);
ClientResult processResponse(ClientModel m, UploadKeysResponse r);
- ClientModel tryDecryptEvents(ClientModel m);
+ [[nodiscard]] ClientModel tryDecryptEvents(ClientModel m);
Event decryptEvent(ClientModel &m, Event e);
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);
ClientResult processResponse(ClientModel m, ClaimKeysResponse r);
ClientResult updateClient(ClientModel m, EncryptMegOlmEventAction a);
ClientResult updateClient(ClientModel m, SetDeviceTrustLevelAction a);
ClientResult updateClient(ClientModel m, SetDevicesTrustLevelsAction a);
ClientResult updateClient(ClientModel m, SetTrustLevelNeededToSendKeysAction a);
ClientResult updateClient(ClientModel m, PrepareForSharingRoomKeyAction a);
ClientResult updateClient(ClientModel m, ImportFromKeyBackupFileAction a);
ClientResult updateClient(ClientModel m, NotifyVerificationTrackerModelAction a);
ClientResult updateClient(ClientModel m, RequestShareRoomSessionKeyAction a);
}
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
index 5713df2..32f36a4 100644
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -1,894 +1,952 @@
/*
* 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 void queryEachOtherKeys(ClientModel &m1, ClientModel &m2)
+// Make a aware of b
+static void queryOne(ClientModel &a, ClientModel &b)
{
- auto queryOne = [](ClientModel &a, ClientModel &b) {
- auto queryKeysRespJson = json{
- {"device_keys", {{a.userId, {
- {a.deviceId, makeDeviceInfo(a)}
- }}}},
- };
- std::tie(b, std::ignore) = processResponse(b, QueryKeysResponse(
- makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJson)
- | withResponseDataKV("deviceKeys", json::object({
- {a.userId, {a.deviceId}}
- })))
- ));
+ auto queryKeysRespJson = json{
+ {"device_keys", {{b.userId, {
+ {b.deviceId, makeDeviceInfo(b)}
+ }}}},
};
+ std::tie(a, std::ignore) = processResponse(a, QueryKeysResponse(
+ makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJson)
+ | withResponseDataKV("deviceKeys", json::object({
+ {b.userId, {b.deviceId}}
+ })))
+ ));
+}
+
+static void queryEachOtherKeys(ClientModel &m1, ClientModel &m2)
+{
queryOne(m1, m2);
queryOne(m2, m1);
}
static void createAndClaimOneTimeKey(ClientModel &sender, ClientModel &receiver)
{
auto oneTimeKeys = receiver.withCrypto([](Crypto &c) {
c.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto k = c.unpublishedOneTimeKeys();
c.markOneTimeKeysAsPublished();
return k;
});
auto cv25519Key = oneTimeKeys["curve25519"].items().begin().value().template get<std::string>();
sender.withCrypto([&](auto &c) {
c.createOutboundSessionWithRandom(
genRandomData(Crypto::createOutboundSessionRandomSize()),
receiver.constCrypto().curve25519IdentityKey(), cv25519Key);
});
}
static CreateE2EESessionResult createE2EESession()
{
auto r1Crypto = makeCrypto();
auto r1 = makeClient(withCrypto(r1Crypto));
r1.userId = "@receiver:example.com";
r1.deviceId = "device1";
auto r2Crypto = makeCrypto();
auto r2 = makeClient(withCrypto(r2Crypto));
r2.userId = "@receiver:example.com";
r2.deviceId = "device2";
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";
queryEachOtherKeys(client, r1);
queryEachOtherKeys(client, r2);
createAndClaimOneTimeKey(client, r1);
createAndClaimOneTimeKey(client, r2);
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"));
}
+static json encryptToOneDevice(ClientModel &sender, const ClientModel &receiver, const json &plain)
+{
+ auto res = sender.olmEncryptSplit(Event(plain),
+ {{receiver.userId, {receiver.deviceId}}},
+ genRandomData(Crypto::encryptOlmMaxRandomSize()));
+
+ auto e = res[receiver.userId][receiver.deviceId].originalJson().get();
+ e["sender"] = sender.userId;
+ return e;
+}
+
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 process forwarded room keys", "[client][encryption]")
{
auto r = createE2EESession();
auto sender = r.sender;
auto receiver1 = r.receiver1;
auto receiver2 = r.receiver2;
std::string roomId = "!someroom:example.com";
Event plainText = json{
{"type", "m.room.message"},
{"content", {
{"body", "mew"},
}},
{"room_id", roomId},
};
queryEachOtherKeys(receiver1, receiver2);
createAndClaimOneTimeKey(receiver1, receiver2);
auto [encryptedRoomEvent, encryptedKeyEvent, unencryptedKeyEvent] = createMegOlmSession(sender, receiver1, roomId, plainText);
auto sessionId = unencryptedKeyEvent.content().get().at("session_id").template get<std::string>();
auto sessionKey = unencryptedKeyEvent.content().get().at("session_key").template get<std::string>();
auto forwardKeyEventJson = json{
{"content", {{"algorithm", CryptoConstants::megOlmAlgo},
{"forwarding_curve25519_key_chain", json::array()},
{"sender_claimed_ed25519_key", sender.constCrypto().ed25519IdentityKey()},
{"sender_key", sender.constCrypto().curve25519IdentityKey()},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.forwarded_room_key"}
};
auto encryptToReceiver = [receiver2](ClientModel &sender, const json &plain) {
- auto res = sender.olmEncryptSplit(Event(plain),
- {{receiver2.userId, {receiver2.deviceId}}},
- genRandomData(Crypto::encryptOlmMaxRandomSize()));
-
- auto e = res[receiver2.userId][receiver2.deviceId].originalJson().get();
- e["sender"] = sender.userId;
- return e;
+ return encryptToOneDevice(sender, receiver2, plain);
};
SECTION("Process forwarded key") {
std::tie(receiver2, std::ignore) = updateClient(receiver2, SetDeviceTrustLevelAction{receiver1.userId, receiver1.deviceId, Verified});
auto resp = syncResponseFromToDevice(encryptToReceiver(receiver1, forwardKeyEventJson));
auto [next, _dontCareEffect] = ClientModel::update(receiver2, ProcessResponseAction{resp});
REQUIRE(next.constCrypto().hasInboundGroupSession(KeyOfGroupSession{roomId, sessionId}));
REQUIRE(next.toDevice.size() == 0);
}
SECTION("Ignore forwarded key from other users") {
std::tie(receiver2, std::ignore) = updateClient(receiver2, SetDeviceTrustLevelAction{sender.userId, sender.deviceId, Verified});
auto resp = syncResponseFromToDevice(encryptToReceiver(sender, forwardKeyEventJson));
auto [next, _dontCareEffect] = ClientModel::update(receiver2, ProcessResponseAction{resp});
REQUIRE(!next.constCrypto().hasInboundGroupSession(KeyOfGroupSession{roomId, sessionId}));
REQUIRE(next.toDevice.size() == 0);
}
SECTION("Ignore forwarded key from unverified devices") {
auto resp = syncResponseFromToDevice(encryptToReceiver(receiver1, forwardKeyEventJson));
auto [next, _dontCareEffect] = ClientModel::update(receiver2, ProcessResponseAction{resp});
REQUIRE(!next.constCrypto().hasInboundGroupSession(KeyOfGroupSession{roomId, sessionId}));
REQUIRE(next.toDevice.size() == 0);
}
SECTION("Reject unencrypted key event") {
auto resp = syncResponseFromToDevice(forwardKeyEventJson);
auto [next, _dontCareEffect] = ClientModel::update(receiver2, 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, 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;
- }
+ auto bobClient = makeClient(
+ withCrypto(makeCrypto())
+ | withAttr(&ClientModel::userId, "@bob:example.com")
+ | withAttr(&ClientModel::deviceId, "bobdevice")
+ );
- // Alice: a device NOT known to Bob (not in Bob's device list)
- auto aliceCrypto = makeCrypto();
- auto aliceIdentityKey = aliceCrypto.curve25519IdentityKey();
- auto aliceEdKey = aliceCrypto.ed25519IdentityKey();
+ auto aliceClient = makeClient(
+ withCrypto(makeCrypto())
+ | withAttr(&ClientModel::userId, "@alice:example.com")
+ | withAttr(&ClientModel::deviceId, "alicedevice")
+ );
+
+ queryOne(aliceClient, bobClient);
+ createAndClaimOneTimeKey(aliceClient, bobClient);
- // Alice creates an outbound session to Bob (using Bob's published one-time key)
- aliceCrypto.createOutboundSessionWithRandom(
- genRandomData(Crypto::createOutboundSessionRandomSize()),
- bobIdentityKey, bobOneTimeKey);
+ auto bobIdentityKey = bobClient.constCrypto().curve25519IdentityKey();
// 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},
+ {CryptoConstants::ed25519, aliceClient.constCrypto().ed25519IdentityKey()},
}},
{"sender", "@alice:example.com"},
{"recipient", "@bob:example.com"},
{"recipient_keys", {
- {CryptoConstants::ed25519, bobCrypto.ed25519IdentityKey()},
+ {CryptoConstants::ed25519, bobClient.constCrypto().ed25519IdentityKey()},
}},
{"type", "m.room_key"},
};
- auto encryptedCiphertext = aliceCrypto.encryptOlmWithRandom(
- genRandomData(Crypto::encryptOlmMaxRandomSize()),
- plainJson, bobIdentityKey);
+ auto encryptedCiphertext = aliceClient.withCrypto([=](Crypto &c) {
+ return c.encryptOlmWithRandom(
+ genRandomData(Crypto::encryptOlmMaxRandomSize()),
+ plainJson, bobIdentityKey);
+ });
auto toDeviceJson = json{
{"sender", "@alice:example.com"},
{"type", "m.room.encrypted"},
{"content", {
{"algorithm", CryptoConstants::olmAlgo},
- {"sender_key", aliceIdentityKey},
+ {"sender_key", aliceClient.constCrypto().curve25519IdentityKey()},
{"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);
+
+ // After B gets A's identity keys, the event should be recovered
+ queryOne(next, aliceClient);
+ next = tryDecryptEvents(next);
+
+ REQUIRE(next.toDevice.size() == 1);
+ processedEvent = next.toDevice[0];
+ REQUIRE(processedEvent.encrypted());
+ REQUIRE(processedEvent.decrypted());
+ REQUIRE(processedEvent.decryptedJson().get() == plainJson);
+}
+
+TEST_CASE("tryDecryptEvents() will not try to decrypt to-device events that failed too many times", "[client][encryption][olm]")
+{
+ auto bobClient = makeClient(
+ withCrypto(makeCrypto())
+ | withAttr(&ClientModel::userId, "@bob:example.com")
+ | withAttr(&ClientModel::deviceId, "bobdevice")
+ );
+
+ auto aliceClient = makeClient(
+ withCrypto(makeCrypto())
+ | withAttr(&ClientModel::userId, "@alice:example.com")
+ | withAttr(&ClientModel::deviceId, "alicedevice")
+ );
+
+ // only A knows B, but B does not have A's identity keys
+ // this makes B unable to verify A's event, thus rejecting it
+ queryOne(aliceClient, bobClient);
+ createAndClaimOneTimeKey(aliceClient, bobClient);
+
+ auto plainJson = json{
+ {"content", json::object()},
+ {"type", "moe.kazv.mxc.placeholder"},
+ };
+
+ auto encrypted = Event(encryptToOneDevice(aliceClient, bobClient, plainJson));
+
+ bobClient.toDevice = {encrypted};
+
+ auto decryptCycle = [](ClientModel &m, int expectedTimes) {
+ m = tryDecryptEvents(m);
+ REQUIRE(m.toDevice.size() == 1);
+ auto event = m.toDevice.at(0);
+ REQUIRE(event.encrypted());
+ REQUIRE(!event.decrypted());
+ REQUIRE(event.decryptedJson().get().at("content").at("moe.kazv.mxc.failed_times") == expectedTimes);
+ REQUIRE(event.decryptedJson().get().at("content").at("moe.kazv.mxc.errcode") == "MOE.KAZV.MXC_DEVICE_KEY_UNKNOWN");
+ };
+
+ int maxFail = 10;
+ for (int i = 0; i < maxFail; ++i) {
+ decryptCycle(bobClient, i + 1);
+ }
+
+ // the 11th time it encounters a failure, it should not attempt to decrypt this event
+ decryptCycle(bobClient, 10);
}
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();
}
}
TEST_CASE("Client::devicesToRequestKeys()", "[client][encryption]")
{
auto device1 = makeDeviceKeyInfo(withAttr(&DeviceKeyInfo::trustLevel, Verified));
auto device2 = makeDeviceKeyInfo();
auto client = makeClient(
withAttr(&ClientModel::userId, "@mew:example.org")
| withDevice("@foo:example.org", makeDeviceKeyInfo(withAttr(&DeviceKeyInfo::trustLevel, Verified)))
| withDevice("@mew:example.org", device1)
| withDevice("@mew:example.org", device2)
);
auto expected = immer::map<std::string, immer::flex_vector<std::string>>{
{"@mew:example.org", {device1.deviceId}},
};
REQUIRE(client.devicesToRequestKeys() == expected);
}
TEST_CASE("RequestShareRoomSessionKeyAction", "[client][encryption]")
{
auto roomId = "!someroom:example.com";
auto room = makeRoom(
withRoomEncrypted(true)
| withRoomId(roomId)
);
auto sendingClient = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto plainText = makeEvent();
auto [encrypted, sessionId] = sendingClient.megOlmEncrypt(plainText, roomId, 1719196953000,
genRandomData(EncryptMegOlmEventAction::maxRandomSize()));
auto device1 = makeDeviceKeyInfo(withAttr(&DeviceKeyInfo::trustLevel, Verified));
auto client = makeClient(
withCrypto(makeCrypto())
| withAttr(&ClientModel::userId, "@mew:example.org")
| withDevice("@mew:example.org", device1)
| withRoom(room)
);
auto [next, _] = updateClient(client, RequestShareRoomSessionKeyAction{
roomId,
encrypted,
{},
});
assert1Job(next);
for1stJob(next, [device1, roomId, encrypted](const BaseJob &job) {
REQUIRE(job.jobId() == "SendToDevice");
auto body = getRequestJsonBody(job);
REQUIRE(job.url().find("/m.room_key_request/") != std::string::npos);
auto msg = body.at("messages").at("@mew:example.org").at(device1.deviceId);
REQUIRE(msg.at("action") == "request");
REQUIRE(msg.at("body").at("room_id") == roomId);
REQUIRE(msg.at("body").at("session_id") == encrypted.originalJson().get().at("/content/session_id"_json_pointer));
});
}
TEST_CASE("Client::requestShareRoomSessionKey", "[client][encryption]")
{
auto u = makeMockSdkUtil(makeClient());
auto md = u.getMockDispatcher(returnResolved<RequestShareRoomSessionKeyAction>({true, {}}));
auto client = u.getClient(md);
client.requestShareRoomSessionKey("!roomid", makeEvent())
.then([&md, &u](auto) {
REQUIRE(md.template calledTimes<RequestShareRoomSessionKeyAction>() == 1);
u.io.stop();
});
u.io.run();
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Oct 12, 10:21 AM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786133
Default Alt Text
(85 KB)

Event Timeline