Files
Morgan Pretty 0579f5c206 Merge branch 'dev' into multipart
# Conflicts:
#	external/CMakeLists.txt
#	external/oxen-encoding
#	include/session/config.hpp
#	include/session/config/base.hpp
#	include/session/config/convo_info_volatile.hpp
#	include/session/config/protos.hpp
#	include/session/hash.hpp
#	include/session/session_encrypt.hpp
#	include/session/types.hpp
#	include/session/util.hpp
#	src/CMakeLists.txt
#	src/config/base.cpp
#	src/config/convo_info_volatile.cpp
#	src/config/encrypt.cpp
#	src/config/internal.cpp
#	src/config/internal.hpp
#	src/config/protos.cpp
#	src/hash.cpp
#	src/session_encrypt.cpp
#	tests/CMakeLists.txt
#	tests/swarm-auth-test.cpp
#	tests/test_blinding.cpp
#	tests/test_bugs.cpp
#	tests/test_config_contacts.cpp
#	tests/test_config_convo_info_volatile.cpp
#	tests/test_config_user_groups.cpp
#	tests/test_config_userprofile.cpp
#	tests/test_configdata.cpp
#	tests/test_group_info.cpp
#	tests/test_group_keys.cpp
#	tests/test_group_members.cpp
#	tests/test_multi_encrypt.cpp
#	tests/test_session_encrypt.cpp
#	tests/utils.hpp
2025-04-11 11:53:50 +10:00

371 lines
14 KiB
C++

#include <oxenc/endian.h>
#include <oxenc/hex.h>
#include <session/config/contacts.h>
#include <sodium/crypto_sign_ed25519.h>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <session/config/groups/info.hpp>
#include <session/util.hpp>
#include <string_view>
#include "utils.hpp"
using namespace std::literals;
using namespace oxenc::literals;
using namespace session::config;
TEST_CASE("Group Info settings", "[config][groups][info]") {
const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
std::array<unsigned char, 32> ed_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
"cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece");
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
std::vector<std::vector<unsigned char>> enc_keys{
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes};
groups::Info ginfo1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
// This is just for testing: normally you don't load keys manually but just make a groups::Keys
// object that loads the keys into the Members object for you.
for (const auto& k : enc_keys)
ginfo1.add_key(k, false);
enc_keys.insert(
enc_keys.begin(),
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes);
enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes);
enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes);
groups::Info ginfo2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
for (const auto& k : enc_keys) // Just for testing, as above.
ginfo2.add_key(k, false);
ginfo1.set_name("GROUP Name");
CHECK(ginfo1.is_dirty());
CHECK(ginfo1.needs_push());
CHECK(ginfo1.needs_dump());
auto [s1, p1, o1] = ginfo1.push();
CHECK(s1 == 1);
REQUIRE(p1.size() == 1);
CHECK(p1[0].size() == 256);
CHECK(o1.empty());
ginfo1.confirm_pushed(s1, {"fakehash1"});
CHECK(ginfo1.needs_dump());
CHECK_FALSE(ginfo1.needs_push());
std::vector<std::pair<std::string, std::span<const unsigned char>>> merge_configs;
merge_configs.emplace_back("fakehash1", p1[0]);
CHECK(ginfo2.merge(merge_configs) == std::unordered_set{{"fakehash1"s}});
CHECK_FALSE(ginfo2.needs_push());
CHECK(ginfo2.get_name() == "GROUP Name");
ginfo2.set_profile_pic(
"http://example.com/12345",
"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes);
ginfo2.set_expiry_timer(1h);
constexpr int64_t create_time{1682529839};
ginfo2.set_created(create_time);
ginfo2.set_delete_before((create_time + 50 * 86400) * 1'000'000); // as microseconds
ginfo2.set_delete_attach_before((create_time + 70 * 86400) * 1'000); // as milliseconds
ginfo2.destroy_group();
auto [s2, p2, o2] = ginfo2.push();
CHECK(s2 == 2);
CHECK(p2.at(0).size() == 512);
CHECK(o2 == std::vector{"fakehash1"s});
ginfo2.confirm_pushed(s2, {"fakehash2"});
ginfo1.set_name("Better name!");
merge_configs.clear();
merge_configs.emplace_back("fakehash2", p2.at(0));
// This fails because ginfo1 doesn't yet have the new key that ginfo2 used (bbb...)
CHECK(ginfo1.merge(merge_configs) == std::unordered_set<std::string>{});
ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes);
ginfo1.add_key(
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes,
/*prepend=*/false);
CHECK(ginfo1.merge(merge_configs) == std::unordered_set{{"fakehash2"s}});
CHECK(ginfo1.needs_push());
auto [s3, p3, o3] = ginfo1.push();
CHECK(ginfo1.get_name() == "Better name!");
CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345");
CHECK(ginfo1.get_profile_pic().key ==
"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes);
CHECK(ginfo1.get_expiry_timer() == 1h);
CHECK(ginfo1.get_created() == create_time);
CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400);
CHECK(ginfo1.get_delete_attach_before() == create_time + 70 * 86400);
CHECK(ginfo1.is_destroyed());
ginfo1.confirm_pushed(s3, {"fakehash3"});
merge_configs.clear();
merge_configs.emplace_back("fakehash3", p3.at(0));
CHECK(ginfo2.merge(merge_configs) == std::unordered_set{{"fakehash3"s}});
CHECK(ginfo2.get_name() == "Better name!");
CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345");
CHECK(ginfo2.get_profile_pic().key ==
"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes);
CHECK(ginfo2.get_expiry_timer() == 1h);
CHECK(ginfo2.get_created() == create_time);
CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400);
CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400);
CHECK(ginfo2.is_destroyed());
CHECK_THROWS(
ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890"
"123456789012345678901234567890A"));
CHECK_NOTHROW(
ginfo1.set_name_truncated("123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890A"));
CHECK(ginfo1.get_name() ==
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
"901234567890");
}
TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") {
const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
std::array<unsigned char, 32> ed_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
"cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece");
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
std::vector<std::vector<unsigned char>> enc_keys1;
enc_keys1.push_back(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes);
std::vector<std::vector<unsigned char>> enc_keys2;
enc_keys2.push_back(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes);
enc_keys2.push_back(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes);
// This Info object has only the public key, not the priv key, and so cannot modify things:
groups::Info ginfo{session::to_span(ed_pk), std::nullopt, std::nullopt};
for (const auto& k : enc_keys1) // Just for testing, as above.
ginfo.add_key(k, false);
REQUIRE_THROWS_WITH(
ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object");
REQUIRE_THROWS_WITH(
ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object");
CHECK(!ginfo.is_dirty());
// This one is good and has the right signature:
groups::Info ginfo_rw{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
for (const auto& k : enc_keys1) // Just for testing, as above.
ginfo_rw.add_key(k, false);
ginfo_rw.set_name("Super Group!!");
CHECK(ginfo_rw.is_dirty());
CHECK(ginfo_rw.needs_push());
CHECK(ginfo_rw.needs_dump());
auto [seqno, to_push, obs] = ginfo_rw.push();
CHECK(seqno == 1);
ginfo_rw.confirm_pushed(seqno, {"fakehash1"});
CHECK(ginfo_rw.needs_dump());
CHECK_FALSE(ginfo_rw.needs_push());
std::vector<std::pair<std::string, std::span<const unsigned char>>> merge_configs;
merge_configs.emplace_back("fakehash1", to_push.at(0));
CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash1"s}});
CHECK_FALSE(ginfo.needs_push());
groups::Info ginfo_rw2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
for (const auto& k : enc_keys1) // Just for testing, as above.
ginfo_rw2.add_key(k, false);
CHECK(ginfo_rw2.merge(merge_configs) == std::unordered_set{{"fakehash1"s}});
CHECK_FALSE(ginfo.needs_push());
CHECK(ginfo.get_name() == "Super Group!!");
REQUIRE_THROWS_WITH(
ginfo.set_name("Super Group11"), "Unable to make changes to a read-only config object");
// This shouldn't throw because it isn't *actually* changing a config value (i.e. re-setting the
// same value does not dirty the config). It isn't clear why you'd need to do this, but still.
ginfo.set_name("Super Group!!");
// Deliberately use the wrong signing key so that what we produce encrypts successfully but
// doesn't verify
const auto seed_bad1 =
"0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
std::array<unsigned char, 32> ed_pk_bad1;
std::array<unsigned char, 64> ed_sk_bad1;
crypto_sign_ed25519_seed_keypair(
ed_pk_bad1.data(),
ed_sk_bad1.data(),
reinterpret_cast<const unsigned char*>(seed_bad1.data()));
groups::Info ginfo_bad1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
for (const auto& k : enc_keys1) // Just for testing, as above.
ginfo_bad1.add_key(k, false);
ginfo_bad1.merge(merge_configs);
ginfo_bad1.set_sig_keys(session::to_span(ed_sk_bad1));
ginfo_bad1.set_name("Bad name, BAD!");
auto [s_bad, p_bad, o_bad] = ginfo_bad1.push();
merge_configs.clear();
merge_configs.emplace_back("badhash1", p_bad.at(0));
CHECK(ginfo.merge(merge_configs) == std::unordered_set<std::string>{});
CHECK_FALSE(ginfo.needs_push());
// Now let's get more complicated: we will have *two* valid signers who submit competing updates
ginfo_rw2.set_name("Super Group 2");
ginfo_rw2.set_created(12345);
ginfo_rw.set_name("Super Group 3");
ginfo_rw.set_expiry_timer(365 * 24h);
CHECK(ginfo_rw.needs_push());
CHECK(ginfo_rw2.needs_push());
auto [s2, tp2, o2] = ginfo_rw2.push();
auto [s3, tp3, o3] = ginfo_rw.push();
merge_configs.clear();
merge_configs.emplace_back("fakehash2", tp2.at(0));
merge_configs.emplace_back("fakehash3", tp3.at(0));
CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash2"s, "fakehash3"s}});
CHECK(ginfo.is_clean());
CHECK(s2 == 2);
CHECK(s3 == 2);
CHECK_FALSE(ginfo.needs_push());
CHECK(ginfo_rw.merge(merge_configs) == std::unordered_set{{"fakehash2"s, "fakehash3"s}});
CHECK(ginfo_rw2.merge(merge_configs) == std::unordered_set{{"fakehash2"s, "fakehash3"s}});
CHECK(ginfo_rw.needs_push());
CHECK(ginfo_rw2.needs_push());
auto [s23, t23, o23] = ginfo_rw.push();
auto [s32, t32, o32] = ginfo_rw2.push();
CHECK(s23 == s32);
CHECK(t23 == t32);
CHECK(o23 == o32);
ginfo_rw.confirm_pushed(s23, {"fakehash23"});
ginfo_rw2.confirm_pushed(s32, {"fakehash23"});
merge_configs.clear();
merge_configs.emplace_back("fakehash23", t23.at(0));
CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash23"s}});
CHECK(ginfo_rw.merge(merge_configs) == std::unordered_set{{"fakehash23"s}});
CHECK(ginfo_rw2.merge(merge_configs) == std::unordered_set{{"fakehash23"s}});
CHECK_FALSE(ginfo.needs_push());
CHECK_FALSE(ginfo_rw.needs_push());
CHECK_FALSE(ginfo_rw2.needs_push());
auto test = [](groups::Info& g) {
auto n = g.get_name();
REQUIRE(n);
CHECK(*n == "Super Group 2");
auto c = g.get_created();
REQUIRE(c);
CHECK(*c == 12345);
auto et = g.get_expiry_timer();
REQUIRE(et);
CHECK(*et == 365 * 24h);
};
test(ginfo);
test(ginfo_rw);
test(ginfo_rw2);
CHECK(ginfo.needs_dump());
auto dump = ginfo.dump();
groups::Info ginfo2{session::to_span(ed_pk), std::nullopt, dump};
for (const auto& k : enc_keys1) // Just for testing, as above.
ginfo2.add_key(k, false);
CHECK(!ginfo.needs_dump());
CHECK(!ginfo2.needs_dump());
auto [s4, t4, o4] = ginfo.push();
auto [s5, t5, o5] = ginfo.push();
CHECK(s4 == s23);
CHECK(s4 == s5);
CHECK(t4 == t23);
CHECK(t4 == t5);
CHECK(o4.empty());
CHECK(o5.empty());
// This account has a different primary decryption key
groups::Info ginfo_rw3{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt};
for (const auto& k : enc_keys2) // Just for testing, as above.
ginfo_rw3.add_key(k, false);
CHECK(ginfo_rw3.merge(merge_configs) == std::unordered_set{{"fakehash23"s}});
CHECK(ginfo_rw3.get_name() == "Super Group 2");
auto [s6, t6, o6] = ginfo_rw3.push();
CHECK(oxenc::to_hex(ginfo_rw3.key(0)) ==
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
REQUIRE(ginfo_rw3.key_count() == 2);
CHECK(oxenc::to_hex(ginfo_rw3.key(1)) ==
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
CHECK(s6 == s5);
CHECK(t6.size() == t23.size());
CHECK(t6 != t23);
ginfo_rw3.set_profile_pic(
"http://example.com/12345",
"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes);
CHECK(ginfo_rw3.needs_push());
auto [s7, t7, o7] = ginfo_rw3.push();
CHECK(s7 == s6 + 1);
CHECK(t7 != t6);
CHECK(o7 == std::vector{{"fakehash23"s}});
merge_configs.clear();
merge_configs.emplace_back("fakehash7", t7.at(0));
// If we don't have the new "bbb" key loaded yet, this will fail:
CHECK(ginfo.merge(merge_configs) == std::unordered_set<std::string>{});
ginfo.add_key(enc_keys2.front());
CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash7"s}});
auto pic = ginfo.get_profile_pic();
CHECK_FALSE(pic.empty());
CHECK(pic.url == "http://example.com/12345");
CHECK(pic.key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes);
}