Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85630002
D306.1786204725.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
32 KB
Referenced Files
None
Subscribers
None
D306.1786204725.diff
View Options
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,10 @@
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");
-
- auto deviceInfoOpt = m.deviceLists.findByCurve25519Key(e.sender(), senderCurve25519Key);
+ 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 +163,6 @@
);
}
- auto deviceInfo = deviceInfoOpt.value();
-
if (! (plainJson.at("sender") == e.sender())) {
kzo.client.dbg() << "Sender does not match, thus invalid" << std::endl;
return cannotDecryptEvent(
@@ -228,6 +196,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 +236,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 +262,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 +525,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
@@ -36,47 +36,66 @@
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;
}
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,6 +149,46 @@
}
}
+ auto DeviceListTracker::findByOlmEvent(Event e) const
+ -> std::pair<FindByOlmEventStatus, DeviceKeyInfo>
+ {
+ if (!(e.encrypted() && e.decrypted())) {
+ return {NotFound, {}};
+ }
+
+ if (e.raw().get().contains("sender_device_keys")) {
+ auto deviceKeys = e.raw().get().at("sender_device_keys").template get<Api::QueryKeysJob::DeviceInformation>();
+ if (e.originalJson().get().contains("/content/sender_key"_json_pointer)) {
+ auto k = e.originalJson().get().at("/content/sender_key"_json_pointer).template get<std::string>();
+
+ }
+ 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, {}};
+ }
+ } else {
+ 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();
+ auto existingDeviceOpt = findByCurve25519Key(e.sender(), senderCurve25519Key);
+ if (existingDeviceOpt.has_value()) {
+ return {InTracker, existingDeviceOpt.value()};
+ } else {
+ return {NotFound, {}};
+ }
+ }
+ }
+
static bool cryptographicallyEqual(DeviceKeyInfo a, DeviceKeyInfo b)
{
a.displayName = std::nullopt;
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
@@ -7,7 +7,9 @@
#include <libkazv-config.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 +22,135 @@
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")
+{
+ std::string sender = "@example:example.com";
+ std::string recipientId = "@foo:example.com";
+ 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);
+
+ auto infoA = DeviceKeyInfo{
+ "device1",
+ cryptoA.ed25519IdentityKey(),
+ cryptoA.curve25519IdentityKey(),
+ "Device-1",
+ DeviceTrustLevel::Verified,
+ };
+ auto infoB = DeviceKeyInfo{
+ "device2",
+ cryptoB.ed25519IdentityKey(),
+ cryptoB.curve25519IdentityKey(),
+ "Device-2",
+ DeviceTrustLevel::Verified,
+ };
+ auto clientA = makeClient(
+ withAttr(&ClientModel::userId, sender)
+ | withAttr(&ClientModel::deviceId, "device1")
+ | withDevice(recipientId, infoB)
+ | withCrypto(cryptoA)
+ );
+ auto clientWithDevice = makeClient(
+ withAttr(&ClientModel::userId, recipientId)
+ | withAttr(&ClientModel::deviceId, "device2")
+ | withDevice(sender, infoA)
+ | withCrypto(cryptoB)
+ );
+ auto clientNoDevice = makeClient(
+ withAttr(&ClientModel::userId, recipientId)
+ | withAttr(&ClientModel::deviceId, "device2")
+ | withCrypto(cryptoB)
+ );
+
+ auto plainText = json{
+ {"content", json::object()},
+ {"type", "moe.kazv.mxc.xxx"},
+ };
+
+ auto encrypt = [&clientA, recipientId, sender](json pt, bool attachSenderDeviceKeys = false) {
+ auto encrypted = clientA.olmEncryptSplit(
+ Event(pt),
+ {{recipientId, {"device2"}}},
+ {},
+ attachSenderDeviceKeys
+ );
+ auto j = encrypted.at(recipientId).at("device2").originalJson().get();
+ j["sender"] = sender;
+ return Event(j);
+ };
+
+ auto decrypt = [&clientWithDevice](Event e) {
+ 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);
+ };
+
+ SECTION("without sender_device_keys") {
+ auto encrypted = encrypt(plainText);
+ auto e = decrypt(encrypted);
+ auto [st, ki] = clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InTracker);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == cryptoA.ed25519IdentityKey());
+
+ std::tie(st, ki) = clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::NotFound);
+ }
+
+ SECTION("with valid sender_device_keys") {
+ auto encrypted = encrypt(plainText, /* attachSenderDeviceKeys = */ true);
+ auto e = decrypt(encrypted);
+ auto [st, ki] = clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InTracker);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == cryptoA.ed25519IdentityKey());
+
+ std::tie(st, ki) = clientNoDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InEvent);
+ REQUIRE(ki.deviceId == "device1");
+ REQUIRE(ki.ed25519Key == cryptoA.ed25519IdentityKey());
+ }
+
+ SECTION("with invalid sender_device_keys (bad signature)") {
+ // the following creates a bad signature
+ auto fakeClientA = clientA;
+ fakeClientA.deviceId = "device3";
+ plainText["sender_device_keys"] = fakeClientA.makeSelfDeviceKeys();
+ plainText["sender_device_keys"]["device_id"] = "device1";
+
+ auto encrypted = encrypt(plainText);
+ auto e = decrypt(encrypted);
+ auto [st, ki] = clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::NotFound);
+
+ std::tie(st, ki) = 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 = clientA;
+ fakeClientA.deviceId = "device3";
+ plainText["sender_device_keys"] = fakeClientA.makeSelfDeviceKeys();
+
+ auto encrypted = encrypt(plainText);
+ auto e = decrypt(encrypted);
+ auto [st, ki] = clientWithDevice.deviceLists.findByOlmEvent(e);
+ REQUIRE(st == DeviceListTracker::InEvent);
+
+ std::tie(st, ki) = 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
@@ -444,6 +444,157 @@
}
}
+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 with sender_device_keys", "[client][encryption][olm]")
+{
+ std::string sender = "@example:example.com";
+ std::string recipientId = "@foo:example.com";
+ 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);
+
+ auto infoA = DeviceKeyInfo{
+ "device1",
+ cryptoA.ed25519IdentityKey(),
+ cryptoA.curve25519IdentityKey(),
+ "Device-1",
+ DeviceTrustLevel::Verified,
+ };
+ auto infoB = DeviceKeyInfo{
+ "device2",
+ cryptoB.ed25519IdentityKey(),
+ cryptoB.curve25519IdentityKey(),
+ "Device-2",
+ DeviceTrustLevel::Verified,
+ };
+ auto clientA = makeClient(
+ withAttr(&ClientModel::userId, sender)
+ | withAttr(&ClientModel::deviceId, "device1")
+ | withDevice(recipientId, infoB)
+ | withCrypto(cryptoA)
+ );
+ auto clientWithDevice = makeClient(
+ withAttr(&ClientModel::userId, recipientId)
+ | withAttr(&ClientModel::deviceId, "device2")
+ | withDevice(sender, infoA)
+ | withCrypto(cryptoB)
+ );
+ auto clientNoDevice = makeClient(
+ withAttr(&ClientModel::userId, recipientId)
+ | withAttr(&ClientModel::deviceId, "device2")
+ | withCrypto(cryptoB)
+ );
+
+ auto plainText = json{
+ {"content", json::object()},
+ {"type", "moe.kazv.mxc.xxx"},
+ };
+
+ auto encrypt = [&clientA, recipientId, sender](json pt, bool attachSenderDeviceKeys = false) {
+ auto encrypted = clientA.olmEncryptSplit(
+ Event(pt),
+ {{recipientId, {"device2"}}},
+ {},
+ attachSenderDeviceKeys
+ );
+ auto j = encrypted.at(recipientId).at("device2").originalJson().get();
+ j["sender"] = sender;
+ return Event(j);
+ };
+
+ SECTION("good keys") {
+ auto encrypted = encrypt(plainText, /* attachSenderDeviceKeys = */ true);
+ auto ev = decryptEvent(clientNoDevice, encrypted);
+ REQUIRE(ev.decrypted());
+ }
+}
+
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()));
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Aug 8, 8:58 AM (16 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724373
Default Alt Text
D306.1786204725.diff (32 KB)
Attached To
Mode
D306: Validate sender_device_keys for olm-encrypted events
Attached
Detach File
Event Timeline
Log In to Comment