Page MenuHomePhorge

D306.1786204725.diff
No OneTemporary

Size
32 KB
Referenced Files
None
Subscribers
None

D306.1786204725.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,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

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)

Event Timeline