Page MenuHomePhorge

D306.1782650466.diff
No OneTemporary

Size
41 KB
Referenced Files
None
Subscribers
None

D306.1782650466.diff

diff --git a/src/client/actions/encryption.hpp b/src/client/actions/encryption.hpp
--- a/src/client/actions/encryption.hpp
+++ b/src/client/actions/encryption.hpp
@@ -18,6 +18,7 @@
ClientResult processResponse(ClientModel m, UploadKeysResponse r);
ClientModel tryDecryptEvents(ClientModel m);
+ Event decryptEvent(ClientModel &m, Event e);
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a);
ClientResult updateClient(ClientModel m, QueryKeysAction a);
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -21,15 +21,6 @@
{
using namespace CryptoConstants;
- static json convertSignature(const ClientModel &m, std::string signature)
- {
- auto j = json::object();
- j[m.userId] = json::object();
- j[m.userId][ed25519 + ":" + m.deviceId] = signature;
-
- return j;
- }
-
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction)
{
if (! m.crypto) {
@@ -37,24 +28,7 @@
return { std::move(m), lager::noop };
}
- auto keys =
- immer::map<std::string, std::string>{}
- .set(ed25519 + ":" + m.deviceId, m.constCrypto().ed25519IdentityKey())
- .set(curve25519 + ":" + m.deviceId, m.constCrypto().curve25519IdentityKey());
-
- DeviceKeys k {
- m.userId,
- m.deviceId,
- {olmAlgo, megOlmAlgo},
- keys,
- {} // signatures to be added soon
- };
-
- auto j = json(k);
-
- auto sig = m.withCrypto([&](auto &crypto) { return crypto.sign(j); });
-
- k.signatures = convertSignature(m, sig);
+ auto k = m.makeSelfDeviceKeys();
auto job = m.job<UploadKeysJob>()
.make(k)
@@ -103,7 +77,7 @@
for (auto [id, keyStr] : cv25519Keys.items()) {
json keyObject = json::object();
keyObject["key"] = keyStr;
- keyObject["signatures"] = convertSignature(m, m.withCrypto([&](auto &c) { return c.sign(keyObject); }));
+ keyObject["signatures"] = m.convertSignature(m.withCrypto([&](auto &c) { return c.sign(keyObject); }));
oneTimeKeys[signedCurve25519 + ":" + id] = keyObject;
}
@@ -178,14 +152,14 @@
try {
std::string algo = e.originalJson().get().at("content").at("algorithm");
if (algo == olmAlgo) {
- std::string senderCurve25519Key = e.originalJson().get()
- .at("content").at("sender_key");
+ // Perform checks described in
+ // https://spec.matrix.org/v1.18/client-server-api/#validation-of-incoming-decrypted-events
- auto deviceInfoOpt = m.deviceLists.findByCurve25519Key(e.sender(), senderCurve25519Key);
+ // (5) Where sender_device_keys is present in the decrypted content:
+ auto [status, deviceInfo] = m.deviceLists.findByOlmEvent(e.setDecryptedJson(plainJson, Event::Decrypted));
- if (! deviceInfoOpt) {
- kzo.client.dbg() << "Device key " << senderCurve25519Key
- << " unknown, thus invalid" << std::endl;
+ 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",
@@ -193,8 +167,7 @@
);
}
- auto deviceInfo = deviceInfoOpt.value();
-
+ // (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(
@@ -203,6 +176,8 @@
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(
@@ -211,6 +186,9 @@
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(
@@ -219,7 +197,12 @@
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(
@@ -228,6 +211,15 @@
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() ||
@@ -259,7 +251,7 @@
return std::nullopt;
}
- static Event decryptEvent(ClientModel &m, Event e)
+ Event decryptEvent(ClientModel &m, Event e)
{
// no need for decryption
if (e.decrypted() || (! e.encrypted())) {
@@ -285,6 +277,7 @@
} else {
try {
auto plainJson = json::parse(maybePlainText.value());
+ kzo.client.dbg() << "plain:" << plainJson << std::endl;
auto error = verifyEvent(m, e, plainJson);
auto valid = !error.has_value();
if (valid) {
@@ -547,9 +540,7 @@
<< "/" << deviceId
<< ": " << json(deviceInfo).dump()
<< std::endl;
- m.withCrypto([&](Crypto &c) {
- m.deviceLists.addDevice(userId, deviceId, deviceInfo, c);
- });
+ m.deviceLists.addDevice(userId, deviceId, deviceInfo);
}
if (wantedToFetchAllForUser(userId)) {
m.deviceLists.markUpToDate(userId);
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -101,7 +101,17 @@
/// precondition: the one-time keys for those devices must already be claimed
/// @return A map from user id to device id to encrypted event for that device
- immer::map<std::string, immer::map<std::string, Event>> olmEncryptSplit(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random);
+ immer::map<std::string, immer::map<std::string, Event>> olmEncryptSplit(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random, bool attachSenderDeviceKeys = false);
+
+ /// Make a struct of DeviceKeys for identity keys of the current device.
+ DeviceKeys makeSelfDeviceKeys();
+
+ /**
+ * Convert a signature from Crypto into a json object.
+ *
+ * The returned object is a map from user id to key id to signature.
+ */
+ json convertSignature(std::string signature) const;
/// @return number of one-time keys we need to generate
std::size_t numOneTimeKeysNeeded() const;
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -185,7 +185,10 @@
immer::map<std::string, immer::map<std::string, Event>> ClientModel::olmEncryptSplit(
Event e,
- immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random)
+ immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap,
+ RandomData random,
+ bool attachSenderDeviceKeys
+ )
{
using ResT = immer::map<std::string, immer::map<std::string, Event>>;
if (!crypto) {
@@ -209,6 +212,11 @@
encJson["type"] = "m.room.encrypted";
+ if (attachSenderDeviceKeys) {
+ auto senderDeviceKeys = makeSelfDeviceKeys();
+ origJson["sender_device_keys"] = senderDeviceKeys;
+ }
+
ResT messages;
for (auto [userId, devices] : userIdToDeviceIdMap) {
@@ -216,6 +224,7 @@
for (auto dev : devices) {
auto devInfoOpt = deviceLists.get(userId, dev);
if (! devInfoOpt) {
+ kzo.client.warn() << "no device info for " << userId << " / " << dev << std::endl;
continue;
}
auto devInfo = devInfoOpt.value();
@@ -239,6 +248,41 @@
return messages;
}
+ DeviceKeys ClientModel::makeSelfDeviceKeys()
+ {
+ using namespace CryptoConstants;
+
+ auto keys =
+ immer::map<std::string, std::string>{}
+ .set(ed25519 + ":" + deviceId, constCrypto().ed25519IdentityKey())
+ .set(curve25519 + ":" + deviceId, constCrypto().curve25519IdentityKey());
+
+ DeviceKeys k {
+ userId,
+ deviceId,
+ {olmAlgo, megOlmAlgo},
+ keys,
+ {} // signatures to be added soon
+ };
+
+ auto j = json(k);
+
+ auto sig = withCrypto([&](auto &crypto) { return crypto.sign(j); });
+
+ k.signatures = convertSignature(sig);
+
+ return k;
+ }
+
+ json ClientModel::convertSignature(std::string signature) const
+ {
+ auto j = json::object();
+ j[userId] = json::object();
+ j[userId][CryptoConstants::ed25519 + ":" + deviceId] = signature;
+
+ return j;
+ };
+
immer::flex_vector<std::string /* deviceId */> ClientModel::devicesToSendKeys(std::string userId) const
{
auto trustLevelNeeded = this->trustLevelNeededToSendKeys;
diff --git a/src/client/device-list-tracker.hpp b/src/client/device-list-tracker.hpp
--- a/src/client/device-list-tracker.hpp
+++ b/src/client/device-list-tracker.hpp
@@ -77,7 +77,11 @@
immer::flex_vector<std::string> outdatedUsers() const;
- bool addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto);
+ std::optional<DeviceKeyInfo> verifyDeviceInfo(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo) const;
+
+ bool addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo);
+
+ void addVerifiedDeviceKeyInfo(std::string userId, std::string deviceId, DeviceKeyInfo info);
void markUpToDate(std::string userId);
@@ -88,6 +92,18 @@
std::optional<DeviceKeyInfo> findByEd25519Key(std::string userId, std::string ed25519Key) const;
std::optional<DeviceKeyInfo> findByCurve25519Key(std::string userId, std::string curve25519Key) const;
+ enum FindByOlmEventStatus
+ {
+ /// The sending device info cannot be found.
+ NotFound,
+ /// The sending device info is already in tracker.
+ InTracker,
+ /// The sending device info is not in tracker, but it is in the event
+ /// and ready to be added to the tracker.
+ InEvent,
+ };
+ std::pair<FindByOlmEventStatus, DeviceKeyInfo> findByOlmEvent(Event e) const;
+
/// returns a list of users whose device list has changed
immer::flex_vector<std::string> diff(DeviceListTracker that) const;
};
diff --git a/src/client/device-list-tracker.cpp b/src/client/device-list-tracker.cpp
--- a/src/client/device-list-tracker.cpp
+++ b/src/client/device-list-tracker.cpp
@@ -21,6 +21,31 @@
namespace Kazv
{
+ static bool cryptographicallyEqual(DeviceKeyInfo a, DeviceKeyInfo b)
+ {
+ a.displayName = std::nullopt;
+ b.displayName = std::nullopt;
+ a.trustLevel = Unseen;
+ b.trustLevel = Unseen;
+ return std::move(a) == std::move(b);
+ }
+
+ static bool cryptographicallyEqual(const DeviceListTracker::DeviceMapT &a, const DeviceListTracker::DeviceMapT &b)
+ {
+ auto changed = false;
+ auto markChanged = [&changed](const auto &) { changed = true; };
+ immer::diff(a, b, immer::make_differ(
+ /* added = */ markChanged,
+ /* removed = */ markChanged,
+ /* changed = */ [&changed](const auto &x, const auto &y) {
+ if (!cryptographicallyEqual(x.second, y.second)) {
+ changed = true;
+ }
+ }
+ ));
+ return !changed;
+ }
+
immer::flex_vector<std::string> DeviceListTracker::outdatedUsers() const
{
return intoImmer(
@@ -36,47 +61,73 @@
usersToTrackDeviceLists);
}
-
- bool DeviceListTracker::addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto)
+ std::optional<DeviceKeyInfo> DeviceListTracker::verifyDeviceInfo(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo) const
{
using namespace CryptoConstants;
if (userId != deviceInfo.userId
|| deviceId != deviceInfo.deviceId) {
- return false;
+ return std::nullopt;
}
- // if the ed25519 key changed, reject
- auto curEd25519Key = deviceInfo.keys[ed25519 + ":" + deviceId];
DeviceTrustLevel trustLevel{Unseen};
+ std::optional<std::string> displayName;
+
+ auto curEd25519Key = deviceInfo.keys[ed25519 + ":" + deviceId];
if (deviceLists[userId].find(deviceId)) {
+ // if the ed25519 key changed, reject
if (curEd25519Key != deviceLists[userId][deviceId].ed25519Key) {
- return false;
+ return std::nullopt;
}
- // keep device trust level when adding device
+ // keep device trust level and display name when adding device
trustLevel = deviceLists[userId][deviceId].trustLevel;
+ displayName = deviceLists[userId][deviceId].displayName;
+ }
+
+ // reject if there is another device with the same keys
+ if (auto d = findByCurve25519Key(userId, deviceInfo.keys[curve25519 + ":" + deviceId]);
+ d.has_value()
+ && d.value().deviceId != deviceId) {
+ return std::nullopt;
}
kzo.client.dbg() << "verifying device info" << std::endl;
- if (crypto.verify(deviceInfo, userId, deviceId, curEd25519Key)) {
+ if (Crypto::verify(deviceInfo, userId, deviceId, curEd25519Key)) {
kzo.client.dbg() << "passed verification" << std::endl;
auto info = DeviceKeyInfo{
deviceId,
deviceInfo.keys[ed25519 + ":" + deviceId],
deviceInfo.keys[curve25519 + ":" + deviceId],
- deviceInfo.unsignedData ? deviceInfo.unsignedData.value().deviceDisplayName : std::nullopt,
+ deviceInfo.unsignedData ? deviceInfo.unsignedData.value().deviceDisplayName : displayName,
trustLevel,
};
+ return info;
+ } else {
+ kzo.client.dbg() << "did not pass verification" << std::endl;
+ return std::nullopt;
+ }
+ }
- deviceLists = std::move(deviceLists)
- .update(userId, [=](auto deviceMap) {
- return std::move(deviceMap).set(deviceId, info);
- });
- return true;
+ bool DeviceListTracker::addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo)
+ {
+ using namespace CryptoConstants;
+ auto res = verifyDeviceInfo(userId, deviceId, deviceInfo);
+ if (!res.has_value()) {
+ return false;
}
- kzo.client.dbg() << "did not pass verification" << std::endl;
- return false;
+ auto info = res.value();
+ addVerifiedDeviceKeyInfo(userId, deviceId, info);
+
+ return true;
+ }
+
+ void DeviceListTracker::addVerifiedDeviceKeyInfo(std::string userId, std::string deviceId, DeviceKeyInfo info)
+ {
+ deviceLists = std::move(deviceLists)
+ .update(userId, [=](auto deviceMap) {
+ return std::move(deviceMap).set(deviceId, info);
+ });
}
void DeviceListTracker::markUpToDate(std::string userId)
@@ -130,29 +181,52 @@
}
}
- static bool cryptographicallyEqual(DeviceKeyInfo a, DeviceKeyInfo b)
+ auto DeviceListTracker::findByOlmEvent(Event e) const
+ -> std::pair<FindByOlmEventStatus, DeviceKeyInfo>
{
- a.displayName = std::nullopt;
- b.displayName = std::nullopt;
- a.trustLevel = Unseen;
- b.trustLevel = Unseen;
- return std::move(a) == std::move(b);
- }
+ if (!(e.encrypted() && e.decrypted())) {
+ return {NotFound, {}};
+ }
- static bool cryptographicallyEqual(const DeviceListTracker::DeviceMapT &a, const DeviceListTracker::DeviceMapT &b)
- {
- auto changed = false;
- auto markChanged = [&changed](const auto &) { changed = true; };
- immer::diff(a, b, immer::make_differ(
- /* added = */ markChanged,
- /* removed = */ markChanged,
- /* changed = */ [&changed](const auto &x, const auto &y) {
- if (!cryptographicallyEqual(x.second, y.second)) {
- changed = true;
+ auto senderCurve25519Key =
+ e.originalJson().get().contains("/content/sender_key"_json_pointer)
+ && e.originalJson().get().at("/content/sender_key"_json_pointer).is_string()
+ ? e.originalJson().get().at("/content/sender_key"_json_pointer).template get<std::string>()
+ : std::string();
+ if (senderCurve25519Key.empty()) {
+ return {NotFound, {}};
+ }
+ // 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:
+ if (e.raw().get().contains("sender_device_keys")) {
+ auto deviceKeys = e.raw().get().at("sender_device_keys").template get<Api::QueryKeysJob::DeviceInformation>();
+ // The following statement checks:
+ // (1) sender_device_keys.user_id must also match the sender of the event.
+ // (2) [CORRECTED: sender_device_keys.keys.curve25519:<device_id>] must also match the sender_key property in the cleartext m.room.encrypted event body.
+ // (3) ed25519 keys not checked (the check is performed in client/actions/encryption.cpp )
+ // (4) The sender_device_keys structure must have a valid signature from the key with ID ed25519:<device_id> (i.e., the sending device's Ed25519 key).
+ auto verifiedInfo = verifyDeviceInfo(e.sender(), deviceKeys.deviceId, deviceKeys);
+ if (verifiedInfo.has_value()) {
+ auto existingDeviceOpt = get(e.sender(), deviceKeys.deviceId);
+ if (existingDeviceOpt.has_value()) {
+ return {InTracker, existingDeviceOpt.value()};
+ } else {
+ return {InEvent, verifiedInfo.value()};
}
+ } else {
+ kzo.client.dbg() << "sender_device_keys present but invalid, rejecting" << std::endl;
+ return {NotFound, {}};
}
- ));
- return !changed;
+ } else {
+
+ auto existingDeviceOpt = findByCurve25519Key(e.sender(), senderCurve25519Key);
+ if (existingDeviceOpt.has_value()) {
+ return {InTracker, existingDeviceOpt.value()};
+ } else {
+ return {NotFound, {}};
+ }
+ }
}
immer::flex_vector<std::string> DeviceListTracker::diff(DeviceListTracker that) const
diff --git a/src/crypto/crypto.hpp b/src/crypto/crypto.hpp
--- a/src/crypto/crypto.hpp
+++ b/src/crypto/crypto.hpp
@@ -183,7 +183,7 @@
std::string outboundGroupSessionCurrentKey(std::string roomId);
/// Check whether the signature of userId/deviceId is valid in object
- bool verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key);
+ static bool verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key);
MaybeString getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const;
diff --git a/src/tests/client/device-list-tracker-test.cpp b/src/tests/client/device-list-tracker-test.cpp
--- a/src/tests/client/device-list-tracker-test.cpp
+++ b/src/tests/client/device-list-tracker-test.cpp
@@ -5,9 +5,12 @@
*/
#include <libkazv-config.hpp>
+#include "encryption-test-utils.hpp"
#include <catch2/catch_test_macros.hpp>
#include <client-model.hpp>
+#include <actions/encryption.hpp>
#include <factory.hpp>
+#include <debug.hpp>
using namespace Kazv;
using namespace Kazv::Factory;
@@ -20,3 +23,74 @@
REQUIRE(client.deviceLists.findByCurve25519Key("@example:example.com", info.curve25519Key) == info);
REQUIRE(client.deviceLists.findByCurve25519Key("@someone:example.com", info.curve25519Key) == std::nullopt);
}
+
+TEST_CASE("DeviceListTracker::findByOlmEvent")
+{
+ auto s = OlmFirstTimeDecryptTestSetup();
+
+ auto plainText = json{
+ {"content", json::object()},
+ {"type", "moe.kazv.mxc.xxx"},
+ };
+
+ SECTION("without sender_device_keys") {
+ auto encrypted = s.encrypt(plainText);
+ auto e = s.decryptOnly(encrypted);
+ auto [st, ki] = s.clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InTracker);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == s.clientA.constCrypto().ed25519IdentityKey());
+
+ std::tie(st, ki) = s.clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::NotFound);
+ }
+
+ SECTION("with valid sender_device_keys") {
+ auto encrypted = s.encrypt(plainText, /* attachSenderDeviceKeys = */ true);
+ auto e = s.decryptOnly(encrypted);
+ auto [st, ki] = s.clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InTracker);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == s.clientA.constCrypto().ed25519IdentityKey());
+
+ std::tie(st, ki) = s.clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InEvent);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == s.clientA.constCrypto().ed25519IdentityKey());
+ }
+
+ SECTION("with invalid sender_device_keys (bad signature)") {
+ // the following creates a bad signature
+ auto fakeClientA = s.clientA;
+ fakeClientA.deviceId = "device3";
+ plainText["sender_device_keys"] = fakeClientA.makeSelfDeviceKeys();
+ plainText["sender_device_keys"]["device_id"] = s.senderDeviceId;
+
+ auto encrypted = s.encrypt(plainText);
+ auto e = s.decryptOnly(encrypted);
+ auto [st, ki] = s.clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::NotFound);
+
+ std::tie(st, ki) = s.clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::NotFound);
+ }
+
+ SECTION("with invalid sender_device_keys (device id mismatch)") {
+ // the following creates a good signature, but device id is fake
+ // findByOlmEvent() does not rule out this, but it should be ruled out in
+ // verifyEvent().
+ auto fakeClientA = s.clientA;
+ fakeClientA.deviceId = "device3";
+ plainText["sender_device_keys"] = fakeClientA.makeSelfDeviceKeys();
+
+ auto encrypted = s.encrypt(plainText);
+ auto e = s.decryptOnly(encrypted);
+ auto [st, ki] = s.clientWithDevice.deviceLists.findByOlmEvent(e);
+ // It should reject because there is already another device with the same
+ // key
+ REQUIRE(st == DeviceListTracker::NotFound);
+
+ std::tie(st, ki) = s.clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InEvent);
+ }
+}
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -12,6 +12,7 @@
#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;
@@ -444,6 +445,212 @@
}
}
+TEST_CASE("tryDecryptEvents() rejects Olm-encrypted to-device event from unknown device", "[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");
+}
+
+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(
diff --git a/src/tests/crypto-test.cpp b/src/tests/crypto-test.cpp
--- a/src/tests/crypto-test.cpp
+++ b/src/tests/crypto-test.cpp
@@ -176,6 +176,88 @@
roomId, desc).has_value());
}
+TEST_CASE("Should handle unknown device sending Olm pre-key message", "[crypto][olm]")
+{
+ // Bob: the current user, generates and publishes one-time keys
+ Crypto bob(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+ bob.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
+ auto bobKeys = bob.unpublishedOneTimeKeys();
+ bob.markOneTimeKeysAsPublished();
+
+ auto bobIdentityKey = bob.curve25519IdentityKey();
+ auto bobOneTimeKey = std::string{};
+ for (auto [id, key] : bobKeys[curve25519].items()) {
+ bobOneTimeKey = key;
+ }
+
+ // Alice: a device that Bob does not know about
+ // (Bob has never queried her keys or sent her a message)
+ Crypto alice(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+ auto aliceIdentityKey = alice.curve25519IdentityKey();
+
+ // Alice creates an outbound session to Bob and encrypts a message
+ alice.createOutboundSessionWithRandom(
+ genRandomData(Crypto::createOutboundSessionRandomSize()),
+ bobIdentityKey, bobOneTimeKey);
+
+ auto plainText = json{{"test", "mew"}};
+ auto encryptedMsg = alice.encryptOlmWithRandom(
+ genRandomData(Crypto::encryptOlmMaxRandomSize()),
+ plainText, bobIdentityKey);
+
+ // Bob receives the Olm message from Alice
+ auto encJson = makeEncryptedJson(encryptedMsg, aliceIdentityKey);
+
+ auto decryptedOpt = bob.decrypt(encJson);
+ REQUIRE(decryptedOpt);
+
+ auto decryptedJson = json::parse(decryptedOpt.value());
+ REQUIRE(decryptedJson == plainText);
+}
+
+TEST_CASE("Should reuse existing inbound session to encrypt after receiving from unknown device", "[crypto][olm]")
+{
+ Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+ Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+
+ a.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
+ auto k = a.unpublishedOneTimeKeys();
+ a.markOneTimeKeysAsPublished();
+
+ auto oneTimeKey = std::string{};
+ for (auto [id, key] : k[curve25519].items()) {
+ oneTimeKey = key;
+ }
+
+ auto aIdKey = a.curve25519IdentityKey();
+
+ // b (unknown to a) creates outbound session and sends a pre-key message
+ b.createOutboundSessionWithRandom(
+ genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
+
+ auto origJson = json{{"hello", "world"}};
+ auto encryptedMsg = b.encryptOlmWithRandom(
+ genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
+
+ auto encJson = makeEncryptedJson(encryptedMsg, b.curve25519IdentityKey());
+
+ // a decrypts - this creates an inbound session from the pre-key message
+ auto decryptedOpt = a.decrypt(encJson);
+ REQUIRE(decryptedOpt);
+ REQUIRE(json::parse(decryptedOpt.value()) == origJson);
+
+ // Now a can use the inbound session to encrypt back to b
+ using StrMap = immer::map<std::string, std::string>;
+ auto devMap = immer::map<std::string, StrMap>()
+ .set("b", StrMap().set("dev", b.curve25519IdentityKey()));
+
+ auto devices = a.devicesMissingOutboundSessionKey(devMap);
+ // The inbound session should be usable as an outbound session as well,
+ // so no device should be missing an olm session
+ auto expected = immer::map<std::string, immer::flex_vector<std::string>>();
+ REQUIRE(devices == expected);
+}
+
TEST_CASE("Generating and publishing keys should work", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
diff --git a/src/tests/encryption-test-utils.hpp b/src/tests/encryption-test-utils.hpp
new file mode 100644
--- /dev/null
+++ b/src/tests/encryption-test-utils.hpp
@@ -0,0 +1,101 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+#include "factory.hpp"
+#include <catch2/catch_test_macros.hpp>
+
+struct OlmFirstTimeDecryptTestSetup
+{
+ inline OlmFirstTimeDecryptTestSetup()
+ : sender("@example:example.com")
+ , recipient("@foo:example.com")
+ , senderDeviceId("device1")
+ , recipientDeviceId("device2")
+ {
+ using namespace Kazv;
+ using namespace Kazv::Factory;
+
+ auto cryptoA = makeCrypto();
+ auto cryptoB = makeCrypto();
+ // A creates an outbound olm session with B
+ cryptoB.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
+ auto otk = *(cryptoB.unpublishedOneTimeKeys()[CryptoConstants::curve25519].begin());
+ cryptoB.markOneTimeKeysAsPublished();
+ cryptoA.createOutboundSessionWithRandom(
+ genRandomData(Crypto::createOutboundSessionRandomSize()),
+ cryptoB.curve25519IdentityKey(), otk);
+
+ infoA = DeviceKeyInfo{
+ senderDeviceId,
+ cryptoA.ed25519IdentityKey(),
+ cryptoA.curve25519IdentityKey(),
+ "Device-1",
+ DeviceTrustLevel::Verified,
+ };
+ infoB = DeviceKeyInfo{
+ recipientDeviceId,
+ cryptoB.ed25519IdentityKey(),
+ cryptoB.curve25519IdentityKey(),
+ "Device-2",
+ DeviceTrustLevel::Verified,
+ };
+
+ clientA = makeClient(
+ withAttr(&ClientModel::userId, sender)
+ | withAttr(&ClientModel::deviceId, senderDeviceId)
+ | withDevice(recipient, infoB)
+ | withCrypto(cryptoA)
+ );
+ clientWithDevice = makeClient(
+ withAttr(&ClientModel::userId, recipient)
+ | withAttr(&ClientModel::deviceId, recipientDeviceId)
+ | withDevice(sender, infoA)
+ | withCrypto(cryptoB)
+ );
+ clientNoDevice = makeClient(
+ withAttr(&ClientModel::userId, recipient)
+ | withAttr(&ClientModel::deviceId, recipientDeviceId)
+ | withCrypto(cryptoB)
+ );
+ }
+
+ inline Kazv::Event encrypt(Kazv::json pt, bool attachSenderDeviceKeys = false)
+ {
+ using namespace Kazv;
+ auto encrypted = clientA.olmEncryptSplit(
+ Event(pt),
+ {{recipient, {recipientDeviceId}}},
+ {},
+ attachSenderDeviceKeys
+ );
+ auto j = encrypted.at(recipient).at(recipientDeviceId).originalJson().get();
+ j["sender"] = sender;
+ return Event(j);
+ };
+
+ inline Kazv::Event decryptOnly(Kazv::Event e)
+ {
+ using namespace Kazv;
+ auto decryptedText = clientWithDevice.withCrypto([&](Crypto &c) {
+ return c.decrypt(e.originalJson().get());
+ });
+ REQUIRE(decryptedText.has_value());
+ auto decryptedJson = json::parse(decryptedText.value());
+ return e.setDecryptedJson(decryptedJson, Event::Decrypted);
+ };
+
+ std::string sender;
+ std::string recipient;
+ std::string senderDeviceId;
+ std::string recipientDeviceId;
+ Kazv::DeviceKeyInfo infoA;
+ Kazv::DeviceKeyInfo infoB;
+ Kazv::ClientModel clientA;
+ Kazv::ClientModel clientWithDevice;
+ Kazv::ClientModel clientNoDevice;
+};

File Metadata

Mime Type
text/plain
Expires
Sun, Jun 28, 5:41 AM (17 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1633509
Default Alt Text
D306.1782650466.diff (41 KB)

Event Timeline