mirror of
https://github.com/lux-tel/libsession-util.git
synced 2026-10-30 20:25:11 +00:00
1040 lines
42 KiB
C++
1040 lines
42 KiB
C++
#include <oxenc/base64.h>
|
|
#include <oxenc/endian.h>
|
|
#include <oxenc/hex.h>
|
|
#include <session/config/contacts.h>
|
|
#include <session/config/groups/info.h>
|
|
#include <session/config/groups/keys.h>
|
|
#include <session/config/groups/members.h>
|
|
#include <sodium/crypto_sign_ed25519.h>
|
|
|
|
#include <algorithm>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/generators/catch_generators_range.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <iterator>
|
|
#include <session/config/groups/info.hpp>
|
|
#include <session/config/groups/keys.hpp>
|
|
#include <session/config/groups/members.hpp>
|
|
#include <session/config/user_groups.hpp>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
|
|
#include "utils.hpp"
|
|
|
|
using namespace std::literals;
|
|
using namespace session::config;
|
|
|
|
static std::array<unsigned char, 64> sk_from_seed(std::span<const unsigned char> seed) {
|
|
std::array<unsigned char, 32> ignore;
|
|
std::array<unsigned char, 64> sk;
|
|
crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data());
|
|
return sk;
|
|
}
|
|
|
|
static std::string session_id_from_ed(std::span<const unsigned char> ed_pk) {
|
|
std::string sid;
|
|
std::array<unsigned char, 32> xpk;
|
|
int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data());
|
|
REQUIRE(rc == 0);
|
|
sid.reserve(66);
|
|
sid += "05";
|
|
oxenc::to_hex(xpk.begin(), xpk.end(), std::back_inserter(sid));
|
|
return sid;
|
|
}
|
|
|
|
// Hacky little class that implements `[n]` on a std::list. This is inefficient (since it access
|
|
// has to iterate n times through the list) but we only use it on small lists in this test code so
|
|
// convenience wins over efficiency. (Why not just use a vector? Because vectors requires `T` to
|
|
// be moveable, so we'd either have to use std::unique_ptr for members, which is also annoying).
|
|
template <typename T>
|
|
struct hacky_list : std::list<T> {
|
|
T& operator[](size_t n) { return *std::next(std::begin(*this), n); }
|
|
};
|
|
|
|
struct pseudo_client {
|
|
std::array<unsigned char, 64> secret_key;
|
|
const std::span<const unsigned char> public_key{secret_key.data() + 32, 32};
|
|
std::string session_id{session_id_from_ed(public_key)};
|
|
|
|
groups::Info info;
|
|
groups::Members members;
|
|
groups::Keys keys;
|
|
|
|
pseudo_client(
|
|
std::span<const unsigned char> seed,
|
|
bool admin,
|
|
const unsigned char* gpk,
|
|
std::optional<const unsigned char*> gsk,
|
|
std::optional<std::span<const unsigned char>> info_dump = std::nullopt,
|
|
std::optional<std::span<const unsigned char>> members_dump = std::nullopt,
|
|
std::optional<std::span<const unsigned char>> keys_dump = std::nullopt) :
|
|
secret_key{sk_from_seed(seed)},
|
|
info{std::span<const unsigned char>{gpk, 32},
|
|
admin ? std::make_optional<std::span<const unsigned char>>({*gsk, 64})
|
|
: std::nullopt,
|
|
info_dump},
|
|
members{std::span<const unsigned char>{gpk, 32},
|
|
admin ? std::make_optional<std::span<const unsigned char>>({*gsk, 64})
|
|
: std::nullopt,
|
|
members_dump},
|
|
keys{session::to_span(secret_key),
|
|
std::span<const unsigned char>{gpk, 32},
|
|
admin ? std::make_optional<std::span<const unsigned char>>({*gsk, 64})
|
|
: std::nullopt,
|
|
keys_dump,
|
|
info,
|
|
members} {
|
|
if (gsk)
|
|
keys.rekey(info, members);
|
|
}
|
|
};
|
|
|
|
TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") {
|
|
|
|
const std::vector<unsigned char> group_seed =
|
|
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin1_seed =
|
|
"0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin2_seed =
|
|
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes;
|
|
const std::array member_seeds = {
|
|
"000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1
|
|
"00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2
|
|
"00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3
|
|
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes, // member4
|
|
"3333333333333333333333333333333333333333333333333333333333333333"_hexbytes, // member3b
|
|
"4444444444444444444444444444444444444444444444444444444444444444"_hexbytes, // member4b
|
|
};
|
|
|
|
std::array<unsigned char, 32> group_pk;
|
|
std::array<unsigned char, 64> group_sk;
|
|
|
|
crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data());
|
|
REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) ==
|
|
oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32));
|
|
|
|
// Using list instead of vector so that `psuedo_client` doesn't have to be moveable, which lets
|
|
// us put the Info/Member/Keys directly inside it (rather than having to use a unique_ptr, which
|
|
// would also be annoying).
|
|
hacky_list<pseudo_client> admins;
|
|
hacky_list<pseudo_client> members;
|
|
|
|
// Initialize admin and member objects
|
|
admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data());
|
|
admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data());
|
|
|
|
for (int i = 0; i < 4; ++i)
|
|
members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt);
|
|
|
|
REQUIRE(admins[0].session_id ==
|
|
"05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627");
|
|
REQUIRE(admins[1].session_id ==
|
|
"05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e");
|
|
REQUIRE(members[0].session_id ==
|
|
"05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628");
|
|
REQUIRE(members[1].session_id ==
|
|
"053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c");
|
|
REQUIRE(members[2].session_id ==
|
|
"05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118");
|
|
REQUIRE(members[3].session_id ==
|
|
"050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d");
|
|
|
|
for (const auto& a : admins)
|
|
REQUIRE(a.members.size() == 0);
|
|
for (const auto& m : members)
|
|
REQUIRE(m.members.size() == 0);
|
|
|
|
std::vector<std::pair<std::string, std::span<const unsigned char>>> info_configs;
|
|
std::vector<std::pair<std::string, std::span<const unsigned char>>> mem_configs;
|
|
|
|
// add admin account, re-key, distribute
|
|
auto& admin1 = admins[0];
|
|
|
|
auto m = admin1.members.get_or_construct(admin1.session_id);
|
|
m.admin = true;
|
|
m.name = "Admin1";
|
|
admin1.members.set(m);
|
|
|
|
CHECK(admin1.members.needs_push());
|
|
|
|
auto maybe_key_config = admin1.keys.pending_config();
|
|
REQUIRE(maybe_key_config);
|
|
auto new_keys_config1 = *maybe_key_config;
|
|
|
|
auto [iseq1, new_info_config1, iobs1] = admin1.info.push();
|
|
admin1.info.confirm_pushed(iseq1, {"fakehash1"});
|
|
REQUIRE(new_info_config1.size() == 1);
|
|
info_configs.emplace_back("fakehash1", new_info_config1[0]);
|
|
|
|
auto [mseq1, new_mem_config1, mobs1] = admin1.members.push();
|
|
admin1.members.confirm_pushed(mseq1, {"fakehash1"});
|
|
REQUIRE(new_mem_config1.size() == 1);
|
|
mem_configs.emplace_back("fakehash1", new_mem_config1[0]);
|
|
|
|
/* Even though we have only added one admin, admin2 will still be able to see group info
|
|
like group size and merge all configs. This is because they have loaded the key config
|
|
message, which they can decrypt with the group secret key.
|
|
*/
|
|
for (auto& a : admins) {
|
|
a.keys.load_key_message(
|
|
"keyhash1", new_keys_config1, get_timestamp_ms(), a.info, a.members);
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash1"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash1"s}});
|
|
CHECK(a.members.size() == 1);
|
|
CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s}});
|
|
}
|
|
|
|
/* All attempts to merge non-admin members will throw, as none of the non admin members
|
|
will be able to decrypt the new info/member configs using the updated keys
|
|
*/
|
|
for (auto& m : members) {
|
|
m.keys.load_key_message(
|
|
"keyhash1", new_keys_config1, get_timestamp_ms(), m.info, m.members);
|
|
CHECK_THROWS(m.info.merge(info_configs));
|
|
CHECK_THROWS(m.members.merge(mem_configs));
|
|
CHECK(m.members.size() == 0);
|
|
CHECK(m.keys.active_hashes().empty());
|
|
}
|
|
|
|
info_configs.clear();
|
|
mem_configs.clear();
|
|
|
|
// add non-admin members, re-key, distribute
|
|
for (size_t i = 0; i < members.size(); ++i) {
|
|
auto m = admin1.members.get_or_construct(members[i].session_id);
|
|
m.admin = false;
|
|
m.name = "Member{}"_format(i);
|
|
admin1.members.set(m);
|
|
}
|
|
|
|
CHECK(admin1.members.needs_push());
|
|
|
|
auto new_keys_config2 = admin1.keys.rekey(admin1.info, admin1.members);
|
|
CHECK(not new_keys_config2.empty());
|
|
|
|
auto [iseq2, new_info_config2, iobs2] = admin1.info.push();
|
|
admin1.info.confirm_pushed(iseq2, {"fakehash2"});
|
|
REQUIRE(new_info_config2.size() == 1);
|
|
info_configs.emplace_back("fakehash2", new_info_config2[0]);
|
|
|
|
auto [mseq2, new_mem_config2, mobs2] = admin1.members.push();
|
|
admin1.members.confirm_pushed(mseq2, {"fakehash2"});
|
|
REQUIRE(new_mem_config2.size() == 1);
|
|
mem_configs.emplace_back("fakehash2", new_mem_config2[0]);
|
|
|
|
for (auto& a : admins) {
|
|
a.keys.load_key_message(
|
|
"keyhash2", new_keys_config2, get_timestamp_ms(), a.info, a.members);
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash2"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash2"s}});
|
|
CHECK(a.members.size() == 5);
|
|
CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s}});
|
|
}
|
|
|
|
for (auto& m : members) {
|
|
m.keys.load_key_message(
|
|
"keyhash2", new_keys_config2, get_timestamp_ms(), m.info, m.members);
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set{{"fakehash2"s}});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set{{"fakehash2"s}});
|
|
CHECK(m.members.size() == 5);
|
|
CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s}});
|
|
}
|
|
|
|
info_configs.clear();
|
|
mem_configs.clear();
|
|
|
|
// change group info, re-key, distribute
|
|
admin1.info.set_name("tomatosauce"s);
|
|
admin1.info.set_description("this is where you go to play in the tomato sauce, I guess");
|
|
|
|
CHECK(admin1.info.needs_push());
|
|
|
|
auto new_keys_config3 = admin1.keys.rekey(admin1.info, admin1.members);
|
|
CHECK(not new_keys_config3.empty());
|
|
|
|
auto [iseq3, new_info_config3, iobs3] = admin1.info.push();
|
|
admin1.info.confirm_pushed(iseq3, {"fakehash3"});
|
|
REQUIRE(new_info_config3.size() == 1);
|
|
info_configs.emplace_back("fakehash3", new_info_config3[0]);
|
|
|
|
auto [mseq3, new_mem_config3, mobs3] = admin1.members.push();
|
|
admin1.members.confirm_pushed(mseq3, {"fakehash3"});
|
|
REQUIRE(new_mem_config3.size() == 1);
|
|
mem_configs.emplace_back("fakehash3", new_mem_config3[0]);
|
|
|
|
for (auto& a : admins) {
|
|
a.keys.load_key_message(
|
|
"keyhash3", new_keys_config3, get_timestamp_ms(), a.info, a.members);
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash3"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash3"s}});
|
|
CHECK(a.info.get_name() == "tomatosauce"s);
|
|
CHECK(a.info.get_description() ==
|
|
"this is where you go to play in the tomato sauce, I guess"s);
|
|
CHECK(a.keys.active_hashes() ==
|
|
std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s}});
|
|
}
|
|
|
|
for (auto& m : members) {
|
|
m.keys.load_key_message(
|
|
"keyhash3", new_keys_config3, get_timestamp_ms(), m.info, m.members);
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set{{"fakehash3"s}});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set{{"fakehash3"s}});
|
|
CHECK(m.info.get_name() == "tomatosauce"s);
|
|
CHECK(m.info.get_description() ==
|
|
"this is where you go to play in the tomato sauce, I guess"s);
|
|
CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}});
|
|
}
|
|
|
|
info_configs.clear();
|
|
mem_configs.clear();
|
|
|
|
// remove members, re-key, distribute
|
|
CHECK(admin1.members.size() == 5);
|
|
CHECK(admin1.members.erase(members[3].session_id));
|
|
CHECK(admin1.members.erase(members[2].session_id));
|
|
CHECK(admin1.members.size() == 3);
|
|
|
|
CHECK(admin1.members.needs_push());
|
|
|
|
std::vector<unsigned char> old_key = session::to_vector(admin1.keys.group_enc_key());
|
|
auto new_keys_config4 = admin1.keys.rekey(admin1.info, admin1.members);
|
|
CHECK(not new_keys_config4.empty());
|
|
|
|
CHECK(old_key != session::to_vector(admin1.keys.group_enc_key()));
|
|
|
|
auto [iseq4, new_info_config4, iobs4] = admin1.info.push();
|
|
admin1.info.confirm_pushed(iseq4, {"fakehash4"});
|
|
REQUIRE(new_info_config4.size() == 1);
|
|
info_configs.emplace_back("fakehash4", new_info_config4[0]);
|
|
|
|
auto [mseq4, new_mem_config4, mobs4] = admin1.members.push();
|
|
admin1.members.confirm_pushed(mseq4, {"fakehash4"});
|
|
REQUIRE(new_mem_config4.size() == 1);
|
|
mem_configs.emplace_back("fakehash4", new_mem_config4[0]);
|
|
|
|
for (auto& a : admins) {
|
|
CHECK(a.keys.load_key_message(
|
|
"keyhash4", new_keys_config4, get_timestamp_ms(), a.info, a.members));
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash4"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash4"s}});
|
|
CHECK(a.members.size() == 3);
|
|
CHECK(a.keys.active_hashes() ==
|
|
std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s, "keyhash4"s}});
|
|
}
|
|
|
|
for (size_t i = 0; i < members.size(); i++) {
|
|
auto& m = members[i];
|
|
bool found_key = m.keys.load_key_message(
|
|
"keyhash4", new_keys_config2, get_timestamp_ms(), m.info, m.members);
|
|
|
|
CHECK(m.keys.active_hashes() ==
|
|
std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s}});
|
|
if (i < 2) { // We should still be in the group
|
|
CHECK(found_key);
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set{{"fakehash4"s}});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set{{"fakehash4"s}});
|
|
CHECK(m.members.size() == 3);
|
|
} else {
|
|
CHECK_FALSE(found_key);
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set<std::string>{});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set<std::string>{});
|
|
CHECK(m.members.size() == 5);
|
|
}
|
|
}
|
|
|
|
members.pop_back();
|
|
members.pop_back();
|
|
|
|
info_configs.clear();
|
|
mem_configs.clear();
|
|
|
|
// middle-out time
|
|
auto msg = "hello to all my friends sitting in the tomato sauce"s;
|
|
|
|
for (int i = 0; i < 5; ++i)
|
|
msg += msg;
|
|
|
|
auto compressed = admin1.keys.encrypt_message(session::to_span(msg));
|
|
CHECK(compressed.size() == 256);
|
|
auto uncompressed = admin1.keys.encrypt_message(session::to_span(msg), false);
|
|
CHECK(uncompressed.size() == 2048);
|
|
|
|
CHECK(compressed.size() < msg.size());
|
|
|
|
// Add two new members and send them supplemental keys
|
|
for (int i = 0; i < 2; ++i) {
|
|
auto& m = members.emplace_back(member_seeds[4 + i], false, group_pk.data(), std::nullopt);
|
|
|
|
auto memb = admin1.members.get_or_construct(m.session_id);
|
|
memb.set_invite_sent();
|
|
memb.supplement = true;
|
|
memb.name = i == 0 ? "fred" : "JOHN";
|
|
admin1.members.set(memb);
|
|
|
|
CHECK_FALSE(m.keys.admin());
|
|
}
|
|
|
|
REQUIRE(members[2].session_id ==
|
|
"054eb4fafee2bd3018a24e310de8106333c2b364eaed029a7f05d7b45ccc77683a");
|
|
REQUIRE(members[3].session_id ==
|
|
"057ce31baa9a04b5cfb83ab7ccdd7b669b911a082d29883d6aad3256294a0a5e0c");
|
|
|
|
// We actually send supplemental keys to members 1, as well, by mistake just to make sure it
|
|
// doesn't do or hurt anything to get a supplemental key you already have.
|
|
std::vector<std::string> supp_sids;
|
|
std::transform(
|
|
std::next(members.begin()), members.end(), std::back_inserter(supp_sids), [](auto& m) {
|
|
return m.session_id;
|
|
});
|
|
auto supp = admin1.keys.key_supplement(supp_sids);
|
|
CHECK(admin1.members.needs_push());
|
|
CHECK_FALSE(admin1.info.needs_push());
|
|
auto [mseq5, mpush5, mobs5] = admin1.members.push();
|
|
REQUIRE(mpush5.size() == 1);
|
|
mem_configs.emplace_back("fakehash5", mpush5[0]);
|
|
admin1.members.confirm_pushed(mseq5, {"fakehash5"});
|
|
info_configs.emplace_back("fakehash4", new_info_config4[0]);
|
|
|
|
for (auto& a : admins) {
|
|
CHECK_FALSE(
|
|
a.keys.load_key_message("keyhash5", supp, get_timestamp_ms(), a.info, a.members));
|
|
}
|
|
|
|
for (size_t i = 0; i < members.size(); i++) {
|
|
auto& m = members[i];
|
|
bool found_key =
|
|
m.keys.load_key_message("keyhash5", supp, get_timestamp_ms(), m.info, m.members);
|
|
|
|
if (i < 1) {
|
|
// This supp key wasn't for us
|
|
CHECK_FALSE(found_key);
|
|
CHECK(m.keys.size() == 3);
|
|
CHECK(m.keys.group_keys().size() == 3);
|
|
} else {
|
|
CHECK(found_key);
|
|
// new_keys_config1 never went to the initial members, but did go out in the
|
|
// supplement, which is why we have the extra key here.
|
|
CHECK(m.keys.size() == 4);
|
|
CHECK(m.keys.group_keys().size() == 4);
|
|
}
|
|
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set{{"fakehash4"s}});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set{{"fakehash5"s}});
|
|
REQUIRE(m.info.get_name());
|
|
CHECK(*m.info.get_name() == "tomatosauce"sv);
|
|
CHECK(m.members.size() == 5);
|
|
|
|
if (i < 2)
|
|
CHECK(m.keys.active_hashes() ==
|
|
std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}});
|
|
else
|
|
CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash5"s}});
|
|
}
|
|
|
|
std::pair<std::string, std::vector<unsigned char>> decrypted1, decrypted2;
|
|
REQUIRE_NOTHROW(decrypted1 = members.back().keys.decrypt_message(compressed));
|
|
CHECK(decrypted1.first == admin1.session_id);
|
|
CHECK(session::to_string(decrypted1.second) == msg);
|
|
|
|
REQUIRE_NOTHROW(decrypted2 = members.back().keys.decrypt_message(uncompressed));
|
|
CHECK(decrypted2.first == admin1.session_id);
|
|
CHECK(session::to_string(decrypted2.second) == msg);
|
|
|
|
auto bad_compressed = compressed;
|
|
bad_compressed.back() ^= 0b100;
|
|
CHECK_THROWS_WITH(
|
|
members.back().keys.decrypt_message(bad_compressed),
|
|
"unable to decrypt ciphertext with any current group keys; tried 4");
|
|
|
|
// Duplicate members[1] from dumps
|
|
auto& m1b = members.emplace_back(
|
|
member_seeds[1],
|
|
false,
|
|
group_pk.data(),
|
|
std::nullopt,
|
|
members[1].info.dump(),
|
|
members[1].members.dump(),
|
|
members[1].keys.dump());
|
|
CHECK(m1b.keys.size() == 4);
|
|
CHECK(m1b.keys.group_keys().size() == 4);
|
|
CHECK(m1b.keys.active_hashes() ==
|
|
std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}});
|
|
CHECK(m1b.members.size() == 5);
|
|
auto m1b_m2 = m1b.members.get(members[2].session_id);
|
|
REQUIRE(m1b_m2);
|
|
CHECK(m1b.members.get_status(*m1b_m2) == session::config::groups::member::Status::invite_sent);
|
|
CHECK(m1b_m2->name == "fred");
|
|
|
|
// Rekey after 10d, then again after 71d (10+61) and everything except those two new gens should
|
|
// get dropped as stale.
|
|
info_configs.clear();
|
|
mem_configs.clear();
|
|
std::vector<unsigned char> new_keys_config6 =
|
|
session::to_vector(admin1.keys.rekey(admin1.info, admin1.members));
|
|
auto [iseq6, ipush6, iobs6] = admin1.info.push();
|
|
info_configs.emplace_back("ifakehash6", ipush6[0]);
|
|
REQUIRE(info_configs.size() == 1);
|
|
admin1.info.confirm_pushed(iseq6, {"ifakehash6"});
|
|
auto [mseq6, mpush6, mobs6] = admin1.members.push();
|
|
REQUIRE(mpush6.size() == 1);
|
|
mem_configs.emplace_back("mfakehash6", mpush6[0]);
|
|
admin1.members.confirm_pushed(mseq6, {"mfakehash6"});
|
|
|
|
for (auto& a : admins) {
|
|
CHECK(a.keys.load_key_message(
|
|
"keyhash6",
|
|
new_keys_config6,
|
|
get_timestamp_ms() + 10LL * 86400 * 1000,
|
|
a.info,
|
|
a.members));
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s}});
|
|
CHECK(a.members.size() == 5);
|
|
CHECK(a.keys.active_hashes() == std::unordered_set{
|
|
{"keyhash1"s,
|
|
"keyhash2"s,
|
|
"keyhash3"s,
|
|
"keyhash4"s,
|
|
"keyhash5"s,
|
|
"keyhash6"s}});
|
|
}
|
|
|
|
std::vector<unsigned char> new_keys_config7 =
|
|
session::to_vector(admin1.keys.rekey(admin1.info, admin1.members));
|
|
|
|
// Make sure we can encrypt & decrypt even if the rekey is still pending:
|
|
CHECK_NOTHROW(
|
|
admin1.keys.decrypt_message(admin1.keys.encrypt_message(session::to_span("abc"))));
|
|
|
|
auto [iseq7, ipush7, iobs7] = admin1.info.push();
|
|
REQUIRE(ipush7.size() == 1);
|
|
info_configs.emplace_back("ifakehash7", ipush7[0]);
|
|
admin1.info.confirm_pushed(iseq7, {"ifakehash7"});
|
|
|
|
auto [mseq7, mpush7, mobs7] = admin1.members.push();
|
|
REQUIRE(mpush7.size() == 1);
|
|
mem_configs.emplace_back("mfakehash7", mpush7[0]);
|
|
admin1.members.confirm_pushed(mseq7, {"mfakehash7"});
|
|
|
|
for (auto& a : admins) {
|
|
CHECK(a.keys.load_key_message(
|
|
"keyhash7",
|
|
new_keys_config7,
|
|
get_timestamp_ms() + 71LL * 86400 * 1000,
|
|
a.info,
|
|
a.members));
|
|
CHECK(a.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s, "ifakehash7"s}});
|
|
CHECK(a.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s, "mfakehash7"s}});
|
|
CHECK(a.members.size() == 5);
|
|
CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}});
|
|
}
|
|
|
|
for (size_t i = 0; i < members.size(); i++) {
|
|
auto& m = members[i];
|
|
CHECK(m.keys.load_key_message(
|
|
"keyhash6",
|
|
new_keys_config6,
|
|
get_timestamp_ms() + 10LL * 86400 * 1000,
|
|
m.info,
|
|
m.members));
|
|
CHECK(m.keys.load_key_message(
|
|
"keyhash7",
|
|
new_keys_config7,
|
|
get_timestamp_ms() + 71LL * 86400 * 1000,
|
|
m.info,
|
|
m.members));
|
|
CHECK(m.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s, "ifakehash7"s}});
|
|
CHECK(m.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s, "mfakehash7"s}});
|
|
CHECK(m.members.size() == 5);
|
|
CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}});
|
|
}
|
|
|
|
// Make sure keys propagate on dump restore to info/members:
|
|
pseudo_client admin1b{
|
|
admin1_seed,
|
|
true,
|
|
group_pk.data(),
|
|
group_sk.data(),
|
|
admin1.info.dump(),
|
|
admin1.members.dump(),
|
|
admin1.keys.dump()};
|
|
admin1b.info.set_name("Test New Name");
|
|
admin1b.info.set_description("Test New Desc");
|
|
CHECK_THROWS(
|
|
admin1b.info.set_name("Test New Name Really long "
|
|
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
|
|
"mnopqrstuvwxyz"));
|
|
CHECK_THROWS(admin1b.info.set_description(std::string(2050, 'z')));
|
|
CHECK_NOTHROW(admin1b.info.push());
|
|
admin1b.members.set(
|
|
admin1b.members.get_or_construct("05124076571076017981235497801235098712093870981273590"
|
|
"8746387172343"));
|
|
CHECK_NOTHROW(admin1b.members.push());
|
|
|
|
// Test values weren't overrided
|
|
CHECK(admin1b.info.get_name() == "Test New Name");
|
|
CHECK(admin1b.info.get_description() == "Test New Desc");
|
|
}
|
|
|
|
TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") {
|
|
struct pseudo_client {
|
|
std::array<unsigned char, 64> secret_key;
|
|
const std::span<const unsigned char> public_key{secret_key.data() + 32, 32};
|
|
std::string session_id{session_id_from_ed(public_key)};
|
|
|
|
config_group_keys* keys;
|
|
config_object* info;
|
|
config_object* members;
|
|
|
|
pseudo_client(
|
|
std::vector<unsigned char> seed,
|
|
bool is_admin,
|
|
unsigned char* gpk,
|
|
std::optional<unsigned char*> gsk) :
|
|
secret_key{sk_from_seed(seed)} {
|
|
int rv = groups_members_init(&members, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL);
|
|
REQUIRE(rv == 0);
|
|
|
|
rv = groups_info_init(&info, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL);
|
|
REQUIRE(rv == 0);
|
|
|
|
rv = groups_keys_init(
|
|
&keys,
|
|
secret_key.data(),
|
|
gpk,
|
|
is_admin ? *gsk : NULL,
|
|
info,
|
|
members,
|
|
NULL,
|
|
0,
|
|
NULL);
|
|
REQUIRE(rv == 0);
|
|
|
|
if (is_admin)
|
|
REQUIRE(groups_keys_rekey(keys, info, members, nullptr, nullptr));
|
|
}
|
|
|
|
~pseudo_client() {
|
|
config_free(info);
|
|
config_free(members);
|
|
}
|
|
};
|
|
|
|
const std::vector<unsigned char> group_seed =
|
|
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin1_seed =
|
|
"0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin2_seed =
|
|
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes;
|
|
const std::array member_seeds = {
|
|
"000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1
|
|
"00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2
|
|
"00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3
|
|
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4
|
|
};
|
|
|
|
std::array<unsigned char, 32> group_pk;
|
|
std::array<unsigned char, 64> group_sk;
|
|
|
|
crypto_sign_ed25519_seed_keypair(
|
|
group_pk.data(),
|
|
group_sk.data(),
|
|
reinterpret_cast<const unsigned char*>(group_seed.data()));
|
|
REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) ==
|
|
oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32));
|
|
|
|
hacky_list<pseudo_client> admins;
|
|
hacky_list<pseudo_client> members;
|
|
|
|
// Initialize admin and member objects
|
|
admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data());
|
|
admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data());
|
|
|
|
for (int i = 0; i < 4; ++i)
|
|
members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt);
|
|
|
|
REQUIRE(admins[0].session_id ==
|
|
"05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627");
|
|
REQUIRE(admins[1].session_id ==
|
|
"05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e");
|
|
REQUIRE(members[0].session_id ==
|
|
"05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628");
|
|
REQUIRE(members[1].session_id ==
|
|
"053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c");
|
|
REQUIRE(members[2].session_id ==
|
|
"05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118");
|
|
REQUIRE(members[3].session_id ==
|
|
"050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d");
|
|
|
|
for (const auto& a : admins)
|
|
REQUIRE(groups_members_size(a.members) == 0);
|
|
for (const auto& m : members)
|
|
REQUIRE(groups_members_size(m.members) == 0);
|
|
|
|
// add admin account, re-key, distribute
|
|
auto& admin1 = admins[0];
|
|
config_group_member new_admin1;
|
|
|
|
REQUIRE(groups_members_get_or_construct(
|
|
admin1.members, &new_admin1, admin1.session_id.c_str()));
|
|
|
|
new_admin1.admin = true;
|
|
groups_members_set(admin1.members, &new_admin1);
|
|
|
|
CHECK(config_needs_push(admin1.members));
|
|
|
|
const unsigned char* new_keys_config_1;
|
|
size_t key_len1;
|
|
REQUIRE(groups_keys_pending_config(admin1.keys, &new_keys_config_1, &key_len1));
|
|
|
|
config_push_data* new_info_config1 = config_push(admin1.info);
|
|
CHECK(new_info_config1->seqno == 1);
|
|
CHECK(new_info_config1->n_configs == 1);
|
|
|
|
config_push_data* new_mem_config1 = config_push(admin1.members);
|
|
CHECK(new_mem_config1->seqno == 1);
|
|
CHECK(new_mem_config1->n_configs == 1);
|
|
|
|
const char* merge_hash1[1];
|
|
const unsigned char* merge_data1[2];
|
|
size_t merge_size1[2];
|
|
|
|
merge_hash1[0] = "fakehash1";
|
|
|
|
merge_data1[0] = new_info_config1->config[0];
|
|
merge_size1[0] = new_info_config1->config_lens[0];
|
|
|
|
merge_data1[1] = new_mem_config1->config[0];
|
|
merge_size1[1] = new_mem_config1->config_lens[0];
|
|
|
|
const char* tmphash; // test suite cheat: &(tmphash = "asdf") to fake a length-1 array.
|
|
|
|
/* Even though we have only added one admin, admin2 will still be able to see group info
|
|
like group size and merge all configs. This is because they have loaded the key config
|
|
message, which they can decrypt with the group secret key.
|
|
*/
|
|
for (auto& a : admins) {
|
|
REQUIRE(groups_keys_load_message(
|
|
a.keys,
|
|
"fakekeyshash1",
|
|
new_keys_config_1,
|
|
key_len1,
|
|
get_timestamp_ms(),
|
|
a.info,
|
|
a.members));
|
|
config_string_list* hashes;
|
|
hashes = config_merge(a.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1);
|
|
REQUIRE(hashes->len);
|
|
free(hashes);
|
|
config_confirm_pushed(a.info, new_info_config1->seqno, &(tmphash = "fakehash1"), 1);
|
|
|
|
hashes = config_merge(a.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1);
|
|
REQUIRE(hashes->len);
|
|
free(hashes);
|
|
config_confirm_pushed(a.members, new_mem_config1->seqno, &(tmphash = "fakehash1"), 1);
|
|
|
|
REQUIRE(groups_members_size(a.members) == 1);
|
|
}
|
|
|
|
/* All attempts to merge non-admin members will throw, as none of the non admin members
|
|
will be able to decrypt the new info/member configs using the updated keys
|
|
*/
|
|
for (auto& m : members) {
|
|
// this will return true if the message was parsed successfully, NOT if the keys were
|
|
// decrypted
|
|
REQUIRE(groups_keys_load_message(
|
|
m.keys,
|
|
"fakekeyshash1",
|
|
new_keys_config_1,
|
|
key_len1,
|
|
get_timestamp_ms(),
|
|
m.info,
|
|
m.members));
|
|
[[maybe_unused]] config_string_list* hashes;
|
|
hashes = config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1);
|
|
REQUIRE(m.info->last_error == "Cannot merge configs without any decryption keys"sv);
|
|
m.info->last_error = nullptr;
|
|
hashes = config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1);
|
|
REQUIRE(m.members->last_error == "Cannot merge configs without any decryption keys"sv);
|
|
m.members->last_error = nullptr;
|
|
|
|
REQUIRE(groups_members_size(m.members) == 0);
|
|
}
|
|
|
|
free(new_info_config1);
|
|
free(new_mem_config1);
|
|
|
|
for (size_t i = 0; i < members.size(); ++i) {
|
|
config_group_member new_mem;
|
|
|
|
REQUIRE(groups_members_get_or_construct(
|
|
members[i].members, &new_mem, members[i].session_id.c_str()));
|
|
new_mem.admin = false;
|
|
groups_members_set(admin1.members, &new_mem);
|
|
}
|
|
|
|
CHECK(config_needs_push(admin1.members));
|
|
|
|
const unsigned char* new_keys_config_2;
|
|
size_t key_len2;
|
|
REQUIRE(groups_keys_rekey(
|
|
admin1.keys, admin1.info, admin1.members, &new_keys_config_2, &key_len2));
|
|
|
|
config_push_data* new_info_config2 = config_push(admin1.info);
|
|
CHECK(new_info_config2->seqno == 2);
|
|
|
|
config_push_data* new_mem_config2 = config_push(admin1.members);
|
|
CHECK(new_mem_config2->seqno == 2);
|
|
|
|
const char* merge_hash2[1];
|
|
const unsigned char* merge_data2[2];
|
|
size_t merge_size2[2];
|
|
|
|
merge_hash2[0] = "fakehash2";
|
|
|
|
REQUIRE(new_info_config2->n_configs == 1);
|
|
REQUIRE(new_mem_config2->n_configs == 1);
|
|
|
|
merge_data2[0] = new_info_config2->config[0];
|
|
merge_size2[0] = new_info_config2->config_lens[0];
|
|
|
|
merge_data2[1] = new_mem_config2->config[0];
|
|
merge_size2[1] = new_mem_config2->config_lens[0];
|
|
|
|
for (auto& a : admins) {
|
|
REQUIRE(groups_keys_load_message(
|
|
a.keys,
|
|
"fakekeyshash2",
|
|
new_keys_config_2,
|
|
key_len2,
|
|
get_timestamp_ms(),
|
|
a.info,
|
|
a.members));
|
|
config_string_list* hashes;
|
|
hashes = config_merge(a.info, merge_hash2, &merge_data2[0], &merge_size2[0], 1);
|
|
REQUIRE(hashes->len);
|
|
free(hashes);
|
|
config_confirm_pushed(a.info, new_info_config2->seqno, &(tmphash = "fakehash2"), 1);
|
|
hashes = config_merge(a.members, merge_hash2, &merge_data2[1], &merge_size2[1], 1);
|
|
REQUIRE(hashes->len);
|
|
free(hashes);
|
|
config_confirm_pushed(a.members, new_mem_config2->seqno, &(tmphash = "fakehash2"), 1);
|
|
|
|
REQUIRE(groups_members_size(a.members) == 5);
|
|
}
|
|
|
|
{
|
|
// Test get keys bulk API
|
|
{
|
|
size_t keys_size = groups_keys_size(admins[0].keys);
|
|
std::vector<span_u8> keys_list;
|
|
keys_list.resize(keys_size);
|
|
size_t keys_read =
|
|
groups_keys_get_keys(admins[0].keys, 0, keys_list.data(), keys_list.size());
|
|
REQUIRE(keys_read == keys_size);
|
|
}
|
|
|
|
// Test get keys OOB
|
|
{
|
|
size_t keys_size = groups_keys_size(admins[0].keys);
|
|
std::vector<span_u8> keys_list;
|
|
keys_list.resize(keys_size);
|
|
size_t keys_read = groups_keys_get_keys(
|
|
admins[0].keys, keys_size, keys_list.data(), keys_list.size());
|
|
REQUIRE(keys_read == 0);
|
|
}
|
|
}
|
|
|
|
free(new_info_config2);
|
|
free(new_mem_config2);
|
|
}
|
|
|
|
TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") {
|
|
|
|
const std::vector<unsigned char> group_seed =
|
|
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin_seed =
|
|
"0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> member_seed =
|
|
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes;
|
|
|
|
std::array<unsigned char, 32> group_pk;
|
|
std::array<unsigned char, 64> group_sk;
|
|
|
|
crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data());
|
|
REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) ==
|
|
oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32));
|
|
|
|
CHECK(oxenc::to_hex(group_pk.begin(), group_pk.end()) ==
|
|
"c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625");
|
|
|
|
pseudo_client admin{admin_seed, true, group_pk.data(), group_sk.data()};
|
|
pseudo_client member{member_seed, false, group_pk.data(), std::nullopt};
|
|
session::config::UserGroups member_groups{member_seed, std::nullopt};
|
|
|
|
CHECK(admin.session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627");
|
|
|
|
CHECK(member.session_id ==
|
|
"05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5"
|
|
"e");
|
|
CHECK(oxenc::to_hex(group_pk.begin(), group_pk.end()) ==
|
|
"c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625");
|
|
CHECK(member.info.id == "03c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625");
|
|
|
|
auto auth_data = admin.keys.swarm_make_subaccount(member.session_id);
|
|
{
|
|
auto g = member_groups.get_or_construct_group(member.info.id);
|
|
g.auth_data = auth_data;
|
|
member_groups.set(g);
|
|
}
|
|
|
|
session::config::UserGroups member_gr2{member_seed, std::nullopt};
|
|
auto [seqno, push, obs] = member_groups.push();
|
|
|
|
REQUIRE(push.size() == 1);
|
|
|
|
std::vector<std::pair<std::string, std::span<const unsigned char>>> gr_conf;
|
|
gr_conf.emplace_back("fakehash1", push[0]);
|
|
|
|
member_gr2.merge(gr_conf);
|
|
|
|
auto g = member_groups.get_group(member.info.id);
|
|
REQUIRE(g);
|
|
CHECK(g->id == member.info.id);
|
|
CHECK(g->auth_data == auth_data);
|
|
|
|
auto to_sign = session::to_span("retrieve9991693340111000");
|
|
auto subauth_b64 = member.keys.swarm_subaccount_sign(to_sign, auth_data);
|
|
|
|
CHECK(subauth_b64.subaccount == "AwMAAIWvMR2nJXCFnK5+hNahNecWqMC39/TVVLjaR3imNug5");
|
|
CHECK(subauth_b64.subaccount_sig ==
|
|
"6brvv/"
|
|
"2jfciBAJeRKMGSepNJLullyrVVHijyVDE+8GC5Oc89UNxjNrq1kVV1P+pkUIRDOew24gSLFgLZfdl+BQ==");
|
|
CHECK(subauth_b64.signature ==
|
|
"c3PJ4g29v5RivKm8Tdg49vGU2/"
|
|
"6kVd0yONnpz5U5zePMYptqW3iYQ0TYf2rEzv3qqkPhS5p67M5GAccHoBHGDQ==");
|
|
|
|
auto subauth = member.keys.swarm_subaccount_sign(to_sign, auth_data, true);
|
|
CHECK(oxenc::to_base64(subauth.subaccount) == subauth_b64.subaccount);
|
|
CHECK(oxenc::to_base64(subauth.subaccount_sig) == subauth_b64.subaccount_sig);
|
|
CHECK(oxenc::to_base64(subauth.signature) == subauth_b64.signature);
|
|
|
|
CHECK(0 ==
|
|
crypto_sign_ed25519_verify_detached(
|
|
reinterpret_cast<const unsigned char*>(subauth.signature.data()),
|
|
to_sign.data(),
|
|
to_sign.size(),
|
|
reinterpret_cast<const unsigned char*>(subauth.subaccount.substr(4).data())));
|
|
|
|
CHECK(member.keys.swarm_verify_subaccount(auth_data));
|
|
CHECK(session::config::groups::Keys::swarm_verify_subaccount(
|
|
member.info.id, session::to_span(member.secret_key), auth_data));
|
|
|
|
// Try flipping a bit in each position of the auth data and make sure it fails to validate:
|
|
for (size_t i = 0; i < auth_data.size(); i++) {
|
|
for (int b = 0; b < 8; b++) {
|
|
if (i == 35 && b == 7) // This is the sign bit of k, which can be flipped but gets
|
|
// flipped back when dealing with the missing X->Ed conversion
|
|
// sign bit, so won't actually change anything if it flips.
|
|
continue;
|
|
auto auth_data2 = auth_data;
|
|
auth_data2[i] ^= 1 << b;
|
|
CHECK_FALSE(session::config::groups::Keys::swarm_verify_subaccount(
|
|
member.info.id, session::to_span(member.secret_key), auth_data2));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") {
|
|
|
|
const std::vector<unsigned char> group_seed =
|
|
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> admin1_seed =
|
|
"0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
|
|
const std::vector<unsigned char> member1_seed =
|
|
"000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes;
|
|
|
|
std::array<unsigned char, 32> group_pk;
|
|
std::array<unsigned char, 64> group_sk;
|
|
|
|
crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data());
|
|
REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) ==
|
|
oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32));
|
|
|
|
pseudo_client admin{admin1_seed, true, group_pk.data(), group_sk.data()};
|
|
pseudo_client member{member1_seed, false, group_pk.data(), std::nullopt};
|
|
|
|
std::vector<std::pair<std::string, std::span<const unsigned char>>> configs;
|
|
{
|
|
auto m = admin.members.get_or_construct(admin.session_id);
|
|
m.admin = true;
|
|
m.name = "Lrrr";
|
|
admin.members.set(m);
|
|
}
|
|
{
|
|
auto m = admin.members.get_or_construct(member.session_id);
|
|
m.admin = false;
|
|
m.name = "Nibbler";
|
|
admin.members.set(m);
|
|
}
|
|
admin.info.set_name("Omicron Persei 8");
|
|
auto [mseq, mdata, mobs] = admin.members.push();
|
|
admin.members.confirm_pushed(mseq, {"mpush1"});
|
|
auto [iseq, idata, iobs] = admin.info.push();
|
|
admin.info.confirm_pushed(mseq, {"ipush1"});
|
|
|
|
REQUIRE(admin.keys.pending_config());
|
|
member.keys.load_key_message(
|
|
"keyhash1",
|
|
*admin.keys.pending_config(),
|
|
get_timestamp_ms(),
|
|
member.info,
|
|
member.members);
|
|
admin.keys.load_key_message(
|
|
"keyhash1",
|
|
*admin.keys.pending_config(),
|
|
get_timestamp_ms(),
|
|
member.info,
|
|
member.members);
|
|
|
|
member.keys.load_key_message(
|
|
"keyhash2",
|
|
admin.keys.key_supplement(member.session_id),
|
|
get_timestamp_ms(),
|
|
member.info,
|
|
member.members);
|
|
|
|
REQUIRE(mdata.size() == 1);
|
|
configs.emplace_back("mpush1", mdata[0]);
|
|
CHECK(member.members.merge(configs) == std::unordered_set{{"mpush1"s}});
|
|
|
|
configs.clear();
|
|
REQUIRE(idata.size() == 1);
|
|
configs.emplace_back("ipush1", idata[0]);
|
|
CHECK(member.info.merge(configs) == std::unordered_set{{"ipush1"s}});
|
|
|
|
REQUIRE(admin.keys.admin());
|
|
REQUIRE_FALSE(member.keys.admin());
|
|
REQUIRE(member.info.is_readonly());
|
|
REQUIRE(member.members.is_readonly());
|
|
|
|
member.keys.load_admin_key(session::to_span(group_sk), member.info, member.members);
|
|
|
|
CHECK(member.keys.admin());
|
|
CHECK_FALSE(member.members.is_readonly());
|
|
CHECK_FALSE(member.info.is_readonly());
|
|
|
|
member.info.set_name("new name"s);
|
|
|
|
CHECK(member.info.needs_push());
|
|
auto [iseq2, idata2, iobs2] = member.info.push();
|
|
REQUIRE(idata2.size() == 1);
|
|
|
|
configs.clear();
|
|
configs.emplace_back("ihash2", idata2[0]);
|
|
|
|
CHECK(admin.info.merge(configs) == std::unordered_set{{"ihash2"s}});
|
|
|
|
CHECK(admin.info.get_name() == "new name");
|
|
}
|