Page MenuHomePhorge

crypto-test.cpp
No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None

crypto-test.cpp

/*
* 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"},
};
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

Mime Type
text/x-c++
Expires
Tue, Jan 20, 9:47 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
972892
Default Alt Text
crypto-test.cpp (21 KB)

Event Timeline