Page MenuHomePhorge

No OneTemporary

Size
24 KB
Referenced Files
None
Subscribers
None
diff --git a/src/crypto/aes-256-ctr.hpp b/src/crypto/aes-256-ctr.hpp
index 2926c48..3ec4b2b 100644
--- a/src/crypto/aes-256-ctr.hpp
+++ b/src/crypto/aes-256-ctr.hpp
@@ -1,187 +1,193 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <memory>
#include <copy-helper.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
namespace
{
using std::begin;
using std::end;
}
class AES256CTRDesc
{
public:
/// The optimal block size for this cipher.
inline static const int optimalBlockSize{16};
/// The key size for this cipher.
inline static const int keySize{32};
/// The iv size for this cipher.
inline static const int ivSize{16}; // AES block size
+ /// The size used for initialization of the iv for this cipher.
+ /// In CTR mode, there should be zero padding in the lower part of the iv, so the counter can increase from 0 to (the number of blocks - 1).
+ /// We use 8 bytes for random data, and the lower 8 bytes for the counter. This allows us to encrypt 2**(8*8) blocks, which is 2**(8*8) * 16 bytes = 268,435,456 TiB data
+ inline static const int ivSizeInit{8};
/// Random size to generate a new cipher, provided in fromRandom().
- inline static const int randomSize{keySize + ivSize};
+ inline static const int randomSize{keySize + ivSizeInit};
using DataT = std::string;
private:
struct RawTag {};
/**
* Constructs an AES-256-CTR cipher using `key` and `iv`.
*
* `key` and `iv` are provided as-is.
*
* `key` must be of size `keySize` and `iv` must be of size `ivSize`.
* Otherwise, the constructed cipher is invalid.
*/
AES256CTRDesc(RawTag, DataT key, DataT iv);
public:
/**
* Constructs an AES-256-CTR cipher using `key` and `iv`.
*
* `key` is provided as urlsafe unpadded base64.
* `iv` is provided as unpadded base64.
*
* One must check with `valid()` before actual processing with
* the object if the key and iv comes from a remote Matrix event.
*/
AES256CTRDesc(std::string key, std::string iv);
/**
* Generate a new AES-256-CTR cipher from random data.
*
* @param random The random data provided as-is. `random.size()`
* must be at least `randomSize`.
*
* The first keySize bytes will be used as the key, and the
* next ivSize bytes will be used as the iv.
*
* @return An AES-256-CTR cipher generated with the random data.
*/
template<class RangeT>
static AES256CTRDesc fromRandom(RangeT random) {
if (random.size() < randomSize) {
// Not enough random, return an invalid one
return AES256CTRDesc(RawTag{}, DataT(), DataT());
}
+ auto iv = DataT(ivSize, 0);
+ std::copy(random.begin() + keySize, random.begin() + keySize + ivSizeInit, iv.begin());
return AES256CTRDesc(RawTag{},
DataT(random.begin(), random.begin() + keySize),
- DataT(random.begin() + keySize, random.begin() + keySize + ivSize));
+ iv);
}
KAZV_DECLARE_COPYABLE(AES256CTRDesc)
~AES256CTRDesc();
/**
* @return whether this cipher is valid. If the cipher is invalid, you
* must not use any method except destructor or operator=().
*/
bool valid() const;
/// @return The key encoded as urlsafe unpadded base64.
std::string key() const;
/// @return The iv encoded as unpadded base64.
std::string iv() const;
template<class RangeT>
using ResultBase = std::pair<AES256CTRDesc, RangeT>;
using Result = ResultBase<DataT>;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* @param data The data to encrypt or decrypt.
*
* Example:
*
* auto cipher = Kazv::AES256CTRDesc(
* Kazv::genRandom(Kazv::AES256CTRDesc::keySize),
* Kazv::genRandom(Kazv::AES256CTRDesc::ivSize)
* );
* std::string original = "Some test here";
* auto [next, encrypted] = cipher.process(original);
* auto [next2, encrypted2] = cipher.process(original);
* REQUIRE(encrypted == encrypted2);
*
* @return an object that can be destructured into the next cipher state
* (as AES256CTRDesc) and the encrypted or decrypted data (as DataT).
*/
Result process(DataT data) const &;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function only differs from the above in that it takes any range type
* and the second component of returned object is of the same type as the argument.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
ResultBase<std::decay_t<RangeT>> process(RangeT data) const & {
using ActualRangeT = std::decay_t<RangeT>;
auto [next, res] = process(DataT(begin(data), end(data)));
return { next, ActualRangeT(res.begin(), res.end()) };
}
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function differs from the above two only in that its calling object
* is an rvalue reference (a.k.a casted using std::move()).
*/
Result process(DataT data) &&;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function differs from the above only in that it takes any range type
* and the second component of returned object is of the same type as the argument.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
ResultBase<std::decay_t<RangeT>> process(RangeT data) && {
using ActualRangeT = std::decay_t<RangeT>;
auto [next, res] = std::move(*this).process(DataT(begin(data), end(data)));
return { next, ActualRangeT(res.begin(), res.end()) };
}
/**
* Encrypt or decrypt `data` and modify the state of the cipher in-place.
*
* @param data The data to encrypt or decrypt.
*/
DataT processInPlace(DataT data);
/**
* Encrypt or decrypt `data` and modify the state of the cipher in-place.
*
* This function differs from the above only in that it takes any range type
* and returns the processed data as the same range type.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
std::decay_t<RangeT> processInPlace(RangeT data) {
using ActualRangeT = std::decay_t<RangeT>;
auto res = processInPlace(DataT(begin(data), end(data)));
return ActualRangeT(res.begin(), res.end());
}
private:
struct Private;
std::unique_ptr<Private> m_d;
};
}
diff --git a/src/tests/crypto-test.cpp b/src/tests/crypto-test.cpp
index d476c03..ff7c7d9 100644
--- a/src/tests/crypto-test.cpp
+++ b/src/tests/crypto-test.cpp
@@ -1,560 +1,578 @@
/*
* 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>
using namespace Kazv;
using namespace Kazv::CryptoConstants;
using IAr = boost::archive::text_iarchive;
using OAr = boost::archive::text_oarchive;
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;
}
}
TEST_CASE("Crypto constructors", "[crypto]")
{
Crypto crypto;
REQUIRE(!crypto.valid());
Crypto crypto2(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
REQUIRE(crypto2.valid());
}
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::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);
}
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-diff
Expires
Sun, Aug 9, 12:59 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724084
Default Alt Text
(24 KB)

Event Timeline