Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85802641
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
61 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/crypto/crypto.cpp b/src/crypto/crypto.cpp
index 4ebea58..f3ed509 100644
--- a/src/crypto/crypto.cpp
+++ b/src/crypto/crypto.cpp
@@ -1,622 +1,636 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <vector>
#include <zug/transducer/filter.hpp>
#include <vodozemac.h>
#include <nlohmann/json.hpp>
#include <debug.hpp>
#include <event.hpp>
#include <cursorutil.hpp>
#include <types.hpp>
#include "crypto-p.hpp"
#include "session-p.hpp"
#include "crypto-util-p.hpp"
#include "crypto-util.hpp"
#include "time-util.hpp"
namespace Kazv
{
using namespace CryptoConstants;
CryptoPrivate::CryptoPrivate()
: account(std::nullopt)
, valid(false)
{
}
CryptoPrivate::CryptoPrivate(RandomTag, [[maybe_unused]] RandomData data)
: account(std::nullopt)
, valid(true)
{
account = vodozemac::olm::new_account();
}
CryptoPrivate::~CryptoPrivate()
{
}
CryptoPrivate::CryptoPrivate(const CryptoPrivate &that)
: account(std::nullopt)
, uploadedOneTimeKeysCount(that.uploadedOneTimeKeysCount)
, numUnpublishedKeys(that.numUnpublishedKeys)
, knownSessions(that.knownSessions)
, inboundGroupSessions(that.inboundGroupSessions)
, outboundGroupSessions(that.outboundGroupSessions)
{
if (that.valid) {
valid = unpickle(that.pickle());
}
}
std::string CryptoPrivate::pickle() const
{
auto pickleData = account.value()->pickle(VODOZEMAC_PICKLE_KEY);
return static_cast<std::string>(pickleData);
}
bool CryptoPrivate::unpickle(std::string pickleData)
{
account = checkVodozemacError([&]() {
return vodozemac::olm::account_from_pickle(
rust::Str(pickleData),
VODOZEMAC_PICKLE_KEY
);
});
return account.has_value();
}
bool CryptoPrivate::unpickleFromLibolm(std::string pickleData)
{
account = checkVodozemacError([&]() {
return vodozemac::olm::account_from_libolm_pickle(
rust::Str(pickleData),
rust::Slice<const unsigned char>(OLM_PICKLE_KEY.data(), OLM_PICKLE_KEY.size())
);
});
return account.has_value();
}
MaybeString CryptoPrivate::decryptOlm(nlohmann::json content)
{
auto theirCurve25519IdentityKey = content.at("sender_key").get<std::string>();
auto ourCurve25519IdentityKey = curve25519IdentityKey();
if (! content.at("ciphertext").contains(ourCurve25519IdentityKey)) {
return NotBut("Message not intended for us");
}
auto type = content.at("ciphertext").at(ourCurve25519IdentityKey).at("type").get<int>();
auto body = content.at("ciphertext").at(ourCurve25519IdentityKey).at("body").get<std::string>();
auto hasKnownSession = knownSessions.find(theirCurve25519IdentityKey) != knownSessions.end();
if (type == 0) { // pre-key message
bool shouldCreateNewSession =
// there is no possible session
(! hasKnownSession)
// the possible session does not match this message
|| (! knownSessions.at(theirCurve25519IdentityKey).matches(body));
if (shouldCreateNewSession) {
auto created = createInboundSession(theirCurve25519IdentityKey, body);
if (! created) { // cannot create session, thus cannot decrypt
return NotBut("Cannot create session");
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.m_d->takeFirstDecrypted();
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
} else {
if (! hasKnownSession) {
return NotBut("No available session");
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
}
}
MaybeString CryptoPrivate::decryptMegOlm(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, sessionId};
if (inboundGroupSessions.find(k) == inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto msg = content.at("ciphertext").get<std::string>();
auto eventId = eventJson.at("event_id").get<std::string>();
auto originServerTs = eventJson.at("origin_server_ts").get<Timestamp>();
auto &session = inboundGroupSessions.at(k);
return session.decrypt(msg, eventId, originServerTs);
}
}
bool CryptoPrivate::createInboundSession(std::string theirCurve25519IdentityKey,
std::string message)
{
auto s = Session(InboundSessionTag{}, *this,
theirCurve25519IdentityKey, message);
if (s.valid()) {
knownSessions.insert_or_assign(theirCurve25519IdentityKey, std::move(s));
return true;
}
return false;
}
bool CryptoPrivate::reuseOrCreateOutboundGroupSession(
RandomData random, Timestamp timeMs,
std::string roomId, std::optional<MegOlmSessionRotateDesc> desc)
{
bool valid = true;
if (! desc.has_value()) { // force rotate
valid = false;
} else {
auto it = outboundGroupSessions.find(roomId);
if (it == outboundGroupSessions.end()) {
valid = false;
} else {
auto &session = it->second;
if (timeMs - session.creationTimeMs() >= desc.value().ms) {
valid = false;
} else if (session.messageIndex() >= desc.value().messages) {
valid = false;
}
}
}
if (! valid) {
outboundGroupSessions.insert_or_assign(roomId, OutboundGroupSession(RandomTag{}, random, timeMs));
auto &session = outboundGroupSessions.at(roomId);
auto sessionId = session.sessionId();
auto sessionKey = session.sessionKey();
auto k = KeyOfGroupSession{roomId, sessionId};
if (! createInboundGroupSession(k, sessionKey, ed25519IdentityKey())) {
kzo.client.warn() << "Create inbound group session from outbound group session failed. We may not be able to read our own messages." << std::endl;
}
}
return valid;
}
std::size_t Crypto::constructRandomSize()
{
return 0;
}
Crypto::Crypto()
: m_d(new CryptoPrivate{})
{
}
Crypto::Crypto(RandomTag, RandomData data)
: m_d(new CryptoPrivate(RandomTag{}, std::move(data)))
{
}
Crypto::~Crypto() = default;
Crypto::Crypto(const Crypto &that)
: m_d(new CryptoPrivate(*that.m_d))
{
}
Crypto::Crypto(Crypto &&that)
: m_d(std::move(that.m_d))
{
}
Crypto &Crypto::operator=(const Crypto &that)
{
m_d.reset(new CryptoPrivate(*that.m_d));
return *this;
}
Crypto &Crypto::operator=(Crypto &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool Crypto::operator==(const Crypto &that) const
{
return this->m_d == that.m_d;
}
bool Crypto::valid() const
{
return m_d->valid;
}
std::string CryptoPrivate::ed25519IdentityKey() const
{
auto key = account.value()->ed25519_key()->to_base64();
return static_cast<std::string>(key);
}
std::string CryptoPrivate::curve25519IdentityKey() const
{
auto key = account.value()->curve25519_key()->to_base64();
return static_cast<std::string>(key);
}
std::string Crypto::ed25519IdentityKey() const
{
return m_d->ed25519IdentityKey();
}
std::string Crypto::curve25519IdentityKey() const
{
return m_d->curve25519IdentityKey();
}
std::string Crypto::sign(nlohmann::json j)
{
j.erase("signatures");
j.erase("unsigned");
auto str = j.dump();
auto signature = checkVodozemacError([&]() {
return m_d->account.value()->sign(str);
});
if (!signature.has_value()) {
return "";
}
return static_cast<std::string>(signature.value()->to_base64());
}
void Crypto::setUploadedOneTimeKeysCount(immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount)
{
m_d->uploadedOneTimeKeysCount = uploadedOneTimeKeysCount;
}
std::size_t Crypto::maxNumberOfOneTimeKeys() const
{
return m_d->account.value()->max_number_of_one_time_keys();
}
std::size_t Crypto::genOneTimeKeysRandomSize(int num)
{
return 0;
}
void Crypto::genOneTimeKeysWithRandom(RandomData random, int num)
{
assert(random.size() >= genOneTimeKeysRandomSize(num));
m_d->account.value()->generate_one_time_keys(num);
m_d->numUnpublishedKeys += num;
}
nlohmann::json Crypto::unpublishedOneTimeKeys() const
{
auto keys = m_d->account.value()->one_time_keys();
auto ret = nlohmann::json{
{curve25519, nlohmann::json::object()},
};
for (const auto &k : keys) {
auto keyId = static_cast<std::string>(k.key_id);
auto key = static_cast<std::string>(k.key->to_base64());
ret[curve25519][keyId] = key;
}
return ret;
}
void Crypto::markOneTimeKeysAsPublished()
{
m_d->account.value()->mark_keys_as_published();
m_d->numUnpublishedKeys = 0;
}
int Crypto::numUnpublishedOneTimeKeys() const
{
return m_d->numUnpublishedKeys;
}
int Crypto::uploadedOneTimeKeysCount(std::string algorithm) const
{
return m_d->uploadedOneTimeKeysCount[algorithm];
}
MaybeString Crypto::decrypt(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto algo = content.contains("algorithm") ? content.at("algorithm").template get<std::string>() : std::string();
if (algo == olmAlgo) {
return m_d->decryptOlm(std::move(content));
} else if (algo == megOlmAlgo) {
return m_d->decryptMegOlm(eventJson);
}
return NotBut("Algorithm " + algo + " not supported");
}
bool Crypto::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
return m_d->createInboundGroupSession(std::move(k), std::move(sessionKey), std::move(ed25519Key));
}
bool Crypto::hasInboundGroupSession(KeyOfGroupSession k) const
{
return m_d->inboundGroupSessions.find(k) != m_d->inboundGroupSessions.end();
}
bool CryptoPrivate::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
auto session = InboundGroupSession(sessionKey, ed25519Key);
- if (session.valid()) {
- inboundGroupSessions.insert_or_assign(k, std::move(session));
+ if (!session.valid()) {
+ kzo.crypto.warn() << "Invalid session key for: " << k.roomId << ", " << k.sessionId << std::endl;
+ return false;
+ }
+
+ auto currentSessionIt = inboundGroupSessions.find(k);
+ if (currentSessionIt == inboundGroupSessions.end()) {
+ // the session is new, insert it
+ inboundGroupSessions.insert({k, std::move(session)});
return true;
}
- return false;
+
+ // the session already exists, do some check
+ auto ¤tSession = currentSessionIt->second;
+ if (currentSession.ed25519Key() != ed25519Key) {
+ return false;
+ }
+
+ return currentSession.merge(session);
}
bool Crypto::verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key)
{
if (! object.contains("signatures")) {
return false;
}
std::string signature;
try {
signature = object.at("signatures").at(userId).at(ed25519 + ":" + deviceId);
} catch(const std::exception &) {
return false;
}
object.erase("signatures");
object.erase("unsigned");
auto message = object.dump();
auto res = checkVodozemacError([&]() {
auto key = vodozemac::types::ed25519_key_from_base64(ed25519Key);
auto sig = vodozemac::types::ed25519_signature_from_base64(signature);
key->verify(message, *sig);
// It throws if the signature cannot be verified
return true;
});
return res.has_value() && res.value();
}
MaybeString Crypto::getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const
{
auto content = eventJson.at("content");
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, sessionId};
if (m_d->inboundGroupSessions.find(k) == m_d->inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto &session = m_d->inboundGroupSessions.at(k);
return session.ed25519Key();
}
}
std::size_t Crypto::encryptOlmRandomSize(std::string /* theirCurve25519IdentityKey */) const
{
// HACK: To prevent a possible race condition where we call
// encryptedOlmRandomSize() -> randomGenerator.generateRange() ~> [encryptOlm()]
//
// Here, encryptOlm() must be called in the reducer,
// as it changes the status of
// Crypto (so we should not risk any infomation lose in Crypto).
// Also, for the reducer to be pure, we may not call generateRange() in the reducer,
// as *that* is not a pure function. This means it can only be called in an effect,
// or .then() continuation. This means, the sequence from encryptedOlmRandomSize()
// to encryptOlm() can never be atomic. That is, there may be other encryptOlm()
// calls within, and that may increase the random data needed, and as a result,
// we will not have enough random data.
//
// According to the olm headers:
// https://gitlab.matrix.org/matrix-org/olm/-/blob/master/include/olm/ratchet.hh
// The maximum random size needed to encrypt is 32. We use this to ensure we
// will always have enough random data fot the encryption.
return encryptOlmMaxRandomSize();
}
std::size_t Crypto::encryptOlmMaxRandomSize()
{
return 0;
}
nlohmann::json Crypto::encryptOlmWithRandom(
RandomData random, nlohmann::json eventJson, std::string theirCurve25519IdentityKey)
{
assert(random.size() >= encryptOlmRandomSize(theirCurve25519IdentityKey));
try {
auto &session = m_d->knownSessions.at(theirCurve25519IdentityKey);
auto [type, body] = session.encryptWithRandom(random, eventJson.dump());
return nlohmann::json{
{
theirCurve25519IdentityKey, {
{"type", type},
{"body", body}
}
}
};
} catch (const std::exception &) {
return nlohmann::json::object();
}
}
nlohmann::json Crypto::encryptMegOlm(nlohmann::json eventJson)
{
auto roomId = eventJson.at("room_id").get<std::string>();
auto content = eventJson.at("content");
auto type = eventJson.at("type").get<std::string>();
auto jsonToEncrypt = nlohmann::json::object();
jsonToEncrypt["room_id"] = roomId;
jsonToEncrypt["content"] = std::move(content);
jsonToEncrypt["type"] = type;
auto textToEncrypt = std::move(jsonToEncrypt).dump();
auto &session = m_d->outboundGroupSessions.at(roomId);
auto ciphertext = session.encrypt(std::move(textToEncrypt));
return
json{
{"algorithm", CryptoConstants::megOlmAlgo},
// NOTE: we might stop sending sender_key in the future
// as per the Matrix spec
{"sender_key", curve25519IdentityKey()},
{"ciphertext", ciphertext},
{"session_id", session.sessionId()},
};
}
std::size_t Crypto::rotateMegOlmSessionRandomSize()
{
return OutboundGroupSession::constructRandomSize();
}
std::string Crypto::rotateMegOlmSessionWithRandom(RandomData random, Timestamp timeMs, std::string roomId)
{
m_d->reuseOrCreateOutboundGroupSession(
random, timeMs,
roomId, std::nullopt);
return outboundGroupSessionCurrentKey(roomId);
}
std::optional<std::string> Crypto::rotateMegOlmSessionWithRandomIfNeeded(
RandomData random, Timestamp timeMs,
std::string roomId, MegOlmSessionRotateDesc desc)
{
auto oldSessionValid = m_d->reuseOrCreateOutboundGroupSession(random, timeMs, roomId, std::move(desc));
return oldSessionValid ? std::nullopt : std::optional(outboundGroupSessionCurrentKey(roomId));
}
std::string Crypto::outboundGroupSessionInitialKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.initialSessionKey();
}
std::string Crypto::outboundGroupSessionCurrentKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.sessionKey();
}
auto Crypto::devicesMissingOutboundSessionKey(
immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>> keyMap) const -> UserIdToDeviceIdMap
{
auto ret = UserIdToDeviceIdMap{};
for (auto [userId, devices] : keyMap) {
auto unknownDevices =
intoImmer(immer::flex_vector<std::string>{},
zug::filter([=](auto kv) {
auto [deviceId, theirCurve25519IdentityKey] = kv;
return m_d->knownSessions.find(theirCurve25519IdentityKey)
== m_d->knownSessions.end();
})
| zug::map([=](auto kv) {
auto [deviceId, key] = kv;
return deviceId;
}),
devices);
if (! unknownDevices.empty()) {
ret = std::move(ret).set(userId, std::move(unknownDevices));
}
}
return ret;
}
std::size_t Crypto::createOutboundSessionRandomSize()
{
return Session::constructOutboundRandomSize();
}
void Crypto::createOutboundSessionWithRandom(
RandomData random,
std::string theirIdentityKey,
std::string theirOneTimeKey)
{
assert(random.size() >= createOutboundSessionRandomSize());
auto session = Session(OutboundSessionTag{},
RandomTag{},
random,
*m_d,
theirIdentityKey,
theirOneTimeKey);
if (session.valid()) {
m_d->knownSessions.insert_or_assign(theirIdentityKey,
std::move(session));
}
}
nlohmann::json Crypto::toJson() const
{
std::string pickledData = m_d->valid ? m_d->pickle() : std::string();
auto j = nlohmann::json::object({
{"valid", m_d->valid},
{"version", 1},
{"account", std::move(pickledData)},
{"uploadedOneTimeKeysCount", m_d->uploadedOneTimeKeysCount},
{"numUnpublishedKeys", m_d->numUnpublishedKeys},
{"knownSessions", nlohmann::json(m_d->knownSessions)},
{"inboundGroupSessions", nlohmann::json(m_d->inboundGroupSessions)},
{"outboundGroupSessions", nlohmann::json(m_d->outboundGroupSessions)},
});
return j;
}
void Crypto::loadJson(const nlohmann::json &j)
{
m_d->valid = j.contains("valid") ? j["valid"].template get<bool>() : true;
const auto &pickledData = j.at("account").template get<std::string>();
if (m_d->valid) {
if (j.contains("version") && j["version"] == 1) {
m_d->valid = m_d->unpickle(pickledData);
} else {
m_d->valid = m_d->unpickleFromLibolm(pickledData);
}
}
m_d->uploadedOneTimeKeysCount = j.at("uploadedOneTimeKeysCount");
m_d->numUnpublishedKeys = j.at("numUnpublishedKeys");
m_d->knownSessions = j.at("knownSessions").template get<decltype(m_d->knownSessions)>();
m_d->inboundGroupSessions = j.at("inboundGroupSessions").template get<decltype(m_d->inboundGroupSessions)>();
m_d->outboundGroupSessions = j.at("outboundGroupSessions").template get<decltype(m_d->outboundGroupSessions)>();
}
}
diff --git a/src/crypto/crypto.hpp b/src/crypto/crypto.hpp
index b050a0d..0f7edab 100644
--- a/src/crypto/crypto.hpp
+++ b/src/crypto/crypto.hpp
@@ -1,262 +1,271 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <memory>
#include <nlohmann/json.hpp>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <maybe.hpp>
#include "crypto-util.hpp"
#include "time-util.hpp"
namespace Kazv
{
class Session;
struct MegOlmSessionRotateDesc
{
/// The time limit of the megolm session, in milliseconds.
Timestamp ms{};
/// The message limit of the megolm session.
int messages{};
};
struct CryptoPrivate;
class Crypto
{
public:
/**
* Construct an invalid Crypto.
*/
explicit Crypto();
/**
* @return The size of random data needed to construct a Crypto.
*/
static std::size_t constructRandomSize();
/**
* Constructs a Crypto using user-provided random data.
*
* @param data Random data of size at least `constructRandomSize()`.
*/
Crypto(RandomTag, RandomData data);
Crypto(const Crypto &that);
Crypto(Crypto &&that);
Crypto &operator=(const Crypto &that);
Crypto &operator=(Crypto &&that);
~Crypto();
bool operator==(const Crypto &that) const;
/**
* @return whether this Crypto is valid.
*/
bool valid() const;
std::string ed25519IdentityKey() const;
std::string curve25519IdentityKey() const;
std::string sign(nlohmann::json j);
void setUploadedOneTimeKeysCount(immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount);
int uploadedOneTimeKeysCount(std::string algorithm) const;
std::size_t maxNumberOfOneTimeKeys() const;
/**
* Get the size of random data needed to generate `num`
* one-time keys.
*
* @param num The number of one-time keys to generate.
*
* @return The size of random data needed to generate
* `num` one-time keys.
*/
static std::size_t genOneTimeKeysRandomSize(int num);
/**
* Generate `num` one-time keys with user-provided random data.
*
* @param random The random data. Must be of at least size
* `genOneTimeKeysRandomSize(num)`.
* @param num The number of one-time keys to generate.
*/
void genOneTimeKeysWithRandom(RandomData random, int num);
/**
* According to olm.h, this returns an object like
*
* {
* curve25519: {
* "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
* "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
* }
* }
*/
nlohmann::json unpublishedOneTimeKeys() const;
int numUnpublishedOneTimeKeys() const;
void markOneTimeKeysAsPublished();
/// Returns decrypted message if we can decrypt it
/// otherwise returns the error
MaybeString decrypt(nlohmann::json eventJson);
/**
* @return The size of random data needed to encrypt a message
* for the session identified with `theirCurve25519IdentityKey`.
*/
std::size_t encryptOlmRandomSize(std::string theirCurve25519IdentityKey) const;
/**
* @return The maximum size of random data needed to encrypt a message
* for the session identified with `theirCurve25519IdentityKey`.
*/
static std::size_t encryptOlmMaxRandomSize();
/**
* Encrypt `eventJson` with olm, for the recipient identified with `theirCurve25519IdentityKey`.
*
* @param random The random data to use for encryption. Must be of
* at least size `encryptOlmRandomSize(theirCurve25519IdentityKey)`.
* @param eventJson The event json to encrypt.
* @param theirCurve25519IdentityKey The curve25519 identity key of the recipient.
*
* @return A json object that looks like
* ```
* {
* "<their identity key>": {
* "type": <number>,
* "body": "<body>"
* }
* }
* ```
*/
nlohmann::json encryptOlmWithRandom(
RandomData random, nlohmann::json eventJson, std::string theirCurve25519IdentityKey);
/// returns the content template with everything but deviceId
/// eventJson should contain type, room_id and content
nlohmann::json encryptMegOlm(nlohmann::json eventJson);
+ /**
+ * Create or update an inbound group session.
+ *
+ * @param k The key of the group session.
+ * @param sessionKey The session key.
+ * @param ed25519Key The ed25519 identity key of the sender of the m.room_key event.
+ *
+ * @return true iff the session is created or updated.
+ */
bool createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key);
bool hasInboundGroupSession(KeyOfGroupSession k) const;
std::string outboundGroupSessionInitialKey(std::string roomId);
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);
MaybeString getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const;
/**
* @return The size of random data needed for `rotateMegOlmSessionWithRandom()`
* and `rotateMegOlmSessionWithRandomIfNeeded()`.
*/
static std::size_t rotateMegOlmSessionRandomSize();
/**
* Rotate the megolm session using user-provided random data.
*
* @param random The random data. Must be of at least size
* `rotateMegOlmSessionRandomSize()`.
* @param timeMs The creation time of the new megolm session.
* @param roomId The room id of the megolm session to rotate.
*
* @return The new session key.
*/
std::string rotateMegOlmSessionWithRandom(RandomData random, Timestamp timeMs, std::string roomId);
/**
* Rotate the megolm session using user-provided random data,
* if we need to rotate it.
*
* The session will be rotated if and only if
* - The difference between `timeMs` and the creation time of
* the megolm session has reached the time limit in `desc`, OR;
* - The number of messages this megolm session has encrypted has
* reached the message limit in `desc`.
*
* @param random The random data. Must be of at least size
* `rotateMegOlmSessionRandomSize()`.
* @param timeMs The timestamp to judge whether the session
* has reached its time limit. If the megolm session is rotated,
* this will also be the creation time of the new megolm session.
* @param roomId The room id of the megolm session to rotate.
* @param desc The rotation specification of this room.
*
* @return The session key if the session is rotated,
* `std::nullopt` otherwise.
*/
std::optional<std::string> rotateMegOlmSessionWithRandomIfNeeded(
RandomData random,
Timestamp timeMs,
std::string roomId,
MegOlmSessionRotateDesc desc);
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
UserIdToDeviceIdMap devicesMissingOutboundSessionKey(
immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>> keyMap) const;
/**
* @return The size of random data needed for `createOutboundSessionWithRandom()`.
*/
static std::size_t createOutboundSessionRandomSize();
/**
* Create an outbound session using user-provided random data.
*
* @param random The random data to use. It must be at least of
* size `createOutboundSessionRandomSize()`.
* @param theirIdeneityKey The identity key of the recipient.
* @param theirOneTimeKey The one-time key of the recipient.
*/
void createOutboundSessionWithRandom(
RandomData random,
std::string theirIdentityKey,
std::string theirOneTimeKey);
template<class Archive>
void save(Archive & ar, const unsigned int /* version */) const {
ar << toJson().dump();
}
template<class Archive>
void load(Archive &ar, const unsigned int /* version */) {
std::string j;
ar >> j;
loadJson(nlohmann::json::parse(std::move(j)));
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
nlohmann::json toJson() const;
void loadJson(const nlohmann::json &j);
private:
friend class Session;
friend struct SessionPrivate;
std::unique_ptr<CryptoPrivate> m_d;
};
}
BOOST_CLASS_VERSION(Kazv::Crypto, 0)
diff --git a/src/crypto/inbound-group-session.cpp b/src/crypto/inbound-group-session.cpp
index 02759e0..4b1bb50 100644
--- a/src/crypto/inbound-group-session.cpp
+++ b/src/crypto/inbound-group-session.cpp
@@ -1,172 +1,190 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include "inbound-group-session-p.hpp"
#include "crypto-util-p.hpp"
#include <types.hpp>
#include <debug.hpp>
namespace Kazv
{
InboundGroupSessionPrivate::InboundGroupSessionPrivate()
: session(std::nullopt)
{
}
InboundGroupSessionPrivate::InboundGroupSessionPrivate(std::string sessionKey, std::string ed25519Key)
: InboundGroupSessionPrivate()
{
valid = false;
this->ed25519Key = ed25519Key;
auto keyRust = checkVodozemacError([&]() { return vodozemac::megolm::session_key_from_base64(rust::Str(sessionKey)); });
if (!keyRust) {
return;
}
this->session = checkVodozemacError([&]() { return vodozemac::megolm::new_inbound_group_session(*(keyRust.value())); });
if (this->session.has_value()) {
valid = true;
}
}
InboundGroupSessionPrivate::InboundGroupSessionPrivate(const InboundGroupSessionPrivate &that)
: InboundGroupSessionPrivate()
{
ed25519Key = that.ed25519Key;
if (that.valid) {
valid = unpickle(that.pickle());
}
decryptedEvents = that.decryptedEvents;
}
std::string InboundGroupSessionPrivate::pickle() const
{
auto pickleData = this->session.value()->pickle(
VODOZEMAC_PICKLE_KEY);
return static_cast<std::string>(pickleData);
}
bool InboundGroupSessionPrivate::unpickle(std::string pickleData)
{
this->session = checkVodozemacError([&]() {
return vodozemac::megolm::inbound_group_session_from_pickle(pickleData, VODOZEMAC_PICKLE_KEY);
});
return this->session.has_value();
}
bool InboundGroupSessionPrivate::unpickleFromLibolm(std::string pickleData)
{
this->session = checkVodozemacError([&]() {
return vodozemac::megolm::inbound_group_session_from_libolm_pickle(pickleData, rust::Slice<const unsigned char>(OLM_PICKLE_KEY.data(), OLM_PICKLE_KEY.size()));
});
return this->session.has_value();
}
InboundGroupSession::InboundGroupSession()
: m_d(new InboundGroupSessionPrivate)
{
}
InboundGroupSession::InboundGroupSession(std::string sessionKey, std::string ed25519Key)
: m_d(new InboundGroupSessionPrivate(std::move(sessionKey), std::move(ed25519Key)))
{
}
InboundGroupSession::~InboundGroupSession() = default;
InboundGroupSession::InboundGroupSession(const InboundGroupSession &that)
: m_d(new InboundGroupSessionPrivate(*that.m_d))
{
}
InboundGroupSession::InboundGroupSession(InboundGroupSession &&that)
: m_d(std::move(that.m_d))
{
}
InboundGroupSession &InboundGroupSession::operator=(const InboundGroupSession &that)
{
m_d.reset(new InboundGroupSessionPrivate(*that.m_d));
return *this;
}
InboundGroupSession &InboundGroupSession::operator=(InboundGroupSession &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool InboundGroupSession::valid() const
{
return m_d && m_d->valid;
}
MaybeString InboundGroupSession::decrypt(std::string message, std::string eventId, std::int_fast64_t originServerTs)
{
auto messageRust = checkVodozemacError([&]() { return vodozemac::megolm::megolm_message_from_base64(rust::Str(message)); });
if (!messageRust.has_value()) {
return NotBut(messageRust.reason());
}
auto decrypted = checkVodozemacError([&]() { return m_d->session.value()->decrypt(*(messageRust.value())); });
if (!decrypted.has_value()) {
return NotBut(decrypted.reason());
}
auto [plainText, messageIndex] = *decrypted;
// Check for possible replay attack
auto keyForThisMsg = KeyOfDecryptedEvent{eventId, originServerTs};
if (! m_d->decryptedEvents.find(messageIndex)) {
m_d->decryptedEvents = std::move(m_d->decryptedEvents)
.set(messageIndex, keyForThisMsg);
} else { // already decrypted in the past
auto key = m_d->decryptedEvents.at(messageIndex);
if (key != keyForThisMsg) {
return NotBut("This message has been decrypted in the past, but eventId or originServerTs does not match");
}
}
return std::string(plainText.begin(), plainText.end());
}
std::string InboundGroupSession::ed25519Key() const
{
return m_d->ed25519Key;
}
+ bool InboundGroupSession::merge(InboundGroupSession &that)
+ {
+ if (!valid() || !that.valid()) {
+ return false;
+ }
+
+ auto merged = checkVodozemacError([this, &that]() {
+ return m_d->session.value()->merge(*that.m_d->session.value());
+ });
+
+ if (!merged.has_value()) {
+ return false;
+ } else {
+ m_d->session = std::move(merged);
+ return true;
+ }
+ }
+
void to_json(nlohmann::json &j, const InboundGroupSession &s)
{
j = nlohmann::json::object();
j["version"] = 1;
j["ed25519Key"] = s.m_d->ed25519Key;
j["valid"] = s.m_d->valid;
j["decryptedEvents"] = s.m_d->decryptedEvents;
if (s.m_d->valid) {
j["session"] = s.m_d->pickle();
}
}
void from_json(const nlohmann::json &j, InboundGroupSession &s)
{
s.m_d->ed25519Key = j.at("ed25519Key");
s.m_d->valid = j.at("valid");
s.m_d->decryptedEvents = j.at("decryptedEvents");
if (s.m_d->valid) {
if (j.contains("version") && j["version"] == 1) { // vodozemac format
s.m_d->valid = s.m_d->unpickle(j.at("session"));
} else { // libolm format
s.m_d->valid = s.m_d->unpickleFromLibolm(j.at("session"));
}
}
}
}
diff --git a/src/crypto/inbound-group-session.hpp b/src/crypto/inbound-group-session.hpp
index a89c490..394c680 100644
--- a/src/crypto/inbound-group-session.hpp
+++ b/src/crypto/inbound-group-session.hpp
@@ -1,43 +1,51 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <memory>
#include <maybe.hpp>
#include <event.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
struct InboundGroupSessionPrivate;
class InboundGroupSession
{
public:
explicit InboundGroupSession();
explicit InboundGroupSession(std::string sessionKey, std::string ed25519Key);
InboundGroupSession(const InboundGroupSession &that);
InboundGroupSession(InboundGroupSession &&that);
InboundGroupSession &operator=(const InboundGroupSession &that);
InboundGroupSession &operator=(InboundGroupSession &&that);
~InboundGroupSession();
MaybeString decrypt(std::string message, std::string eventId, Timestamp originServerTs);
bool valid() const;
std::string ed25519Key() const;
+
+ /**
+ * Try to merge this session with another session.
+ *
+ * @param that The other session to merge with.
+ * @return true iff the two sessions are mergeable.
+ */
+ bool merge(InboundGroupSession &that);
private:
friend void to_json(nlohmann::json &j, const InboundGroupSession &s);
friend void from_json(const nlohmann::json &j, InboundGroupSession &s);
std::unique_ptr<InboundGroupSessionPrivate> m_d;
};
}
diff --git a/src/tests/crypto-test.cpp b/src/tests/crypto-test.cpp
index d692257..541126a 100644
--- a/src/tests/crypto-test.cpp
+++ b/src/tests/crypto-test.cpp
@@ -1,630 +1,730 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
#include <sstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <crypto/crypto.hpp>
#include <aes-256-ctr.hpp>
#include <base64.hpp>
#include <sha256.hpp>
#include "crypto/crypto-test-resource.hpp"
using namespace Kazv;
using namespace Kazv::CryptoConstants;
using IAr = boost::archive::text_iarchive;
using OAr = boost::archive::text_oarchive;
static const auto resource = cryptoDumpResource();
json makeEncryptedJson(json ciphertext, std::string senderKey)
{
return json{{"content", {
{"algorithm", olmAlgo},
{"ciphertext", std::move(ciphertext)},
{"sender_key", std::move(senderKey)},
}}};
}
template<class T>
static void serializeDup(const T &in, T &out)
{
std::stringstream stream;
{
auto ar = OAr(stream);
ar << in;
}
{
auto ar = IAr(stream);
ar >> out;
}
}
+static bool doesDecryptTo(Crypto &crypto, const nlohmann::json &encryptedEvent, const nlohmann::json &plainText)
+{
+ auto res = crypto.decrypt(encryptedEvent);
+ if (!res) {
+ return false;
+ }
+ auto decrypted = json::parse(res.value());
+ return decrypted == plainText;
+}
+
TEST_CASE("Crypto constructors", "[crypto]")
{
Crypto crypto;
REQUIRE(!crypto.valid());
Crypto crypto2(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
REQUIRE(crypto2.valid());
}
TEST_CASE("Crypto conversion from libolm to vodozemac", "[crypto]")
{
Crypto a;
a.loadJson(resource["a"]);
Crypto b;
b.loadJson(resource["b"]);
// encrypt with existing sessions
auto aIdKey = a.curve25519IdentityKey();
auto origJson = json{{"test", "mew"}};
{
auto encryptedMsg = b.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto decryptedOpt = a.decrypt(makeEncryptedJson(encryptedMsg, b.curve25519IdentityKey()));
REQUIRE(decryptedOpt);
}
// encrypt/decrypt with new sessions
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
{
Crypto c(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
auto encryptedMsg = c.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto decryptedOpt = a.decrypt(makeEncryptedJson(encryptedMsg, c.curve25519IdentityKey()));
REQUIRE(decryptedOpt.reason() == "");
REQUIRE(decryptedOpt);
REQUIRE(decryptedOpt.value() == origJson.dump());
}
}
TEST_CASE("Crypto should be copyable", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto oneTimeKeys = crypto.unpublishedOneTimeKeys();
Crypto cryptoClone(crypto);
REQUIRE(crypto.ed25519IdentityKey() == cryptoClone.ed25519IdentityKey());
REQUIRE(crypto.curve25519IdentityKey() == cryptoClone.curve25519IdentityKey());
auto oneTimeKeys2 = cryptoClone.unpublishedOneTimeKeys();
REQUIRE(oneTimeKeys == oneTimeKeys2);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == cryptoClone.numUnpublishedOneTimeKeys());
}
TEST_CASE("Crypto should be serializable", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto oneTimeKeys = crypto.unpublishedOneTimeKeys();
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(crypto.ed25519IdentityKey() == cryptoClone.ed25519IdentityKey());
REQUIRE(crypto.curve25519IdentityKey() == cryptoClone.curve25519IdentityKey());
auto oneTimeKeys2 = cryptoClone.unpublishedOneTimeKeys();
REQUIRE(oneTimeKeys == oneTimeKeys2);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == cryptoClone.numUnpublishedOneTimeKeys());
}
TEST_CASE("Invalid Crypto should be serializable", "[crypto]")
{
Crypto crypto;
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(!cryptoClone.valid());
}
TEST_CASE("Serialize Crypto with an OutboundGroupSession", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
std::string roomId = "!example:example.org";
auto desc = MegOlmSessionRotateDesc{500000 /* ms */, 100 /* messages */};
crypto.rotateMegOlmSessionWithRandom(genRandomData(Crypto::rotateMegOlmSessionRandomSize()), currentTimeMs(), roomId);
Crypto cryptoClone;
serializeDup(crypto, cryptoClone);
REQUIRE(! cryptoClone.rotateMegOlmSessionWithRandomIfNeeded(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()), currentTimeMs(),
roomId, desc).has_value());
}
TEST_CASE("Generating and publishing keys should work", "[crypto]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 1);
crypto.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 2);
crypto.markOneTimeKeysAsPublished();
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 0);
}
TEST_CASE("Should reuse existing inbound session to encrypt", "[crypto]")
{
Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
a.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
// Get A publish the key and send to B
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
auto aIdKey = a.curve25519IdentityKey();
b.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), aIdKey, oneTimeKey);
auto origJson = json{{"test", "mew"}};
auto encryptedMsg = b.encryptOlmWithRandom(genRandomData(Crypto::encryptOlmMaxRandomSize()), origJson, aIdKey);
auto encJson = json{
{"content",
{
{"algorithm", olmAlgo},
{"ciphertext", encryptedMsg},
{"sender_key", b.curve25519IdentityKey()}
}
}
};
auto decryptedOpt = a.decrypt(encJson);
REQUIRE(decryptedOpt);
auto decryptedJson = json::parse(decryptedOpt.value());
REQUIRE(decryptedJson == origJson);
using StrMap = immer::map<std::string, std::string>;
auto devMap = immer::map<std::string, StrMap>()
.set("b", StrMap().set("dev", b.curve25519IdentityKey()));
Crypto aClone{a};
auto devices = a.devicesMissingOutboundSessionKey(devMap);
auto devicesAClone = aClone.devicesMissingOutboundSessionKey(devMap);
// No device should be missing an olm session, as A has received an
// inbound olm session before.
auto expected = immer::map<std::string, immer::flex_vector<std::string>>();
REQUIRE(devices == expected);
REQUIRE(devicesAClone == expected);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
std::string original = "test for aes-256-ctr";
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR with any sequence type", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
std::string oStr = "test for aes-256-ctr";
std::vector<unsigned char> original(oStr.begin(), oStr.end());
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR in a non-destructive way", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
std::string original = "test for aes-256-ctr";
auto [next, encrypted] = desc.process(original);
auto [next2, encrypted2] = desc.process(original);
REQUIRE(encrypted == encrypted2);
auto [next3, decrypted] = desc.process(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Encrypt and decrypt AES-256-CTR in batches", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
std::string original = "test for aes-256-ctr";
std::string orig2 = "another test string...";
auto [next, encrypted] = desc.process(original);
auto [next2, encrypted2] = next.process(orig2);
auto [next3, decrypted] = desc.process(encrypted + encrypted2);
REQUIRE(original + orig2 == decrypted);
}
TEST_CASE("AES-256-CTR should be movable", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = std::move(desc);
REQUIRE(desc2.valid());
REQUIRE(! desc.valid());
std::string original = "test for aes-256-ctr";
std::string encrypted;
// Can be moved from itself
desc2 = std::move(desc2);
REQUIRE(desc2.valid());
std::tie(desc2, encrypted) = std::move(desc2).process(original);
REQUIRE(desc2.valid());
}
TEST_CASE("AES-256-CTR should be copyable", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = desc;
REQUIRE(desc2.valid());
REQUIRE(desc.valid());
desc = AES256CTRDesc::fromRandom(RandomData{});
std::string original = "test for aes-256-ctr";
std::string encrypted;
REQUIRE(desc2.valid());
std::tie(desc2, encrypted) = desc2.process(original);
REQUIRE(desc2.valid());
}
TEST_CASE("Construct AES-256-CTR from known key and iv", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto desc2 = AES256CTRDesc(desc.key(), desc.iv());
REQUIRE(desc2.valid());
REQUIRE(desc.key() == desc2.key());
REQUIRE(desc.iv() == desc2.iv());
}
TEST_CASE("AES-256-CTR validity check", "[crypto][aes256ctr]")
{
SECTION("Not enough random, should reject") {
ByteArray random = genRandom(AES256CTRDesc::randomSize - 1);
auto desc = AES256CTRDesc::fromRandom(random);
REQUIRE(! desc.valid());
}
SECTION("More than enough random, should accept") {
ByteArray random = genRandom(AES256CTRDesc::randomSize + 1);
auto desc = AES256CTRDesc::fromRandom(random);
REQUIRE(desc.valid());
}
}
TEST_CASE("AES256CTRDesc::fromRandom() should leave the lower 8 bytes as 0 for the counter", "[crypto][aes256ctr]")
{
auto r = genRandom(AES256CTRDesc::randomSize);
auto desc = AES256CTRDesc::fromRandom(r);
auto iv = decodeBase64(desc.iv());
REQUIRE(iv.size() == AES256CTRDesc::ivSize);
REQUIRE(std::all_of(iv.begin() + AES256CTRDesc::ivSizeInit, iv.end(), [](auto ch) { return ch == 0; }));
auto desc2 = desc;
std::string original = "test for aes-256-ctr";
auto encrypted = desc.processInPlace(original);
auto decrypted = desc2.processInPlace(encrypted);
REQUIRE(original == decrypted);
}
TEST_CASE("Base64 encoder and decoder", "[crypto][base64]")
{
std::string orig = "The Quick Brown Fox Jumps Over the Lazy Dog";
// no padding
std::string expected = "VGhlIFF1aWNrIEJyb3duIEZveCBKdW1wcyBPdmVyIHRoZSBMYXp5IERvZw";
std::string encoded = encodeBase64(orig);
REQUIRE(encoded == expected);
std::string decoded = decodeBase64(encoded);
REQUIRE(decoded == orig);
}
TEST_CASE("Urlsafe base64 encoder and decoder", "[crypto][base64]")
{
std::string orig = "The Quick Brown Fox Jumps Over the Lazy Dog";
// no padding
std::string expected = "VGhlIFF1aWNrIEJyb3duIEZveCBKdW1wcyBPdmVyIHRoZSBMYXp5IERvZw";
std::string encoded = encodeBase64(orig, Base64Opts::urlSafe);
REQUIRE(encoded == expected);
std::string decoded = decodeBase64(encoded, Base64Opts::urlSafe);
REQUIRE(decoded == orig);
}
TEST_CASE("Base64 encoder and decoder, example from Matrix specs", "[crypto][base64]")
{
std::string orig = "JGLn/yafz74HB2AbPLYJWIVGnKAtqECOBf11yyXac2Y";
std::string decoded = decodeBase64(orig);
std::string encoded = encodeBase64(decoded);
REQUIRE(encoded == orig);
}
TEST_CASE("Urlsafe base64 encoder and decoder, example from Matrix specs", "[crypto][base64]")
{
std::string orig = "JGLn_yafz74HB2AbPLYJWIVGnKAtqECOBf11yyXac2Y";
std::string decoded = decodeBase64(orig, Base64Opts::urlSafe);
std::string encoded = encodeBase64(decoded, Base64Opts::urlSafe);
REQUIRE(encoded == orig);
}
TEST_CASE("SHA256 hashing support", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
hash.processInPlace(message1);
auto res = hash.get();
auto expected = std::string("Y2QCZISah8kDVhKdmeoWXjeqX6vB/qRpBt8afKUNtJI");
REQUIRE(res == expected);
}
TEST_CASE("SHA256 hashing streaming", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
auto message2 = std::string("abcdefghijklmn");
hash.processInPlace(message1);
hash.processInPlace(message2);
auto res = hash.get();
auto hash2 = SHA256Desc{};
hash2.processInPlace(message1 + message2);
auto expected = hash2.get();
REQUIRE(res == expected);
}
TEST_CASE("SHA256Desc should be copyable", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
auto message2 = std::string("abcdefghijklmn");
hash.processInPlace(message1);
auto hash2 = hash;
hash.processInPlace(message2);
hash2.processInPlace(message2);
auto res = hash.get();
auto res2 = hash2.get();
REQUIRE(res == res2);
}
TEST_CASE("SHA256Desc should be self-copyable and -movable", "[crypto][sha256]")
{
auto hash = SHA256Desc{};
auto message1 = std::string("12345678910");
hash = hash.process(message1);
auto message2 = std::string("abcdefghijklmn");
hash = std::move(hash).process(message2);
auto hash2 = SHA256Desc{};
hash2.processInPlace(message1 + message2);
auto res = hash.get();
auto res2 = hash2.get();
REQUIRE(res == res2);
}
TEST_CASE("SHA256 should accept any range type", "[crypto][sha256]")
{
std::string msg = "12345678910";
std::vector<char> arr(msg.begin(), msg.end());
auto hash = SHA256Desc{};
auto res1 = hash.process(arr).get();
auto res2 = std::move(hash).process(arr).get();
// after moving, hash is no longer valid, reset it here
hash = SHA256Desc{};
hash.processInPlace(arr);
auto res3 = hash.get();
hash = SHA256Desc{};
auto reference = hash.process(msg).get();
REQUIRE(res1 == res2);
REQUIRE(res1 == res3);
REQUIRE(res1 == reference);
}
+TEST_CASE("Crypto::createInboundGroupSession should not allow session key replacement attacks", "[crypto][group-session]")
+{
+ std::string roomId = "!someroom:example.com";
+ Crypto a(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+
+ // creating a outbound group session will add it to inbound group sessions
+ auto initialKey = a.rotateMegOlmSessionWithRandom(
+ genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
+ 0,
+ roomId
+ );
+
+ // encrypt to get the session id
+ auto plainText = R"({
+ "content": {},
+ "type": "m.room.message",
+ "room_id": "!someroom:example.com"
+ })"_json;
+ auto encryptedContent = a.encryptMegOlm(plainText);
+ auto encryptedEvent = json{
+ {"event_id", "$some-event-id1"},
+ {"origin_server_ts", 1719196953000},
+ {"content", encryptedContent},
+ {"type", "m.room.encrypted"},
+ {"room_id", roomId},
+ };
+
+ // message index is currently at 1
+ auto currentKey = a.outboundGroupSessionCurrentKey(roomId);
+
+ auto sessionId = encryptedContent["session_id"].template get<std::string>();
+
+ auto plainText2 = R"({
+ "content": {"a": "b"},
+ "type": "m.room.message",
+ "room_id": "!someroom:example.com"
+ })"_json;
+ auto encryptedContent2 = a.encryptMegOlm(plainText2);
+
+ Crypto b(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+ Crypto malice(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+
+ auto encryptedEvent2 = json{
+ {"event_id", "$some-event-id"},
+ {"origin_server_ts", 1719196953000},
+ {"content", encryptedContent2},
+ {"type", "m.room.encrypted"},
+ {"room_id", roomId},
+ };
+
+ auto keyOfSession = KeyOfGroupSession{roomId, sessionId};
+ auto created = b.createInboundGroupSession(keyOfSession, currentKey, a.ed25519IdentityKey());
+ REQUIRE(created);
+ REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
+
+ SECTION("it should reject if the identity key is not the same") {
+ auto updated = b.createInboundGroupSession(keyOfSession, initialKey, malice.ed25519IdentityKey());
+ REQUIRE(!updated);
+ }
+
+ SECTION("it should reject if the given session key does not belong to the same session") {
+ auto anotherSessionKey = a.rotateMegOlmSessionWithRandom(
+ genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
+ 0,
+ roomId
+ );
+ auto updated = b.createInboundGroupSession(keyOfSession, anotherSessionKey, a.ed25519IdentityKey());
+ REQUIRE(!updated);
+ }
+
+ SECTION("it should prevent replay attack if merging with itself") {
+ auto updated = b.createInboundGroupSession(keyOfSession, currentKey, a.ed25519IdentityKey());
+ REQUIRE(updated);
+
+ REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
+ auto replay = encryptedEvent2;
+ replay["event_id"] = "$some-other-id";
+ REQUIRE(!b.decrypt(replay).has_value());
+ }
+
+ SECTION("it should prevent replay attack if session is updated") {
+ auto updated = b.createInboundGroupSession(keyOfSession, initialKey, a.ed25519IdentityKey());
+ REQUIRE(updated);
+
+ REQUIRE(doesDecryptTo(b, encryptedEvent2, plainText2));
+ REQUIRE(doesDecryptTo(b, encryptedEvent, plainText));
+ auto replay = encryptedEvent2;
+ replay["event_id"] = "$some-other-id";
+ REQUIRE(!b.decrypt(replay).has_value());
+ }
+}
+
TEST_CASE("Crypto::hasInboundGroupSession", "[crypto][group-session]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
// creating a outbound group session will add it to inbound group sessions
crypto.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
"!someroom:example.com"
);
// encrypt to get the session id
auto encryptedContent = crypto.encryptMegOlm(R"({
"content": {},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json);
auto sessionId = encryptedContent["session_id"].template get<std::string>();
REQUIRE(crypto.hasInboundGroupSession(KeyOfGroupSession{
"!someroom:example.com",
sessionId,
}));
REQUIRE(!crypto.hasInboundGroupSession(KeyOfGroupSession{
"!someroom:example.com",
sessionId + "something something",
}));
}
TEST_CASE("Crypto::decrypt(MegOlmEvent)", "[crypto][group-session]")
{
Crypto crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
crypto.rotateMegOlmSessionWithRandom(
genRandomData(Crypto::rotateMegOlmSessionRandomSize()),
0,
"!someroom:example.com"
);
auto plainText = R"({
"content": {"body": "something"},
"type": "m.room.message",
"room_id": "!someroom:example.com"
})"_json;
// encrypt to get the session id
auto encryptedContent = crypto.encryptMegOlm(plainText);
auto encryptedEvent = json{
{"event_id", "$some-event-id"},
{"origin_server_ts", 1719196953000},
{"content", encryptedContent},
{"type", "m.room.encrypted"},
{"room_id", "!someroom:example.com"},
};
- auto res = crypto.decrypt(encryptedEvent);
- auto decrypted = json::parse(res.value());
- REQUIRE(decrypted == plainText);
+ REQUIRE(doesDecryptTo(crypto, encryptedEvent, plainText));
}
TEST_CASE("KeyOfGroupSession serialization", "[crypto][group-session]")
{
{
auto k = KeyOfGroupSession{"!someroom:example.com", "some-session-id"};
json j = k;
REQUIRE(j == json{
{"roomId", "!someroom:example.com"},
{"sessionId", "some-session-id"},
});
}
{
json j{
{"roomId", "!someroom:example.com"},
{"senderKey", "some-key"}, // legacy version
{"sessionId", "some-session-id"},
};
auto k = j.template get<KeyOfGroupSession>();
REQUIRE(k == KeyOfGroupSession{"!someroom:example.com", "some-session-id"});
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 8, 5:25 PM (1 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784147
Default Alt Text
(61 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment