mirror of
https://github.com/lux-tel/libsession-util.git
synced 2026-10-30 20:25:11 +00:00
The *_unix_ts members/params are sys_seconds / sys_ms time points, so the name baked the representation into the identifier. Rename them to *_at (as the backend did) to stop embedding the type. Pure rename: JSON wire keys and protobuf accessors are untouched, so there is no wire change. Also drop the stale expiry_unix_ts_ms grace-period doc formula (the wire is integer seconds now; no *_ms field survives). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
655 lines
28 KiB
C++
655 lines
28 KiB
C++
#include <oxenc/hex.h>
|
|
#include <session/config/encrypt.h>
|
|
#include <session/config/user_profile.h>
|
|
#include <sodium/crypto_sign_ed25519.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstring>
|
|
#include <session/config/base.hpp>
|
|
#include <session/config/user_profile.hpp>
|
|
#include <session/util.hpp>
|
|
#include <string_view>
|
|
|
|
#include "utils.hpp"
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace {
|
|
struct UserProfileTester {
|
|
static std::chrono::sys_seconds get_profile_updated_value(config_object* conf) {
|
|
return std::chrono::sys_seconds{std::chrono::seconds{
|
|
session::config::unbox<session::config::UserProfile>(conf)->data["t"].integer_or(
|
|
0)}};
|
|
}
|
|
|
|
static void set_profile_updated(config_object* conf, std::chrono::sys_seconds value) {
|
|
session::config::unbox<session::config::UserProfile>(conf)->data["t"] =
|
|
static_cast<int>(value.time_since_epoch().count());
|
|
}
|
|
|
|
static void set_profile_updated(
|
|
session::config::UserProfile& profile, std::chrono::sys_seconds value) {
|
|
profile.data["t"] = static_cast<int>(value.time_since_epoch().count());
|
|
}
|
|
|
|
static std::chrono::sys_seconds get_reupload_profile_updated_value(config_object* conf) {
|
|
return std::chrono::sys_seconds{std::chrono::seconds{
|
|
session::config::unbox<session::config::UserProfile>(conf)->data["T"].integer_or(
|
|
0)}};
|
|
}
|
|
|
|
static void set_reupload_profile_updated(config_object* conf, std::chrono::sys_seconds value) {
|
|
session::config::unbox<session::config::UserProfile>(conf)->data["T"] =
|
|
static_cast<int>(value.time_since_epoch().count());
|
|
}
|
|
|
|
static uint64_t get_raw_profile_updated_value(config_object* conf) {
|
|
return session::config::unbox<session::config::UserProfile>(conf)->data["t"].integer_or(0);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("UserProfile", "[config][user_profile]") {
|
|
|
|
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
|
|
std::array<unsigned char, 32> ed_pk, curve_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()));
|
|
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
|
|
REQUIRE(rc == 0);
|
|
|
|
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
|
|
"4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7");
|
|
REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) ==
|
|
"d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72");
|
|
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
|
|
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
|
|
|
|
session::config::UserProfile profile{std::span<const unsigned char>{seed}, std::nullopt};
|
|
|
|
CHECK_THROWS(
|
|
profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789"
|
|
"0123456789012345678901234567890A"));
|
|
CHECK_NOTHROW(
|
|
profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789"
|
|
"01234567890123456789012345678901234567890A"));
|
|
CHECK(profile.get_name() ==
|
|
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
|
"901234567890");
|
|
CHECK_NOTHROW(
|
|
profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789"
|
|
"01234567890123456789012345678901234567🎂"));
|
|
CHECK(profile.get_name() ==
|
|
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
|
"901234567");
|
|
CHECK_NOTHROW(
|
|
profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789"
|
|
"012345678901234567890123456789012345🎂🎂"));
|
|
CHECK(profile.get_name() ==
|
|
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
|
"9012345🎂");
|
|
}
|
|
|
|
TEST_CASE("user profile C API", "[config][user_profile][c]") {
|
|
|
|
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex;
|
|
std::array<unsigned char, 32> ed_pk, curve_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()));
|
|
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
|
|
REQUIRE(rc == 0);
|
|
|
|
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
|
|
"4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7");
|
|
REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) ==
|
|
"d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72");
|
|
CHECK(oxenc::to_hex(seed) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
|
|
|
|
// Initialize a brand new, empty config because we have no dump data to deal with.
|
|
char err[256];
|
|
config_object* conf;
|
|
rc = user_profile_init(&conf, ed_sk.data(), NULL, 0, err);
|
|
REQUIRE(rc == 0);
|
|
|
|
// We don't need to push anything, since this is an empty config
|
|
CHECK_FALSE(config_needs_push(conf));
|
|
// And we haven't changed anything so don't need to dump to db
|
|
CHECK_FALSE(config_needs_dump(conf));
|
|
|
|
// Since it's empty there shouldn't be a name.
|
|
const char* name = user_profile_get_name(conf);
|
|
CHECK(name == nullptr); // (should be NULL instead of nullptr in C)
|
|
|
|
// We don't need to push since we haven't changed anything, so this call is mainly just for
|
|
// testing:
|
|
config_push_data* to_push = config_push(conf);
|
|
REQUIRE(to_push);
|
|
CHECK(to_push->seqno == 0);
|
|
REQUIRE(to_push->n_configs == 1);
|
|
CHECK(to_push->config_lens[0] == 256 + 176); // 176 = protobuf overhead
|
|
const char* enc_domain = "UserProfile";
|
|
REQUIRE(config_encryption_domain(conf) == std::string_view{enc_domain});
|
|
|
|
// There's nothing particularly profound about this value (it is multiple layers of nested
|
|
// protobuf with some encryption and padding halfway through); this test is just here to ensure
|
|
// that our pushed messages are deterministic:
|
|
CHECK(oxenc::to_hex(to_push->config[0], to_push->config[0] + to_push->config_lens[0]) ==
|
|
"080112ab030a0012001aa20308062801429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39"
|
|
"98af44ca65f967768dd78ee80fffab6f809f6cef49c73a36c82a89622ff0de2ceee06b8c638e2c876fa9047f"
|
|
"449dbe24b1fc89281a264fe90abdeffcdd44f797bd4572a6c5ae8d88bf372c3c717943ebd570222206fabf0e"
|
|
"e9f3c6756f5d71a32616b1df53d12887961f5c129207a79622ccc1a4bba976886d9a6ddf0fe5d570e5075d01"
|
|
"ecd627f656e95f27b4c40d5661b5664cedd3e568206effa1308b0ccd663ca61a6d39c0731891804a8cf5edcf"
|
|
"8b98eaa5580c3d436e22156e38455e403869700956c3c1dd0b4470b663e75c98c5b859b53ccef6559215d804"
|
|
"9f755be9c2d6b3f4a310f97c496fc392f65b6431dd87788ac61074fd8cd409702e1b839b3f774d38cf8b28f0"
|
|
"226c4efa5220ac6ae060793e36e7ef278d42d042f15b21291f3bb29e3158f09d154b93f83fd8a319811a26cb"
|
|
"5240d90cbb360fafec0b7eff4c676ae598540813d062dc9468365c73b4cfa2ffd02d48cdcd8f0c71324c6d0a"
|
|
"60346a7a0e50af3be64684b37f9e6c831115bf112ddd18acde08eaec376f0872a3952000");
|
|
|
|
free(to_push);
|
|
|
|
// These should also be unset:
|
|
auto pic = user_profile_get_pic(conf);
|
|
CHECK(strlen(pic.url) == 0);
|
|
CHECK(user_profile_get_nts_priority(conf) == 0);
|
|
CHECK(user_profile_get_nts_expiry(conf) == 0);
|
|
|
|
// Now let's go set them:
|
|
CHECK(0 == user_profile_set_name(conf, "Kallie"));
|
|
user_profile_pic p;
|
|
strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < sizeof(p.url)!
|
|
memcpy(p.key, "secret78901234567890123456789012", 32);
|
|
CHECK(0 == user_profile_set_pic(conf, p));
|
|
user_profile_set_nts_priority(conf, 9);
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{123s});
|
|
|
|
// Retrieve them just to make sure they set properly:
|
|
name = user_profile_get_name(conf);
|
|
REQUIRE(name != nullptr); // (should be NULL instead of nullptr in C)
|
|
CHECK(name == "Kallie"sv);
|
|
|
|
pic = user_profile_get_pic(conf);
|
|
REQUIRE(pic.url != ""s);
|
|
REQUIRE(pic.key != session::to_vector("").data());
|
|
CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv);
|
|
CHECK(session::to_vector(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"secret78901234567890123456789012"_bytes);
|
|
|
|
CHECK(user_profile_get_nts_priority(conf) == 9);
|
|
|
|
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
|
// to dump the updated state:
|
|
|
|
CHECK(config_needs_push(conf));
|
|
CHECK(config_needs_dump(conf));
|
|
to_push = config_push(conf);
|
|
CHECK(to_push->seqno == 1); // incremented since we made changes (this only increments once
|
|
// between dumps; even though we changed two fields here).
|
|
|
|
// The hash of a completely empty, initial seqno=0 message:
|
|
auto exp_hash0 = "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes;
|
|
|
|
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
|
|
// clang-format off
|
|
auto exp_push1_decrypted = session::to_vector(
|
|
"d"
|
|
"1:#" "i1e"
|
|
"1:&" "d"
|
|
"1:+" "i9e"
|
|
"1:n" "6:Kallie"
|
|
"1:p" "34:http://example.org/omg-pic-123.bmp"
|
|
"1:q" "32:secret78901234567890123456789012"
|
|
"1:t" "i123e"
|
|
"e"
|
|
"1:<" "l"
|
|
"l" "i0e" "32:" + session::to_string(exp_hash0) + "de" "e"
|
|
"e"
|
|
"1:=" "d"
|
|
"1:+" "0:"
|
|
"1:n" "0:"
|
|
"1:p" "0:"
|
|
"1:q" "0:"
|
|
"1:t" "0:"
|
|
"e"
|
|
"e");
|
|
// clang-format on
|
|
auto exp_push1_encrypted =
|
|
"9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f"
|
|
"1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84"
|
|
"aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868"
|
|
"2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d"
|
|
"e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656"
|
|
"056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea"
|
|
"49bf122762d7bc1d6d9c02f6d54f8384"_hexbytes;
|
|
|
|
// Copy this out; we need to hold onto it to do the confirmation later on
|
|
seqno_t seqno = to_push->seqno;
|
|
|
|
// config_push gives us back a buffer that we are required to free when done. (Without this
|
|
// we'd leak memory!)
|
|
free(to_push);
|
|
|
|
// We haven't dumped, so still need to dump:
|
|
CHECK(config_needs_dump(conf));
|
|
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
|
|
CHECK(config_needs_push(conf));
|
|
unsigned char* dump1;
|
|
size_t dump1len;
|
|
|
|
config_dump(conf, &dump1, &dump1len);
|
|
// (in a real client we'd now store this to disk)
|
|
|
|
CHECK_FALSE(config_needs_dump(conf));
|
|
|
|
// clang-format off
|
|
CHECK(printable(dump1, dump1len) == printable(
|
|
"d"
|
|
"1:!" "i2e"
|
|
"1:${}:{}"
|
|
"1:(" "le"
|
|
"1:)" "le"
|
|
"1:*" "de"
|
|
"1:+" "de"
|
|
"e"_format(exp_push1_decrypted.size(), session::to_string(exp_push1_decrypted))));
|
|
// clang-format on
|
|
free(dump1); // done with the dump; don't leak!
|
|
|
|
const char* tmphash; // test suite cheat: &(tmphash = "asdf") to fake a length-1 array.
|
|
|
|
// So now imagine we got back confirmation from the swarm that the push has been stored:
|
|
config_confirm_pushed(conf, seqno, &(tmphash = "fakehash1"), 1);
|
|
|
|
CHECK_FALSE(config_needs_push(conf));
|
|
CHECK(config_needs_dump(conf)); // The confirmation changes state, so this makes us need a dump
|
|
// again.
|
|
config_dump(conf, &dump1, &dump1len);
|
|
|
|
// clang-format off
|
|
CHECK(printable(dump1, dump1len) == printable(
|
|
"d"
|
|
"1:!" "i0e"
|
|
"1:${}:{}"
|
|
"1:(" "l" "9:fakehash1" "e"
|
|
"1:)" "le"
|
|
"1:*" "de"
|
|
"1:+" "de"
|
|
"e"_format(exp_push1_decrypted.size(), session::to_string(exp_push1_decrypted))));
|
|
// clang-format on
|
|
free(dump1);
|
|
|
|
CHECK_FALSE(config_needs_dump(conf));
|
|
|
|
// Now we're going to set up a second, competing config object (in the real world this would be
|
|
// another Session client somewhere).
|
|
|
|
// Start with an empty config, as above:
|
|
config_object* conf2;
|
|
REQUIRE(user_profile_init(&conf2, ed_sk.data(), NULL, 0, err) == 0);
|
|
CHECK_FALSE(config_needs_dump(conf2));
|
|
|
|
// Now imagine we just pulled down the encrypted string from the swarm; we merge it into conf2:
|
|
const unsigned char* merge_data[1];
|
|
const char* merge_hash[1];
|
|
size_t merge_size[1];
|
|
merge_hash[0] = "fakehash1";
|
|
merge_data[0] = exp_push1_encrypted.data();
|
|
merge_size[0] = exp_push1_encrypted.size();
|
|
config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1);
|
|
REQUIRE(accepted->len == 1);
|
|
CHECK(accepted->value[0] == "fakehash1"sv);
|
|
free(accepted);
|
|
|
|
// Our state has changed, so we need to dump:
|
|
CHECK(config_needs_dump(conf2));
|
|
unsigned char* dump2;
|
|
size_t dump2len;
|
|
config_dump(conf2, &dump2, &dump2len);
|
|
// (store in db)
|
|
free(dump2);
|
|
CHECK_FALSE(config_needs_dump(conf2));
|
|
|
|
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
|
|
// didn't have any sort of merge conflict needed):
|
|
REQUIRE_FALSE(config_needs_push(conf2));
|
|
|
|
// Now let's create a conflicting update:
|
|
|
|
// Change the name on both clients:
|
|
user_profile_set_name(conf, "Nibbler");
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{123s});
|
|
|
|
user_profile_set_name(conf2, "Raz");
|
|
|
|
// And, on conf2, we're also going to change some other things:
|
|
strcpy(p.url, "http://new.example.com/pic");
|
|
memcpy(p.key, "qwert\0yuio1234567890123456789012", 32);
|
|
user_profile_set_pic(conf2, p);
|
|
|
|
user_profile_set_nts_expiry(conf2, 86400);
|
|
CHECK(user_profile_get_nts_expiry(conf2) == 86400);
|
|
|
|
CHECK(user_profile_get_blinded_msgreqs(conf2) == -1);
|
|
user_profile_set_blinded_msgreqs(conf2, 0);
|
|
CHECK(user_profile_get_blinded_msgreqs(conf2) == 0);
|
|
user_profile_set_blinded_msgreqs(conf2, -1);
|
|
CHECK(user_profile_get_blinded_msgreqs(conf2) == -1);
|
|
user_profile_set_blinded_msgreqs(conf2, 1);
|
|
CHECK(user_profile_get_blinded_msgreqs(conf2) == 1);
|
|
UserProfileTester::set_profile_updated(conf2, std::chrono::sys_seconds{124s});
|
|
|
|
// Both have changes, so push need a push
|
|
CHECK(config_needs_push(conf));
|
|
CHECK(config_needs_push(conf2));
|
|
to_push = config_push(conf);
|
|
|
|
CHECK(to_push->seqno == 2); // incremented, since we made a field change
|
|
config_confirm_pushed(conf2, to_push->seqno, &(tmphash = "fakehash2"), 1);
|
|
|
|
config_push_data* to_push2 = config_push(conf2);
|
|
CHECK(to_push2->seqno == 2); // incremented, since we made a field change
|
|
config_confirm_pushed(conf2, to_push2->seqno, &(tmphash = "fakehash3"), 1);
|
|
|
|
config_dump(conf, &dump1, &dump1len);
|
|
config_dump(conf2, &dump2, &dump2len);
|
|
// (store in db)
|
|
free(dump1);
|
|
free(dump2);
|
|
|
|
// Since we set different things, we're going to get back different serialized data to be
|
|
// pushed:
|
|
REQUIRE(to_push->n_configs == 1);
|
|
REQUIRE(to_push2->n_configs == 1);
|
|
CHECK(printable(to_push->config[0], to_push->config_lens[0]) !=
|
|
printable(to_push2->config[0], to_push2->config_lens[0]));
|
|
|
|
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
|
|
// also fetches new messages and pulls down the other client's `seqno=2` value.
|
|
|
|
// Feed the new config into each other. (This array could hold multiple configs if we pulled
|
|
// down more than one).
|
|
merge_hash[0] = "fakehash2";
|
|
merge_data[0] = to_push->config[0];
|
|
merge_size[0] = to_push->config_lens[0];
|
|
accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1);
|
|
free(to_push);
|
|
REQUIRE(accepted->len == 1);
|
|
CHECK(accepted->value[0] == "fakehash2"sv);
|
|
free(accepted);
|
|
merge_hash[0] = "fakehash3";
|
|
merge_data[0] = to_push2->config[0];
|
|
merge_size[0] = to_push2->config_lens[0];
|
|
accepted = config_merge(conf, merge_hash, merge_data, merge_size, 1);
|
|
REQUIRE(accepted->len == 1);
|
|
CHECK(accepted->value[0] == "fakehash3"sv);
|
|
free(accepted);
|
|
free(to_push2);
|
|
|
|
// Now after the merge we *will* want to push from both client, since both will have generated a
|
|
// merge conflict update (with seqno = 3).
|
|
to_push = config_push(conf);
|
|
to_push2 = config_push(conf2);
|
|
|
|
REQUIRE(to_push->seqno == 3);
|
|
REQUIRE(to_push2->seqno == 3);
|
|
REQUIRE(config_needs_push(conf));
|
|
REQUIRE(config_needs_push(conf2));
|
|
|
|
// They should have resolved the conflict to the same thing:
|
|
CHECK(user_profile_get_name(conf) == "Nibbler"sv);
|
|
CHECK(user_profile_get_name(conf2) == "Nibbler"sv);
|
|
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
|
|
// message just happens to have a higher hash -- and thus gets priority -- for this particular
|
|
// test).
|
|
|
|
// Since only one of them set a profile pic there should be no conflict there:
|
|
pic = user_profile_get_pic(conf);
|
|
#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__)
|
|
REQUIRE(pic.url);
|
|
#else
|
|
REQUIRE(pic.url != nullptr);
|
|
#endif
|
|
CHECK(pic.url == "http://new.example.com/pic"sv);
|
|
#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__)
|
|
REQUIRE(pic.key);
|
|
#else
|
|
REQUIRE(pic.key != nullptr);
|
|
#endif
|
|
CHECK(oxenc::to_hex(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"7177657274007975696f31323334353637383930313233343536373839303132");
|
|
pic = user_profile_get_pic(conf2);
|
|
#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__)
|
|
REQUIRE(pic.url);
|
|
#else
|
|
REQUIRE(pic.url != nullptr);
|
|
#endif
|
|
CHECK(pic.url == "http://new.example.com/pic"sv);
|
|
#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__)
|
|
REQUIRE(pic.key);
|
|
#else
|
|
REQUIRE(pic.key != nullptr);
|
|
#endif
|
|
CHECK(oxenc::to_hex(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"7177657274007975696f31323334353637383930313233343536373839303132");
|
|
|
|
CHECK(user_profile_get_nts_priority(conf) == 9);
|
|
CHECK(user_profile_get_nts_priority(conf2) == 9);
|
|
CHECK(user_profile_get_nts_expiry(conf) == 86400);
|
|
CHECK(user_profile_get_nts_expiry(conf2) == 86400);
|
|
CHECK(user_profile_get_blinded_msgreqs(conf) == 1);
|
|
CHECK(user_profile_get_blinded_msgreqs(conf2) == 1);
|
|
|
|
config_confirm_pushed(conf, to_push->seqno, &(tmphash = "fakehash4"), 1);
|
|
config_confirm_pushed(conf2, to_push2->seqno, &(tmphash = "fakehash4"), 1);
|
|
|
|
config_dump(conf, &dump1, &dump1len);
|
|
config_dump(conf2, &dump2, &dump2len);
|
|
// (store in db)
|
|
free(dump1);
|
|
free(dump2);
|
|
|
|
CHECK_FALSE(config_needs_dump(conf));
|
|
CHECK_FALSE(config_needs_dump(conf2));
|
|
CHECK_FALSE(config_needs_push(conf));
|
|
CHECK_FALSE(config_needs_push(conf2));
|
|
|
|
// Check the current pic
|
|
pic = user_profile_get_pic(conf);
|
|
REQUIRE(pic.url != ""s);
|
|
REQUIRE(pic.key != session::to_vector("").data());
|
|
CHECK(pic.url == "http://new.example.com/pic"sv);
|
|
CHECK(session::to_vector(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"qwert\0yuio1234567890123456789012"_bytes);
|
|
|
|
// Reupload the "current" pic and confirm it gets returned
|
|
strcpy(p.url, "testUrl");
|
|
memcpy(p.key, "secret78901234567890123456789000", 32);
|
|
CHECK(0 == user_profile_set_reupload_pic(conf, p));
|
|
|
|
pic = user_profile_get_pic(conf);
|
|
REQUIRE(pic.url != ""s);
|
|
REQUIRE(pic.key != session::to_vector("").data());
|
|
CHECK(pic.url == "testUrl"sv);
|
|
CHECK(session::to_vector(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"secret78901234567890123456789000"_bytes);
|
|
|
|
// Upload a "new" pic and it now gets returned
|
|
strcpy(p.url, "testNewUrl");
|
|
memcpy(p.key, "secret78901234567890123456789111", 32);
|
|
CHECK(0 == user_profile_set_pic(conf, p));
|
|
pic = user_profile_get_pic(conf);
|
|
REQUIRE(pic.url != ""s);
|
|
REQUIRE(pic.key != session::to_vector("").data());
|
|
CHECK(pic.url == "testNewUrl"sv);
|
|
CHECK(session::to_vector(std::span<const unsigned char>{pic.key, 32}) ==
|
|
"secret78901234567890123456789111"_bytes);
|
|
|
|
// Ensure the timestamp for the last modified pic gets updated correctly when the name gets set
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
UserProfileTester::set_reupload_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
|
|
CHECK(0 == user_profile_set_pic(conf, p));
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{123s});
|
|
user_profile_set_name(conf, "test1");
|
|
CHECK(UserProfileTester::get_profile_updated_value(conf).time_since_epoch().count() != 123);
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
|
|
UserProfileTester::set_reupload_profile_updated(conf, std::chrono::sys_seconds{124s});
|
|
CHECK(0 == user_profile_set_reupload_pic(conf, p));
|
|
user_profile_set_name(conf, "test2");
|
|
CHECK(UserProfileTester::get_reupload_profile_updated_value(conf).time_since_epoch().count() !=
|
|
124);
|
|
|
|
// Ensure the timestamp for the last modified pic gets updated correctly when the blinded msgreq
|
|
// is set
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
UserProfileTester::set_reupload_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
|
|
strcpy(p.url, "http://example.org/omg-pic-124.bmp"); // NB: length must be < sizeof(p.url)!
|
|
CHECK(0 == user_profile_set_pic(conf, p));
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{123s});
|
|
user_profile_set_blinded_msgreqs(conf, 0);
|
|
CHECK(UserProfileTester::get_profile_updated_value(conf).time_since_epoch().count() != 123);
|
|
UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{0s});
|
|
|
|
UserProfileTester::set_reupload_profile_updated(conf, std::chrono::sys_seconds{124s});
|
|
CHECK(0 == user_profile_set_reupload_pic(conf, p));
|
|
user_profile_set_blinded_msgreqs(conf, 1);
|
|
CHECK(UserProfileTester::get_reupload_profile_updated_value(conf).time_since_epoch().count() !=
|
|
124);
|
|
|
|
// Ensure the timestamp is stored in seconds seconds (was incorrectly stored as microseconds)
|
|
auto time_before_call = std::chrono::system_clock::now();
|
|
strcpy(p.url, "http://example.org/omg-pic-125.bmp"); // NB: length must be < sizeof(p.url)!
|
|
CHECK(0 == user_profile_set_pic(conf, p));
|
|
auto time_after_call = std::chrono::system_clock::now();
|
|
auto before_seconds =
|
|
std::chrono::duration_cast<std::chrono::seconds>(time_before_call.time_since_epoch())
|
|
.count();
|
|
auto after_seconds =
|
|
std::chrono::duration_cast<std::chrono::seconds>(time_before_call.time_since_epoch())
|
|
.count();
|
|
|
|
auto raw_value = UserProfileTester::get_raw_profile_updated_value(conf);
|
|
INFO("Checking if raw_value " << raw_value << " is within the range [" << before_seconds << ", "
|
|
<< after_seconds << "]");
|
|
CHECK((raw_value >= before_seconds && raw_value <= after_seconds));
|
|
}
|
|
|
|
TEST_CASE("user profile timestamp update bug", "[config][user_profile]") {
|
|
|
|
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
|
|
|
|
session::config::UserProfile profile{std::span<const unsigned char>{seed}, std::nullopt};
|
|
|
|
// Initially the code would update `profile_updated` even if the data hadn't changed, this test
|
|
// verifies that no longer happens
|
|
std::vector<unsigned char> key = "qwerty78901234567890123456789012"_bytes;
|
|
std::string url = "http://example.com/huge.bmp";
|
|
profile.set_name("Nibbler");
|
|
profile.set_blinded_msgreqs(true);
|
|
profile.set_profile_pic(url, key);
|
|
auto seconds_before_call = profile.get_profile_updated();
|
|
std::this_thread::sleep_for(2s);
|
|
profile.set_name("Nibbler");
|
|
profile.set_blinded_msgreqs(true);
|
|
profile.set_profile_pic(url, key);
|
|
auto seconds_after_call = profile.get_profile_updated();
|
|
CHECK(profile.get_profile_updated() == seconds_before_call);
|
|
|
|
// Also make sure it does change
|
|
profile.set_name("Nibbler1");
|
|
CHECK(profile.get_profile_updated() != seconds_before_call);
|
|
}
|
|
|
|
TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") {
|
|
|
|
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
|
|
|
|
session::config::UserProfile profile{std::span<const unsigned char>{seed}, std::nullopt};
|
|
|
|
// Ensure the bitset is being updated correctly
|
|
CHECK(profile.get_profile_bitset().data == 0);
|
|
|
|
profile.set_pro_badge(true);
|
|
CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE));
|
|
|
|
profile.set_pro_badge(false);
|
|
CHECK(profile.get_profile_bitset().data == 0);
|
|
|
|
profile.set_animated_avatar(true);
|
|
CHECK(profile.get_profile_bitset().is_set(
|
|
SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR));
|
|
|
|
profile.set_animated_avatar(false);
|
|
CHECK(profile.get_profile_bitset().data == 0);
|
|
|
|
profile.set_pro_badge(true);
|
|
profile.set_animated_avatar(true);
|
|
CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE));
|
|
CHECK(profile.get_profile_bitset().is_set(
|
|
SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR));
|
|
|
|
profile.set_animated_avatar(false);
|
|
CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE));
|
|
CHECK_FALSE(profile.get_profile_bitset().is_set(
|
|
SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR));
|
|
|
|
{
|
|
session::config::UserProfile profile2{std::span<const unsigned char>{seed}, profile.dump()};
|
|
CHECK(profile2.get_profile_bitset().is_set(
|
|
SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE));
|
|
CHECK_FALSE(profile2.get_profile_bitset().is_set(
|
|
SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR));
|
|
}
|
|
|
|
// Ensure the pro config is being stored correctly
|
|
std::array<uint8_t, crypto_sign_ed25519_PUBLICKEYBYTES> rotating_pk, signing_pk;
|
|
session::cleared_uc64 rotating_sk, signing_sk;
|
|
{
|
|
crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data());
|
|
crypto_sign_ed25519_keypair(signing_pk.data(), signing_sk.data());
|
|
}
|
|
|
|
session::config::ProConfig pro_cpp = {};
|
|
pro_pro_config pro = {};
|
|
{
|
|
// CPP
|
|
pro_cpp.rotating_privkey = rotating_sk;
|
|
pro_cpp.proof.version = 2;
|
|
pro_cpp.proof.rotating_pubkey = rotating_pk;
|
|
pro_cpp.proof.expiry_at = std::chrono::sys_seconds(1s);
|
|
constexpr auto revocation_tag =
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_u;
|
|
static_assert(pro_cpp.proof.revocation_tag.max_size() == revocation_tag.size());
|
|
std::memcpy(
|
|
pro_cpp.proof.revocation_tag.data(), revocation_tag.data(), revocation_tag.size());
|
|
|
|
// C
|
|
std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size());
|
|
pro.proof.version = pro_cpp.proof.version;
|
|
std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size());
|
|
pro.proof.expiry_ts = pro_cpp.proof.expiry_at.time_since_epoch().count();
|
|
std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size());
|
|
}
|
|
|
|
UserProfileTester::set_profile_updated(profile, std::chrono::sys_seconds{123s});
|
|
CHECK(profile.get_profile_updated().time_since_epoch().count() == 123);
|
|
CHECK_FALSE(profile.get_pro_config().has_value());
|
|
profile.set_pro_config(pro_cpp);
|
|
CHECK(profile.get_pro_config() == pro_cpp);
|
|
CHECK(profile.get_profile_updated().time_since_epoch().count() != 123);
|
|
|
|
{
|
|
session::config::UserProfile profile2{std::span<const unsigned char>{seed}, profile.dump()};
|
|
CHECK(profile.get_pro_config() == pro_cpp);
|
|
}
|
|
|
|
profile.remove_pro_config();
|
|
CHECK_FALSE(profile.get_pro_config().has_value());
|
|
|
|
auto access_expiry = std::chrono::sys_seconds{std::chrono::seconds{500}};
|
|
profile.set_pro_access_expiry(access_expiry);
|
|
CHECK(profile.get_pro_access_expiry() == access_expiry);
|
|
}
|