From 09282b2e56dd78a8a35b466e6e22d21366a45a00 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 15 Dec 2023 15:10:14 +1100 Subject: [PATCH 01/24] Created the State class and started wrapping functions --- include/session/config/groups/keys.hpp | 14 + include/session/config/namespaces.h | 16 + include/session/state.h | 307 ++++++++++++++++++++ include/session/state.hpp | 215 ++++++++++++++ include/session/util.hpp | 4 + src/CMakeLists.txt | 13 + src/curve25519.cpp | 4 + src/ed25519.cpp | 4 + src/state.cpp | 386 +++++++++++++++++++++++++ tests/CMakeLists.txt | 2 + tests/test_state.cpp | 84 ++++++ 11 files changed, 1049 insertions(+) create mode 100644 include/session/config/namespaces.h create mode 100644 include/session/state.h create mode 100644 include/session/state.hpp create mode 100644 src/state.cpp create mode 100644 tests/test_state.cpp diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index f7dd59ff..3a383608 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -186,6 +186,20 @@ class Keys final : public ConfigSig { Info& info, Members& members); + /// Same as the above but takes pointers instead of references. For internal use only. + Keys(ustring_view user_ed25519_secretkey, + ustring_view group_ed25519_pubkey, + std::optional group_ed25519_secretkey, + std::optional dumped, + Info* info, + Members* members) : + Keys(user_ed25519_secretkey, + group_ed25519_pubkey, + group_ed25519_secretkey, + dumped, + *info, + *members) {} + /// API: groups/Keys::storage_namespace /// /// Returns the Keys namespace. Is constant, will always return Namespace::GroupKeys diff --git a/include/session/config/namespaces.h b/include/session/config/namespaces.h new file mode 100644 index 00000000..e0f98be6 --- /dev/null +++ b/include/session/config/namespaces.h @@ -0,0 +1,16 @@ +#pragma once + +typedef enum NAMESPACE { + NAMESPACE_USER_PROFILE = 2, + NAMESPACE_CONTACTS = 3, + NAMESPACE_CONVO_INFO_VOLATILE = 4, + NAMESPACE_USER_GROUPS = 5, + + // Messages sent to a closed group: + NAMESPACE_GROUP_MESSAGES = 5, + // Groups config namespaces (i.e. for shared config of the group itself, not one user's group + // settings) + NAMESPACE_GROUP_KEYS = 12, + NAMESPACE_GROUP_INFO = 13, + NAMESPACE_GROUP_MEMBERS = 14, +} NAMESPACE; diff --git a/include/session/state.h b/include/session/state.h new file mode 100644 index 00000000..2c6acc08 --- /dev/null +++ b/include/session/state.h @@ -0,0 +1,307 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "config/namespaces.h" +#include "config/profile_pic.h" +#include "export.h" + +// State object: this type holds the internal object which manages the entire state. +typedef struct state_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; + + // When an error occurs in the C API this string will be set to the specific error message. May + // be empty. + const char* last_error; + + // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. + char _error_buf[256]; +} state_object; + +/// API: state/state_create +/// +/// Constructs a new state which generates it's own random ed25519 key pair. +/// +/// When done with the object the `state_object` must be destroyed by passing the pointer to +/// state_free(). +/// +/// Declaration: +/// ```cpp +/// INT state_init( +/// [out] state_object** state, +/// [out] char* error +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [out] Pointer to the state object +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +/// +/// Outputs: +/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message +/// as a C-string into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool state_create(state_object** state, char* error) + __attribute__((warn_unused_result)); + +/// API: state/state_create +/// +/// Constructs a new state which generates it's own random ed25519 key pair. +/// +/// When done with the object the `state_object` must be destroyed by passing the pointer to +/// state_free(). +/// +/// Declaration: +/// ```cpp +/// INT state_init( +/// [out] state_object** state, +/// [in] const unsigned char* ed25519_secretkey, +/// [out] char* error +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [out] Pointer to the state object +/// - `ed25519_secretkey` -- [in] must be the 32-byte secret key seed value. (You can also pass the +/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32 +/// bytes of that are the seed). This field cannot be null. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +/// +/// Outputs: +/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message +/// as a C-string into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool state_init( + state_object** state, const unsigned char* ed25519_secretkey, char* error) + __attribute__((warn_unused_result)); + +/// API: state/state_free +/// +/// Frees a state object. +/// +/// Declaration: +/// ```cpp +/// VOID state_free( +/// [in, out] state_object* state +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_object object +LIBSESSION_EXPORT void state_free(state_object* state); + +LIBSESSION_EXPORT bool state_load( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex, + const unsigned char* dump, + size_t dumplen); + +/// API: state/state_dump +/// +/// Returns a bt-encoded dict containing the dumps of each of the current config states for +/// storage in the database; the values in the dict would individually get passed into `load` to +/// reconstitute the object (including the push/not pushed status). Resets the `needs_dump()` +/// flag to false. Allocates a new buffer and sets +/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated +/// C string. +/// +/// NB: It is the caller's responsibility to `free()` the buffer when done with it. +/// +/// Immediately after this is called `state_needs_dump` will start returning falst (until the +/// configuration is next modified). +/// +/// Declaration: +/// ```cpp +/// VOID state_dump( +/// [in] state_object* state +/// [in] bool full_dump +/// [out] unsigned char** out +/// [out] size_t* outlen +/// ); +/// +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `full_dump` -- [in] Flag when true the returned bt-encoded dict will include dumps for the +/// entire state, even if they would normally return `false` for `needs_dump()`. +/// - `out` -- [out] Pointer to the output location +/// - `outlen` -- [out] Length of output +LIBSESSION_EXPORT void state_dump( + state_object* state, bool full_dump, unsigned char** out, size_t* outlen); + +/// API: state/state_dump_namespace +/// +/// Returns a binary dump of the current state of the config object for the specified namespace and +/// pubkey. This dump can be used to resurrect the object at a later point (e.g. after a restart). +/// Allocates a new buffer and sets it in `out` and the length in `outlen`. Note that this is +/// binary data, *not* a null-terminated C string. +/// +/// NB: It is the caller's responsibility to `free()` the buffer when done with it. +/// +/// Immediately after this is called `state_needs_dump` will start returning false (until the +/// configuration is next modified). +/// +/// Declaration: +/// ```cpp +/// VOID state_dump( +/// [in] state_object* state +/// [in] NAMESPACE namespace +/// [in] const char* pubkey_hex +/// [out] unsigned char** out +/// [out] size_t* outlen +/// ); +/// +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `namespace` -- [in] the namespace where config messages of the desired dump are stored. +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex). Required for group +/// dumps. +/// - `out` -- [out] Pointer to the output location +/// - `outlen` -- [out] Length of output +LIBSESSION_EXPORT void state_dump_namespace( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex, + unsigned char** out, + size_t* outlen); + +/// User Profile functions + +/// API: state/state_get_profile_name +/// +/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at +/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. +/// +/// Declaration: +/// ```cpp +/// CONST CHAR* state_get_profile_name( +/// [in] const state_object* state +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is +/// no name +LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); + +/// API: state/state_set_profile_name +/// +/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on +/// error (and sets the state_object's error string). +/// +/// Declaration: +/// ```cpp +/// BOOL state_set_profile_name( +/// [in] state_object* state, +/// [in] const char* name +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `name` -- [in] Pointer to the name as a null-terminated C string +/// +/// Outputs: +/// - `bool` -- Returns true on success, false on error +LIBSESSION_EXPORT bool state_set_profile_name(state_object* state, const char* name); + +/// API: state/state_get_profile_pic +/// +/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile +/// pic is not currently set, and otherwise should be copied right away (they will not be valid +/// beyond other API calls on this config object). +/// +/// Declaration: +/// ```cpp +/// USER_PROFILE_PIC state_get_profile_pic( +/// [in] const state_object* state +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `user_profile_pic` -- Pointer to the currently-set profile pic +LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* state); + +/// API: state/state_set_profile_pic +/// +/// Sets a user profile +/// +/// Declaration: +/// ```cpp +/// BOOL state_set_profile_pic( +/// [in] state_object* state, +/// [in] user_profile_pic pic +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the satet object +/// - `pic` -- [in] Pointer to the pic +/// +/// Outputs: +/// - `bool` -- Returns true on success, false on error +LIBSESSION_EXPORT bool state_set_profile_pic(state_object* state, user_profile_pic pic); + +/// API: state/state_get_profile_blinded_msgreqs +/// +/// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they +/// should be ignored. +/// +/// Declaration: +/// ```cpp +/// INT state_get_profile_blinded_msgreqs( +/// [in] const state_object* state +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Will be -1 if the state does not have the value explicitly set, 0 if the setting is +/// explicitly disabled, and 1 if the setting is explicitly enabled. +LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* state); + +/// API: state/state_set_profile_blinded_msgreqs +/// +/// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any +/// positive value) to enable; 0 to disable; and -1 to clear the setting. +/// +/// Declaration: +/// ```cpp +/// VOID state_set_profile_blinded_msgreqs( +/// [in] state_object* state, +/// [in] int enabled +/// ); +/// ``` +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `enabled` -- [in] true if they should be enabled, false if disabled +/// +/// Outputs: +/// - `void` -- Returns Nothing +LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs(state_object* state, int enabled); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/state.hpp b/include/session/state.hpp new file mode 100644 index 00000000..de8cd08a --- /dev/null +++ b/include/session/state.hpp @@ -0,0 +1,215 @@ +#pragma once + +#include + +#include "config/contacts.hpp" +#include "config/convo_info_volatile.hpp" +#include "config/groups/info.hpp" +#include "config/groups/keys.hpp" +#include "config/groups/members.hpp" +#include "config/namespaces.hpp" +#include "config/user_groups.hpp" +#include "config/user_profile.hpp" +#include "ed25519.hpp" + +namespace session::state { + +// Levels for the logging callback +enum class LogLevel { debug = 0, info, warning, error }; + +using Ed25519PubKey = std::array; +using Ed25519Secret = sodium_array; + +/// Struct containing group configs. +class GroupConfigs { + public: + GroupConfigs(ustring_view pubkey, ustring_view user_sk); + + GroupConfigs(GroupConfigs&&) = delete; + GroupConfigs(const GroupConfigs&) = delete; + GroupConfigs& operator=(GroupConfigs&&) = delete; + GroupConfigs& operator=(const GroupConfigs&) = delete; + + std::unique_ptr _config_info; + std::unique_ptr _config_members; + std::unique_ptr _config_keys; +}; + +class State { + private: + std::unique_ptr _config_contacts; + std::unique_ptr _config_convo_info_volatile; + std::unique_ptr _config_user_groups; + std::unique_ptr _config_user_profile; + std::map> _config_groups; + + protected: + Ed25519PubKey _user_pk; + Ed25519Secret _user_sk; + + // Invokes the `logger` callback if set, does nothing if there is no logger. + void log(LogLevel lvl, std::string msg) { + if (logger) + logger(lvl, std::move(msg)); + } + + public: + // Constructs a state with a secretkey that will be used for signing. + State(ustring_view ed25519_secretkey); + + // Constructs a new state, this will generate a random secretkey and should only be used for + // creating a new account. + State() : State(to_unsigned_sv(session::ed25519::ed25519_key_pair().second)){}; + + // Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to + // be managed. + State(State&&) = delete; + State(const State&) = delete; + State& operator=(State&&) = delete; + State& operator=(const State&) = delete; + + // If set then we log things by calling this callback + std::function logger; + + /// API: state/State::load + /// + /// Loads a dump into the state. Calling this will replace the current config instance with + /// with a new instance initialised with the provided dump. The USER_GROUPS config must be + /// loaded before any GROUPS config dumps are loaded or an exception will be thrown. + /// + /// Inputs: + /// - `namespace` -- the namespace where config messages for this dump are stored. + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex). Required for group + /// dumps. + /// - `dump` -- binary state data that was previously dumped by calling `dump()`. + /// + /// Outputs: None + void load( + config::Namespace namespace_, + std::optional pubkey_hex, + ustring_view dump); + + /// API: state/State::dump + /// + /// Returns a bt-encoded dict containing the dumps of each of the current config states for + /// storage in the database; the values in the dict would individually get passed into `load` to + /// reconstitute the object (including the push/not pushed status). Resets the `needs_dump()` + /// flag to false. + /// + /// Inputs: + /// - `full_dump` -- when true the returned bt-encoded dict will include dumps for the entire + /// state, even if they would normally return `false` for `needs_dump()`. + /// + /// Outputs: + /// - `ustring` -- Returns bt-encoded dict of the state dump + ustring dump(bool full_dump = false); + + /// API: state/State::dump + /// + /// Returns a dump of the current config state for the specified namespace and pubkey for + /// storage in the database; this value would get passed into `load` to reconstitute the object + /// (including the push/not pushed status). Resets the `needs_dump()` flag to false for the + /// specific config. + /// + /// Inputs: + /// - `namespace` -- the namespace where config messages of the desired dump are stored. + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex). Required for group + /// dumps. + /// + /// Outputs: + /// - `ustring` -- Returns binary data of the state dump + ustring dump( + config::Namespace namespace_, + std::optional pubkey_hex = std::nullopt); + + // User Profile functions + public: + /// API: state/State::get_profile_name + /// + /// Returns the user profile name, or std::nullopt if there is no profile name set. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` - Returns the user profile name if it exists + std::optional get_profile_name() const { + return _config_user_profile->get_name(); + }; + + /// API: state/State::set_profile_name + /// + /// Sets the user profile name; if given an empty string then the name is removed. + /// + /// Inputs: + /// - `new_name` -- The name to be put into the user profile + void set_profile_name(std::string_view new_name) { _config_user_profile->set_name(new_name); }; + + /// API: user_profile/UserProfile::get_profile_pic + /// + /// Gets the user's current profile pic URL and decryption key. The returned object will + /// evaluate as false if the URL and/or key are not set. + /// + /// Inputs: None + /// + /// Outputs: + /// - `profile_pic` - Returns the profile pic + config::profile_pic get_profile_pic() const { return _config_user_profile->get_profile_pic(); }; + + /// API: state/State::set_profile_pic + /// + /// Sets the user's current profile pic to a new URL and decryption key. Clears both if either + /// one is empty. + /// + /// Declaration: + /// ```cpp + /// void set_profile_pic(std::string_view url, ustring_view key); + /// void set_profile_pic(profile_pic pic); + /// ``` + /// + /// Inputs: + /// - First function: + /// - `url` -- URL pointing to the profile pic + /// - `key` -- Decryption key + /// - Second function: + /// - `pic` -- Profile pic object + void set_profile_pic(std::string_view url, ustring_view key) { + _config_user_profile->set_profile_pic(url, key); + }; + void set_profile_pic(config::profile_pic pic) { _config_user_profile->set_profile_pic(pic); }; + + /// API: state/State::get_profile_blinded_msgreqs + /// + /// Accesses whether or not blinded message requests are enabled for the client. Can have three + /// values: + /// + /// - std::nullopt -- the value has not been given an explicit value so the client should use + /// its default. + /// - true -- the value is explicitly enabled (i.e. user wants blinded message requests) + /// - false -- the value is explicitly disabled (i.e. user disabled blinded message requests) + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` - true/false if blinded message requests are enabled or disabled; + /// `std::nullopt` if the option has not been set either way. + std::optional get_profile_blinded_msgreqs() const { + return _config_user_profile->get_blinded_msgreqs(); + }; + + /// API: state/State::set_profile_blinded_msgreqs + /// + /// Sets whether blinded message requests (i.e. from SOGS servers you are connected to) should + /// be enabled or not. This is typically invoked with either `true` or `false`, but can also be + /// called with `std::nullopt` to explicitly clear the value. + /// + /// Inputs: + /// - `enabled` -- true if blinded message requests should be retrieved, false if they should + /// not, and `std::nullopt` to drop the setting from the config (and thus use the client's + /// default). + void set_profile_blinded_msgreqs(std::optional enabled) { + _config_user_profile->set_blinded_msgreqs(enabled); + }; +}; + +} // namespace session::state + diff --git a/include/session/util.hpp b/include/session/util.hpp index 5cea9fa4..6d2df4b7 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -46,6 +46,10 @@ inline ustring_view to_unsigned_sv(std::string_view v) { inline ustring_view to_unsigned_sv(std::basic_string_view v) { return {to_unsigned(v.data()), v.size()}; } +template +inline ustring_view to_unsigned_sv(const std::array& v) { + return {v.data(), v.size()}; +} inline ustring_view to_unsigned_sv(ustring_view v) { return v; // no-op, but helps with template metaprogamming } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c3f1158..762778a7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -76,6 +76,9 @@ add_libsession_util_library(config fields.cpp ) +add_libsession_util_library(state + state.cpp +) target_link_libraries(crypto @@ -95,6 +98,16 @@ target_link_libraries(config libzstd::static ) +target_link_libraries(state + PUBLIC + crypto + common + libsession::protos + PRIVATE + libsodium::sodium-internal + libzstd::static +) + if(ENABLE_ONIONREQ) add_libsession_util_library(onionreq onionreq/builder.cpp diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 81870cc3..7c0035db 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,5 +1,6 @@ #include "session/curve25519.hpp" +#include #include #include @@ -11,6 +12,9 @@ namespace session::curve25519 { std::pair, std::array> curve25519_key_pair() { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + std::array curve_pk; std::array curve_sk; crypto_box_keypair(curve_pk.data(), curve_sk.data()); diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 87297b09..d3a6f0f9 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -1,5 +1,6 @@ #include "session/ed25519.hpp" +#include #include #include @@ -17,6 +18,9 @@ using uc32 = std::array; using cleared_uc64 = cleared_array<64>; std::pair, std::array> ed25519_key_pair() { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + std::array ed_pk; std::array ed_sk; crypto_sign_ed25519_keypair(ed_pk.data(), ed_sk.data()); diff --git a/src/state.cpp b/src/state.cpp new file mode 100644 index 00000000..7b37b375 --- /dev/null +++ b/src/state.cpp @@ -0,0 +1,386 @@ +#include "session/state.hpp" + +#include +#include +#include + +#include +#include + +#include "config/internal.hpp" +#include "session/config/base.hpp" +#include "session/config/contacts.hpp" +#include "session/config/convo_info_volatile.hpp" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" +#include "session/config/user_groups.hpp" +#include "session/config/user_profile.hpp" +#include "session/export.h" +#include "session/state.h" +#include "session/util.hpp" + +using namespace std::literals; +using namespace session::config; + +namespace session::state { + +GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { + auto info = std::make_unique(pubkey, std::nullopt, std::nullopt); + auto members = std::make_unique(pubkey, std::nullopt, std::nullopt); + auto keys = std::make_unique( + user_sk, pubkey, std::nullopt, std::nullopt, *info, *members); + _config_info = std::move(info); + _config_members = std::move(members); + _config_keys = std::move(keys); +} + +State::State(ustring_view ed25519_secretkey) { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + if (ed25519_secretkey.size() != 64) + throw std::invalid_argument{"Invalid ed25519_secretkey: expected 64 bytes"}; + + _user_sk.reset(64); + std::memcpy(_user_sk.data(), ed25519_secretkey.data(), ed25519_secretkey.size()); + crypto_sign_ed25519_sk_to_pk(_user_pk.data(), _user_sk.data()); + + // Initialise empty config states for the standard config types + _config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); + _config_convo_info_volatile = + std::make_unique(ed25519_secretkey, std::nullopt); + _config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); + _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); +} + +void State::load( + Namespace namespace_, std::optional pubkey_hex_, ustring_view dump) { + switch (namespace_) { + case Namespace::Contacts: + _config_contacts = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + return; + + case Namespace::ConvoInfoVolatile: + _config_convo_info_volatile = std::make_unique( + to_unsigned_sv({_user_sk.data(), 64}), dump); + return; + + case Namespace::UserGroups: + _config_user_groups = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + return; + + case Namespace::UserProfile: + _config_user_profile = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + return; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex_->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + + // Retrieve any keys for the group + auto user_group_info = _config_user_groups->get_group(*pubkey_hex_); + + if (!user_group_info) + throw std::runtime_error{"Unable to retrieve group from user_groups config"}; + + std::string_view pubkey_hex = *pubkey_hex_; + ustring_view pubkey = to_unsigned_sv(session_id_to_bytes(*pubkey_hex_, "03")); + ustring_view user_ed25519_secretkey = {_user_sk.data(), 64}; + std::optional opt_dump = dump; + std::optional group_ed25519_secretkey; + + if (!user_group_info.value().secretkey.empty()) + group_ed25519_secretkey = {user_group_info.value().secretkey.data(), 64}; + + // Create a fresh `GroupConfigs` state + if (!_config_groups.count(pubkey_hex)) { + if (namespace_ == Namespace::GroupKeys) + throw std::runtime_error{ + "Attempted to load groups_keys config before groups_info or groups_members " + "configs"}; + + _config_groups[pubkey_hex] = std::make_unique(pubkey, user_ed25519_secretkey); + } + + // Reload the specified namespace with the dump + if (namespace_ == Namespace::GroupInfo) + _config_groups[pubkey_hex]->_config_info = + std::make_unique(pubkey, group_ed25519_secretkey, dump); + else if (namespace_ == Namespace::GroupMembers) + _config_groups[pubkey_hex]->_config_members = + std::make_unique(pubkey, group_ed25519_secretkey, dump); + else if (namespace_ == Namespace::GroupKeys) { + auto info = _config_groups[pubkey_hex]->_config_info.get(); + auto members = _config_groups[pubkey_hex]->_config_members.get(); + auto keys = std::make_unique( + user_ed25519_secretkey, pubkey, pubkey, group_ed25519_secretkey, info, members); + + _config_groups[pubkey_hex]->_config_keys = std::move(keys); + } else + throw std::runtime_error{"Attempted to load unknown namespace"}; +} + +ustring State::dump(bool full_dump) { + oxenc::bt_dict_producer combined; + + // NOTE: the keys have to be in ascii-sorted order: + if (full_dump || _config_contacts->needs_dump()) + combined.append("contacts", session::from_unsigned_sv(_config_contacts->dump())); + + if (full_dump || _config_convo_info_volatile->needs_dump()) + combined.append( + "convo_info_volatile", + session::from_unsigned_sv(_config_convo_info_volatile->dump())); + + if (full_dump || _config_user_groups->needs_dump()) + combined.append("user_groups", session::from_unsigned_sv(_config_user_groups->dump())); + + if (full_dump || _config_user_profile->needs_dump()) + combined.append("user_profile", session::from_unsigned_sv(_config_user_profile->dump())); + + // NOTE: `std::map` sorts keys in ascending order so can just add them in order + if (_config_groups.size() > 0) { + for (const auto& [key, config] : _config_groups) { + if (full_dump || config->_config_info->needs_dump() || + config->_config_keys->needs_dump() || config->_config_members->needs_dump()) { + oxenc::bt_dict_producer group_combined = combined.append_dict(key); + + if (full_dump || config->_config_info->needs_dump()) + group_combined.append( + "info", session::from_unsigned_sv(config->_config_info->dump())); + + if (full_dump || config->_config_keys->needs_dump()) + group_combined.append( + "keys", session::from_unsigned_sv(config->_config_keys->dump())); + + if (full_dump || config->_config_members->needs_dump()) + group_combined.append( + "members", session::from_unsigned_sv(config->_config_members->dump())); + } + } + } + + auto to_dump = std::move(combined).str(); + + return session::ustring{to_unsigned_sv(to_dump)}; +} + +ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex_) { + switch (namespace_) { + case Namespace::Contacts: return _config_contacts->dump(); + case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->dump(); + case Namespace::UserGroups: return _config_user_groups->dump(); + case Namespace::UserProfile: return _config_user_profile->dump(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex_->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + if (!_config_groups.count(*pubkey_hex_)) + throw std::runtime_error{"Unable to retrieve group"}; + + // Retrieve the group configs for this pubkey + auto group_configs = _config_groups[*pubkey_hex_].get(); + + switch (namespace_) { + case Namespace::GroupInfo: return group_configs->_config_info->dump(); + case Namespace::GroupMembers: return group_configs->_config_members->dump(); + case Namespace::GroupKeys: return group_configs->_config_keys->dump(); + default: throw std::runtime_error{"Attempted to load unknown namespace"}; + } +} + +} // namespace session::state + +using namespace session; +using namespace session::state; + +namespace { +State& unbox(state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +const State& unbox(const state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} + +bool set_error(state_object* state, std::string_view e) { + if (e.size() > 255) + e.remove_suffix(e.size() - 255); + std::memcpy(state->_error_buf, e.data(), e.size()); + state->_error_buf[e.size()] = 0; + state->last_error = state->_error_buf; + return false; +} +} // namespace + +extern "C" { + +LIBSESSION_EXPORT void state_free(state_object* state) { + delete state; +} + +LIBSESSION_C_API bool state_create(state_object** state, char* error) { + try { + auto s = std::make_unique(); + auto s_object = std::make_unique(); + + s_object->internals = s.release(); + s_object->last_error = nullptr; + *state = s_object.release(); + return true; + } catch (const std::exception& e) { + if (error) { + std::string msg = e.what(); + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + } + return false; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool state_init( + state_object** state, const unsigned char* ed25519_secretkey_bytes, char* error) { + try { + auto s = std::make_unique( + session::ustring_view{ed25519_secretkey_bytes, 64}); + auto s_object = std::make_unique(); + + s_object->internals = s.release(); + s_object->last_error = nullptr; + *state = s_object.release(); + return true; + } catch (const std::exception& e) { + if (error) { + std::string msg = e.what(); + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + } + return false; + } +} + +LIBSESSION_C_API bool state_load( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + const unsigned char* dump, + size_t dumplen) { + assert(state && dump && dumplen); + + session::ustring_view dumped{dump, dumplen}; + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 64); + + try { + auto target_namespace = static_cast(namespace_); + + unbox(state).load(target_namespace, pubkey_hex, dumped); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_EXPORT void state_dump( + state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { + assert(out && outlen); + auto data = unbox(state).dump(full_dump); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); +} + +LIBSESSION_EXPORT void state_dump_namespace( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen) { + assert(out && outlen); + + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 64); + + auto target_namespace = static_cast(namespace_); + auto data = unbox(state).dump(target_namespace, pubkey_hex); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); +} + +// User Profile Functions + +LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { + if (auto s = unbox(state).get_profile_name()) + return s->data(); + return nullptr; +} + +LIBSESSION_C_API bool state_set_profile_name(state_object* state, const char* name) { + try { + unbox(state).set_profile_name(name); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { + user_profile_pic p; + if (auto pic = unbox(state).get_profile_pic(); pic) { + copy_c_str(p.url, pic.url); + std::memcpy(p.key, pic.key.data(), 32); + } else { + p.url[0] = 0; + } + return p; +} + +LIBSESSION_C_API bool state_set_profile_pic(state_object* state, user_profile_pic pic) { + std::string_view url{pic.url}; + ustring_view key; + if (!url.empty()) + key = {pic.key, 32}; + + try { + unbox(state).set_profile_pic(url, key); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { + if (auto opt = unbox(state).get_profile_blinded_msgreqs()) + return static_cast(*opt); + return -1; +} + +LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int enabled) { + std::optional val; + if (enabled >= 0) + val = static_cast(enabled); + unbox(state).set_profile_blinded_msgreqs(std::move(val)); +} + +} // extern "C" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a6a1d127..e60bdddf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,12 +22,14 @@ add_executable(testAll test_proto.cpp test_random.cpp test_session_encrypt.cpp + test_state.cpp test_xed25519.cpp ) target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq + libsession::state libsodium::sodium-internal Catch2::Catch2WithMain) diff --git a/tests/test_state.cpp b/tests/test_state.cpp new file mode 100644 index 00000000..3dbdbab8 --- /dev/null +++ b/tests/test_state.cpp @@ -0,0 +1,84 @@ +#include + +#include "session/config/namespaces.hpp" +#include "session/config/user_profile.h" +#include "session/config/user_profile.hpp" +#include "session/state.h" +#include "session/state.hpp" +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; +using namespace session; +using namespace session::state; +using namespace session::config; + +TEST_CASE("State", "[state][state]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + auto state = State(ed_sk); + + // User Profile forwarding + CHECK_FALSE(state.get_profile_name().has_value()); + state.set_profile_name("Test Name"); + CHECK(state.get_profile_name() == "Test Name"); + + CHECK(state.get_profile_pic().empty()); + state.set_profile_pic("https://oxen.io", to_unsigned_sv("secret78901234567890123456789012")); + CHECK(state.get_profile_pic().url == "https://oxen.io"); + CHECK(state.get_profile_pic().key == "secret78901234567890123456789012"_bytes); + + auto second_pic = + profile_pic("https://oxen.io/2", to_unsigned_sv("secret78901234567890123456789012")); + state.set_profile_pic(second_pic); + CHECK(state.get_profile_pic().url == "https://oxen.io/2"); + + CHECK_FALSE(state.get_profile_blinded_msgreqs()); + state.set_profile_blinded_msgreqs(true); + CHECK(state.get_profile_blinded_msgreqs()); + + auto dump = state.dump(Namespace::UserProfile); + auto state2 = State(ed_sk); + CHECK_FALSE(state2.get_profile_name().has_value()); + state2.load(Namespace::UserProfile, std::nullopt, {dump.data(), dump.size()}); + CHECK(state2.get_profile_name() == "Test Name"); +} + +TEST_CASE("State c API", "[state][state][c]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), err)); + + // User Profile forwarding + CHECK(state_get_profile_name(state) == nullptr); + CHECK(state_set_profile_name(state, "Test Name")); + CHECK(state_get_profile_name(state) == "Test Name"sv); + + auto p = user_profile_pic(); + strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < sizeof(p.url)! + memcpy(p.key, "secret78901234567890123456789012", 32); + CHECK(strlen(state_get_profile_pic(state).url) == 0); + CHECK(state_set_profile_pic(state, p)); + auto stored_pic = state_get_profile_pic(state); + CHECK(stored_pic.url == "http://example.org/omg-pic-123.bmp"sv); + CHECK(ustring_view{stored_pic.key, 32} == "secret78901234567890123456789012"_bytes); + + CHECK(state_get_profile_blinded_msgreqs(state) == -1); + state_set_profile_blinded_msgreqs(state, 1); + CHECK(state_get_profile_blinded_msgreqs(state) == 1); + + unsigned char* dump1; + size_t dump1len; + state_dump_namespace(state, NAMESPACE_USER_PROFILE, nullptr, &dump1, &dump1len); + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), err)); + CHECK(state_get_profile_name(state2) == nullptr); + CHECK(state_load(state2, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); + CHECK(state_get_profile_name(state2) == "Test Name"sv); +} From f33d342ca493bb57532600f15307d40ebe0a4683 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 18 Dec 2023 17:41:02 +1100 Subject: [PATCH 02/24] [WIP] Working on the State 'send' hook --- include/session/config/base.hpp | 20 + include/session/config/contacts.hpp | 5 +- .../session/config/convo_info_volatile.hpp | 5 +- include/session/config/groups/info.hpp | 3 +- include/session/config/groups/keys.hpp | 22 +- include/session/config/groups/members.hpp | 3 +- include/session/config/namespaces.hpp | 28 ++ include/session/config/user_groups.hpp | 5 +- include/session/config/user_profile.hpp | 5 +- include/session/state.h | 46 ++ include/session/state.hpp | 117 ++++- src/CMakeLists.txt | 1 + src/config/base.cpp | 8 + src/config/contacts.cpp | 7 +- src/config/convo_info_volatile.cpp | 6 +- src/config/groups/info.cpp | 5 +- src/config/groups/keys.cpp | 6 +- src/config/groups/members.cpp | 5 +- src/config/user_groups.cpp | 7 +- src/config/user_profile.cpp | 7 +- src/state.cpp | 461 ++++++++++++++++-- tests/test_state.cpp | 26 +- 22 files changed, 689 insertions(+), 109 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 138ba51f..89332007 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -13,6 +14,10 @@ #include "base.h" #include "namespaces.hpp" +namespace session::state { +class State; +} + namespace session::config { template @@ -137,6 +142,10 @@ class ConfigSig { /// sub-types. class ConfigBase : public ConfigSig { private: + // The parent state which owns this config object. By providing a pointer to the parent state + // we can inform the parent when changes occur. + std::optional _parent_state; + // The object (either base config message or MutableConfigMessage) that stores the current // config message. Subclasses do not directly access this: instead they call `dirty()` if they // intend to make changes, or the `set_config_field` wrapper. @@ -174,6 +183,7 @@ class ConfigBase : public ConfigSig { // verification of incoming messages using the associated pubkey, and will be signed using the // secretkey (if a secret key is given). explicit ConfigBase( + std::optional parent_state = std::nullopt, std::optional dump = std::nullopt, std::optional ed25519_pubkey = std::nullopt, std::optional ed25519_secretkey = std::nullopt); @@ -868,6 +878,16 @@ class ConfigBase : public ConfigSig { /// - `std::optional` -- Returns the compression level virtual std::optional compression_level() const { return 1; } + /// API: base/ConfigBase::default_ttl + /// + /// The default duration the config message should last for before it expires. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::chrono::milliseconds` -- Duration the mesage should last for in milliseconds. + virtual std::chrono::milliseconds default_ttl() const { return std::chrono::hours(30 * 24); } + /// API: base/ConfigBase::config_lags /// /// How many config lags should be used for this object; default to 5. Implementing subclasses diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 7ac4b237..19679c2d 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -118,7 +118,10 @@ class Contacts : public ConfigBase { /// /// Outputs: /// - `Contact` - Constructor - Contacts(ustring_view ed25519_secretkey, std::optional dumped); + Contacts( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: contacts/Contacts::storage_namespace /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 24c3605c..b57feaeb 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -176,7 +176,10 @@ class ConvoInfoVolatile : public ConfigBase { /// the secret key. /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. - ConvoInfoVolatile(ustring_view ed25519_secretkey, std::optional dumped); + ConvoInfoVolatile( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: convo_info_volatile/ConvoInfoVolatile::storage_namespace /// diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 4010bf35..fb5bcb1f 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -57,7 +57,8 @@ class Info final : public ConfigBase { /// that was previously dumped from an instance of this class by calling `dump()`. Info(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped); + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: groups/Info::storage_namespace /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 3a383608..50ffb683 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -73,6 +73,9 @@ using namespace std::literals; /// key="SessionGroupKeyGen"), where S = H(group_seed, key="SessionGroupKeySeed"). class Keys final : public ConfigSig { + // The parent state which owns this config object. By providing a pointer to the parent state + // we can inform the parent when changes occur. + std::optional _parent_state; Ed25519Secret user_ed25519_sk; @@ -184,7 +187,8 @@ class Keys final : public ConfigSig { std::optional group_ed25519_secretkey, std::optional dumped, Info& info, - Members& members); + Members& members, + std::optional parent_state = std::nullopt); /// Same as the above but takes pointers instead of references. For internal use only. Keys(ustring_view user_ed25519_secretkey, @@ -192,13 +196,15 @@ class Keys final : public ConfigSig { std::optional group_ed25519_secretkey, std::optional dumped, Info* info, - Members* members) : + Members* members, + std::optional parent_state = std::nullopt) : Keys(user_ed25519_secretkey, group_ed25519_pubkey, group_ed25519_secretkey, dumped, *info, - *members) {} + *members, + parent_state) {} /// API: groups/Keys::storage_namespace /// @@ -220,6 +226,16 @@ class Keys final : public ConfigSig { /// - `const char*` - Will return "groups::Keys" const char* encryption_domain() const { return "groups::Keys"; } + /// API: groups/Keys::default_ttl + /// + /// The default duration the config message should last for before it expires. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::chrono::milliseconds` -- Duration the mesage should last for in milliseconds. + virtual std::chrono::milliseconds default_ttl() const { return std::chrono::hours(30 * 24); } + /// API: groups/Keys::group_keys /// /// Returns all the unexpired decryption keys that we know about. Keys are returned ordered diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..6ab5060b 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -292,7 +292,8 @@ class Members final : public ConfigBase { /// that was previously dumped from an instance of this class by calling `dump()`. Members(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped); + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: groups/Members::storage_namespace /// diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index c5c29ec5..5b6f04d5 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -19,4 +19,32 @@ enum class Namespace : std::int16_t { GroupMembers = 14, }; +namespace { + /// Returns a number indicating the order that messages from the specified namespace should be + /// merged in (lower numbers shold be merged first), + /// by merging in a specific order we can prevent certain edge-cases where data/logic between + /// different configs could be dependant on each other (eg. there could be `ConvoInfoVolatile` + /// data related to a new conversation which hasn't been created yet because it's associated + /// `Contacts`/`UserGroups` message hasn't been processed; or a `GroupInfo` which was encrypted + /// with a key included in the `GroupKeys` within the same poll) + int namespace_merge_order(const Namespace& n) { + if (n == Namespace::UserProfile || n == Namespace::Contacts || n == Namespace::GroupKeys) + return 0; + if (n == Namespace::UserGroups || n == Namespace::GroupInfo || n == Namespace::GroupMembers) + return 1; + if (n == Namespace::ConvoInfoVolatile) + return 2; + return 3; + }; + + /// Returns a number indicating the order that the config messages should be sent in, we need to + /// send the `GroupKeys` config _before_ the `GroupInfo` and `GroupMembers` configs as they both + /// get encrypted with the latest key and we want to avoid weird edge-cases + int namespace_store_order(const Namespace& n) { + if (n == Namespace::GroupKeys) + return 0; + return 1; + } +} // namespace + } // namespace session::config diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index d35855dd..6fc7846f 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -262,7 +262,10 @@ class UserGroups : public ConfigBase { /// /// Outputs: /// - `UserGroups` - Constructor - UserGroups(ustring_view ed25519_secretkey, std::optional dumped); + UserGroups( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: user_groups/UserGroups::storage_namespace /// diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index d99f19e9..674533e7 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -47,7 +47,10 @@ class UserProfile final : public ConfigBase { /// /// Outputs: /// - `UserProfile` - Constructor - UserProfile(ustring_view ed25519_secretkey, std::optional dumped); + UserProfile( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state = std::nullopt); /// API: user_profile/UserProfile::storage_namespace /// diff --git a/include/session/state.h b/include/session/state.h index 2c6acc08..1e066559 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -25,6 +25,45 @@ typedef struct state_object { char _error_buf[256]; } state_object; +typedef struct state_config_message { + NAMESPACE namespace_; + const char* hash; + uint64_t timestamp_ms; + const unsigned char* data; + size_t datalen; +} state_config_message; + +/// API: state/state_set_logger +/// +/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL +/// if not needed). The given function pointer will be invoked with one of the above values, a +/// null-terminated c string containing the log message, and the void* context object given when +/// setting the logger (this is for caller-specific state data and won't be touched). +/// +/// The logging function must have signature: +/// +/// void log(config_log_level lvl, const char* msg, void* ctx); +/// +/// Can be called with callback set to NULL to clear an existing logger. +/// +/// The config object itself has no log level: the caller should filter by level as needed. +/// +/// Declaration: +/// ```cpp +/// VOID config_set_logger( +/// [in, out] state_object* state, +/// [in] void(*)(config_log_level, const char*, void*) callback, +/// [in] void* ctx +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_set_logger( + state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx); + /// API: state/state_create /// /// Constructs a new state which generates it's own random ed25519 key pair. @@ -106,6 +145,13 @@ LIBSESSION_EXPORT bool state_load( const unsigned char* dump, size_t dumplen); +LIBSESSION_EXPORT void state_set_send_callback( + state_object* state, void (*callback)(const char*, const unsigned char*, size_t)); +// std::function send; + +LIBSESSION_EXPORT config_string_list* state_merge( + state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count); + /// API: state/state_dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for diff --git a/include/session/state.hpp b/include/session/state.hpp index de8cd08a..7b77f5d8 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -30,17 +30,44 @@ class GroupConfigs { GroupConfigs& operator=(GroupConfigs&&) = delete; GroupConfigs& operator=(const GroupConfigs&) = delete; - std::unique_ptr _config_info; - std::unique_ptr _config_members; - std::unique_ptr _config_keys; + std::unique_ptr config_info; + std::unique_ptr config_members; + std::unique_ptr config_keys; +}; + +struct config_message { + config::Namespace namespace_; + std::string hash; + uint64_t timestamp_ms; + ustring data; + + config_message( + config::Namespace namespace_, std::string hash, uint64_t timestamp_ms, ustring data) : + namespace_{namespace_}, hash{hash}, timestamp_ms{timestamp_ms}, data{data} {}; + config_message( + config::Namespace namespace_, + std::string hash, + uint64_t timestamp_ms, + ustring_view data) : + namespace_{namespace_}, hash{hash}, timestamp_ms{timestamp_ms}, data{data} {}; + + config_message() = delete; + config_message(config_message&&) = default; + config_message(const config_message&) = default; + config_message& operator=(config_message&&) = default; + config_message& operator=(const config_message&) = default; + + auto cmpval() const { return std::tie(namespace_, hash, timestamp_ms, data); } + bool operator<(const config_message& b) const { return cmpval() < b.cmpval(); } + bool operator>(const config_message& b) const { return cmpval() > b.cmpval(); } + bool operator<=(const config_message& b) const { return cmpval() <= b.cmpval(); } + bool operator>=(const config_message& b) const { return cmpval() >= b.cmpval(); } + bool operator==(const config_message& b) const { return cmpval() == b.cmpval(); } + bool operator!=(const config_message& b) const { return cmpval() != b.cmpval(); } }; class State { private: - std::unique_ptr _config_contacts; - std::unique_ptr _config_convo_info_volatile; - std::unique_ptr _config_user_groups; - std::unique_ptr _config_user_profile; std::map> _config_groups; protected: @@ -54,6 +81,15 @@ class State { } public: + std::unique_ptr config_contacts; + std::unique_ptr config_convo_info_volatile; + std::unique_ptr config_user_groups; + std::unique_ptr config_user_profile; + + std::chrono::milliseconds network_offset; + + GroupConfigs* group_config(std::string_view pubkey_hex); + // Constructs a state with a secretkey that will be used for signing. State(ustring_view ed25519_secretkey); @@ -89,6 +125,52 @@ class State { std::optional pubkey_hex, ustring_view dump); + /// API: base/ConfigBase::merge + /// + /// This takes all of the messages pulled down from the server and does whatever is necessary to + /// merge (or replace) the current values. + /// + /// Values are pairs of the message hash (as provided by the server) and the raw message body. + /// + /// For backwards compatibility, for certain message types (ones that have a + /// `accepts_protobuf()` override returning true) optional protobuf unwrapping of the incoming + /// message is performed; if successful then the unwrapped raw value is used; if the protobuf + /// unwrapping fails, the value is used directly as a raw value. + /// + /// After this call the caller should check `needs_push()` to see if the data on hand was + /// updated and needs to be pushed to the server again (for example, because the data contained + /// conflicts that required another update to resolve). + /// + /// Returns the number of the given config messages that were successfully parsed. + /// + /// Will throw on serious error (i.e. if neither the current nor any of the given configs are + /// parseable). This should not happen (the current config, at least, should always be + /// re-parseable). + /// + /// Declaration: + /// ```cpp + /// std::vector merge( + /// const std::vector>& configs); + /// std::vector merge( + /// const std::vector>& configs); + /// ``` + /// + /// Inputs: + /// - `configs` -- vector of pairs containing the message hash and the raw message body (or + /// protobuf-wrapped raw message for certain config types). + /// + /// Outputs: + /// - vector of successfully parsed hashes. Note that this does not mean the hash was recent or + /// that it changed the config, merely that the returned hash was properly parsed and + /// processed as a config message, even if it was too old to be useful (or was already known + /// to be included). The hashes will be in the same order as in the input vector. + std::vector merge( + std::optional pubkey_hex, const std::vector& configs); + + std::function send; + + void config_changed(std::optional pubkey_hex = std::nullopt); + /// API: state/State::dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for @@ -122,6 +204,13 @@ class State { config::Namespace namespace_, std::optional pubkey_hex = std::nullopt); + public: + void set_service_node_timestamp(std::chrono::milliseconds timestamp) { + network_offset = + (timestamp - std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch())); + }; + // User Profile functions public: /// API: state/State::get_profile_name @@ -133,7 +222,7 @@ class State { /// Outputs: /// - `std::optional` - Returns the user profile name if it exists std::optional get_profile_name() const { - return _config_user_profile->get_name(); + return config_user_profile->get_name(); }; /// API: state/State::set_profile_name @@ -142,7 +231,7 @@ class State { /// /// Inputs: /// - `new_name` -- The name to be put into the user profile - void set_profile_name(std::string_view new_name) { _config_user_profile->set_name(new_name); }; + void set_profile_name(std::string_view new_name) { config_user_profile->set_name(new_name); }; /// API: user_profile/UserProfile::get_profile_pic /// @@ -153,7 +242,7 @@ class State { /// /// Outputs: /// - `profile_pic` - Returns the profile pic - config::profile_pic get_profile_pic() const { return _config_user_profile->get_profile_pic(); }; + config::profile_pic get_profile_pic() const { return config_user_profile->get_profile_pic(); }; /// API: state/State::set_profile_pic /// @@ -173,9 +262,9 @@ class State { /// - Second function: /// - `pic` -- Profile pic object void set_profile_pic(std::string_view url, ustring_view key) { - _config_user_profile->set_profile_pic(url, key); + config_user_profile->set_profile_pic(url, key); }; - void set_profile_pic(config::profile_pic pic) { _config_user_profile->set_profile_pic(pic); }; + void set_profile_pic(config::profile_pic pic) { config_user_profile->set_profile_pic(pic); }; /// API: state/State::get_profile_blinded_msgreqs /// @@ -193,7 +282,7 @@ class State { /// - `std::optional` - true/false if blinded message requests are enabled or disabled; /// `std::nullopt` if the option has not been set either way. std::optional get_profile_blinded_msgreqs() const { - return _config_user_profile->get_blinded_msgreqs(); + return config_user_profile->get_blinded_msgreqs(); }; /// API: state/State::set_profile_blinded_msgreqs @@ -207,7 +296,7 @@ class State { /// not, and `std::nullopt` to drop the setting from the config (and thus use the client's /// default). void set_profile_blinded_msgreqs(std::optional enabled) { - _config_user_profile->set_blinded_msgreqs(enabled); + config_user_profile->set_blinded_msgreqs(enabled); }; }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 762778a7..5272af48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,6 +104,7 @@ target_link_libraries(state common libsession::protos PRIVATE + nlohmann_json::nlohmann_json libsodium::sodium-internal libzstd::static ) diff --git a/src/config/base.cpp b/src/config/base.cpp index ae44ffcc..54c3fc46 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -17,6 +17,7 @@ #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" #include "session/export.h" +#include "session/state.hpp" #include "session/util.hpp" using namespace std::literals; @@ -33,6 +34,9 @@ void ConfigBase::set_state(ConfigState s) { } _state = s; _needs_dump = true; + + if (_parent_state && _sign_pk) + (*_parent_state)->config_changed("03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end())); } MutableConfigMessage& ConfigBase::dirty() { @@ -347,6 +351,7 @@ ustring ConfigBase::make_dump() const { } ConfigBase::ConfigBase( + std::optional parent_state, std::optional dump, std::optional ed25519_pubkey, std::optional ed25519_secretkey) { @@ -354,6 +359,9 @@ ConfigBase::ConfigBase( if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; + if (parent_state) + _parent_state = *parent_state; + if (dump) init_from_dump(from_unsigned_sv(*dump)); else diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index fa61e1ed..21a119e2 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -51,8 +51,11 @@ void contact_info::set_nickname(std::string n) { nickname = std::move(n); } -Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped} { +Contacts::Contacts( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped} { load_key(ed25519_secretkey); } diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 742549c5..9b011107 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -92,8 +92,10 @@ namespace convo { } // namespace convo ConvoInfoVolatile::ConvoInfoVolatile( - ustring_view ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped} { + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped} { load_key(ed25519_secretkey); } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 88b3a1eb..9cabf6cf 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -19,8 +19,9 @@ namespace session::config::groups { Info::Info( ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped) : - ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey}, + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped, ed25519_pubkey, ed25519_secretkey}, id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} {} std::optional Info::get_name() const { diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3b034ab..241568c1 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -38,7 +38,8 @@ Keys::Keys( std::optional group_ed25519_secretkey, std::optional dumped, Info& info, - Members& members) { + Members& members, + std::optional parent_state) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; @@ -50,6 +51,9 @@ Keys::Keys( if (group_ed25519_secretkey && group_ed25519_secretkey->size() != 64) throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 secret key"}; + if (parent_state) + _parent_state = *parent_state; + init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..614c1925 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -10,8 +10,9 @@ namespace session::config::groups { Members::Members( ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped) : - ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} {} + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped, ed25519_pubkey, ed25519_secretkey} {} std::optional Members::get(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 9b08aa0c..751f76f2 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -261,8 +261,11 @@ void community_info::load(const dict& info_dict) { set_room(std::move(*n)); } -UserGroups::UserGroups(ustring_view ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped} { +UserGroups::UserGroups( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped} { load_key(ed25519_secretkey); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 1c5a3df2..143490a4 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -13,8 +13,11 @@ using session::ustring_view; LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; -UserProfile::UserProfile(ustring_view ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped} { +UserProfile::UserProfile( + ustring_view ed25519_secretkey, + std::optional dumped, + std::optional parent_state) : + ConfigBase{parent_state, dumped} { load_key(ed25519_secretkey); } diff --git a/src/state.cpp b/src/state.cpp index 7b37b375..1d4ad8dd 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -1,9 +1,12 @@ #include "session/state.hpp" +#include #include #include #include +#include +#include #include #include @@ -25,13 +28,13 @@ using namespace session::config; namespace session::state { GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { - auto info = std::make_unique(pubkey, std::nullopt, std::nullopt); - auto members = std::make_unique(pubkey, std::nullopt, std::nullopt); + auto info = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); + auto members = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); auto keys = std::make_unique( - user_sk, pubkey, std::nullopt, std::nullopt, *info, *members); - _config_info = std::move(info); - _config_members = std::move(members); - _config_keys = std::move(keys); + user_sk, pubkey, std::nullopt, std::nullopt, *info, *members, std::nullopt); + config_info = std::move(info); + config_members = std::move(members); + config_keys = std::move(keys); } State::State(ustring_view ed25519_secretkey) { @@ -45,34 +48,37 @@ State::State(ustring_view ed25519_secretkey) { crypto_sign_ed25519_sk_to_pk(_user_pk.data(), _user_sk.data()); // Initialise empty config states for the standard config types - _config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); - _config_convo_info_volatile = - std::make_unique(ed25519_secretkey, std::nullopt); - _config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); - _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); + std::optional parent = this; + config_contacts = std::make_unique(ed25519_secretkey, std::nullopt, parent); + config_convo_info_volatile = + std::make_unique(ed25519_secretkey, std::nullopt, parent); + config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt, parent); + config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt, parent); } void State::load( Namespace namespace_, std::optional pubkey_hex_, ustring_view dump) { + std::optional parent = this; + switch (namespace_) { case Namespace::Contacts: - _config_contacts = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + config_contacts = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; case Namespace::ConvoInfoVolatile: - _config_convo_info_volatile = std::make_unique( - to_unsigned_sv({_user_sk.data(), 64}), dump); + config_convo_info_volatile = std::make_unique( + to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; case Namespace::UserGroups: - _config_user_groups = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + config_user_groups = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; case Namespace::UserProfile: - _config_user_profile = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + config_user_profile = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; default: break; @@ -86,7 +92,7 @@ void State::load( throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; // Retrieve any keys for the group - auto user_group_info = _config_user_groups->get_group(*pubkey_hex_); + auto user_group_info = config_user_groups->get_group(*pubkey_hex_); if (!user_group_info) throw std::runtime_error{"Unable to retrieve group from user_groups config"}; @@ -112,58 +118,349 @@ void State::load( // Reload the specified namespace with the dump if (namespace_ == Namespace::GroupInfo) - _config_groups[pubkey_hex]->_config_info = - std::make_unique(pubkey, group_ed25519_secretkey, dump); + _config_groups[pubkey_hex]->config_info = + std::make_unique(pubkey, group_ed25519_secretkey, dump, parent); else if (namespace_ == Namespace::GroupMembers) - _config_groups[pubkey_hex]->_config_members = - std::make_unique(pubkey, group_ed25519_secretkey, dump); + _config_groups[pubkey_hex]->config_members = + std::make_unique(pubkey, group_ed25519_secretkey, dump, parent); else if (namespace_ == Namespace::GroupKeys) { - auto info = _config_groups[pubkey_hex]->_config_info.get(); - auto members = _config_groups[pubkey_hex]->_config_members.get(); + auto info = _config_groups[pubkey_hex]->config_info.get(); + auto members = _config_groups[pubkey_hex]->config_members.get(); auto keys = std::make_unique( - user_ed25519_secretkey, pubkey, pubkey, group_ed25519_secretkey, info, members); + user_ed25519_secretkey, pubkey, pubkey, group_ed25519_secretkey, info, members, parent); - _config_groups[pubkey_hex]->_config_keys = std::move(keys); + _config_groups[pubkey_hex]->config_keys = std::move(keys); } else throw std::runtime_error{"Attempted to load unknown namespace"}; } +GroupConfigs* State::group_config(std::string_view pubkey_hex) { + if (pubkey_hex.size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + if (!_config_groups.count(pubkey_hex)) + throw std::runtime_error{ + "Attempted to merge group configs before for group with no config state"}; + + return _config_groups[pubkey_hex].get(); +} + +std::vector State::merge( + std::optional pubkey_hex, const std::vector& configs) { + if (configs.empty()) + return {}; + + // Sort the namespaces based on the order they should be merged in to minimise conflicts between + // different config messages + auto sorted_configs = configs; + std::sort(sorted_configs.begin(), sorted_configs.end(), [](const auto& a, const auto& b) { + return namespace_merge_order(a.namespace_) < namespace_merge_order(b.namespace_); + }); + + std::vector good_hashes; + std::vector> pending_configs; + + for (size_t i = 0; i < sorted_configs.size(); ++i) { + auto& config = sorted_configs[i]; + + // If this is different from the last config, or it's a 'GroupKeys' config (GroupKeys + // only support individual merging) then clear 'pending_configs' so we can prepare for + // a new batch-merge + if (config.namespace_ == Namespace::GroupKeys || + (i > 0 && config.namespace_ != sorted_configs[i - 1].namespace_)) + pending_configs.clear(); + + pending_configs.emplace_back(config.hash, config.data); + + // If this is not a GroupKeys config, the last config or the next config is not in the same + // namespace then go to the next loop so we can batch-merge the configs in a later loop + if (config.namespace_ != Namespace::GroupKeys && i != (sorted_configs.size() - 1) && + config.namespace_ == sorted_configs[i + 1].namespace_) + continue; + + // Process the previously grouped configs + std::vector merged_hashes; + switch (config.namespace_) { + case Namespace::Contacts: + merged_hashes = config_contacts->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::ConvoInfoVolatile: + merged_hashes = config_convo_info_volatile->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::UserGroups: + merged_hashes = config_user_groups->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::UserProfile: + merged_hashes = config_user_profile->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + if (!_config_groups.count(*pubkey_hex)) + throw std::runtime_error{ + "Attempted to merge group configs before for group with no config state"}; + + auto info = _config_groups[*pubkey_hex]->config_info.get(); + auto members = _config_groups[*pubkey_hex]->config_members.get(); + + if (config.namespace_ == Namespace::GroupInfo) + merged_hashes = info->merge(pending_configs); + else if (config.namespace_ == Namespace::GroupMembers) + merged_hashes = members->merge(pending_configs); + else if (config.namespace_ == Namespace::GroupKeys) { + // GroupKeys doesn't support merging multiple messages at once so do them individually + if (_config_groups[*pubkey_hex]->config_keys->load_key_message( + config.hash, config.data, config.timestamp_ms, *info, *members)) { + good_hashes.emplace_back(config.hash); + } + } else + throw std::runtime_error{"Attempted to merge from unknown namespace"}; + } + + return good_hashes; +} + +void State::config_changed(std::optional pubkey_hex) { + throw std::runtime_error{"ASDASFSDFGSDF"}; + if (!send) + return; + + bool needs_push = false; + bool needs_dump = false; + std::string target_pubkey; + std::vector configs; + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + if (!pubkey_hex) { + // Convert the _user_pk to the user's session ID + std::array user_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + + // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to + // the sender session ID + target_pubkey.reserve(66); + target_pubkey += "05"; + oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey)); + + needs_push = + (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || + config_user_groups->needs_push() || config_user_profile->needs_push()); + configs = { + config_contacts.get(), + config_convo_info_volatile.get(), + config_user_groups.get(), + config_user_profile.get()}; + } else { + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + + target_pubkey = *pubkey_hex; + + if (target_pubkey.size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + if (!_config_groups.count(target_pubkey)) + throw std::runtime_error{"Change trigger in group configs with no state"}; + + // Ensure we have the admin key for the group + auto user_group_info = config_user_groups->get_group(target_pubkey); + + if (!user_group_info) + throw std::runtime_error{"Unable to retrieve group from user_groups config"}; + + // Only group admins can push group config changes + needs_push = + (!user_group_info->secretkey.empty() && + (_config_groups[target_pubkey]->config_info->needs_push() || + _config_groups[target_pubkey]->config_members->needs_push() || + _config_groups[target_pubkey]->config_keys->pending_config())); + configs = { + _config_groups[target_pubkey]->config_info.get(), + _config_groups[target_pubkey]->config_members.get()}; + } + + // Call the hook to perform a push if needed + if (needs_push) { + std::vector requests; + std::vector obsolete_hashes; + + for (auto& config : configs) { + auto [seqno, msg, obs] = config->push(); + + for (auto hash : obs) + obsolete_hashes.emplace_back(hash); + + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store") + + static_cast(config->storage_namespace()) + + static_cast(timestamp.count()); + + if (0 != crypto_sign_ed25519_detached( + sig.data(), + nullptr, + verification.data(), + verification.size(), + _user_sk.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"namespace", static_cast(config->storage_namespace())}, + {"pubkey", target_pubkey}, + {"ttl", config->default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(msg)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + + // For user config storage we also need to add `pubkey_ed25519` + if (!pubkey_hex) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + requests.emplace_back(params); + } + + // GroupKeys needs special handling as it's not a `ConfigBase` + if (pubkey_hex) { + auto pending = _config_groups[target_pubkey]->config_keys->pending_config(); + + if (pending) { + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = + to_unsigned("store") + + static_cast( + _config_groups[target_pubkey]->config_keys->storage_namespace()) + + static_cast(timestamp.count()); + + if (0 != crypto_sign_ed25519_detached( + sig.data(), + nullptr, + verification.data(), + verification.size(), + _user_sk.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"namespace", + _config_groups[target_pubkey]->config_keys->storage_namespace()}, + {"pubkey", target_pubkey}, + {"ttl", _config_groups[target_pubkey]->config_keys->default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(*pending)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + requests.emplace_back(params); + } + } + + // Sort the namespaces based on the order they should be stored in to minimise the chance + // that config messages dependant on others are stored before their dependencies + auto sorted_requests = requests; + std::sort(sorted_requests.begin(), sorted_requests.end(), [](const auto& a, const auto& b) { + return namespace_store_order(static_cast(a["namespace"])) < + namespace_store_order(static_cast(b["namespace"])); + }); + + nlohmann::json payload; + + for (auto& request : sorted_requests) { + nlohmann::json request_json{{"method", "store"}, {"params", request}}; + payload["requests"].push_back(request_json); + } + + // Also delete obsolete hashes + if (!obsolete_hashes.empty()) { + // Ed25519 signature of `("delete" || messages...)` + std::array sig; + ustring verification = to_unsigned("delete"); + + for (auto& hash : obsolete_hashes) + verification += to_unsigned_sv(hash); + + if (0 != crypto_sign_ed25519_detached( + sig.data(), + nullptr, + verification.data(), + verification.size(), + _user_sk.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"messages", obsolete_hashes}, + {"pubkey", target_pubkey}, + {"timestamp", timestamp.count()}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + + // For user config storage we also need to add `pubkey_ed25519` + if (!pubkey_hex) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + nlohmann::json request_json{{"method", "delete"}, {"params", params}}; + payload["requests"].push_back(request_json); + } + + send(target_pubkey, payload); +} + ustring State::dump(bool full_dump) { oxenc::bt_dict_producer combined; // NOTE: the keys have to be in ascii-sorted order: - if (full_dump || _config_contacts->needs_dump()) - combined.append("contacts", session::from_unsigned_sv(_config_contacts->dump())); + if (full_dump || config_contacts->needs_dump()) + combined.append("contacts", session::from_unsigned_sv(config_contacts->dump())); - if (full_dump || _config_convo_info_volatile->needs_dump()) + if (full_dump || config_convo_info_volatile->needs_dump()) combined.append( "convo_info_volatile", - session::from_unsigned_sv(_config_convo_info_volatile->dump())); + session::from_unsigned_sv(config_convo_info_volatile->dump())); - if (full_dump || _config_user_groups->needs_dump()) - combined.append("user_groups", session::from_unsigned_sv(_config_user_groups->dump())); + if (full_dump || config_user_groups->needs_dump()) + combined.append("user_groups", session::from_unsigned_sv(config_user_groups->dump())); - if (full_dump || _config_user_profile->needs_dump()) - combined.append("user_profile", session::from_unsigned_sv(_config_user_profile->dump())); + if (full_dump || config_user_profile->needs_dump()) + combined.append("user_profile", session::from_unsigned_sv(config_user_profile->dump())); // NOTE: `std::map` sorts keys in ascending order so can just add them in order if (_config_groups.size() > 0) { for (const auto& [key, config] : _config_groups) { - if (full_dump || config->_config_info->needs_dump() || - config->_config_keys->needs_dump() || config->_config_members->needs_dump()) { + if (full_dump || config->config_info->needs_dump() || + config->config_keys->needs_dump() || config->config_members->needs_dump()) { oxenc::bt_dict_producer group_combined = combined.append_dict(key); - if (full_dump || config->_config_info->needs_dump()) + if (full_dump || config->config_info->needs_dump()) group_combined.append( - "info", session::from_unsigned_sv(config->_config_info->dump())); + "info", session::from_unsigned_sv(config->config_info->dump())); - if (full_dump || config->_config_keys->needs_dump()) + if (full_dump || config->config_keys->needs_dump()) group_combined.append( - "keys", session::from_unsigned_sv(config->_config_keys->dump())); + "keys", session::from_unsigned_sv(config->config_keys->dump())); - if (full_dump || config->_config_members->needs_dump()) + if (full_dump || config->config_members->needs_dump()) group_combined.append( - "members", session::from_unsigned_sv(config->_config_members->dump())); + "members", session::from_unsigned_sv(config->config_members->dump())); } } } @@ -175,10 +472,10 @@ ustring State::dump(bool full_dump) { ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex_) { switch (namespace_) { - case Namespace::Contacts: return _config_contacts->dump(); - case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->dump(); - case Namespace::UserGroups: return _config_user_groups->dump(); - case Namespace::UserProfile: return _config_user_profile->dump(); + case Namespace::Contacts: return config_contacts->dump(); + case Namespace::ConvoInfoVolatile: return config_convo_info_volatile->dump(); + case Namespace::UserGroups: return config_user_groups->dump(); + case Namespace::UserProfile: return config_user_profile->dump(); default: break; } @@ -195,9 +492,9 @@ ustring State::dump(config::Namespace namespace_, std::optional_config_info->dump(); - case Namespace::GroupMembers: return group_configs->_config_members->dump(); - case Namespace::GroupKeys: return group_configs->_config_keys->dump(); + case Namespace::GroupInfo: return group_configs->config_info->dump(); + case Namespace::GroupMembers: return group_configs->config_members->dump(); + case Namespace::GroupKeys: return group_configs->config_keys->dump(); default: throw std::runtime_error{"Attempted to load unknown namespace"}; } } @@ -300,7 +597,37 @@ LIBSESSION_C_API bool state_load( } } -LIBSESSION_EXPORT void state_dump( +LIBSESSION_C_API void state_set_send_callback( + state_object* state, void (*callback)(const char*, const unsigned char*, size_t)) { + if (!callback) + unbox(state).logger = nullptr; + else { + unbox(state).send = [callback](std::string pubkey, ustring data) { + callback(pubkey.c_str(), data.data(), data.size()); + }; + } +} + +LIBSESSION_C_API config_string_list* state_merge( + state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count) { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 64); + + std::vector confs; + confs.reserve(count); + + for (size_t i = 0; i < count; i++) + confs.emplace_back( + static_cast(configs[i].namespace_), + configs[i].hash, + configs[i].timestamp_ms, + ustring{configs[i].data, configs[i].datalen}); + + return make_string_list(unbox(state).merge(pubkey_hex, confs)); +} + +LIBSESSION_C_API void state_dump( state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { assert(out && outlen); auto data = unbox(state).dump(full_dump); @@ -309,7 +636,7 @@ LIBSESSION_EXPORT void state_dump( std::memcpy(*out, data.data(), data.size()); } -LIBSESSION_EXPORT void state_dump_namespace( +LIBSESSION_C_API void state_dump_namespace( state_object* state, NAMESPACE namespace_, const char* pubkey_hex_, @@ -383,4 +710,32 @@ LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int unbox(state).set_profile_blinded_msgreqs(std::move(val)); } +LIBSESSION_C_API void state_set_logger( + state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx) { + if (!callback) + unbox(state).logger = nullptr; + else { + unbox(state).config_contacts->logger = [callback, ctx]( + session::config::LogLevel lvl, + std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + unbox(state).config_convo_info_volatile->logger = [callback, ctx]( + session::config::LogLevel lvl, + std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + unbox(state).config_user_groups->logger = [callback, ctx]( + session::config::LogLevel lvl, + std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + unbox(state).config_user_profile->logger = [callback, ctx]( + session::config::LogLevel lvl, + std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + } +} + } // extern "C" diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 3dbdbab8..aa5d9c5b 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -20,30 +20,16 @@ TEST_CASE("State", "[state][state]") { auto state = State(ed_sk); - // User Profile forwarding - CHECK_FALSE(state.get_profile_name().has_value()); - state.set_profile_name("Test Name"); - CHECK(state.get_profile_name() == "Test Name"); - - CHECK(state.get_profile_pic().empty()); - state.set_profile_pic("https://oxen.io", to_unsigned_sv("secret78901234567890123456789012")); - CHECK(state.get_profile_pic().url == "https://oxen.io"); - CHECK(state.get_profile_pic().key == "secret78901234567890123456789012"_bytes); - - auto second_pic = - profile_pic("https://oxen.io/2", to_unsigned_sv("secret78901234567890123456789012")); - state.set_profile_pic(second_pic); - CHECK(state.get_profile_pic().url == "https://oxen.io/2"); - - CHECK_FALSE(state.get_profile_blinded_msgreqs()); - state.set_profile_blinded_msgreqs(true); - CHECK(state.get_profile_blinded_msgreqs()); + // Sanity check direct config access + CHECK_FALSE(state.config_user_profile->get_name().has_value()); + state.config_user_profile->set_name("Test Name"); + CHECK(state.config_user_profile->get_name() == "Test Name"); auto dump = state.dump(Namespace::UserProfile); auto state2 = State(ed_sk); - CHECK_FALSE(state2.get_profile_name().has_value()); + CHECK_FALSE(state2.config_user_profile->get_name().has_value()); state2.load(Namespace::UserProfile, std::nullopt, {dump.data(), dump.size()}); - CHECK(state2.get_profile_name() == "Test Name"); + CHECK(state2.config_user_profile->get_name() == "Test Name"); } TEST_CASE("State c API", "[state][state][c]") { From 1a1e3b659f31aea238982185a0a8e008790a332b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Jan 2024 12:04:00 +1100 Subject: [PATCH 03/24] Progress on the state object Cleaned up the send and store hooks Added a batch merge function Added the ability to suppress triggering hooks (for use when triggering multiple changes at once) Added a function to handle the response from sending the 'send' hook data to the swarm Added code to route the config logging to it's parent state logger (if available and an explicit logger hasn't been set) Extracted the state C wrapper into it's own file to keep the main logic cleaner --- .gitignore | 3 +- include/session/config/base.hpp | 5 +- include/session/config/groups/keys.hpp | 4 + include/session/config/namespaces.hpp | 12 + include/session/onionreq/parser.hpp | 4 +- include/session/state.h | 334 ++++++---- include/session/state.hpp | 281 +++++---- include/session/util.hpp | 4 + src/CMakeLists.txt | 1 + src/config/base.cpp | 20 +- src/config/groups/keys.cpp | 18 +- src/config/protos.cpp | 1 + src/session_encrypt.cpp | 6 +- src/state.cpp | 828 ++++++++++++++----------- src/state_c_wrapper.cpp | 399 ++++++++++++ 15 files changed, 1263 insertions(+), 657 deletions(-) create mode 100644 src/state_c_wrapper.cpp diff --git a/.gitignore b/.gitignore index f13ebfd0..e705fdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build*/ /compile_commands.json /.cache/ -/.vscode/ \ No newline at end of file +/.vscode/ +.DS_Store \ No newline at end of file diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 89332007..3c88ef76 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -198,10 +198,7 @@ class ConfigBase : public ConfigSig { void set_state(ConfigState s); // Invokes the `logger` callback if set, does nothing if there is no logger. - void log(LogLevel lvl, std::string msg) { - if (logger) - logger(lvl, std::move(msg)); - } + void log(LogLevel lvl, std::string msg); // Returns a reference to the current MutableConfigMessage. If the current message is not // already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter. diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 50ffb683..b2bb8b96 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -106,6 +106,10 @@ class Keys final : public ConfigSig { bool needs_dump_ = false; + // Updates the `needs_dump_` value, should always be called instead of setting directly as there + // are side effects we want to trigger when the value changes. + void set_needs_dump(bool updated_needs_dump); + ConfigMessage::verify_callable verifier_; ConfigMessage::sign_callable signer_; diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 5b6f04d5..ec7ef396 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -20,6 +20,18 @@ enum class Namespace : std::int16_t { }; namespace { + /// Returns a number indicating the order that the config dumps should be loaded in, we need to + /// load the `UserGroups` config before any group configs (due to how the configs are stored) + /// and the `GroupKeys` config _after_ the `GroupInfo` and `GroupMembers` configs as it requires + /// those to be passed as arguments + int namespace_load_order(const Namespace& n) { + if (n == Namespace::GroupInfo || n == Namespace::GroupMembers) + return 1; + if (n == Namespace::GroupKeys) + return 2; + return 0; + } + /// Returns a number indicating the order that messages from the specified namespace should be /// merged in (lower numbers shold be merged first), /// by merging in a specific order we can prevent certain edge-cases where data/logic between diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 8d2d290e..03e80353 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -1,6 +1,8 @@ +#pragma once + #include -#include "session/onionreq/hop_encryption.hpp" +#include "hop_encryption.hpp" #include "session/types.hpp" namespace session::onionreq { diff --git a/include/session/state.h b/include/session/state.h index 1e066559..cc7794a6 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -12,7 +12,6 @@ extern "C" { #include "config/profile_pic.h" #include "export.h" -// State object: this type holds the internal object which manages the entire state. typedef struct state_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; @@ -25,6 +24,13 @@ typedef struct state_object { char _error_buf[256]; } state_object; +typedef struct state_namespaced_dump { + NAMESPACE namespace_; + const char* pubkey_hex; + const unsigned char* data; + size_t datalen; +} state_namespaced_dump; + typedef struct state_config_message { NAMESPACE namespace_; const char* hash; @@ -33,37 +39,6 @@ typedef struct state_config_message { size_t datalen; } state_config_message; -/// API: state/state_set_logger -/// -/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL -/// if not needed). The given function pointer will be invoked with one of the above values, a -/// null-terminated c string containing the log message, and the void* context object given when -/// setting the logger (this is for caller-specific state data and won't be touched). -/// -/// The logging function must have signature: -/// -/// void log(config_log_level lvl, const char* msg, void* ctx); -/// -/// Can be called with callback set to NULL to clear an existing logger. -/// -/// The config object itself has no log level: the caller should filter by level as needed. -/// -/// Declaration: -/// ```cpp -/// VOID config_set_logger( -/// [in, out] state_object* state, -/// [in] void(*)(config_log_level, const char*, void*) callback, -/// [in] void* ctx -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `callback` -- [in] Callback function -/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused -LIBSESSION_EXPORT void state_set_logger( - state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx); - /// API: state/state_create /// /// Constructs a new state which generates it's own random ed25519 key pair. @@ -71,14 +46,6 @@ LIBSESSION_EXPORT void state_set_logger( /// When done with the object the `state_object` must be destroyed by passing the pointer to /// state_free(). /// -/// Declaration: -/// ```cpp -/// INT state_init( -/// [out] state_object** state, -/// [out] char* error -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [out] Pointer to the state object /// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error @@ -98,20 +65,14 @@ LIBSESSION_EXPORT bool state_create(state_object** state, char* error) /// When done with the object the `state_object` must be destroyed by passing the pointer to /// state_free(). /// -/// Declaration: -/// ```cpp -/// INT state_init( -/// [out] state_object** state, -/// [in] const unsigned char* ed25519_secretkey, -/// [out] char* error -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [out] Pointer to the state object /// - `ed25519_secretkey` -- [in] must be the 32-byte secret key seed value. (You can also pass the /// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32 /// bytes of that are the seed). This field cannot be null. +/// - `dumps` -- [in] pointer to an array of `state_namespaced_dump` which should include all dumps +/// which should be loaded into the state. +/// - `count` -- [in] number of items in the `dumps` pointer. /// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error /// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a /// buffer of at least 256 bytes. @@ -120,24 +81,34 @@ LIBSESSION_EXPORT bool state_create(state_object** state, char* error) /// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message /// as a C-string into `error` (if not NULL) on failure. LIBSESSION_EXPORT bool state_init( - state_object** state, const unsigned char* ed25519_secretkey, char* error) - __attribute__((warn_unused_result)); + state_object** state, + const unsigned char* ed25519_secretkey, + state_namespaced_dump* dumps, + size_t count, + char* error) __attribute__((warn_unused_result)); /// API: state/state_free /// /// Frees a state object. /// -/// Declaration: -/// ```cpp -/// VOID state_free( -/// [in, out] state_object* state -/// ); -/// ``` -/// /// Inputs: /// - `conf` -- [in] Pointer to config_object object LIBSESSION_EXPORT void state_free(state_object* state); +/// API: state/state_load +/// +/// Loads a dump into the state. Calling this will replace the current config instance with +/// with a new instance initialised with the provided dump. The configs must be loaded according +/// to the order 'namespace_load_order' in 'namespaces.hpp' or an exception will be thrown. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `namespace` -- the namespace where config messages for this dump are stored. +/// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `dump` -- pointer to the binary state data that was previously dumped by calling `dump()` or +/// from the `store` hook. +/// - `dumplen` -- length of `dump`. LIBSESSION_EXPORT bool state_load( state_object* state, NAMESPACE namespace_, @@ -145,12 +116,142 @@ LIBSESSION_EXPORT bool state_load( const unsigned char* dump, size_t dumplen); -LIBSESSION_EXPORT void state_set_send_callback( - state_object* state, void (*callback)(const char*, const unsigned char*, size_t)); -// std::function send; +/// API: state/state_set_logger +/// +/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL +/// if not needed). The given function pointer will be invoked with one of the above values, a +/// null-terminated c string containing the log message, and the void* context object given when +/// setting the logger (this is for caller-specific state data and won't be touched). +/// +/// The logging function must have signature: +/// +/// void log(config_log_level lvl, const char* msg, void* ctx); +/// +/// Can be called with callback set to NULL to clear an existing logger. +/// +/// The config object itself has no log level: the caller should filter by level as needed. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_set_logger( + state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx); + +/// API: state/state_set_send_callback +/// +/// Takes a function pointer and a context pointer (which can be NULL if not needed). The given +/// function pointer will be invoked whenever a config `needs_push` as long as the state isn't +/// suppressing send events. +/// +/// The function must have signature: +/// +/// void callback(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*); +/// +/// Can be called with callback set to NULL to clear an existing hook. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT bool state_set_send_callback( + state_object* state, + void (*callback)(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*), + void* ctx); + +/// API: state/state_set_store_callback +/// +/// Takes a function pointer and a context pointer (which can be NULL if not needed). The given +/// function pointer will be invoked whenever a config `needs_dump` as long as the state isn't +/// suppressing store events. +/// +/// The function must have signature: +/// +/// void callback(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*); +/// +/// Can be called with callback set to NULL to clear an existing hook. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT bool state_set_store_callback( + state_object* state, + void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), + void* ctx); + +/// API: state/state_set_service_node_offset +/// +/// Updates the state service node offset. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `offset_ms` -- [in] the delta between the current device time and service node time in the +/// most recent API response +LIBSESSION_EXPORT void state_set_service_node_offset(state_object* state, int64_t offset_ms); + +/// API: state/state_network_offset +/// +/// Retrieves the state service node offset. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// +/// Outputs: +/// - `int64_t` -- the delta between the current device time and service node time in the +/// most recent API response +LIBSESSION_EXPORT int64_t state_network_offset(state_object* state); + +/// API: state/state_suppress_hooks_start +/// +/// This will suppress the `send` and `store` hooks until `state_suppress_hooks_stop` is called and +/// should be used when making multiple config changes to avoid sending and storing unnecessary +/// partial changes. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `send` -- [in] controls whether the `send` hook should be suppressed. +/// - `store` -- [in] controls whether the `store` hook should be suppressed. +/// - `pubkey_hex` -- [in] pubkey to suppress changes for (in hex, with prefix - 66 +/// bytes). If none is provided then all changes for all configs will be supressed. +LIBSESSION_EXPORT bool state_suppress_hooks_start( + state_object* state, bool send, bool store, const char* pubkey_hex); + +/// API: state/state_suppress_hooks_stop +/// +/// This will stop suppressing the `send` and `store` hooks. When this is called, if there are +/// any pending changes, the `send` and `store` hooks will immediately be called. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `send` -- [in] controls whether the `send` hook should no longer be suppressed. +/// - `store` -- [in] controls whether the `store` hook should no longer be suppressed. +/// - `pubkey_hex` -- [in] pubkey to stop suppressing changes for (in hex, with prefix - 66 bytes). +/// If the value provided doesn't match a entry created by `state_suppress_hooks_start` those +/// changes will continue to be suppressed. If none is provided then the hooks for all configs +/// with pending changes will be triggered. +LIBSESSION_EXPORT bool state_suppress_hooks_stop( + state_object* state, bool send, bool store, const char* pubkey_hex); -LIBSESSION_EXPORT config_string_list* state_merge( - state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count); +/// API: state/state_merge +/// +/// Takes an pointer to an array of `state_config_message`, sorts them and merges them into the +/// relevant configs. Allocates a new buffer and sets it in `successful_hashes`. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `configs` -- [in] Pointer to an array of `state_config_message` objects +/// - `count` -- [in] Number of objects in `configs` +/// - `successful_hashes` -- [out] Pointer to an array of message hashes that were successfully +/// merged +LIBSESSION_EXPORT bool state_merge( + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + config_string_list** successful_hashes); /// API: state/state_dump /// @@ -166,24 +267,13 @@ LIBSESSION_EXPORT config_string_list* state_merge( /// Immediately after this is called `state_needs_dump` will start returning falst (until the /// configuration is next modified). /// -/// Declaration: -/// ```cpp -/// VOID state_dump( -/// [in] state_object* state -/// [in] bool full_dump -/// [out] unsigned char** out -/// [out] size_t* outlen -/// ); -/// -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to state_object object /// - `full_dump` -- [in] Flag when true the returned bt-encoded dict will include dumps for the /// entire state, even if they would normally return `false` for `needs_dump()`. /// - `out` -- [out] Pointer to the output location /// - `outlen` -- [out] Length of output -LIBSESSION_EXPORT void state_dump( +LIBSESSION_EXPORT bool state_dump( state_object* state, bool full_dump, unsigned char** out, size_t* outlen); /// API: state/state_dump_namespace @@ -198,32 +288,51 @@ LIBSESSION_EXPORT void state_dump( /// Immediately after this is called `state_needs_dump` will start returning false (until the /// configuration is next modified). /// -/// Declaration: -/// ```cpp -/// VOID state_dump( -/// [in] state_object* state -/// [in] NAMESPACE namespace -/// [in] const char* pubkey_hex -/// [out] unsigned char** out -/// [out] size_t* outlen -/// ); -/// -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to state_object object /// - `namespace` -- [in] the namespace where config messages of the desired dump are stored. -/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex). Required for group -/// dumps. +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. /// - `out` -- [out] Pointer to the output location /// - `outlen` -- [out] Length of output -LIBSESSION_EXPORT void state_dump_namespace( +LIBSESSION_EXPORT bool state_dump_namespace( state_object* state, NAMESPACE namespace_, const char* pubkey_hex, unsigned char** out, size_t* outlen); +/// API: state/state_received_send_response +/// +/// Takes the network response from sending the data from the `send` hook and confirms the configs +/// were successfully pushed. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `seqnos` -- [in] Pointer to an array of sequence numbers for each config which was sent. Must +/// be in the same order the push data was in. Can just pass the pointer which was provided from the +/// `send` hook. +/// - `seqnos_len` -- [in] Number of items in `seqnos`. +/// - `payload_data` -- [in] Pointer to the push data payload that resulted in this response. Can +/// just pass the pointer which was provided from the `send` hook. +/// - `payload_data_len` -- [in] Length of the `payload_data`. +/// - `response_data` -- [in] Pointer to the response from the swarm after sending the +/// `payload_data`. +/// - `response_data_len` -- [in] Length of the `response_data`. +/// - `out` -- [out] Pointer to the output location +/// - `outlen` -- [out] Length of output +LIBSESSION_EXPORT bool state_received_send_response( + state_object* state, + const char* pubkey_hex, + const seqno_t* seqnos, + size_t seqnos_len, + unsigned char* payload_data, + size_t payload_data_len, + unsigned char* response_data, + size_t response_data_len); + /// User Profile functions /// API: state/state_get_profile_name @@ -231,13 +340,6 @@ LIBSESSION_EXPORT void state_dump_namespace( /// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at /// all. Should be copied right away as the pointer may not remain valid beyond other API calls. /// -/// Declaration: -/// ```cpp -/// CONST CHAR* state_get_profile_name( -/// [in] const state_object* state -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the state object /// @@ -251,14 +353,6 @@ LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); /// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on /// error (and sets the state_object's error string). /// -/// Declaration: -/// ```cpp -/// BOOL state_set_profile_name( -/// [in] state_object* state, -/// [in] const char* name -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the state object /// - `name` -- [in] Pointer to the name as a null-terminated C string @@ -273,13 +367,6 @@ LIBSESSION_EXPORT bool state_set_profile_name(state_object* state, const char* n /// pic is not currently set, and otherwise should be copied right away (they will not be valid /// beyond other API calls on this config object). /// -/// Declaration: -/// ```cpp -/// USER_PROFILE_PIC state_get_profile_pic( -/// [in] const state_object* state -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the state object /// @@ -291,14 +378,6 @@ LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* sta /// /// Sets a user profile /// -/// Declaration: -/// ```cpp -/// BOOL state_set_profile_pic( -/// [in] state_object* state, -/// [in] user_profile_pic pic -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the satet object /// - `pic` -- [in] Pointer to the pic @@ -312,13 +391,6 @@ LIBSESSION_EXPORT bool state_set_profile_pic(state_object* state, user_profile_p /// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they /// should be ignored. /// -/// Declaration: -/// ```cpp -/// INT state_get_profile_blinded_msgreqs( -/// [in] const state_object* state -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the state object /// @@ -332,14 +404,6 @@ LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* stat /// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any /// positive value) to enable; 0 to disable; and -1 to clear the setting. /// -/// Declaration: -/// ```cpp -/// VOID state_set_profile_blinded_msgreqs( -/// [in] state_object* state, -/// [in] int enabled -/// ); -/// ``` -/// /// Inputs: /// - `state` -- [in] Pointer to the state object /// - `enabled` -- [in] true if they should be enabled, false if disabled diff --git a/include/session/state.hpp b/include/session/state.hpp index 7b77f5d8..d98d612b 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include "config/contacts.hpp" #include "config/convo_info_volatile.hpp" #include "config/groups/info.hpp" @@ -11,14 +9,12 @@ #include "config/user_groups.hpp" #include "config/user_profile.hpp" #include "ed25519.hpp" +#include "session/util.hpp" namespace session::state { -// Levels for the logging callback -enum class LogLevel { debug = 0, info, warning, error }; - using Ed25519PubKey = std::array; -using Ed25519Secret = sodium_array; +using Ed25519Secret = std::array; /// Struct containing group configs. class GroupConfigs { @@ -35,6 +31,24 @@ class GroupConfigs { std::unique_ptr config_keys; }; +struct namespaced_dump { + config::Namespace namespace_; + std::optional pubkey_hex; + ustring data; + + namespaced_dump( + config::Namespace namespace_, + std::optional pubkey_hex, + ustring data) : + namespace_{namespace_}, pubkey_hex{pubkey_hex}, data{data} {}; + + namespaced_dump() = delete; + namespaced_dump(namespaced_dump&&) = default; + namespaced_dump(const namespaced_dump&) = default; + namespaced_dump& operator=(namespaced_dump&&) = default; + namespaced_dump& operator=(const namespaced_dump&) = default; +}; + struct config_message { config::Namespace namespace_; std::string hash; @@ -68,14 +82,25 @@ struct config_message { class State { private: + // Storage of pubkeys which are currently being suppressed, the value specifies whether the + // `send` or `store` hook is suppressed. + std::map> _open_suppressions = {}; std::map> _config_groups; protected: Ed25519PubKey _user_pk; Ed25519Secret _user_sk; + std::function + _store; + std::function seqnos, ustring data)> _send; + // Invokes the `logger` callback if set, does nothing if there is no logger. - void log(LogLevel lvl, std::string msg) { + void log(session::config::LogLevel lvl, std::string msg) { if (logger) logger(lvl, std::move(msg)); } @@ -91,11 +116,11 @@ class State { GroupConfigs* group_config(std::string_view pubkey_hex); // Constructs a state with a secretkey that will be used for signing. - State(ustring_view ed25519_secretkey); + State(ustring_view ed25519_secretkey, std::vector dumps); // Constructs a new state, this will generate a random secretkey and should only be used for // creating a new account. - State() : State(to_unsigned_sv(session::ed25519::ed25519_key_pair().second)){}; + State() : State(to_unsigned_sv(session::ed25519::ed25519_key_pair().second), {}){}; // Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to // be managed. @@ -105,18 +130,56 @@ class State { State& operator=(const State&) = delete; // If set then we log things by calling this callback - std::function logger; + std::function logger; + + // Hook which will be called whenever config dumps need to be saved to persistent storage. The + // hook will immediately be called upon assignment if the state needs to be stored. + void onStore(std::function< + void(config::Namespace namespace_, + std::string prefixed_pubkey, + uint64_t timestamp_ms, + ustring data)> hook) { + _store = hook; + + if (!hook) + return; + + _open_suppressions[""] = {false, true}; + suppress_hooks_stop(); // Trigger config change hooks + _open_suppressions.erase(""); + }; + + /// Hook which will be called whenever config messages need to be sent via the API. The hook + /// will immediately be called upon assignment if the state needs to be pushed. + /// + /// Parameters: + /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. + /// - `seqnos` -- a vector of the seqnos for the each updated config message included in the + /// payload. + /// - `data` -- payload which should be sent to the API. + void onSend(std::function seqnos, ustring data)> + hook) { + _send = hook; + + if (!hook) + return; + + _open_suppressions[""] = {true, false}; + suppress_hooks_stop(); // Trigger config change hooks + _open_suppressions.erase(""); + }; /// API: state/State::load /// /// Loads a dump into the state. Calling this will replace the current config instance with - /// with a new instance initialised with the provided dump. The USER_GROUPS config must be - /// loaded before any GROUPS config dumps are loaded or an exception will be thrown. + /// with a new instance initialised with the provided dump. The configs must be loaded according + /// to the order 'namespace_load_order' in 'namespaces.hpp' or an exception will be thrown. /// /// Inputs: /// - `namespace` -- the namespace where config messages for this dump are stored. - /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex). Required for group - /// dumps. + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). + /// Required for group dumps. /// - `dump` -- binary state data that was previously dumped by calling `dump()`. /// /// Outputs: None @@ -125,39 +188,76 @@ class State { std::optional pubkey_hex, ustring_view dump); - /// API: base/ConfigBase::merge + /// API: state/State::config_changed + /// + /// This is called internally whenever a config gets dirtied. This function then validates the + /// state of all config objects associated to the `pubkey_hex` and triggers the `store` and + /// `send` hooks if needed. If there is an open suppression then the suppressed hook(s) will not + /// be called. + /// + /// Inputs: + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). Required for group changes. + /// + /// Outputs: None + void config_changed(std::optional pubkey_hex = std::nullopt); + + /// API: state/State::suppress_hooks_start + /// + /// This will suppress the `send` and `store` hooks until `suppress_hooks_stop` is called and + /// should be used when making multiple config changes to avoid sending and storing unnecessary + /// partial changes. + /// + /// Inputs: + /// - `send` -- controls whether the `send` hook should be suppressed. + /// - `store` -- controls whether the `store` hook should be suppressed. + /// - `pubkey_hex` -- pubkey to suppress changes for (in hex, with prefix - 66 + /// bytes). If none is provided then all changes for all configs will be supressed. + /// + /// Outputs: None + void suppress_hooks_start( + bool send = true, bool store = true, std::string_view pubkey_hex = ""); + + /// API: state/State::suppress_hooks_stop + /// + /// This will stop suppressing the `send` and `store` hooks. When this is called, if there are + /// any pending changes, the `send` and `store` hooks will immediately be called. + /// + /// Inputs: + /// - `send` -- controls whether the `send` hook should no longer be suppressed. + /// - `store` -- controls whether the `store` hook should no longer be suppressed. + /// - `pubkey_hex` -- pubkey to stop suppressing changes for (in hex, with prefix - 66 bytes). + /// If the value provided doesn't match a entry created by `suppress_hooks_start` those + /// changes will continue to be suppressed. If none is provided then the hooks for all configs + /// with pending changes will be triggered. + /// + /// Outputs: None + void suppress_hooks_stop(bool send = true, bool store = true, std::string_view pubkey_hex = ""); + + /// API: state/State::merge /// /// This takes all of the messages pulled down from the server and does whatever is necessary to /// merge (or replace) the current values. /// /// Values are pairs of the message hash (as provided by the server) and the raw message body. /// - /// For backwards compatibility, for certain message types (ones that have a - /// `accepts_protobuf()` override returning true) optional protobuf unwrapping of the incoming - /// message is performed; if successful then the unwrapped raw value is used; if the protobuf - /// unwrapping fails, the value is used directly as a raw value. - /// - /// After this call the caller should check `needs_push()` to see if the data on hand was - /// updated and needs to be pushed to the server again (for example, because the data contained - /// conflicts that required another update to resolve). + /// During this call the `send` and `store` callbacks will be triggered at the appropriate times + /// to correctly update the dump data and push any data to the server again if needed (for + /// example, because the data contained conflicts that required another update to resolve). /// - /// Returns the number of the given config messages that were successfully parsed. + /// Returns a vector of successfully merged hashes. /// /// Will throw on serious error (i.e. if neither the current nor any of the given configs are /// parseable). This should not happen (the current config, at least, should always be /// re-parseable). /// - /// Declaration: - /// ```cpp - /// std::vector merge( - /// const std::vector>& configs); - /// std::vector merge( - /// const std::vector>& configs); - /// ``` /// /// Inputs: - /// - `configs` -- vector of pairs containing the message hash and the raw message body (or - /// protobuf-wrapped raw message for certain config types). + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). + /// Required for group dumps. + /// - `configs` -- vector of `config_message` types which include the data needed to properly + /// merge. /// /// Outputs: /// - vector of successfully parsed hashes. Note that this does not mean the hash was recent or @@ -167,10 +267,6 @@ class State { std::vector merge( std::optional pubkey_hex, const std::vector& configs); - std::function send; - - void config_changed(std::optional pubkey_hex = std::nullopt); - /// API: state/State::dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for @@ -195,8 +291,8 @@ class State { /// /// Inputs: /// - `namespace` -- the namespace where config messages of the desired dump are stored. - /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex). Required for group - /// dumps. + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). Required for group dumps. /// /// Outputs: /// - `ustring` -- Returns binary data of the state dump @@ -204,101 +300,22 @@ class State { config::Namespace namespace_, std::optional pubkey_hex = std::nullopt); - public: - void set_service_node_timestamp(std::chrono::milliseconds timestamp) { - network_offset = - (timestamp - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch())); - }; - - // User Profile functions - public: - /// API: state/State::get_profile_name - /// - /// Returns the user profile name, or std::nullopt if there is no profile name set. + /// API: state/State::received_send_response /// - /// Inputs: None - /// - /// Outputs: - /// - `std::optional` - Returns the user profile name if it exists - std::optional get_profile_name() const { - return config_user_profile->get_name(); - }; - - /// API: state/State::set_profile_name - /// - /// Sets the user profile name; if given an empty string then the name is removed. + /// Takes the network response from sending the data from the `send` hook and confirms the + /// configs were successfully pushed. /// /// Inputs: - /// - `new_name` -- The name to be put into the user profile - void set_profile_name(std::string_view new_name) { config_user_profile->set_name(new_name); }; - - /// API: user_profile/UserProfile::get_profile_pic - /// - /// Gets the user's current profile pic URL and decryption key. The returned object will - /// evaluate as false if the URL and/or key are not set. - /// - /// Inputs: None - /// - /// Outputs: - /// - `profile_pic` - Returns the profile pic - config::profile_pic get_profile_pic() const { return config_user_profile->get_profile_pic(); }; - - /// API: state/State::set_profile_pic - /// - /// Sets the user's current profile pic to a new URL and decryption key. Clears both if either - /// one is empty. - /// - /// Declaration: - /// ```cpp - /// void set_profile_pic(std::string_view url, ustring_view key); - /// void set_profile_pic(profile_pic pic); - /// ``` - /// - /// Inputs: - /// - First function: - /// - `url` -- URL pointing to the profile pic - /// - `key` -- Decryption key - /// - Second function: - /// - `pic` -- Profile pic object - void set_profile_pic(std::string_view url, ustring_view key) { - config_user_profile->set_profile_pic(url, key); - }; - void set_profile_pic(config::profile_pic pic) { config_user_profile->set_profile_pic(pic); }; - - /// API: state/State::get_profile_blinded_msgreqs - /// - /// Accesses whether or not blinded message requests are enabled for the client. Can have three - /// values: - /// - /// - std::nullopt -- the value has not been given an explicit value so the client should use - /// its default. - /// - true -- the value is explicitly enabled (i.e. user wants blinded message requests) - /// - false -- the value is explicitly disabled (i.e. user disabled blinded message requests) - /// - /// Inputs: None - /// - /// Outputs: - /// - `std::optional` - true/false if blinded message requests are enabled or disabled; - /// `std::nullopt` if the option has not been set either way. - std::optional get_profile_blinded_msgreqs() const { - return config_user_profile->get_blinded_msgreqs(); - }; - - /// API: state/State::set_profile_blinded_msgreqs - /// - /// Sets whether blinded message requests (i.e. from SOGS servers you are connected to) should - /// be enabled or not. This is typically invoked with either `true` or `false`, but can also be - /// called with `std::nullopt` to explicitly clear the value. - /// - /// Inputs: - /// - `enabled` -- true if blinded message requests should be retrieved, false if they should - /// not, and `std::nullopt` to drop the setting from the config (and thus use the client's - /// default). - void set_profile_blinded_msgreqs(std::optional enabled) { - config_user_profile->set_blinded_msgreqs(enabled); - }; + /// - `pubkey` -- the pubkey (in hex, with prefix - 66 bytes) for the swarm where the data was + /// sent. + /// - `seqnos` -- the seqnos for each config messages included in the payload. + /// - `payload_data` -- payload which was sent to the swarm. + /// - `response_data` -- response that was returned from the swarm. + void received_send_response( + std::string pubkey, + std::vector seqnos, + ustring payload_data, + ustring response_data); }; -} // namespace session::state - +}; // namespace session::state diff --git a/include/session/util.hpp b/include/session/util.hpp index 6d2df4b7..63bf4ced 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -73,6 +73,10 @@ inline uint64_t get_timestamp() { return std::chrono::steady_clock::now().time_since_epoch().count(); } +inline std::string bool_to_string(bool v) { + return (v ? "true" : "false"); +} + /// Returns true if the first string is equal to the second string, compared case-insensitively. inline bool string_iequal(std::string_view s1, std::string_view s2) { return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5272af48..f7a482c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,6 +78,7 @@ add_libsession_util_library(config add_libsession_util_library(state state.cpp + state_c_wrapper.cpp ) diff --git a/src/config/base.cpp b/src/config/base.cpp index 54c3fc46..20efc48d 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -34,9 +34,13 @@ void ConfigBase::set_state(ConfigState s) { } _state = s; _needs_dump = true; - - if (_parent_state && _sign_pk) - (*_parent_state)->config_changed("03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end())); +} + +void ConfigBase::log(LogLevel lvl, std::string msg) { + if (logger) + logger(lvl, std::move(msg)); + else if (_parent_state && (*_parent_state)->logger) + (*_parent_state)->logger(lvl, msg); } MutableConfigMessage& ConfigBase::dirty() { @@ -45,6 +49,16 @@ MutableConfigMessage& ConfigBase::dirty() { _config = std::make_unique(*_config, increment_seqno); } + // If there is a parent state then notify it about the config change + if (_parent_state) { + std::optional pubkey_hex; + + if (_sign_pk) + pubkey_hex = "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()); + + (*_parent_state)->config_changed(pubkey_hex); + } + if (auto* mut = dynamic_cast(_config.get())) return *mut; throw std::runtime_error{"Internal error: unexpected dirty but non-mutable ConfigMessage"}; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 241568c1..9c0964af 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -22,6 +22,7 @@ #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" #include "session/multi_encrypt.hpp" +#include "session/state.hpp" #include "session/xed25519.hpp" using namespace std::literals; @@ -32,6 +33,13 @@ static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { return std::chrono::system_clock::time_point{milliseconds_since_epoch * 1ms}; } +void Keys::set_needs_dump(bool updated_needs_dump) { + needs_dump_ = updated_needs_dump; + + if (updated_needs_dump && _parent_state && _sign_pk) + (*_parent_state)->config_changed("03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end())); +} + Keys::Keys( ustring_view user_ed25519_secretkey, ustring_view group_ed25519_pubkey, @@ -75,7 +83,7 @@ bool Keys::needs_dump() const { ustring Keys::dump() { auto dumped = make_dump(); - needs_dump_ = false; + set_needs_dump(false); return dumped; } @@ -414,7 +422,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { members.replace_keys(new_key_list, /*dirty=*/true); info.replace_keys(new_key_list, /*dirty=*/true); - needs_dump_ = true; + set_needs_dump(true); return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } @@ -868,7 +876,7 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { active_msgs_[new_key.generation].emplace(msg_hash); keys_.insert(it, std::move(new_key)); remove_expired(); - needs_dump_ = true; + set_needs_dump(true); } // Attempts xchacha20 decryption. @@ -1103,7 +1111,7 @@ bool Keys::load_key_message( if (admin() && !new_keys.empty() && !pending_key_config_.empty() && (new_keys[0].generation > pending_gen_ || new_keys[0].key == pending_key_)) { pending_key_config_.clear(); - needs_dump_ = true; + set_needs_dump(true); } if (!new_keys.empty()) { @@ -1117,7 +1125,7 @@ bool Keys::load_key_message( } else if (max_gen) { active_msgs_[*max_gen].emplace(hash); remove_expired(); - needs_dump_ = true; + set_needs_dump(true); } return false; diff --git a/src/config/protos.cpp b/src/config/protos.cpp index affcc83c..855784bb 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -8,6 +8,7 @@ #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" +#include "session/config/namespaces.hpp" #include "session/session_encrypt.hpp" namespace session::config::protos { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 10cac8b3..a95073be 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -561,6 +561,8 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { using namespace session; +extern "C" { + LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( const unsigned char* plaintext_in, size_t plaintext_len, @@ -720,4 +722,6 @@ LIBSESSION_C_API bool session_decrypt_push_notification( } catch (...) { return false; } -} \ No newline at end of file +} + +} // extern "C" diff --git a/src/state.cpp b/src/state.cpp index 1d4ad8dd..3595931e 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -29,7 +29,8 @@ namespace session::state { GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { auto info = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); - auto members = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); + auto members = + std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); auto keys = std::make_unique( user_sk, pubkey, std::nullopt, std::nullopt, *info, *members, std::nullopt); config_info = std::move(info); @@ -37,23 +38,41 @@ GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { config_keys = std::move(keys); } -State::State(ustring_view ed25519_secretkey) { +State::State(ustring_view ed25519_secretkey, std::vector dumps) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; if (ed25519_secretkey.size() != 64) throw std::invalid_argument{"Invalid ed25519_secretkey: expected 64 bytes"}; - _user_sk.reset(64); std::memcpy(_user_sk.data(), ed25519_secretkey.data(), ed25519_secretkey.size()); crypto_sign_ed25519_sk_to_pk(_user_pk.data(), _user_sk.data()); - // Initialise empty config states for the standard config types + // Load in the dumps + auto sorted_dumps = dumps; + std::sort(sorted_dumps.begin(), sorted_dumps.end(), [](const auto& a, const auto& b) { + return namespace_load_order(a.namespace_) < namespace_load_order(b.namespace_); + }); + + for (auto dump : sorted_dumps) { + load(dump.namespace_, dump.pubkey_hex, dump.data); + } + + // Initialise empty config states for any missing required config types std::optional parent = this; - config_contacts = std::make_unique(ed25519_secretkey, std::nullopt, parent); - config_convo_info_volatile = - std::make_unique(ed25519_secretkey, std::nullopt, parent); - config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt, parent); - config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt, parent); + + if (!config_contacts) + config_contacts = std::make_unique(ed25519_secretkey, std::nullopt, parent); + + if (!config_convo_info_volatile) + config_convo_info_volatile = + std::make_unique(ed25519_secretkey, std::nullopt, parent); + + if (!config_user_groups) + config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt, parent); + + if (!config_user_profile) + config_user_profile = + std::make_unique(ed25519_secretkey, std::nullopt, parent); } void State::load( @@ -72,13 +91,13 @@ void State::load( return; case Namespace::UserGroups: - config_user_groups = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + config_user_groups = std::make_unique( + to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; case Namespace::UserProfile: - config_user_profile = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + config_user_profile = std::make_unique( + to_unsigned_sv({_user_sk.data(), 64}), dump, parent); return; default: break; @@ -87,18 +106,20 @@ void State::load( // Other namespaces are unique for a given pubkey_hex_ if (!pubkey_hex_) throw std::invalid_argument{ - "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; - if (pubkey_hex_->size() != 64) - throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + "load: Invalid pubkey_hex - required for group config namespaces"}; + if (pubkey_hex_->size() != 66) + throw std::invalid_argument{"load: Invalid pubkey_hex - expected 66 bytes"}; // Retrieve any keys for the group - auto user_group_info = config_user_groups->get_group(*pubkey_hex_); + std::string_view pubkey_hex = *pubkey_hex_; + auto user_group_info = config_user_groups->get_group(pubkey_hex); if (!user_group_info) - throw std::runtime_error{"Unable to retrieve group from user_groups config"}; + throw std::runtime_error{ + "Unable to retrieve group " + std::string(pubkey_hex) + " from user_groups config"}; - std::string_view pubkey_hex = *pubkey_hex_; - ustring_view pubkey = to_unsigned_sv(session_id_to_bytes(*pubkey_hex_, "03")); + auto pubkey = session_id_pk(pubkey_hex, "03"); + ustring_view pubkey_sv = to_unsigned_sv(pubkey); ustring_view user_ed25519_secretkey = {_user_sk.data(), 64}; std::optional opt_dump = dump; std::optional group_ed25519_secretkey; @@ -113,21 +134,28 @@ void State::load( "Attempted to load groups_keys config before groups_info or groups_members " "configs"}; - _config_groups[pubkey_hex] = std::make_unique(pubkey, user_ed25519_secretkey); + _config_groups[pubkey_hex] = + std::make_unique(pubkey_sv, user_ed25519_secretkey); } // Reload the specified namespace with the dump if (namespace_ == Namespace::GroupInfo) _config_groups[pubkey_hex]->config_info = - std::make_unique(pubkey, group_ed25519_secretkey, dump, parent); + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump, parent); else if (namespace_ == Namespace::GroupMembers) _config_groups[pubkey_hex]->config_members = - std::make_unique(pubkey, group_ed25519_secretkey, dump, parent); + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump, parent); else if (namespace_ == Namespace::GroupKeys) { auto info = _config_groups[pubkey_hex]->config_info.get(); auto members = _config_groups[pubkey_hex]->config_members.get(); auto keys = std::make_unique( - user_ed25519_secretkey, pubkey, pubkey, group_ed25519_secretkey, info, members, parent); + user_ed25519_secretkey, + pubkey_sv, + group_ed25519_secretkey, + dump, + info, + members, + parent); _config_groups[pubkey_hex]->config_keys = std::move(keys); } else @@ -144,103 +172,72 @@ GroupConfigs* State::group_config(std::string_view pubkey_hex) { return _config_groups[pubkey_hex].get(); } -std::vector State::merge( - std::optional pubkey_hex, const std::vector& configs) { - if (configs.empty()) - return {}; - - // Sort the namespaces based on the order they should be merged in to minimise conflicts between - // different config messages - auto sorted_configs = configs; - std::sort(sorted_configs.begin(), sorted_configs.end(), [](const auto& a, const auto& b) { - return namespace_merge_order(a.namespace_) < namespace_merge_order(b.namespace_); - }); - - std::vector good_hashes; - std::vector> pending_configs; - - for (size_t i = 0; i < sorted_configs.size(); ++i) { - auto& config = sorted_configs[i]; - - // If this is different from the last config, or it's a 'GroupKeys' config (GroupKeys - // only support individual merging) then clear 'pending_configs' so we can prepare for - // a new batch-merge - if (config.namespace_ == Namespace::GroupKeys || - (i > 0 && config.namespace_ != sorted_configs[i - 1].namespace_)) - pending_configs.clear(); - - pending_configs.emplace_back(config.hash, config.data); - - // If this is not a GroupKeys config, the last config or the next config is not in the same - // namespace then go to the next loop so we can batch-merge the configs in a later loop - if (config.namespace_ != Namespace::GroupKeys && i != (sorted_configs.size() - 1) && - config.namespace_ == sorted_configs[i + 1].namespace_) - continue; - - // Process the previously grouped configs - std::vector merged_hashes; - switch (config.namespace_) { - case Namespace::Contacts: - merged_hashes = config_contacts->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - continue; - - case Namespace::ConvoInfoVolatile: - merged_hashes = config_convo_info_volatile->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - continue; - - case Namespace::UserGroups: - merged_hashes = config_user_groups->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - continue; - - case Namespace::UserProfile: - merged_hashes = config_user_profile->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - continue; +void State::suppress_hooks_start(bool send, bool store, std::string_view pubkey_hex) { + log(LogLevel::debug, + "suppress_hooks_start: " + std::string(pubkey_hex) + "(send: " + bool_to_string(send) + + ", store: " + bool_to_string(store) + ")"); + _open_suppressions[pubkey_hex] = {send, store}; +} - default: break; +void State::suppress_hooks_stop(bool send, bool store, std::string_view pubkey_hex) { + log(LogLevel::debug, + "suppress_hooks_stop: " + std::string(pubkey_hex) + "(send: " + bool_to_string(send) + + ", store: " + bool_to_string(store) + ")"); + + // If `_open_suppressions` doesn't contain a value it'll default to {false, false} + if ((send && store) || (send && !_open_suppressions[pubkey_hex].second) || + (store && !_open_suppressions[pubkey_hex].first)) + _open_suppressions.erase(pubkey_hex); + else if (send) + _open_suppressions[pubkey_hex] = {false, _open_suppressions[pubkey_hex].second}; + else if (store) + _open_suppressions[pubkey_hex] = {_open_suppressions[pubkey_hex].first, false}; + + // Trigger the config change hooks if needed with the relevant pubkey information + if (pubkey_hex.substr(0, 2) == "05") + config_changed(std::nullopt); // User config storage + else if (pubkey_hex.empty()) { + // Update all configs (as it's possible this change affected multiple configs) + config_changed(std::nullopt); // User config storage + + for (auto& [key, val] : _config_groups) { + config_changed(key); // Group config storage } + } else + config_changed(pubkey_hex); // Key-specific configs +} - // Other namespaces are unique for a given pubkey_hex_ - if (!pubkey_hex) - throw std::invalid_argument{ - "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; - if (pubkey_hex->size() != 64) - throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; - if (!_config_groups.count(*pubkey_hex)) - throw std::runtime_error{ - "Attempted to merge group configs before for group with no config state"}; +void State::config_changed(std::optional pubkey_hex) { + std::string target_pubkey_hex; - auto info = _config_groups[*pubkey_hex]->config_info.get(); - auto members = _config_groups[*pubkey_hex]->config_members.get(); + if (!pubkey_hex) { + // Convert the _user_pk to the user's session ID + std::array user_x_pk; - if (config.namespace_ == Namespace::GroupInfo) - merged_hashes = info->merge(pending_configs); - else if (config.namespace_ == Namespace::GroupMembers) - merged_hashes = members->merge(pending_configs); - else if (config.namespace_ == Namespace::GroupKeys) { - // GroupKeys doesn't support merging multiple messages at once so do them individually - if (_config_groups[*pubkey_hex]->config_keys->load_key_message( - config.hash, config.data, config.timestamp_ms, *info, *members)) { - good_hashes.emplace_back(config.hash); - } - } else - throw std::runtime_error{"Attempted to merge from unknown namespace"}; - } + if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - return good_hashes; -} + // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to + // the sender session ID + target_pubkey_hex.reserve(66); + target_pubkey_hex += "05"; + oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey_hex)); + } else + target_pubkey_hex = *pubkey_hex; -void State::config_changed(std::optional pubkey_hex) { - throw std::runtime_error{"ASDASFSDFGSDF"}; - if (!send) + // Check if there both `send` and `store` hooks are suppressed (and if so ignore this change) + std::pair suppressions = + (_open_suppressions.count(target_pubkey_hex) ? _open_suppressions[target_pubkey_hex] + : _open_suppressions[""]); + + if (suppressions.first && suppressions.second) { + log(LogLevel::debug, "config_changed: Ignoring due to hooks being suppressed"); return; + } + std::string info_title = "User configs"; bool needs_push = false; bool needs_dump = false; - std::string target_pubkey; std::vector configs; std::chrono::milliseconds timestamp = (std::chrono::duration_cast( @@ -248,21 +245,14 @@ void State::config_changed(std::optional pubkey_hex) { network_offset); if (!pubkey_hex) { - // Convert the _user_pk to the user's session ID - std::array user_x_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) - throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - - // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to - // the sender session ID - target_pubkey.reserve(66); - target_pubkey += "05"; - oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey)); - needs_push = - (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || - config_user_groups->needs_push() || config_user_profile->needs_push()); + (!suppressions.first && + (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || + config_user_groups->needs_push() || config_user_profile->needs_push())); + needs_dump = + (!suppressions.second && + (config_contacts->needs_dump() || config_convo_info_volatile->needs_dump() || + config_user_groups->needs_dump() || config_user_profile->needs_dump())); configs = { config_contacts.get(), config_convo_info_volatile.get(), @@ -272,38 +262,86 @@ void State::config_changed(std::optional pubkey_hex) { // Other namespaces are unique for a given pubkey_hex_ if (!pubkey_hex) throw std::invalid_argument{ - "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; - - target_pubkey = *pubkey_hex; - - if (target_pubkey.size() != 64) - throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; - if (!_config_groups.count(target_pubkey)) - throw std::runtime_error{"Change trigger in group configs with no state"}; + "config_changed: Invalid pubkey_hex - required for group config namespaces"}; + if (target_pubkey_hex.size() != 66) + throw std::invalid_argument{"config_changed: Invalid pubkey_hex - expected 66 bytes"}; + if (!_config_groups.count(target_pubkey_hex)) + throw std::runtime_error{ + "config_changed: Change trigger in group configs with no state"}; // Ensure we have the admin key for the group - auto user_group_info = config_user_groups->get_group(target_pubkey); + auto user_group_info = config_user_groups->get_group(target_pubkey_hex); if (!user_group_info) - throw std::runtime_error{"Unable to retrieve group from user_groups config"}; + throw std::runtime_error{ + "config_changed: Unable to retrieve group " + target_pubkey_hex + + " from user_groups config"}; // Only group admins can push group config changes needs_push = - (!user_group_info->secretkey.empty() && - (_config_groups[target_pubkey]->config_info->needs_push() || - _config_groups[target_pubkey]->config_members->needs_push() || - _config_groups[target_pubkey]->config_keys->pending_config())); + (!suppressions.first && !user_group_info->secretkey.empty() && + (_config_groups[target_pubkey_hex]->config_info->needs_push() || + _config_groups[target_pubkey_hex]->config_members->needs_push() || + _config_groups[target_pubkey_hex]->config_keys->pending_config())); + needs_dump = + (!suppressions.second && + (_config_groups[target_pubkey_hex]->config_info->needs_dump() || + _config_groups[target_pubkey_hex]->config_members->needs_dump() || + _config_groups[target_pubkey_hex]->config_keys->needs_dump())); configs = { - _config_groups[target_pubkey]->config_info.get(), - _config_groups[target_pubkey]->config_members.get()}; + _config_groups[target_pubkey_hex]->config_info.get(), + _config_groups[target_pubkey_hex]->config_members.get()}; + info_title = "Group configs for " + target_pubkey_hex; + } + + std::string send_info = + (suppressions.first ? "send suppressed" + : ("needs send: " + bool_to_string(needs_push))); + std::string store_info = + (suppressions.second ? "store suppressed" + : ("needs store: " + bool_to_string(needs_dump))); + log(LogLevel::debug, + "config_changed: " + info_title + " (" + send_info + ", " + store_info + ")"); + + // Call the hook to store the dump if needed + if (_store && needs_dump && !suppressions.second) { + for (auto& config : configs) { + if (!config->needs_dump()) + continue; + log(LogLevel::debug, + "config_changed: call 'store' for namespace: " + + std::to_string(static_cast(config->storage_namespace()))); + _store(config->storage_namespace(), + target_pubkey_hex, + timestamp.count(), + config->dump()); + } + + // GroupKeys needs special handling as it's not a `ConfigBase` + if (pubkey_hex && _config_groups[target_pubkey_hex]->config_keys->needs_dump()) { + log(LogLevel::debug, + "config_changed: Group Keys config for " + target_pubkey_hex + " needs_dump"); + auto keys_config = _config_groups[target_pubkey_hex]->config_keys.get(); + + _store(keys_config->storage_namespace(), + target_pubkey_hex, + timestamp.count(), + keys_config->dump()); + } } // Call the hook to perform a push if needed - if (needs_push) { + if (_send && needs_push && !suppressions.first) { + std::vector seqnos; std::vector requests; std::vector obsolete_hashes; for (auto& config : configs) { + if (!config->needs_push()) + continue; + log(LogLevel::debug, + "config_changed: generate 'send' request for namespace: " + + std::to_string(static_cast(config->storage_namespace()))); auto [seqno, msg, obs] = config->push(); for (auto hash : obs) @@ -312,9 +350,10 @@ void State::config_changed(std::optional pubkey_hex) { // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; - ustring verification = to_unsigned("store") + - static_cast(config->storage_namespace()) + - static_cast(timestamp.count()); + ustring verification = to_unsigned("store"); + verification += + to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); if (0 != crypto_sign_ed25519_detached( sig.data(), @@ -322,11 +361,12 @@ void State::config_changed(std::optional pubkey_hex) { verification.data(), verification.size(), _user_sk.data())) - throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ {"namespace", static_cast(config->storage_namespace())}, - {"pubkey", target_pubkey}, + {"pubkey", target_pubkey_hex}, {"ttl", config->default_ttl().count()}, {"timestamp", timestamp.count()}, {"data", oxenc::to_base64(msg)}, @@ -337,21 +377,24 @@ void State::config_changed(std::optional pubkey_hex) { if (!pubkey_hex) params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + seqnos.emplace_back(seqno); requests.emplace_back(params); } // GroupKeys needs special handling as it's not a `ConfigBase` if (pubkey_hex) { - auto pending = _config_groups[target_pubkey]->config_keys->pending_config(); + auto pending = _config_groups[target_pubkey_hex]->config_keys->pending_config(); if (pending) { + log(LogLevel::debug, + "config_changed: generate 'send' request for group keys " + target_pubkey_hex); // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; ustring verification = to_unsigned("store") + - static_cast( - _config_groups[target_pubkey]->config_keys->storage_namespace()) + + static_cast(_config_groups[target_pubkey_hex] + ->config_keys->storage_namespace()) + static_cast(timestamp.count()); if (0 != crypto_sign_ed25519_detached( @@ -360,13 +403,15 @@ void State::config_changed(std::optional pubkey_hex) { verification.data(), verification.size(), _user_sk.data())) - throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ {"namespace", - _config_groups[target_pubkey]->config_keys->storage_namespace()}, - {"pubkey", target_pubkey}, - {"ttl", _config_groups[target_pubkey]->config_keys->default_ttl().count()}, + _config_groups[target_pubkey_hex]->config_keys->storage_namespace()}, + {"pubkey", target_pubkey_hex}, + {"ttl", + _config_groups[target_pubkey_hex]->config_keys->default_ttl().count()}, {"timestamp", timestamp.count()}, {"data", oxenc::to_base64(*pending)}, {"signature", oxenc::to_base64(sig.begin(), sig.end())}, @@ -383,11 +428,11 @@ void State::config_changed(std::optional pubkey_hex) { namespace_store_order(static_cast(b["namespace"])); }); - nlohmann::json payload; + nlohmann::json sequence_params; for (auto& request : sorted_requests) { nlohmann::json request_json{{"method", "store"}, {"params", request}}; - payload["requests"].push_back(request_json); + sequence_params["requests"].push_back(request_json); } // Also delete obsolete hashes @@ -395,7 +440,7 @@ void State::config_changed(std::optional pubkey_hex) { // Ed25519 signature of `("delete" || messages...)` std::array sig; ustring verification = to_unsigned("delete"); - + log(LogLevel::debug, "config_changed: has obsolete hashes"); for (auto& hash : obsolete_hashes) verification += to_unsigned_sv(hash); @@ -405,12 +450,12 @@ void State::config_changed(std::optional pubkey_hex) { verification.data(), verification.size(), _user_sk.data())) - throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ {"messages", obsolete_hashes}, - {"pubkey", target_pubkey}, - {"timestamp", timestamp.count()}, + {"pubkey", target_pubkey_hex}, {"signature", oxenc::to_base64(sig.begin(), sig.end())}, }; @@ -419,10 +464,142 @@ void State::config_changed(std::optional pubkey_hex) { params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); nlohmann::json request_json{{"method", "delete"}, {"params", params}}; - payload["requests"].push_back(request_json); + sequence_params["requests"].push_back(request_json); } + log(LogLevel::debug, "config_changed: Call 'send'"); + nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; + auto payload_dump = payload.dump(); + _send(target_pubkey_hex, seqnos, {to_unsigned(payload_dump.data()), payload_dump.size()}); + } + log(LogLevel::debug, "config_changed: Complete"); +} + +std::vector State::merge( + std::optional pubkey_hex, const std::vector& configs) { + log(LogLevel::debug, "merge: Called with " + std::to_string(configs.size()) + " configs"); + if (configs.empty()) + return {}; - send(target_pubkey, payload); + // Sort the namespaces based on the order they should be merged in to minimise conflicts between + // different config messages + auto sorted_configs = configs; + std::sort(sorted_configs.begin(), sorted_configs.end(), [](const auto& a, const auto& b) { + return namespace_merge_order(a.namespace_) < namespace_merge_order(b.namespace_); + }); + + bool is_group_merge = false; + std::vector good_hashes; + std::vector> pending_configs; + std::string target_pubkey_hex; + + if (!pubkey_hex) { + // Convert the _user_pk to the user's session ID + std::array user_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) + throw std::runtime_error{ + "merge: Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + + // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to + // the sender session ID + target_pubkey_hex.reserve(66); + target_pubkey_hex += "05"; + oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey_hex)); + } else + target_pubkey_hex = *pubkey_hex; + + // Suppress triggering the `send` hook until the merge is complete + suppress_hooks_start(true, false, target_pubkey_hex); + + for (size_t i = 0; i < sorted_configs.size(); ++i) { + auto& config = sorted_configs[i]; + + // If this is different from the last config, or it's a 'GroupKeys' config (GroupKeys + // only support individual merging) then clear 'pending_configs' so we can prepare for + // a new batch-merge + if (config.namespace_ == Namespace::GroupKeys || + (i > 0 && config.namespace_ != sorted_configs[i - 1].namespace_)) + pending_configs.clear(); + + pending_configs.emplace_back(config.hash, config.data); + + // If this is not a GroupKeys config, the last config or the next config is not in the same + // namespace then go to the next loop so we can batch-merge the configs in a later loop + if (config.namespace_ != Namespace::GroupKeys && i != (sorted_configs.size() - 1) && + config.namespace_ == sorted_configs[i + 1].namespace_) + continue; + + // Process the previously grouped configs + std::vector merged_hashes; + switch (config.namespace_) { + case Namespace::Contacts: + log(LogLevel::debug, "merge: Merging CONTACTS config"); + merged_hashes = config_contacts->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::ConvoInfoVolatile: + log(LogLevel::debug, "merge: Merging CONVO_INFO_VOLATILE config"); + merged_hashes = config_convo_info_volatile->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::UserGroups: + log(LogLevel::debug, "merge: Merging USER_GROUPS config"); + merged_hashes = config_user_groups->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + case Namespace::UserProfile: + log(LogLevel::debug, "merge: Merging USER_PROFILE config"); + merged_hashes = config_user_profile->merge(pending_configs); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + continue; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex) + throw std::invalid_argument{ + "merge: Invalid pubkey_hex - required for group config namespaces"}; + if (target_pubkey_hex.size() != 66) + throw std::invalid_argument{"merge: Invalid pubkey_hex - expected 66 bytes"}; + if (!_config_groups.count(target_pubkey_hex)) + throw std::runtime_error{ + "merge: Attempted to merge group configs before for group with no config " + "state"}; + + auto info = _config_groups[target_pubkey_hex]->config_info.get(); + auto members = _config_groups[target_pubkey_hex]->config_members.get(); + is_group_merge = true; + + if (config.namespace_ == Namespace::GroupInfo) { + log(LogLevel::debug, + "merge: Merging GROUP_INFO config for: " + std::string(target_pubkey_hex)); + merged_hashes = info->merge(pending_configs); + } else if (config.namespace_ == Namespace::GroupMembers) { + log(LogLevel::debug, + "merge: Merging GROUP_MEMBERS config for: " + std::string(target_pubkey_hex)); + merged_hashes = members->merge(pending_configs); + } else if (config.namespace_ == Namespace::GroupKeys) { + log(LogLevel::debug, + "merge: Merging GROUP_KEYS config for: " + std::string(target_pubkey_hex)); + // GroupKeys doesn't support merging multiple messages at once so do them individually + if (_config_groups[target_pubkey_hex]->config_keys->load_key_message( + config.hash, config.data, config.timestamp_ms, *info, *members)) { + good_hashes.emplace_back(config.hash); + } + } else + throw std::runtime_error{"merge: Attempted to merge from unknown namespace"}; + } + + // Now that all of the merges have been completed we stop suppressing the `send` hook which + // will be triggered if there is a pending push + suppress_hooks_stop(true, false, target_pubkey_hex); + + log(LogLevel::debug, "merge: Complete"); + return good_hashes; } ustring State::dump(bool full_dump) { @@ -499,243 +676,144 @@ ustring State::dump(config::Namespace namespace_, std::optional seqnos, ustring payload, ustring response) { + log(LogLevel::debug, "received_send_response: Called"); + auto response_json = nlohmann::json::parse(response); -namespace { -State& unbox(state_object* state) { - assert(state && state->internals); - return *static_cast(state->internals); -} -const State& unbox(const state_object* state) { - assert(state && state->internals); - return *static_cast(state->internals); -} + if (!response_json.contains("results")) + throw std::invalid_argument{"Invalid response: expected to contain 'results' array"}; + if (response_json["results"].size() == 0) + throw std::invalid_argument{"Invalid response: 'results' array is empty"}; -bool set_error(state_object* state, std::string_view e) { - if (e.size() > 255) - e.remove_suffix(e.size() - 255); - std::memcpy(state->_error_buf, e.data(), e.size()); - state->_error_buf[e.size()] = 0; - state->last_error = state->_error_buf; - return false; -} -} // namespace + auto results = response_json["results"]; -extern "C" { + // Check if all of the results has the same status code + int single_status_code = -1; + std::optional error_body; + for (const auto& result : results.items()) { + if (!result.value().contains("code")) + throw std::invalid_argument{ + "Invalid result: expected to contain 'code'" + result.value().dump()}; -LIBSESSION_EXPORT void state_free(state_object* state) { - delete state; -} + // If the code was different from all former codes then break the loop + auto code = result.value()["code"].get(); -LIBSESSION_C_API bool state_create(state_object** state, char* error) { - try { - auto s = std::make_unique(); - auto s_object = std::make_unique(); - - s_object->internals = s.release(); - s_object->last_error = nullptr; - *state = s_object.release(); - return true; - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); + if (single_status_code != -1 && code != single_status_code) { + single_status_code = 200; + error_body = std::nullopt; + break; } - return false; - } catch (...) { - return false; - } -} -LIBSESSION_C_API bool state_init( - state_object** state, const unsigned char* ed25519_secretkey_bytes, char* error) { - try { - auto s = std::make_unique( - session::ustring_view{ed25519_secretkey_bytes, 64}); - auto s_object = std::make_unique(); - - s_object->internals = s.release(); - s_object->last_error = nullptr; - *state = s_object.release(); - return true; - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return false; - } -} + single_status_code = code; -LIBSESSION_C_API bool state_load( - state_object* state, - NAMESPACE namespace_, - const char* pubkey_hex_, - const unsigned char* dump, - size_t dumplen) { - assert(state && dump && dumplen); - - session::ustring_view dumped{dump, dumplen}; - std::optional pubkey_hex; - if (pubkey_hex_) - pubkey_hex.emplace(pubkey_hex_, 64); - - try { - auto target_namespace = static_cast(namespace_); - - unbox(state).load(target_namespace, pubkey_hex, dumped); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); + if (result.value().contains("body") && result.value()["body"].is_string()) + error_body = result.value()["body"].get(); } -} -LIBSESSION_C_API void state_set_send_callback( - state_object* state, void (*callback)(const char*, const unsigned char*, size_t)) { - if (!callback) - unbox(state).logger = nullptr; - else { - unbox(state).send = [callback](std::string pubkey, ustring data) { - callback(pubkey.c_str(), data.data(), data.size()); - }; - } -} + // Throw if all results failed with the same error + if (single_status_code < 200 || single_status_code > 299) { + auto error = "Failed with status code: " + std::to_string(single_status_code) + "."; -LIBSESSION_C_API config_string_list* state_merge( - state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count) { - std::optional pubkey_hex; - if (pubkey_hex_) - pubkey_hex.emplace(pubkey_hex_, 64); + // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just in + // case) + if (single_status_code == 406 || single_status_code == 425) + error = "The user's clock is out of sync with the service node network."; + else if (single_status_code == 401) + error = "Unauthorised (sinature verification failed)."; - std::vector confs; - confs.reserve(count); + if (error_body) + error += " Server error: " + *error_body + "."; - for (size_t i = 0; i < count; i++) - confs.emplace_back( - static_cast(configs[i].namespace_), - configs[i].hash, - configs[i].timestamp_ms, - ustring{configs[i].data, configs[i].datalen}); + throw std::runtime_error{error}; + } + log(LogLevel::debug, "received_send_response: Doesn't have a consistent error across requests"); - return make_string_list(unbox(state).merge(pubkey_hex, confs)); -} + // If the response includes a timestamp value then we should update the network offset + if (auto first_result = results[0]; + first_result.contains("body") && first_result["body"].contains("t")) + network_offset = + (std::chrono::milliseconds(first_result["body"]["t"].get()) - + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch())); -LIBSESSION_C_API void state_dump( - state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { - assert(out && outlen); - auto data = unbox(state).dump(full_dump); - *outlen = data.size(); - *out = static_cast(std::malloc(data.size())); - std::memcpy(*out, data.data(), data.size()); -} + // The 'results' array will be in the same order as the requests sent within 'payload' so + // iterate through them both and mark any successful request as pushed + auto payload_json = nlohmann::json::parse(payload); -LIBSESSION_C_API void state_dump_namespace( - state_object* state, - NAMESPACE namespace_, - const char* pubkey_hex_, - unsigned char** out, - size_t* outlen) { - assert(out && outlen); - - std::optional pubkey_hex; - if (pubkey_hex_) - pubkey_hex.emplace(pubkey_hex_, 64); - - auto target_namespace = static_cast(namespace_); - auto data = unbox(state).dump(target_namespace, pubkey_hex); - *outlen = data.size(); - *out = static_cast(std::malloc(data.size())); - std::memcpy(*out, data.data(), data.size()); -} + if (!payload_json.contains("params") || !payload_json["params"].contains("requests")) + throw std::invalid_argument{ + "Invalid payload: expected to contain 'params.requests' array."}; + if (payload_json["params"]["requests"].size() == 0) + throw std::invalid_argument{"Invalid payload: 'params.requests' array is empty."}; -// User Profile Functions + auto requests = payload_json["params"]["requests"]; + auto num_configs = requests.size(); -LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { - if (auto s = unbox(state).get_profile_name()) - return s->data(); - return nullptr; -} + // Subtract one if we also had a 'delete' request to remove obsolete hashes + if (requests[requests.size() - 1].contains("method") && + requests[requests.size() - 1]["method"].get() == "delete") + num_configs -= 1; -LIBSESSION_C_API bool state_set_profile_name(state_object* state, const char* name) { - try { - unbox(state).set_profile_name(name); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} + if (seqnos.size() != num_configs) + throw std::invalid_argument{ + "Invalid seqnos: Size doesn't match the number of config changes."}; -LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { - user_profile_pic p; - if (auto pic = unbox(state).get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; - } - return p; -} + log(LogLevel::debug, "received_send_response: Confirming pushed"); + for (int i = 0, n = results.size(); i < n; ++i) { + auto result_code = results[i]["code"].get(); -LIBSESSION_C_API bool state_set_profile_pic(state_object* state, user_profile_pic pic) { - std::string_view url{pic.url}; - ustring_view key; - if (!url.empty()) - key = {pic.key, 32}; - - try { - unbox(state).set_profile_pic(url, key); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} + if (result_code < 200 || result_code > 299) + continue; + if (!results[i].contains("body")) + continue; + if (!results[i]["body"].contains("hash")) + continue; + if (!payload_json["params"]["requests"][i].contains("params")) + continue; + if (!payload_json["params"]["requests"][i]["params"].contains("namespace")) + continue; -LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { - if (auto opt = unbox(state).get_profile_blinded_msgreqs()) - return static_cast(*opt); - return -1; -} + auto hash = results[i]["body"]["hash"].get(); + auto namespace_ = + payload_json["params"]["requests"][i]["params"]["namespace"].get(); -LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int enabled) { - std::optional val; - if (enabled >= 0) - val = static_cast(enabled); - unbox(state).set_profile_blinded_msgreqs(std::move(val)); -} + switch (namespace_) { + case Namespace::Contacts: config_contacts->confirm_pushed(seqnos[i], hash); continue; + case Namespace::ConvoInfoVolatile: + config_convo_info_volatile->confirm_pushed(seqnos[i], hash); + continue; + case Namespace::UserGroups: + config_user_groups->confirm_pushed(seqnos[i], hash); + continue; + case Namespace::UserProfile: + config_user_profile->confirm_pushed(seqnos[i], hash); + continue; + default: break; + } -LIBSESSION_C_API void state_set_logger( - state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx) { - if (!callback) - unbox(state).logger = nullptr; - else { - unbox(state).config_contacts->logger = [callback, ctx]( - session::config::LogLevel lvl, - std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; - unbox(state).config_convo_info_volatile->logger = [callback, ctx]( - session::config::LogLevel lvl, - std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; - unbox(state).config_user_groups->logger = [callback, ctx]( - session::config::LogLevel lvl, - std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; - unbox(state).config_user_profile->logger = [callback, ctx]( - session::config::LogLevel lvl, - std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; + // Other namespaces are unique for a given pubkey + if (!payload_json["params"]["requests"][i]["params"].contains("pubkey")) + throw std::invalid_argument{ + "Invalid payload: Group config change was missing 'pubkey' param."}; + + auto pubkey = payload_json["params"]["requests"][i]["params"]["pubkey"].get(); + if (pubkey.size() != 66) + throw std::invalid_argument{"Invalid pubkey: expected 66 characters"}; + if (!_config_groups.count(pubkey)) + throw std::runtime_error{"received_send_response: Unable to retrieve group"}; + + // Retrieve the group configs for this pubkey + auto group_configs = _config_groups[pubkey].get(); + + switch (namespace_) { + case Namespace::GroupInfo: group_configs->config_info->confirm_pushed(seqnos[i], hash); + case Namespace::GroupMembers: + group_configs->config_members->confirm_pushed(seqnos[i], hash); + case Namespace::GroupKeys: continue; // No need to do anything here + default: throw std::runtime_error{"Attempted to load unknown namespace"}; + } } + log(LogLevel::debug, "received_send_response: Completed"); } - -} // extern "C" +} // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp new file mode 100644 index 00000000..1f770c46 --- /dev/null +++ b/src/state_c_wrapper.cpp @@ -0,0 +1,399 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "config/internal.hpp" +#include "session/config/base.hpp" +#include "session/config/contacts.hpp" +#include "session/config/convo_info_volatile.hpp" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" +#include "session/config/user_groups.hpp" +#include "session/config/user_profile.hpp" +#include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" +#include "session/util.hpp" + +using namespace std::literals; +using namespace session; +using namespace session::config; +using namespace session::state; + +namespace { +State& unbox(state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +const State& unbox(const state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} + +bool set_error(state_object* state, std::string_view e) { + if (e.size() > 255) + e.remove_suffix(e.size() - 255); + std::memcpy(state->_error_buf, e.data(), e.size()); + state->_error_buf[e.size()] = 0; + state->last_error = state->_error_buf; + return false; +} +} // namespace + +extern "C" { + +LIBSESSION_EXPORT void state_free(state_object* state) { + delete state; +} + +LIBSESSION_C_API bool state_create(state_object** state, char* error) { + try { + auto s = std::make_unique(); + auto s_object = std::make_unique(); + + s_object->internals = s.release(); + s_object->last_error = nullptr; + *state = s_object.release(); + return true; + } catch (const std::exception& e) { + if (error) { + std::string msg = e.what(); + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + } + return false; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool state_init( + state_object** state, + const unsigned char* ed25519_secretkey_bytes, + state_namespaced_dump* dumps_, + size_t count, + char* error) { + try { + std::vector dumps = {}; + dumps.reserve(count); + + for (size_t i = 0; i < count; i++) { + std::optional pubkey_hex; + + if (dumps_[i].pubkey_hex) + pubkey_hex.emplace(dumps_[i].pubkey_hex, 66); + + dumps.emplace_back( + static_cast(dumps_[i].namespace_), + pubkey_hex, + ustring{dumps_[i].data, dumps_[i].datalen}); + } + + auto s = std::make_unique( + session::ustring_view{ed25519_secretkey_bytes, 64}, dumps); + auto s_object = std::make_unique(); + + s_object->internals = s.release(); + s_object->last_error = nullptr; + *state = s_object.release(); + return true; + } catch (const std::exception& e) { + if (error) { + std::string msg = e.what(); + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + } + return false; + } +} + +LIBSESSION_C_API bool state_load( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + const unsigned char* dump, + size_t dumplen) { + assert(state && dump && dumplen); + + session::ustring_view dumped{dump, dumplen}; + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + try { + auto target_namespace = static_cast(namespace_); + + unbox(state).load(target_namespace, pubkey_hex, dumped); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_set_logger( + state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx) { + if (!callback) + unbox(state).logger = nullptr; + else { + unbox(state).logger = [callback, ctx](session::config::LogLevel lvl, std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + } +} + +LIBSESSION_C_API bool state_set_send_callback( + state_object* state, + void (*callback)(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*), + void* ctx) { + try { + if (!callback) + unbox(state).onSend(nullptr); + else { + // Setting this can result in the callback being immediately triggered which could throw + unbox(state).onSend( + [callback, ctx](std::string pubkey, std::vector seqnos, ustring data) { + callback( + pubkey.c_str(), + seqnos.data(), + seqnos.size(), + data.data(), + data.size(), + ctx); + }); + } + + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_set_store_callback( + state_object* state, + void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), + void* ctx) { + try { + if (!callback) + unbox(state).onStore(nullptr); + else { + // Setting this can result in the callback being immediately triggered which could throw + unbox(state).onStore([callback, ctx]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + callback( + static_cast(namespace_), + pubkey.c_str(), + timestamp_ms, + data.data(), + data.size(), + ctx); + }); + } + + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_set_service_node_offset(state_object* state, int64_t offset_ms) { + unbox(state).network_offset = std::chrono::milliseconds(offset_ms); +} + +LIBSESSION_C_API int64_t state_network_offset(state_object* state) { + return unbox(state).network_offset.count(); +} + +LIBSESSION_C_API bool state_suppress_hooks_start( + state_object* state, bool send, bool store, const char* pubkey_hex_) { + try { + std::string_view pubkey_hex = ""; + if (pubkey_hex_) + pubkey_hex = {pubkey_hex_, 66}; + + unbox(state).suppress_hooks_start(send, store, pubkey_hex); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_suppress_hooks_stop( + state_object* state, bool send, bool store, const char* pubkey_hex_) { + try { + std::string_view pubkey_hex = ""; + if (pubkey_hex_) + pubkey_hex = {pubkey_hex_, 66}; + + unbox(state).suppress_hooks_stop(send, store, pubkey_hex); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_merge( + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + config_string_list** successful_hashes) { + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + std::vector confs; + confs.reserve(count); + + for (size_t i = 0; i < count; i++) + confs.emplace_back( + static_cast(configs[i].namespace_), + configs[i].hash, + configs[i].timestamp_ms, + ustring{configs[i].data, configs[i].datalen}); + + auto result = unbox(state).merge(pubkey_hex, confs); + unbox(state).logger(LogLevel::info, "Merged " + std::to_string(result.size())); + *successful_hashes = make_string_list(result); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_dump( + state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { + try { + assert(out && outlen); + auto data = unbox(state).dump(full_dump); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_dump_namespace( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen) { + assert(out && outlen); + + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto target_namespace = static_cast(namespace_); + auto data = unbox(state).dump(target_namespace, pubkey_hex); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_received_send_response( + state_object* state, + const char* pubkey_hex, + const seqno_t* seqnos_, + size_t seqnos_len, + unsigned char* payload_data, + size_t payload_data_len, + unsigned char* response_data, + size_t response_data_len) { + try { + std::vector seqnos; + seqnos.reserve(seqnos_len); + + for (size_t i = 0; i < seqnos_len; i++) + seqnos.emplace_back(seqnos_[i]); + + unbox(state).received_send_response( + {pubkey_hex, 66}, + seqnos, + {payload_data, payload_data_len}, + {response_data, response_data_len}); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +// User Profile Functions + +LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { + if (auto s = unbox(state).config_user_profile->get_name()) + return s->data(); + return nullptr; +} + +LIBSESSION_C_API bool state_set_profile_name(state_object* state, const char* name) { + try { + unbox(state).logger(LogLevel::info, "state_set_profile_name: called"); + unbox(state).config_user_profile->set_name(name); + unbox(state).logger(LogLevel::info, "state_set_profile_name: done"); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { + user_profile_pic p; + if (auto pic = unbox(state).config_user_profile->get_profile_pic(); pic) { + copy_c_str(p.url, pic.url); + std::memcpy(p.key, pic.key.data(), 32); + } else { + p.url[0] = 0; + } + return p; +} + +LIBSESSION_C_API bool state_set_profile_pic(state_object* state, user_profile_pic pic) { + std::string_view url{pic.url}; + ustring_view key; + if (!url.empty()) + key = {pic.key, 32}; + + try { + unbox(state).logger(LogLevel::info, "state_set_profile_pic: called"); + unbox(state).config_user_profile->set_profile_pic(url, key); + unbox(state).logger(LogLevel::info, "state_set_profile_pic: done"); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { + if (auto opt = unbox(state).config_user_profile->get_blinded_msgreqs()) + return static_cast(*opt); + return -1; +} + +LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int enabled) { + std::optional val; + if (enabled >= 0) + val = static_cast(enabled); + unbox(state).config_user_profile->set_blinded_msgreqs(std::move(val)); +} + +} // extern "C" \ No newline at end of file From 4fe1e447ed5233e361a1c83332f20fa9452e1a24 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 2 Feb 2024 13:04:27 +1100 Subject: [PATCH 04/24] Added current_hashes, made send hook more generic --- include/session/config/namespaces.hpp | 22 +++ include/session/state.h | 52 +++--- include/session/state.hpp | 48 +++--- include/session/util.hpp | 3 + src/state.cpp | 233 ++++++++++++++++---------- src/state_c_wrapper.cpp | 67 ++++---- tests/test_state.cpp | 90 +++++++++- 7 files changed, 334 insertions(+), 181 deletions(-) diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index ec7ef396..93de4995 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace session::config { @@ -17,9 +18,30 @@ enum class Namespace : std::int16_t { GroupKeys = 12, GroupInfo = 13, GroupMembers = 14, + + // Messages sent to an updated group which should be able to be retrieved by revoked members are + // stored in this namespace + RevokedRetrievableGroupMessages = -11, }; namespace { + std::string namespace_name(const Namespace& n) { + switch (n) { + case Namespace::UserProfile: return "USER_PROFILE"; + case Namespace::Contacts: return "CONTACTS"; + case Namespace::ConvoInfoVolatile: return "CONVO_INFO_VOLATILE"; + case Namespace::UserGroups: return "USER_GROUPS"; + + case Namespace::GroupMessages: return "GroupMessages"; + case Namespace::GroupKeys: return "GROUP_KEYS"; + case Namespace::GroupInfo: return "GROUP_INFO"; + case Namespace::GroupMembers: return "GROUP_MEMBERS"; + + case Namespace::RevokedRetrievableGroupMessages: + return "RevokedRetrievableGroupMessages"; + } + } + /// Returns a number indicating the order that the config dumps should be loaded in, we need to /// load the `UserGroups` config before any group configs (due to how the configs are stored) /// and the `GroupKeys` config _after_ the `GroupInfo` and `GroupMembers` configs as it requires diff --git a/include/session/state.h b/include/session/state.h index cc7794a6..9f5d7402 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -146,7 +146,7 @@ LIBSESSION_EXPORT void state_set_logger( /// /// The function must have signature: /// -/// void callback(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*); +/// void callback(const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*); /// /// Can be called with callback set to NULL to clear an existing hook. /// @@ -156,7 +156,8 @@ LIBSESSION_EXPORT void state_set_logger( /// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused LIBSESSION_EXPORT bool state_set_send_callback( state_object* state, - void (*callback)(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*), + void (*callback)( + const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*), void* ctx); /// API: state/state_set_store_callback @@ -253,6 +254,19 @@ LIBSESSION_EXPORT bool state_merge( size_t count, config_string_list** successful_hashes); +/// API: state/state_current_hashes +/// +/// The current config hashes; this can be empty if the current hashes are unknown or the current +/// state is not clean (i.e. a push is needed or pending). +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 +/// bytes). Required for group hashes. +/// - `current_hashes` -- [out] Pointer to an array of the current config hashes +LIBSESSION_EXPORT bool state_current_hashes( + state_object* state, const char* pubkey_hex_, config_string_list** current_hashes); + /// API: state/state_dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for @@ -304,34 +318,26 @@ LIBSESSION_EXPORT bool state_dump_namespace( /// API: state/state_received_send_response /// -/// Takes the network response from sending the data from the `send` hook and confirms the configs -/// were successfully pushed. +/// Takes the network respons and request context from sending the data from the `send` hook and +/// processes the response updating the state as needed. /// /// Inputs: /// - `state` -- [in] Pointer to state_object object /// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 /// bytes). Required for group dumps. -/// - `seqnos` -- [in] Pointer to an array of sequence numbers for each config which was sent. Must -/// be in the same order the push data was in. Can just pass the pointer which was provided from the -/// `send` hook. -/// - `seqnos_len` -- [in] Number of items in `seqnos`. -/// - `payload_data` -- [in] Pointer to the push data payload that resulted in this response. Can -/// just pass the pointer which was provided from the `send` hook. -/// - `payload_data_len` -- [in] Length of the `payload_data`. /// - `response_data` -- [in] Pointer to the response from the swarm after sending the /// `payload_data`. /// - `response_data_len` -- [in] Length of the `response_data`. -/// - `out` -- [out] Pointer to the output location -/// - `outlen` -- [out] Length of output +/// - `request_ctx` -- [in] Pointer to the request context data which was provided by the `send` +/// hook. +/// - `request_ctx_len` -- [in] Length of the `request_ctx`. LIBSESSION_EXPORT bool state_received_send_response( state_object* state, const char* pubkey_hex, - const seqno_t* seqnos, - size_t seqnos_len, - unsigned char* payload_data, - size_t payload_data_len, unsigned char* response_data, - size_t response_data_len); + size_t response_data_len, + unsigned char* request_ctx, + size_t request_ctx_len); /// User Profile functions @@ -356,10 +362,7 @@ LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); /// Inputs: /// - `state` -- [in] Pointer to the state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `bool` -- Returns true on success, false on error -LIBSESSION_EXPORT bool state_set_profile_name(state_object* state, const char* name); +LIBSESSION_EXPORT void state_set_profile_name(state_object* state, const char* name); /// API: state/state_get_profile_pic /// @@ -381,10 +384,7 @@ LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* sta /// Inputs: /// - `state` -- [in] Pointer to the satet object /// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `bool` -- Returns true on success, false on error -LIBSESSION_EXPORT bool state_set_profile_pic(state_object* state, user_profile_pic pic); +LIBSESSION_EXPORT void state_set_profile_pic(state_object* state, user_profile_pic pic); /// API: state/state_get_profile_blinded_msgreqs /// diff --git a/include/session/state.hpp b/include/session/state.hpp index d98d612b..cf0b73cb 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -97,13 +97,7 @@ class State { uint64_t timestamp_ms, ustring data)> _store; - std::function seqnos, ustring data)> _send; - - // Invokes the `logger` callback if set, does nothing if there is no logger. - void log(session::config::LogLevel lvl, std::string msg) { - if (logger) - logger(lvl, std::move(msg)); - } + std::function _send; public: std::unique_ptr config_contacts; @@ -132,6 +126,12 @@ class State { // If set then we log things by calling this callback std::function logger; + // Invokes the `logger` callback if set, does nothing if there is no logger. + void log(session::config::LogLevel lvl, std::string msg) { + if (logger) + logger(lvl, std::move(msg)); + } + // Hook which will be called whenever config dumps need to be saved to persistent storage. The // hook will immediately be called upon assignment if the state needs to be stored. void onStore(std::function< @@ -154,11 +154,9 @@ class State { /// /// Parameters: /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. - /// - `seqnos` -- a vector of the seqnos for the each updated config message included in the - /// payload. - /// - `data` -- payload which should be sent to the API. - void onSend(std::function seqnos, ustring data)> - hook) { + /// - `payload` -- payload which should be sent to the API. + /// - `ctx` -- contextual data which should be used when processing the response. + void onSend(std::function hook) { _send = hook; if (!hook) @@ -267,6 +265,20 @@ class State { std::vector merge( std::optional pubkey_hex, const std::vector& configs); + /// API: state/State::current_hashes + /// + /// The current config hashes; this can be empty if the current hashes are unknown or the + /// current state is not clean (i.e. a push is needed or pending). + /// + /// Inputs: + /// - `pubkey_hex` -- optional pubkey to retrieve the hashes for (in hex, with prefix - 66 + /// bytes). Required for group hashes. + /// + /// Outputs: + /// - `std::vector` -- Returns current config hashes + std::vector current_hashes( + std::optional pubkey_hex = std::nullopt); + /// API: state/State::dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for @@ -308,14 +320,12 @@ class State { /// Inputs: /// - `pubkey` -- the pubkey (in hex, with prefix - 66 bytes) for the swarm where the data was /// sent. - /// - `seqnos` -- the seqnos for each config messages included in the payload. - /// - `payload_data` -- payload which was sent to the swarm. /// - `response_data` -- response that was returned from the swarm. - void received_send_response( - std::string pubkey, - std::vector seqnos, - ustring payload_data, - ustring response_data); + /// - `ctx` -- the contextual data provided by the onSend hook. + void received_send_response(std::string pubkey, ustring response_data, ustring ctx); + + private: + void handle_config_push_response(std::string pubkey, ustring response, ustring ctx); }; }; // namespace session::state diff --git a/include/session/util.hpp b/include/session/util.hpp index 67e03260..d3e06d4f 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -27,6 +27,9 @@ inline const unsigned char* to_unsigned(const std::byte* x) { inline unsigned char* to_unsigned(std::byte* x) { return reinterpret_cast(x); } +inline ustring to_unsigned(std::string x) { + return {to_unsigned(x.data()), x.size()}; +} // These do nothing, but having them makes template metaprogramming easier: inline const unsigned char* to_unsigned(const unsigned char* x) { return x; diff --git a/src/state.cpp b/src/state.cpp index 3595931e..2385fdaa 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -27,6 +27,10 @@ using namespace session::config; namespace session::state { +enum class RequestType : std::uint8_t { + ConfigPush = 2, +}; + GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { auto info = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); auto members = @@ -210,7 +214,7 @@ void State::suppress_hooks_stop(bool send, bool store, std::string_view pubkey_h void State::config_changed(std::optional pubkey_hex) { std::string target_pubkey_hex; - if (!pubkey_hex) { + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { // Convert the _user_pk to the user's session ID std::array user_x_pk; @@ -244,7 +248,7 @@ void State::config_changed(std::optional pubkey_hex) { std::chrono::system_clock::now().time_since_epoch()) + network_offset); - if (!pubkey_hex) { + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { needs_push = (!suppressions.first && (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || @@ -262,7 +266,7 @@ void State::config_changed(std::optional pubkey_hex) { // Other namespaces are unique for a given pubkey_hex_ if (!pubkey_hex) throw std::invalid_argument{ - "config_changed: Invalid pubkey_hex - required for group config namespaces"}; + "config_changed: Invalid pubkey_hex - required for group config changes"}; if (target_pubkey_hex.size() != 66) throw std::invalid_argument{"config_changed: Invalid pubkey_hex - expected 66 bytes"}; if (!_config_groups.count(target_pubkey_hex)) @@ -309,8 +313,7 @@ void State::config_changed(std::optional pubkey_hex) { if (!config->needs_dump()) continue; log(LogLevel::debug, - "config_changed: call 'store' for namespace: " + - std::to_string(static_cast(config->storage_namespace()))); + "config_changed: call 'store' for " + namespace_name(config->storage_namespace())); _store(config->storage_namespace(), target_pubkey_hex, timestamp.count(), @@ -332,7 +335,6 @@ void State::config_changed(std::optional pubkey_hex) { // Call the hook to perform a push if needed if (_send && needs_push && !suppressions.first) { - std::vector seqnos; std::vector requests; std::vector obsolete_hashes; @@ -340,8 +342,9 @@ void State::config_changed(std::optional pubkey_hex) { if (!config->needs_push()) continue; log(LogLevel::debug, - "config_changed: generate 'send' request for namespace: " + - std::to_string(static_cast(config->storage_namespace()))); + "config_changed: generate 'send' request for " + + namespace_name(config->storage_namespace()) + ", (" + target_pubkey_hex + + ")"); auto [seqno, msg, obs] = config->push(); for (auto hash : obs) @@ -374,28 +377,32 @@ void State::config_changed(std::optional pubkey_hex) { }; // For user config storage we also need to add `pubkey_ed25519` - if (!pubkey_hex) + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); - seqnos.emplace_back(seqno); + // Add the 'seqno' temporarily to the params (this will be removed from the payload + // before sending but is needed to generate the request context) + params["seqno"] = seqno; + requests.emplace_back(params); } // GroupKeys needs special handling as it's not a `ConfigBase` if (pubkey_hex) { - auto pending = _config_groups[target_pubkey_hex]->config_keys->pending_config(); + auto config = _config_groups[target_pubkey_hex]->config_keys.get(); + auto pending = config->pending_config(); if (pending) { log(LogLevel::debug, - "config_changed: generate 'send' request for group keys " + target_pubkey_hex); + "config_changed: generate 'send' request for " + + namespace_name(config->storage_namespace()) + ", (" + + target_pubkey_hex + ")"); // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; - ustring verification = - to_unsigned("store") + - static_cast(_config_groups[target_pubkey_hex] - ->config_keys->storage_namespace()) + - static_cast(timestamp.count()); + ustring verification = to_unsigned("store") + + static_cast(config->storage_namespace()) + + static_cast(timestamp.count()); if (0 != crypto_sign_ed25519_detached( sig.data(), @@ -407,15 +414,19 @@ void State::config_changed(std::optional pubkey_hex) { "config_changed: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ - {"namespace", - _config_groups[target_pubkey_hex]->config_keys->storage_namespace()}, + {"namespace", config->storage_namespace()}, {"pubkey", target_pubkey_hex}, - {"ttl", - _config_groups[target_pubkey_hex]->config_keys->default_ttl().count()}, + {"ttl", config->default_ttl().count()}, {"timestamp", timestamp.count()}, {"data", oxenc::to_base64(*pending)}, {"signature", oxenc::to_base64(sig.begin(), sig.end())}, }; + + // The 'GROUP_KEYS' push data doesn't need a 'seqno', but to avoid index + // out-of-bounds issues we add one anyway (this will be removed from the payload + // before sending but is needed to generate the request context) + params["seqno"] = 0; + requests.emplace_back(params); } } @@ -428,9 +439,15 @@ void State::config_changed(std::optional pubkey_hex) { namespace_store_order(static_cast(b["namespace"])); }); + std::vector seqnos; + std::vector namespaces; nlohmann::json sequence_params; for (auto& request : sorted_requests) { + seqnos.push_back(request["seqno"].get()); + namespaces.push_back(request["namespace"].get()); + request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload + nlohmann::json request_json{{"method", "store"}, {"params", request}}; sequence_params["requests"].push_back(request_json); } @@ -460,7 +477,7 @@ void State::config_changed(std::optional pubkey_hex) { }; // For user config storage we also need to add `pubkey_ed25519` - if (!pubkey_hex) + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); nlohmann::json request_json{{"method", "delete"}, {"params", params}}; @@ -468,8 +485,13 @@ void State::config_changed(std::optional pubkey_hex) { } log(LogLevel::debug, "config_changed: Call 'send'"); nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; - auto payload_dump = payload.dump(); - _send(target_pubkey_hex, seqnos, {to_unsigned(payload_dump.data()), payload_dump.size()}); + nlohmann::json ctx{ + {"type", RequestType::ConfigPush}, + {"pubkey", target_pubkey_hex}, + {"seqnos", seqnos}, + {"namespaces", namespaces}}; + + _send(target_pubkey_hex, to_unsigned(payload.dump()), to_unsigned(ctx.dump())); } log(LogLevel::debug, "config_changed: Complete"); } @@ -492,7 +514,7 @@ std::vector State::merge( std::vector> pending_configs; std::string target_pubkey_hex; - if (!pubkey_hex) { + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { // Convert the _user_pk to the user's session ID std::array user_x_pk; @@ -530,28 +552,28 @@ std::vector State::merge( continue; // Process the previously grouped configs + log(LogLevel::debug, + "merge: Merging " + namespace_name(config.namespace_) + " config (" + + std::string(target_pubkey_hex) + ")"); + std::vector merged_hashes; switch (config.namespace_) { case Namespace::Contacts: - log(LogLevel::debug, "merge: Merging CONTACTS config"); merged_hashes = config_contacts->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); continue; case Namespace::ConvoInfoVolatile: - log(LogLevel::debug, "merge: Merging CONVO_INFO_VOLATILE config"); merged_hashes = config_convo_info_volatile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); continue; case Namespace::UserGroups: - log(LogLevel::debug, "merge: Merging USER_GROUPS config"); merged_hashes = config_user_groups->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); continue; case Namespace::UserProfile: - log(LogLevel::debug, "merge: Merging USER_PROFILE config"); merged_hashes = config_user_profile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); continue; @@ -574,17 +596,11 @@ std::vector State::merge( auto members = _config_groups[target_pubkey_hex]->config_members.get(); is_group_merge = true; - if (config.namespace_ == Namespace::GroupInfo) { - log(LogLevel::debug, - "merge: Merging GROUP_INFO config for: " + std::string(target_pubkey_hex)); + if (config.namespace_ == Namespace::GroupInfo) merged_hashes = info->merge(pending_configs); - } else if (config.namespace_ == Namespace::GroupMembers) { - log(LogLevel::debug, - "merge: Merging GROUP_MEMBERS config for: " + std::string(target_pubkey_hex)); + else if (config.namespace_ == Namespace::GroupMembers) merged_hashes = members->merge(pending_configs); - } else if (config.namespace_ == Namespace::GroupKeys) { - log(LogLevel::debug, - "merge: Merging GROUP_KEYS config for: " + std::string(target_pubkey_hex)); + else if (config.namespace_ == Namespace::GroupKeys) { // GroupKeys doesn't support merging multiple messages at once so do them individually if (_config_groups[target_pubkey_hex]->config_keys->load_key_message( config.hash, config.data, config.timestamp_ms, *info, *members)) { @@ -602,6 +618,38 @@ std::vector State::merge( return good_hashes; } +std::vector State::current_hashes(std::optional pubkey_hex) { + std::vector result; + + if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { + auto contact_hashes = config_contacts->current_hashes(); + auto convo_info_volatile_hashes = config_convo_info_volatile->current_hashes(); + auto user_group_hashes = config_user_groups->current_hashes(); + auto user_profile_hashes = config_user_profile->current_hashes(); + result.insert(result.end(), contact_hashes.begin(), contact_hashes.end()); + result.insert( + result.end(), convo_info_volatile_hashes.begin(), convo_info_volatile_hashes.end()); + result.insert(result.end(), user_group_hashes.begin(), user_group_hashes.end()); + result.insert(result.end(), user_profile_hashes.begin(), user_profile_hashes.end()); + } else { + if (pubkey_hex->size() != 66) + throw std::invalid_argument{"current_hashes: Invalid pubkey_hex - expected 66 bytes"}; + if (!_config_groups.count(*pubkey_hex)) + throw std::runtime_error{ + "current_hashes: Attempted to retrieve current hashes for group with no config " + "state"}; + + auto info_hashes = _config_groups[*pubkey_hex]->config_info->current_hashes(); + auto members_hashes = _config_groups[*pubkey_hex]->config_members->current_hashes(); + auto keys_hashes = _config_groups[*pubkey_hex]->config_keys->current_hashes(); + result.insert(result.end(), info_hashes.begin(), info_hashes.end()); + result.insert(result.end(), members_hashes.begin(), members_hashes.end()); + result.insert(result.end(), keys_hashes.begin(), keys_hashes.end()); + } + + return result; +} + ustring State::dump(bool full_dump) { oxenc::bt_dict_producer combined; @@ -676,15 +724,35 @@ ustring State::dump(config::Namespace namespace_, std::optional seqnos, ustring payload, ustring response) { - log(LogLevel::debug, "received_send_response: Called"); +void State::received_send_response(std::string pubkey, ustring response, ustring ctx) { + auto ctx_json = nlohmann::json::parse(ctx); + + if (!ctx_json.contains("type")) + throw std::invalid_argument{ + "received_send_response: Invalid ctx - expected to contain 'type'"}; + + auto request_type = static_cast(ctx_json["type"].get()); + + switch (request_type) { + case RequestType::ConfigPush: handle_config_push_response(pubkey, response, ctx); break; + default: + throw std::invalid_argument{ + "received_send_response: Unrecognised ctx.type '" + + std::to_string(static_cast(request_type)) + "'"}; + } +} + +void State::handle_config_push_response(std::string pubkey, ustring response, ustring ctx) { + log(LogLevel::debug, "handle_config_push_response: Called"); auto response_json = nlohmann::json::parse(response); if (!response_json.contains("results")) - throw std::invalid_argument{"Invalid response: expected to contain 'results' array"}; + throw std::invalid_argument{ + "handle_config_push_response: Invalid response - expected to contain 'results' " + "array"}; if (response_json["results"].size() == 0) - throw std::invalid_argument{"Invalid response: 'results' array is empty"}; + throw std::invalid_argument{ + "handle_config_push_response: Invalid response - 'results' array is empty"}; auto results = response_json["results"]; @@ -694,7 +762,8 @@ void State::received_send_response( for (const auto& result : results.items()) { if (!result.value().contains("code")) throw std::invalid_argument{ - "Invalid result: expected to contain 'code'" + result.value().dump()}; + "handle_config_push_response: Invalid result - expected to contain 'code'" + + result.value().dump()}; // If the code was different from all former codes then break the loop auto code = result.value()["code"].get(); @@ -727,7 +796,8 @@ void State::received_send_response( throw std::runtime_error{error}; } - log(LogLevel::debug, "received_send_response: Doesn't have a consistent error across requests"); + log(LogLevel::debug, + "handle_config_push_response: Doesn't have a consistent error across requests"); // If the response includes a timestamp value then we should update the network offset if (auto first_result = results[0]; @@ -739,81 +809,62 @@ void State::received_send_response( // The 'results' array will be in the same order as the requests sent within 'payload' so // iterate through them both and mark any successful request as pushed - auto payload_json = nlohmann::json::parse(payload); - - if (!payload_json.contains("params") || !payload_json["params"].contains("requests")) - throw std::invalid_argument{ - "Invalid payload: expected to contain 'params.requests' array."}; - if (payload_json["params"]["requests"].size() == 0) - throw std::invalid_argument{"Invalid payload: 'params.requests' array is empty."}; - - auto requests = payload_json["params"]["requests"]; - auto num_configs = requests.size(); + auto ctx_json = nlohmann::json::parse(ctx); - // Subtract one if we also had a 'delete' request to remove obsolete hashes - if (requests[requests.size() - 1].contains("method") && - requests[requests.size() - 1]["method"].get() == "delete") - num_configs -= 1; - - if (seqnos.size() != num_configs) + if (!ctx_json.contains("seqnos") || !ctx_json.contains("namespaces") || + results.size() < ctx_json["seqnos"].size() || + results.size() < ctx_json["namespaces"].size()) throw std::invalid_argument{ - "Invalid seqnos: Size doesn't match the number of config changes."}; + "handle_config_push_response: Invalid response - Number of responses doesn't match " + "the number of requests."}; - log(LogLevel::debug, "received_send_response: Confirming pushed"); for (int i = 0, n = results.size(); i < n; ++i) { auto result_code = results[i]["code"].get(); - if (result_code < 200 || result_code > 299) - continue; - if (!results[i].contains("body")) - continue; - if (!results[i]["body"].contains("hash")) - continue; - if (!payload_json["params"]["requests"][i].contains("params")) - continue; - if (!payload_json["params"]["requests"][i]["params"].contains("namespace")) + if (result_code < 200 || result_code > 299 || !results[i].contains("body") || + !results[i]["body"].contains("hash")) continue; auto hash = results[i]["body"]["hash"].get(); - auto namespace_ = - payload_json["params"]["requests"][i]["params"]["namespace"].get(); + auto seqno = ctx_json["seqnos"][i].get(); + auto namespace_ = ctx_json["namespaces"][i].get(); switch (namespace_) { - case Namespace::Contacts: config_contacts->confirm_pushed(seqnos[i], hash); continue; + case Namespace::Contacts: config_contacts->confirm_pushed(seqno, hash); continue; case Namespace::ConvoInfoVolatile: - config_convo_info_volatile->confirm_pushed(seqnos[i], hash); - continue; - case Namespace::UserGroups: - config_user_groups->confirm_pushed(seqnos[i], hash); - continue; - case Namespace::UserProfile: - config_user_profile->confirm_pushed(seqnos[i], hash); + config_convo_info_volatile->confirm_pushed(seqno, hash); continue; + case Namespace::UserGroups: config_user_groups->confirm_pushed(seqno, hash); continue; + case Namespace::UserProfile: config_user_profile->confirm_pushed(seqno, hash); continue; default: break; } // Other namespaces are unique for a given pubkey - if (!payload_json["params"]["requests"][i]["params"].contains("pubkey")) + if (!ctx_json.contains("pubkey")) throw std::invalid_argument{ - "Invalid payload: Group config change was missing 'pubkey' param."}; + "handle_config_push_response: Invalid ctx - Missing 'pubkey' for group config " + "push."}; - auto pubkey = payload_json["params"]["requests"][i]["params"]["pubkey"].get(); + auto pubkey = ctx_json["pubkey"].get(); if (pubkey.size() != 66) - throw std::invalid_argument{"Invalid pubkey: expected 66 characters"}; + throw std::invalid_argument{ + "handle_config_push_response: Invalid ctx.pubkey - expected 66 characters"}; if (!_config_groups.count(pubkey)) - throw std::runtime_error{"received_send_response: Unable to retrieve group"}; + throw std::runtime_error{"handle_config_push_response: Unable to retrieve group"}; // Retrieve the group configs for this pubkey auto group_configs = _config_groups[pubkey].get(); switch (namespace_) { - case Namespace::GroupInfo: group_configs->config_info->confirm_pushed(seqnos[i], hash); + case Namespace::GroupInfo: group_configs->config_info->confirm_pushed(seqno, hash); case Namespace::GroupMembers: - group_configs->config_members->confirm_pushed(seqnos[i], hash); + group_configs->config_members->confirm_pushed(seqno, hash); case Namespace::GroupKeys: continue; // No need to do anything here - default: throw std::runtime_error{"Attempted to load unknown namespace"}; + default: + throw std::runtime_error{ + "handle_config_push_response: Attempted to load unknown namespace"}; } } - log(LogLevel::debug, "received_send_response: Completed"); + log(LogLevel::debug, "handle_config_push_response: Completed"); } } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 1f770c46..e743405f 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -151,7 +151,8 @@ LIBSESSION_C_API void state_set_logger( LIBSESSION_C_API bool state_set_send_callback( state_object* state, - void (*callback)(const char*, const seqno_t*, size_t, const unsigned char*, size_t, void*), + void (*callback)( + const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*), void* ctx) { try { if (!callback) @@ -159,13 +160,13 @@ LIBSESSION_C_API bool state_set_send_callback( else { // Setting this can result in the callback being immediately triggered which could throw unbox(state).onSend( - [callback, ctx](std::string pubkey, std::vector seqnos, ustring data) { + [callback, ctx](std::string pubkey, ustring data, ustring request_ctx) { callback( pubkey.c_str(), - seqnos.data(), - seqnos.size(), data.data(), data.size(), + request_ctx.data(), + request_ctx.size(), ctx); }); } @@ -264,7 +265,7 @@ LIBSESSION_C_API bool state_merge( ustring{configs[i].data, configs[i].datalen}); auto result = unbox(state).merge(pubkey_hex, confs); - unbox(state).logger(LogLevel::info, "Merged " + std::to_string(result.size())); + unbox(state).log(LogLevel::info, "Merged " + std::to_string(result.size())); *successful_hashes = make_string_list(result); return true; } catch (const std::exception& e) { @@ -272,6 +273,21 @@ LIBSESSION_C_API bool state_merge( } } +LIBSESSION_C_API bool state_current_hashes( + state_object* state, const char* pubkey_hex_, config_string_list** current_hashes) { + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto result = unbox(state).current_hashes(pubkey_hex); + *current_hashes = make_string_list(result); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + LIBSESSION_C_API bool state_dump( state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { try { @@ -313,24 +329,15 @@ LIBSESSION_C_API bool state_dump_namespace( LIBSESSION_C_API bool state_received_send_response( state_object* state, const char* pubkey_hex, - const seqno_t* seqnos_, - size_t seqnos_len, - unsigned char* payload_data, - size_t payload_data_len, unsigned char* response_data, - size_t response_data_len) { + size_t response_data_len, + unsigned char* request_ctx, + size_t request_ctx_len) { try { - std::vector seqnos; - seqnos.reserve(seqnos_len); - - for (size_t i = 0; i < seqnos_len; i++) - seqnos.emplace_back(seqnos_[i]); - unbox(state).received_send_response( {pubkey_hex, 66}, - seqnos, - {payload_data, payload_data_len}, - {response_data, response_data_len}); + {response_data, response_data_len}, + {request_ctx, request_ctx_len}); return true; } catch (const std::exception& e) { return set_error(state, e.what()); @@ -345,15 +352,8 @@ LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { return nullptr; } -LIBSESSION_C_API bool state_set_profile_name(state_object* state, const char* name) { - try { - unbox(state).logger(LogLevel::info, "state_set_profile_name: called"); - unbox(state).config_user_profile->set_name(name); - unbox(state).logger(LogLevel::info, "state_set_profile_name: done"); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } +LIBSESSION_C_API void state_set_profile_name(state_object* state, const char* name) { + unbox(state).config_user_profile->set_name(name); } LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { @@ -367,20 +367,13 @@ LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* stat return p; } -LIBSESSION_C_API bool state_set_profile_pic(state_object* state, user_profile_pic pic) { +LIBSESSION_C_API void state_set_profile_pic(state_object* state, user_profile_pic pic) { std::string_view url{pic.url}; ustring_view key; if (!url.empty()) key = {pic.key, 32}; - try { - unbox(state).logger(LogLevel::info, "state_set_profile_pic: called"); - unbox(state).config_user_profile->set_profile_pic(url, key); - unbox(state).logger(LogLevel::info, "state_set_profile_pic: done"); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } + unbox(state).config_user_profile->set_profile_pic(url, key); } LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { diff --git a/tests/test_state.cpp b/tests/test_state.cpp index aa5d9c5b..06ccc8d6 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -13,23 +13,97 @@ using namespace session; using namespace session::state; using namespace session::config; +std::string replace_suffix_between( + std::string_view value, + int suffix_start_distance_from_end, + int suffix_end_distance_from_end, + std::string_view replacement = "") { + auto start_index = (value.size() - suffix_start_distance_from_end); + auto end_index = (value.size() - suffix_end_distance_from_end); + + return std::string(value.substr(0, start_index)) + std::string(replacement) + + std::string(value.substr(end_index, value.size() - end_index)); +} + TEST_CASE("State", "[state][state]") { auto ed_sk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - auto state = State(ed_sk); + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + std::optional last_send_pubkey = std::nullopt; + std::optional last_send_data = std::nullopt; + std::optional last_send_ctx = std::nullopt; + std::optional last_store_namespace = std::nullopt; + std::optional last_store_pubkey = std::nullopt; + std::optional last_store_timestamp = std::nullopt; + std::optional last_store_data = std::nullopt; + + state.onStore( + [&last_store_namespace, &last_store_pubkey, &last_store_timestamp, &last_store_data]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + last_store_namespace = namespace_; + last_store_pubkey = pubkey; + last_store_timestamp = timestamp_ms; + last_store_data = data; + }); + state.onSend([&last_send_pubkey, &last_send_data, &last_send_ctx]( + std::string pubkey, ustring data, ustring ctx) { + last_send_pubkey = pubkey; + last_send_data = data; + last_send_ctx = ctx; + }); // Sanity check direct config access CHECK_FALSE(state.config_user_profile->get_name().has_value()); state.config_user_profile->set_name("Test Name"); CHECK(state.config_user_profile->get_name() == "Test Name"); + CHECK(*last_store_namespace == Namespace::UserProfile); + CHECK(*last_store_pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(oxenc::to_hex(last_store_data->begin(), last_store_data->end()) == + "64313a21693165313a2436353a64313a23693165313a266465313a3c6c6c69306533323aea173b57beca8af1" + "8c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d646565313a28303a313a296c65" + "65"); + CHECK(*last_send_pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" + "6"); + auto send_data_no_ts = replace_suffix_between(to_sv(*last_send_data), (13 + 22), 22, "0"); + auto send_data_no_sig = replace_suffix_between(send_data_no_ts, (37 + 88), 37, "sig"); + CHECK(send_data_no_sig == + "{\"method\":\"sequence\",\"params\":{\"requests\":[{\"method\":\"store\",\"params\":{" + "\"data\":" + "\"CAESqwMKABIAGqIDCAYoAUKbAxBjSP+U6QQAfuYdxoPMnN/" + "0oleiZOybnqWg9dfVOJR02kXQ7Eypogv5MwlCtRGO1L452dJXroLIGJtu/pJe2FwROk/" + "FoQ5XLHDeY9LaPYj7l0I+Mzt+LG3BMcTEZYLlAVI/2sk80QWDJvlRFyihKJOx5lGEb/" + "lxTrgDf8pQ1dLGxoiNEv47Ygvy4xlzxEbGRVwSp8LPJByKu5YGFMGpTP+pZ9L0vZasFxjK3xnw2/" + "0G1g54zb/p3orgdlUoXUJGSr7d+F7UtSm34KtBTHIGhhCn4CCIxLv1olmmIkGcBwZ7ldVTICcqu+" + "GaNh2jTR1KZPjEef2xIGz8tdzVCKnup6HJO0M+" + "JBT8FSPqvbFt1z9Y7D12wA0Ou82IXXv6ltGGHy3xqMb6IQUw4N+MlfQszNAc7lNUn+" + "wj0DzLzQtorw5oqjbdq2DbxY5bMQq2ACML4MEHUyh0yN/qVc31Q49Edinvuc2ccATeGTysr/y9G+" + "CRTbt88jxgrCP2dcLzEPqIHNyhaWBnqFyfLntYqtsk8KTrSE6N0V7iDxeDFiAA\"," + "\"namespace\":2,\"pubkey\":" + "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"pubkey_" + "ed25519\":\"8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f\"," + "\"signature\":\"sig\",\"timestamp\":0,\"ttl\":2592000000}}]}}"); + CHECK(to_sv(*last_send_ctx) == + "{\"namespaces\":[2],\"pubkey\":" + "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"seqnos\":[1]," + "\"type\":2}"); + // Init with dumps auto dump = state.dump(Namespace::UserProfile); - auto state2 = State(ed_sk); - CHECK_FALSE(state2.config_user_profile->get_name().has_value()); - state2.load(Namespace::UserProfile, std::nullopt, {dump.data(), dump.size()}); + auto state2 = + State({ed_sk.data(), ed_sk.size()}, {{Namespace::UserProfile, std::nullopt, dump}}); CHECK(state2.config_user_profile->get_name() == "Test Name"); + + // Explicit load + auto state3 = State({ed_sk.data(), ed_sk.size()}, {}); + CHECK_FALSE(state3.config_user_profile->get_name().has_value()); + state3.load(Namespace::UserProfile, std::nullopt, dump); + CHECK(state3.config_user_profile->get_name() == "Test Name"); } TEST_CASE("State c API", "[state][state][c]") { @@ -39,18 +113,18 @@ TEST_CASE("State c API", "[state][state][c]") { char err[256]; state_object* state; - REQUIRE(state_init(&state, ed_sk.data(), err)); + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); // User Profile forwarding CHECK(state_get_profile_name(state) == nullptr); - CHECK(state_set_profile_name(state, "Test Name")); + state_set_profile_name(state, "Test Name"); CHECK(state_get_profile_name(state) == "Test Name"sv); auto p = user_profile_pic(); strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < sizeof(p.url)! memcpy(p.key, "secret78901234567890123456789012", 32); CHECK(strlen(state_get_profile_pic(state).url) == 0); - CHECK(state_set_profile_pic(state, p)); + state_set_profile_pic(state, p); auto stored_pic = state_get_profile_pic(state); CHECK(stored_pic.url == "http://example.org/omg-pic-123.bmp"sv); CHECK(ustring_view{stored_pic.key, 32} == "secret78901234567890123456789012"_bytes); @@ -63,7 +137,7 @@ TEST_CASE("State c API", "[state][state][c]") { size_t dump1len; state_dump_namespace(state, NAMESPACE_USER_PROFILE, nullptr, &dump1, &dump1len); state_object* state2; - REQUIRE(state_init(&state2, ed_sk.data(), err)); + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, err)); CHECK(state_get_profile_name(state2) == nullptr); CHECK(state_load(state2, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); CHECK(state_get_profile_name(state2) == "Test Name"sv); From e582ca92113b2d22883e4a721578f120dd81a791 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 2 Feb 2024 17:07:32 +1100 Subject: [PATCH 05/24] Moved the C API for USER_PROFILE entirely into the state object --- include/session/config/user_profile.h | 254 --------------------- include/session/state.h | 43 ++++ src/config/user_profile.cpp | 84 +------ src/state_c_wrapper.cpp | 18 ++ tests/test_compression.cpp | 1 - tests/test_config_userprofile.cpp | 312 ++++++++++++-------------- tests/test_state.cpp | 1 - 7 files changed, 200 insertions(+), 513 deletions(-) delete mode 100644 include/session/config/user_profile.h diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h deleted file mode 100644 index 651f46af..00000000 --- a/include/session/config/user_profile.h +++ /dev/null @@ -1,254 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "base.h" -#include "profile_pic.h" - -/// API: user_profile/user_profile_init -/// -/// Constructs a user profile config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT user_profile_init( -/// [out] config_object** conf, -/// [in] const unsigned char* ed25519_secretkey, -/// [in] const unsigned char* dump, -/// [in] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ed25519_secretkey` -- [in] must be the 32-byte secret key seed value. (You can also pass the -/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32 -/// bytes of that are the seed). This field cannot be null. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty profile this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is NULL. -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int user_profile_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) -#if defined(__GNUC__) || defined(__clang__) - __attribute__((warn_unused_result)) -#endif - ; - -/// API: user_profile/user_profile_get_name -/// -/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at -/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. -/// -/// Declaration: -/// ```cpp -/// CONST CHAR* user_profile_get_name( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_EXPORT const char* user_profile_get_name(const config_object* conf); - -/// API: user_profile/user_profile_set_name -/// -/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Declaration: -/// ```cpp -/// INT user_profile_set_name( -/// [in] config_object* conf, -/// [in] const char* name -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int user_profile_set_name(config_object* conf, const char* name); - -/// API: user_profile/user_profile_get_pic -/// -/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile -/// pic is not currently set, and otherwise should be copied right away (they will not be valid -/// beyond other API calls on this config object). -/// -/// Declaration: -/// ```cpp -/// USER_PROFILE_PIC user_profile_get_pic( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic -LIBSESSION_EXPORT user_profile_pic user_profile_get_pic(const config_object* conf); - -/// API: user_profile/user_profile_set_pic -/// -/// Sets a user profile -/// -/// Declaration: -/// ```cpp -/// INT user_profile_set_pic( -/// [in] config_object* conf, -/// [in] user_profile_pic pic -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int user_profile_set_pic(config_object* conf, user_profile_pic pic); - -/// API: user_profile/user_profile_get_nts_priority -/// -/// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > -/// 0 for pinned (with higher value = higher priority). -/// -/// Declaration: -/// ```cpp -/// INT user_profile_get_nts_priority( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int` -- Returns the priority level -LIBSESSION_EXPORT int user_profile_get_nts_priority(const config_object* conf); - -/// API: user_profile/user_profile_set_nts_priority -/// -/// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for -/// higher priority in the conversation list. -/// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_nts_priority( -/// [in] config_object* conf, -/// [in] int priority -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `priority` -- [in] Integer of the priority -/// -/// Outputs: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void user_profile_set_nts_priority(config_object* conf, int priority); - -/// API: user_profile/user_profile_get_nts_expiry -/// -/// Gets the Note-to-self message expiry timer (seconds). Returns 0 if not set. -/// -/// Declaration: -/// ```cpp -/// INT user_profile_get_nts_expiry( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_EXPORT int user_profile_get_nts_expiry(const config_object* conf); - -/// API: user_profile/user_profile_set_nts_expiry -/// -/// Sets the Note-to-self message expiry timer (seconds). Setting 0 (or negative) will clear the -/// current timer. -/// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_nts_expiry( -/// [in] config_object* conf, -/// [in] int expiry -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void user_profile_set_nts_expiry(config_object* conf, int expiry); - -/// API: user_profile/user_profile_get_blinded_msgreqs -/// -/// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they -/// should be ignored. -/// -/// Declaration: -/// ```cpp -/// INT user_profile_get_blinded_msgreqs( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int` -- Will be -1 if the config does not have the value explicitly set, 0 if the setting is -/// explicitly disabled, and 1 if the setting is explicitly enabled. -LIBSESSION_EXPORT int user_profile_get_blinded_msgreqs(const config_object* conf); - -/// API: user_profile/user_profile_set_blinded_msgreqs -/// -/// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any -/// positive value) to enable; 0 to disable; and -1 to clear the setting. -/// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_blinded_msgreqs( -/// [in] config_object* conf, -/// [in] int enabled -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `enabled` -- [in] true if they should be enabled, false if disabled -/// -/// Outputs: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void user_profile_set_blinded_msgreqs(config_object* conf, int enabled); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/include/session/state.h b/include/session/state.h index 9f5d7402..4af8f5be 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -386,6 +386,49 @@ LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* sta /// - `pic` -- [in] Pointer to the pic LIBSESSION_EXPORT void state_set_profile_pic(state_object* state, user_profile_pic pic); +/// API: state/state_get_profile_nts_priority +/// +/// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > +/// 0 for pinned (with higher value = higher priority). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Returns the priority level +LIBSESSION_EXPORT int state_get_profile_nts_priority(const state_object* state); + +/// API: state/state_set_profile_nts_priority +/// +/// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for +/// higher priority in the conversation list. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `priority` -- [in] Integer of the priority +LIBSESSION_EXPORT void state_set_profile_nts_priority(state_object* state, int priority); + +/// API: state/state_get_profile_nts_expiry +/// +/// Gets the Note-to-self message expiry timer (seconds). Returns 0 if not set. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set +LIBSESSION_EXPORT int state_get_profile_nts_expiry(const state_object* state); + +/// API: state/state_set_profile_nts_expiry +/// +/// Sets the Note-to-self message expiry timer (seconds). Setting 0 (or negative) will clear the +/// current timer. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `expiry` -- [in] Integer of the expiry timer in seconds +LIBSESSION_EXPORT void state_set_profile_nts_expiry(state_object* state, int expiry); + /// API: state/state_get_profile_blinded_msgreqs /// /// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 143490a4..37758c84 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -1,18 +1,13 @@ -#include "session/config/user_profile.h" +#include "session/config/user_profile.hpp" #include #include "internal.hpp" -#include "session/config/error.h" -#include "session/config/user_profile.hpp" -#include "session/export.h" #include "session/types.hpp" using namespace session::config; using session::ustring_view; -LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; - UserProfile::UserProfile( ustring_view ed25519_secretkey, std::optional dumped, @@ -21,37 +16,15 @@ UserProfile::UserProfile( load_key(ed25519_secretkey); } -LIBSESSION_C_API int user_profile_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - std::optional UserProfile::get_name() const { if (auto* s = data["n"].string(); s && !s->empty()) return *s; return std::nullopt; } -LIBSESSION_C_API const char* user_profile_get_name(const config_object* conf) { - if (auto s = unbox(conf)->get_name()) - return s->data(); - return nullptr; -} void UserProfile::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } -LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { - try { - unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - return 0; -} profile_pic UserProfile::get_profile_pic() const { profile_pic pic{}; @@ -62,17 +35,6 @@ profile_pic UserProfile::get_profile_pic() const { return pic; } -LIBSESSION_C_API user_profile_pic user_profile_get_pic(const config_object* conf) { - user_profile_pic p; - if (auto pic = unbox(conf)->get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; - } - return p; -} - void UserProfile::set_profile_pic(std::string_view url, ustring_view key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); } @@ -81,21 +43,6 @@ void UserProfile::set_profile_pic(profile_pic pic) { set_profile_pic(pic.url, pic.key); } -LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic pic) { - std::string_view url{pic.url}; - ustring_view key; - if (!url.empty()) - key = {pic.key, 32}; - - try { - unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - - return 0; -} - void UserProfile::set_nts_priority(int priority) { set_nonzero_int(data["+"], priority); } @@ -104,14 +51,6 @@ int UserProfile::get_nts_priority() const { return data["+"].integer_or(0); } -LIBSESSION_C_API int user_profile_get_nts_priority(const config_object* conf) { - return unbox(conf)->get_nts_priority(); -} - -LIBSESSION_C_API void user_profile_set_nts_priority(config_object* conf, int priority) { - unbox(conf)->set_nts_priority(priority); -} - void UserProfile::set_nts_expiry(std::chrono::seconds expiry) { set_positive_int(data["e"], expiry.count()); } @@ -122,14 +61,6 @@ std::optional UserProfile::get_nts_expiry() const { return std::nullopt; } -LIBSESSION_C_API int user_profile_get_nts_expiry(const config_object* conf) { - return unbox(conf)->get_nts_expiry().value_or(0s).count(); -} - -LIBSESSION_C_API void user_profile_set_nts_expiry(config_object* conf, int expiry) { - unbox(conf)->set_nts_expiry(std::max(0, expiry) * 1s); -} - void UserProfile::set_blinded_msgreqs(std::optional value) { if (!value) data["M"].erase(); @@ -142,16 +73,3 @@ std::optional UserProfile::get_blinded_msgreqs() const { return static_cast(*M); return std::nullopt; } - -LIBSESSION_C_API int user_profile_get_blinded_msgreqs(const config_object* conf) { - if (auto opt = unbox(conf)->get_blinded_msgreqs()) - return static_cast(*opt); - return -1; -} - -LIBSESSION_C_API void user_profile_set_blinded_msgreqs(config_object* conf, int enabled) { - std::optional val; - if (enabled >= 0) - val = static_cast(enabled); - unbox(conf)->set_blinded_msgreqs(std::move(val)); -} diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index e743405f..d483f267 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -26,6 +26,8 @@ using namespace session; using namespace session::config; using namespace session::state; +LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; + namespace { State& unbox(state_object* state) { assert(state && state->internals); @@ -376,6 +378,22 @@ LIBSESSION_C_API void state_set_profile_pic(state_object* state, user_profile_pi unbox(state).config_user_profile->set_profile_pic(url, key); } +LIBSESSION_C_API int state_get_profile_nts_priority(const state_object* state) { + return unbox(state).config_user_profile->get_nts_priority(); +} + +LIBSESSION_C_API void state_set_profile_nts_priority(state_object* state, int priority) { + unbox(state).config_user_profile->set_nts_priority(priority); +} + +LIBSESSION_C_API int state_get_profile_nts_expiry(const state_object* state) { + return unbox(state).config_user_profile->get_nts_expiry().value_or(0s).count(); +} + +LIBSESSION_C_API void state_set_profile_nts_expiry(state_object* state, int expiry) { + unbox(state).config_user_profile->set_nts_expiry(std::max(0, expiry) * 1s); +} + LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { if (auto opt = unbox(state).config_user_profile->get_blinded_msgreqs()) return static_cast(*opt); diff --git a/tests/test_compression.cpp b/tests/test_compression.cpp index 24892fbd..f160e204 100644 --- a/tests/test_compression.cpp +++ b/tests/test_compression.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 66c81cac..a4ff4af4 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -1,6 +1,7 @@ #include +#include #include -#include +#include #include #include @@ -12,17 +13,17 @@ using namespace std::literals; using namespace oxenc::literals; -void log_msg(config_log_level lvl, const char* msg, void*) { - INFO((lvl == LOG_LEVEL_ERROR ? "ERROR" - : lvl == LOG_LEVEL_WARNING ? "Warning" - : lvl == LOG_LEVEL_INFO ? "Info" +void log_msg(session::config::LogLevel lvl, std::string msg) { + INFO((lvl == session::config::LogLevel::error ? "ERROR" + : lvl == session::config::LogLevel::warning ? "Warning" + : lvl == session::config::LogLevel::info ? "Info" : "debug") << ": " << msg); } -TEST_CASE("user profile C API", "[config][user_profile][c]") { +TEST_CASE("user profile", "[config][user_profile]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk, curve_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair( @@ -36,36 +37,29 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "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); - - config_set_logger(conf, log_msg, NULL); + session::config::UserProfile conf{ustring_view{seed}, std::nullopt}; + conf.logger = log_msg; // We don't need to push anything, since this is an empty config - CHECK_FALSE(config_needs_push(conf)); + CHECK_FALSE(conf.needs_push()); // And we haven't changed anything so don't need to dump to db - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // 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) + auto name = conf.get_name(); + CHECK(name == std::nullopt); // 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); - CHECK(to_push->config_len == 256 + 176); // 176 = protobuf overhead - const char* enc_domain = "UserProfile"; - REQUIRE(config_encryption_domain(conf) == std::string_view{enc_domain}); + auto [seqno, to_push, obs] = conf.push(); + CHECK(seqno == 0); + CHECK(to_push.size() == 256 + 176); // 176 = protobuf overhead + REQUIRE(conf.encryption_domain() == "UserProfile"sv); // 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, to_push->config + to_push->config_len) == + CHECK(oxenc::to_hex(to_push.begin(), to_push.end()) == "080112ab030a0012001aa20308062801429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39" "98af44ca65f967768dd78ee80fffab6f809f6cef49c73a36c82a89622ff0de2ceee06b8c638e2c876fa9047f" "449dbe24b1fc89281a264fe90abdeffcdd44f797bd4572a6c5ae8d88bf372c3c717943ebd570222206fabf0e" @@ -77,42 +71,45 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "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); + auto pic = conf.get_profile_pic(); + CHECK(pic.url.size() == 0); + CHECK(conf.get_nts_priority() == 0); + CHECK(conf.get_nts_expiry() == std::nullopt); // 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); + conf.set_name("Kallie"); + session::config::profile_pic p; + { + // These don't stay alive, so we use set_key/set_url to make a local copy: + ustring key = "secret78901234567890123456789012"_bytes; + std::string url = "http://example.org/omg-pic-123.bmp"; // NB: length must be < sizeof(p.url)! + p.set_key(std::move(key)); + p.url = std::move(url); + } + conf.set_profile_pic(p); + conf.set_nts_priority(9); // 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) + name = conf.get_name(); + REQUIRE(name != std::nullopt); CHECK(name == "Kallie"sv); - pic = user_profile_get_pic(conf); - REQUIRE(pic.url); - REQUIRE(pic.key); - CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv); - CHECK(ustring_view{pic.key, 32} == "secret78901234567890123456789012"_bytes); + pic = conf.get_profile_pic(); + REQUIRE(pic.url.size() > 0); + REQUIRE(pic.key.size() > 0); + CHECK(pic.url == "http://example.org/omg-pic-123.bmp"); + CHECK(pic.key == "secret78901234567890123456789012"_bytes); - CHECK(user_profile_get_nts_priority(conf) == 9); + CHECK(conf.get_nts_priority() == 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 + CHECK(conf.needs_push()); + CHECK(conf.needs_dump()); + std::tie(seqno, to_push, obs) = conf.push(); + CHECK(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: @@ -149,27 +146,18 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "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)); + CHECK(conf.needs_dump()); // 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); + CHECK(conf.needs_push()); + + auto dump1 = conf.dump(); // (in a real client we'd now store this to disk) - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // clang-format off - CHECK(printable(dump1, dump1len) == printable( + CHECK(printable({dump1.data(), dump1.size()}) == printable( "d" "1:!" "i2e" "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" @@ -177,18 +165,17 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:)" "le" "e")); // clang-format on - free(dump1); // done with the dump; don't leak! // So now imagine we got back confirmation from the swarm that the push has been stored: - config_confirm_pushed(conf, seqno, "fakehash1"); + conf.confirm_pushed(seqno, "fakehash1"); - 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); + CHECK_FALSE(conf.needs_push()); + CHECK(conf.needs_dump()); // The confirmation changes state, so this makes us need a dump + // again. + dump1 = conf.dump(); // clang-format off - CHECK(printable(dump1, dump1len) == printable( + CHECK(printable({dump1.data(), dump1.size()}) == printable( "d" "1:!" "i0e" "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" @@ -196,159 +183,136 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:)" "le" "e")); // clang-format on - free(dump1); - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // 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); - config_set_logger(conf2, log_msg, NULL); - CHECK_FALSE(config_needs_dump(conf2)); + session::config::UserProfile conf2{ustring_view{seed}, std::nullopt}; + conf2.logger = log_msg; + CHECK_FALSE(conf2.needs_dump()); // 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); + std::vector> merge_configs; + merge_configs.emplace_back("fakehash1", exp_push1_encrypted); + auto accepted = conf2.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash1"sv); // 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); + CHECK(conf2.needs_dump()); + auto dump2 = conf2.dump(); // (store in db) - free(dump2); - CHECK_FALSE(config_needs_dump(conf2)); + CHECK_FALSE(conf2.needs_dump()); // 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)); + REQUIRE_FALSE(conf2.needs_push()); // Now let's create a conflicting update: // Change the name on both clients: - user_profile_set_name(conf, "Nibbler"); - user_profile_set_name(conf2, "Raz"); + conf.set_name("Nibbler"); + conf2.set_name("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); + ustring key2 = "qwert\0yuio1234567890123456789012"_bytes; + std::string url2 = "http://new.example.com/pic"; + p.set_key(std::move(key2)); + p.url = std::move(url2); + conf2.set_profile_pic(p); + + conf2.set_nts_expiry(86400s); + CHECK(conf2.get_nts_expiry() == 86400s); + + CHECK(conf2.get_blinded_msgreqs() == std::nullopt); + conf2.set_blinded_msgreqs(false); + CHECK(conf2.get_blinded_msgreqs() == false); + conf2.set_blinded_msgreqs(std::nullopt); + CHECK(conf2.get_blinded_msgreqs() == std::nullopt); + conf2.set_blinded_msgreqs(true); + CHECK(conf2.get_blinded_msgreqs() == true); // 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, "fakehash2"); - - 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, "fakehash3"); - - config_dump(conf, &dump1, &dump1len); - config_dump(conf2, &dump2, &dump2len); + CHECK(conf.needs_push()); + CHECK(conf2.needs_push()); + std::tie(seqno, to_push, obs) = conf.push(); + CHECK(seqno == 2); // incremented, since we made a field change + conf.confirm_pushed(seqno, "fakehash2"); + + auto [seqno2, to_push2, obs2] = conf2.push(); + CHECK(seqno2 == 2); // incremented, since we made a field change + conf2.confirm_pushed(seqno2, "fakehash3"); + + dump1 = conf.dump(); + dump2 = conf2.dump(); // (store in db) - free(dump1); - free(dump2); // Since we set different things, we're going to get back different serialized data to be // pushed: - CHECK(printable(to_push->config, to_push->config_len) != - printable(to_push2->config, to_push2->config_len)); + CHECK(printable({to_push.data(), to_push.size()}) != + printable({to_push2.data(), to_push2.size()})); // 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; - merge_size[0] = to_push->config_len; - 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; - merge_size[0] = to_push2->config_len; - 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); + merge_configs[0] = {"fakehash2", to_push}; + accepted = conf2.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash2"sv); + + merge_configs[0] = {"fakehash3", to_push2}; + accepted = conf.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash3"sv); // 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); + std::tie(seqno, to_push, obs) = conf.push(); + std::tie(seqno2, to_push2, obs2) = conf2.push(); - REQUIRE(to_push->seqno == 3); - REQUIRE(to_push2->seqno == 3); - REQUIRE(config_needs_push(conf)); - REQUIRE(config_needs_push(conf2)); + REQUIRE(seqno == 3); + REQUIRE(seqno2 == 3); + REQUIRE(conf.needs_push()); + REQUIRE(conf2.needs_push()); // 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); + CHECK(conf.get_name() == "Nibbler"sv); + CHECK(conf2.get_name() == "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); - REQUIRE(pic.url); + pic = conf.get_profile_pic(); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(oxenc::to_hex(pic.key.begin(), pic.key.end()) == "7177657274007975696f31323334353637383930313233343536373839303132"); - pic = user_profile_get_pic(conf2); - REQUIRE(pic.url); + pic = conf2.get_profile_pic(); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(oxenc::to_hex(pic.key.begin(), pic.key.end()) == "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); + CHECK(conf.get_nts_priority() == 9); + CHECK(conf2.get_nts_priority() == 9); + CHECK(conf.get_nts_expiry() == 86400s); + CHECK(conf2.get_nts_expiry() == 86400s); + CHECK(conf.get_blinded_msgreqs() == true); + CHECK(conf2.get_blinded_msgreqs() == true); - config_confirm_pushed(conf, to_push->seqno, "fakehash4"); - config_confirm_pushed(conf2, to_push2->seqno, "fakehash4"); + conf.confirm_pushed(seqno, "fakehash4"); + conf2.confirm_pushed(seqno2, "fakehash4"); - config_dump(conf, &dump1, &dump1len); - config_dump(conf2, &dump2, &dump2len); + dump1 = conf.dump(); + dump2 = conf2.dump(); // (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_FALSE(conf.needs_dump()); + CHECK_FALSE(conf2.needs_dump()); + CHECK_FALSE(conf.needs_push()); + CHECK_FALSE(conf2.needs_push()); } diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 06ccc8d6..b93e5ce8 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -1,7 +1,6 @@ #include #include "session/config/namespaces.hpp" -#include "session/config/user_profile.h" #include "session/config/user_profile.hpp" #include "session/state.h" #include "session/state.hpp" From 3f1f6d223a8fbf039f431d207ecfb23a712e14ba Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 5 Feb 2024 16:34:30 +1100 Subject: [PATCH 06/24] Updated the Contact C API to run via the state object --- include/session/config/base.hpp | 8 +- include/session/config/contacts.h | 222 ----------------------- include/session/state.h | 115 +++++++++++- include/session/state.hpp | 41 ++++- src/config/contacts.cpp | 143 +++++---------- src/state.cpp | 157 ++++++++-------- src/state_c_wrapper.cpp | 61 ++++++- tests/CMakeLists.txt | 1 + tests/test_config_contacts.cpp | 158 ---------------- tests/test_config_userprofile.cpp | 19 +- tests/test_state.cpp | 288 ++++++++++++++++++++++++++---- 11 files changed, 607 insertions(+), 606 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 3c88ef76..606e4686 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -142,10 +142,6 @@ class ConfigSig { /// sub-types. class ConfigBase : public ConfigSig { private: - // The parent state which owns this config object. By providing a pointer to the parent state - // we can inform the parent when changes occur. - std::optional _parent_state; - // The object (either base config message or MutableConfigMessage) that stores the current // config message. Subclasses do not directly access this: instead they call `dirty()` if they // intend to make changes, or the `set_config_field` wrapper. @@ -174,6 +170,10 @@ class ConfigBase : public ConfigSig { std::unordered_set _old_hashes; protected: + // The parent state which owns this config object. By providing a pointer to the parent state + // we can inform the parent when changes occur. + std::optional _parent_state; + // Constructs a base config by loading the data from a dump as produced by `dump()`. If the // dump is nullopt then an empty base config is constructed with no config settings and seqno // set to 0. diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 7c6bc0e4..388af83f 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -4,7 +4,6 @@ extern "C" { #endif -#include "base.h" #include "expiring.h" #include "notify.h" #include "profile_pic.h" @@ -36,220 +35,14 @@ typedef struct contacts_contact { } contacts_contact; -/// API: contacts/contacts_init -/// -/// Constructs a contacts config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT contacts_init( -/// [out] config_object** conf, -/// [in] const unsigned char* ed25519_secretkey, -/// [in] const unsigned char* dump, -/// [in] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [out] Pointer to the config object -/// - `ed25519_secretkey` -- [in] must be the 32-byte secret key seed value. (You can also pass the -/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32 -/// bytes of that are the seed). This field cannot be null. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is NULL. -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int contacts_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: contacts/contacts_get -/// -/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex -/// string), if the contact exists, and returns true. If the contact does not exist then `contact` -/// is left unchanged and false is returned. -/// -/// Declaration: -/// ```cpp -/// BOOL contacts_get( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `contact` -- [out] the contact info data -/// - `session_id` -- [in] null terminated hex string -/// -/// Output: -/// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool contacts_get( - config_object* conf, contacts_contact* contact, const char* session_id) - __attribute__((warn_unused_result)); - -/// API: contacts/contacts_get_or_construct -/// -/// Same as the above `contacts_get()` except that when the contact does not exist, this sets all -/// the contact fields to defaults and loads it with the given session_id. -/// -/// Returns true as long as it is given a valid session_id. A false return is considered an error, -/// and means the session_id was not a valid session_id. -/// -/// This is the method that should usually be used to create or update a contact, followed by -/// setting fields in the contact, and then giving it to contacts_set(). -/// -/// Declaration: -/// ```cpp -/// BOOL contacts_get_or_construct( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `contact` -- [out] the contact info data -/// - `session_id` -- [in] null terminated hex string -/// -/// Output: -/// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool contacts_get_or_construct( - config_object* conf, contacts_contact* contact, const char* session_id) - __attribute__((warn_unused_result)); - -/// API: contacts/contacts_set -/// -/// Adds or updates a contact from the given contact info struct. -/// -/// Declaration: -/// ```cpp -/// VOID contacts_set( -/// [in, out] config_object* conf, -/// [in] const contacts_contact* contact -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to the config object -/// - `contact` -- [in] Pointer containing the contact info data -/// -/// Output: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void contacts_set(config_object* conf, const contacts_contact* contact); - -// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would -// save very little in actual calling code. The procedure for updating a single field without them -// is simple enough; for example to update `approved` and leave everything else unchanged: -// -// contacts_contact c; -// if (contacts_get_or_construct(conf, &c, some_session_id)) { -// const char* new_nickname = "Joe"; -// c.approved = new_nickname; -// contacts_set_or_create(conf, &c); -// } else { -// // some_session_id was invalid! -// } - -/// API: contacts/contacts_erase -/// -/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was -/// found and removed, false if the contact was not present. You must not call this during -/// iteration; see details below. -/// -/// Declaration: -/// ```cpp -/// BOOL contacts_erase( -/// [in, out] config_object* conf, -/// [in] const char* session_id -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to the config object -/// - `session_id` -- [in] Text containing null terminated hex string -/// -/// Outputs: -/// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool contacts_erase(config_object* conf, const char* session_id); - -/// API: contacts/contacts_size -/// -/// Returns the number of contacts. -/// -/// Declaration: -/// ```cpp -/// SIZE_T contacts_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- input - Pointer to the config object -/// -/// Outputs: -/// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); - typedef struct contacts_iterator { void* _internals; } contacts_iterator; -/// API: contacts/contacts_iterator_new -/// -/// Starts a new iterator. -/// -/// Functions for iterating through the entire contact list, in sorted order. Intended use is: -/// -/// contacts_contact c; -/// contacts_iterator *it = contacts_iterator_new(contacts); -/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) { -/// // c.session_id, c.nickname, etc. are loaded -/// } -/// contacts_iterator_free(it); -/// -/// It is NOT permitted to add/remove/modify records while iterating. -/// -/// Declaration: -/// ```cpp -/// CONTACTS_ITERATOR* contacts_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `contacts_iterator*` -- pointer to the iterator -LIBSESSION_EXPORT contacts_iterator* contacts_iterator_new(const config_object* conf); - /// API: contacts/contacts_iterator_free /// /// Frees an iterator once no longer needed. /// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_free( -/// [in] contacts_iterator* it -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator LIBSESSION_EXPORT void contacts_iterator_free(contacts_iterator* it); @@ -259,14 +52,6 @@ LIBSESSION_EXPORT void contacts_iterator_free(contacts_iterator* it); /// Returns true if iteration has reached the end. Otherwise `c` is populated and false is /// returned. /// -/// Declaration: -/// ```cpp -/// BOOL contacts_iterator_done( -/// [in] contacts_iterator* it, -/// [out] contacts_contact* c -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator /// - `c` -- [out] Pointer to the contact, will be populated if false @@ -279,13 +64,6 @@ LIBSESSION_EXPORT bool contacts_iterator_done(contacts_iterator* it, contacts_co /// /// Advances the iterator. /// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_advance( -/// [in] contacts_iterator* it -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator LIBSESSION_EXPORT void contacts_iterator_advance(contacts_iterator* it); diff --git a/include/session/state.h b/include/session/state.h index 4af8f5be..2cef0374 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -8,6 +8,7 @@ extern "C" { #include #include +#include "config/contacts.h" #include "config/namespaces.h" #include "config/profile_pic.h" #include "export.h" @@ -227,12 +228,14 @@ LIBSESSION_EXPORT bool state_suppress_hooks_start( /// - `state` -- [in] Pointer to state_object object /// - `send` -- [in] controls whether the `send` hook should no longer be suppressed. /// - `store` -- [in] controls whether the `store` hook should no longer be suppressed. +/// - `force` -- [in] controls whether we should clear out multiple suppressions for the specified +/// hooks or just a single suppression. /// - `pubkey_hex` -- [in] pubkey to stop suppressing changes for (in hex, with prefix - 66 bytes). /// If the value provided doesn't match a entry created by `state_suppress_hooks_start` those /// changes will continue to be suppressed. If none is provided then the hooks for all configs /// with pending changes will be triggered. LIBSESSION_EXPORT bool state_suppress_hooks_stop( - state_object* state, bool send, bool store, const char* pubkey_hex); + state_object* state, bool send, bool stor, bool force, const char* pubkey_hex); /// API: state/state_merge /// @@ -455,6 +458,116 @@ LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* stat /// - `void` -- Returns Nothing LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs(state_object* state, int enabled); +/// Contact functions + +/// API: state/state_get_contacts +/// +/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex +/// string), if the contact exists, and returns true. If the contact does not exist then `contact` +/// is left unchanged and false is returned. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `contact` -- [out] the contact info data +/// - `session_id` -- [in] null terminated hex string +/// +/// Output: +/// - `bool` -- Returns true if contact exsts +LIBSESSION_EXPORT bool state_get_contacts( + state_object* state, contacts_contact* contact, const char* session_id) + __attribute__((warn_unused_result)); + +/// API: state/state_get_or_construct_contacts +/// +/// Same as the above `state_get_contacts()` except that when the contact does not exist, this sets +/// all the contact fields to defaults and loads it with the given session_id. +/// +/// Returns true as long as it is given a valid session_id. A false return is considered an error, +/// and means the session_id was not a valid session_id. +/// +/// This is the method that should usually be used to create or update a contact, followed by +/// setting fields in the contact, and then giving it to state_set_contacts(). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `contact` -- [out] the contact info data +/// - `session_id` -- [in] null terminated hex string +/// +/// Output: +/// - `bool` -- Returns true if contact exsts +LIBSESSION_EXPORT bool state_get_or_construct_contacts( + state_object* state, contacts_contact* contact, const char* session_id) + __attribute__((warn_unused_result)); + +/// API: state/state_set_contacts +/// +/// Adds or updates a contact from the given contact info struct. +/// +/// Inputs: +/// - `state` -- [in, out] Pointer to the state object +/// - `contact` -- [in] Pointer containing the contact info data +LIBSESSION_EXPORT void state_set_contacts(state_object* state, const contacts_contact* contact); + +// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would +// save very little in actual calling code. The procedure for updating a single field without them +// is simple enough; for example to update `approved` and leave everything else unchanged: +// +// contacts_contact c; +// if (contacts_get_or_construct(conf, &c, some_session_id)) { +// const char* new_nickname = "Joe"; +// c.approved = new_nickname; +// contacts_set_or_create(conf, &c); +// } else { +// // some_session_id was invalid! +// } + +/// API: state/state_erase_contacts +/// +/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was +/// found and removed, false if the contact was not present. You must not call this during +/// iteration; see details below. +/// +/// Inputs: +/// - `state` -- [in, out] Pointer to the state object +/// - `session_id` -- [in] Text containing null terminated hex string +/// +/// Outputs: +/// - `bool` -- True if erasing was successful +LIBSESSION_EXPORT bool state_erase_contacts(state_object* state, const char* session_id); + +/// API: state/state_size_contacts +/// +/// Returns the number of contacts. +/// +/// Inputs: +/// - `state` -- input - Pointer to the state object +/// +/// Outputs: +/// - `size_t` -- number of contacts +LIBSESSION_EXPORT size_t state_size_contacts(const state_object* state); + +/// API: state/state_new_iterator_contacts +/// +/// Starts a new iterator. +/// +/// Functions for iterating through the entire contact list, in sorted order. Intended use is: +/// +/// contacts_contact c; +/// contacts_iterator *it = state_new_iterator_contacts(state); +/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) { +/// // c.session_id, c.nickname, etc. are loaded +/// } +/// contacts_iterator_free(it); +/// +/// It is NOT permitted to add/remove/modify records while iterating. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `contacts_iterator*` -- pointer to the iterator +LIBSESSION_EXPORT contacts_iterator* state_new_iterator_contacts(const state_object* state); + #ifdef __cplusplus } // extern "C" #endif diff --git a/include/session/state.hpp b/include/session/state.hpp index cf0b73cb..5dd94d62 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -82,14 +82,15 @@ struct config_message { class State { private: - // Storage of pubkeys which are currently being suppressed, the value specifies whether the - // `send` or `store` hook is suppressed. - std::map> _open_suppressions = {}; + // Storage of pubkeys which are currently being suppressed, the value specifies how many active + // suppressions the `send` or `store` hooks have. + std::map> _open_suppressions = {}; std::map> _config_groups; protected: Ed25519PubKey _user_pk; Ed25519Secret _user_sk; + std::string _user_x_pk_hex; std::function changes); /// API: state/State::merge /// diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 21a119e2..f0f76365 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -9,6 +9,7 @@ #include "session/config/contacts.h" #include "session/config/error.h" #include "session/export.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -31,10 +32,6 @@ static_assert(CONVO_NOTIFY_ALL == static_cast(notify_mode::all)); static_assert(CONVO_NOTIFY_DISABLED == static_cast(notify_mode::disabled)); static_assert(CONVO_NOTIFY_MENTIONS_ONLY == static_cast(notify_mode::mentions_only)); -LIBSESSION_C_API bool session_id_is_valid(const char* session_id) { - return std::strlen(session_id) == 66 && oxenc::is_hex(session_id, session_id + 66); -} - contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } @@ -59,15 +56,6 @@ Contacts::Contacts( load_key(ed25519_secretkey); } -LIBSESSION_C_API int contacts_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - void contact_info::load(const dict& info_dict) { name = maybe_string(info_dict, "n").value_or(""); nickname = maybe_string(info_dict, "N").value_or(""); @@ -175,21 +163,6 @@ std::optional Contacts::get(std::string_view pubkey_hex) const { return result; } -LIBSESSION_C_API bool contacts_get( - config_object* conf, contacts_contact* contact, const char* session_id) { - try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get(session_id)) { - c->into(*contact); - return true; - } - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return false; -} - contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { if (auto maybe = get(pubkey_hex)) return *std::move(maybe); @@ -197,59 +170,49 @@ contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { return contact_info{std::string{pubkey_hex}}; } -LIBSESSION_C_API bool contacts_get_or_construct( - config_object* conf, contacts_contact* contact, const char* session_id) { - try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct(session_id).into(*contact); - return true; - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; - } -} - void Contacts::set(const contact_info& contact) { - std::string pk = session_id_to_bytes(contact.session_id); - auto info = data["c"][pk]; - - // Always set the name, even if empty, to keep the dict from getting pruned if there are no - // other entries. - info["n"] = contact.name.substr(0, contact_info::MAX_NAME_LENGTH); - set_nonempty_str(info["N"], contact.nickname.substr(0, contact_info::MAX_NAME_LENGTH)); - - set_pair_if( - contact.profile_picture, - info["p"], - contact.profile_picture.url, - info["q"], - contact.profile_picture.key); - - set_flag(info["a"], contact.approved); - set_flag(info["A"], contact.approved_me); - set_flag(info["b"], contact.blocked); - - set_nonzero_int(info["+"], contact.priority); - - auto notify = contact.notifications; - if (notify == notify_mode::mentions_only) - notify = notify_mode::all; - set_positive_int(info["@"], static_cast(notify)); - set_positive_int(info["!"], contact.mute_until); - - set_pair_if( - contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, - info["e"], - static_cast(contact.exp_mode), - info["E"], - contact.exp_timer.count()); - - set_positive_int(info["j"], contact.created); -} - -LIBSESSION_C_API void contacts_set(config_object* conf, const contacts_contact* contact) { - unbox(conf)->set(contact_info{*contact}); + auto changes = [this, &contact]() { + std::string pk = session_id_to_bytes(contact.session_id); + auto info = data["c"][pk]; + + // Always set the name, even if empty, to keep the dict from getting pruned if there are no + // other entries. + info["n"] = contact.name.substr(0, contact_info::MAX_NAME_LENGTH); + set_nonempty_str(info["N"], contact.nickname.substr(0, contact_info::MAX_NAME_LENGTH)); + + set_pair_if( + contact.profile_picture, + info["p"], + contact.profile_picture.url, + info["q"], + contact.profile_picture.key); + + set_flag(info["a"], contact.approved); + set_flag(info["A"], contact.approved_me); + set_flag(info["b"], contact.blocked); + + set_nonzero_int(info["+"], contact.priority); + + auto notify = contact.notifications; + if (notify == notify_mode::mentions_only) + notify = notify_mode::all; + set_positive_int(info["@"], static_cast(notify)); + set_positive_int(info["!"], contact.mute_until); + + set_pair_if( + contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, + info["e"], + static_cast(contact.exp_mode), + info["E"], + contact.exp_timer.count()); + + set_positive_int(info["j"], contact.created); + }; + + if (_parent_state) + (*_parent_state)->perform_while_suppressing_hooks(static_cast(this), changes); + else + changes(); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -317,24 +280,12 @@ bool Contacts::erase(std::string_view session_id) { return ret; } -LIBSESSION_C_API bool contacts_erase(config_object* conf, const char* session_id) { - try { - return unbox(conf)->erase(session_id); - } catch (...) { - return false; - } -} - size_t Contacts::size() const { if (auto* c = data["c"].dict()) return c->size(); return 0; } -LIBSESSION_C_API size_t contacts_size(const config_object* conf) { - return unbox(conf)->size(); -} - /// Load _val from the current iterator position; if it is invalid, skip to the next key until we /// find one that is valid (or hit the end). void Contacts::iterator::_load_info() { @@ -375,11 +326,7 @@ Contacts::iterator& Contacts::iterator::operator++() { return *this; } -LIBSESSION_C_API contacts_iterator* contacts_iterator_new(const config_object* conf) { - auto* it = new contacts_iterator{}; - it->_internals = new Contacts::iterator{unbox(conf)->begin()}; - return it; -} +extern "C" { LIBSESSION_C_API void contacts_iterator_free(contacts_iterator* it) { delete static_cast(it->_internals); @@ -397,3 +344,5 @@ LIBSESSION_C_API bool contacts_iterator_done(contacts_iterator* it, contacts_con LIBSESSION_C_API void contacts_iterator_advance(contacts_iterator* it) { ++*static_cast(it->_internals); } + +} // extern "C" diff --git a/src/state.cpp b/src/state.cpp index 2385fdaa..5040ab69 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -48,9 +48,18 @@ State::State(ustring_view ed25519_secretkey, std::vector dumps) if (ed25519_secretkey.size() != 64) throw std::invalid_argument{"Invalid ed25519_secretkey: expected 64 bytes"}; + // Setup the keys + std::array user_x_pk; std::memcpy(_user_sk.data(), ed25519_secretkey.data(), ed25519_secretkey.size()); crypto_sign_ed25519_sk_to_pk(_user_pk.data(), _user_sk.data()); + if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) + throw std::runtime_error{"Ed25519 pubkey to x25519 pubkey conversion failed"}; + + _user_x_pk_hex.reserve(66); + _user_x_pk_hex += "05"; + oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(_user_x_pk_hex)); + // Load in the dumps auto sorted_dumps = dumps; std::sort(sorted_dumps.begin(), sorted_dumps.end(), [](const auto& a, const auto& b) { @@ -180,61 +189,74 @@ void State::suppress_hooks_start(bool send, bool store, std::string_view pubkey_ log(LogLevel::debug, "suppress_hooks_start: " + std::string(pubkey_hex) + "(send: " + bool_to_string(send) + ", store: " + bool_to_string(store) + ")"); - _open_suppressions[pubkey_hex] = {send, store}; + _open_suppressions[pubkey_hex] = { + _open_suppressions[pubkey_hex].first + (send ? 1 : 0), + _open_suppressions[pubkey_hex].second + (store ? 1 : 0)}; } -void State::suppress_hooks_stop(bool send, bool store, std::string_view pubkey_hex) { +void State::suppress_hooks_stop(bool send, bool store, bool force, std::string_view pubkey_hex) { log(LogLevel::debug, - "suppress_hooks_stop: " + std::string(pubkey_hex) + "(send: " + bool_to_string(send) + - ", store: " + bool_to_string(store) + ")"); + "suppress_hooks_stop: '" + std::string(pubkey_hex) + "' (send: " + bool_to_string(send) + + ", store: " + bool_to_string(store) + ", force: " + bool_to_string(force) + ")"); + + // If `_open_suppressions` doesn't contain a value it'll default to {0, 0} + auto final_send = (send && force ? 0 : _open_suppressions[pubkey_hex].first); + auto final_store = (store && force ? 0 : _open_suppressions[pubkey_hex].second); + + if (!force) { + final_send = + (send ? std::max(0, _open_suppressions[pubkey_hex].first - 1) + : _open_suppressions[pubkey_hex].first); + final_store = + (store ? std::max(0, _open_suppressions[pubkey_hex].second - 1) + : _open_suppressions[pubkey_hex].second); + } - // If `_open_suppressions` doesn't contain a value it'll default to {false, false} - if ((send && store) || (send && !_open_suppressions[pubkey_hex].second) || - (store && !_open_suppressions[pubkey_hex].first)) + if (final_send == 0 && final_store == 0) _open_suppressions.erase(pubkey_hex); - else if (send) - _open_suppressions[pubkey_hex] = {false, _open_suppressions[pubkey_hex].second}; - else if (store) - _open_suppressions[pubkey_hex] = {_open_suppressions[pubkey_hex].first, false}; - - // Trigger the config change hooks if needed with the relevant pubkey information - if (pubkey_hex.substr(0, 2) == "05") - config_changed(std::nullopt); // User config storage - else if (pubkey_hex.empty()) { - // Update all configs (as it's possible this change affected multiple configs) - config_changed(std::nullopt); // User config storage - - for (auto& [key, val] : _config_groups) { - config_changed(key); // Group config storage - } - } else - config_changed(pubkey_hex); // Key-specific configs + else + _open_suppressions[pubkey_hex] = {final_send, final_store}; + + // If the hooks are still suppressed then don't trigger the 'config_changed' call + if (final_send > 0 && final_store > 0) + return; + + // Trigger the config change hooks if needed for the specified config + config_changed(pubkey_hex); + + // If no pubkey was provided then we want to check for changes across all configs, the + // above line will have defaulted to checking the user configs so we now need to check + // all group configs + if (pubkey_hex.empty()) + for (auto& [key, val] : _config_groups) + config_changed(key); } -void State::config_changed(std::optional pubkey_hex) { - std::string target_pubkey_hex; +void State::perform_while_suppressing_hooks( + session::config::ConfigSig* conf, std::function changes) { + std::string target_pubkey_hex = _user_x_pk_hex; + auto sign_pk = conf->get_sig_pubkey(); - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { - // Convert the _user_pk to the user's session ID - std::array user_x_pk; + // If we have a signature pubkey then assume it's a group config, use the `03` prefix with that + // instead of the user x_pk + if (sign_pk) + target_pubkey_hex = "03" + oxenc::to_hex(sign_pk->begin(), sign_pk->end()); - if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) - throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + suppress_hooks_start(true, true, target_pubkey_hex); + changes(); + suppress_hooks_stop(true, true, false, target_pubkey_hex); +} - // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to - // the sender session ID - target_pubkey_hex.reserve(66); - target_pubkey_hex += "05"; - oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey_hex)); - } else - target_pubkey_hex = *pubkey_hex; +void State::config_changed(std::optional pubkey_hex) { + auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); + std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); // Check if there both `send` and `store` hooks are suppressed (and if so ignore this change) - std::pair suppressions = + std::pair suppressions = (_open_suppressions.count(target_pubkey_hex) ? _open_suppressions[target_pubkey_hex] : _open_suppressions[""]); - if (suppressions.first && suppressions.second) { + if (suppressions.first > 0 && suppressions.second > 0) { log(LogLevel::debug, "config_changed: Ignoring due to hooks being suppressed"); return; } @@ -248,13 +270,13 @@ void State::config_changed(std::optional pubkey_hex) { std::chrono::system_clock::now().time_since_epoch()) + network_offset); - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { + if (!is_group_pubkey) { needs_push = - (!suppressions.first && + (suppressions.first == 0 && (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || config_user_groups->needs_push() || config_user_profile->needs_push())); needs_dump = - (!suppressions.second && + (suppressions.second == 0 && (config_contacts->needs_dump() || config_convo_info_volatile->needs_dump() || config_user_groups->needs_dump() || config_user_profile->needs_dump())); configs = { @@ -264,14 +286,12 @@ void State::config_changed(std::optional pubkey_hex) { config_user_profile.get()}; } else { // Other namespaces are unique for a given pubkey_hex_ - if (!pubkey_hex) - throw std::invalid_argument{ - "config_changed: Invalid pubkey_hex - required for group config changes"}; if (target_pubkey_hex.size() != 66) throw std::invalid_argument{"config_changed: Invalid pubkey_hex - expected 66 bytes"}; if (!_config_groups.count(target_pubkey_hex)) throw std::runtime_error{ - "config_changed: Change trigger in group configs with no state"}; + "config_changed: Change trigger in group configs with no state: " + + target_pubkey_hex}; // Ensure we have the admin key for the group auto user_group_info = config_user_groups->get_group(target_pubkey_hex); @@ -283,12 +303,12 @@ void State::config_changed(std::optional pubkey_hex) { // Only group admins can push group config changes needs_push = - (!suppressions.first && !user_group_info->secretkey.empty() && + (suppressions.first == 0 && !user_group_info->secretkey.empty() && (_config_groups[target_pubkey_hex]->config_info->needs_push() || _config_groups[target_pubkey_hex]->config_members->needs_push() || _config_groups[target_pubkey_hex]->config_keys->pending_config())); needs_dump = - (!suppressions.second && + (suppressions.second == 0 && (_config_groups[target_pubkey_hex]->config_info->needs_dump() || _config_groups[target_pubkey_hex]->config_members->needs_dump() || _config_groups[target_pubkey_hex]->config_keys->needs_dump())); @@ -299,16 +319,16 @@ void State::config_changed(std::optional pubkey_hex) { } std::string send_info = - (suppressions.first ? "send suppressed" - : ("needs send: " + bool_to_string(needs_push))); + (suppressions.first > 0 ? "send suppressed" + : ("needs send: " + bool_to_string(needs_push))); std::string store_info = - (suppressions.second ? "store suppressed" - : ("needs store: " + bool_to_string(needs_dump))); + (suppressions.second > 0 ? "store suppressed" + : ("needs store: " + bool_to_string(needs_dump))); log(LogLevel::debug, "config_changed: " + info_title + " (" + send_info + ", " + store_info + ")"); // Call the hook to store the dump if needed - if (_store && needs_dump && !suppressions.second) { + if (_store && needs_dump && suppressions.second == 0) { for (auto& config : configs) { if (!config->needs_dump()) continue; @@ -321,7 +341,7 @@ void State::config_changed(std::optional pubkey_hex) { } // GroupKeys needs special handling as it's not a `ConfigBase` - if (pubkey_hex && _config_groups[target_pubkey_hex]->config_keys->needs_dump()) { + if (is_group_pubkey && _config_groups[target_pubkey_hex]->config_keys->needs_dump()) { log(LogLevel::debug, "config_changed: Group Keys config for " + target_pubkey_hex + " needs_dump"); auto keys_config = _config_groups[target_pubkey_hex]->config_keys.get(); @@ -334,7 +354,7 @@ void State::config_changed(std::optional pubkey_hex) { } // Call the hook to perform a push if needed - if (_send && needs_push && !suppressions.first) { + if (_send && needs_push && suppressions.first == 0) { std::vector requests; std::vector obsolete_hashes; @@ -388,7 +408,7 @@ void State::config_changed(std::optional pubkey_hex) { } // GroupKeys needs special handling as it's not a `ConfigBase` - if (pubkey_hex) { + if (is_group_pubkey) { auto config = _config_groups[target_pubkey_hex]->config_keys.get(); auto pending = config->pending_config(); @@ -512,23 +532,8 @@ std::vector State::merge( bool is_group_merge = false; std::vector good_hashes; std::vector> pending_configs; - std::string target_pubkey_hex; - - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { - // Convert the _user_pk to the user's session ID - std::array user_x_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) - throw std::runtime_error{ - "merge: Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - - // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to - // the sender session ID - target_pubkey_hex.reserve(66); - target_pubkey_hex += "05"; - oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(target_pubkey_hex)); - } else - target_pubkey_hex = *pubkey_hex; + auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); + std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); // Suppress triggering the `send` hook until the merge is complete suppress_hooks_start(true, false, target_pubkey_hex); @@ -612,7 +617,7 @@ std::vector State::merge( // Now that all of the merges have been completed we stop suppressing the `send` hook which // will be triggered if there is a pending push - suppress_hooks_stop(true, false, target_pubkey_hex); + suppress_hooks_stop(true, false, false, target_pubkey_hex); log(LogLevel::debug, "merge: Complete"); return good_hashes; @@ -621,7 +626,7 @@ std::vector State::merge( std::vector State::current_hashes(std::optional pubkey_hex) { std::vector result; - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") { + if (!pubkey_hex || pubkey_hex->empty() || pubkey_hex->substr(0, 2) == "05") { auto contact_hashes = config_contacts->current_hashes(); auto convo_info_volatile_hashes = config_convo_info_volatile->current_hashes(); auto user_group_hashes = config_user_groups->current_hashes(); diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index d483f267..6d17bedf 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -10,6 +10,7 @@ #include "config/internal.hpp" #include "session/config/base.hpp" +#include "session/config/contacts.h" #include "session/config/contacts.hpp" #include "session/config/convo_info_volatile.hpp" #include "session/config/namespaces.h" @@ -50,6 +51,14 @@ bool set_error(state_object* state, std::string_view e) { extern "C" { +// Util Functions + +LIBSESSION_C_API bool session_id_is_valid(const char* session_id) { + return std::strlen(session_id) == 66 && oxenc::is_hex(session_id, session_id + 66); +} + +// State Functions + LIBSESSION_EXPORT void state_free(state_object* state) { delete state; } @@ -232,13 +241,13 @@ LIBSESSION_C_API bool state_suppress_hooks_start( } LIBSESSION_C_API bool state_suppress_hooks_stop( - state_object* state, bool send, bool store, const char* pubkey_hex_) { + state_object* state, bool send, bool store, bool force, const char* pubkey_hex_) { try { std::string_view pubkey_hex = ""; if (pubkey_hex_) pubkey_hex = {pubkey_hex_, 66}; - unbox(state).suppress_hooks_stop(send, store, pubkey_hex); + unbox(state).suppress_hooks_stop(send, store, force, pubkey_hex); return true; } catch (const std::exception& e) { return set_error(state, e.what()); @@ -407,4 +416,52 @@ LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int unbox(state).config_user_profile->set_blinded_msgreqs(std::move(val)); } +// Contact Functions + +LIBSESSION_C_API bool state_get_contacts( + state_object* state, contacts_contact* contact, const char* session_id) { + try { + if (auto c = unbox(state).config_contacts->get(session_id)) { + c->into(*contact); + return true; + } + } catch (const std::exception& e) { + set_error(state, e.what()); + } + return false; +} + +LIBSESSION_C_API bool state_get_or_construct_contacts( + state_object* state, contacts_contact* contact, const char* session_id) { + try { + unbox(state).config_contacts->get_or_construct(session_id).into(*contact); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_set_contacts(state_object* state, const contacts_contact* contact) { + unbox(state).config_contacts->set(contact_info{*contact}); +} + +LIBSESSION_C_API bool state_erase_contacts(state_object* state, const char* session_id) { + try { + return unbox(state).config_contacts->erase(session_id); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API size_t state_size_contacts(const state_object* state) { + return unbox(state).config_contacts->size(); +} + +LIBSESSION_C_API contacts_iterator* state_new_iterator_contacts(const state_object* state) { + auto* it = new contacts_iterator{}; + auto it2 = unbox(state).config_contacts->begin(); + it->_internals = new Contacts::iterator{unbox(state).config_contacts->begin()}; + return it; +} + } // extern "C" \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e60bdddf..550bd2f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,6 +31,7 @@ target_link_libraries(testAll PRIVATE libsession::onionreq libsession::state libsodium::sodium-internal + nlohmann_json::nlohmann_json Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 17883d75..b97dabc1 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -225,164 +225,6 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(nicknames[1] == "Nickname 3"); } -TEST_CASE("Contacts (C API)", "[config][contacts][c]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(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)); - - config_object* conf; - REQUIRE(0 == contacts_init(&conf, ed_sk.data(), NULL, 0, NULL)); - - const char* const definitely_real_id = - "050000000000000000000000000000000000000000000000000000000000000000"; - - contacts_contact c; - CHECK_FALSE(contacts_get(conf, &c, definitely_real_id)); - - CHECK(contacts_get_or_construct(conf, &c, definitely_real_id)); - - CHECK(c.session_id == std::string_view{definitely_real_id}); - CHECK(strlen(c.name) == 0); - CHECK(strlen(c.nickname) == 0); - CHECK_FALSE(c.approved); - CHECK_FALSE(c.approved_me); - CHECK_FALSE(c.blocked); - CHECK(strlen(c.profile_pic.url) == 0); - CHECK(c.created == 0); - - strcpy(c.name, "Joe"); - strcpy(c.nickname, "Joey"); - c.approved = true; - c.approved_me = true; - c.created = created_ts; - - contacts_set(conf, &c); - - contacts_contact c2; - REQUIRE(contacts_get(conf, &c2, definitely_real_id)); - - CHECK(c2.name == "Joe"sv); - CHECK(c2.nickname == "Joey"sv); - CHECK(c2.approved); - CHECK(c2.approved_me); - CHECK_FALSE(c2.blocked); - CHECK(strlen(c2.profile_pic.url) == 0); - - CHECK(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); - - config_push_data* to_push = config_push(conf); - CHECK(to_push->seqno == 1); - - config_object* conf2; - REQUIRE(contacts_init(&conf2, ed_sk.data(), NULL, 0, NULL) == 0); - - const char* merge_hash[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - merge_hash[0] = "fakehash1"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - 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); - - config_confirm_pushed(conf, to_push->seqno, "fakehash1"); - free(to_push); - - contacts_contact c3; - REQUIRE(contacts_get(conf2, &c3, definitely_real_id)); - CHECK(c3.name == "Joe"sv); - CHECK(c3.nickname == "Joey"sv); - CHECK(c3.approved); - CHECK(c3.approved_me); - CHECK_FALSE(c3.blocked); - CHECK(strlen(c3.profile_pic.url) == 0); - CHECK(c3.created == created_ts); - - auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; - REQUIRE(contacts_get_or_construct(conf, &c3, another_id)); - CHECK(strlen(c3.name) == 0); - CHECK(strlen(c3.nickname) == 0); - CHECK_FALSE(c3.approved); - CHECK_FALSE(c3.approved_me); - CHECK_FALSE(c3.blocked); - CHECK(strlen(c3.profile_pic.url) == 0); - CHECK(c3.created == 0); - - contacts_set(conf2, &c3); - - to_push = config_push(conf2); - - merge_hash[0] = "fakehash2"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - accepted = config_merge(conf, merge_hash, merge_data, merge_size, 1); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash2"sv); - free(accepted); - - config_confirm_pushed(conf2, to_push->seqno, "fakehash2"); - - REQUIRE(to_push->obsolete_len > 0); - CHECK(to_push->obsolete_len == 1); - CHECK(to_push->obsolete[0] == "fakehash1"sv); - free(to_push); - - // Iterate through and make sure we got everything we expected - std::vector session_ids; - std::vector nicknames; - - CHECK(contacts_size(conf) == 2); - contacts_iterator* it = contacts_iterator_new(conf); - contacts_contact ci; - for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { - session_ids.push_back(ci.session_id); - nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); - } - contacts_iterator_free(it); - - REQUIRE(session_ids.size() == 2); - CHECK(session_ids[0] == definitely_real_id); - CHECK(session_ids[1] == another_id); - CHECK(nicknames[0] == "Joey"); - CHECK(nicknames[1] == "(N/A)"); - - // Changing things while iterating: - it = contacts_iterator_new(conf); - int deletions = 0, non_deletions = 0; - std::vector contacts_to_remove; - while (!contacts_iterator_done(it, &ci)) { - if (ci.session_id != std::string_view{definitely_real_id}) { - contacts_to_remove.push_back(ci.session_id); - deletions++; - } else { - non_deletions++; - } - contacts_iterator_advance(it); - } - for (auto& cont : contacts_to_remove) - contacts_erase(conf, cont.c_str()); - - CHECK(deletions == 1); - CHECK(non_deletions == 1); - - CHECK(contacts_get(conf, &ci, definitely_real_id)); - CHECK_FALSE(contacts_get(conf, &ci, another_id)); -} - TEST_CASE("huge contacts compression", "[config][compression][contacts]") { // Test that we can produce a config message whose *uncompressed* length exceeds the maximum // message length as long as its *compressed* length does not. diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index a4ff4af4..7e37f42e 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -1,11 +1,11 @@ #include -#include #include -#include #include #include #include +#include +#include #include #include "utils.hpp" @@ -17,7 +17,7 @@ void log_msg(session::config::LogLevel lvl, std::string msg) { INFO((lvl == session::config::LogLevel::error ? "ERROR" : lvl == session::config::LogLevel::warning ? "Warning" : lvl == session::config::LogLevel::info ? "Info" - : "debug") + : "debug") << ": " << msg); } @@ -83,7 +83,8 @@ TEST_CASE("user profile", "[config][user_profile]") { { // These don't stay alive, so we use set_key/set_url to make a local copy: ustring key = "secret78901234567890123456789012"_bytes; - std::string url = "http://example.org/omg-pic-123.bmp"; // NB: length must be < sizeof(p.url)! + std::string url = + "http://example.org/omg-pic-123.bmp"; // NB: length must be < sizeof(p.url)! p.set_key(std::move(key)); p.url = std::move(url); } @@ -109,8 +110,8 @@ TEST_CASE("user profile", "[config][user_profile]") { CHECK(conf.needs_push()); CHECK(conf.needs_dump()); std::tie(seqno, to_push, obs) = conf.push(); - CHECK(seqno == 1); // incremented since we made changes (this only increments once - // between dumps; even though we changed two fields here). + CHECK(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; @@ -150,7 +151,7 @@ TEST_CASE("user profile", "[config][user_profile]") { CHECK(conf.needs_dump()); // We did call push, but we haven't confirmed it as stored yet, so this will still return true: CHECK(conf.needs_push()); - + auto dump1 = conf.dump(); // (in a real client we'd now store this to disk) @@ -170,8 +171,8 @@ TEST_CASE("user profile", "[config][user_profile]") { conf.confirm_pushed(seqno, "fakehash1"); CHECK_FALSE(conf.needs_push()); - CHECK(conf.needs_dump()); // The confirmation changes state, so this makes us need a dump - // again. + CHECK(conf.needs_dump()); // The confirmation changes state, so this makes us need a dump + // again. dump1 = conf.dump(); // clang-format off diff --git a/tests/test_state.cpp b/tests/test_state.cpp index b93e5ce8..e23bd2d4 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -1,5 +1,9 @@ +#include + #include +#include +#include "session/config/contacts.h" #include "session/config/namespaces.hpp" #include "session/config/user_profile.hpp" #include "session/state.h" @@ -12,6 +16,44 @@ using namespace session; using namespace session::state; using namespace session::config; +static constexpr int64_t created_ts = 1680064059; +struct last_store_data { + config::Namespace namespace_; + std::string pubkey; + uint64_t timestamp; + ustring data; +}; +struct last_send_data { + std::string pubkey; + ustring data; + ustring ctx; +}; + +void c_store_callback( + NAMESPACE namespace_, + const char* pubkey, + uint64_t timestamp_ms, + const unsigned char* data, + size_t data_len, + void* ctx) { + *static_cast(ctx) = last_store_data{ + static_cast(namespace_), + {pubkey, 64}, + timestamp_ms, + {data, data_len}}; +} + +void c_send_callback( + const char* pubkey, + const unsigned char* data, + size_t data_len, + const unsigned char* request_ctx, + size_t request_ctx_len, + void* ctx) { + *static_cast(ctx) = + last_send_data{{pubkey, 64}, {data, data_len}, {request_ctx, request_ctx_len}}; +} + std::string replace_suffix_between( std::string_view value, int suffix_start_distance_from_end, @@ -30,46 +72,35 @@ TEST_CASE("State", "[state][state]") { "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto state = State({ed_sk.data(), ed_sk.size()}, {}); - std::optional last_send_pubkey = std::nullopt; - std::optional last_send_data = std::nullopt; - std::optional last_send_ctx = std::nullopt; - std::optional last_store_namespace = std::nullopt; - std::optional last_store_pubkey = std::nullopt; - std::optional last_store_timestamp = std::nullopt; - std::optional last_store_data = std::nullopt; - - state.onStore( - [&last_store_namespace, &last_store_pubkey, &last_store_timestamp, &last_store_data]( - config::Namespace namespace_, - std::string pubkey, - uint64_t timestamp_ms, - ustring data) { - last_store_namespace = namespace_; - last_store_pubkey = pubkey; - last_store_timestamp = timestamp_ms; - last_store_data = data; - }); - state.onSend([&last_send_pubkey, &last_send_data, &last_send_ctx]( - std::string pubkey, ustring data, ustring ctx) { - last_send_pubkey = pubkey; - last_send_data = data; - last_send_ctx = ctx; + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + + state.onStore([&last_store]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + last_store = {namespace_, pubkey, timestamp_ms, data}; + }); + state.onSend([&last_send](std::string pubkey, ustring data, ustring ctx) { + last_send = {pubkey, data, ctx}; }); // Sanity check direct config access CHECK_FALSE(state.config_user_profile->get_name().has_value()); state.config_user_profile->set_name("Test Name"); CHECK(state.config_user_profile->get_name() == "Test Name"); - CHECK(*last_store_namespace == Namespace::UserProfile); - CHECK(*last_store_pubkey == + CHECK(last_store->namespace_ == Namespace::UserProfile); + CHECK(last_store->pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); - CHECK(oxenc::to_hex(last_store_data->begin(), last_store_data->end()) == + CHECK(oxenc::to_hex(last_store->data.begin(), last_store->data.end()) == "64313a21693165313a2436353a64313a23693165313a266465313a3c6c6c69306533323aea173b57beca8af1" "8c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d646565313a28303a313a296c65" "65"); - CHECK(*last_send_pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" - "6"); - auto send_data_no_ts = replace_suffix_between(to_sv(*last_send_data), (13 + 22), 22, "0"); + CHECK(last_send->pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" + "6"); + auto send_data_no_ts = replace_suffix_between(to_sv(last_send->data), (13 + 22), 22, "0"); auto send_data_no_sig = replace_suffix_between(send_data_no_ts, (37 + 88), 37, "sig"); CHECK(send_data_no_sig == "{\"method\":\"sequence\",\"params\":{\"requests\":[{\"method\":\"store\",\"params\":{" @@ -87,7 +118,7 @@ TEST_CASE("State", "[state][state]") { "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"pubkey_" "ed25519\":\"8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f\"," "\"signature\":\"sig\",\"timestamp\":0,\"ttl\":2592000000}}]}}"); - CHECK(to_sv(*last_send_ctx) == + CHECK(to_sv(last_send->ctx) == "{\"namespaces\":[2],\"pubkey\":" "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"seqnos\":[1]," "\"type\":2}"); @@ -141,3 +172,198 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(state_load(state2, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); CHECK(state_get_profile_name(state2) == "Test Name"sv); } + +TEST_CASE("State contacts (C API)", "[state][contacts][c]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + std::optional last_store_2 = std::nullopt; + std::optional last_send_2 = std::nullopt; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + + const char* const definitely_real_id = + "050000000000000000000000000000000000000000000000000000000000000000"; + + contacts_contact c; + CHECK_FALSE(state_get_contacts(state, &c, definitely_real_id)); + + CHECK(state_get_or_construct_contacts(state, &c, definitely_real_id)); + + CHECK(c.session_id == std::string_view{definitely_real_id}); + CHECK(strlen(c.name) == 0); + CHECK(strlen(c.nickname) == 0); + CHECK_FALSE(c.approved); + CHECK_FALSE(c.approved_me); + CHECK_FALSE(c.blocked); + CHECK(strlen(c.profile_pic.url) == 0); + CHECK(c.created == 0); + + strcpy(c.name, "Joe"); + strcpy(c.nickname, "Joey"); + c.approved = true; + c.approved_me = true; + c.created = created_ts; + + state_set_contacts(state, &c); + + contacts_contact c2; + REQUIRE(state_get_contacts(state, &c2, definitely_real_id)); + + CHECK(c2.name == "Joe"sv); + CHECK(c2.nickname == "Joey"sv); + CHECK(c2.approved); + CHECK(c2.approved_me); + CHECK_FALSE(c2.blocked); + CHECK(strlen(c2.profile_pic.url) == 0); + + CHECK((*last_store).pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f"); + CHECK((*last_send).pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" + "f"); + + auto ctx_json = nlohmann::json::parse(last_send->ctx); + + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 1); + + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(last_send->data); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + config_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash1", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash1"sv); + free(accepted); + free(merge_data); + + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + contacts_contact c3; + REQUIRE(state_get_contacts(state2, &c3, definitely_real_id)); + CHECK(c3.name == "Joe"sv); + CHECK(c3.nickname == "Joey"sv); + CHECK(c3.approved); + CHECK(c3.approved_me); + CHECK_FALSE(c3.blocked); + CHECK(strlen(c3.profile_pic.url) == 0); + CHECK(c3.created == created_ts); + + contacts_contact c4; + auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; + REQUIRE(state_get_or_construct_contacts(state, &c4, another_id)); + CHECK(strlen(c4.name) == 0); + CHECK(strlen(c4.nickname) == 0); + CHECK_FALSE(c4.approved); + CHECK_FALSE(c4.approved_me); + CHECK_FALSE(c4.blocked); + CHECK(strlen(c4.profile_pic.url) == 0); + CHECK(c4.created == 0); + + state_set_contacts(state2, &c4); + + auto last_send_json_2 = nlohmann::json::parse(last_send_2->data); + REQUIRE(last_send_json_2.contains(first_request_data)); + auto last_send_data_2 = to_unsigned( + oxenc::from_base64(last_send_json_2[first_request_data].get())); + merge_data = new state_config_message[1]; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash2", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; + REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash2"sv); + free(accepted); + free(merge_data); + + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); + CHECK(state_received_send_response( + state2, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); + REQUIRE(last_send_json_2.contains(messages_key)); + auto obsolete = last_send_json_2[messages_key].get>(); + REQUIRE(obsolete.size() > 0); + CHECK(obsolete.size() == 1); + CHECK(obsolete[0] == "fakehash1"sv); + + // Iterate through and make sure we got everything we expected + std::vector session_ids; + std::vector nicknames; + + CHECK(state_size_contacts(state) == 2); + contacts_iterator* it = state_new_iterator_contacts(state); + contacts_contact ci; + for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { + session_ids.push_back(ci.session_id); + nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); + } + contacts_iterator_free(it); + + REQUIRE(session_ids.size() == 2); + CHECK(session_ids[0] == definitely_real_id); + CHECK(session_ids[1] == another_id); + CHECK(nicknames[0] == "Joey"); + CHECK(nicknames[1] == "(N/A)"); + + // Changing things while iterating: + it = state_new_iterator_contacts(state); + int deletions = 0, non_deletions = 0; + std::vector contacts_to_remove; + while (!contacts_iterator_done(it, &ci)) { + if (ci.session_id != std::string_view{definitely_real_id}) { + contacts_to_remove.push_back(ci.session_id); + deletions++; + } else { + non_deletions++; + } + contacts_iterator_advance(it); + } + for (auto& cont : contacts_to_remove) + state_erase_contacts(state, cont.c_str()); + + CHECK(deletions == 1); + CHECK(non_deletions == 1); + + CHECK(state_get_contacts(state, &ci, definitely_real_id)); + CHECK_FALSE(state_get_contacts(state, &ci, another_id)); +} + From 5a305f0fee03843d7b7b4d434b6d3ac2e5203b93 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 14 Feb 2024 13:38:23 +1100 Subject: [PATCH 07/24] Updated to Immutable/Mutable access approach, reverted unneeded changes Moved the state-based functions back into their respective cpp files (class was getting too large) UserGroups & ConvoInfoVolatile C APIs running via the state object --- include/session/config/base.hpp | 14 +- include/session/config/contacts.h | 117 ++++ include/session/config/contacts.hpp | 5 +- include/session/config/convo_info_volatile.h | 498 +++++------------- .../session/config/convo_info_volatile.hpp | 5 +- include/session/config/groups/info.hpp | 3 +- include/session/config/groups/keys.hpp | 27 +- include/session/config/groups/members.hpp | 3 +- include/session/config/user_groups.h | 404 +++++--------- include/session/config/user_groups.hpp | 5 +- include/session/config/user_profile.h | 127 +++++ include/session/config/user_profile.hpp | 5 +- include/session/state.h | 297 +++-------- include/session/state.hpp | 223 +++++--- src/config/base.cpp | 22 - src/config/contacts.cpp | 135 +++-- src/config/convo_info_volatile.cpp | 220 ++++---- src/config/groups/info.cpp | 5 +- src/config/groups/keys.cpp | 24 +- src/config/groups/members.cpp | 5 +- src/config/user_groups.cpp | 253 ++++----- src/config/user_profile.cpp | 78 ++- src/state.cpp | 350 ++++++------ src/state_c_wrapper.cpp | 193 ++----- tests/test_config_contacts.cpp | 218 ++++++++ tests/test_config_convo_info_volatile.cpp | 448 ++++++++++------ tests/test_config_user_groups.cpp | 116 ++-- tests/test_state.cpp | 308 ++--------- tests/utils.hpp | 39 ++ 29 files changed, 2099 insertions(+), 2048 deletions(-) create mode 100644 include/session/config/user_profile.h diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 606e4686..2789b5ca 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -14,10 +14,6 @@ #include "base.h" #include "namespaces.hpp" -namespace session::state { -class State; -} - namespace session::config { template @@ -170,10 +166,6 @@ class ConfigBase : public ConfigSig { std::unordered_set _old_hashes; protected: - // The parent state which owns this config object. By providing a pointer to the parent state - // we can inform the parent when changes occur. - std::optional _parent_state; - // Constructs a base config by loading the data from a dump as produced by `dump()`. If the // dump is nullopt then an empty base config is constructed with no config settings and seqno // set to 0. @@ -183,7 +175,6 @@ class ConfigBase : public ConfigSig { // verification of incoming messages using the associated pubkey, and will be signed using the // secretkey (if a secret key is given). explicit ConfigBase( - std::optional parent_state = std::nullopt, std::optional dump = std::nullopt, std::optional ed25519_pubkey = std::nullopt, std::optional ed25519_secretkey = std::nullopt); @@ -198,7 +189,10 @@ class ConfigBase : public ConfigSig { void set_state(ConfigState s); // Invokes the `logger` callback if set, does nothing if there is no logger. - void log(LogLevel lvl, std::string msg); + void log(LogLevel lvl, std::string msg) { + if (logger) + logger(lvl, std::move(msg)); + } // Returns a reference to the current MutableConfigMessage. If the current message is not // already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter. diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 388af83f..393db90e 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -4,6 +4,7 @@ extern "C" { #endif +#include "../state.h" #include "expiring.h" #include "notify.h" #include "profile_pic.h" @@ -35,10 +36,126 @@ typedef struct contacts_contact { } contacts_contact; +/// API: contacts/state_get_contact +/// +/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex +/// string), if the contact exists, and returns true. If the contact does not exist then `contact` +/// is left unchanged and false is returned. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `contact` -- [out] the contact info data +/// - `session_id` -- [in] null terminated hex string +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +/// +/// Output: +/// - `bool` -- Returns true if contact exsts +LIBSESSION_EXPORT bool state_get_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) + __attribute__((warn_unused_result)); + +/// API: contacts/state_get_or_construct_contact +/// +/// Same as the above `state_get_contact()` except that when the contact does not exist, this sets +/// all the contact fields to defaults and loads it with the given session_id. +/// +/// Returns true as long as it is given a valid session_id. A false return is considered an error, +/// and means the session_id was not a valid session_id. +/// +/// This is the method that should usually be used to create or update a contact, followed by +/// setting fields in the contact, and then giving it to state_set_contact(). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `contact` -- [out] the contact info data +/// - `session_id` -- [in] null terminated hex string +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +/// +/// Output: +/// - `bool` -- Returns true if contact exsts +LIBSESSION_EXPORT bool state_get_or_construct_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) + __attribute__((warn_unused_result)); + +/// API: contacts/state_set_contact +/// +/// Adds or updates a contact from the given contact info struct. +/// +/// Inputs: +/// - `state` -- [in, out] Pointer to the mutable state object +/// - `contact` -- [in] Pointer containing the contact info data +LIBSESSION_EXPORT void state_set_contact( + mutable_state_user_object* state, const contacts_contact* contact); + +// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would +// save very little in actual calling code. The procedure for updating a single field without them +// is simple enough; for example to update `approved` and leave everything else unchanged: +// +// contacts_contact c; +// if (state_get_or_construct_contact(conf, &c, some_session_id)) { +// const char* new_nickname = "Joe"; +// c.approved = new_nickname; +// contacts_set_or_create(conf, &c); +// } else { +// // some_session_id was invalid! +// } + +/// API: contacts/state_erase_contact +/// +/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was +/// found and removed, false if the contact was not present. You must not call this during +/// iteration; see details below. +/// +/// Inputs: +/// - `state` -- [in, out] Pointer to the mutable state object +/// - `session_id` -- [in] Text containing null terminated hex string +/// +/// Outputs: +/// - `bool` -- True if erasing was successful +LIBSESSION_EXPORT bool state_erase_contact( + mutable_state_user_object* state, const char* session_id); + +/// API: contacts/state_size_contacts +/// +/// Returns the number of contacts. +/// +/// Inputs: +/// - `state` -- input - Pointer to the state object +/// +/// Outputs: +/// - `size_t` -- number of contacts +LIBSESSION_EXPORT size_t state_size_contacts(const state_object* state); + typedef struct contacts_iterator { void* _internals; } contacts_iterator; +/// API: contacts/contacts_iterator_new +/// +/// Starts a new iterator. +/// +/// Functions for iterating through the entire contact list, in sorted order. Intended use is: +/// +/// contacts_contact c; +/// contacts_iterator *it = contacts_iterator_new(state); +/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) { +/// // c.session_id, c.nickname, etc. are loaded +/// } +/// contacts_iterator_free(it); +/// +/// It is NOT permitted to add/remove/modify records while iterating. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `contacts_iterator*` -- pointer to the iterator +LIBSESSION_EXPORT contacts_iterator* contacts_iterator_new(const state_object* state); + /// API: contacts/contacts_iterator_free /// /// Frees an iterator once no longer needed. diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 19679c2d..7ac4b237 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -118,10 +118,7 @@ class Contacts : public ConfigBase { /// /// Outputs: /// - `Contact` - Constructor - Contacts( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + Contacts(ustring_view ed25519_secretkey, std::optional dumped); /// API: contacts/Contacts::storage_namespace /// diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 6ddba3eb..20e61935 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -4,6 +4,7 @@ extern "C" { #endif +#include "../state.h" #include "base.h" #include "profile_pic.h" @@ -38,111 +39,59 @@ typedef struct convo_info_volatile_legacy_group { bool unread; // true if marked unread } convo_info_volatile_legacy_group; -/// API: convo_info_volatile/convo_info_volatile_init -/// -/// Constructs a conversations config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT convo_info_volatile_init( -/// [out] config_object** conf, -/// [in] unsigned char* ed25519_secretkey, -/// [in, optional] unsigned char* dump, -/// [in, optional] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `ed25519_secretkey` -- [out] must be the 32-byte secret key seed value. (You can also pass -/// the pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first -/// 32 bytes of that are the seed). This field cannot be null. -/// -/// - `dump` -- [in, optional] if non-NULL this restores the state from the dumped byte string -/// produced by a past instantiation's call to `dump()`. To construct a new, empty object this -/// should be NULL. -/// -/// - `dumplen` -- [in, optional] the length of `dump` when restoring from a dump, or 0 when `dump` -/// is NULL. -/// -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` --Returns 0 on success; returns a non-zero error code and write the exception message as -/// a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int convo_info_volatile_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: convo_info_volatile/convo_info_volatile_get_1to1 +/// API: convo_info_volatile/state_get_convo_info_volatile_1to1 /// /// Fills `convo` with the conversation info given a session ID (specified as a null-terminated hex /// string), if the conversation exists, and returns true. If the conversation does not exist then /// `convo` is left unchanged and false is returned. If an error occurs, false is returned and -/// `conf->last_error` will be set to non-NULL containing the error string (if no error occurs, such -/// as in the case where the conversation merely doesn't exist, `last_error` will be set to NULL). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_1to1( -/// [in] config_object* conf, -/// [out] convo_info_volatile_1to1* convo, -/// [in] const char* session_id -/// ); -/// ``` +/// the error buffer will be set to non-NULL containing the error string (if no error occurs, such +/// as in the case where the conversation merely doesn't exist, the error buffer will not be set). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the costatenfig object /// - `convo` -- [out] Pointer to conversation info /// - `session_id` -- [in] Null terminated hex string of the session_id +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the conversation exists -LIBSESSION_EXPORT bool convo_info_volatile_get_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_1to1 +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_1to1 /// -/// Same as the above convo_info_volatile_get_1to1 except that when the conversation does not exist, -/// this sets all the convo fields to defaults and loads it with the given session_id. +/// Same as the above state_get_convo_info_volatile_1to1 except that when the conversation does not +/// exist, this sets all the convo fields to defaults and loads it with the given session_id. /// /// Returns true as long as it is given a valid session_id. A false return is considered an error, -/// and means the session_id was not a valid session_id. In such a case `conf->last_error` will be +/// and means the session_id was not a valid session_id. In such a case the error buffer will be /// set to an error string. /// /// This is the method that should usually be used to create or update a conversation, followed by -/// setting fields in the convo, and then giving it to convo_info_volatile_set(). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_or_construct_1to1( -/// [in] config_object* conf, -/// [out] convo_info_volatile_1to1* convo, -/// [in] const char* session_id -/// ); -/// ``` +/// setting fields in the convo, and then giving it to state_set_convo_info_volatile(). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to conversation info /// - `session_id` -- [in] Null terminated hex string of the session_id +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the conversation exists -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_community +/// API: convo_info_volatile/state_get_convo_info_volatile_community /// /// community versions of the 1-to-1 functions: /// @@ -151,31 +100,25 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_1to1( /// /// Error handling works the same as the 1-to-1 version. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_community( -/// [in] config_object* conf, -/// [out] convo_info_volatile_community* comm, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `comm` -- [out] Pointer to community info structure /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the community exists -LIBSESSION_EXPORT bool convo_info_volatile_get_community( - config_object* conf, +LIBSESSION_EXPORT bool state_get_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* comm, const char* base_url, - const char* room) __attribute__((warn_unused_result)); + const char* room, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_community +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_community /// /// Gets a community convo info, but if the community does not exist will set all the fileds to /// defaults and load it. `base_url` and `room` are null-terminated c strings; pubkey is 32 bytes. @@ -193,404 +136,278 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_community( /// /// Error handling works the same as the 1-to-1 version. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_or_construct_community( -/// [in] config_object* conf, -/// [out] convo_info_volatile_community* comm, -/// [in] const char* base_url, -/// [in] const char* room, -/// [in] unsigned const char* pubkey -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to community info structure /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string /// - `pubkey` -- [in] 32 byte binary data of the pubkey +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( - config_object* conf, +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* convo, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_group +/// API: convo_info_volatile/state_get_convo_info_volatile_group /// /// Fills `convo` with the conversation info given a group ID (specified as a null-terminated /// hex string), if the conversation exists, and returns true. If the conversation does not exist -/// then `convo` is left unchanged and false is returned. On error, false is returned and the error -/// is set in conf->last_error (on non-error, last_error is cleared). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_group* convo, -/// [in] const char* id -/// ); -/// ``` +/// then `convo` is left unchanged and false is returned. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to group /// - `id` -- [in] Null terminated hex string (66 chars, beginning with 03) specifying the ID of the /// group +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the group exists -LIBSESSION_EXPORT bool convo_info_volatile_get_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) +LIBSESSION_EXPORT bool state_get_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_group +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_group /// /// Same as the above except that when the conversation does not exist, this sets all the convo /// fields to defaults and loads it with the given id. /// /// Returns true as long as it is given a valid group id (i.e. 66 hex chars beginning with "03"). A /// false return is considered an error, and means the id was not a valid session id; an error -/// string will be set in `conf->last_error`. +/// string will be set in the error buffer. /// /// This is the method that should usually be used to create or update a conversation, followed by /// setting fields in the convo, and then giving it to convo_info_volatile_set(). /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_or_construct_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_group* convo, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to group /// - `id` -- [in] Null terminated hex string specifying the ID of the group +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_legacy_group +/// API: convo_info_volatile/state_get_convo_info_volatile_legacy_group /// /// Fills `convo` with the conversation info given a legacy group ID (specified as a null-terminated /// hex string), if the conversation exists, and returns true. If the conversation does not exist /// then `convo` is left unchanged and false is returned. On error, false is returned and the error -/// is set in conf->last_error (on non-error, last_error is cleared). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_legacy_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_legacy_group* convo, -/// [in] const char* id -/// ); -/// ``` +/// is set in the error buffer. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to legacy group /// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the legacy group exists -LIBSESSION_EXPORT bool convo_info_volatile_get_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_legacy_group +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_legacy_group /// /// Same as the above except that when the conversation does not exist, this sets all the convo /// fields to defaults and loads it with the given id. /// /// Returns true as long as it is given a valid legacy group id (i.e. same format as a session id). /// A false return is considered an error, and means the id was not a valid session id; an error -/// string will be set in `conf->last_error`. +/// string will be set in the error buffer. /// /// This is the method that should usually be used to create or update a conversation, followed by /// setting fields in the convo, and then giving it to convo_info_volatile_set(). /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_or_construct_legacy_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_legacy_group* convo, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to legacy group /// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` - Returns true if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_set_1to1 +/// API: convo_info_volatile/state_set_convo_info_volatile_1to1 /// /// Adds or updates a conversation from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_1to1( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_1to1* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to conversation info structure -LIBSESSION_EXPORT void convo_info_volatile_set_1to1( - config_object* conf, const convo_info_volatile_1to1* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_1to1( + mutable_state_user_object* state, const convo_info_volatile_1to1* convo); -/// API: convo_info_volatile/convo_info_volatile_set_community +/// API: convo_info_volatile/state_set_convo_info_volatile_community /// /// Adds or updates a community from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_community( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_community* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to community info structure -LIBSESSION_EXPORT void convo_info_volatile_set_community( - config_object* conf, const convo_info_volatile_community* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_community( + mutable_state_user_object* state, const convo_info_volatile_community* convo); -/// API: convo_info_volatile/convo_info_volatile_set_group +/// API: convo_info_volatile/state_set_convo_info_volatile_group /// /// Adds or updates a group from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_group( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_group* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_group( - config_object* conf, const convo_info_volatile_group* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_group( + mutable_state_user_object* state, const convo_info_volatile_group* convo); -/// API: convo_info_volatile/convo_info_volatile_set_legacy_group +/// API: convo_info_volatile/state_set_convo_info_volatile_legacy_group /// /// Adds or updates a legacy group from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_legacy_group( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_legacy_group* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to legacy group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_legacy_group( - config_object* conf, const convo_info_volatile_legacy_group* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_legacy_group( + mutable_state_user_object* state, const convo_info_volatile_legacy_group* convo); -/// API: convo_info_volatile/convo_info_volatile_erase_1to1 +/// API: convo_info_volatile/state_erase_convo_info_volatile_1to1 /// /// Erases a conversation from the conversation list. Returns true if the conversation was found /// and removed, false if the conversation was not present. You must not call this during /// iteration; see details below. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_1to1( -/// [in] config_object* conf, -/// [in] const char* session_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to community info structure /// /// Outputs: /// - `bool` - Returns true if conversation was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_1to1( + mutable_state_user_object* state, const char* session_id); -/// API: convo_info_volatile/convo_info_volatile_erase_community +/// API: convo_info_volatile/state_erase_convo_info_volatile_community /// /// Erases a community. Returns true if the community was found /// and removed, false if the community was not present. You must not call this during /// iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_community( -/// [in] config_object* conf, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string /// /// Outputs: /// - `bool` - Returns true if community was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_community( - config_object* conf, const char* base_url, const char* room); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_community( + mutable_state_user_object* state, const char* base_url, const char* room); -/// API: convo_info_volatile/convo_info_volatile_erase_group +/// API: convo_info_volatile/state_erase_convo_info_volatile_group /// /// Erases a group. Returns true if the group was found and removed, false if the group was not /// present. You must not call this during iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `group_id` -- [in] Null terminated hex string /// /// Outputs: /// - `bool` - Returns true if group was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_group( + mutable_state_user_object* state, const char* group_id); -/// API: convo_info_volatile/convo_info_volatile_erase_legacy_group +/// API: convo_info_volatile/state_erase_convo_info_volatile_legacy_group /// /// Erases a legacy group. Returns true if the group was found /// and removed, false if the group was not present. You must not call this during /// iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_legacy_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `group_id` -- [in] Null terminated hex string /// /// Outputs: /// - `bool` - Returns true if group was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_legacy_group( - config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_legacy_group( + mutable_state_user_object* state, const char* group_id); -/// API: convo_info_volatile/convo_info_volatile_size +/// API: convo_info_volatile/state_size_convo_info_volatile /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of conversations -LIBSESSION_EXPORT size_t convo_info_volatile_size(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_1to1 +/// API: convo_info_volatile/state_convo_info_volatile_1to1 /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_1to1( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of conversations -LIBSESSION_EXPORT size_t convo_info_volatile_size_1to1(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_1to1(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_communities +/// API: convo_info_volatile/state_size_convo_info_volatile_communities /// /// Returns the number of communitites. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of communities -LIBSESSION_EXPORT size_t convo_info_volatile_size_communities(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_communities(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_groups +/// API: convo_info_volatile/state_size_convo_info_volatile_groups /// /// Returns the number of groups. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of groups -LIBSESSION_EXPORT size_t convo_info_volatile_size_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_groups(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_legacy_groups +/// API: convo_info_volatile/state_size_convo_info_volatile_legacy_groups /// /// Returns the number of legacy groups. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of legacy groups -LIBSESSION_EXPORT size_t convo_info_volatile_size_legacy_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_legacy_groups(const state_object* state); typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; @@ -622,20 +439,13 @@ typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// It is NOT permitted to add/modify/remove records while iterating; instead you must use two /// loops: a first one to identify changes, and a second to apply them. /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_1to1 /// @@ -645,20 +455,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// of the `it_is_whatever` function: it will always be true for the particular type being iterated /// over). /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_1to1( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_communities /// @@ -668,20 +471,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// of the `it_is_whatever` function: it will always be true for the particular type being iterated /// over). /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_groups /// @@ -691,20 +487,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// of the `it_is_whatever` function: it will always be true for the particular type being iterated /// over). /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_legacy_groups /// @@ -714,20 +503,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// of the `it_is_whatever` function: it will always be true for the particular type being iterated /// over). /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_free /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 7692dc27..b9884e68 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -171,10 +171,7 @@ class ConvoInfoVolatile : public ConfigBase { /// the secret key. /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. - ConvoInfoVolatile( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + ConvoInfoVolatile(ustring_view ed25519_secretkey, std::optional dumped); /// API: convo_info_volatile/ConvoInfoVolatile::storage_namespace /// diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index fb5bcb1f..4010bf35 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -57,8 +57,7 @@ class Info final : public ConfigBase { /// that was previously dumped from an instance of this class by calling `dump()`. Info(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + std::optional dumped); /// API: groups/Info::storage_namespace /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index b2bb8b96..0848c2bf 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -73,10 +73,6 @@ using namespace std::literals; /// key="SessionGroupKeyGen"), where S = H(group_seed, key="SessionGroupKeySeed"). class Keys final : public ConfigSig { - // The parent state which owns this config object. By providing a pointer to the parent state - // we can inform the parent when changes occur. - std::optional _parent_state; - Ed25519Secret user_ed25519_sk; struct key_info { @@ -106,10 +102,6 @@ class Keys final : public ConfigSig { bool needs_dump_ = false; - // Updates the `needs_dump_` value, should always be called instead of setting directly as there - // are side effects we want to trigger when the value changes. - void set_needs_dump(bool updated_needs_dump); - ConfigMessage::verify_callable verifier_; ConfigMessage::sign_callable signer_; @@ -191,24 +183,7 @@ class Keys final : public ConfigSig { std::optional group_ed25519_secretkey, std::optional dumped, Info& info, - Members& members, - std::optional parent_state = std::nullopt); - - /// Same as the above but takes pointers instead of references. For internal use only. - Keys(ustring_view user_ed25519_secretkey, - ustring_view group_ed25519_pubkey, - std::optional group_ed25519_secretkey, - std::optional dumped, - Info* info, - Members* members, - std::optional parent_state = std::nullopt) : - Keys(user_ed25519_secretkey, - group_ed25519_pubkey, - group_ed25519_secretkey, - dumped, - *info, - *members, - parent_state) {} + Members& members); /// API: groups/Keys::storage_namespace /// diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 6ab5060b..9a6dd4c5 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -292,8 +292,7 @@ class Members final : public ConfigBase { /// that was previously dumped from an instance of this class by calling `dump()`. Members(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + std::optional dumped); /// API: groups/Members::storage_namespace /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index b17bf2f7..3016d751 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -4,6 +4,7 @@ extern "C" { #endif +#include "../state.h" #include "base.h" #include "notify.h" #include "util.h" @@ -88,68 +89,43 @@ typedef struct ugroups_community_info { } ugroups_community_info; -/// API: user_groups/user_groups_init -/// -/// Initializes the user groups object -/// -/// Declaration: -/// ```cpp -/// INT user_groups_init( -/// [out] config_object** conf, -/// [in] unsigned char* ed25519_secretkey, -/// [in, optional] unsigned char* dump, -/// [in, optional] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] pointer to config_object object -/// - `ed25519_secretkey` -- [in] pointer to secret key -/// - `dump` -- [in, optional] text of dump -/// - `dumplen` -- [in, optional] size of the text passed in as dump -/// - `error` -- [out] of the error if failed -/// -/// Outputs: -/// - `int` -- Whether the function succeeded or not -LIBSESSION_EXPORT int user_groups_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: user_groups/user_groups_get_group +/// API: user_groups/state_get_ugroups_group /// /// Gets (non-legacy) group info into `group`, if the group was found. `group_id` is a /// null-terminated C string containing the 66 character group id in hex (beginning with "03"). /// /// Inputs: -/// `conf` -- pointer to the group config object +/// `state` -- pointer to the state object /// `group` -- [out] `ugroups_group_info` struct into which to store the group info. /// `group_id` -- C string containing the hex group id (starting with "03") +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// Returns `true` and populates `group` if the group was found; returns false otherwise. -LIBSESSION_EXPORT bool user_groups_get_group( - config_object* conf, ugroups_group_info* group, const char* group_id); +LIBSESSION_EXPORT bool state_get_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error); -/// API: user_groups/user_groups_get_or_construct_group +/// API: user_groups/state_get_or_construct_ugroups_group /// /// Gets (non-legacy) group info into `group`, if the group was found. Otherwise initialize `group` /// to default values (and set its `.id` appropriately). /// /// Inputs: -/// `conf` -- pointer to the group config object +/// `state` -- pointer to the mutable state object /// `group` -- [out] `ugroups_group_info` struct into which to store the group info. /// `group_id` -- C string containing the hex group id (starting with "03") +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: -/// Returns `true` on success, `false` upon error (such as when given an invalid group id). -LIBSESSION_EXPORT bool user_groups_get_or_construct_group( - config_object* conf, ugroups_group_info* group, const char* group_id); +/// - `bool` -- `true` on success, `false` upon error (such as when given an invalid group id). +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error); -/// API: user_groups/user_groups_get_community +/// API: user_groups/state_get_ugroups_community /// /// Gets community conversation info into `comm`, if the community info was found. `base_url` and /// `room` are null-terminated c strings. base_url will be normalized/lower-cased; room is @@ -157,32 +133,28 @@ LIBSESSION_EXPORT bool user_groups_get_or_construct_group( /// different room capitalization than the one provided to the call. /// /// Returns true if the community was found and `comm` populated; false otherwise. A false return -/// can either be because it didn't exist (`conf->last_error` will be NULL) or because of some error -/// (`last_error` will be set to an error string). -/// -/// Declaration: -/// ```cpp -/// BOOL user_groups_get_community( -/// [in] config_object* conf, -/// [out] ugroups_community_info* comm, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` +/// can either be because it didn't exist (the error buffer will be NULL) or because of some error +/// (the error buffer will be set to an error string). /// /// Inputs: -/// - `conf` -- [in] pointer to config_object object +/// - `state` -- [in] pointer to state object /// - `comm` -- [out] pointer to ugroups_community_info object /// - `base_url` -- [in] text of the url /// - `room` -- [in] text of the room +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` -- Whether the function succeeded or not -LIBSESSION_EXPORT bool user_groups_get_community( - config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + char* error) __attribute__((warn_unused_result)); -/// API: user_groups/user_groups_get_or_construct_community +/// API: user_groups/state_get_or_construct_ugroups_community /// /// Like the above, but if the community was not found, this constructs one that can be inserted. /// `base_url` will be normalized in the returned object. `room` is a case-insensitive lookup key @@ -195,62 +167,48 @@ LIBSESSION_EXPORT bool user_groups_get_community( /// Note that this is all different from convo_info_volatile, which always forces the room token to /// lower-case (because it does not preserve the case). /// -/// Returns false (and sets `conf->last_error`) on error. -/// -/// Declaration: -/// ```cpp -/// BOOL user_groups_get_or_construct_community( -/// [in] config_object* conf, -/// [out] ugroups_community_info* comm, -/// [in] const char* base_url, -/// [in] const char* room, -/// [in] unsigned const char* pubkey -/// ); -/// ``` +/// Returns false (and sets the error buffer) on error. /// /// Inputs: -/// - `conf` -- [in] pointer to config_object object +/// - `state` -- [in] pointer to mutable state object /// - `comm` -- [out] pointer to ugroups_community_info object /// - `base_url` -- [in] text of the url /// - `room` -- [in] text of the room /// - `pubkey` -- [in] binary of pubkey -/// -/// Outputs: -/// - `bool` -- Whether the function succeeded or not -LIBSESSION_EXPORT bool user_groups_get_or_construct_community( - config_object* conf, +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_community( + const state_object* state, ugroups_community_info* comm, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey, + char* error); -/// API: user_groups/user_groups_get_legacy_group +/// API: user_groups/state_get_ugroups_legacy_group /// /// Returns a ugroups_legacy_group_info pointer containing the conversation info for a given legacy /// group ID (specified as a null-terminated hex string), if the conversation exists. If the -/// conversation does not exist, returns NULL. Sets conf->last_error on error. +/// conversation does not exist, returns NULL. Sets the error buffer on error. /// /// The returned pointer *must* be freed either by calling `ugroups_legacy_group_free()` when done /// with it, or by passing it to `user_groups_set_free_legacy_group()`. /// -/// Declaration: -/// ```cpp -/// UGROUPS_LEGACY_GROUP_INFO* user_groups_get_legacy_group( -/// [in] config_object* conf, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object +/// - `legacy_group_info` -- [out] Pointer containing conversation info /// - `id` -- [in] Null terminated hex string -/// -/// Outputs: -/// - `ugroupts_legacy_group_info*` -- Pointer containing conversation info -LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_legacy_group( - config_object* conf, const char* id) __attribute__((warn_unused_result)); +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +LIBSESSION_EXPORT bool state_get_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error); -/// API: user_groups/user_groups_get_or_construct_legacy_group +/// API: user_groups/state_get_or_construct_ugroups_legacy_group /// /// Same as the above `get_legacy_group()`except that when the conversation does not exist, this /// sets all the group fields to defaults and loads it with the given id. @@ -265,177 +223,111 @@ LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_legacy_group( /// This is the method that should usually be used to create or update a conversation, followed by /// setting fields in the group, and then giving it to user_groups_set(). /// -/// On error, this returns NULL and sets `conf->last_error`. -/// -/// Declaration: -/// ```cpp -/// UGROUPS_LEGACY_GROUP_INFO* user_groups_get_or_construct_legacy_group( -/// [in] config_object* conf, -/// [in] const char* id -/// ); -/// ``` +/// On error, this returns NULL and sets the error buffer. /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object +/// - `legacy_group_info` -- [out] Pointer containing conversation info /// - `id` -- [in] Null terminated hex string -/// -/// Outputs: -/// - `ugroupts_legacy_group_info*` -- Pointer containing conversation info -LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( - config_object* conf, const char* id) __attribute__((warn_unused_result)); +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error); -/// API: user_groups/ugroups_legacy_group_free -/// -/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by -/// get_legacy_group/get_or_construct_legacy_group). -/// -/// Declaration: -/// ```cpp -/// VOID ugroups_legacy_group_free( -/// [in] ugroups_community_info* group -/// ); -/// ``` -/// -/// Inputs: -/// - `group` -- [in] Pointer to ugroups_legacy_group_info -LIBSESSION_EXPORT void ugroups_legacy_group_free(ugroups_legacy_group_info* group); - -/// API: user_groups/user_groups_set_community +/// API: user_groups/state_set_ugroups_community /// /// Adds or updates a community conversation from the given group info /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_community( -/// [in] config_object* conf, -/// [in] const ugroups_community_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a community group info object -LIBSESSION_EXPORT void user_groups_set_community( - config_object* conf, const ugroups_community_info* group); +LIBSESSION_EXPORT void state_set_ugroups_community( + mutable_state_user_object* state, const ugroups_community_info* group); -/// API: user_groups/user_groups_set_group +/// API: user_groups/state_set_ugroups_group /// /// Adds or updates a (non-legacy) group conversation from the given group info /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a group info object -LIBSESSION_EXPORT void user_groups_set_group(config_object* conf, const ugroups_group_info* group); +LIBSESSION_EXPORT void state_set_ugroups_group( + mutable_state_user_object* state, const ugroups_group_info* group); -/// API: user_groups/user_groups_set_legacy_group +/// API: user_groups/state_set_ugroups_legacy_group /// /// Adds or updates a legacy group conversation from the into. This version of the method should /// only be used when you explicitly want the `group` to remain valid; if the set is the last thing /// you need to do with it (which is common) it is more efficient to call the freeing version, /// below. /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_legacy_group( -/// [in] config_object* conf, -/// [in] const ugroups_legacy_group_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_legacy_group( - config_object* conf, const ugroups_legacy_group_info* group); +LIBSESSION_EXPORT void state_set_ugroups_legacy_group( + mutable_state_user_object* state, const ugroups_legacy_group_info* group); -/// API: user_groups/user_groups_set_free_legacy_group +/// API: user_groups/state_set_free_ugroups_legacy_group /// /// Same as above `user_groups_set_free_legacy_group()`, except that this also frees the pointer for /// you, which is commonly what is wanted when updating fields. This is equivalent to, but more /// efficient than, setting and then freeing. /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_free_legacy_group( -/// [in] config_object* conf, -/// [in] const ugroups_legacy_group_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_free_legacy_group( - config_object* conf, ugroups_legacy_group_info* group); +LIBSESSION_EXPORT void state_set_free_ugroups_legacy_group( + mutable_state_user_object* state, ugroups_legacy_group_info* group); -/// API: user_groups/user_groups_erase_community +/// API: user_groups/state_erase_ugroups_community /// /// Erases a conversation from the conversation list. Returns true if the conversation was found /// and removed, false if the conversation was not present. You must not call this during /// iteration; see details below. /// -/// Declaration: -/// ```cpp -/// BOOL user_groups_erase_community( -/// [in] config_object* conf, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `base_url` -- [in] null terminated string of the base url /// - `room` -- [in] null terminated string of the room /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_community( - config_object* conf, const char* base_url, const char* room); +LIBSESSION_EXPORT bool state_erase_ugroups_community( + mutable_state_user_object* state, const char* base_url, const char* room); -/// API: user_groups/user_groups_erase_group +/// API: user_groups/state_erase_ugroups_group /// /// Erases a group conversation from the conversation list. Returns true if the conversation was /// found and removed, false if the conversation was not present. You must not call this during /// iteration; see details below. /// -/// Declaration: -/// ```cpp -/// BOOL user_groups_erase_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `group_id` -- [in] null terminated string of the hex group id (starting with "03") /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_ugroups_group( + mutable_state_user_object* state, const char* group_id); -/// API: user_groups/user_groups_erase_legacy_group +/// API: user_groups/state_erase_ugroups_legacy_group /// /// Erases a conversation from the conversation list. Returns true if the conversation was found /// and removed, false if the conversation was not present. You must not call this during /// iteration; see details below. /// -/// Declaration: -/// ```cpp -/// BOOL user_groups_erase_legacy_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `group_id` -- [in] null terminated string of the base url /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_legacy_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_ugroups_legacy_group( + mutable_state_user_object* state, const char* group_id); /// API: user_groups/ugroups_group_set_kicked /// @@ -450,6 +342,8 @@ LIBSESSION_EXPORT void ugroups_group_set_kicked(ugroups_group_info* group); /// API: user_groups/ugroups_group_is_kicked /// /// Returns true if we have been kicked (i.e. our secret key and auth data are empty). +/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by +/// get_legacy_group/get_or_construct_legacy_group). /// /// Inputs: /// - `group` -- [in] pointer to the group info to query @@ -458,6 +352,15 @@ LIBSESSION_EXPORT bool ugroups_group_is_kicked(const ugroups_group_info* group); typedef struct ugroups_legacy_members_iterator ugroups_legacy_members_iterator; +/// API: user_groups/ugroups_legacy_group_free +/// +/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by +/// get_legacy_group/get_or_construct_legacy_group). +/// +/// Inputs: +/// - `group` -- [in] Pointer to ugroups_legacy_group_info +LIBSESSION_EXPORT void ugroups_legacy_group_free(ugroups_legacy_group_info* group); + /// API: user_groups/ugroups_legacy_members_begin /// /// Group member iteration; this lets you walk through the full group member list. Example usage: @@ -632,81 +535,53 @@ LIBSESSION_EXPORT bool ugroups_legacy_member_remove( LIBSESSION_EXPORT size_t ugroups_legacy_members_count( const ugroups_legacy_group_info* group, size_t* members, size_t* admins); -/// API: user_groups/user_groups_size +/// API: user_groups/state_size_ugroups /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups(const state_object* state); -/// API: user_groups/user_groups_size_communities +/// API: user_groups/state_size_ugroups_communities /// /// Returns the number of community conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_communities(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_communities(const state_object* state); -/// API: user_groups/user_groups_size_groups +/// API: user_groups/state_size_ugroups_groups /// /// Returns the number of (non-legacy) group conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_groups(const state_object* state); -/// API: user_groups/user_groups_size_legacy_groups +/// API: user_groups/state_size_ugroups_legacy_groups /// /// Returns the number of legacy group conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: /// - `conf` -- [in] Pointer to config_object object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_legacy_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_legacy_groups(const state_object* state); typedef struct user_groups_iterator user_groups_iterator; -/// API: user_groups/user_groups_iterator_new +/// API: user_groups/state_iterator_new_user_groups /// /// Starts a new iterator that iterates over all conversations. /// @@ -715,7 +590,7 @@ typedef struct user_groups_iterator user_groups_iterator; /// ugroups_community_info c2; /// ugroups_legacy_group_info c3; /// ugroups_group_info c4; -/// user_groups_iterator *it = user_groups_iterator_new(my_groups); +/// user_groups_iterator *it = state_iterator_new_user_groups(my_groups); /// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) { /// if (user_groups_it_is_community(it, &c2)) { /// // use c2.whatever @@ -730,79 +605,58 @@ typedef struct user_groups_iterator user_groups_iterator; /// /// It is NOT permitted to add/remove/modify records while iterating. /// -/// Declaration: -/// ```cpp -/// USER_GROUPS_ITERATOR* user_groups_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator -LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new(const config_object* conf); +LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new(const state_object* state); -/// API: user_groups/user_groups_iterator_new_communities +/// API: user_groups/state_iterator_new_user_groups_communities /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation. You still need to use `user_groups_it_is_community` (or the alternatives) /// to load the data in each pass of the loop. (You can, however, safely ignore the bool return /// value of the `it_is_whatever` function: it will always be true for the particular type being /// iterated over). /// -/// Declaration: -/// ```cpp -/// USER_GROUPS_ITERATOR* user_groups_iterator_new_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_communities( - const config_object* conf); + const state_object* state); -/// API: user_groups/user_groups_iterator_new_legacy_groups +/// API: user_groups/state_iterator_new_user_groups_legacy_groups /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation. You still need to use `user_groups_it_is_community` (or the alternatives) /// to load the data in each pass of the loop. (You can, however, safely ignore the bool return /// value of the `it_is_whatever` function: it will always be true for the particular type being /// iterated over). /// -/// Declaration: -/// ```cpp -/// USER_GROUPS_ITERATOR* user_groups_iterator_new_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_legacy_groups( - const config_object* conf); + const state_object* state); -/// API: user_groups/user_groups_iterator_new_groups +/// API: user_groups/state_iterator_new_user_groups_groups /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation: non-legacy groups. You still need to use `user_groups_it_is_group` to load the /// data in each pass of the loop. (You can, however, safely ignore the bool return value of the /// `it_is_group` function: it will always be true for iterations for this iterator). /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator -LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_groups(const config_object* conf); +LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_groups(const state_object* state); /// API: user_groups/user_groups_iterator_free /// diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 6fc7846f..d35855dd 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -262,10 +262,7 @@ class UserGroups : public ConfigBase { /// /// Outputs: /// - `UserGroups` - Constructor - UserGroups( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + UserGroups(ustring_view ed25519_secretkey, std::optional dumped); /// API: user_groups/UserGroups::storage_namespace /// diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h new file mode 100644 index 00000000..590ebfa5 --- /dev/null +++ b/include/session/config/user_profile.h @@ -0,0 +1,127 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../state.h" +#include "base.h" +#include "profile_pic.h" + +/// API: state/state_get_profile_name +/// +/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at +/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is +/// no name +LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); + +/// API: state/state_set_profile_name +/// +/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on +/// error (and sets the state_object's error string). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `name` -- [in] Pointer to the name as a null-terminated C string +LIBSESSION_EXPORT void state_set_profile_name(mutable_state_user_object* state, const char* name); + +/// API: state/state_get_profile_pic +/// +/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile +/// pic is not currently set, and otherwise should be copied right away (they will not be valid +/// beyond other API calls on this config object). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `user_profile_pic` -- Pointer to the currently-set profile pic +LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* state); + +/// API: state/state_set_profile_pic +/// +/// Sets a user profile +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `pic` -- [in] Pointer to the pic +LIBSESSION_EXPORT void state_set_profile_pic( + mutable_state_user_object* state, user_profile_pic pic); + +/// API: state/state_get_profile_nts_priority +/// +/// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > +/// 0 for pinned (with higher value = higher priority). +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Returns the priority level +LIBSESSION_EXPORT int state_get_profile_nts_priority(const state_object* state); + +/// API: state/state_set_profile_nts_priority +/// +/// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for +/// higher priority in the conversation list. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `priority` -- [in] Integer of the priority +LIBSESSION_EXPORT void state_set_profile_nts_priority( + mutable_state_user_object* state, int priority); + +/// API: state/state_get_profile_nts_expiry +/// +/// Gets the Note-to-self message expiry timer (seconds). Returns 0 if not set. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set +LIBSESSION_EXPORT int state_get_profile_nts_expiry(const state_object* state); + +/// API: state/state_set_profile_nts_expiry +/// +/// Sets the Note-to-self message expiry timer (seconds). Setting 0 (or negative) will clear the +/// current timer. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `expiry` -- [in] Integer of the expiry timer in seconds +LIBSESSION_EXPORT void state_set_profile_nts_expiry(mutable_state_user_object* state, int expiry); + +/// API: state/state_get_profile_blinded_msgreqs +/// +/// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they +/// should be ignored. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// +/// Outputs: +/// - `int` -- Will be -1 if the state does not have the value explicitly set, 0 if the setting is +/// explicitly disabled, and 1 if the setting is explicitly enabled. +LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* state); + +/// API: state/state_set_profile_blinded_msgreqs +/// +/// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any +/// positive value) to enable; 0 to disable; and -1 to clear the setting. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `enabled` -- [in] true if they should be enabled, false if disabled +LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs( + mutable_state_user_object* state, int enabled); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 674533e7..d99f19e9 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -47,10 +47,7 @@ class UserProfile final : public ConfigBase { /// /// Outputs: /// - `UserProfile` - Constructor - UserProfile( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state = std::nullopt); + UserProfile(ustring_view ed25519_secretkey, std::optional dumped); /// API: user_profile/UserProfile::storage_namespace /// diff --git a/include/session/state.h b/include/session/state.h index 2cef0374..97899fac 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -8,9 +8,8 @@ extern "C" { #include #include -#include "config/contacts.h" +#include "config/base.h" #include "config/namespaces.h" -#include "config/profile_pic.h" #include "export.h" typedef struct state_object { @@ -25,6 +24,16 @@ typedef struct state_object { char _error_buf[256]; } state_object; +typedef struct mutable_state_user_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} mutable_state_user_object; + +typedef struct mutable_state_group_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} mutable_state_group_object; + typedef struct state_namespaced_dump { NAMESPACE namespace_; const char* pubkey_hex; @@ -204,39 +213,6 @@ LIBSESSION_EXPORT void state_set_service_node_offset(state_object* state, int64_ /// most recent API response LIBSESSION_EXPORT int64_t state_network_offset(state_object* state); -/// API: state/state_suppress_hooks_start -/// -/// This will suppress the `send` and `store` hooks until `state_suppress_hooks_stop` is called and -/// should be used when making multiple config changes to avoid sending and storing unnecessary -/// partial changes. -/// -/// Inputs: -/// - `state` -- [in] Pointer to state_object object -/// - `send` -- [in] controls whether the `send` hook should be suppressed. -/// - `store` -- [in] controls whether the `store` hook should be suppressed. -/// - `pubkey_hex` -- [in] pubkey to suppress changes for (in hex, with prefix - 66 -/// bytes). If none is provided then all changes for all configs will be supressed. -LIBSESSION_EXPORT bool state_suppress_hooks_start( - state_object* state, bool send, bool store, const char* pubkey_hex); - -/// API: state/state_suppress_hooks_stop -/// -/// This will stop suppressing the `send` and `store` hooks. When this is called, if there are -/// any pending changes, the `send` and `store` hooks will immediately be called. -/// -/// Inputs: -/// - `state` -- [in] Pointer to state_object object -/// - `send` -- [in] controls whether the `send` hook should no longer be suppressed. -/// - `store` -- [in] controls whether the `store` hook should no longer be suppressed. -/// - `force` -- [in] controls whether we should clear out multiple suppressions for the specified -/// hooks or just a single suppression. -/// - `pubkey_hex` -- [in] pubkey to stop suppressing changes for (in hex, with prefix - 66 bytes). -/// If the value provided doesn't match a entry created by `state_suppress_hooks_start` those -/// changes will continue to be suppressed. If none is provided then the hooks for all configs -/// with pending changes will be triggered. -LIBSESSION_EXPORT bool state_suppress_hooks_stop( - state_object* state, bool send, bool stor, bool force, const char* pubkey_hex); - /// API: state/state_merge /// /// Takes an pointer to an array of `state_config_message`, sorts them and merges them into the @@ -342,231 +318,92 @@ LIBSESSION_EXPORT bool state_received_send_response( unsigned char* request_ctx, size_t request_ctx_len); -/// User Profile functions - -/// API: state/state_get_profile_name +/// API: state/state_get_keys /// -/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at -/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. +/// Obtains the current group decryption keys. /// -/// Inputs: -/// - `state` -- [in] Pointer to the state object +/// Returns a buffer where each consecutive 32 bytes is an encryption key for the object, in +/// priority order (i.e. the key at 0 is the encryption key, and the first decryption key). /// -/// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); - -/// API: state/state_set_profile_name -/// -/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the state_object's error string). +/// This function is mainly for debugging/diagnostics purposes; most config types have one single +/// key (based on the secret key), and multi-keyed configs such as groups have their own methods for +/// encryption/decryption that are already aware of the multiple keys. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `name` -- [in] Pointer to the name as a null-terminated C string -LIBSESSION_EXPORT void state_set_profile_name(state_object* state, const char* name); - -/// API: state/state_get_profile_pic -/// -/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile -/// pic is not currently set, and otherwise should be copied right away (they will not be valid -/// beyond other API calls on this config object). -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// -/// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic -LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* state); - -/// API: state/state_set_profile_pic -/// -/// Sets a user profile -/// -/// Inputs: -/// - `state` -- [in] Pointer to the satet object -/// - `pic` -- [in] Pointer to the pic -LIBSESSION_EXPORT void state_set_profile_pic(state_object* state, user_profile_pic pic); - -/// API: state/state_get_profile_nts_priority -/// -/// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > -/// 0 for pinned (with higher value = higher priority). -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// -/// Outputs: -/// - `int` -- Returns the priority level -LIBSESSION_EXPORT int state_get_profile_nts_priority(const state_object* state); - -/// API: state/state_set_profile_nts_priority -/// -/// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for -/// higher priority in the conversation list. -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// - `priority` -- [in] Integer of the priority -LIBSESSION_EXPORT void state_set_profile_nts_priority(state_object* state, int priority); - -/// API: state/state_get_profile_nts_expiry -/// -/// Gets the Note-to-self message expiry timer (seconds). Returns 0 if not set. -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// -/// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_EXPORT int state_get_profile_nts_expiry(const state_object* state); +/// - `out` -- [out] pointer to newly malloced key data (a multiple of 32 bytes); the pointer +/// belongs to the caller and must be `free()`d when done with it. +/// - `outlen` -- [out] Pointer where the number of keys will be written (that is: the returned +/// pointer +/// will be to a buffer which has a size of of this value times 32). +LIBSESSION_EXPORT bool state_get_keys( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen); -/// API: state/state_set_profile_nts_expiry +/// API: state/state_mutate_user /// -/// Sets the Note-to-self message expiry timer (seconds). Setting 0 (or negative) will clear the -/// current timer. +/// Calls the callback provided with a mutable version of the `state_object` for user changes. /// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void state_set_profile_nts_expiry(state_object* state, int expiry); - -/// API: state/state_get_profile_blinded_msgreqs -/// -/// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they -/// should be ignored. +/// If an error occurs while the mutation callback is being performed the function will return false +/// and the `state->last_error` will be populated with the error information. /// /// Inputs: /// - `state` -- [in] Pointer to the state object +/// - `callback` -- [in] callback to be called with the `mutable_state_user_object` in order to +/// modify the user state. +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused /// /// Outputs: -/// - `int` -- Will be -1 if the state does not have the value explicitly set, 0 if the setting is -/// explicitly disabled, and 1 if the setting is explicitly enabled. -LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* state); - -/// API: state/state_set_profile_blinded_msgreqs -/// -/// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any -/// positive value) to enable; 0 to disable; and -1 to clear the setting. -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// - `enabled` -- [in] true if they should be enabled, false if disabled -/// -/// Outputs: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs(state_object* state, int enabled); - -/// Contact functions - -/// API: state/state_get_contacts -/// -/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex -/// string), if the contact exists, and returns true. If the contact does not exist then `contact` -/// is left unchanged and false is returned. -/// -/// Inputs: -/// - `state` -- [in] Pointer to the state object -/// - `contact` -- [out] the contact info data -/// - `session_id` -- [in] null terminated hex string -/// -/// Output: -/// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool state_get_contacts( - state_object* state, contacts_contact* contact, const char* session_id) - __attribute__((warn_unused_result)); +/// - `bool` -- Whether the mutation succeeded or not +LIBSESSION_EXPORT bool state_mutate_user( + state_object* state, void (*callback)(mutable_state_user_object*, void*), void* ctx); -/// API: state/state_get_or_construct_contacts -/// -/// Same as the above `state_get_contacts()` except that when the contact does not exist, this sets -/// all the contact fields to defaults and loads it with the given session_id. +/// API: state/state_mutate_group /// -/// Returns true as long as it is given a valid session_id. A false return is considered an error, -/// and means the session_id was not a valid session_id. +/// Calls the callback provided with a mutable version of the `state_object` for group changes. /// -/// This is the method that should usually be used to create or update a contact, followed by -/// setting fields in the contact, and then giving it to state_set_contacts(). +/// If an error occurs while the mutation callback is being performed the function will return false +/// and the `state->last_error` will be populated with the error information. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `contact` -- [out] the contact info data -/// - `session_id` -- [in] null terminated hex string -/// -/// Output: -/// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool state_get_or_construct_contacts( - state_object* state, contacts_contact* contact, const char* session_id) - __attribute__((warn_unused_result)); - -/// API: state/state_set_contacts -/// -/// Adds or updates a contact from the given contact info struct. -/// -/// Inputs: -/// - `state` -- [in, out] Pointer to the state object -/// - `contact` -- [in] Pointer containing the contact info data -LIBSESSION_EXPORT void state_set_contacts(state_object* state, const contacts_contact* contact); - -// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would -// save very little in actual calling code. The procedure for updating a single field without them -// is simple enough; for example to update `approved` and leave everything else unchanged: -// -// contacts_contact c; -// if (contacts_get_or_construct(conf, &c, some_session_id)) { -// const char* new_nickname = "Joe"; -// c.approved = new_nickname; -// contacts_set_or_create(conf, &c); -// } else { -// // some_session_id was invalid! -// } - -/// API: state/state_erase_contacts -/// -/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was -/// found and removed, false if the contact was not present. You must not call this during -/// iteration; see details below. -/// -/// Inputs: -/// - `state` -- [in, out] Pointer to the state object -/// - `session_id` -- [in] Text containing null terminated hex string +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `callback` -- [in] callback to be called with the `mutable_state_group_object` in order to +/// modify the group state. +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused /// /// Outputs: -/// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool state_erase_contacts(state_object* state, const char* session_id); +/// - `bool` -- Whether the mutation succeeded or not +LIBSESSION_EXPORT bool state_mutate_group( + state_object* state, + const char* pubkey_hex, + void (*callback)(mutable_state_group_object*, void*), + void* ctx); -/// API: state/state_size_contacts +/// API: state/mutable_state_user_set_error_if_empty /// -/// Returns the number of contacts. +/// Updates the `state->last_error` value to the provided message if it is currently empty. /// /// Inputs: -/// - `state` -- input - Pointer to the state object -/// -/// Outputs: -/// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t state_size_contacts(const state_object* state); +/// - `state` -- [in] Pointer to the mutable state object +/// - `err` -- [in] the error value to store in the state +/// - `err_len` -- [in] length of 'err' +LIBSESSION_EXPORT void mutable_state_user_set_error_if_empty( + mutable_state_user_object* state, const char* err, size_t err_len); -/// API: state/state_new_iterator_contacts +/// API: state/mutable_state_group_set_error_if_empty /// -/// Starts a new iterator. -/// -/// Functions for iterating through the entire contact list, in sorted order. Intended use is: -/// -/// contacts_contact c; -/// contacts_iterator *it = state_new_iterator_contacts(state); -/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) { -/// // c.session_id, c.nickname, etc. are loaded -/// } -/// contacts_iterator_free(it); -/// -/// It is NOT permitted to add/remove/modify records while iterating. +/// Updates the `state->last_error` value to the provided message if it is currently empty. /// /// Inputs: -/// - `state` -- [in] Pointer to the state object -/// -/// Outputs: -/// - `contacts_iterator*` -- pointer to the iterator -LIBSESSION_EXPORT contacts_iterator* state_new_iterator_contacts(const state_object* state); +/// - `state` -- [in] Pointer to the mutable state object +/// - `err` -- [in] the error value to store in the state +/// - `err_len` -- [in] length of 'err' +LIBSESSION_EXPORT void mutable_state_group_set_error_if_empty( + mutable_state_group_object* state, const char* err, size_t err_len); #ifdef __cplusplus } // extern "C" diff --git a/include/session/state.hpp b/include/session/state.hpp index 5dd94d62..ca4ea270 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -10,9 +10,12 @@ #include "config/user_profile.hpp" #include "ed25519.hpp" #include "session/util.hpp" +#include "state.h" namespace session::state { +class State; + using Ed25519PubKey = std::array; using Ed25519Secret = std::array; @@ -31,6 +34,55 @@ class GroupConfigs { std::unique_ptr config_keys; }; +class MutableUserConfigs { + private: + State* parent_state; + + public: + MutableUserConfigs( + State* state, + session::config::Contacts& contacts, + session::config::ConvoInfoVolatile& convo_info_volatile, + session::config::UserGroups& user_groups, + session::config::UserProfile& user_profile, + std::optional> set_error) : + parent_state(state), + contacts(contacts), + convo_info_volatile(convo_info_volatile), + user_groups(user_groups), + user_profile(user_profile), + set_error(set_error) {} + + session::config::Contacts& contacts; + session::config::ConvoInfoVolatile& convo_info_volatile; + session::config::UserGroups& user_groups; + session::config::UserProfile& user_profile; + std::optional> set_error; + + ~MutableUserConfigs(); +}; + +class MutableGroupConfigs { + private: + State* parent_state; + + public: + MutableGroupConfigs( + State* state, + session::config::groups::Info& info, + session::config::groups::Members& members, + session::config::groups::Keys& keys, + std::optional> set_error) : + parent_state(state), info(info), members(members), keys(keys), set_error(set_error) {} + + session::config::groups::Info& info; + session::config::groups::Members& members; + session::config::groups::Keys& keys; + std::optional> set_error; + + ~MutableGroupConfigs(); +}; + struct namespaced_dump { config::Namespace namespace_; std::optional pubkey_hex; @@ -82,9 +134,10 @@ struct config_message { class State { private: - // Storage of pubkeys which are currently being suppressed, the value specifies how many active - // suppressions the `send` or `store` hooks have. - std::map> _open_suppressions = {}; + std::unique_ptr config_contacts; + std::unique_ptr config_convo_info_volatile; + std::unique_ptr config_user_groups; + std::unique_ptr config_user_profile; std::map> _config_groups; protected: @@ -101,15 +154,8 @@ class State { std::function _send; public: - std::unique_ptr config_contacts; - std::unique_ptr config_convo_info_volatile; - std::unique_ptr config_user_groups; - std::unique_ptr config_user_profile; - std::chrono::milliseconds network_offset; - GroupConfigs* group_config(std::string_view pubkey_hex); - // Constructs a state with a secretkey that will be used for signing. State(ustring_view ed25519_secretkey, std::vector dumps); @@ -145,9 +191,10 @@ class State { if (!hook) return; - _open_suppressions[""] = {false, true}; - suppress_hooks_stop(); // Trigger config change hooks - _open_suppressions.erase(""); + config_changed(std::nullopt, true, false); + + for (auto& [key, val] : _config_groups) + config_changed(key, true, false); }; /// Hook which will be called whenever config messages need to be sent via the API. The hook @@ -163,9 +210,10 @@ class State { if (!hook) return; - _open_suppressions[""] = {true, false}; - suppress_hooks_stop(); // Trigger config change hooks - _open_suppressions.erase(""); + config_changed(std::nullopt, false, true); + + for (auto& [key, val] : _config_groups) + config_changed(key, false, true); }; /// API: state/State::load @@ -197,69 +245,14 @@ class State { /// Inputs: /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 /// bytes). Required for group changes. + /// - `allow_store` -- boolean value to specify whether this change can trigger the store hook. + /// - `allow_send` -- boolean value to specify whether this change can trigger the send hook. /// /// Outputs: None - void config_changed(std::optional pubkey_hex = std::nullopt); - - /// API: state/State::suppress_hooks_start - /// - /// This will suppress the `send` and `store` hooks until `suppress_hooks_stop` is called and - /// should be used when making multiple config changes to avoid sending and storing unnecessary - /// partial changes. - /// - /// Calling this function multiple times will result in multiple suppressions - /// for the specified hook, `suppress_hooks_stop` will need to be called multiple times (or with - /// `force = true`) in order for the hooks to start being triggered again. - /// - /// Inputs: - /// - `send` -- controls whether the `send` hook should be suppressed. - /// - `store` -- controls whether the `store` hook should be suppressed. - /// - `pubkey_hex` -- pubkey to suppress changes for (in hex, with prefix - 66 - /// bytes). If none is provided then all changes for all configs will be supressed. - /// - /// Outputs: None - void suppress_hooks_start( - bool send = true, bool store = true, std::string_view pubkey_hex = ""); - - /// API: state/State::suppress_hooks_stop - /// - /// This will remove a single supression for the `send` and `store` hooks. When this is called, - /// if there are are no more supresssions and any pending changes, the `send` and `store` hooks - /// will immediately be called. - /// - /// Calling this function with `force = true` will result in all supressions being removed. - /// - /// Inputs: - /// - `send` -- controls whether the `send` hook should no longer be suppressed. - /// - `store` -- controls whether the `store` hook should no longer be suppressed. - /// - `force` -- controls whether we should clear out multiple suppressions for the specified - /// hooks or just a single suppression. - /// - `pubkey_hex` -- pubkey to stop suppressing changes for (in hex, with prefix - 66 bytes). - /// If the value provided doesn't match a entry created by `suppress_hooks_start` those - /// changes will continue to be suppressed. If none is provided then the hooks for all configs - /// with pending changes will be triggered. - /// - /// Outputs: None - void suppress_hooks_stop( - bool send = true, - bool store = true, - bool force = false, - std::string_view pubkey_hex = ""); - - /// API: state/State::perform_while_suppressing_hooks - /// - /// This will prevent the `send` and `store` hooks from being called while the `changes` - /// function is running. Upon completion of `changes` the hooks will be triggered if there are - /// any pending changes. - /// - /// Inputs: - /// - `conf` -- a pointer to the config which the changes are occurring on (this will be used to - /// extract the target pubkey). - /// - `changes` -- a function encapsulating the desired changes. - /// - /// Outputs: None - void perform_while_suppressing_hooks( - session::config::ConfigSig* conf, std::function changes); + void config_changed( + std::optional pubkey_hex = std::nullopt, + bool allow_store = true, + bool allow_send = true); /// API: state/State::merge /// @@ -353,8 +346,84 @@ class State { /// - `ctx` -- the contextual data provided by the onSend hook. void received_send_response(std::string pubkey, ustring response_data, ustring ctx); + /// API: state/State::get_keys + /// + /// Returns a vector of encryption keys, in priority order (i.e. element 0 is the encryption + /// key, and the first decryption key). + /// + /// This method is mainly for debugging/diagnostics purposes; most config types have one single + /// key (based on the secret key), and multi-keyed configs such as groups have their own methods + /// for encryption/decryption that are already aware of the multiple keys. + /// + /// Inputs: + /// - `namespace` -- the namespace where the desired config messages are stored. + /// - `pubkey_hex` -- optional pubkey the config is associated to (in hex, with prefix - 66 + /// bytes). Required for group configs. + /// + /// Outputs: + /// - `std::vector` -- Returns vector of encryption keys + std::vector get_keys( + config::Namespace namespace_, std::optional pubkey_hex_); + + // Retrieves a read-only version of the user config + template + const ConfigType& config() const; + + // Retrieves a read-only version of the group config for the given public key + template + const ConfigType& config(std::string_view pubkey_hex) const; + + // Retrieves an editable version of the user config. Once the returned value is deconstructed it + // will trigger the `send` and `store` hooks. + MutableUserConfigs mutableConfig( + std::optional> set_error = std::nullopt); + + // Retrieves an editable version of the group config for the given public key. Once the returned + // value is deconstructed it will trigger the `send` and `store` hooks. + MutableGroupConfigs mutableConfig( + std::string_view pubkey_hex, + std::optional> set_error = std::nullopt); + private: + template + void add_child_logger(ConfigType& base); + void handle_config_push_response(std::string pubkey, ustring response, ustring ctx); + void validate_group_pubkey(std::string_view pubkey_hex) const; }; +inline State& unbox(state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline const State& unbox(const state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline MutableUserConfigs& unbox(mutable_state_user_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline MutableGroupConfigs& unbox(mutable_state_group_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} + +inline bool set_error(state_object* state, std::string_view e) { + if (e.size() > 255) + e.remove_suffix(e.size() - 255); + std::memcpy(state->_error_buf, e.data(), e.size()); + state->_error_buf[e.size()] = 0; + state->last_error = state->_error_buf; + return false; +} + +inline bool set_error_value(char* error, std::string_view e) { + std::string msg = {e.data(), e.size()}; + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + return false; +} + }; // namespace session::state diff --git a/src/config/base.cpp b/src/config/base.cpp index 20efc48d..ae44ffcc 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -17,7 +17,6 @@ #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" #include "session/export.h" -#include "session/state.hpp" #include "session/util.hpp" using namespace std::literals; @@ -36,29 +35,12 @@ void ConfigBase::set_state(ConfigState s) { _needs_dump = true; } -void ConfigBase::log(LogLevel lvl, std::string msg) { - if (logger) - logger(lvl, std::move(msg)); - else if (_parent_state && (*_parent_state)->logger) - (*_parent_state)->logger(lvl, msg); -} - MutableConfigMessage& ConfigBase::dirty() { if (_state != ConfigState::Dirty) { set_state(ConfigState::Dirty); _config = std::make_unique(*_config, increment_seqno); } - // If there is a parent state then notify it about the config change - if (_parent_state) { - std::optional pubkey_hex; - - if (_sign_pk) - pubkey_hex = "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()); - - (*_parent_state)->config_changed(pubkey_hex); - } - if (auto* mut = dynamic_cast(_config.get())) return *mut; throw std::runtime_error{"Internal error: unexpected dirty but non-mutable ConfigMessage"}; @@ -365,7 +347,6 @@ ustring ConfigBase::make_dump() const { } ConfigBase::ConfigBase( - std::optional parent_state, std::optional dump, std::optional ed25519_pubkey, std::optional ed25519_secretkey) { @@ -373,9 +354,6 @@ ConfigBase::ConfigBase( if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; - if (parent_state) - _parent_state = *parent_state; - if (dump) init_from_dump(from_unsigned_sv(*dump)); else diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index f0f76365..f6a6ec8a 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -9,6 +9,7 @@ #include "session/config/contacts.h" #include "session/config/error.h" #include "session/export.h" +#include "session/state.h" #include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -48,11 +49,8 @@ void contact_info::set_nickname(std::string n) { nickname = std::move(n); } -Contacts::Contacts( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped} { +Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : + ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -171,48 +169,41 @@ contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { } void Contacts::set(const contact_info& contact) { - auto changes = [this, &contact]() { - std::string pk = session_id_to_bytes(contact.session_id); - auto info = data["c"][pk]; - - // Always set the name, even if empty, to keep the dict from getting pruned if there are no - // other entries. - info["n"] = contact.name.substr(0, contact_info::MAX_NAME_LENGTH); - set_nonempty_str(info["N"], contact.nickname.substr(0, contact_info::MAX_NAME_LENGTH)); - - set_pair_if( - contact.profile_picture, - info["p"], - contact.profile_picture.url, - info["q"], - contact.profile_picture.key); - - set_flag(info["a"], contact.approved); - set_flag(info["A"], contact.approved_me); - set_flag(info["b"], contact.blocked); - - set_nonzero_int(info["+"], contact.priority); - - auto notify = contact.notifications; - if (notify == notify_mode::mentions_only) - notify = notify_mode::all; - set_positive_int(info["@"], static_cast(notify)); - set_positive_int(info["!"], contact.mute_until); - - set_pair_if( - contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, - info["e"], - static_cast(contact.exp_mode), - info["E"], - contact.exp_timer.count()); - - set_positive_int(info["j"], contact.created); - }; - - if (_parent_state) - (*_parent_state)->perform_while_suppressing_hooks(static_cast(this), changes); - else - changes(); + std::string pk = session_id_to_bytes(contact.session_id); + auto info = data["c"][pk]; + + // Always set the name, even if empty, to keep the dict from getting pruned if there are no + // other entries. + info["n"] = contact.name.substr(0, contact_info::MAX_NAME_LENGTH); + set_nonempty_str(info["N"], contact.nickname.substr(0, contact_info::MAX_NAME_LENGTH)); + + set_pair_if( + contact.profile_picture, + info["p"], + contact.profile_picture.url, + info["q"], + contact.profile_picture.key); + + set_flag(info["a"], contact.approved); + set_flag(info["A"], contact.approved_me); + set_flag(info["b"], contact.blocked); + + set_nonzero_int(info["+"], contact.priority); + + auto notify = contact.notifications; + if (notify == notify_mode::mentions_only) + notify = notify_mode::all; + set_positive_int(info["@"], static_cast(notify)); + set_positive_int(info["!"], contact.mute_until); + + set_pair_if( + contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, + info["e"], + static_cast(contact.exp_mode), + info["E"], + contact.exp_timer.count()); + + set_positive_int(info["j"], contact.created); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -326,8 +317,58 @@ Contacts::iterator& Contacts::iterator::operator++() { return *this; } +using namespace session::state; +using namespace session::config; + extern "C" { +LIBSESSION_C_API bool state_get_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) { + try { + if (auto c = unbox(state).config().get(session_id)) { + c->into(*contact); + return true; + } + } catch (const std::exception& e) { + set_error_value(error, e.what()); + } + return false; +} + +LIBSESSION_C_API bool state_get_or_construct_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) { + try { + unbox(state).config().get_or_construct(session_id).into(*contact); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API void state_set_contact( + mutable_state_user_object* state, const contacts_contact* contact) { + unbox(state).contacts.set(contact_info{*contact}); +} + +LIBSESSION_C_API bool state_erase_contact( + mutable_state_user_object* state, const char* session_id) { + try { + return unbox(state).contacts.erase(session_id); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API size_t state_size_contacts(const state_object* state) { + return unbox(state).config().size(); +} + +LIBSESSION_C_API contacts_iterator* contacts_iterator_new(const state_object* state) { + auto* it = new contacts_iterator{}; + it->_internals = new Contacts::iterator{unbox(state).config().begin()}; + return it; +} + LIBSESSION_C_API void contacts_iterator_free(contacts_iterator* it) { delete static_cast(it->_internals); delete it; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 99dd930e..02e83d14 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -15,6 +15,8 @@ #include "session/config/convo_info_volatile.h" #include "session/config/error.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -92,10 +94,8 @@ namespace convo { } // namespace convo ConvoInfoVolatile::ConvoInfoVolatile( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped} { + ustring_view ed25519_secretkey, std::optional dumped) : + ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -478,6 +478,7 @@ ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { } // namespace session::config +using namespace session::state; using namespace session::config; extern "C" { @@ -486,233 +487,224 @@ struct convo_info_volatile_iterator { }; } -LIBSESSION_C_API -int convo_info_volatile_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, +LIBSESSION_C_API bool state_get_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, char* error) { - return c_wrapper_init( - conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - -LIBSESSION_C_API bool convo_info_volatile_get_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_1to1(session_id)) { + if (auto c = unbox(state).config().get_1to1(session_id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_1to1(session_id).into(*convo); + unbox(state).config().get_or_construct_1to1(session_id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_community( - config_object* conf, +LIBSESSION_C_API bool state_get_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* og, const char* base_url, - const char* room) { + const char* room, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_community(base_url, room)) { + if (auto c = unbox(state).config().get_community(base_url, room)) { c->into(*og); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( - config_object* conf, +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* convo, const char* base_url, const char* room, - unsigned const char* pubkey) { + unsigned const char* pubkey, + char* error) { try { - conf->last_error = nullptr; - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + unbox(state) + .config() + .get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) .into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_group(id)) { + if (auto c = unbox(state).config().get_group(id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_group(id).into(*convo); + unbox(state).config().get_or_construct_group(id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_legacy_group(id)) { + if (auto c = unbox(state).config().get_legacy_group(id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_legacy_group(id).into(*convo); + unbox(state).config().get_or_construct_legacy_group(id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void convo_info_volatile_set_1to1( - config_object* conf, const convo_info_volatile_1to1* convo) { - unbox(conf)->set(convo::one_to_one{*convo}); +LIBSESSION_C_API void state_set_convo_info_volatile_1to1( + mutable_state_user_object* state, const convo_info_volatile_1to1* convo) { + unbox(state).convo_info_volatile.set(convo::one_to_one{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_community( - config_object* conf, const convo_info_volatile_community* convo) { - unbox(conf)->set(convo::community{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_community( + mutable_state_user_object* state, const convo_info_volatile_community* convo) { + unbox(state).convo_info_volatile.set(convo::community{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_group( - config_object* conf, const convo_info_volatile_group* convo) { - unbox(conf)->set(convo::group{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_group( + mutable_state_user_object* state, const convo_info_volatile_group* convo) { + unbox(state).convo_info_volatile.set(convo::group{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_legacy_group( - config_object* conf, const convo_info_volatile_legacy_group* convo) { - unbox(conf)->set(convo::legacy_group{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_legacy_group( + mutable_state_user_object* state, const convo_info_volatile_legacy_group* convo) { + unbox(state).convo_info_volatile.set(convo::legacy_group{*convo}); } -LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_1to1( + mutable_state_user_object* state, const char* session_id) { try { - return unbox(conf)->erase_1to1(session_id); + return unbox(state).convo_info_volatile.erase_1to1(session_id); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_community( - config_object* conf, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_community( + mutable_state_user_object* state, const char* base_url, const char* room) { try { - return unbox(conf)->erase_community(base_url, room); + return unbox(state).convo_info_volatile.erase_community(base_url, room); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_group(config_object* conf, const char* group_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_group( + mutable_state_user_object* state, const char* group_id) { try { - return unbox(conf)->erase_group(group_id); + return unbox(state).convo_info_volatile.erase_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( - config_object* conf, const char* group_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_legacy_group( + mutable_state_user_object* state, const char* group_id) { try { - return unbox(conf)->erase_legacy_group(group_id); + return unbox(state).convo_info_volatile.erase_legacy_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API size_t convo_info_volatile_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t state_size_convo_info_volatile(const state_object* state) { + return unbox(state).config().size(); } -LIBSESSION_C_API size_t convo_info_volatile_size_1to1(const config_object* conf) { - return unbox(conf)->size_1to1(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_1to1(const state_object* state) { + return unbox(state).config().size_1to1(); } -LIBSESSION_C_API size_t convo_info_volatile_size_communities(const config_object* conf) { - return unbox(conf)->size_communities(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_communities(const state_object* state) { + return unbox(state).config().size_communities(); } -LIBSESSION_C_API size_t convo_info_volatile_size_groups(const config_object* conf) { - return unbox(conf)->size_groups(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_groups(const state_object* state) { + return unbox(state).config().size_groups(); } -LIBSESSION_C_API size_t convo_info_volatile_size_legacy_groups(const config_object* conf) { - return unbox(conf)->size_legacy_groups(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_legacy_groups(const state_object* state) { + return unbox(state).config().size_legacy_groups(); } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = new ConvoInfoVolatile::iterator{unbox(conf)->begin()}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(state).config().begin()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = new ConvoInfoVolatile::iterator{unbox(conf)->begin_1to1()}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(state).config().begin_1to1()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_communities()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_communities()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_groups()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_groups()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_legacy_groups()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_legacy_groups()}; return it; } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 9cabf6cf..88b3a1eb 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -19,9 +19,8 @@ namespace session::config::groups { Info::Info( ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped, ed25519_pubkey, ed25519_secretkey}, + std::optional dumped) : + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey}, id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} {} std::optional Info::get_name() const { diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 9c0964af..a3b034ab 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -22,7 +22,6 @@ #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" #include "session/multi_encrypt.hpp" -#include "session/state.hpp" #include "session/xed25519.hpp" using namespace std::literals; @@ -33,21 +32,13 @@ static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { return std::chrono::system_clock::time_point{milliseconds_since_epoch * 1ms}; } -void Keys::set_needs_dump(bool updated_needs_dump) { - needs_dump_ = updated_needs_dump; - - if (updated_needs_dump && _parent_state && _sign_pk) - (*_parent_state)->config_changed("03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end())); -} - Keys::Keys( ustring_view user_ed25519_secretkey, ustring_view group_ed25519_pubkey, std::optional group_ed25519_secretkey, std::optional dumped, Info& info, - Members& members, - std::optional parent_state) { + Members& members) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; @@ -59,9 +50,6 @@ Keys::Keys( if (group_ed25519_secretkey && group_ed25519_secretkey->size() != 64) throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 secret key"}; - if (parent_state) - _parent_state = *parent_state; - init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); @@ -83,7 +71,7 @@ bool Keys::needs_dump() const { ustring Keys::dump() { auto dumped = make_dump(); - set_needs_dump(false); + needs_dump_ = false; return dumped; } @@ -422,7 +410,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { members.replace_keys(new_key_list, /*dirty=*/true); info.replace_keys(new_key_list, /*dirty=*/true); - set_needs_dump(true); + needs_dump_ = true; return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } @@ -876,7 +864,7 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { active_msgs_[new_key.generation].emplace(msg_hash); keys_.insert(it, std::move(new_key)); remove_expired(); - set_needs_dump(true); + needs_dump_ = true; } // Attempts xchacha20 decryption. @@ -1111,7 +1099,7 @@ bool Keys::load_key_message( if (admin() && !new_keys.empty() && !pending_key_config_.empty() && (new_keys[0].generation > pending_gen_ || new_keys[0].key == pending_key_)) { pending_key_config_.clear(); - set_needs_dump(true); + needs_dump_ = true; } if (!new_keys.empty()) { @@ -1125,7 +1113,7 @@ bool Keys::load_key_message( } else if (max_gen) { active_msgs_[*max_gen].emplace(hash); remove_expired(); - set_needs_dump(true); + needs_dump_ = true; } return false; diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 614c1925..8db53d9b 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -10,9 +10,8 @@ namespace session::config::groups { Members::Members( ustring_view ed25519_pubkey, std::optional ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped, ed25519_pubkey, ed25519_secretkey} {} + std::optional dumped) : + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} {} std::optional Members::get(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 751f76f2..d72b33ad 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -16,6 +16,8 @@ #include "session/config/error.h" #include "session/config/user_groups.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -261,11 +263,8 @@ void community_info::load(const dict& info_dict) { set_room(std::move(*n)); } -UserGroups::UserGroups( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped} { +UserGroups::UserGroups(ustring_view ed25519_secretkey, std::optional dumped) : + ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -459,6 +458,7 @@ bool UserGroups::erase(const community_info& c) { server_info.erase(); } } + return gone; } bool UserGroups::erase(const group_info& c) { @@ -607,158 +607,175 @@ UserGroups::iterator& UserGroups::iterator::operator++() { } // namespace session::config using namespace session::config; +using namespace session::state; extern "C" { struct user_groups_iterator { UserGroups::iterator it; }; -} +} // extern "C" -LIBSESSION_C_API -int user_groups_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); +namespace { +template +bool user_groups_it_is_impl(user_groups_iterator* it, C* c) { + auto& convo = *it->it; + if (auto* d = std::get_if(&convo)) { + d->into(*c); + return true; + } + return false; } +} // namespace + +extern "C" { -LIBSESSION_C_API bool user_groups_get_community( - config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_get_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_community(base_url, room)) { - c->into(*comm); + if (auto g = unbox(state).config().get_group(group_id)) { + g->into(*group); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool user_groups_get_or_construct_community( - config_object* conf, - ugroups_community_info* comm, - const char* base_url, - const char* room, - unsigned const char* pubkey) { + +LIBSESSION_C_API bool state_get_or_construct_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error) { try { - conf->last_error = nullptr; - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) - .into(*comm); + unbox(state).config().get_or_construct_group(group_id).into(*group); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool user_groups_get_group( - config_object* conf, ugroups_group_info* group, const char* group_id) { + +LIBSESSION_C_API bool state_get_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + char* error) { try { - conf->last_error = nullptr; - if (auto g = unbox(conf)->get_group(group_id)) { - g->into(*group); + if (auto c = unbox(state).config().get_community(base_url, room)) { + c->into(*comm); return true; } } catch (const std::exception& e) { - set_error(conf, e.what()); + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool user_groups_get_or_construct_group( - config_object* conf, ugroups_group_info* group, const char* group_id) { + +LIBSESSION_C_API bool state_get_or_construct_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + unsigned const char* pubkey, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_group(group_id).into(*group); + unbox(state) + .config() + .get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + .into(*comm); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { - if (group && group->_internal) { - delete static_cast(group->_internal); - group->_internal = nullptr; - } -} - -LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_legacy_group( - config_object* conf, const char* id) { +LIBSESSION_C_API bool state_get_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error) { try { - conf->last_error = nullptr; auto group = std::make_unique(); group->_internal = nullptr; - if (auto c = unbox(conf)->get_legacy_group(id)) { + if (auto c = unbox(state).config().get_legacy_group(id)) { std::move(c)->into(*group); - return group.release(); + *legacy_group_info = group.release(); + return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } - return nullptr; + return false; } -LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( - config_object* conf, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error) { try { - conf->last_error = nullptr; auto group = std::make_unique(); group->_internal = nullptr; - unbox(conf)->get_or_construct_legacy_group(id).into(*group); - return group.release(); + unbox(state).config().get_or_construct_legacy_group(id).into(*group); + *legacy_group_info = group.release(); + return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return nullptr; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void user_groups_set_community( - config_object* conf, const ugroups_community_info* comm) { - unbox(conf)->set(community_info{*comm}); +LIBSESSION_C_API void state_set_ugroups_community( + mutable_state_user_object* state, const ugroups_community_info* comm) { + unbox(state).user_groups.set(community_info{*comm}); } -LIBSESSION_C_API void user_groups_set_group(config_object* conf, const ugroups_group_info* group) { - unbox(conf)->set(group_info{*group}); + +LIBSESSION_C_API void state_set_ugroups_group( + mutable_state_user_object* state, const ugroups_group_info* group) { + unbox(state).user_groups.set(group_info{*group}); } -LIBSESSION_C_API void user_groups_set_legacy_group( - config_object* conf, const ugroups_legacy_group_info* group) { - unbox(conf)->set(legacy_group_info{*group}); + +LIBSESSION_C_API void state_set_ugroups_legacy_group( + mutable_state_user_object* state, const ugroups_legacy_group_info* group) { + unbox(state).user_groups.set(legacy_group_info{*group}); } -LIBSESSION_C_API void user_groups_set_free_legacy_group( - config_object* conf, ugroups_legacy_group_info* group) { - unbox(conf)->set(legacy_group_info{std::move(*group)}); + +LIBSESSION_C_API void state_set_free_ugroups_legacy_group( + mutable_state_user_object* state, ugroups_legacy_group_info* group) { + unbox(state).user_groups.set(legacy_group_info{std::move(*group)}); } -LIBSESSION_C_API bool user_groups_erase_community( - config_object* conf, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_erase_ugroups_community( + mutable_state_user_object* state, const char* base_url, const char* room) { try { - return unbox(conf)->erase_community(base_url, room); + return unbox(state).user_groups.erase_community(base_url, room); } catch (...) { return false; } } -LIBSESSION_C_API bool user_groups_erase_group(config_object* conf, const char* group_id) { + +LIBSESSION_C_API bool state_erase_ugroups_group( + mutable_state_user_object* state, const char* group_id) { try { - return unbox(conf)->erase_group(group_id); + return unbox(state).user_groups.erase_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API bool user_groups_erase_legacy_group(config_object* conf, const char* group_id) { + +LIBSESSION_C_API bool state_erase_ugroups_legacy_group( + mutable_state_user_object* state, const char* group_id) { try { - return unbox(conf)->erase_legacy_group(group_id); + return unbox(state).user_groups.erase_legacy_group(group_id); } catch (...) { return false; } } +LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { + if (group && group->_internal) { + delete static_cast(group->_internal); + group->_internal = nullptr; + } +} + LIBSESSION_C_API void ugroups_group_set_kicked(ugroups_group_info* group) { assert(group); group->have_auth_data = false; @@ -796,19 +813,16 @@ LIBSESSION_C_API bool ugroups_legacy_members_next( return false; } -LIBSESSION_C_API -void ugroups_legacy_members_erase(ugroups_legacy_members_iterator* it) { +LIBSESSION_C_API void ugroups_legacy_members_erase(ugroups_legacy_members_iterator* it) { it->it = it->members.erase(it->it); it->need_advance = false; } -LIBSESSION_C_API -void ugroups_legacy_members_free(ugroups_legacy_members_iterator* it) { +LIBSESSION_C_API void ugroups_legacy_members_free(ugroups_legacy_members_iterator* it) { delete it; } -LIBSESSION_C_API -bool ugroups_legacy_member_add( +LIBSESSION_C_API bool ugroups_legacy_member_add( ugroups_legacy_group_info* group, const char* session_id, bool admin) { try { check_session_id(session_id); @@ -826,8 +840,8 @@ bool ugroups_legacy_member_add( return true; } -LIBSESSION_C_API -bool ugroups_legacy_member_remove(ugroups_legacy_group_info* group, const char* session_id) { +LIBSESSION_C_API bool ugroups_legacy_member_remove( + ugroups_legacy_group_info* group, const char* session_id) { return static_cast(group->_internal)->members.erase(session_id); } @@ -852,33 +866,36 @@ LIBSESSION_C_API size_t ugroups_legacy_members_count( return mems.size(); } -LIBSESSION_C_API size_t user_groups_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t state_size_ugroups(const state_object* state) { + return unbox(state).config().size(); } -LIBSESSION_C_API size_t user_groups_size_communities(const config_object* conf) { - return unbox(conf)->size_communities(); + +LIBSESSION_C_API size_t state_size_ugroups_communities(const state_object* state) { + return unbox(state).config().size_communities(); } -LIBSESSION_C_API size_t user_groups_size_groups(const config_object* conf) { - return unbox(conf)->size_groups(); + +LIBSESSION_C_API size_t state_size_ugroups_groups(const state_object* state) { + return unbox(state).config().size_groups(); } -LIBSESSION_C_API size_t user_groups_size_legacy_groups(const config_object* conf) { - return unbox(conf)->size_legacy_groups(); + +LIBSESSION_C_API size_t state_size_ugroups_legacy_groups(const state_object* state) { + return unbox(state).config().size_legacy_groups(); } -LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new(const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin()}}; +LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new(const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin()}}; } LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_communities( - const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_communities()}}; + const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_communities()}}; } -LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_groups(const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_groups()}}; +LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_groups(const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_groups()}}; } LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_legacy_groups( - const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_legacy_groups()}}; + const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_legacy_groups()}}; } LIBSESSION_C_API void user_groups_iterator_free(user_groups_iterator* it) { @@ -893,18 +910,6 @@ LIBSESSION_C_API void user_groups_iterator_advance(user_groups_iterator* it) { ++it->it; } -namespace { -template -bool user_groups_it_is_impl(user_groups_iterator* it, C* c) { - auto& convo = *it->it; - if (auto* d = std::get_if(&convo)) { - d->into(*c); - return true; - } - return false; -} -} // namespace - LIBSESSION_C_API bool user_groups_it_is_community( user_groups_iterator* it, ugroups_community_info* c) { return user_groups_it_is_impl(it, c); @@ -918,3 +923,5 @@ LIBSESSION_C_API bool user_groups_it_is_legacy_group( user_groups_iterator* it, ugroups_legacy_group_info* g) { return user_groups_it_is_impl(it, g); } + +} // extern "C" \ No newline at end of file diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 37758c84..069a6d8d 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -3,16 +3,16 @@ #include #include "internal.hpp" +#include "session/config/user_profile.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" using namespace session::config; using session::ustring_view; -UserProfile::UserProfile( - ustring_view ed25519_secretkey, - std::optional dumped, - std::optional parent_state) : - ConfigBase{parent_state, dumped} { +UserProfile::UserProfile(ustring_view ed25519_secretkey, std::optional dumped) : + ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -73,3 +73,71 @@ std::optional UserProfile::get_blinded_msgreqs() const { return static_cast(*M); return std::nullopt; } + +using namespace session::state; + +extern "C" { + +LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { + if (auto s = unbox(state).config().get_name()) + return s->data(); + return nullptr; +} + +LIBSESSION_C_API void state_set_profile_name(mutable_state_user_object* state, const char* name) { + unbox(state).user_profile.set_name(name); +} + +LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { + user_profile_pic p; + if (auto pic = unbox(state).config().get_profile_pic(); pic) { + copy_c_str(p.url, pic.url); + std::memcpy(p.key, pic.key.data(), 32); + } else { + p.url[0] = 0; + } + return p; +} + +LIBSESSION_C_API void state_set_profile_pic( + mutable_state_user_object* state, user_profile_pic pic) { + std::string_view url{pic.url}; + ustring_view key; + if (!url.empty()) + key = {pic.key, 32}; + + unbox(state).user_profile.set_profile_pic(url, key); +} + +LIBSESSION_C_API int state_get_profile_nts_priority(const state_object* state) { + return unbox(state).config().get_nts_priority(); +} + +LIBSESSION_C_API void state_set_profile_nts_priority( + mutable_state_user_object* state, int priority) { + unbox(state).user_profile.set_nts_priority(priority); +} + +LIBSESSION_C_API int state_get_profile_nts_expiry(const state_object* state) { + return unbox(state).config().get_nts_expiry().value_or(0s).count(); +} + +LIBSESSION_C_API void state_set_profile_nts_expiry(mutable_state_user_object* state, int expiry) { + unbox(state).user_profile.set_nts_expiry(std::max(0, expiry) * 1s); +} + +LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { + if (auto opt = unbox(state).config().get_blinded_msgreqs()) + return static_cast(*opt); + return -1; +} + +LIBSESSION_C_API void state_set_profile_blinded_msgreqs( + mutable_state_user_object* state, int enabled) { + std::optional val; + if (enabled >= 0) + val = static_cast(enabled); + unbox(state).user_profile.set_blinded_msgreqs(val); +} + +} // extern "C" \ No newline at end of file diff --git a/src/state.cpp b/src/state.cpp index 5040ab69..0812d70b 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -32,11 +32,10 @@ enum class RequestType : std::uint8_t { }; GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { - auto info = std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); - auto members = - std::make_unique(pubkey, std::nullopt, std::nullopt, std::nullopt); + auto info = std::make_unique(pubkey, std::nullopt, std::nullopt); + auto members = std::make_unique(pubkey, std::nullopt, std::nullopt); auto keys = std::make_unique( - user_sk, pubkey, std::nullopt, std::nullopt, *info, *members, std::nullopt); + user_sk, pubkey, std::nullopt, std::nullopt, *info, *members); config_info = std::move(info); config_members = std::move(members); config_keys = std::move(keys); @@ -71,46 +70,53 @@ State::State(ustring_view ed25519_secretkey, std::vector dumps) } // Initialise empty config states for any missing required config types - std::optional parent = this; - - if (!config_contacts) - config_contacts = std::make_unique(ed25519_secretkey, std::nullopt, parent); + if (!config_contacts) { + config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(config_contacts); + } - if (!config_convo_info_volatile) + if (!config_convo_info_volatile) { config_convo_info_volatile = - std::make_unique(ed25519_secretkey, std::nullopt, parent); + std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(config_convo_info_volatile); + } - if (!config_user_groups) - config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt, parent); + if (!config_user_groups) { + config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(config_user_groups); + } - if (!config_user_profile) - config_user_profile = - std::make_unique(ed25519_secretkey, std::nullopt, parent); + if (!config_user_profile) { + config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(config_user_profile); + } } void State::load( Namespace namespace_, std::optional pubkey_hex_, ustring_view dump) { - std::optional parent = this; - switch (namespace_) { case Namespace::Contacts: config_contacts = - std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(config_contacts); return; case Namespace::ConvoInfoVolatile: config_convo_info_volatile = std::make_unique( - to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(config_convo_info_volatile); return; case Namespace::UserGroups: - config_user_groups = std::make_unique( - to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + config_user_groups = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(config_user_groups); return; case Namespace::UserProfile: - config_user_profile = std::make_unique( - to_unsigned_sv({_user_sk.data(), 64}), dump, parent); + config_user_profile = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(config_user_profile); return; default: break; @@ -152,115 +158,29 @@ void State::load( } // Reload the specified namespace with the dump - if (namespace_ == Namespace::GroupInfo) + if (namespace_ == Namespace::GroupInfo) { _config_groups[pubkey_hex]->config_info = - std::make_unique(pubkey_sv, group_ed25519_secretkey, dump, parent); - else if (namespace_ == Namespace::GroupMembers) + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); + add_child_logger(_config_groups[pubkey_hex]->config_info); + } else if (namespace_ == Namespace::GroupMembers) { _config_groups[pubkey_hex]->config_members = - std::make_unique(pubkey_sv, group_ed25519_secretkey, dump, parent); - else if (namespace_ == Namespace::GroupKeys) { + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); + add_child_logger(_config_groups[pubkey_hex]->config_members); + } else if (namespace_ == Namespace::GroupKeys) { auto info = _config_groups[pubkey_hex]->config_info.get(); auto members = _config_groups[pubkey_hex]->config_members.get(); auto keys = std::make_unique( - user_ed25519_secretkey, - pubkey_sv, - group_ed25519_secretkey, - dump, - info, - members, - parent); - + user_ed25519_secretkey, pubkey_sv, group_ed25519_secretkey, dump, *info, *members); _config_groups[pubkey_hex]->config_keys = std::move(keys); } else throw std::runtime_error{"Attempted to load unknown namespace"}; } -GroupConfigs* State::group_config(std::string_view pubkey_hex) { - if (pubkey_hex.size() != 64) - throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; - if (!_config_groups.count(pubkey_hex)) - throw std::runtime_error{ - "Attempted to merge group configs before for group with no config state"}; - - return _config_groups[pubkey_hex].get(); -} - -void State::suppress_hooks_start(bool send, bool store, std::string_view pubkey_hex) { - log(LogLevel::debug, - "suppress_hooks_start: " + std::string(pubkey_hex) + "(send: " + bool_to_string(send) + - ", store: " + bool_to_string(store) + ")"); - _open_suppressions[pubkey_hex] = { - _open_suppressions[pubkey_hex].first + (send ? 1 : 0), - _open_suppressions[pubkey_hex].second + (store ? 1 : 0)}; -} - -void State::suppress_hooks_stop(bool send, bool store, bool force, std::string_view pubkey_hex) { - log(LogLevel::debug, - "suppress_hooks_stop: '" + std::string(pubkey_hex) + "' (send: " + bool_to_string(send) + - ", store: " + bool_to_string(store) + ", force: " + bool_to_string(force) + ")"); - - // If `_open_suppressions` doesn't contain a value it'll default to {0, 0} - auto final_send = (send && force ? 0 : _open_suppressions[pubkey_hex].first); - auto final_store = (store && force ? 0 : _open_suppressions[pubkey_hex].second); - - if (!force) { - final_send = - (send ? std::max(0, _open_suppressions[pubkey_hex].first - 1) - : _open_suppressions[pubkey_hex].first); - final_store = - (store ? std::max(0, _open_suppressions[pubkey_hex].second - 1) - : _open_suppressions[pubkey_hex].second); - } - - if (final_send == 0 && final_store == 0) - _open_suppressions.erase(pubkey_hex); - else - _open_suppressions[pubkey_hex] = {final_send, final_store}; - - // If the hooks are still suppressed then don't trigger the 'config_changed' call - if (final_send > 0 && final_store > 0) - return; - - // Trigger the config change hooks if needed for the specified config - config_changed(pubkey_hex); - - // If no pubkey was provided then we want to check for changes across all configs, the - // above line will have defaulted to checking the user configs so we now need to check - // all group configs - if (pubkey_hex.empty()) - for (auto& [key, val] : _config_groups) - config_changed(key); -} - -void State::perform_while_suppressing_hooks( - session::config::ConfigSig* conf, std::function changes) { - std::string target_pubkey_hex = _user_x_pk_hex; - auto sign_pk = conf->get_sig_pubkey(); - - // If we have a signature pubkey then assume it's a group config, use the `03` prefix with that - // instead of the user x_pk - if (sign_pk) - target_pubkey_hex = "03" + oxenc::to_hex(sign_pk->begin(), sign_pk->end()); - - suppress_hooks_start(true, true, target_pubkey_hex); - changes(); - suppress_hooks_stop(true, true, false, target_pubkey_hex); -} - -void State::config_changed(std::optional pubkey_hex) { +void State::config_changed( + std::optional pubkey_hex, bool allow_store, bool allow_send) { auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); - // Check if there both `send` and `store` hooks are suppressed (and if so ignore this change) - std::pair suppressions = - (_open_suppressions.count(target_pubkey_hex) ? _open_suppressions[target_pubkey_hex] - : _open_suppressions[""]); - - if (suppressions.first > 0 && suppressions.second > 0) { - log(LogLevel::debug, "config_changed: Ignoring due to hooks being suppressed"); - return; - } - std::string info_title = "User configs"; bool needs_push = false; bool needs_dump = false; @@ -272,11 +192,11 @@ void State::config_changed(std::optional pubkey_hex) { if (!is_group_pubkey) { needs_push = - (suppressions.first == 0 && + (allow_send && (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || config_user_groups->needs_push() || config_user_profile->needs_push())); needs_dump = - (suppressions.second == 0 && + (allow_store && (config_contacts->needs_dump() || config_convo_info_volatile->needs_dump() || config_user_groups->needs_dump() || config_user_profile->needs_dump())); configs = { @@ -303,15 +223,14 @@ void State::config_changed(std::optional pubkey_hex) { // Only group admins can push group config changes needs_push = - (suppressions.first == 0 && !user_group_info->secretkey.empty() && + (allow_send && !user_group_info->secretkey.empty() && (_config_groups[target_pubkey_hex]->config_info->needs_push() || _config_groups[target_pubkey_hex]->config_members->needs_push() || _config_groups[target_pubkey_hex]->config_keys->pending_config())); needs_dump = - (suppressions.second == 0 && - (_config_groups[target_pubkey_hex]->config_info->needs_dump() || - _config_groups[target_pubkey_hex]->config_members->needs_dump() || - _config_groups[target_pubkey_hex]->config_keys->needs_dump())); + (allow_store && (_config_groups[target_pubkey_hex]->config_info->needs_dump() || + _config_groups[target_pubkey_hex]->config_members->needs_dump() || + _config_groups[target_pubkey_hex]->config_keys->needs_dump())); configs = { _config_groups[target_pubkey_hex]->config_info.get(), _config_groups[target_pubkey_hex]->config_members.get()}; @@ -319,16 +238,14 @@ void State::config_changed(std::optional pubkey_hex) { } std::string send_info = - (suppressions.first > 0 ? "send suppressed" - : ("needs send: " + bool_to_string(needs_push))); + (!allow_send ? "send suppressed" : ("needs send: " + bool_to_string(needs_push))); std::string store_info = - (suppressions.second > 0 ? "store suppressed" - : ("needs store: " + bool_to_string(needs_dump))); + (!allow_store ? "store suppressed" : ("needs store: " + bool_to_string(needs_dump))); log(LogLevel::debug, "config_changed: " + info_title + " (" + send_info + ", " + store_info + ")"); // Call the hook to store the dump if needed - if (_store && needs_dump && suppressions.second == 0) { + if (_store && needs_dump && allow_store) { for (auto& config : configs) { if (!config->needs_dump()) continue; @@ -354,7 +271,7 @@ void State::config_changed(std::optional pubkey_hex) { } // Call the hook to perform a push if needed - if (_send && needs_push && suppressions.first == 0) { + if (_send && needs_push && allow_send) { std::vector requests; std::vector obsolete_hashes; @@ -535,9 +452,6 @@ std::vector State::merge( auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); - // Suppress triggering the `send` hook until the merge is complete - suppress_hooks_start(true, false, target_pubkey_hex); - for (size_t i = 0; i < sorted_configs.size(); ++i) { auto& config = sorted_configs[i]; @@ -566,21 +480,25 @@ std::vector State::merge( case Namespace::Contacts: merged_hashes = config_contacts->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::ConvoInfoVolatile: merged_hashes = config_convo_info_volatile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::UserGroups: merged_hashes = config_user_groups->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::UserProfile: merged_hashes = config_user_profile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; default: break; @@ -613,11 +531,13 @@ std::vector State::merge( } } else throw std::runtime_error{"merge: Attempted to merge from unknown namespace"}; + + config_changed(target_pubkey_hex, true, false); // Immediately store changes } - // Now that all of the merges have been completed we stop suppressing the `send` hook which - // will be triggered if there is a pending push - suppress_hooks_stop(true, false, false, target_pubkey_hex); + // Now that all of the merges have been completed we want to trigger the `send` hook if + // there is a pending push + config_changed(target_pubkey_hex, false, true); log(LogLevel::debug, "merge: Complete"); return good_hashes; @@ -732,6 +652,9 @@ ustring State::dump(config::Namespace namespace_, std::optional(); - if (pubkey.size() != 66) - throw std::invalid_argument{ - "handle_config_push_response: Invalid ctx.pubkey - expected 66 characters"}; if (!_config_groups.count(pubkey)) throw std::runtime_error{"handle_config_push_response: Unable to retrieve group"}; @@ -870,6 +787,145 @@ void State::handle_config_push_response(std::string pubkey, ustring response, us "handle_config_push_response: Attempted to load unknown namespace"}; } } + + // Now that we have confirmed the push we need to store the configs again + config_changed(pubkey, true, false); + log(LogLevel::debug, "handle_config_push_response: Completed"); } + +std::vector State::get_keys( + Namespace namespace_, std::optional pubkey_hex_) { + switch (namespace_) { + case Namespace::Contacts: return config_contacts->get_keys(); + case Namespace::ConvoInfoVolatile: return config_convo_info_volatile->get_keys(); + case Namespace::UserGroups: return config_user_groups->get_keys(); + case Namespace::UserProfile: return config_user_profile->get_keys(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex_->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + if (!_config_groups.count(*pubkey_hex_)) + throw std::runtime_error{"Unable to retrieve group"}; + + // Retrieve the group configs for this pubkey + auto group_configs = _config_groups[*pubkey_hex_].get(); + + switch (namespace_) { + case Namespace::GroupInfo: return group_configs->config_info->get_keys(); + case Namespace::GroupMembers: return group_configs->config_members->get_keys(); + case Namespace::GroupKeys: return group_configs->config_keys->group_keys(); + default: throw std::runtime_error{"Attempted to load unknown namespace"}; + } +} + +void State::validate_group_pubkey(std::string_view pubkey_hex) const { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + if (!_config_groups.count(pubkey_hex)) + throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; +} + +// Template functions + +template +void State::add_child_logger(ConfigType& config) { + config->logger = [this](LogLevel lvl, std::string msg) { log(lvl, msg); }; +} + +template +const ConfigType& State::config() const { + throw std::runtime_error{"config: Attempted to retrieve config for unknown namespace"}; +}; + +template +const ConfigType& State::config(std::string_view pubkey_hex) const { + throw std::runtime_error{"config: Attempted to retrieve config for unknown namespace"}; +}; + +template <> +const Contacts& State::config() const { + return *config_contacts; +} + +template <> +const ConvoInfoVolatile& State::config() const { + return *config_convo_info_volatile; +}; + +template <> +const UserGroups& State::config() const { + return *config_user_groups; +}; + +template <> +const UserProfile& State::config() const { + return *config_user_profile; +}; + +template <> +const groups::Info& State::config(std::string_view pubkey_hex) const { + validate_group_pubkey(pubkey_hex); + + if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) + return *it->second->config_info; + + throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; +}; + +template <> +const groups::Members& State::config(std::string_view pubkey_hex) const { + validate_group_pubkey(pubkey_hex); + + if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) + return *it->second->config_members; + + throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; +}; + +template <> +const groups::Keys& State::config(std::string_view pubkey_hex) const { + if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) + return *it->second->config_keys; + + throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; +}; + +MutableUserConfigs State::mutableConfig( + std::optional> set_error) { + return MutableUserConfigs( + this, + *config_contacts, + *config_convo_info_volatile, + *config_user_groups, + *config_user_profile, + set_error); +}; + +MutableUserConfigs::~MutableUserConfigs() { + parent_state->config_changed(); +}; + +MutableGroupConfigs State::mutableConfig( + std::string_view pubkey_hex, + std::optional> set_error) { + validate_group_pubkey(pubkey_hex); + return MutableGroupConfigs( + this, + *_config_groups[pubkey_hex]->config_info, + *_config_groups[pubkey_hex]->config_members, + *_config_groups[pubkey_hex]->config_keys, + set_error); +}; + +MutableGroupConfigs::~MutableGroupConfigs() { + if (auto sign_pk = info.get_sig_pubkey()) + parent_state->config_changed("03" + oxenc::to_hex(sign_pk->begin(), sign_pk->end())); +}; + } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 6d17bedf..3f84ec3e 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -29,26 +29,6 @@ using namespace session::state; LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; -namespace { -State& unbox(state_object* state) { - assert(state && state->internals); - return *static_cast(state->internals); -} -const State& unbox(const state_object* state) { - assert(state && state->internals); - return *static_cast(state->internals); -} - -bool set_error(state_object* state, std::string_view e) { - if (e.size() > 255) - e.remove_suffix(e.size() - 255); - std::memcpy(state->_error_buf, e.data(), e.size()); - state->_error_buf[e.size()] = 0; - state->last_error = state->_error_buf; - return false; -} -} // namespace - extern "C" { // Util Functions @@ -226,34 +206,6 @@ LIBSESSION_C_API int64_t state_network_offset(state_object* state) { return unbox(state).network_offset.count(); } -LIBSESSION_C_API bool state_suppress_hooks_start( - state_object* state, bool send, bool store, const char* pubkey_hex_) { - try { - std::string_view pubkey_hex = ""; - if (pubkey_hex_) - pubkey_hex = {pubkey_hex_, 66}; - - unbox(state).suppress_hooks_start(send, store, pubkey_hex); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} - -LIBSESSION_C_API bool state_suppress_hooks_stop( - state_object* state, bool send, bool store, bool force, const char* pubkey_hex_) { - try { - std::string_view pubkey_hex = ""; - if (pubkey_hex_) - pubkey_hex = {pubkey_hex_, 66}; - - unbox(state).suppress_hooks_stop(send, store, force, pubkey_hex); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} - LIBSESSION_C_API bool state_merge( state_object* state, const char* pubkey_hex_, @@ -355,113 +307,80 @@ LIBSESSION_C_API bool state_received_send_response( } } -// User Profile Functions - -LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { - if (auto s = unbox(state).config_user_profile->get_name()) - return s->data(); - return nullptr; -} - -LIBSESSION_C_API void state_set_profile_name(state_object* state, const char* name) { - unbox(state).config_user_profile->set_name(name); -} - -LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { - user_profile_pic p; - if (auto pic = unbox(state).config_user_profile->get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; - } - return p; -} - -LIBSESSION_C_API void state_set_profile_pic(state_object* state, user_profile_pic pic) { - std::string_view url{pic.url}; - ustring_view key; - if (!url.empty()) - key = {pic.key, 32}; - - unbox(state).config_user_profile->set_profile_pic(url, key); -} - -LIBSESSION_C_API int state_get_profile_nts_priority(const state_object* state) { - return unbox(state).config_user_profile->get_nts_priority(); -} - -LIBSESSION_C_API void state_set_profile_nts_priority(state_object* state, int priority) { - unbox(state).config_user_profile->set_nts_priority(priority); -} - -LIBSESSION_C_API int state_get_profile_nts_expiry(const state_object* state) { - return unbox(state).config_user_profile->get_nts_expiry().value_or(0s).count(); -} - -LIBSESSION_C_API void state_set_profile_nts_expiry(state_object* state, int expiry) { - unbox(state).config_user_profile->set_nts_expiry(std::max(0, expiry) * 1s); -} - -LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { - if (auto opt = unbox(state).config_user_profile->get_blinded_msgreqs()) - return static_cast(*opt); - return -1; -} - -LIBSESSION_C_API void state_set_profile_blinded_msgreqs(state_object* state, int enabled) { - std::optional val; - if (enabled >= 0) - val = static_cast(enabled); - unbox(state).config_user_profile->set_blinded_msgreqs(std::move(val)); -} - -// Contact Functions - -LIBSESSION_C_API bool state_get_contacts( - state_object* state, contacts_contact* contact, const char* session_id) { +LIBSESSION_C_API bool state_get_keys( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen) { try { - if (auto c = unbox(state).config_contacts->get(session_id)) { - c->into(*contact); - return true; - } + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto target_namespace = static_cast(namespace_); + auto data = unbox(state).get_keys(target_namespace, pubkey_hex); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; } catch (const std::exception& e) { - set_error(state, e.what()); + return set_error(state, e.what()); } - return false; } -LIBSESSION_C_API bool state_get_or_construct_contacts( - state_object* state, contacts_contact* contact, const char* session_id) { +LIBSESSION_C_API bool state_mutate_user( + state_object* state, void (*callback)(mutable_state_user_object*, void*), void* ctx) { try { - unbox(state).config_contacts->get_or_construct(session_id).into(*contact); + auto s_object = new mutable_state_user_object(); + auto mutable_state = unbox(state).mutableConfig([state](std::string_view e) { + // Don't override an existing error + if (state->last_error) + return; + + set_error(state, e); + }); + s_object->internals = &mutable_state; + callback(s_object, ctx); return true; } catch (const std::exception& e) { return set_error(state, e.what()); } } -LIBSESSION_C_API void state_set_contacts(state_object* state, const contacts_contact* contact) { - unbox(state).config_contacts->set(contact_info{*contact}); -} - -LIBSESSION_C_API bool state_erase_contacts(state_object* state, const char* session_id) { +LIBSESSION_C_API bool state_mutate_group( + state_object* state, + const char* pubkey_hex, + void (*callback)(mutable_state_group_object*, void*), + void* ctx) { try { - return unbox(state).config_contacts->erase(session_id); - } catch (...) { - return false; + auto s_object = new mutable_state_group_object(); + auto mutable_state = + unbox(state).mutableConfig({pubkey_hex, 66}, [state](std::string_view e) { + // Don't override an existing error + if (state->last_error) + return; + + set_error(state, e); + }); + s_object->internals = &mutable_state; + callback(s_object, ctx); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); } } -LIBSESSION_C_API size_t state_size_contacts(const state_object* state) { - return unbox(state).config_contacts->size(); +LIBSESSION_C_API void mutable_state_user_set_error_if_empty( + mutable_state_user_object* state, const char* err, size_t err_len) { + if (auto set_error = unbox(state).set_error; set_error.has_value()) + set_error.value()({err, err_len}); } -LIBSESSION_C_API contacts_iterator* state_new_iterator_contacts(const state_object* state) { - auto* it = new contacts_iterator{}; - auto it2 = unbox(state).config_contacts->begin(); - it->_internals = new Contacts::iterator{unbox(state).config_contacts->begin()}; - return it; +LIBSESSION_C_API void mutable_state_group_set_error_if_empty( + mutable_state_group_object* state, const char* err, size_t err_len) { + if (auto set_error = unbox(state).set_error; set_error.has_value()) + set_error.value()({err, err_len}); } } // extern "C" \ No newline at end of file diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index b97dabc1..e3af8c96 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -1,16 +1,23 @@ +#include #include #include #include +#include +#include #include #include +#include #include +#include +#include #include #include "utils.hpp" using namespace std::literals; using namespace oxenc::literals; +using namespace session; static constexpr int64_t created_ts = 1680064059; @@ -225,6 +232,217 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(nicknames[1] == "Nickname 3"); } +TEST_CASE("State contacts (C API)", "[state][contacts][c]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + std::optional last_store_2 = std::nullopt; + std::optional last_send_2 = std::nullopt; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + + const char* const definitely_real_id = + "050000000000000000000000000000000000000000000000000000000000000000"; + + contacts_contact c; + CHECK_FALSE(state_get_contact(state, &c, definitely_real_id, nullptr)); + + CHECK(state_get_or_construct_contact(state, &c, definitely_real_id, nullptr)); + + CHECK(c.session_id == std::string_view{definitely_real_id}); + CHECK(strlen(c.name) == 0); + CHECK(strlen(c.nickname) == 0); + CHECK_FALSE(c.approved); + CHECK_FALSE(c.approved_me); + CHECK_FALSE(c.blocked); + CHECK(strlen(c.profile_pic.url) == 0); + CHECK(c.created == 0); + + strcpy(c.name, "Joe"); + strcpy(c.nickname, "Joey"); + c.approved = true; + c.approved_me = true; + c.created = created_ts; + + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_contact(mutable_state, static_cast(ctx)); + }, + &c); + + contacts_contact c2; + REQUIRE(state_get_contact(state, &c2, definitely_real_id, nullptr)); + + CHECK(c2.name == "Joe"sv); + CHECK(c2.nickname == "Joey"sv); + CHECK(c2.approved); + CHECK(c2.approved_me); + CHECK_FALSE(c2.blocked); + CHECK(strlen(c2.profile_pic.url) == 0); + + CHECK((*last_store).pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK((*last_send).pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" + "f46"); + + auto ctx_json = nlohmann::json::parse(last_send->ctx); + + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 1); + + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(last_send->data); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + config_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash1", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash1"sv); + free(accepted); + free(merge_data); + + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + contacts_contact c3; + REQUIRE(state_get_contact(state2, &c3, definitely_real_id, nullptr)); + CHECK(c3.name == "Joe"sv); + CHECK(c3.nickname == "Joey"sv); + CHECK(c3.approved); + CHECK(c3.approved_me); + CHECK_FALSE(c3.blocked); + CHECK(strlen(c3.profile_pic.url) == 0); + CHECK(c3.created == created_ts); + + contacts_contact c4; + auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; + REQUIRE(state_get_or_construct_contact(state, &c4, another_id, nullptr)); + CHECK(strlen(c4.name) == 0); + CHECK(strlen(c4.nickname) == 0); + CHECK_FALSE(c4.approved); + CHECK_FALSE(c4.approved_me); + CHECK_FALSE(c4.blocked); + CHECK(strlen(c4.profile_pic.url) == 0); + CHECK(c4.created == 0); + + state_mutate_user( + state2, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_contact(mutable_state, static_cast(ctx)); + }, + &c4); + + auto last_send_json_2 = nlohmann::json::parse(last_send_2->data); + REQUIRE(last_send_json_2.contains(first_request_data)); + auto last_send_data_2 = to_unsigned( + oxenc::from_base64(last_send_json_2[first_request_data].get())); + merge_data = new state_config_message[1]; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash2", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; + REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash2"sv); + free(accepted); + free(merge_data); + + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); + CHECK(state_received_send_response( + state2, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); + REQUIRE(last_send_json_2.contains(messages_key)); + auto obsolete = last_send_json_2[messages_key].get>(); + REQUIRE(obsolete.size() > 0); + CHECK(obsolete.size() == 1); + CHECK(obsolete[0] == "fakehash1"sv); + + // Iterate through and make sure we got everything we expected + std::vector session_ids; + std::vector nicknames; + + CHECK(state_size_contacts(state) == 2); + contacts_iterator* it = contacts_iterator_new(state); + contacts_contact ci; + for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { + session_ids.push_back(ci.session_id); + nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); + } + contacts_iterator_free(it); + + REQUIRE(session_ids.size() == 2); + CHECK(session_ids[0] == definitely_real_id); + CHECK(session_ids[1] == another_id); + CHECK(nicknames[0] == "Joey"); + CHECK(nicknames[1] == "(N/A)"); + + // Changing things while iterating: + it = contacts_iterator_new(state); + int deletions = 0, non_deletions = 0; + std::vector contacts_to_remove; + while (!contacts_iterator_done(it, &ci)) { + if (ci.session_id != std::string_view{definitely_real_id}) { + contacts_to_remove.push_back(ci.session_id); + deletions++; + } else { + non_deletions++; + } + contacts_iterator_advance(it); + } + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + auto contacts_to_remove = static_cast*>(ctx); + + for (auto& cont : *contacts_to_remove) + state_erase_contact(mutable_state, cont.c_str()); + }, + &contacts_to_remove); + + CHECK(deletions == 1); + CHECK(non_deletions == 1); + + CHECK(state_get_contact(state, &ci, definitely_real_id, nullptr)); + CHECK_FALSE(state_get_contact(state, &ci, another_id, nullptr)); +} + TEST_CASE("huge contacts compression", "[config][compression][contacts]") { // Test that we can produce a config message whose *uncompressed* length exceeds the maximum // message length as long as its *compressed* length does not. diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 85395a5a..486f0917 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -1,10 +1,14 @@ +#include #include #include #include #include #include +#include #include +#include +#include #include #include @@ -12,6 +16,9 @@ using namespace std::literals; using namespace oxenc::literals; +using namespace session; + +static constexpr int64_t created_ts = 1680064059; TEST_CASE("Conversations", "[config][conversations]") { @@ -242,30 +249,39 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + char err[256]; + memset(err, 0, 255); + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + std::optional last_store_2 = std::nullopt; + std::optional last_send_2 = std::nullopt; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); const char* const definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; convo_info_volatile_1to1 c; - CHECK_FALSE(convo_info_volatile_get_1to1(conf, &c, definitely_real_id)); - CHECK(conf->last_error == nullptr); - CHECK_FALSE(convo_info_volatile_get_1to1(conf, &c, "05123456")); - CHECK(conf->last_error == - "Invalid session ID: expected 66 hex digits starting with 05; got 05123456"sv); + CHECK_FALSE(state_get_convo_info_volatile_1to1(state, &c, definitely_real_id, err)); + CHECK(err == ""sv); + + CHECK_FALSE(state_get_convo_info_volatile_1to1(state, &c, "05123456", err)); + CHECK(err == "Invalid session ID: expected 66 hex digits starting with 05; got 05123456"sv); - CHECK(convo_info_volatile_size(conf) == 0); + CHECK(state_size_convo_info_volatile(state) == 0); - CHECK(convo_info_volatile_get_or_construct_1to1(conf, &c, definitely_real_id)); + CHECK(state_get_or_construct_convo_info_volatile_1to1(state, &c, definitely_real_id, nullptr)); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(c.last_read == 0); CHECK_FALSE(c.unread); - CHECK_FALSE(config_needs_push(conf)); - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + CHECK_FALSE(session::state::unbox(state).config().needs_dump()); auto now_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) @@ -273,40 +289,36 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { c.last_read = now_ms; - // The new data doesn't get stored until we call this: - convo_info_volatile_set_1to1(conf, &c); - convo_info_volatile_legacy_group cg; - REQUIRE_FALSE(convo_info_volatile_get_legacy_group(conf, &cg, definitely_real_id)); - REQUIRE(convo_info_volatile_get_1to1(conf, &c, definitely_real_id)); - CHECK(c.last_read == now_ms); - - CHECK(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); + REQUIRE_FALSE( + state_get_convo_info_volatile_legacy_group(state, &cg, definitely_real_id, nullptr)); const auto open_group_pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; convo_info_volatile_community og; - CHECK_FALSE(convo_info_volatile_get_or_construct_community( - conf, + CHECK_FALSE(state_get_or_construct_convo_info_volatile_community( + state, &og, "bad-url", "room", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); - CHECK(conf->last_error == "Invalid community URL: invalid/missing protocol://"sv); - CHECK_FALSE(convo_info_volatile_get_or_construct_community( - conf, + "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data(), + err)); + CHECK(err == "Invalid community URL: invalid/missing protocol://"sv); + CHECK_FALSE(state_get_or_construct_convo_info_volatile_community( + state, &og, "https://example.com", "bad room name", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); - CHECK(conf->last_error == "Invalid community URL: room token contains invalid characters"sv); - - CHECK(convo_info_volatile_get_or_construct_community( - conf, &og, "http://Example.ORG:5678", "SudokuRoom", open_group_pubkey.data())); - CHECK(conf->last_error == nullptr); + "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data(), + err)); + CHECK(err == "Invalid community URL: room token contains invalid characters"sv); + + memset(err, 0, 255); + CHECK(state_get_or_construct_convo_info_volatile_community( + state, &og, "http://Example.ORG:5678", "SudokuRoom", open_group_pubkey.data(), err)); + CHECK(err == ""sv); CHECK(og.base_url == "http://example.org:5678"sv); // Note: lower-case CHECK(og.room == "sudokuroom"sv); // Note: lower-case CHECK(oxenc::to_hex(og.pubkey, og.pubkey + 32) == @@ -314,82 +326,141 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { og.unread = true; // The new data doesn't get stored until we call this: - convo_info_volatile_set_community(conf, &og); + std::pair convos = {&c, &og}; + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + auto convos = static_cast< + std::pair*>(ctx); + state_set_convo_info_volatile_1to1(mutable_state, convos->first); + state_set_convo_info_volatile_community(mutable_state, convos->second); + }, + &convos); + + REQUIRE(state_get_convo_info_volatile_1to1(state, &c, definitely_real_id, nullptr)); + CHECK(c.last_read == now_ms); - config_push_data* to_push = config_push(conf); - auto seqno = to_push->seqno; - free(to_push); - CHECK(seqno == 1); + CHECK(session::state::unbox(state).config().needs_push()); + CHECK(session::state::unbox(state).config().needs_dump()); + auto ctx_json = nlohmann::json::parse(last_send->ctx); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 1); // Pretend we uploaded it - config_confirm_pushed(conf, seqno, "hash1"); - CHECK(config_needs_dump(conf)); - CHECK_FALSE(config_needs_push(conf)); - - unsigned char* dump; - size_t dumplen; - config_dump(conf, &dump, &dumplen); - - config_object* conf2; - REQUIRE(convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL) == 0); - free(dump); - - CHECK_FALSE(config_needs_push(conf2)); - CHECK_FALSE(config_needs_dump(conf2)); - - REQUIRE(convo_info_volatile_get_1to1(conf2, &c, definitely_real_id)); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash1\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + CHECK_FALSE(session::state::unbox(state).config().needs_dump()); + + state_namespaced_dump* dumps = new state_namespaced_dump[1]; + dumps[0] = { + static_cast((*last_store).namespace_), + (*last_store).pubkey.c_str(), + (*last_store).data.data(), + (*last_store).data.size()}; + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + free(dumps); + + CHECK_FALSE(session::state::unbox(state2).config().needs_push()); + CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); + + REQUIRE(state_get_convo_info_volatile_1to1(state2, &c, definitely_real_id, nullptr)); CHECK(c.last_read == now_ms); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK_FALSE(c.unread); - REQUIRE(convo_info_volatile_get_community(conf2, &og, "http://EXAMPLE.org:5678", "sudokuRoom")); + REQUIRE(state_get_convo_info_volatile_community( + state2, &og, "http://EXAMPLE.org:5678", "sudokuRoom", nullptr)); CHECK(og.base_url == "http://example.org:5678"sv); CHECK(og.room == "sudokuroom"sv); CHECK(oxenc::to_hex(og.pubkey, og.pubkey + 32) == to_hex(open_group_pubkey)); auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; convo_info_volatile_1to1 c2; - REQUIRE(convo_info_volatile_get_or_construct_1to1(conf2, &c2, another_id)); + REQUIRE(state_get_or_construct_convo_info_volatile_1to1(state2, &c2, another_id, nullptr)); c2.unread = true; - convo_info_volatile_set_1to1(conf2, &c2); - REQUIRE(convo_info_volatile_get_or_construct_legacy_group( - conf2, &cg, "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")); + REQUIRE(state_get_or_construct_convo_info_volatile_legacy_group( + state2, + &cg, + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + nullptr)); cg.last_read = now_ms - 50; - convo_info_volatile_set_legacy_group(conf2, &cg); - CHECK(config_needs_push(conf2)); - - to_push = config_push(conf2); - CHECK(to_push->seqno == 2); - - const char* hash_data[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - hash_data[0] = "hash123"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - config_string_list* accepted = config_merge(conf, hash_data, merge_data, merge_size, 1); + std::pair convos2 = {&c2, &cg}; + state_mutate_user( + state2, + [](mutable_state_user_object* mutable_state, void* ctx) { + auto convos = static_cast< + std::pair*>( + ctx); + state_set_convo_info_volatile_1to1(mutable_state, convos->first); + state_set_convo_info_volatile_legacy_group(mutable_state, convos->second); + }, + &convos2); + REQUIRE(state_get_or_construct_convo_info_volatile_legacy_group( + state2, + &cg, + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + nullptr)); + CHECK(session::state::unbox(state2).config().needs_push()); + ctx_json = nlohmann::json::parse(last_send_2->ctx); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 2); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(last_send_2->data); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + config_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONVO_INFO_VOLATILE, + "hash123", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "hash123"sv); free(accepted); - config_confirm_pushed(conf2, seqno, "hash123"); - free(to_push); - - CHECK_FALSE(config_needs_push(conf)); + free(merge_data); + + ctx_json = nlohmann::json::parse(last_send_2->ctx); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash123\"}}]}"); + CHECK(state_received_send_response( + state2, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send_2->ctx.data(), + last_send_2->ctx.size())); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); std::vector seen; - for (auto* conf : {conf, conf2}) { + for (auto* state : {state, state2}) { // Iterate through and make sure we got everything we expected seen.clear(); - CHECK(convo_info_volatile_size(conf) == 4); - CHECK(convo_info_volatile_size_1to1(conf) == 2); - CHECK(convo_info_volatile_size_communities(conf) == 1); - CHECK(convo_info_volatile_size_legacy_groups(conf) == 1); + CHECK(state_size_convo_info_volatile(state) == 4); + CHECK(state_size_convo_info_volatile_1to1(state) == 2); + CHECK(state_size_convo_info_volatile_communities(state) == 1); + CHECK(state_size_convo_info_volatile_legacy_groups(state) == 1); convo_info_volatile_1to1 c1; convo_info_volatile_community c2; convo_info_volatile_legacy_group c3; - convo_info_volatile_iterator* it = convo_info_volatile_iterator_new(conf); + convo_info_volatile_iterator* it = convo_info_volatile_iterator_new(state); for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { if (convo_info_volatile_it_is_1to1(it, &c1)) { seen.push_back("1-to-1: "s + c1.session_id); @@ -412,22 +483,35 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "c"}}); } - CHECK_FALSE(config_needs_push(conf)); - convo_info_volatile_erase_1to1( - conf, "052000000000000000000000000000000000000000000000000000000000000000"); - CHECK_FALSE(config_needs_push(conf)); - convo_info_volatile_erase_1to1( - conf, "055000000000000000000000000000000000000000000000000000000000000000"); - CHECK(config_needs_push(conf)); - CHECK(convo_info_volatile_size(conf) == 3); - CHECK(convo_info_volatile_size_1to1(conf) == 1); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_erase_convo_info_volatile_1to1( + mutable_state, + "052000000000000000000000000000000000000000000000000000000000000000"); + }, + nullptr); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_erase_convo_info_volatile_1to1( + mutable_state, + "055000000000000000000000000000000000000000000000000000000000000000"); + }, + nullptr); + CHECK(session::state::unbox(state).config().needs_push()); + CHECK(state_size_convo_info_volatile(state) == 3); + CHECK(state_size_convo_info_volatile_1to1(state) == 1); // Check the single-type iterators: seen.clear(); convo_info_volatile_iterator* it; convo_info_volatile_1to1 ci; - for (it = convo_info_volatile_iterator_new_1to1(conf); !convo_info_volatile_iterator_done(it); + for (it = convo_info_volatile_iterator_new_1to1(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_1to1(it, &ci)); seen.push_back(ci.session_id); @@ -439,7 +523,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.clear(); convo_info_volatile_community ogi; - for (it = convo_info_volatile_iterator_new_communities(conf); + for (it = convo_info_volatile_iterator_new_communities(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_community(it, &ogi)); @@ -452,7 +536,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.clear(); convo_info_volatile_legacy_group cgi; - for (it = convo_info_volatile_iterator_new_legacy_groups(conf); + for (it = convo_info_volatile_iterator_new_legacy_groups(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_legacy_group(it, &cgi)); @@ -579,89 +663,157 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + std::optional last_store_2 = std::nullopt; + std::optional last_send_2 = std::nullopt; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); convo_info_volatile_1to1 c; - CHECK(convo_info_volatile_get_or_construct_1to1( - conf, &c, "050123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state, &c, "050123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", err)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf, &c); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); // Fake push: - config_push_data* to_push = config_push(conf); - seqno_t seqno = to_push->seqno; - free(to_push); - CHECK(seqno == 1); - config_confirm_pushed(conf, seqno, "somehash"); - CHECK(config_needs_dump(conf)); - - // Dump: - unsigned char* dump; - size_t dumplen; - config_dump(conf, &dump, &dumplen); + auto ctx_json = nlohmann::json::parse(last_send->ctx); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 1); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"somehash\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); // Load the dump: - config_object* conf2; - REQUIRE(0 == convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL)); - - free(dump); + state_namespaced_dump* dumps = new state_namespaced_dump[1]; + dumps[0] = { + static_cast((*last_store).namespace_), + (*last_store).pubkey.c_str(), + (*last_store).data.data(), + (*last_store).data.size()}; + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + free(dumps); // Change the original again, then push it for conf2: - CHECK(convo_info_volatile_get_or_construct_1to1( - conf, &c, "051111111111111111111111111111111111111111111111111111111111111111")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state, + &c, + "051111111111111111111111111111111111111111111111111111111111111111", + nullptr)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf, &c); - - to_push = config_push(conf); - CHECK(to_push->seqno == 2); - config_confirm_pushed(conf, to_push->seqno, "hash5235"); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); + + ctx_json = nlohmann::json::parse(last_send->ctx); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 2); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash5235\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); // But *before* we load the push make a dirtying change to conf2 that we *don't* push (so that // we'll be merging into a dirty-state config): - CHECK(convo_info_volatile_get_or_construct_1to1( - conf2, &c, "052222111111111111111111111111111111111111111111111111111111111111")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state2, + &c, + "052222111111111111111111111111111111111111111111111111111111111111", + nullptr)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf2, &c); - - // And now, *before* we push the dirty config, also merge the incoming push from `conf`: - const char* merge_hash[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - merge_hash[0] = "hash5235"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - - config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); + state_mutate_user( + state2, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); + + // And now, *before* we push the dirty config, also merge the incoming push from `state`: + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(last_send->data); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + config_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONVO_INFO_VOLATILE, + "hash5235", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "hash5235"sv); free(accepted); - free(to_push); + free(merge_data); - CHECK(config_needs_push(conf2)); + CHECK(session::state::unbox(state2).config().needs_push()); convo_info_volatile_1to1 c1; - REQUIRE(convo_info_volatile_get_or_construct_1to1( - conf2, &c1, "051111111111111111111111111111111111111111111111111111111111111111")); + REQUIRE(state_get_or_construct_convo_info_volatile_1to1( + state2, + &c1, + "051111111111111111111111111111111111111111111111111111111111111111", + nullptr)); c1.last_read += 10; // Prior to the commit that added this test case (and fix), this call would fail with: // Internal error: unexpected dirty but non-mutable ConfigMessage // because of the above dirty->merge->dirty (without an intermediate push) pattern. - REQUIRE_NOTHROW(convo_info_volatile_set_1to1(conf2, &c1)); - - CHECK(config_needs_push(conf2)); - to_push = config_push(conf2); - CHECK(to_push->seqno == 3); - config_confirm_pushed(conf2, to_push->seqno, "hashz"); - CHECK_FALSE(config_needs_push(conf2)); - - config_dump(conf2, &dump, &dumplen); - free(dump); - CHECK_FALSE(config_needs_dump(conf2)); + state_mutate_user( + state2, + [](mutable_state_user_object* mutable_state, void* ctx) { + REQUIRE_NOTHROW(state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx))); + }, + &c1); + + CHECK(session::state::unbox(state2).config().needs_push()); + ctx_json = nlohmann::json::parse(last_send_2->ctx); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 4); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hashz\"}}]}"); + CHECK(state_received_send_response( + state2, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send_2->ctx.data(), + last_send_2->ctx.size())); + CHECK_FALSE(session::state::unbox(state2).config().needs_push()); + CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 25425a4e..52db555b 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -1,9 +1,11 @@ +#include #include #include #include #include #include +#include #include #include #include @@ -13,6 +15,7 @@ using namespace std::literals; using namespace oxenc::literals; +using namespace session; static constexpr int64_t created_ts = 1680064059; @@ -576,15 +579,22 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); char err[256]; - config_object* conf; - rc = user_groups_init(&conf, ed_sk.data(), NULL, 0, err); - REQUIRE(rc == 0); + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; + std::optional last_store_2 = std::nullopt; + std::optional last_send_2 = std::nullopt; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; - ugroups_legacy_group_info* group = - user_groups_get_or_construct_legacy_group(conf, definitely_real_id); + ugroups_legacy_group_info* group; + REQUIRE(state_get_or_construct_ugroups_legacy_group( + state, &group, definitely_real_id, nullptr)); CHECK(group->joined_at == 0); group->joined_at = created_ts; @@ -663,47 +673,91 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { expected_members.emplace(users[i], true); // Non-freeing, so we can keep using `group`; this is less common: - user_groups_set_legacy_group(conf, group); - - group->session_id[2] = 'e'; - // The "normal" way to set a group when you're done with it (also properly frees `group`). - user_groups_set_free_legacy_group(conf, group); - - config_string_list* hashes = config_current_hashes(conf); - REQUIRE(hashes); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + auto group = static_cast(ctx); + state_set_ugroups_legacy_group(mutable_state, group); + + group->session_id[2] = 'e'; + // The "normal" way to set a group when you're done with it (also properly frees + //`group`). + state_set_free_ugroups_legacy_group(mutable_state, group); + }, + group); + + config_string_list* hashes; + REQUIRE(state_current_hashes(state, nullptr, &hashes)); CHECK(hashes->len == 0); free(hashes); - config_push_data* to_push = config_push(conf); - CHECK(to_push->seqno == 1); + CHECK((*last_store).pubkey == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK((*last_send).pubkey == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3" + "a72"); - hashes = config_current_hashes(conf); - REQUIRE(hashes); - CHECK(hashes->len == 0); - free(hashes); + auto ctx_json = nlohmann::json::parse(last_send->ctx); - config_confirm_pushed(conf, to_push->seqno, "fakehash1"); + REQUIRE(ctx_json.contains("seqnos")); + CHECK(ctx_json["seqnos"][0] == 1); - hashes = config_current_hashes(conf); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + CHECK(state_received_send_response( + state, + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response.data(), + send_response.size(), + last_send->ctx.data(), + last_send->ctx.size())); + + REQUIRE(state_current_hashes(state, nullptr, &hashes)); REQUIRE(hashes); REQUIRE(hashes->len == 1); CHECK(hashes->value[0] == "fakehash1"sv); free(hashes); size_t key_len; - unsigned char* keys = config_get_keys(conf, &key_len); + unsigned char* keys; + state_get_keys(state, NAMESPACE_USER_GROUPS, nullptr, &keys, &key_len); REQUIRE(keys); REQUIRE(key_len == 1); - session::config::UserGroups c2{ustring_view{seed}, std::nullopt}; - - std::vector> to_merge; - to_merge.emplace_back("fakehash1", ustring_view{to_push->config, to_push->config_len}); - CHECK(c2.merge(to_merge) == std::vector{{"fakehash1"}}); - - auto grp = c2.get_legacy_group(definitely_real_id); - REQUIRE(grp); - CHECK(grp->members() == expected_members); + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(last_send->data); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + config_string_list* accepted; + merge_data[0] = { + NAMESPACE_USER_GROUPS, + "fakehash1", + created_ts, + last_send_data.data(), + last_send_data.size()}; + CHECK(state_merge(state2, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash1"sv); + free(accepted); + free(merge_data); + + ugroups_legacy_group_info* grp; + REQUIRE(state_get_ugroups_legacy_group(state2, &grp, definitely_real_id, nullptr)); + + found_members.clear(); + it = ugroups_legacy_members_begin(grp); + while (ugroups_legacy_members_next(it, &session_id, &admin)) { + found_members[session_id] = admin; + } + ugroups_legacy_members_free(it); + CHECK(found_members == expected_members); CHECK(grp->joined_at == created_ts); } diff --git a/tests/test_state.cpp b/tests/test_state.cpp index e23bd2d4..9d2f24b5 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -5,6 +5,7 @@ #include "session/config/contacts.h" #include "session/config/namespaces.hpp" +#include "session/config/user_profile.h" #include "session/config/user_profile.hpp" #include "session/state.h" #include "session/state.hpp" @@ -17,42 +18,6 @@ using namespace session::state; using namespace session::config; static constexpr int64_t created_ts = 1680064059; -struct last_store_data { - config::Namespace namespace_; - std::string pubkey; - uint64_t timestamp; - ustring data; -}; -struct last_send_data { - std::string pubkey; - ustring data; - ustring ctx; -}; - -void c_store_callback( - NAMESPACE namespace_, - const char* pubkey, - uint64_t timestamp_ms, - const unsigned char* data, - size_t data_len, - void* ctx) { - *static_cast(ctx) = last_store_data{ - static_cast(namespace_), - {pubkey, 64}, - timestamp_ms, - {data, data_len}}; -} - -void c_send_callback( - const char* pubkey, - const unsigned char* data, - size_t data_len, - const unsigned char* request_ctx, - size_t request_ctx_len, - void* ctx) { - *static_cast(ctx) = - last_send_data{{pubkey, 64}, {data, data_len}, {request_ctx, request_ctx_len}}; -} std::string replace_suffix_between( std::string_view value, @@ -87,16 +52,16 @@ TEST_CASE("State", "[state][state]") { }); // Sanity check direct config access - CHECK_FALSE(state.config_user_profile->get_name().has_value()); - state.config_user_profile->set_name("Test Name"); - CHECK(state.config_user_profile->get_name() == "Test Name"); + CHECK_FALSE(state.config().get_name().has_value()); + state.mutableConfig().user_profile.set_name("Test Name"); + CHECK(state.config().get_name() == "Test Name"); CHECK(last_store->namespace_ == Namespace::UserProfile); CHECK(last_store->pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); CHECK(oxenc::to_hex(last_store->data.begin(), last_store->data.end()) == - "64313a21693165313a2436353a64313a23693165313a266465313a3c6c6c69306533323aea173b57beca8af1" - "8c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d646565313a28303a313a296c65" - "65"); + "64313a21693165313a2438343a64313a23693165313a2664313a6e393a54657374204e616d6565313a3c6c6c" + "69306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a" + "3d64313a6e303a6565313a28303a313a296c6565"); CHECK(last_send->pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" "6"); @@ -105,15 +70,16 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data_no_sig == "{\"method\":\"sequence\",\"params\":{\"requests\":[{\"method\":\"store\",\"params\":{" "\"data\":" - "\"CAESqwMKABIAGqIDCAYoAUKbAxBjSP+U6QQAfuYdxoPMnN/" - "0oleiZOybnqWg9dfVOJR02kXQ7Eypogv5MwlCtRGO1L452dJXroLIGJtu/pJe2FwROk/" - "FoQ5XLHDeY9LaPYj7l0I+Mzt+LG3BMcTEZYLlAVI/2sk80QWDJvlRFyihKJOx5lGEb/" - "lxTrgDf8pQ1dLGxoiNEv47Ygvy4xlzxEbGRVwSp8LPJByKu5YGFMGpTP+pZ9L0vZasFxjK3xnw2/" - "0G1g54zb/p3orgdlUoXUJGSr7d+F7UtSm34KtBTHIGhhCn4CCIxLv1olmmIkGcBwZ7ldVTICcqu+" - "GaNh2jTR1KZPjEef2xIGz8tdzVCKnup6HJO0M+" - "JBT8FSPqvbFt1z9Y7D12wA0Ou82IXXv6ltGGHy3xqMb6IQUw4N+MlfQszNAc7lNUn+" - "wj0DzLzQtorw5oqjbdq2DbxY5bMQq2ACML4MEHUyh0yN/qVc31Q49Edinvuc2ccATeGTysr/y9G+" - "CRTbt88jxgrCP2dcLzEPqIHNyhaWBnqFyfLntYqtsk8KTrSE6N0V7iDxeDFiAA\"," + "\"CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" + "piV1rPki0Quo5Zp34MwwdZXqMFEwRpKGZJwpFPSre6jln5XlmH8tnq8djJo/" + "7QP8kH4m8uUfzsRNgZ1K6agbnGgRolBXgk86/" + "yFmmEsyC81rJF1dgqtkmOhA3nIFpk+yaPt5U5BzsELMQj3sydDB+" + "2iLQE4rIwH43lUtNj2S2YoQ27Mv2FDclbPMOdCOJyTENWt5k/" + "eo0Zovg012oOixj1Uq9I7M9fajgklO+GmE3I3LFGXkmDoDwLYyPavWe68FU8zV9OtFFfUKdIxRJUTZXgU8Kwxzc/" + "U3RzIm8Sc7APgIPkJsTmJr+ckYzLEdzbrqae4gxvzFB22lZYt62rg7KVoaBWUcB3NgFhTxMGc37ysti0pfoxO/" + "T+zkKertLqX+iWNZLRhy3kLaXhEkqafYQzikepvhzD8/" + "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/" + "yAA\"," "\"namespace\":2,\"pubkey\":" "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"pubkey_" "ed25519\":\"8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f\"," @@ -123,17 +89,27 @@ TEST_CASE("State", "[state][state]") { "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"seqnos\":[1]," "\"type\":2}"); + // Confirm the push + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + state.received_send_response( + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", + send_response, + last_send->ctx); + // Init with dumps auto dump = state.dump(Namespace::UserProfile); auto state2 = State({ed_sk.data(), ed_sk.size()}, {{Namespace::UserProfile, std::nullopt, dump}}); - CHECK(state2.config_user_profile->get_name() == "Test Name"); + CHECK_FALSE(state2.config().needs_push()); + CHECK_FALSE(state2.config().needs_dump()); + CHECK(state2.config().get_name() == "Test Name"); // Explicit load auto state3 = State({ed_sk.data(), ed_sk.size()}, {}); - CHECK_FALSE(state3.config_user_profile->get_name().has_value()); + CHECK_FALSE(state3.config().get_name().has_value()); state3.load(Namespace::UserProfile, std::nullopt, dump); - CHECK(state3.config_user_profile->get_name() == "Test Name"); + CHECK(state3.config().get_name() == "Test Name"); } TEST_CASE("State c API", "[state][state][c]") { @@ -147,20 +123,38 @@ TEST_CASE("State c API", "[state][state][c]") { // User Profile forwarding CHECK(state_get_profile_name(state) == nullptr); - state_set_profile_name(state, "Test Name"); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_profile_name(mutable_state, "Test Name"); + }, + nullptr); CHECK(state_get_profile_name(state) == "Test Name"sv); - auto p = user_profile_pic(); - strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < sizeof(p.url)! - memcpy(p.key, "secret78901234567890123456789012", 32); CHECK(strlen(state_get_profile_pic(state).url) == 0); - state_set_profile_pic(state, p); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + auto p = user_profile_pic(); + strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < + // sizeof(p.url)! + + memcpy(p.key, "secret78901234567890123456789012", 32); + state_set_profile_pic(mutable_state, p); + }, + nullptr); + auto stored_pic = state_get_profile_pic(state); CHECK(stored_pic.url == "http://example.org/omg-pic-123.bmp"sv); CHECK(ustring_view{stored_pic.key, 32} == "secret78901234567890123456789012"_bytes); CHECK(state_get_profile_blinded_msgreqs(state) == -1); - state_set_profile_blinded_msgreqs(state, 1); + state_mutate_user( + state, + [](mutable_state_user_object* mutable_state, void* ctx) { + state_set_profile_blinded_msgreqs(mutable_state, 1); + }, + nullptr); CHECK(state_get_profile_blinded_msgreqs(state) == 1); unsigned char* dump1; @@ -173,197 +167,3 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(state_get_profile_name(state2) == "Test Name"sv); } -TEST_CASE("State contacts (C API)", "[state][contacts][c]") { - auto ed_sk = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - - char err[256]; - state_object* state; - REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; - std::optional last_store_2 = std::nullopt; - std::optional last_send_2 = std::nullopt; - - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); - - const char* const definitely_real_id = - "050000000000000000000000000000000000000000000000000000000000000000"; - - contacts_contact c; - CHECK_FALSE(state_get_contacts(state, &c, definitely_real_id)); - - CHECK(state_get_or_construct_contacts(state, &c, definitely_real_id)); - - CHECK(c.session_id == std::string_view{definitely_real_id}); - CHECK(strlen(c.name) == 0); - CHECK(strlen(c.nickname) == 0); - CHECK_FALSE(c.approved); - CHECK_FALSE(c.approved_me); - CHECK_FALSE(c.blocked); - CHECK(strlen(c.profile_pic.url) == 0); - CHECK(c.created == 0); - - strcpy(c.name, "Joe"); - strcpy(c.nickname, "Joey"); - c.approved = true; - c.approved_me = true; - c.created = created_ts; - - state_set_contacts(state, &c); - - contacts_contact c2; - REQUIRE(state_get_contacts(state, &c2, definitely_real_id)); - - CHECK(c2.name == "Joe"sv); - CHECK(c2.nickname == "Joey"sv); - CHECK(c2.approved); - CHECK(c2.approved_me); - CHECK_FALSE(c2.blocked); - CHECK(strlen(c2.profile_pic.url) == 0); - - CHECK((*last_store).pubkey == - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f"); - CHECK((*last_send).pubkey == - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" - "f"); - - auto ctx_json = nlohmann::json::parse(last_send->ctx); - - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 1); - - state_object* state2; - REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); - state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); - state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); - - auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->data); - REQUIRE(last_send_json.contains(first_request_data)); - auto last_send_data = - to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); - state_config_message* merge_data = new state_config_message[1]; - config_string_list* accepted; - merge_data[0] = { - NAMESPACE_CONTACTS, - "fakehash1", - created_ts, - last_send_data.data(), - last_send_data.size()}; - REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash1"sv); - free(accepted); - free(merge_data); - - ustring send_response = - to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); - - contacts_contact c3; - REQUIRE(state_get_contacts(state2, &c3, definitely_real_id)); - CHECK(c3.name == "Joe"sv); - CHECK(c3.nickname == "Joey"sv); - CHECK(c3.approved); - CHECK(c3.approved_me); - CHECK_FALSE(c3.blocked); - CHECK(strlen(c3.profile_pic.url) == 0); - CHECK(c3.created == created_ts); - - contacts_contact c4; - auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; - REQUIRE(state_get_or_construct_contacts(state, &c4, another_id)); - CHECK(strlen(c4.name) == 0); - CHECK(strlen(c4.nickname) == 0); - CHECK_FALSE(c4.approved); - CHECK_FALSE(c4.approved_me); - CHECK_FALSE(c4.blocked); - CHECK(strlen(c4.profile_pic.url) == 0); - CHECK(c4.created == 0); - - state_set_contacts(state2, &c4); - - auto last_send_json_2 = nlohmann::json::parse(last_send_2->data); - REQUIRE(last_send_json_2.contains(first_request_data)); - auto last_send_data_2 = to_unsigned( - oxenc::from_base64(last_send_json_2[first_request_data].get())); - merge_data = new state_config_message[1]; - merge_data[0] = { - NAMESPACE_CONTACTS, - "fakehash2", - created_ts, - last_send_data_2.data(), - last_send_data_2.size()}; - REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash2"sv); - free(accepted); - free(merge_data); - - send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); - CHECK(state_received_send_response( - state2, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); - - auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); - REQUIRE(last_send_json_2.contains(messages_key)); - auto obsolete = last_send_json_2[messages_key].get>(); - REQUIRE(obsolete.size() > 0); - CHECK(obsolete.size() == 1); - CHECK(obsolete[0] == "fakehash1"sv); - - // Iterate through and make sure we got everything we expected - std::vector session_ids; - std::vector nicknames; - - CHECK(state_size_contacts(state) == 2); - contacts_iterator* it = state_new_iterator_contacts(state); - contacts_contact ci; - for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { - session_ids.push_back(ci.session_id); - nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); - } - contacts_iterator_free(it); - - REQUIRE(session_ids.size() == 2); - CHECK(session_ids[0] == definitely_real_id); - CHECK(session_ids[1] == another_id); - CHECK(nicknames[0] == "Joey"); - CHECK(nicknames[1] == "(N/A)"); - - // Changing things while iterating: - it = state_new_iterator_contacts(state); - int deletions = 0, non_deletions = 0; - std::vector contacts_to_remove; - while (!contacts_iterator_done(it, &ci)) { - if (ci.session_id != std::string_view{definitely_real_id}) { - contacts_to_remove.push_back(ci.session_id); - deletions++; - } else { - non_deletions++; - } - contacts_iterator_advance(it); - } - for (auto& cont : contacts_to_remove) - state_erase_contacts(state, cont.c_str()); - - CHECK(deletions == 1); - CHECK(non_deletions == 1); - - CHECK(state_get_contacts(state, &ci, definitely_real_id)); - CHECK_FALSE(state_get_contacts(state, &ci, another_id)); -} - diff --git a/tests/utils.hpp b/tests/utils.hpp index 76b145c1..63518ef4 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -11,6 +11,8 @@ #include #include "session/config/base.h" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" using ustring = std::basic_string; using ustring_view = std::basic_string_view; @@ -88,3 +90,40 @@ std::vector> view_vec(const std::vector*>(ctx) = last_store_data{ + static_cast(namespace_), + {pubkey, 66}, + timestamp_ms, + {data, data_len}}; +} + +inline void c_send_callback( + const char* pubkey, + const unsigned char* data, + size_t data_len, + const unsigned char* request_ctx, + size_t request_ctx_len, + void* ctx) { + *static_cast*>(ctx) = + last_send_data{{pubkey, 66}, {data, data_len}, {request_ctx, request_ctx_len}}; +} From 9fa587d3413a8d13a0d183634e6829807d1ff05a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 15 Feb 2024 16:22:10 +1100 Subject: [PATCH 08/24] Updated GroupInfo C API, added create_group function, tweaks to send hook --- include/session/config/base.hpp | 9 + include/session/config/groups/info.h | 194 +++--- include/session/state.h | 121 ++-- include/session/state.hpp | 81 ++- src/config/groups/info.cpp | 380 +++++------- src/state.cpp | 683 +++++++++++++--------- src/state_c_wrapper.cpp | 219 ++++--- tests/test_config_contacts.cpp | 28 +- tests/test_config_convo_info_volatile.cpp | 70 +-- tests/test_config_user_groups.cpp | 17 +- tests/test_state.cpp | 50 +- tests/utils.hpp | 26 +- 12 files changed, 1008 insertions(+), 870 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 2789b5ca..9096713d 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1235,6 +1235,15 @@ class ConfigBase : public ConfigSig { assert(i < _keys.size()); return {_keys[i].data(), _keys[i].size()}; } + + /// API: base/ConfigBase::get_seqno + /// + /// Retrieves the current seqno for the config. If there is a pending push then this will return + /// the updated seqno. + /// + /// Outputs: + /// - `seqno_t` -- current seqno + seqno_t get_seqno() const { return _config->seqno(); }; }; // The C++ struct we hold opaquely inside the C internals struct. This is designed so that any diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 32da3ae9..3623969f 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -4,6 +4,7 @@ extern "C" { #endif +#include "../../state.h" #include "../base.h" #include "../profile_pic.h" #include "../util.h" @@ -11,50 +12,24 @@ extern "C" { LIBSESSION_EXPORT extern const size_t GROUP_INFO_NAME_MAX_LENGTH; LIBSESSION_EXPORT extern const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH; -/// API: groups/groups_info_init -/// -/// Constructs a group info config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Inputs: -/// - `conf` -- [out] Pointer to the config object -/// - `ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is NULL. -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_info_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups_info/groups_info_get_name +/// API: groups_info/state_get_groups_info_name /// /// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at /// all. Should be copied right away as the pointer may not remain valid beyond other API calls. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `name` -- [out] the pointer to a buffer in which we will write the null-terminated name +/// string. This must be a +/// buffer of at least 'GROUP_INFO_NAME_MAX_LENGTH' bytes. /// /// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_EXPORT const char* groups_info_get_name(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group name +LIBSESSION_EXPORT bool state_get_groups_info_name( + const state_object* state, const char* pubkey_hex, char* name); -/// API: groups_info/groups_info_set_name +/// API: groups_info/state_set_groups_info_name /// /// Sets the group's name to the null-terminated C string. Returns 0 on success, non-zero on /// error (and sets the config_object's error string). @@ -63,28 +38,30 @@ LIBSESSION_EXPORT const char* groups_info_get_name(const config_object* conf); /// truncated. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_name(config_object* conf, const char* name); +LIBSESSION_EXPORT void state_set_groups_info_name( + mutable_state_group_object* state, const char* name); -/// API: groups_info/groups_info_get_description +/// API: groups_info/state_get_groups_info_description /// /// Returns a pointer to the currently-set description (null-terminated), or NULL if there is no /// description at all. Should be copied right away as the pointer may not remain valid beyond /// other API calls. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `description` -- [out] the pointer to a buffer in which we will write the null-terminated +/// description string. This must be a +/// buffer of at least 'GROUP_INFO_DESCRIPTION_MAX_LENGTH' bytes. /// /// Outputs: -/// - `char*` -- Pointer to the currently-set description as a null-terminated string, or NULL if -/// there is no description -LIBSESSION_EXPORT const char* groups_info_get_description(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group description +LIBSESSION_EXPORT bool state_get_groups_info_description( + const state_object* state, const char* pubkey_hex, char* description); -/// API: groups_info/groups_info_set_description +/// API: groups_info/state_set_groups_info_description /// /// Sets the group's description to the null-terminated C string. Returns 0 on success, non-zero on /// error (and sets the config_object's error string). @@ -93,144 +70,167 @@ LIBSESSION_EXPORT const char* groups_info_get_description(const config_object* c /// will be truncated. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `description` -- [in] Pointer to the description as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_description(config_object* conf, const char* description); +LIBSESSION_EXPORT void state_set_groups_info_description( + mutable_state_group_object* state, const char* description); -/// API: groups_info/groups_info_get_pic +/// API: groups_info/state_get_groups_info_pic /// /// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile /// pic is not currently set, and otherwise should be copied right away (they will not be valid /// beyond other API calls on this config object). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `description` -- [out] the pointer that will be set to the current profile pic (despite the +/// "user_profile" in +/// the struct name, this is the group's profile pic). /// /// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic (despite the "user_profile" in -/// the struct name, this is the group's profile pic). -LIBSESSION_EXPORT user_profile_pic groups_info_get_pic(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group profile pic +LIBSESSION_EXPORT bool state_get_groups_info_pic( + const state_object* state, const char* pubkey_hex, user_profile_pic* pic); -/// API: groups_info/groups_info_set_pic +/// API: groups_info/state_set_groups_info_pic /// /// Sets a user profile /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_pic(config_object* conf, user_profile_pic pic); +LIBSESSION_EXPORT void state_set_groups_info_pic( + mutable_state_group_object* state, user_profile_pic pic); -/// API: groups_info/groups_info_get_expiry_timer +/// API: groups_info/state_get_groups_info_expiry_timer /// /// Gets the group's message expiry timer (seconds). Returns 0 if not set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `timer` -- [out] Pointer that will be set to the expiry timer in seconds. /// /// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_EXPORT int groups_info_get_expiry_timer(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group expiry timer +LIBSESSION_EXPORT bool state_get_groups_info_expiry_timer( + const state_object* state, const char* pubkey_hex, int* timer); -/// API: groups_info/groups_info_set_expiry_timer +/// API: groups_info/state_set_groups_info_expiry_timer /// /// Sets the group's message expiry timer (seconds). Setting 0 (or negative) will clear the current /// timer. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void groups_info_set_expiry_timer(config_object* conf, int expiry); +LIBSESSION_EXPORT void state_set_groups_info_expiry_timer( + mutable_state_group_object* state, int expiry); -/// API: groups_info/groups_info_get_created +/// API: groups_info/state_get_groups_info_created /// /// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `created` -- [out] Pointer that will be set to the unix timestamp when the group was created +/// (if set by an admin). /// /// Outputs: -/// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). -LIBSESSION_EXPORT int64_t groups_info_get_created(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group created +/// timestamp +LIBSESSION_EXPORT bool state_get_groups_info_created( + const state_object* state, const char* pubkey_hex, int64_t* created); -/// API: groups_info/groups_info_set_created +/// API: groups_info/state_set_groups_info_created /// /// Sets the creation time (unix timestamp, in seconds) when the group was created. Setting 0 /// clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_created(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void groups_info_set_created(mutable_state_group_object* state, int64_t ts); -/// API: groups_info/groups_info_get_delete_before +/// API: groups_info/state_get_groups_info_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds); clients should delete all messages /// from the group with timestamps earlier than this value, if set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which messages +/// should be deleted. Returns 0 if not set. /// /// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_EXPORT int64_t groups_info_get_delete_before(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted +/// before value +LIBSESSION_EXPORT bool state_get_groups_info_delete_before( + const state_object* state, const char* pubkey_hex, int64_t* delete_before); -/// API: groups_info/groups_info_set_delete_before +/// API: groups_info/state_set_groups_info_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) before which messages should be /// deleted. Setting 0 clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_delete_before(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void state_set_groups_info_delete_before( + mutable_state_group_object* state, int64_t ts); -/// API: groups_info/groups_info_get_attach_delete_before +/// API: groups_info/state_get_groups_info_attach_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds) for attachments; clients should drop /// all attachments from messages from the group with timestamps earlier than this value, if set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which message +/// attachments should be deleted. Returns 0 if not set. /// /// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_EXPORT int64_t groups_info_get_attach_delete_before(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted +/// before value +LIBSESSION_EXPORT bool state_get_groups_info_attach_delete_before( + const state_object* state, const char* pubkey_hex, int64_t* delete_before); -/// API: groups_info/groups_info_set_attach_delete_before +/// API: groups_info/state_set_groups_info_attach_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) for attachments; attachments should be /// dropped from messages older than this value. Setting 0 clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_attach_delete_before(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void state_set_groups_info_attach_delete_before( + mutable_state_group_object* state, int64_t ts); -/// API: groups_info/groups_info_is_destroyed(const config_object* conf); +/// API: groups_info/state_groups_info_is_destroyed /// /// Returns true if this group has been marked destroyed by an admin, which indicates to a receiving /// client that they should destroy it locally. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// /// Outputs: /// - `true` if the group has been nuked, `false` otherwise. -LIBSESSION_EXPORT bool groups_info_is_destroyed(const config_object* conf); +LIBSESSION_EXPORT bool state_groups_info_is_destroyed( + const state_object* state, const char* pubkey_hex); -/// API: groups_info/groups_info_destroy_group(const config_object* conf); +/// API: groups_info/state_destroy_group /// /// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object -LIBSESSION_EXPORT void groups_info_destroy_group(config_object* conf); +/// - `state` -- [in] Pointer to the mutable state object +LIBSESSION_EXPORT void state_destroy_group(mutable_state_group_object* state); #ifdef __cplusplus } // extern "C" diff --git a/include/session/state.h b/include/session/state.h index 97899fac..29e8a5bc 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -9,7 +9,9 @@ extern "C" { #include #include "config/base.h" +#include "config/groups/members.h" #include "config/namespaces.h" +#include "config/profile_pic.h" #include "export.h" typedef struct state_object { @@ -49,26 +51,12 @@ typedef struct state_config_message { size_t datalen; } state_config_message; -/// API: state/state_create -/// -/// Constructs a new state which generates it's own random ed25519 key pair. -/// -/// When done with the object the `state_object` must be destroyed by passing the pointer to -/// state_free(). -/// -/// Inputs: -/// - `state` -- [out] Pointer to the state object -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT bool state_create(state_object** state, char* error) - __attribute__((warn_unused_result)); +typedef struct state_send_response { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} state_send_response; -/// API: state/state_create +/// API: state/state_init /// /// Constructs a new state which generates it's own random ed25519 key pair. /// @@ -151,24 +139,31 @@ LIBSESSION_EXPORT void state_set_logger( /// API: state/state_set_send_callback /// /// Takes a function pointer and a context pointer (which can be NULL if not needed). The given -/// function pointer will be invoked whenever a config `needs_push` as long as the state isn't -/// suppressing send events. -/// -/// The function must have signature: -/// -/// void callback(const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*); +/// function pointer will be invoked whenever a config `needs_push`. The function pointer contains +/// it's own callback function pointer which should be called by the client when it receives a +/// network response to the original send request. /// /// Can be called with callback set to NULL to clear an existing hook. /// /// Inputs: /// - `state` -- [in] Pointer to state_object object /// - `callback` -- [in] Callback function -/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +/// - `app_ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused LIBSESSION_EXPORT bool state_set_send_callback( state_object* state, void (*callback)( - const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*), - void* ctx); + const char* pubkey, + const unsigned char* data, + size_t data_len, + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context), + void* app_ctx); /// API: state/state_set_store_callback /// @@ -211,7 +206,7 @@ LIBSESSION_EXPORT void state_set_service_node_offset(state_object* state, int64_ /// Outputs: /// - `int64_t` -- the delta between the current device time and service node time in the /// most recent API response -LIBSESSION_EXPORT int64_t state_network_offset(state_object* state); +LIBSESSION_EXPORT int64_t state_network_offset(const state_object* state); /// API: state/state_merge /// @@ -246,6 +241,37 @@ LIBSESSION_EXPORT bool state_merge( LIBSESSION_EXPORT bool state_current_hashes( state_object* state, const char* pubkey_hex_, config_string_list** current_hashes); +/// API: state/state_current_hashes +/// +/// The current config hashes; this can be empty if the current hashes are unknown or the current +/// state is not clean (i.e. a push is needed or pending). +/// +/// Inputs: +/// - `state` -- [in] Pointer to state object +/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 +/// bytes). Required for group hashes. +/// - `current_hashes` -- [out] Pointer to an array of the current config hashes +LIBSESSION_EXPORT bool state_current_hashes( + state_object* state, const char* pubkey_hex_, config_string_list** current_hashes); + +/// API: state/state_current_seqno +/// +/// The current config seqno; this will return the updated seqno if there is a pending push. If +/// an invalid pubkey is provided when trying to retrieve for a group namespace then '-1' is +/// returned. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state object +/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 +/// bytes). Required for group namespaces. +/// - `namespace` -- [in] The namespace to retrieve the seqno for. +/// +/// Outputs: +/// - `seqno_t` -- The seqno for the config state associated with the given pubkey and namespace (or +/// -1 if invalid). +LIBSESSION_EXPORT seqno_t +state_current_seqno(state_object* state, const char* pubkey_hex, NAMESPACE namespace_); + /// API: state/state_dump /// /// Returns a bt-encoded dict containing the dumps of each of the current config states for @@ -302,21 +328,24 @@ LIBSESSION_EXPORT bool state_dump_namespace( /// /// Inputs: /// - `state` -- [in] Pointer to state_object object -/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 -/// bytes). Required for group dumps. -/// - `response_data` -- [in] Pointer to the response from the swarm after sending the -/// `payload_data`. -/// - `response_data_len` -- [in] Length of the `response_data`. /// - `request_ctx` -- [in] Pointer to the request context data which was provided by the `send` /// hook. /// - `request_ctx_len` -- [in] Length of the `request_ctx`. +/// - `response_data` -- [in] Pointer to the response from the swarm after sending the +/// `payload_data`. +/// - `response_data_len` -- [in] Length of the `response_data`. +// LIBSESSION_EXPORT bool state_received_send_response( +// state_object* state, +// unsigned char* request_ctx, +// size_t request_ctx_len, +// unsigned char* response_data, +// size_t response_data_len); + LIBSESSION_EXPORT bool state_received_send_response( state_object* state, - const char* pubkey_hex, - unsigned char* response_data, - size_t response_data_len, - unsigned char* request_ctx, - size_t request_ctx_len); + const state_send_response* callback, + const unsigned char* response, + const size_t size); /// API: state/state_get_keys /// @@ -343,6 +372,20 @@ LIBSESSION_EXPORT bool state_get_keys( unsigned char** out, size_t* outlen); +LIBSESSION_EXPORT void state_create_group( + state_object* state, + const char* name, + const char* description, + const user_profile_pic pic_, + const config_group_member* members_, + const size_t members_len, + void (*callback)( + bool success, const char* group_id, unsigned const char* group_sk, void* ctx), + void* ctx); + +LIBSESSION_EXPORT void state_approve_group( + state_object* state, const char* group_id, unsigned const char* group_sk); + /// API: state/state_mutate_user /// /// Calls the callback provided with a mutable version of the `state_object` for user changes. diff --git a/include/session/state.hpp b/include/session/state.hpp index ca4ea270..89ec9dbf 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -22,16 +22,19 @@ using Ed25519Secret = std::array; /// Struct containing group configs. class GroupConfigs { public: - GroupConfigs(ustring_view pubkey, ustring_view user_sk); + GroupConfigs( + ustring_view pubkey, + ustring_view user_sk, + std::optional ed25519_secretkey = std::nullopt); GroupConfigs(GroupConfigs&&) = delete; GroupConfigs(const GroupConfigs&) = delete; GroupConfigs& operator=(GroupConfigs&&) = delete; GroupConfigs& operator=(const GroupConfigs&) = delete; - std::unique_ptr config_info; - std::unique_ptr config_members; - std::unique_ptr config_keys; + std::unique_ptr info; + std::unique_ptr members; + std::unique_ptr keys; }; class MutableUserConfigs { @@ -132,12 +135,17 @@ struct config_message { bool operator!=(const config_message& b) const { return cmpval() != b.cmpval(); } }; +struct PreparedPush { + ustring payload; + std::vector> namespace_seqno; +}; + class State { private: - std::unique_ptr config_contacts; - std::unique_ptr config_convo_info_volatile; - std::unique_ptr config_user_groups; - std::unique_ptr config_user_profile; + std::unique_ptr _config_contacts; + std::unique_ptr _config_convo_info_volatile; + std::unique_ptr _config_user_groups; + std::unique_ptr _config_user_profile; std::map> _config_groups; protected: @@ -151,7 +159,12 @@ class State { uint64_t timestamp_ms, ustring data)> _store; - std::function _send; + std::function + received_response)> + _send; public: std::chrono::milliseconds network_offset; @@ -181,7 +194,7 @@ class State { // Hook which will be called whenever config dumps need to be saved to persistent storage. The // hook will immediately be called upon assignment if the state needs to be stored. - void onStore(std::function< + void on_store(std::function< void(config::Namespace namespace_, std::string prefixed_pubkey, uint64_t timestamp_ms, @@ -204,7 +217,13 @@ class State { /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. /// - `payload` -- payload which should be sent to the API. /// - `ctx` -- contextual data which should be used when processing the response. - void onSend(std::function hook) { + /// - `received_response` -- callback which should be called with the response from the send + /// request. + void on_send(std::function< + void(std::string pubkey, + ustring payload, + std::function + received_response)> hook) { _send = hook; if (!hook) @@ -334,18 +353,6 @@ class State { config::Namespace namespace_, std::optional pubkey_hex = std::nullopt); - /// API: state/State::received_send_response - /// - /// Takes the network response from sending the data from the `send` hook and confirms the - /// configs were successfully pushed. - /// - /// Inputs: - /// - `pubkey` -- the pubkey (in hex, with prefix - 66 bytes) for the swarm where the data was - /// sent. - /// - `response_data` -- response that was returned from the swarm. - /// - `ctx` -- the contextual data provided by the onSend hook. - void received_send_response(std::string pubkey, ustring response_data, ustring ctx); - /// API: state/State::get_keys /// /// Returns a vector of encryption keys, in priority order (i.e. element 0 is the encryption @@ -365,6 +372,16 @@ class State { std::vector get_keys( config::Namespace namespace_, std::optional pubkey_hex_); + void create_group( + std::string_view name, + std::optional description, + std::optional pic, + std::vector members, + std::function + callback); + + void approve_group(std::string_view group_id, std::optional group_sk); + // Retrieves a read-only version of the user config template const ConfigType& config() const; @@ -375,12 +392,12 @@ class State { // Retrieves an editable version of the user config. Once the returned value is deconstructed it // will trigger the `send` and `store` hooks. - MutableUserConfigs mutableConfig( + MutableUserConfigs mutable_config( std::optional> set_error = std::nullopt); // Retrieves an editable version of the group config for the given public key. Once the returned // value is deconstructed it will trigger the `send` and `store` hooks. - MutableGroupConfigs mutableConfig( + MutableGroupConfigs mutable_config( std::string_view pubkey_hex, std::optional> set_error = std::nullopt); @@ -388,7 +405,16 @@ class State { template void add_child_logger(ConfigType& base); - void handle_config_push_response(std::string pubkey, ustring response, ustring ctx); + PreparedPush prepare_push( + std::string pubkey_hex, + std::chrono::milliseconds timestamp, + std::vector configs); + void handle_config_push_response( + std::string pubkey, + std::vector> namespace_seqnos, + bool success, + uint16_t status_code, + ustring response); void validate_group_pubkey(std::string_view pubkey_hex) const; }; @@ -419,6 +445,9 @@ inline bool set_error(state_object* state, std::string_view e) { } inline bool set_error_value(char* error, std::string_view e) { + if (!error) + return false; + std::string msg = {e.data(), e.size()}; if (msg.size() > 255) msg.resize(255); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 88b3a1eb..cd79897d 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -9,6 +9,8 @@ #include "session/config/error.h" #include "session/config/groups/info.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -117,270 +119,154 @@ bool Info::is_destroyed() const { } // namespace session::config::groups using namespace session; +using namespace session::state; using namespace session::config; LIBSESSION_C_API const size_t GROUP_INFO_NAME_MAX_LENGTH = groups::Info::NAME_MAX_LENGTH; LIBSESSION_C_API const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH = groups::Info::DESCRIPTION_MAX_LENGTH; -LIBSESSION_C_API int groups_info_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) { - return c_group_wrapper_init( - conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); -} - -/// API: groups_info/groups_info_get_name -/// -/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at -/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_C_API const char* groups_info_get_name(const config_object* conf) { - if (auto s = unbox(conf)->get_name()) - return s->data(); - return nullptr; -} - -/// API: groups_info/groups_info_set_name -/// -/// Sets the group's name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_name(config_object* conf, const char* name) { +LIBSESSION_C_API bool state_get_groups_info_name( + const state_object* state, const char* pubkey_hex, char* name) { try { - unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + if (auto s = unbox(state).config({pubkey_hex, 66}).get_name()) { + std::string res = {s->data(), s->size()}; + if (res.size() > groups::Info::NAME_MAX_LENGTH) + res.resize(groups::Info::NAME_MAX_LENGTH); + std::memcpy(name, res.c_str(), res.size() + 1); + return true; + } + } catch (...) { } - return 0; -} - -/// API: groups_info/groups_info_get_description -/// -/// Returns a pointer to the currently-set description (null-terminated), or NULL if there is -/// no description at all. Should be copied right away as the pointer may not remain valid -/// beyond other API calls. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `char*` -- Pointer to the currently-set description as a null-terminated string, or NULL -/// if there is no description -LIBSESSION_C_API const char* groups_info_get_description(const config_object* conf) { - if (auto s = unbox(conf)->get_description()) - return s->data(); - return nullptr; -} - -/// API: groups_info/groups_info_set_description -/// -/// Sets the group's description to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `description` -- [in] Pointer to the description as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_description(config_object* conf, const char* description) { + return false; +} + +LIBSESSION_C_API void state_set_groups_info_name( + mutable_state_group_object* state, const char* name) { + unbox(state).info.set_name(name); +} + +LIBSESSION_C_API bool state_get_groups_info_description( + const state_object* state, const char* pubkey_hex, char* description) { try { - unbox(conf)->set_description(description); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + if (auto s = unbox(state).config({pubkey_hex, 66}).get_description()) { + std::string res = {s->data(), s->size()}; + if (res.size() > groups::Info::DESCRIPTION_MAX_LENGTH) + res.resize(groups::Info::DESCRIPTION_MAX_LENGTH); + std::memcpy(description, res.c_str(), res.size() + 1); + return true; + } + } catch (...) { } - return 0; -} - -/// API: groups_info/groups_info_get_pic -/// -/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile -/// pic is not currently set, and otherwise should be copied right away (they will not be valid -/// beyond other API calls on this config object). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic (despite the "user_profile" in -/// the struct name, this is the group's profile pic). -LIBSESSION_C_API user_profile_pic groups_info_get_pic(const config_object* conf) { - user_profile_pic p; - if (auto pic = unbox(conf)->get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; + return false; +} + +LIBSESSION_C_API void state_set_groups_info_description( + mutable_state_group_object* state, const char* description) { + unbox(state).info.set_description(description); +} + +LIBSESSION_C_API bool state_get_groups_info_pic( + const state_object* state, const char* pubkey_hex, user_profile_pic* pic) { + try { + if (auto p = unbox(state).config({pubkey_hex, 66}).get_profile_pic()) { + copy_c_str(pic->url, p.url); + std::memcpy(pic->key, p.key.data(), 32); + return true; + } + } catch (...) { } - return p; -} - -/// API: groups_info/groups_info_set_pic -/// -/// Sets a user profile -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic pic) { + return false; +} + +LIBSESSION_C_API void state_set_groups_info_pic( + mutable_state_group_object* state, user_profile_pic pic) { std::string_view url{pic.url}; ustring_view key; if (!url.empty()) key = {pic.key, 32}; + unbox(state).info.set_profile_pic(url, key); +} + +LIBSESSION_C_API bool state_get_groups_info_expiry_timer( + const state_object* state, const char* pubkey_hex, int* timer) { + try { + *timer = unbox(state) + .config({pubkey_hex, 66}) + .get_expiry_timer() + .value_or(0s) + .count(); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_set_groups_info_expiry_timer( + mutable_state_group_object* state, int expiry) { + unbox(state).info.set_expiry_timer(std::max(0, expiry) * 1s); +} + +LIBSESSION_C_API bool state_get_groups_info_created( + const state_object* state, const char* pubkey_hex, int64_t* created) { + try { + *created = unbox(state).config({pubkey_hex, 66}).get_created().value_or(0); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void groups_info_set_created(mutable_state_group_object* state, int64_t ts) { + unbox(state).info.set_created(std::max(0, ts)); +} + +LIBSESSION_C_API bool state_get_groups_info_delete_before( + const state_object* state, const char* pubkey_hex, int64_t* delete_before) { try { - unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + *delete_before = + unbox(state).config({pubkey_hex, 66}).get_delete_before().value_or(0); + return true; + } catch (...) { } + return false; +} + +LIBSESSION_C_API void state_set_groups_info_delete_before( + mutable_state_group_object* state, int64_t ts) { + unbox(state).info.set_delete_before(std::max(0, ts)); +} + +LIBSESSION_C_API bool state_get_groups_info_attach_delete_before( + const state_object* state, const char* pubkey_hex, int64_t* delete_before) { + try { + *delete_before = unbox(state) + .config({pubkey_hex, 66}) + .get_delete_attach_before() + .value_or(0); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_set_groups_info_attach_delete_before( + mutable_state_group_object* state, int64_t ts) { + unbox(state).info.set_delete_attach_before(std::max(0, ts)); +} + +LIBSESSION_C_API bool state_groups_info_is_destroyed( + const state_object* state, const char* pubkey_hex) { + try { + if (unbox(state).config({pubkey_hex, 66}).is_destroyed()) { + return true; + } + } catch (...) { + } + return false; +} - return 0; -} - -/// API: groups_info/groups_info_get_expiry_timer -/// -/// Gets the group's message expiry timer (seconds). Returns 0 if not set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_C_API int groups_info_get_expiry_timer(const config_object* conf) { - if (auto t = unbox(conf)->get_expiry_timer(); t && *t > 0s) - return t->count(); - return 0; -} - -/// API: groups_info/groups_info_set_expiry_timer -/// -/// Sets the group's message expiry timer (seconds). Setting 0 (or negative) will clear the current -/// timer. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_C_API void groups_info_set_expiry_timer(config_object* conf, int expiry) { - unbox(conf)->set_expiry_timer(std::max(0, expiry) * 1s); -} - -/// API: groups_info/groups_info_get_created -/// -/// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). -LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { - return unbox(conf)->get_created().value_or(0); -} - -/// API: groups_info/groups_info_set_created -/// -/// Sets the creation time (unix timestamp, in seconds) when the group was created. Setting 0 -/// clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { - unbox(conf)->set_created(std::max(0, ts)); -} - -/// API: groups_info/groups_info_get_delete_before -/// -/// Returns the delete-before timestamp (unix time, in seconds); clients should deleted all messages -/// from the group with timestamps earlier than this value, if set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_before().value_or(0); -} - -/// API: groups_info/groups_info_set_delete_before -/// -/// Sets the delete-before time (unix timestamp, in seconds) before which messages should be delete. -/// Setting 0 clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_before(std::max(0, ts)); -} - -/// API: groups_info/groups_info_get_attach_delete_before -/// -/// Returns the delete-before timestamp (unix time, in seconds) for attachments; clients should drop -/// all attachments from messages from the group with timestamps earlier than this value, if set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_attach_before().value_or(0); -} - -/// API: groups_info/groups_info_set_attach_delete_before -/// -/// Sets the delete-before time (unix timestamp, in seconds) for attachments; attachments should be -/// dropped from messages older than this value. Setting 0 clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_attach_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_attach_before(std::max(0, ts)); -} - -/// API: groups_info/groups_info_is_destroyed(const config_object* conf); -/// -/// Returns true if this group has been marked destroyed by an admin, which indicates to a receiving -/// client that they should destroy it locally. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `true` if the group has been nuked, `false` otherwise. -LIBSESSION_C_API bool groups_info_is_destroyed(const config_object* conf) { - return unbox(conf)->is_destroyed(); -} - -/// API: groups_info/groups_info_destroy_group(const config_object* conf); -/// -/// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -LIBSESSION_C_API void groups_info_destroy_group(config_object* conf) { - unbox(conf)->destroy_group(); +LIBSESSION_C_API void state_destroy_group(mutable_state_group_object* state) { + unbox(state).info.destroy_group(); } diff --git a/src/state.cpp b/src/state.cpp index 0812d70b..b25ec06e 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -14,6 +14,7 @@ #include "session/config/base.hpp" #include "session/config/contacts.hpp" #include "session/config/convo_info_volatile.hpp" +#include "session/config/groups/members.hpp" #include "session/config/namespaces.h" #include "session/config/namespaces.hpp" #include "session/config/user_groups.hpp" @@ -27,18 +28,12 @@ using namespace session::config; namespace session::state { -enum class RequestType : std::uint8_t { - ConfigPush = 2, -}; - -GroupConfigs::GroupConfigs(ustring_view pubkey, ustring_view user_sk) { - auto info = std::make_unique(pubkey, std::nullopt, std::nullopt); - auto members = std::make_unique(pubkey, std::nullopt, std::nullopt); - auto keys = std::make_unique( - user_sk, pubkey, std::nullopt, std::nullopt, *info, *members); - config_info = std::move(info); - config_members = std::move(members); - config_keys = std::move(keys); +GroupConfigs::GroupConfigs( + ustring_view pubkey, ustring_view user_sk, std::optional ed25519_secretkey) { + info = std::make_unique(pubkey, ed25519_secretkey, std::nullopt); + members = std::make_unique(pubkey, ed25519_secretkey, std::nullopt); + keys = std::make_unique( + user_sk, pubkey, ed25519_secretkey, std::nullopt, *info, *members); } State::State(ustring_view ed25519_secretkey, std::vector dumps) { @@ -70,25 +65,25 @@ State::State(ustring_view ed25519_secretkey, std::vector dumps) } // Initialise empty config states for any missing required config types - if (!config_contacts) { - config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); - add_child_logger(config_contacts); + if (!_config_contacts) { + _config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_contacts); } - if (!config_convo_info_volatile) { - config_convo_info_volatile = + if (!_config_convo_info_volatile) { + _config_convo_info_volatile = std::make_unique(ed25519_secretkey, std::nullopt); - add_child_logger(config_convo_info_volatile); + add_child_logger(_config_convo_info_volatile); } - if (!config_user_groups) { - config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); - add_child_logger(config_user_groups); + if (!_config_user_groups) { + _config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_user_groups); } - if (!config_user_profile) { - config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); - add_child_logger(config_user_profile); + if (!_config_user_profile) { + _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_user_profile); } } @@ -96,27 +91,27 @@ void State::load( Namespace namespace_, std::optional pubkey_hex_, ustring_view dump) { switch (namespace_) { case Namespace::Contacts: - config_contacts = + _config_contacts = std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); - add_child_logger(config_contacts); + add_child_logger(_config_contacts); return; case Namespace::ConvoInfoVolatile: - config_convo_info_volatile = std::make_unique( + _config_convo_info_volatile = std::make_unique( to_unsigned_sv({_user_sk.data(), 64}), dump); - add_child_logger(config_convo_info_volatile); + add_child_logger(_config_convo_info_volatile); return; case Namespace::UserGroups: - config_user_groups = + _config_user_groups = std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); - add_child_logger(config_user_groups); + add_child_logger(_config_user_groups); return; case Namespace::UserProfile: - config_user_profile = + _config_user_profile = std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); - add_child_logger(config_user_profile); + add_child_logger(_config_user_profile); return; default: break; @@ -131,7 +126,7 @@ void State::load( // Retrieve any keys for the group std::string_view pubkey_hex = *pubkey_hex_; - auto user_group_info = config_user_groups->get_group(pubkey_hex); + auto user_group_info = _config_user_groups->get_group(pubkey_hex); if (!user_group_info) throw std::runtime_error{ @@ -159,19 +154,19 @@ void State::load( // Reload the specified namespace with the dump if (namespace_ == Namespace::GroupInfo) { - _config_groups[pubkey_hex]->config_info = + _config_groups[pubkey_hex]->info = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[pubkey_hex]->config_info); + add_child_logger(_config_groups[pubkey_hex]->info); } else if (namespace_ == Namespace::GroupMembers) { - _config_groups[pubkey_hex]->config_members = + _config_groups[pubkey_hex]->members = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[pubkey_hex]->config_members); + add_child_logger(_config_groups[pubkey_hex]->members); } else if (namespace_ == Namespace::GroupKeys) { - auto info = _config_groups[pubkey_hex]->config_info.get(); - auto members = _config_groups[pubkey_hex]->config_members.get(); + auto info = _config_groups[pubkey_hex]->info.get(); + auto members = _config_groups[pubkey_hex]->members.get(); auto keys = std::make_unique( user_ed25519_secretkey, pubkey_sv, group_ed25519_secretkey, dump, *info, *members); - _config_groups[pubkey_hex]->config_keys = std::move(keys); + _config_groups[pubkey_hex]->keys = std::move(keys); } else throw std::runtime_error{"Attempted to load unknown namespace"}; } @@ -193,17 +188,17 @@ void State::config_changed( if (!is_group_pubkey) { needs_push = (allow_send && - (config_contacts->needs_push() || config_convo_info_volatile->needs_push() || - config_user_groups->needs_push() || config_user_profile->needs_push())); + (_config_contacts->needs_push() || _config_convo_info_volatile->needs_push() || + _config_user_groups->needs_push() || _config_user_profile->needs_push())); needs_dump = (allow_store && - (config_contacts->needs_dump() || config_convo_info_volatile->needs_dump() || - config_user_groups->needs_dump() || config_user_profile->needs_dump())); + (_config_contacts->needs_dump() || _config_convo_info_volatile->needs_dump() || + _config_user_groups->needs_dump() || _config_user_profile->needs_dump())); configs = { - config_contacts.get(), - config_convo_info_volatile.get(), - config_user_groups.get(), - config_user_profile.get()}; + _config_contacts.get(), + _config_convo_info_volatile.get(), + _config_user_groups.get(), + _config_user_profile.get()}; } else { // Other namespaces are unique for a given pubkey_hex_ if (target_pubkey_hex.size() != 66) @@ -214,7 +209,7 @@ void State::config_changed( target_pubkey_hex}; // Ensure we have the admin key for the group - auto user_group_info = config_user_groups->get_group(target_pubkey_hex); + auto user_group_info = _config_user_groups->get_group(target_pubkey_hex); if (!user_group_info) throw std::runtime_error{ @@ -224,16 +219,16 @@ void State::config_changed( // Only group admins can push group config changes needs_push = (allow_send && !user_group_info->secretkey.empty() && - (_config_groups[target_pubkey_hex]->config_info->needs_push() || - _config_groups[target_pubkey_hex]->config_members->needs_push() || - _config_groups[target_pubkey_hex]->config_keys->pending_config())); + (_config_groups[target_pubkey_hex]->info->needs_push() || + _config_groups[target_pubkey_hex]->members->needs_push() || + _config_groups[target_pubkey_hex]->keys->pending_config())); needs_dump = - (allow_store && (_config_groups[target_pubkey_hex]->config_info->needs_dump() || - _config_groups[target_pubkey_hex]->config_members->needs_dump() || - _config_groups[target_pubkey_hex]->config_keys->needs_dump())); + (allow_store && (_config_groups[target_pubkey_hex]->info->needs_dump() || + _config_groups[target_pubkey_hex]->members->needs_dump() || + _config_groups[target_pubkey_hex]->keys->needs_dump())); configs = { - _config_groups[target_pubkey_hex]->config_info.get(), - _config_groups[target_pubkey_hex]->config_members.get()}; + _config_groups[target_pubkey_hex]->info.get(), + _config_groups[target_pubkey_hex]->members.get()}; info_title = "Group configs for " + target_pubkey_hex; } @@ -258,10 +253,10 @@ void State::config_changed( } // GroupKeys needs special handling as it's not a `ConfigBase` - if (is_group_pubkey && _config_groups[target_pubkey_hex]->config_keys->needs_dump()) { + if (is_group_pubkey && _config_groups[target_pubkey_hex]->keys->needs_dump()) { log(LogLevel::debug, "config_changed: Group Keys config for " + target_pubkey_hex + " needs_dump"); - auto keys_config = _config_groups[target_pubkey_hex]->config_keys.get(); + auto keys_config = _config_groups[target_pubkey_hex]->keys.get(); _store(keys_config->storage_namespace(), target_pubkey_hex, @@ -272,28 +267,88 @@ void State::config_changed( // Call the hook to perform a push if needed if (_send && needs_push && allow_send) { - std::vector requests; - std::vector obsolete_hashes; + auto push = prepare_push(target_pubkey_hex, timestamp, configs); - for (auto& config : configs) { - if (!config->needs_push()) - continue; - log(LogLevel::debug, - "config_changed: generate 'send' request for " + - namespace_name(config->storage_namespace()) + ", (" + target_pubkey_hex + - ")"); - auto [seqno, msg, obs] = config->push(); + log(LogLevel::debug, "config_changed: Call 'send'"); + _send(target_pubkey_hex, + push.payload, + [this, target_pubkey_hex, push]( + bool success, uint16_t status_code, ustring response) { + handle_config_push_response( + target_pubkey_hex, push.namespace_seqno, success, status_code, response); + }); + } + log(LogLevel::debug, "config_changed: Complete"); +} - for (auto hash : obs) - obsolete_hashes.emplace_back(hash); +PreparedPush State::prepare_push( + std::string pubkey_hex, + std::chrono::milliseconds timestamp, + std::vector configs) { + auto is_group_pubkey = (!pubkey_hex.empty() && pubkey_hex.substr(0, 2) != "05"); + std::vector requests; + std::vector obsolete_hashes; + for (auto& config : configs) { + if (!config->needs_push()) + continue; + log(LogLevel::debug, + "prepare_push: generate push for " + namespace_name(config->storage_namespace()) + + ", (" + pubkey_hex + ")"); + auto [seqno, msg, obs] = config->push(); + + for (auto hash : obs) + obsolete_hashes.emplace_back(hash); + + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store"); + verification += + to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); + + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), _user_sk.data())) + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"namespace", static_cast(config->storage_namespace())}, + {"pubkey", pubkey_hex}, + {"ttl", config->default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(msg)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + + // For user config storage we also need to add `pubkey_ed25519` + if (!is_group_pubkey) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + // Add the 'seqno' temporarily to the params (this will be removed from the payload + // before sending but is needed to generate the request context) + params["seqno"] = seqno; + + requests.emplace_back(params); + } + + // GroupKeys needs special handling as it's not a `ConfigBase` + if (is_group_pubkey) { + auto config = _config_groups[pubkey_hex]->keys.get(); + auto pending = config->pending_config(); + + if (pending) { + log(LogLevel::debug, + "prepare_push: generate push for " + namespace_name(config->storage_namespace()) + + ", (" + pubkey_hex + ")"); // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; - ustring verification = to_unsigned("store"); - verification += - to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); - verification += to_unsigned_sv(std::to_string(timestamp.count())); + ustring verification = to_unsigned("store") + + static_cast(config->storage_namespace()) + + static_cast(timestamp.count()); if (0 != crypto_sign_ed25519_detached( sig.data(), @@ -305,132 +360,75 @@ void State::config_changed( "config_changed: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ - {"namespace", static_cast(config->storage_namespace())}, - {"pubkey", target_pubkey_hex}, + {"namespace", config->storage_namespace()}, + {"pubkey", pubkey_hex}, {"ttl", config->default_ttl().count()}, {"timestamp", timestamp.count()}, - {"data", oxenc::to_base64(msg)}, + {"data", oxenc::to_base64(*pending)}, {"signature", oxenc::to_base64(sig.begin(), sig.end())}, }; - // For user config storage we also need to add `pubkey_ed25519` - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") - params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); - - // Add the 'seqno' temporarily to the params (this will be removed from the payload + // The 'GROUP_KEYS' push data doesn't need a 'seqno', but to avoid index + // out-of-bounds issues we add one anyway (this will be removed from the payload // before sending but is needed to generate the request context) - params["seqno"] = seqno; + params["seqno"] = 0; requests.emplace_back(params); } + } - // GroupKeys needs special handling as it's not a `ConfigBase` - if (is_group_pubkey) { - auto config = _config_groups[target_pubkey_hex]->config_keys.get(); - auto pending = config->pending_config(); - - if (pending) { - log(LogLevel::debug, - "config_changed: generate 'send' request for " + - namespace_name(config->storage_namespace()) + ", (" + - target_pubkey_hex + ")"); - // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and - // `timestamp` are the base10 expression of the namespace and `timestamp` values - std::array sig; - ustring verification = to_unsigned("store") + - static_cast(config->storage_namespace()) + - static_cast(timestamp.count()); - - if (0 != crypto_sign_ed25519_detached( - sig.data(), - nullptr, - verification.data(), - verification.size(), - _user_sk.data())) - throw std::runtime_error{ - "config_changed: Failed to sign; perhaps the secret key is invalid?"}; - - nlohmann::json params{ - {"namespace", config->storage_namespace()}, - {"pubkey", target_pubkey_hex}, - {"ttl", config->default_ttl().count()}, - {"timestamp", timestamp.count()}, - {"data", oxenc::to_base64(*pending)}, - {"signature", oxenc::to_base64(sig.begin(), sig.end())}, - }; - - // The 'GROUP_KEYS' push data doesn't need a 'seqno', but to avoid index - // out-of-bounds issues we add one anyway (this will be removed from the payload - // before sending but is needed to generate the request context) - params["seqno"] = 0; - - requests.emplace_back(params); - } - } + // Sort the namespaces based on the order they should be stored in to minimise the chance + // that config messages dependant on others are stored before their dependencies + auto sorted_requests = requests; + std::sort(sorted_requests.begin(), sorted_requests.end(), [](const auto& a, const auto& b) { + return namespace_store_order(static_cast(a["namespace"])) < + namespace_store_order(static_cast(b["namespace"])); + }); - // Sort the namespaces based on the order they should be stored in to minimise the chance - // that config messages dependant on others are stored before their dependencies - auto sorted_requests = requests; - std::sort(sorted_requests.begin(), sorted_requests.end(), [](const auto& a, const auto& b) { - return namespace_store_order(static_cast(a["namespace"])) < - namespace_store_order(static_cast(b["namespace"])); - }); - - std::vector seqnos; - std::vector namespaces; - nlohmann::json sequence_params; - - for (auto& request : sorted_requests) { - seqnos.push_back(request["seqno"].get()); - namespaces.push_back(request["namespace"].get()); - request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload - - nlohmann::json request_json{{"method", "store"}, {"params", request}}; - sequence_params["requests"].push_back(request_json); - } + std::vector> namespace_seqnos; + nlohmann::json sequence_params; - // Also delete obsolete hashes - if (!obsolete_hashes.empty()) { - // Ed25519 signature of `("delete" || messages...)` - std::array sig; - ustring verification = to_unsigned("delete"); - log(LogLevel::debug, "config_changed: has obsolete hashes"); - for (auto& hash : obsolete_hashes) - verification += to_unsigned_sv(hash); + for (auto& request : sorted_requests) { + namespace_seqnos.push_back( + {request["namespace"].get(), request["seqno"].get()}); + request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload - if (0 != crypto_sign_ed25519_detached( - sig.data(), - nullptr, - verification.data(), - verification.size(), - _user_sk.data())) - throw std::runtime_error{ - "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + nlohmann::json request_json{{"method", "store"}, {"params", request}}; + sequence_params["requests"].push_back(request_json); + } - nlohmann::json params{ - {"messages", obsolete_hashes}, - {"pubkey", target_pubkey_hex}, - {"signature", oxenc::to_base64(sig.begin(), sig.end())}, - }; + // Also delete obsolete hashes + if (!obsolete_hashes.empty()) { + // Ed25519 signature of `("delete" || messages...)` + std::array sig; + ustring verification = to_unsigned("delete"); + log(LogLevel::debug, "config_changed: has obsolete hashes"); + for (auto& hash : obsolete_hashes) + verification += to_unsigned_sv(hash); + + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), _user_sk.data())) + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; - // For user config storage we also need to add `pubkey_ed25519` - if (!pubkey_hex || pubkey_hex->substr(0, 2) == "05") - params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + nlohmann::json params{ + {"messages", obsolete_hashes}, + {"pubkey", pubkey_hex}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; - nlohmann::json request_json{{"method", "delete"}, {"params", params}}; - sequence_params["requests"].push_back(request_json); - } - log(LogLevel::debug, "config_changed: Call 'send'"); - nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; - nlohmann::json ctx{ - {"type", RequestType::ConfigPush}, - {"pubkey", target_pubkey_hex}, - {"seqnos", seqnos}, - {"namespaces", namespaces}}; - - _send(target_pubkey_hex, to_unsigned(payload.dump()), to_unsigned(ctx.dump())); + // For user config storage we also need to add `pubkey_ed25519` + if (!is_group_pubkey) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + nlohmann::json request_json{{"method", "delete"}, {"params", params}}; + sequence_params["requests"].push_back(request_json); } - log(LogLevel::debug, "config_changed: Complete"); + + nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; + + return {to_unsigned(payload.dump()), namespace_seqnos}; } std::vector State::merge( @@ -478,25 +476,25 @@ std::vector State::merge( std::vector merged_hashes; switch (config.namespace_) { case Namespace::Contacts: - merged_hashes = config_contacts->merge(pending_configs); + merged_hashes = _config_contacts->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::ConvoInfoVolatile: - merged_hashes = config_convo_info_volatile->merge(pending_configs); + merged_hashes = _config_convo_info_volatile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::UserGroups: - merged_hashes = config_user_groups->merge(pending_configs); + merged_hashes = _config_user_groups->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; case Namespace::UserProfile: - merged_hashes = config_user_profile->merge(pending_configs); + merged_hashes = _config_user_profile->merge(pending_configs); good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); config_changed(target_pubkey_hex, true, false); // Immediately store changes continue; @@ -515,8 +513,8 @@ std::vector State::merge( "merge: Attempted to merge group configs before for group with no config " "state"}; - auto info = _config_groups[target_pubkey_hex]->config_info.get(); - auto members = _config_groups[target_pubkey_hex]->config_members.get(); + auto info = _config_groups[target_pubkey_hex]->info.get(); + auto members = _config_groups[target_pubkey_hex]->members.get(); is_group_merge = true; if (config.namespace_ == Namespace::GroupInfo) @@ -525,7 +523,7 @@ std::vector State::merge( merged_hashes = members->merge(pending_configs); else if (config.namespace_ == Namespace::GroupKeys) { // GroupKeys doesn't support merging multiple messages at once so do them individually - if (_config_groups[target_pubkey_hex]->config_keys->load_key_message( + if (_config_groups[target_pubkey_hex]->keys->load_key_message( config.hash, config.data, config.timestamp_ms, *info, *members)) { good_hashes.emplace_back(config.hash); } @@ -547,10 +545,10 @@ std::vector State::current_hashes(std::optional p std::vector result; if (!pubkey_hex || pubkey_hex->empty() || pubkey_hex->substr(0, 2) == "05") { - auto contact_hashes = config_contacts->current_hashes(); - auto convo_info_volatile_hashes = config_convo_info_volatile->current_hashes(); - auto user_group_hashes = config_user_groups->current_hashes(); - auto user_profile_hashes = config_user_profile->current_hashes(); + auto contact_hashes = _config_contacts->current_hashes(); + auto convo_info_volatile_hashes = _config_convo_info_volatile->current_hashes(); + auto user_group_hashes = _config_user_groups->current_hashes(); + auto user_profile_hashes = _config_user_profile->current_hashes(); result.insert(result.end(), contact_hashes.begin(), contact_hashes.end()); result.insert( result.end(), convo_info_volatile_hashes.begin(), convo_info_volatile_hashes.end()); @@ -564,9 +562,9 @@ std::vector State::current_hashes(std::optional p "current_hashes: Attempted to retrieve current hashes for group with no config " "state"}; - auto info_hashes = _config_groups[*pubkey_hex]->config_info->current_hashes(); - auto members_hashes = _config_groups[*pubkey_hex]->config_members->current_hashes(); - auto keys_hashes = _config_groups[*pubkey_hex]->config_keys->current_hashes(); + auto info_hashes = _config_groups[*pubkey_hex]->info->current_hashes(); + auto members_hashes = _config_groups[*pubkey_hex]->members->current_hashes(); + auto keys_hashes = _config_groups[*pubkey_hex]->keys->current_hashes(); result.insert(result.end(), info_hashes.begin(), info_hashes.end()); result.insert(result.end(), members_hashes.begin(), members_hashes.end()); result.insert(result.end(), keys_hashes.begin(), keys_hashes.end()); @@ -579,38 +577,36 @@ ustring State::dump(bool full_dump) { oxenc::bt_dict_producer combined; // NOTE: the keys have to be in ascii-sorted order: - if (full_dump || config_contacts->needs_dump()) - combined.append("contacts", session::from_unsigned_sv(config_contacts->dump())); + if (full_dump || _config_contacts->needs_dump()) + combined.append("contacts", session::from_unsigned_sv(_config_contacts->dump())); - if (full_dump || config_convo_info_volatile->needs_dump()) + if (full_dump || _config_convo_info_volatile->needs_dump()) combined.append( "convo_info_volatile", - session::from_unsigned_sv(config_convo_info_volatile->dump())); + session::from_unsigned_sv(_config_convo_info_volatile->dump())); - if (full_dump || config_user_groups->needs_dump()) - combined.append("user_groups", session::from_unsigned_sv(config_user_groups->dump())); + if (full_dump || _config_user_groups->needs_dump()) + combined.append("user_groups", session::from_unsigned_sv(_config_user_groups->dump())); - if (full_dump || config_user_profile->needs_dump()) - combined.append("user_profile", session::from_unsigned_sv(config_user_profile->dump())); + if (full_dump || _config_user_profile->needs_dump()) + combined.append("user_profile", session::from_unsigned_sv(_config_user_profile->dump())); // NOTE: `std::map` sorts keys in ascending order so can just add them in order if (_config_groups.size() > 0) { for (const auto& [key, config] : _config_groups) { - if (full_dump || config->config_info->needs_dump() || - config->config_keys->needs_dump() || config->config_members->needs_dump()) { + if (full_dump || config->info->needs_dump() || config->keys->needs_dump() || + config->members->needs_dump()) { oxenc::bt_dict_producer group_combined = combined.append_dict(key); - if (full_dump || config->config_info->needs_dump()) - group_combined.append( - "info", session::from_unsigned_sv(config->config_info->dump())); + if (full_dump || config->info->needs_dump()) + group_combined.append("info", session::from_unsigned_sv(config->info->dump())); - if (full_dump || config->config_keys->needs_dump()) - group_combined.append( - "keys", session::from_unsigned_sv(config->config_keys->dump())); + if (full_dump || config->keys->needs_dump()) + group_combined.append("keys", session::from_unsigned_sv(config->keys->dump())); - if (full_dump || config->config_members->needs_dump()) + if (full_dump || config->members->needs_dump()) group_combined.append( - "members", session::from_unsigned_sv(config->config_members->dump())); + "members", session::from_unsigned_sv(config->members->dump())); } } } @@ -622,10 +618,10 @@ ustring State::dump(bool full_dump) { ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex_) { switch (namespace_) { - case Namespace::Contacts: return config_contacts->dump(); - case Namespace::ConvoInfoVolatile: return config_convo_info_volatile->dump(); - case Namespace::UserGroups: return config_user_groups->dump(); - case Namespace::UserProfile: return config_user_profile->dump(); + case Namespace::Contacts: return _config_contacts->dump(); + case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->dump(); + case Namespace::UserGroups: return _config_user_groups->dump(); + case Namespace::UserProfile: return _config_user_profile->dump(); default: break; } @@ -642,41 +638,29 @@ ustring State::dump(config::Namespace namespace_, std::optionalconfig_info->dump(); - case Namespace::GroupMembers: return group_configs->config_members->dump(); - case Namespace::GroupKeys: return group_configs->config_keys->dump(); + case Namespace::GroupInfo: return group_configs->info->dump(); + case Namespace::GroupMembers: return group_configs->members->dump(); + case Namespace::GroupKeys: return group_configs->keys->dump(); default: throw std::runtime_error{"Attempted to load unknown namespace"}; } } -void State::received_send_response(std::string pubkey, ustring response, ustring ctx) { - auto ctx_json = nlohmann::json::parse(ctx); +void State::handle_config_push_response( + std::string pubkey, + std::vector> namespace_seqnos, + bool success, + uint16_t status_code, + ustring response) { + std::string response_string = {from_unsigned(response.data()), response.size()}; - if (pubkey.size() != 66) + // If the request failed then just error + if (!success || (status_code < 200 && status_code > 299)) throw std::invalid_argument{ - "received_send_response: Invalid pubkey - expected 66 characters"}; - if (!ctx_json.contains("type")) - throw std::invalid_argument{ - "received_send_response: Invalid ctx - expected to contain 'type'"}; - - auto request_type = static_cast(ctx_json["type"].get()); - - switch (request_type) { - case RequestType::ConfigPush: handle_config_push_response(pubkey, response, ctx); break; - default: - throw std::invalid_argument{ - "received_send_response: Unrecognised ctx.type '" + - std::to_string(static_cast(request_type)) + "'"}; - } -} + "handle_config_push_response: Request failed with data - " + response_string}; -void State::handle_config_push_response(std::string pubkey, ustring response, ustring ctx) { - log(LogLevel::debug, "handle_config_push_response: Called"); + // Otherwise process the response data auto response_json = nlohmann::json::parse(response); - if (pubkey.size() != 66) - throw std::invalid_argument{ - "handle_config_push_response: Invalid pubkey - expected 66 characters"}; if (!response_json.contains("results")) throw std::invalid_argument{ "handle_config_push_response: Invalid response - expected to contain 'results' " @@ -738,13 +722,7 @@ void State::handle_config_push_response(std::string pubkey, ustring response, us std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch())); - // The 'results' array will be in the same order as the requests sent within 'payload' so - // iterate through them both and mark any successful request as pushed - auto ctx_json = nlohmann::json::parse(ctx); - - if (!ctx_json.contains("seqnos") || !ctx_json.contains("namespaces") || - results.size() < ctx_json["seqnos"].size() || - results.size() < ctx_json["namespaces"].size()) + if (results.size() < namespace_seqnos.size()) throw std::invalid_argument{ "handle_config_push_response: Invalid response - Number of responses doesn't match " "the number of requests."}; @@ -757,16 +735,18 @@ void State::handle_config_push_response(std::string pubkey, ustring response, us continue; auto hash = results[i]["body"]["hash"].get(); - auto seqno = ctx_json["seqnos"][i].get(); - auto namespace_ = ctx_json["namespaces"][i].get(); + auto seqno = namespace_seqnos[i].second; + auto namespace_ = namespace_seqnos[i].first; switch (namespace_) { - case Namespace::Contacts: config_contacts->confirm_pushed(seqno, hash); continue; + case Namespace::Contacts: _config_contacts->confirm_pushed(seqno, hash); continue; case Namespace::ConvoInfoVolatile: - config_convo_info_volatile->confirm_pushed(seqno, hash); + _config_convo_info_volatile->confirm_pushed(seqno, hash); + continue; + case Namespace::UserGroups: _config_user_groups->confirm_pushed(seqno, hash); continue; + case Namespace::UserProfile: + _config_user_profile->confirm_pushed(seqno, hash); continue; - case Namespace::UserGroups: config_user_groups->confirm_pushed(seqno, hash); continue; - case Namespace::UserProfile: config_user_profile->confirm_pushed(seqno, hash); continue; default: break; } @@ -778,9 +758,8 @@ void State::handle_config_push_response(std::string pubkey, ustring response, us auto group_configs = _config_groups[pubkey].get(); switch (namespace_) { - case Namespace::GroupInfo: group_configs->config_info->confirm_pushed(seqno, hash); - case Namespace::GroupMembers: - group_configs->config_members->confirm_pushed(seqno, hash); + case Namespace::GroupInfo: group_configs->info->confirm_pushed(seqno, hash); + case Namespace::GroupMembers: group_configs->members->confirm_pushed(seqno, hash); case Namespace::GroupKeys: continue; // No need to do anything here default: throw std::runtime_error{ @@ -797,10 +776,10 @@ void State::handle_config_push_response(std::string pubkey, ustring response, us std::vector State::get_keys( Namespace namespace_, std::optional pubkey_hex_) { switch (namespace_) { - case Namespace::Contacts: return config_contacts->get_keys(); - case Namespace::ConvoInfoVolatile: return config_convo_info_volatile->get_keys(); - case Namespace::UserGroups: return config_user_groups->get_keys(); - case Namespace::UserProfile: return config_user_profile->get_keys(); + case Namespace::Contacts: return _config_contacts->get_keys(); + case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->get_keys(); + case Namespace::UserGroups: return _config_user_groups->get_keys(); + case Namespace::UserProfile: return _config_user_profile->get_keys(); default: break; } @@ -817,13 +796,144 @@ std::vector State::get_keys( auto group_configs = _config_groups[*pubkey_hex_].get(); switch (namespace_) { - case Namespace::GroupInfo: return group_configs->config_info->get_keys(); - case Namespace::GroupMembers: return group_configs->config_members->get_keys(); - case Namespace::GroupKeys: return group_configs->config_keys->group_keys(); + case Namespace::GroupInfo: return group_configs->info->get_keys(); + case Namespace::GroupMembers: return group_configs->members->get_keys(); + case Namespace::GroupKeys: return group_configs->keys->group_keys(); default: throw std::runtime_error{"Attempted to load unknown namespace"}; } } +void State::create_group( + std::string_view name, + std::optional description, + std::optional pic, + std::vector members_, + std::function + callback) { + auto key_pair = ed25519::ed25519_key_pair(); + auto group_id = "03" + oxenc::to_hex(key_pair.first.begin(), key_pair.first.end()); + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + // Sanity check to avoid group collision + if (_config_groups.count(group_id)) + throw std::runtime_error{"create_group: Tried to create group matching an existing group"}; + + ustring_view ed_pk = to_unsigned_sv(key_pair.first); + ustring_view ed_sk = to_unsigned_sv(key_pair.second); + _config_groups[group_id] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk)); + + // Store the group info + _config_groups[group_id]->info = std::make_unique(ed_pk, ed_sk, std::nullopt); + _config_groups[group_id]->info->set_name(name); + _config_groups[group_id]->info->set_created(timestamp.count()); + + if (description) + _config_groups[group_id]->info->set_description(*description); + + if (pic) + _config_groups[group_id]->info->set_profile_pic(*pic); + + // Need to load the members before creating the Keys config to ensure they + // are included in the initial key rotation + _config_groups[group_id]->members = + std::make_unique(ed_pk, ed_sk, std::nullopt); + + // Insert the current user as a group admin + auto admin_member = groups::member{_user_x_pk_hex}; + admin_member.admin = true; + admin_member.profile_picture = _config_user_profile->get_profile_pic(); + + if (auto name = _config_user_profile->get_name()) + admin_member.name = *name; + + _config_groups[group_id]->members->set(admin_member); + + // Add other members (ignore the current user if they happen to be included) + for (auto m : members_) + if (m.session_id != _user_x_pk_hex) + _config_groups[group_id]->members->set(m); + + // Finally create the keys + auto info = _config_groups[group_id]->info.get(); + auto members = _config_groups[group_id]->members.get(); + _config_groups[group_id]->keys = std::make_unique( + to_unsigned_sv(_user_sk), ed_pk, ed_sk, std::nullopt, *info, *members); + + // Prepare and trigger the push for the group configs + std::vector configs = { + _config_groups[group_id]->info.get(), _config_groups[group_id]->members.get()}; + auto push = prepare_push(group_id, timestamp, configs); + _send(group_id, + push.payload, + [this, group_id, push, ed_sk, name, timestamp, callback]( + bool success, int16_t status_code, ustring response) { + // Call through to the default 'handle_config_push_response' first to update it's + // state correctly (this will also result in the configs getting stored to disk) + handle_config_push_response( + group_id, push.namespace_seqno, success, status_code, response); + + // Double check that the group state still exists + if (!_config_groups.count(group_id)) { + log(LogLevel::error, + "create_group: Unable to retrieve group when processing create response"); + callback(false, "", to_unsigned_sv("")); + return; + } + + try { + // Retrieve the group configs for this pubkey and setup an entry in the user + // groups config for it + auto group_configs = _config_groups[group_id].get(); + auto group = _config_user_groups->get_or_construct_group(group_id); + group.name = name; + group.joined_at = timestamp.count(); + group.secretkey = ed_sk; + _config_user_groups->set(group); + + // Manually trigger 'config_changed' because we modified '_config_user_groups' + // directly rather than via the 'MutableUserConfigs' so it won't automatically get + // triggered + config_changed(); + + // Lastly trigger the 'callback' to communicate the group was successfully created + callback(true, group_id, ed_sk); + } catch (...) { + callback(false, "", to_unsigned_sv("")); + } + }); +} + +void State::approve_group(std::string_view group_id, std::optional group_sk) { + // If we don't already have GroupConfigs then create them + if (!_config_groups[group_id]) { + auto ed_pk = to_unsigned_sv(oxenc::from_hex(group_id.begin() + 2, group_id.end())); + _config_groups[group_id] = + std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); + _config_groups[group_id]->info = + std::make_unique(ed_pk, group_sk, std::nullopt); + _config_groups[group_id]->members = + std::make_unique(ed_pk, group_sk, std::nullopt); + + auto info = _config_groups[group_id]->info.get(); + auto members = _config_groups[group_id]->members.get(); + _config_groups[group_id]->keys = std::make_unique( + to_unsigned_sv(_user_sk), ed_pk, group_sk, std::nullopt, *info, *members); + } + + // Update the USER_GROUPS config to have the group marked as approved + auto group = _config_user_groups->get_or_construct_group(group_id); + group.invited = false; + _config_user_groups->set(group); + + // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(); +} + void State::validate_group_pubkey(std::string_view pubkey_hex) const { if (pubkey_hex.size() != 66) throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; @@ -850,22 +960,22 @@ const ConfigType& State::config(std::string_view pubkey_hex) const { template <> const Contacts& State::config() const { - return *config_contacts; + return *_config_contacts; } template <> const ConvoInfoVolatile& State::config() const { - return *config_convo_info_volatile; + return *_config_convo_info_volatile; }; template <> const UserGroups& State::config() const { - return *config_user_groups; + return *_config_user_groups; }; template <> const UserProfile& State::config() const { - return *config_user_profile; + return *_config_user_profile; }; template <> @@ -873,7 +983,7 @@ const groups::Info& State::config(std::string_view pubkey_hex) const { validate_group_pubkey(pubkey_hex); if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->config_info; + return *it->second->info; throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; }; @@ -883,7 +993,7 @@ const groups::Members& State::config(std::string_view pubkey_hex) const { validate_group_pubkey(pubkey_hex); if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->config_members; + return *it->second->members; throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; }; @@ -891,19 +1001,19 @@ const groups::Members& State::config(std::string_view pubkey_hex) const { template <> const groups::Keys& State::config(std::string_view pubkey_hex) const { if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->config_keys; + return *it->second->keys; throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; }; -MutableUserConfigs State::mutableConfig( +MutableUserConfigs State::mutable_config( std::optional> set_error) { return MutableUserConfigs( this, - *config_contacts, - *config_convo_info_volatile, - *config_user_groups, - *config_user_profile, + *_config_contacts, + *_config_convo_info_volatile, + *_config_user_groups, + *_config_user_profile, set_error); }; @@ -911,21 +1021,20 @@ MutableUserConfigs::~MutableUserConfigs() { parent_state->config_changed(); }; -MutableGroupConfigs State::mutableConfig( +MutableGroupConfigs State::mutable_config( std::string_view pubkey_hex, std::optional> set_error) { validate_group_pubkey(pubkey_hex); return MutableGroupConfigs( this, - *_config_groups[pubkey_hex]->config_info, - *_config_groups[pubkey_hex]->config_members, - *_config_groups[pubkey_hex]->config_keys, + *_config_groups[pubkey_hex]->info, + *_config_groups[pubkey_hex]->members, + *_config_groups[pubkey_hex]->keys, set_error); }; MutableGroupConfigs::~MutableGroupConfigs() { - if (auto sign_pk = info.get_sig_pubkey()) - parent_state->config_changed("03" + oxenc::to_hex(sign_pk->begin(), sign_pk->end())); + parent_state->config_changed(info.id); }; } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 3f84ec3e..37e9955b 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -13,6 +13,7 @@ #include "session/config/contacts.h" #include "session/config/contacts.hpp" #include "session/config/convo_info_volatile.hpp" +#include "session/config/groups/members.h" #include "session/config/namespaces.h" #include "session/config/namespaces.hpp" #include "session/config/user_groups.hpp" @@ -43,28 +44,6 @@ LIBSESSION_EXPORT void state_free(state_object* state) { delete state; } -LIBSESSION_C_API bool state_create(state_object** state, char* error) { - try { - auto s = std::make_unique(); - auto s_object = std::make_unique(); - - s_object->internals = s.release(); - s_object->last_error = nullptr; - *state = s_object.release(); - return true; - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return false; - } catch (...) { - return false; - } -} - LIBSESSION_C_API bool state_init( state_object** state, const unsigned char* ed25519_secretkey_bytes, @@ -96,13 +75,7 @@ LIBSESSION_C_API bool state_init( *state = s_object.release(); return true; } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return false; + return set_error_value(error, e.what()); } } @@ -140,26 +113,60 @@ LIBSESSION_C_API void state_set_logger( } } +using response_callback_t = + std::function; + LIBSESSION_C_API bool state_set_send_callback( state_object* state, void (*callback)( - const char*, const unsigned char*, size_t, const unsigned char*, size_t, void*), - void* ctx) { + const char* pubkey, + const unsigned char* data, + size_t data_len, + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context), + void* app_ctx) { try { if (!callback) - unbox(state).onSend(nullptr); + unbox(state).on_send(nullptr); else { - // Setting this can result in the callback being immediately triggered which could throw - unbox(state).onSend( - [callback, ctx](std::string pubkey, ustring data, ustring request_ctx) { - callback( - pubkey.c_str(), - data.data(), - data.size(), - request_ctx.data(), - request_ctx.size(), - ctx); - }); + unbox(state).on_send([callback, app_ctx]( + std::string pubkey, + ustring data, + response_callback_t received_response) { + // We leak ownership of this std::function below in the `.release()` call, then we + // recapture it inside the inner response callback below. + auto on_response = + std::make_unique(std::move(received_response)); + + callback( + pubkey.c_str(), + data.data(), + data.size(), + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so that + // we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + app_ctx, + on_response.release()); + }); } return true; @@ -168,16 +175,32 @@ LIBSESSION_C_API bool state_set_send_callback( } } +LIBSESSION_C_API bool state_received_send_response( + state_object* state, + const state_send_response* callback, + const unsigned char* response, + const size_t size) { + try { + assert(callback && callback->internals); + auto received_response = + *static_cast*>(callback->internals); + received_response({response, size}); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + LIBSESSION_C_API bool state_set_store_callback( state_object* state, void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), void* ctx) { try { if (!callback) - unbox(state).onStore(nullptr); + unbox(state).on_store(nullptr); else { // Setting this can result in the callback being immediately triggered which could throw - unbox(state).onStore([callback, ctx]( + unbox(state).on_store([callback, ctx]( config::Namespace namespace_, std::string pubkey, uint64_t timestamp_ms, @@ -202,7 +225,7 @@ LIBSESSION_C_API void state_set_service_node_offset(state_object* state, int64_t unbox(state).network_offset = std::chrono::milliseconds(offset_ms); } -LIBSESSION_C_API int64_t state_network_offset(state_object* state) { +LIBSESSION_C_API int64_t state_network_offset(const state_object* state) { return unbox(state).network_offset.count(); } @@ -251,6 +274,37 @@ LIBSESSION_C_API bool state_current_hashes( } } +LIBSESSION_C_API seqno_t +state_current_seqno(state_object* state, const char* pubkey_hex_, NAMESPACE namespace_) { + switch (namespace_) { + case NAMESPACE_CONTACTS: return unbox(state).config().get_seqno(); + case NAMESPACE_CONVO_INFO_VOLATILE: + return unbox(state).config().get_seqno(); + case NAMESPACE_USER_GROUPS: return unbox(state).config().get_seqno(); + case NAMESPACE_USER_PROFILE: return unbox(state).config().get_seqno(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + return -1; + + try { + std::string_view pubkey_hex = {pubkey_hex_, 66}; + + switch (namespace_) { + case NAMESPACE_GROUP_INFO: + return unbox(state).config({pubkey_hex_, 66}).get_seqno(); + case NAMESPACE_GROUP_MEMBERS: + return unbox(state).config({pubkey_hex_, 66}).get_seqno(); + case NAMESPACE_GROUP_KEYS: return 0; // No seqno needed for GROUP_KEYS + default: return -1; + } + } catch (...) { + return -1; + } +} + LIBSESSION_C_API bool state_dump( state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { try { @@ -289,24 +343,6 @@ LIBSESSION_C_API bool state_dump_namespace( } } -LIBSESSION_C_API bool state_received_send_response( - state_object* state, - const char* pubkey_hex, - unsigned char* response_data, - size_t response_data_len, - unsigned char* request_ctx, - size_t request_ctx_len) { - try { - unbox(state).received_send_response( - {pubkey_hex, 66}, - {response_data, response_data_len}, - {request_ctx, request_ctx_len}); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} - LIBSESSION_C_API bool state_get_keys( state_object* state, NAMESPACE namespace_, @@ -329,11 +365,62 @@ LIBSESSION_C_API bool state_get_keys( } } +LIBSESSION_C_API void state_create_group( + state_object* state, + const char* name, + const char* description, + const user_profile_pic pic_, + const config_group_member* members_, + const size_t members_len, + void (*callback)( + bool success, const char* group_id, unsigned const char* group_sk, void* ctx), + void* ctx) { + try { + std::string_view url{pic_.url}; + ustring_view key; + if (!url.empty()) + key = {pic_.key, 32}; + + std::optional pic = profile_pic{url, key}; + std::vector members = {}; + members.reserve(members_len); + + for (size_t i = 0; i < members_len; i++) { + members.emplace_back(groups::member{members_[i]}); + } + + unbox(state).create_group( + name, + description, + pic, + members, + [callback, ctx](bool success, std::string_view group_id, ustring_view group_sk) { + callback(success, group_id.data(), group_sk.data(), ctx); + }); + } catch (const std::exception& e) { + set_error(state, e.what()); + callback(false, nullptr, nullptr, ctx); + } +} + +LIBSESSION_EXPORT void state_approve_group( + state_object* state, const char* group_id, unsigned const char* group_sk) { + try { + std::optional ed_sk; + if (group_sk) + ed_sk = {group_sk, 64}; + + unbox(state).approve_group({group_id, 66}, ed_sk); + } catch (const std::exception& e) { + set_error(state, e.what()); + } +} + LIBSESSION_C_API bool state_mutate_user( state_object* state, void (*callback)(mutable_state_user_object*, void*), void* ctx) { try { auto s_object = new mutable_state_user_object(); - auto mutable_state = unbox(state).mutableConfig([state](std::string_view e) { + auto mutable_state = unbox(state).mutable_config([state](std::string_view e) { // Don't override an existing error if (state->last_error) return; @@ -356,7 +443,7 @@ LIBSESSION_C_API bool state_mutate_group( try { auto s_object = new mutable_state_group_object(); auto mutable_state = - unbox(state).mutableConfig({pubkey_hex, 66}, [state](std::string_view e) { + unbox(state).mutable_config({pubkey_hex, 66}, [state](std::string_view e) { // Don't override an existing error if (state->last_error) return; diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index e3af8c96..edddc8cb 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -293,11 +293,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { CHECK((*last_send).pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" "f46"); - - auto ctx_json = nlohmann::json::parse(last_send->ctx); - - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 1); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONTACTS) == 1); state_object* state2; REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); @@ -305,7 +301,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->data); + auto last_send_json = nlohmann::json::parse(last_send->payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -325,13 +321,8 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); contacts_contact c3; REQUIRE(state_get_contact(state2, &c3, definitely_real_id, nullptr)); @@ -361,7 +352,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { }, &c4); - auto last_send_json_2 = nlohmann::json::parse(last_send_2->data); + auto last_send_json_2 = nlohmann::json::parse(last_send_2->payload); REQUIRE(last_send_json_2.contains(first_request_data)); auto last_send_data_2 = to_unsigned( oxenc::from_base64(last_send_json_2[first_request_data].get())); @@ -379,13 +370,8 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { free(merge_data); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); - CHECK(state_received_send_response( - state2, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send_2->response_cb( + true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); REQUIRE(last_send_json_2.contains(messages_key)); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 486f0917..c047ac83 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -342,20 +342,13 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(session::state::unbox(state).config().needs_push()); CHECK(session::state::unbox(state).config().needs_dump()); - auto ctx_json = nlohmann::json::parse(last_send->ctx); - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 1); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); // Pretend we uploaded it ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash1\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); CHECK_FALSE(session::state::unbox(state).config().needs_dump()); @@ -414,12 +407,10 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", nullptr)); CHECK(session::state::unbox(state2).config().needs_push()); - ctx_json = nlohmann::json::parse(last_send_2->ctx); - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 2); + CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send_2->data); + auto last_send_json = nlohmann::json::parse(last_send_2->payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -437,15 +428,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { free(accepted); free(merge_data); - ctx_json = nlohmann::json::parse(last_send_2->ctx); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash123\"}}]}"); - CHECK(state_received_send_response( - state2, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send_2->ctx.data(), - last_send_2->ctx.size())); + last_send_2->response_cb( + true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); std::vector seen; @@ -689,18 +674,11 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load &c); // Fake push: - auto ctx_json = nlohmann::json::parse(last_send->ctx); - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 1); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"somehash\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); // Load the dump: state_namespaced_dump* dumps = new state_namespaced_dump[1]; @@ -732,17 +710,10 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load }, &c); - ctx_json = nlohmann::json::parse(last_send->ctx); - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 2); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash5235\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); // But *before* we load the push make a dirtying change to conf2 that we *don't* push (so that // we'll be merging into a dirty-state config): @@ -764,7 +735,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load // And now, *before* we push the dirty config, also merge the incoming push from `state`: auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->data); + auto last_send_json = nlohmann::json::parse(last_send->payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -803,17 +774,10 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load &c1); CHECK(session::state::unbox(state2).config().needs_push()); - ctx_json = nlohmann::json::parse(last_send_2->ctx); - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 4); + CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 4); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hashz\"}}]}"); - CHECK(state_received_send_response( - state2, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send_2->ctx.data(), - last_send_2->ctx.size())); + last_send_2->response_cb( + true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); CHECK_FALSE(session::state::unbox(state2).config().needs_push()); CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 52db555b..3c5b5247 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -697,20 +697,11 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3" "a72"); - auto ctx_json = nlohmann::json::parse(last_send->ctx); - - REQUIRE(ctx_json.contains("seqnos")); - CHECK(ctx_json["seqnos"][0] == 1); - + CHECK(state_current_seqno(state, nullptr, NAMESPACE_USER_GROUPS) == 1); ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - CHECK(state_received_send_response( - state, - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response.data(), - send_response.size(), - last_send->ctx.data(), - last_send->ctx.size())); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); REQUIRE(state_current_hashes(state, nullptr, &hashes)); REQUIRE(hashes); @@ -730,7 +721,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->data); + auto last_send_json = nlohmann::json::parse(last_send->payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 9d2f24b5..7baa8f76 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -18,6 +18,8 @@ using namespace session::state; using namespace session::config; static constexpr int64_t created_ts = 1680064059; +using response_callback_t = + std::function; std::string replace_suffix_between( std::string_view value, @@ -40,20 +42,45 @@ TEST_CASE("State", "[state][state]") { std::optional last_store = std::nullopt; std::optional last_send = std::nullopt; - state.onStore([&last_store]( + state.on_store([&last_store]( config::Namespace namespace_, std::string pubkey, uint64_t timestamp_ms, ustring data) { last_store = {namespace_, pubkey, timestamp_ms, data}; }); - state.onSend([&last_send](std::string pubkey, ustring data, ustring ctx) { - last_send = {pubkey, data, ctx}; - }); + state.on_send( + [&last_send]( + std::string pubkey, ustring payload, response_callback_t received_response) { + // Replicate the behaviour in the C wrapper + auto on_response = + std::make_unique(std::move(received_response)); + last_send = { + pubkey, + payload, + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so that + // we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + nullptr, + on_response.release()}; + }); // Sanity check direct config access CHECK_FALSE(state.config().get_name().has_value()); - state.mutableConfig().user_profile.set_name("Test Name"); + state.mutable_config().user_profile.set_name("Test Name"); CHECK(state.config().get_name() == "Test Name"); CHECK(last_store->namespace_ == Namespace::UserProfile); CHECK(last_store->pubkey == @@ -65,7 +92,7 @@ TEST_CASE("State", "[state][state]") { CHECK(last_send->pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" "6"); - auto send_data_no_ts = replace_suffix_between(to_sv(last_send->data), (13 + 22), 22, "0"); + auto send_data_no_ts = replace_suffix_between(to_sv(last_send->payload), (13 + 22), 22, "0"); auto send_data_no_sig = replace_suffix_between(send_data_no_ts, (37 + 88), 37, "sig"); CHECK(send_data_no_sig == "{\"method\":\"sequence\",\"params\":{\"requests\":[{\"method\":\"store\",\"params\":{" @@ -84,18 +111,13 @@ TEST_CASE("State", "[state][state]") { "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"pubkey_" "ed25519\":\"8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f\"," "\"signature\":\"sig\",\"timestamp\":0,\"ttl\":2592000000}}]}}"); - CHECK(to_sv(last_send->ctx) == - "{\"namespaces\":[2],\"pubkey\":" - "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"seqnos\":[1]," - "\"type\":2}"); + CHECK(state.config().get_seqno() == 1); // Confirm the push ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - state.received_send_response( - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46", - send_response, - last_send->ctx); + last_send->response_cb( + true, 200, send_response.data(), send_response.size(), last_send->callback_context); // Init with dumps auto dump = state.dump(Namespace::UserProfile); diff --git a/tests/utils.hpp b/tests/utils.hpp index 63518ef4..0de22dbc 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -99,8 +99,15 @@ struct last_store_data { }; struct last_send_data { std::string pubkey; - ustring data; - ustring ctx; + ustring payload; + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context); + void* app_ctx; + void* callback_context; }; inline void c_store_callback( @@ -121,9 +128,14 @@ inline void c_send_callback( const char* pubkey, const unsigned char* data, size_t data_len, - const unsigned char* request_ctx, - size_t request_ctx_len, - void* ctx) { - *static_cast*>(ctx) = - last_send_data{{pubkey, 66}, {data, data_len}, {request_ctx, request_ctx_len}}; + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context) { + *static_cast*>(app_ctx) = + last_send_data{{pubkey, 66}, {data, data_len}, response_cb, app_ctx, callback_context}; } From 1e353707155c92cd48e96bd9f0e3f1012dd80abe Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 15 Feb 2024 17:48:12 +1100 Subject: [PATCH 09/24] Updated the GroupMembers C API to be state based --- include/session/config/groups/members.h | 104 +++++++++++------------- include/session/state.h | 15 ---- include/session/state.hpp | 16 ++-- include/session/state_groups.h | 33 ++++++++ src/config/groups/info.cpp | 4 + src/config/groups/members.cpp | 69 +++++++++------- src/state_c_wrapper.cpp | 15 ++-- tests/test_state.cpp | 9 +- 8 files changed, 145 insertions(+), 120 deletions(-) create mode 100644 include/session/state_groups.h diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index a4624b7d..7a7ab358 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -4,6 +4,7 @@ extern "C" { #endif +#include "../../state.h" #include "../base.h" #include "../profile_pic.h" #include "../util.h" @@ -27,86 +28,71 @@ typedef struct config_group_member { } config_group_member; -/// API: groups/groups_members_init -/// -/// Constructs a group members config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Inputs: -/// - `conf` -- [out] Pointer to the config object -/// - `ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is NULL. -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_members_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups/groups_members_get +/// API: groups/state_get_group_member /// /// Fills `member` with the member info given a session ID (specified as a null-terminated hex /// string), if the member exists, and returns true. If the member does not exist then `member` /// is left unchanged and false is returned. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// - `member` -- [out] the member info data /// - `session_id` -- [in] null terminated hex string +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Output: /// - `bool` -- Returns true if member exists -LIBSESSION_EXPORT bool groups_members_get( - config_object* conf, config_group_member* member, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_group_member( + const state_object* state, + const char* pubkey_hex, + config_group_member* member, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: groups/groups_members_get_or_construct +/// API: groups/state_get_or_construct_group_member /// -/// Same as the above `groups_members_get()` except that when the member does not exist, this sets -/// all the member fields to defaults and loads it with the given session_id. +/// Same as the above `state_get_group_members()` except that when the member does not exist, this +/// sets all the member fields to defaults and loads it with the given session_id. /// /// Returns true as long as it is given a valid session_id. A false return is considered an error, /// and means the session_id was not a valid session_id. /// /// This is the method that should usually be used to create or update a member, followed by -/// setting fields in the member, and then giving it to groups_members_set(). +/// setting fields in the member, and then giving it to state_set_group_member(). +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// - `member` -- [out] the member info data /// - `session_id` -- [in] null terminated hex string /// /// Output: /// - `bool` -- Returns true if the call succeeds, false if an error occurs (e.g. because of an /// invalid session_id). -LIBSESSION_EXPORT bool groups_members_get_or_construct( - config_object* conf, config_group_member* member, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_group_member( + const state_object* state, + const char* pubkey_hex, + config_group_member* member, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: groups/groups_members_set +/// API: groups/state_set_group_member /// /// Adds or updates a member from the given member info struct. /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `member` -- [in] Pointer containing the member info data -LIBSESSION_EXPORT void groups_members_set(config_object* conf, const config_group_member* member); +LIBSESSION_EXPORT void state_set_group_member( + mutable_state_group_object* state, const config_group_member* member); -/// API: groups/groups_members_erase +/// API: groups/state_erase_group_member /// /// Erases a member from the member list. session_id is in hex. Returns true if the member was /// found and removed, false if the member was not present. You must not call this during @@ -116,23 +102,27 @@ LIBSESSION_EXPORT void groups_members_set(config_object* conf, const config_grou /// group). /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `session_id` -- [in] Text containing null terminated hex string /// /// Outputs: /// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool groups_members_erase(config_object* conf, const char* session_id); +LIBSESSION_EXPORT bool state_erase_group_member( + mutable_state_group_object* state, const char* session_id); -/// API: groups/groups_members_size +/// API: groups/state_size_group_members /// /// Returns the number of group members. /// /// Inputs: -/// - `conf` -- input - Pointer to the config object +/// - `state` -- [in] - Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// /// Outputs: -/// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t groups_members_size(const config_object* conf); +/// - `size_t` -- number of members in the group (will be 0 if the group doesn't exist or the +/// 'pubkey_hex' is invalid) +LIBSESSION_EXPORT size_t +state_size_group_members(const state_object* state, const char* pubkey_hex); typedef struct groups_members_iterator { void* _internals; @@ -145,7 +135,7 @@ typedef struct groups_members_iterator { /// Functions for iterating through the entire member list, in sorted order. Intended use is: /// /// group_member m; -/// groups_members_iterator *it = groups_members_iterator_new(group); +/// groups_members_iterator *it = groups_members_iterator_new(state, group_id); /// for (; !groups_members_iterator_done(it, &c); groups_members_iterator_advance(it)) { /// // c.session_id, c.name, etc. are loaded /// } @@ -154,11 +144,13 @@ typedef struct groups_members_iterator { /// It is NOT permitted to add/remove/modify members while iterating. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// /// Outputs: /// - `groups_members_iterator*` -- pointer to the new iterator -LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new(const config_object* conf); +LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new( + const state_object* state, const char* pubkey_hex); /// API: groups/groups_members_iterator_free /// diff --git a/include/session/state.h b/include/session/state.h index 29e8a5bc..28e1e4f9 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -9,7 +9,6 @@ extern "C" { #include #include "config/base.h" -#include "config/groups/members.h" #include "config/namespaces.h" #include "config/profile_pic.h" #include "export.h" @@ -372,20 +371,6 @@ LIBSESSION_EXPORT bool state_get_keys( unsigned char** out, size_t* outlen); -LIBSESSION_EXPORT void state_create_group( - state_object* state, - const char* name, - const char* description, - const user_profile_pic pic_, - const config_group_member* members_, - const size_t members_len, - void (*callback)( - bool success, const char* group_id, unsigned const char* group_sk, void* ctx), - void* ctx); - -LIBSESSION_EXPORT void state_approve_group( - state_object* state, const char* group_id, unsigned const char* group_sk); - /// API: state/state_mutate_user /// /// Calls the callback provided with a mutable version of the `state_object` for user changes. diff --git a/include/session/state.hpp b/include/session/state.hpp index 89ec9dbf..d52fda60 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -195,10 +195,10 @@ class State { // Hook which will be called whenever config dumps need to be saved to persistent storage. The // hook will immediately be called upon assignment if the state needs to be stored. void on_store(std::function< - void(config::Namespace namespace_, - std::string prefixed_pubkey, - uint64_t timestamp_ms, - ustring data)> hook) { + void(config::Namespace namespace_, + std::string prefixed_pubkey, + uint64_t timestamp_ms, + ustring data)> hook) { _store = hook; if (!hook) @@ -220,10 +220,10 @@ class State { /// - `received_response` -- callback which should be called with the response from the send /// request. void on_send(std::function< - void(std::string pubkey, - ustring payload, - std::function - received_response)> hook) { + void(std::string pubkey, + ustring payload, + std::function + received_response)> hook) { _send = hook; if (!hook) diff --git a/include/session/state_groups.h b/include/session/state_groups.h new file mode 100644 index 00000000..13522144 --- /dev/null +++ b/include/session/state_groups.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "config/base.h" +#include "config/groups/members.h" +#include "config/namespaces.h" +#include "config/profile_pic.h" +#include "export.h" + +LIBSESSION_EXPORT void state_create_group( + state_object* state, + const char* name, + const char* description, + const user_profile_pic pic_, + const config_group_member* members_, + const size_t members_len, + void (*callback)( + bool success, const char* group_id, unsigned const char* group_sk, void* ctx), + void* ctx); + +LIBSESSION_EXPORT void state_approve_group( + state_object* state, const char* group_id, unsigned const char* group_sk); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index cd79897d..76436509 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -122,6 +122,8 @@ using namespace session; using namespace session::state; using namespace session::config; +extern "C" { + LIBSESSION_C_API const size_t GROUP_INFO_NAME_MAX_LENGTH = groups::Info::NAME_MAX_LENGTH; LIBSESSION_C_API const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH = groups::Info::DESCRIPTION_MAX_LENGTH; @@ -270,3 +272,5 @@ LIBSESSION_C_API bool state_groups_info_is_destroyed( LIBSESSION_C_API void state_destroy_group(mutable_state_group_object* state) { unbox(state).info.destroy_group(); } + +} // extern "C" \ No newline at end of file diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..204996c4 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -4,6 +4,8 @@ #include "../internal.hpp" #include "session/config/groups/members.h" +#include "session/state.h" +#include "session/state.hpp" namespace session::config::groups { @@ -177,65 +179,70 @@ void member::set_name(std::string n) { using namespace session; using namespace session::config; +using namespace session::state; -LIBSESSION_C_API int groups_members_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) { - return c_group_wrapper_init( - conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); -} +extern "C" { -LIBSESSION_C_API bool groups_members_get( - config_object* conf, config_group_member* member, const char* session_id) { +LIBSESSION_C_API bool state_get_group_member( + const state_object* state, + const char* pubkey_hex, + config_group_member* member, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get(session_id)) { + if (auto c = unbox(state).config(pubkey_hex).get(session_id)) { c->into(*member); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool groups_members_get_or_construct( - config_object* conf, config_group_member* member, const char* session_id) { +LIBSESSION_C_API bool state_get_or_construct_group_member( + const state_object* state, + const char* pubkey_hex, + config_group_member* member, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct(session_id).into(*member); + unbox(state).config(pubkey_hex).get_or_construct(session_id).into(*member); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); return false; } } -LIBSESSION_C_API void groups_members_set(config_object* conf, const config_group_member* member) { - unbox(conf)->set(groups::member{*member}); +LIBSESSION_C_API void state_set_group_member( + mutable_state_group_object* state, const config_group_member* member) { + unbox(state).members.set(groups::member{*member}); } -LIBSESSION_C_API bool groups_members_erase(config_object* conf, const char* session_id) { +LIBSESSION_C_API bool state_erase_group_member( + mutable_state_group_object* state, const char* session_id) { try { - return unbox(conf)->erase(session_id); + return unbox(state).members.erase(session_id); } catch (...) { return false; } } -LIBSESSION_C_API size_t groups_members_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t +state_size_group_members(const state_object* state, const char* pubkey_hex) { + try { + return unbox(state).config(pubkey_hex).size(); + } catch (...) { + return 0; + } } -LIBSESSION_C_API groups_members_iterator* groups_members_iterator_new(const config_object* conf) { +LIBSESSION_C_API groups_members_iterator* groups_members_iterator_new( + const state_object* state, const char* pubkey_hex) { auto* it = new groups_members_iterator{}; - it->_internals = new groups::Members::iterator{unbox(conf)->begin()}; + it->_internals = + new groups::Members::iterator{unbox(state).config(pubkey_hex).begin()}; return it; } @@ -256,3 +263,5 @@ LIBSESSION_C_API bool groups_members_iterator_done( LIBSESSION_C_API void groups_members_iterator_advance(groups_members_iterator* it) { ++*static_cast(it->_internals); } + +} // extern "C" diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 37e9955b..e00396d8 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -21,6 +21,7 @@ #include "session/export.h" #include "session/state.h" #include "session/state.hpp" +#include "session/state_groups.h" #include "session/util.hpp" using namespace std::literals; @@ -136,9 +137,9 @@ LIBSESSION_C_API bool state_set_send_callback( unbox(state).on_send(nullptr); else { unbox(state).on_send([callback, app_ctx]( - std::string pubkey, - ustring data, - response_callback_t received_response) { + std::string pubkey, + ustring data, + response_callback_t received_response) { // We leak ownership of this std::function below in the `.release()` call, then we // recapture it inside the inner response callback below. auto on_response = @@ -201,10 +202,10 @@ LIBSESSION_C_API bool state_set_store_callback( else { // Setting this can result in the callback being immediately triggered which could throw unbox(state).on_store([callback, ctx]( - config::Namespace namespace_, - std::string pubkey, - uint64_t timestamp_ms, - ustring data) { + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { callback( static_cast(namespace_), pubkey.c_str(), diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 7baa8f76..369a306e 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -43,10 +43,10 @@ TEST_CASE("State", "[state][state]") { std::optional last_send = std::nullopt; state.on_store([&last_store]( - config::Namespace namespace_, - std::string pubkey, - uint64_t timestamp_ms, - ustring data) { + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { last_store = {namespace_, pubkey, timestamp_ms, data}; }); state.on_send( @@ -55,6 +55,7 @@ TEST_CASE("State", "[state][state]") { // Replicate the behaviour in the C wrapper auto on_response = std::make_unique(std::move(received_response)); + last_send = { pubkey, payload, From faff84441eb8909967764010765e638953c0eaaf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 20 Feb 2024 09:32:02 +1100 Subject: [PATCH 10/24] Updated group keys C API to run via state, some renaming for clarity --- include/session/config.h | 12 + include/session/config/base.h | 539 ------------------- include/session/config/base.hpp | 26 - include/session/config/contacts.h | 4 +- include/session/config/convo_info_volatile.h | 17 +- include/session/config/groups/info.h | 106 ++-- include/session/config/groups/keys.h | 422 ++++++--------- include/session/config/groups/keys.hpp | 14 + include/session/config/groups/members.h | 32 +- include/session/config/groups/members.hpp | 6 +- include/session/config/user_groups.h | 17 +- include/session/config/user_profile.h | 11 +- include/session/state.h | 41 +- include/session/state.hpp | 47 +- include/session/state_groups.h | 12 +- include/session/util.hpp | 4 + src/CMakeLists.txt | 1 + src/config/base.cpp | 171 ------ src/config/contacts.cpp | 4 +- src/config/convo_info_volatile.cpp | 16 +- src/config/groups/info.cpp | 71 ++- src/config/groups/keys.cpp | 428 +++++++-------- src/config/groups/members.cpp | 31 +- src/config/internal.hpp | 78 +-- src/config/user_groups.cpp | 14 +- src/config/user_profile.cpp | 10 +- src/state.cpp | 240 +++++---- src/state_c_wrapper.cpp | 51 +- tests/test_config_contacts.cpp | 8 +- tests/test_config_convo_info_volatile.cpp | 20 +- tests/test_config_user_groups.cpp | 6 +- tests/test_group_keys.cpp | 474 +++++++++------- tests/test_state.cpp | 6 +- tests/utils.hpp | 1 - 34 files changed, 1098 insertions(+), 1842 deletions(-) delete mode 100644 include/session/config/base.h diff --git a/include/session/config.h b/include/session/config.h index eea54c1d..97f14164 100644 --- a/include/session/config.h +++ b/include/session/config.h @@ -4,10 +4,22 @@ extern "C" { #endif +#include #include typedef int64_t seqno_t; +/// Struct containing a list of C strings. Typically where this is returned by this API it must be +/// freed (via `free()`) when done with it. +/// +/// When returned as a pointer by a libsession-util function this is allocated in such a way that +/// just the outer session_string_list can be free()d to free both the list *and* the inner `value` +/// and pointed-at values. +typedef struct session_string_list { + char** value; // array of null-terminated C strings + size_t len; // length of `value` +} session_string_list; + #ifdef __cplusplus } #endif diff --git a/include/session/config/base.h b/include/session/config/base.h deleted file mode 100644 index 8707d903..00000000 --- a/include/session/config/base.h +++ /dev/null @@ -1,539 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#include "../config.h" -#include "../export.h" - -// Config object base type: this type holds the internal object and is initialized by the various -// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions. -typedef struct config_object { - // Internal opaque object pointer; calling code should leave this alone. - void* internals; - // When an error occurs in the C API this string will be set to the specific error message. May - // be empty. - const char* last_error; - - // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. - char _error_buf[256]; -} config_object; - -// Common functions callable on any config instance: - -/// API: base/config_free -/// -/// Frees a config object created with one of the config-dependent ..._init functions (e.g. -/// user_profile_init). -/// -/// Declaration: -/// ```cpp -/// VOID config_free( -/// [in, out] config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -LIBSESSION_EXPORT void config_free(config_object* conf); - -typedef enum config_log_level { - LOG_LEVEL_DEBUG = 0, - LOG_LEVEL_INFO, - LOG_LEVEL_WARNING, - LOG_LEVEL_ERROR -} config_log_level; - -/// API: base/config_set_logger -/// -/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL -/// if not needed). The given function pointer will be invoked with one of the above values, a -/// null-terminated c string containing the log message, and the void* context object given when -/// setting the logger (this is for caller-specific state data and won't be touched). -/// -/// The logging function must have signature: -/// -/// void log(config_log_level lvl, const char* msg, void* ctx); -/// -/// Can be called with callback set to NULL to clear an existing logger. -/// -/// The config object itself has no log level: the caller should filter by level as needed. -/// -/// Declaration: -/// ```cpp -/// VOID config_set_logger( -/// [in, out] config_object* conf, -/// [in] void(*)(config_log_level, const char*, void*) callback, -/// [in] void* ctx -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `callback` -- [in] Callback function -/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused -LIBSESSION_EXPORT void config_set_logger( - config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx); - -/// API: base/config_storage_namespace -/// -/// Returns the numeric namespace in which config messages of this type should be stored. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int16_t` -- integer of the namespace -LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf); - -/// Struct containing a list of C strings. Typically where this is returned by this API it must be -/// freed (via `free()`) when done with it. -/// -/// When returned as a pointer by a libsession-util function this is allocated in such a way that -/// just the outer config_string_list can be free()d to free both the list *and* the inner `value` -/// and pointed-at values. -typedef struct config_string_list { - char** value; // array of null-terminated C strings - size_t len; // length of `value` -} config_string_list; - -/// API: base/config_merge -/// -/// Merges the config object with one or more remotely obtained config strings. After this call the -/// config object may be unchanged, complete replaced, or updated and needing a push, depending on -/// the messages that are merged; the caller should check config_needs_push(). -/// -/// Declaration: -/// ```cpp -/// INT config_merge( -/// [in, out] config_object* conf, -/// [in] const char** msg_hashes, -/// [in] const unsigned char** configs, -/// [in] const size_t* lengths, -/// [in] size_t count -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `msg_hashes` -- [in] is an array of null-terminated C strings containing the hashes of the -/// configs being provided. -/// - `configs` -- [in] is an array of pointers to the start of the (binary) data. -/// - `lengths` -- [in] is an array of lengths of the binary data -/// - `count` -- [in] is the length of all three arrays. -/// -/// Outputs: -/// - `config_string_list*` -- pointer to the list of successfully parsed hashes; the pointer -/// belongs to the caller and must be freed when done with it. - -LIBSESSION_EXPORT config_string_list* config_merge( - config_object* conf, - const char** msg_hashes, - const unsigned char** configs, - const size_t* lengths, - size_t count) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; - -/// API: base/config_needs_push -/// -/// Returns true if this config object contains updated data that has not yet been confirmed stored -/// on the server. -/// -/// Declaration: -/// ```cpp -/// BOOL config_needs_push( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `bool` -- returns true if object contains updated data -LIBSESSION_EXPORT bool config_needs_push(const config_object* conf); - -/// Returned struct of config push data. -typedef struct config_push_data { - // The config seqno (to be provided later in `config_confirm_pushed`). - seqno_t seqno; - // The config message to push (binary data, not null-terminated). - unsigned char* config; - // The length of `config` - size_t config_len; - // Array of obsolete message hashes to delete; each element is a null-terminated C string - char** obsolete; - // length of `obsolete` - size_t obsolete_len; -} config_push_data; - -/// API: base/config_push -/// -/// Obtains the configuration data that needs to be pushed to the server. -/// -/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used -/// to re-obtain the current serialized config even if no push is needed (for example, if the client -/// wants to re-submit it after a network error). -/// -/// NB: The returned pointer belongs to the caller: that is, the caller *MUST* free() it when -/// done with it. -/// -/// Declaration: -/// ```cpp -/// CONFIG_PUSH_DATA* config_push( -/// [in, out] config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `config_push_data*` -- pointer to the config object. Pointer belongs to the caller. -LIBSESSION_EXPORT config_push_data* config_push(config_object* conf); - -/// API: base/config_confirm_pushed -/// -/// Reports that data obtained from `config_push` has been successfully stored on the server with -/// message hash `msg_hash`. The seqno value is the one returned by the config_push call that -/// yielded the config data. -/// -/// Declaration: -/// ```cpp -/// VOID config_confirm_pushed( -/// [in, out] config_object* conf, -/// [out] seqno_t seqno, -/// [out] const char* msg_hash -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `seqno` -- [out] Value returned by config_push call -/// - `msg_hash` -- [out] Value returned by config_push call -LIBSESSION_EXPORT void config_confirm_pushed( - config_object* conf, seqno_t seqno, const char* msg_hash); - -/// API: base/config_dump -/// -/// Returns a binary dump of the current state of the config object. This dump can be used to -/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets -/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated -/// C string. -/// -/// NB: It is the caller's responsibility to `free()` the buffer when done with it. -/// -/// Immediately after this is called `config_needs_dump` will start returning true (until the -/// configuration is next modified). -/// -/// Declaration: -/// ```cpp -/// VOID config_dump( -/// [in] config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `out` -- [out] Pointer to the output location -/// - `outlen` -- [out] Length of output -LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen); - -/// API: base/config_needs_dump -/// -/// Returns true if something has changed since the last call to `dump()` that requires calling -/// and saving the `config_dump()` data again. -/// -/// Declaration: -/// ```cpp -/// BOOL config_needs_dump( -/// [in] const config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `bool` -- True if config has changed since last call to `dump()` -LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf); - -/// API: base/config_current_hashes -/// -/// Obtains the current active hashes. Note that this will be empty if the current hash is unknown -/// or not yet determined (for example, because the current state is dirty or because the most -/// recent push is still pending and we don't know the hash yet). -/// -/// The returned pointer belongs to the caller and must be freed via `free()` when done with it. -/// -/// Declaration: -/// ```cpp -/// CONFIG_STRING_LIST* config_current_hashes( -/// [in] const config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `config_string_list*` -- pointer to the list of hashes; the pointer belongs to the caller -LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; - -/// API: base/config_get_keys -/// -/// Obtains the current group decryption keys. -/// -/// Returns a buffer where each consecutive 32 bytes is an encryption key for the object, in -/// priority order (i.e. the key at 0 is the encryption key, and the first decryption key). -/// -/// This function is mainly for debugging/diagnostics purposes; most config types have one single -/// key (based on the secret key), and multi-keyed configs such as groups have their own methods for -/// encryption/decryption that are already aware of the multiple keys. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config_object object -/// - `len` -- [out] Pointer where the number of keys will be written (that is: the returned pointer -/// will be to a buffer which has a size of of this value times 32). -/// -/// Outputs: -/// - `unsigned char*` -- pointer to newly malloced key data (a multiple of 32 bytes); the pointer -/// belongs to the caller and must be `free()`d when done with it. -LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size_t* len); - -/// Config key management; see the corresponding method docs in base.hpp. All `key` arguments here -/// are 32-byte binary buffers (and since fixed-length, there is no keylen argument). - -/// API: base/config_add_key -/// -/// Adds an encryption/decryption key, without removing existing keys. They key must be exactly -/// 32 bytes long. The newly added key becomes the highest priority key: it will be used for -/// encryption of config pushes after the call, and will be tried first when decrypting, followed by -/// keys present (if any) before this call. If the given key is already present in the key list -/// then this call moves it to the front of the list (if not already at the front). -/// -/// Declaration: -/// ```cpp -/// VOID config_add_key( -/// [in, out] config_object* conf, -/// [in] const unsigned char* key -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key); - -/// API: base/config_add_key_low_prio -/// -/// Adds an encryption/decryption key, without removing existing keys. They key must be exactly -/// 32 bytes long. The newly added key becomes the lowest priority key -/// -/// Declaration: -/// ```cpp -/// VOID config_add_key_low_prio( -/// [in, out] config_object* conf, -/// [in] const unsigned char* key -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key); - -/// API: base/config_clear_keys -/// -/// Clears all stored encryption/decryption keys. This is typically immediately followed with -/// one or more `add_key` call to replace existing keys. Returns the number of keys removed. -/// -/// Declaration: -/// ```cpp -/// INT config_clear_keys( -/// [in] config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int` -- Number of keys removed -LIBSESSION_EXPORT int config_clear_keys(config_object* conf); - -/// API: base/config_remove_key -/// -/// Removes the given encryption/decryption key, if present. Returns true if it was found and -/// removed, false if it was not in the key list. -/// -/// Declaration: -/// ```cpp -/// BOOL config_remove_key( -/// [in] const config_object* conf, -/// [in] const unsigned char* key -/// ), -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -/// -/// Outputs: -/// - `bool` -- True if key successfully removed -LIBSESSION_EXPORT bool config_remove_key(config_object* conf, const unsigned char* key); - -/// API: base/config_key_count -/// -/// Returns the number of encryption keys. -/// -/// Declaration: -/// ```cpp -/// INT config_key_count( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int` -- Number of encryption keys -LIBSESSION_EXPORT int config_key_count(const config_object* conf); - -/// API: base/config_key_count -/// -/// Returns true if the given key is already in the keys list. -/// -/// Declaration: -/// ```cpp -/// BOOL config_has_key( -/// [in] const config_object* conf, -/// [in] const unsigned char* key -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -/// -/// Outputs: -/// - `bool` -- True if key exists -LIBSESSION_EXPORT bool config_has_key(const config_object* conf, const unsigned char* key); - -/// API: base/config_has_key -/// -/// Returns a pointer to the 32-byte binary key at position i. This is *not* null terminated (and -/// is exactly 32 bytes long). `i < config_key_count(conf)` must be satisfied. Ownership of the -/// data remains in the object (that is: the caller must not attempt to free it). -/// -/// Declaration: -/// ```cpp -/// CONST UNSIGNED CHAR* config_key( -/// [in] const config_object* conf, -/// [in] size_t i -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `i` -- [in] Position of key in config object -/// -/// Outputs: -/// - `unsigned char*` -- binary data of the key, exactly 32 bytes and is not null terminated -LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i); - -/// API: base/config_encryption_domain -/// -/// Returns the encryption domain C-str used to encrypt values for this config object. (This is -/// here only for debugging/testing). -/// -/// Declaration: -/// ```cpp -/// CONST CHAR* config_encryption_domain( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `char*` -- encryption domain C-str used to encrypt values -LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf); - -/// API: base/config_set_sig_keys -/// -/// Sets an Ed25519 keypair pair for signing and verifying config messages. When set, this adds an -/// additional signature for verification into the config message (*after* decryption) that -/// validates a config message. -/// -/// This is used in config contexts where the encryption/decryption keys are insufficient for -/// permission verification to produce new messages, such as in groups where non-admins need to be -/// able to decrypt group data, but are not permitted to push new group data. In such a case only -/// the admins have the secret key with which messages can be signed; regular users can only read, -/// but cannot write, config messages. -/// -/// When a signature public key (with or without a secret key) is set the config object enters a -/// "signing-required" mode, which has some implications worth noting: -/// - incoming messages must contain a signature that verifies with the public key; messages -/// without such a signature will be dropped as invalid. -/// - because of the above, a config object cannot push config updates without the secret key: -/// thus any attempt to modify the config message with a pubkey-only config object will raise -/// an exception. -/// -/// Inputs: -/// - `secret` -- pointer to a 64-byte sodium-style Ed25519 "secret key" buffer (technically the -/// seed+precomputed pubkey concatenated together) that sets both the secret key and public key. -LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret); - -/// API: base/config_set_sig_pubkey -/// -/// Sets a Ed25519 signing pubkey which incoming messages must be signed by to be acceptable. This -/// is intended for use when the secret key is not known (see `config_set_sig_keys()` to set both -/// secret and pubkey keys together). -/// -/// Inputs: -/// - `pubkey` -- pointer to the 32-byte Ed25519 pubkey that must have signed incoming messages. -LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey); - -/// API: base/config_get_sig_pubkey -/// -/// Returns a pointer to the 32-byte Ed25519 signing pubkey, if set. Returns nullptr if there is no -/// current signing pubkey. -/// -/// Inputs: none. -/// -/// Outputs: -/// - pointer to the 32-byte pubkey, or NULL if not set. -LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf); - -/// API: base/config_clear_sig_keys -/// -/// Drops the signature pubkey and/or secret key, if the object has them. -/// -/// Inputs: none. -LIBSESSION_EXPORT void config_clear_sig_keys(config_object* conf); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 9096713d..27f9498a 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -11,7 +11,6 @@ #include #include -#include "base.h" #include "namespaces.hpp" namespace session::config { @@ -1279,31 +1278,6 @@ struct internals final { const ConfigT& operator*() const { return *operator->(); } }; -template , int> = 0> -inline internals& unbox(config_object* conf) { - return *static_cast*>(conf->internals); -} -template , int> = 0> -inline const internals& unbox(const config_object* conf) { - return *static_cast*>(conf->internals); -} - -// Sets an error message in the internals.error string and updates the last_error pointer in the -// outer (C) config_object struct to point at it. -void set_error(config_object* conf, std::string e); - -// Same as above, but gets the error string out of an exception and passed through a return value. -// Intended to simplify catch-and-return-error such as: -// try { -// whatever(); -// } catch (const std::exception& e) { -// return set_error(conf, LIB_SESSION_ERR_OHNOES, e); -// } -inline int set_error(config_object* conf, int errcode, const std::exception& e) { - set_error(conf, e.what()); - return errcode; -} - // Copies a value contained in a string into a new malloced char buffer, returning the buffer and // size via the two pointer arguments. void copy_out(ustring_view data, unsigned char** out, size_t* outlen); diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 393db90e..1c4640b6 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -89,7 +89,7 @@ LIBSESSION_EXPORT bool state_get_or_construct_contact( /// - `state` -- [in, out] Pointer to the mutable state object /// - `contact` -- [in] Pointer containing the contact info data LIBSESSION_EXPORT void state_set_contact( - mutable_state_user_object* state, const contacts_contact* contact); + mutable_user_state_object* state, const contacts_contact* contact); // NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would // save very little in actual calling code. The procedure for updating a single field without them @@ -117,7 +117,7 @@ LIBSESSION_EXPORT void state_set_contact( /// Outputs: /// - `bool` -- True if erasing was successful LIBSESSION_EXPORT bool state_erase_contact( - mutable_state_user_object* state, const char* session_id); + mutable_user_state_object* state, const char* session_id); /// API: contacts/state_size_contacts /// diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 20e61935..ca2aed8b 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -5,7 +5,6 @@ extern "C" { #endif #include "../state.h" -#include "base.h" #include "profile_pic.h" typedef struct convo_info_volatile_1to1 { @@ -262,7 +261,7 @@ LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_legacy_group( /// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to conversation info structure LIBSESSION_EXPORT void state_set_convo_info_volatile_1to1( - mutable_state_user_object* state, const convo_info_volatile_1to1* convo); + mutable_user_state_object* state, const convo_info_volatile_1to1* convo); /// API: convo_info_volatile/state_set_convo_info_volatile_community /// @@ -272,7 +271,7 @@ LIBSESSION_EXPORT void state_set_convo_info_volatile_1to1( /// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to community info structure LIBSESSION_EXPORT void state_set_convo_info_volatile_community( - mutable_state_user_object* state, const convo_info_volatile_community* convo); + mutable_user_state_object* state, const convo_info_volatile_community* convo); /// API: convo_info_volatile/state_set_convo_info_volatile_group /// @@ -282,7 +281,7 @@ LIBSESSION_EXPORT void state_set_convo_info_volatile_community( /// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to group info structure LIBSESSION_EXPORT void state_set_convo_info_volatile_group( - mutable_state_user_object* state, const convo_info_volatile_group* convo); + mutable_user_state_object* state, const convo_info_volatile_group* convo); /// API: convo_info_volatile/state_set_convo_info_volatile_legacy_group /// @@ -292,7 +291,7 @@ LIBSESSION_EXPORT void state_set_convo_info_volatile_group( /// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to legacy group info structure LIBSESSION_EXPORT void state_set_convo_info_volatile_legacy_group( - mutable_state_user_object* state, const convo_info_volatile_legacy_group* convo); + mutable_user_state_object* state, const convo_info_volatile_legacy_group* convo); /// API: convo_info_volatile/state_erase_convo_info_volatile_1to1 /// @@ -307,7 +306,7 @@ LIBSESSION_EXPORT void state_set_convo_info_volatile_legacy_group( /// Outputs: /// - `bool` - Returns true if conversation was found and removed LIBSESSION_EXPORT bool state_erase_convo_info_volatile_1to1( - mutable_state_user_object* state, const char* session_id); + mutable_user_state_object* state, const char* session_id); /// API: convo_info_volatile/state_erase_convo_info_volatile_community /// @@ -323,7 +322,7 @@ LIBSESSION_EXPORT bool state_erase_convo_info_volatile_1to1( /// Outputs: /// - `bool` - Returns true if community was found and removed LIBSESSION_EXPORT bool state_erase_convo_info_volatile_community( - mutable_state_user_object* state, const char* base_url, const char* room); + mutable_user_state_object* state, const char* base_url, const char* room); /// API: convo_info_volatile/state_erase_convo_info_volatile_group /// @@ -337,7 +336,7 @@ LIBSESSION_EXPORT bool state_erase_convo_info_volatile_community( /// Outputs: /// - `bool` - Returns true if group was found and removed LIBSESSION_EXPORT bool state_erase_convo_info_volatile_group( - mutable_state_user_object* state, const char* group_id); + mutable_user_state_object* state, const char* group_id); /// API: convo_info_volatile/state_erase_convo_info_volatile_legacy_group /// @@ -352,7 +351,7 @@ LIBSESSION_EXPORT bool state_erase_convo_info_volatile_group( /// Outputs: /// - `bool` - Returns true if group was found and removed LIBSESSION_EXPORT bool state_erase_convo_info_volatile_legacy_group( - mutable_state_user_object* state, const char* group_id); + mutable_user_state_object* state, const char* group_id); /// API: convo_info_volatile/state_size_convo_info_volatile /// diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 3623969f..f2631d36 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -5,34 +5,33 @@ extern "C" { #endif #include "../../state.h" -#include "../base.h" #include "../profile_pic.h" #include "../util.h" LIBSESSION_EXPORT extern const size_t GROUP_INFO_NAME_MAX_LENGTH; LIBSESSION_EXPORT extern const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH; -/// API: groups_info/state_get_groups_info_name +/// API: groups_info/state_get_group_name /// /// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at /// all. Should be copied right away as the pointer may not remain valid beyond other API calls. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `name` -- [out] the pointer to a buffer in which we will write the null-terminated name /// string. This must be a /// buffer of at least 'GROUP_INFO_NAME_MAX_LENGTH' bytes. /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group name -LIBSESSION_EXPORT bool state_get_groups_info_name( - const state_object* state, const char* pubkey_hex, char* name); +LIBSESSION_EXPORT bool state_get_group_name( + const state_object* state, const char* group_id, char* name); -/// API: groups_info/state_set_groups_info_name +/// API: groups_info/state_set_group_name /// /// Sets the group's name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). +/// error (and sets the state_object's error string). /// /// If the given name is longer than GROUP_INFO_NAME_MAX_LENGTH (100) bytes then it will be /// truncated. @@ -40,10 +39,9 @@ LIBSESSION_EXPORT bool state_get_groups_info_name( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -LIBSESSION_EXPORT void state_set_groups_info_name( - mutable_state_group_object* state, const char* name); +LIBSESSION_EXPORT void state_set_group_name(mutable_group_state_object* state, const char* name); -/// API: groups_info/state_get_groups_info_description +/// API: groups_info/state_get_group_description /// /// Returns a pointer to the currently-set description (null-terminated), or NULL if there is no /// description at all. Should be copied right away as the pointer may not remain valid beyond @@ -51,20 +49,20 @@ LIBSESSION_EXPORT void state_set_groups_info_name( /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `description` -- [out] the pointer to a buffer in which we will write the null-terminated /// description string. This must be a /// buffer of at least 'GROUP_INFO_DESCRIPTION_MAX_LENGTH' bytes. /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group description -LIBSESSION_EXPORT bool state_get_groups_info_description( - const state_object* state, const char* pubkey_hex, char* description); +LIBSESSION_EXPORT bool state_get_group_description( + const state_object* state, const char* group_id, char* description); -/// API: groups_info/state_set_groups_info_description +/// API: groups_info/state_set_group_description /// /// Sets the group's description to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). +/// error (and sets the state_object's error string). /// /// If the given description is longer than GROUP_INFO_DESCRIPTION_MAX_LENGTH (2000) bytes then it /// will be truncated. @@ -72,10 +70,10 @@ LIBSESSION_EXPORT bool state_get_groups_info_description( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `description` -- [in] Pointer to the description as a null-terminated C string -LIBSESSION_EXPORT void state_set_groups_info_description( - mutable_state_group_object* state, const char* description); +LIBSESSION_EXPORT void state_set_group_description( + mutable_group_state_object* state, const char* description); -/// API: groups_info/state_get_groups_info_pic +/// API: groups_info/state_get_group_pic /// /// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile /// pic is not currently set, and otherwise should be copied right away (they will not be valid @@ -83,41 +81,40 @@ LIBSESSION_EXPORT void state_set_groups_info_description( /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `description` -- [out] the pointer that will be set to the current profile pic (despite the /// "user_profile" in /// the struct name, this is the group's profile pic). /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group profile pic -LIBSESSION_EXPORT bool state_get_groups_info_pic( - const state_object* state, const char* pubkey_hex, user_profile_pic* pic); +LIBSESSION_EXPORT bool state_get_group_pic( + const state_object* state, const char* group_id, user_profile_pic* pic); -/// API: groups_info/state_set_groups_info_pic +/// API: groups_info/state_set_group_pic /// /// Sets a user profile /// /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `pic` -- [in] Pointer to the pic -LIBSESSION_EXPORT void state_set_groups_info_pic( - mutable_state_group_object* state, user_profile_pic pic); +LIBSESSION_EXPORT void state_set_group_pic(mutable_group_state_object* state, user_profile_pic pic); -/// API: groups_info/state_get_groups_info_expiry_timer +/// API: groups_info/state_get_group_expiry_timer /// /// Gets the group's message expiry timer (seconds). Returns 0 if not set. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `timer` -- [out] Pointer that will be set to the expiry timer in seconds. /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group expiry timer -LIBSESSION_EXPORT bool state_get_groups_info_expiry_timer( - const state_object* state, const char* pubkey_hex, int* timer); +LIBSESSION_EXPORT bool state_get_group_expiry_timer( + const state_object* state, const char* group_id, int* timer); -/// API: groups_info/state_set_groups_info_expiry_timer +/// API: groups_info/state_set_group_expiry_timer /// /// Sets the group's message expiry timer (seconds). Setting 0 (or negative) will clear the current /// timer. @@ -125,26 +122,25 @@ LIBSESSION_EXPORT bool state_get_groups_info_expiry_timer( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void state_set_groups_info_expiry_timer( - mutable_state_group_object* state, int expiry); +LIBSESSION_EXPORT void state_set_group_expiry_timer(mutable_group_state_object* state, int expiry); -/// API: groups_info/state_get_groups_info_created +/// API: groups_info/state_get_group_created /// /// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `created` -- [out] Pointer that will be set to the unix timestamp when the group was created /// (if set by an admin). /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group created /// timestamp -LIBSESSION_EXPORT bool state_get_groups_info_created( - const state_object* state, const char* pubkey_hex, int64_t* created); +LIBSESSION_EXPORT bool state_get_group_created( + const state_object* state, const char* group_id, int64_t* created); -/// API: groups_info/state_set_groups_info_created +/// API: groups_info/state_set_group_created /// /// Sets the creation time (unix timestamp, in seconds) when the group was created. Setting 0 /// clears the value. @@ -152,26 +148,26 @@ LIBSESSION_EXPORT bool state_get_groups_info_created( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_created(mutable_state_group_object* state, int64_t ts); +LIBSESSION_EXPORT void state_set_group_created(mutable_group_state_object* state, int64_t ts); -/// API: groups_info/state_get_groups_info_delete_before +/// API: groups_info/state_get_group_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds); clients should delete all messages /// from the group with timestamps earlier than this value, if set. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which messages /// should be deleted. Returns 0 if not set. /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted /// before value -LIBSESSION_EXPORT bool state_get_groups_info_delete_before( - const state_object* state, const char* pubkey_hex, int64_t* delete_before); +LIBSESSION_EXPORT bool state_get_group_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before); -/// API: groups_info/state_set_groups_info_delete_before +/// API: groups_info/state_set_group_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) before which messages should be /// deleted. Setting 0 clears the value. @@ -179,27 +175,26 @@ LIBSESSION_EXPORT bool state_get_groups_info_delete_before( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void state_set_groups_info_delete_before( - mutable_state_group_object* state, int64_t ts); +LIBSESSION_EXPORT void state_set_group_delete_before(mutable_group_state_object* state, int64_t ts); -/// API: groups_info/state_get_groups_info_attach_delete_before +/// API: groups_info/state_get_group_attach_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds) for attachments; clients should drop /// all attachments from messages from the group with timestamps earlier than this value, if set. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which message /// attachments should be deleted. Returns 0 if not set. /// /// Outputs: /// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted /// before value -LIBSESSION_EXPORT bool state_get_groups_info_attach_delete_before( - const state_object* state, const char* pubkey_hex, int64_t* delete_before); +LIBSESSION_EXPORT bool state_get_group_attach_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before); -/// API: groups_info/state_set_groups_info_attach_delete_before +/// API: groups_info/state_set_group_attach_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) for attachments; attachments should be /// dropped from messages older than this value. Setting 0 clears the value. @@ -207,22 +202,21 @@ LIBSESSION_EXPORT bool state_get_groups_info_attach_delete_before( /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void state_set_groups_info_attach_delete_before( - mutable_state_group_object* state, int64_t ts); +LIBSESSION_EXPORT void state_set_group_attach_delete_before( + mutable_group_state_object* state, int64_t ts); -/// API: groups_info/state_groups_info_is_destroyed +/// API: groups_info/state_group_is_destroyed /// /// Returns true if this group has been marked destroyed by an admin, which indicates to a receiving /// client that they should destroy it locally. /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `true` if the group has been nuked, `false` otherwise. -LIBSESSION_EXPORT bool state_groups_info_is_destroyed( - const state_object* state, const char* pubkey_hex); +LIBSESSION_EXPORT bool state_group_is_destroyed(const state_object* state, const char* group_id); /// API: groups_info/state_destroy_group /// @@ -230,7 +224,7 @@ LIBSESSION_EXPORT bool state_groups_info_is_destroyed( /// /// Inputs: /// - `state` -- [in] Pointer to the mutable state object -LIBSESSION_EXPORT void state_destroy_group(mutable_state_group_object* state); +LIBSESSION_EXPORT void state_destroy_group(mutable_group_state_object* state); #ifdef __cplusplus } // extern "C" diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 9dab2ac7..998a4b37 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -4,84 +4,24 @@ extern "C" { #endif -#include "../base.h" +#include "../../state.h" #include "../util.h" -// This is an opaque type analagous to `config_object` but specific to the groups keys object. -// -// It is constructed via groups_keys_init and destructed via groups_keys_free. -typedef struct config_group_keys { - // Internal opaque object pointer; calling code should leave this alone. - void* internals; - - // When an error occurs in the C API this string will be set to the specific error message. May - // be empty. - const char* last_error; - - // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. - char _error_buf[256]; - -} config_group_keys; - -/// API: groups/groups_keys_init -/// -/// Constructs a group keys management config object and sets a pointer to it in `conf`. -/// -/// Note that this is *not* a regular `config_object` and thus does not use the usual -/// `config_free()` and similar methods from `session/config/base.h`; instead it must be managed by -/// the functions declared in the header. -/// -/// Inputs: -/// - `conf` -- [out] Pointer-pointer to a `config_group_keys` pointer (i.e. double pointer); the -/// pointer will be set to a new config_group_keys object on success. -/// -/// Intended use: -/// -/// ```C -/// config_group_keys* keys; -/// int rc = groups_keys_init(&keys, ...); -/// ``` -/// - `user_ed25519_secretkey` -- [in] 64-byte pointer to the **user**'s (not group's) secret -/// ed25519 key. (Used to be able to decrypt keys encrypted individually for us). -/// - `group_ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `group_ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `group_info_conf` -- the group info config instance (keys will be added) -/// - `group_members_conf` -- the group members config instance (keys will be added) -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is NULL. -/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error -/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a -/// buffer of at least 256 bytes. -/// -/// Outputs: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_keys_init( - config_group_keys** conf, - const unsigned char* user_ed25519_secretkey, - const unsigned char* group_ed25519_pubkey, - const unsigned char* group_ed25519_secretkey, - config_object* group_info_conf, - config_object* group_members_conf, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_size +/// API: groups/state_size_group_keys /// /// Returns the number of decryption keys stored in this Keys object. Mainly for /// debugging/information purposes. /// /// Inputs: -/// - `conf` -- keys config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: -/// - `size_t` number of keys -LIBSESSION_EXPORT size_t groups_keys_size(const config_group_keys* conf); +/// - `size_t` -- number of members in the group (will be 0 if the group doesn't exist or the +/// 'pubkey_hex' is invalid) +LIBSESSION_EXPORT size_t state_size_group_keys(const state_object* state, const char* group_id); -/// API: groups/groups_keys_get_key +/// API: groups/state_get_group_key /// /// Accesses the Nth encryption key, ordered from most-to-least recent starting from index 0. /// Calling this with 0 thus returns the most-current key (which is also the current _en_cryption @@ -101,141 +41,47 @@ LIBSESSION_EXPORT size_t groups_keys_size(const config_group_keys* conf); /// freed. /// /// Inputs: -/// - `conf` -- keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `N` -- the index of the key to obtain /// /// Outputs: -/// - `const unsigned char*` -- pointer to the 32-byte key, or nullptr if there -LIBSESSION_EXPORT const unsigned char* groups_keys_get_key(const config_group_keys* conf, size_t N); - -/// API: groups/groups_keys_is_admin +/// - `const unsigned char*` -- pointer to the 32-byte key, or nullptr if thereis no group or key +LIBSESSION_EXPORT const unsigned char* state_get_group_key( + const state_object* state, const char* group_id, size_t N); +// +/// API: groups/state_is_group_admin /// /// Returns true if this object has the group private keys, i.e. the user is an all-powerful /// wiz^H^H^Hadmin of the group. /// /// Inputs: -/// - `conf` -- the groups config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `true` if we have admin keys, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_is_admin(const config_group_keys* conf); +LIBSESSION_EXPORT bool state_is_group_admin(const state_object* state, const char* group_id); -/// API: groups/groups_keys_load_admin_key +/// API: groups/state_load_group_admin_key /// /// Loads the admin keys, effectively upgrading this keys object from a member to an admin. /// /// This does nothing if the keys object already has admin keys. /// /// Inputs: -/// - `conf` -- the groups keys config object +/// - `state` -- Pointer to the mutable state object /// - `secret` -- pointer to the 32-byte group seed. (This a 64-byte libsodium "secret key" begins /// with the seed, this can also be a given a pointer to such a value). -/// - `group_info_conf` -- the group info config instance (the key will be added) -/// - `group_members_conf` -- the group members config instance (the key will be added) /// /// Outputs: /// - `true` if the object has been upgraded to admin status, or was already admin status; `false` /// if the given seed value does not match the group's public key. If this returns `true` then -/// after the call a call to `groups_keys_is_admin` would also return `true`. -LIBSESSION_EXPORT bool groups_keys_load_admin_key( - config_group_keys* conf, - const unsigned char* secret, - config_object* group_info_conf, - config_object* group_members_conf); +/// after the call a call to `state_is_group_admin` would also return `true`. +LIBSESSION_EXPORT bool state_load_group_admin_key( + mutable_group_state_object* state, const unsigned char* secret); -/// API: groups/groups_keys_rekey -/// -/// Generates a new encryption key for the group and returns an encrypted key message to be pushed -/// to the swarm containing the key, encrypted for the members of the group. -/// -/// The returned binary key message to be pushed is written into a newly-allocated buffer. A -/// pointer to this buffer is set in the pointer-pointer `out` argument, and its length is set in -/// the `outlen` pointer. -/// -/// See Keys::rekey in the C++ API for more details about intended use. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `info` -- [in] Pointer to group Info object -/// - `members` -- [in] Pointer to group Members object -/// - `out` -- [out] Will be set to a pointer to the message to be pushed (only if the function -/// returns true). This value must be used immediately (it is not guaranteed to remain valid -/// beyond other calls to the config object), and must not be freed (i.e. ownership remains with -/// the keys config object). -/// - `outlen` -- [out] Length of the output value. Only set when the function returns true. -/// -/// Output: -/// - `bool` -- Returns true on success, false on failure. -LIBSESSION_EXPORT bool groups_keys_rekey( - config_group_keys* conf, - config_object* info, - config_object* members, - const unsigned char** out, - size_t* outlen) __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_pending_config -/// -/// If a `rekey()` is currently in progress (and not yet confirmed, or possibly lost), this returns -/// the config message that should be pushed. As with the result of `rekey()` the pointer ownership -/// remains with the keys config object, and the value should be used/copied immediately. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `out` -- [out] Pointer-pointer that will be updated to point at the config data. Only set if -/// this function returns true! -/// - `outlen` -- [out] Pointer to the config data size (only set if the function returns true). -/// -/// Outputs: -/// - `bool` -- true if `out` and `outlen` have been updated to point to a pending config message; -/// false if there is no pending config message. -LIBSESSION_EXPORT bool groups_keys_pending_config( - const config_group_keys* conf, const unsigned char** out, size_t* outlen) - __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_load_message -/// -/// Loads a key config message downloaded from the swarm, and loads the key into the info/member -/// configs. -/// -/// Such messages should be processed via this method *before* attempting to load config messages -/// downloaded from an info/members namespace. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `msg_hash` -- [in] Null-terminated C string containing the message hash -/// - `data` -- [in] Pointer to the incoming key config message -/// - `datalen` -- [in] length of `data` -/// - `timestamp_ms` -- [in] the timestamp (from the swarm) of the message -/// - `info` -- [in] the info config object to update with newly discovered keys -/// - `members` -- [in] the members config object to update with newly discovered keys -/// -/// Outputs: -/// Returns `true` if the message was parsed successfully (whether or not any new keys were -/// decrypted or loaded). Returns `false` on failure to parse (and sets `conf->last_error`). -LIBSESSION_EXPORT bool groups_keys_load_message( - config_group_keys* conf, - const char* msg_hash, - const unsigned char* data, - size_t datalen, - int64_t timestamp_ms, - config_object* info, - config_object* members) __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_current_hashes -/// -/// Returns the hashes of currently active keys messages, that is, messages that have a decryption -/// key that new devices or clients might require; these are the messages that should have their -/// expiries renewed periodically. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the keys config object -/// -/// Outputs: -/// - `config_string_list*` -- pointer to an array of message hashes. The returned pointer belongs -/// to the caller and must be free()d when done. -LIBSESSION_EXPORT config_string_list* groups_keys_current_hashes(const config_group_keys* conf); - -/// API: groups/groups_keys_needs_rekey +/// API: groups/state_group_needs_rekey /// /// Checks whether a rekey is required (for instance, because of key generation conflict). Note /// that this is *not* a check for when members changed (such rekeys are up to the caller to @@ -244,43 +90,31 @@ LIBSESSION_EXPORT config_string_list* groups_keys_current_hashes(const config_gr /// See the C++ Keys::needs_rekey and Keys::rekey descriptions for more details. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `bool` -- `true` if `rekey()` needs to be called, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_needs_rekey(const config_group_keys* conf) +LIBSESSION_EXPORT bool state_group_needs_rekey(const state_object* state, const char* group_id) __attribute__((warn_unused_result)); -/// API: groups/groups_keys_needs_dump +/// API: groups/state_rekey_group +/// +/// Generates a new encryption key for the group containing the key, encrypted for the members of +/// the group. This function should be used after modify group members when mutating a group to +/// ensure the updated keys include the changes. /// -/// Checks whether a groups_keys_dump needs to be called to save state. This is analagous to -/// config_dump, but specific for the group keys object. The value becomes false as soon as -/// `groups_keys_dump` is called, and remains false until the object's state is mutated (e.g. by -/// rekeying or loading new config messages). +/// See Keys::rekey in the C++ API for more details about intended use. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- Pointer to the mutable state object /// -/// Outputs: -/// - `bool` -- `true` if a dump is needed, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) +/// Output: +/// - `bool` -- Returns true on success, false on failure. +LIBSESSION_EXPORT bool state_rekey_group(mutable_group_state_object* state) __attribute__((warn_unused_result)); -/// API: groups/groups_keys_dump -/// -/// Produces a dump of the keys object state to be stored by the application to later restore the -/// object by passing the dump into the constructor. This is analagous to config_dump, but specific -/// for the group keys object. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `out` -- [out] Pointer-pointer to a data buffer; this will be set to a newly malloc'd pointer -/// containing the dump data. The caller is responsible for freeing the data when done! -/// - `outlen` -- [out] Pointer to a size_t where the length of `out` will be stored. -LIBSESSION_EXPORT void groups_keys_dump( - config_group_keys* conf, unsigned char** out, size_t* outlen); - -/// API: groups/groups_keys_key_supplement +/// API: groups/state_supplement_group_key /// /// Generates a supplemental key message for one or more session IDs. This is used to distribute /// existing active keys to a new member so that that member can access existing keys, configs, and @@ -296,44 +130,46 @@ LIBSESSION_EXPORT void groups_keys_dump( /// instead of this method. /// /// Inputs: -/// - `conf` -- pointer to the keys config object +/// - `state` -- [in] - Pointer to the mutable state object /// - `sids` -- array of session IDs of the members to generate a supplemental key for; each element /// must be an ordinary (null-terminated) C string containing the 66-character session id. /// - `sids_len` -- length of the `sids` array -/// - `message` -- pointer-pointer that will be set to a newly allocated buffer containing the -/// message that should be sent to the swarm. The caller must free() the pointer when finished to -/// not leak the message memory (but only if the function returns true). -/// - `message_len` -- pointer to a `size_t` that will be set to the length of the `message` buffer. -/// -/// Oututs: -/// - `true` and sets `*message` and `*message_len` on success; returns `false` and does not set -/// them on failure. -LIBSESSION_EXPORT bool groups_keys_key_supplement( - config_group_keys* conf, +/// - `callback` -- [in] Callback function called once the send process completes +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_supplement_group_key( + mutable_group_state_object* state, const char** sids, size_t sids_len, - unsigned char** message, - size_t* message_len); + void (*callback)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* ctx), + void* ctx); -/// API: groups/groups_keys_current_generation +/// API: groups/state_get_current_group_generation /// /// Returns the current generation number for the latest keys message. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Oututs: -/// - `int` -- latest keys generation number -LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); +/// - `int` -- latest keys generation number, returns 0 if there is no group or the key is invalid +LIBSESSION_EXPORT int state_get_current_group_generation( + const state_object* state, const char* group_id); -/// API: groups/groups_keys_swarm_make_subaccount +/// API: groups/state_make_group_swarm_subaccount /// /// Constructs a swarm subaccount signing value that a member can use to access messages in the /// swarm. The member will have read and write access, but not delete access. Requires group /// admins keys. /// /// Inputs: -/// - `conf` -- the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `sign_value` -- [out] pointer to a 100 byte (or larger) buffer where the 100 byte signing /// value will be written. This is the value that should be sent to a member to allow @@ -342,17 +178,21 @@ LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); /// Outputs: /// - `true` -- if making the subaccount succeeds, false if it fails (e.g. because of an invalid /// session id, or not being an admin). If a failure occurs, sign_value will not be written to. -LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount( - config_group_keys* conf, const char* session_id, unsigned char* sign_value); +LIBSESSION_EXPORT bool state_make_group_swarm_subaccount( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* sign_value, + char* error); -/// API: groups/groups_keys_swarm_make_subaccount_flags +/// API: groups/state_make_group_swarm_subaccount_flags /// -/// Same as groups_keys_swarm_make_subaccount, but lets you specify whether the write/del flags are +/// Same as state_make_group_swarm_subaccount, but lets you specify whether the write/del flags are /// present. /// -/// /// Inputs: -/// - `conf` -- the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the member session id (hex c string) /// - `write` -- if true then the member shall be allowed to submit messages into the group account /// of the swarm and extend (but not shorten) the expiry of messages in the group account. If @@ -362,17 +202,22 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount( /// without having the full admin group keys. Typically this is false. /// - `sign_value` -- pointer to a buffer with at least 100 bytes where the 100 byte signing value /// will be written. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: -/// - `bool` - same as groups_keys_swarm_make_subaccount -LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( - config_group_keys* conf, +/// - `bool` - same as state_make_group_swarm_subaccount +LIBSESSION_EXPORT bool state_make_group_swarm_subaccount_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* sign_value); + unsigned char* sign_value, + char* error); -/// API: groups/groups_keys_swarm_verify_subaccount +/// API: groups/verify_group_swarm_subaccount /// /// Verifies that a received subaccount signing value (allegedly produced by /// groups_keys_swarm_make_subaccount) is a valid subaccount signing value for the given group @@ -384,9 +229,12 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( /// Inputs: /// - note that this function does *not* take a config object as it is intended for use to validate /// an invitation before constructing the keys config objects. -/// - `groupid` -- the group id/pubkey, in hex, beginning with "03". +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_ed25519_secretkey` -- the user's Session ID secret key (64 bytes). /// - `signing_value` -- the 100-byte subaccount signing value to validate +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// The key will require read and write access to be acceptable. (See the _flags version if you /// need something else). @@ -395,36 +243,36 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( /// - `true` if `signing_value` is a valid subaccount signing value for `groupid` with (at least) /// read and write permissions, `false` if the signing value does not validate or does not meet /// the requirements. -LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount( +LIBSESSION_EXPORT bool verify_group_swarm_subaccount( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value); -/// API: groups/groups_keys_swarm_verify_subaccount_flags +/// API: groups/verify_group_swarm_subaccount_flags /// -/// Same as groups_keys_swarm_verify_subaccount, except that you can specify whether you want to +/// Same as verify_group_swarm_subaccount, except that you can specify whether you want to /// require the write and or delete flags. /// /// Inputs: -/// - same as groups_keys_swarm_verify_subaccount +/// - same as verify_group_swarm_subaccount /// - `write` -- if true, require that the signing_value has write permission (i.e. that the /// user will be allowed to post messages). /// - `del` -- if true, required that the signing_value has delete permissions (i.e. that the /// user will be allowed to remove storage messages from the group's swarm). Note that this /// permission is about forcible swarm message deletion, and has no effect on an ability to /// submit a deletion meta-message to the group (which only requires writing a message). -LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( +LIBSESSION_EXPORT bool verify_group_swarm_subaccount_flags( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value, bool write, bool del); -/// API: groups/groups_keys_swarm_subaccount_sign +/// API: groups/state_sign_group_swarm_subaccount /// /// This helper function generates the required signature for swarm subaccount authentication, /// given the user's keys and swarm auth keys (as provided by an admin, produced via -/// `groups_keys_swarm_make_subaccount`). +/// `state_make_group_swarm_subaccount`). /// /// Storage server subaccount authentication requires passing the three values in the returned /// struct in the storage server request. @@ -433,7 +281,8 @@ LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( /// also a `_binary` version that writes raw values. /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `msg` -- the binary data that needs to be signed (which depends on the storage server request /// being made; for example, "retrieve9991234567890123" for a retrieve request to namespace 999 /// made at unix time 1234567890.123; see storage server RPC documentation for details). @@ -451,44 +300,51 @@ LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( /// 88-character request signature will be written, base64 encoded. This is passes as the /// `signature` value, alongside `subaccount`/`subaccoung_sig` to perform subaccount signature /// authentication. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - true if the values were written, false if an error occured (e.g. from an invalid signing_value /// or cryptography error). -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_sign_group_swarm_subaccount( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, char* subaccount, char* subaccount_sig, - char* signature); + char* signature, + char* error); -/// API: groups/groups_keys_swarm_subaccount_sign_binary +/// API: groups/state_sign_group_swarm_subaccount_binary /// -/// Does exactly the same as groups_keys_swarm_subaccount_sign except that the subaccount, +/// Does exactly the same as state_sign_group_swarm_subaccount except that the subaccount, /// subaccount_sig, and signature values are written in binary (without null termination) of exactly /// 36, 64, and 64 bytes, respectively. /// /// Inputs: -/// - see groups_keys_swarm_subaccount_sign +/// - see state_sign_group_swarm_subaccount /// - `subaccount`, `subaccount_sig`, and `signature` are binary output buffers of size 36, 64, and /// 64, respectively. /// /// Outputs: /// See groups_keys_swarm_subaccount. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign_binary( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_sign_group_swarm_subaccount_binary( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, unsigned char* subaccount, unsigned char* subaccount_sig, - unsigned char* signature); + unsigned char* signature, + char* error); -/// API: groups/groups_keys_swarm_subaccount_token +/// API: groups/state_get_group_swarm_subaccount_token /// /// Constructs the subaccount token for a session id. The main use of this is to submit a swarm /// token revocation; for issuing subaccount tokens you want to use @@ -502,36 +358,50 @@ LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign_binary( /// gains access to messages, they cannot read them). /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `token` -- [out] a 36-byte buffer into which to write the subaccount token. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - true if the call succeeded, false if an error occured. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token( - config_group_keys* conf, const char* session_id, unsigned char* token); +LIBSESSION_EXPORT bool state_get_group_swarm_subaccount_token( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* token, + char* error); -/// API: groups/groups_keys_swarm_subaccount_token_flags +/// API: groups/state_get_group_swarm_subaccount_token_flags /// -/// Same as `groups_keys_swarm_subaccount_token`, but takes `write` and `del` flags for creating a -/// token matching a user with non-standard permissions. +/// Same as `state_get_group_swarm_subaccount_token`, but takes `write` and `del` flags for creating +/// a token matching a user with non-standard permissions. /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `write`, `del` -- see groups_keys_swarm_make_subaccount_flags /// - `token` -- [out] a 36-byte buffer into which to write the subaccount token. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - true if the call succeeded, false if an error occured. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_get_group_swarm_subaccount_token_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* token); + unsigned char* token, + char* error); -/// API: groups/groups_keys_encrypt_message +/// API: groups/state_encrypt_group_message /// /// Encrypts a message using the most recent group encryption key of this object. /// @@ -544,21 +414,23 @@ LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( /// and should not be read or free()d. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". /// - `plaintext_in` -- [in] Pointer to a data buffer containing the unencrypted data. /// - `plaintext_len` -- [in] Length of `plaintext_in` /// - `ciphertext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// encrypted data written to it, and then the pointer to that buffer is stored here. This /// buffer must be `free()`d by the caller when done with it! /// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. -LIBSESSION_EXPORT void groups_keys_encrypt_message( - const config_group_keys* conf, +LIBSESSION_EXPORT void state_encrypt_group_message( + const state_object* state, + const char* group_id, const unsigned char* plaintext_in, size_t plaintext_len, unsigned char** ciphertext_out, size_t* ciphertext_len); -/// API: groups/groups_keys_decrypt_message +/// API: groups/state_decrypt_group_message /// /// Attempts to decrypt a message using all of the known active encryption keys of this object. The /// message will be de-padded, decompressed (if compressed), and have its signature verified after @@ -568,7 +440,8 @@ LIBSESSION_EXPORT void groups_keys_encrypt_message( /// reason the decryption failed (intended for logging, not for end-user display). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data (as was /// produced by `groups_keys_encrypt_message`). /// - `ciphertext_len` -- [in] Length of `ciphertext_in` @@ -581,19 +454,24 @@ LIBSESSION_EXPORT void groups_keys_encrypt_message( /// false, in which case the buffer pointer will not be set. /// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. /// Not touched if the function returns false. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. /// /// Outputs: /// - `bool` -- True if the message was successfully decrypted, false if decryption (or parsing or /// decompression) failed with all of our known keys. If (and only if) true is returned then /// `plaintext_out` must be freed when done with it. If false is returned then `conf.last_error` /// will contain a diagnostic message describing why the decryption failed. -LIBSESSION_EXPORT bool groups_keys_decrypt_message( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_decrypt_group_message( + const state_object* state, + const char* group_id, const unsigned char* cipherext_in, size_t cipherext_len, char* session_id_out, unsigned char** plaintext_out, - size_t* plaintext_len); + size_t* plaintext_len, + char* error); #ifdef __cplusplus } // extern "C" diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 0848c2bf..3dc593d7 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -354,6 +354,20 @@ class Keys final : public ConfigSig { return key_supplement(std::vector{{std::move(sid)}}); } + /// API: groups/Keys::prepare_supplement_payload + /// + /// Generates a payload to send the supplemental keys message to the swarm. + /// + /// Inputs: + /// - `supplement_msg` -- the message generated by `key_supplement`. + /// - `timestamp` -- the current timestamp offset by the latest service node network offset. + /// + /// Outputs: + /// - `std::pair` containing the swarm public key and payload that should + /// be sent to the swarm to store the supplemental keys message. + std::pair prepare_supplement_payload( + ustring supplement_msg, std::chrono::milliseconds timestamp) const; + /// API: groups/current_generation /// /// Returns the current generation number for the latest keys message. diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 7a7ab358..393d2851 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -5,14 +5,13 @@ extern "C" { #endif #include "../../state.h" -#include "../base.h" #include "../profile_pic.h" #include "../util.h" enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2 }; enum groups_members_remove_status { REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2 }; -typedef struct config_group_member { +typedef struct state_group_member { char session_id[67]; // in hex; 66 hex chars + null terminator. // These two will be 0-length strings when unset: @@ -26,7 +25,7 @@ typedef struct config_group_member { // member and their messages bool supplement; -} config_group_member; +} state_group_member; /// API: groups/state_get_group_member /// @@ -48,7 +47,7 @@ typedef struct config_group_member { LIBSESSION_EXPORT bool state_get_group_member( const state_object* state, const char* pubkey_hex, - config_group_member* member, + state_group_member* member, const char* session_id, char* error) __attribute__((warn_unused_result)); @@ -68,7 +67,7 @@ LIBSESSION_EXPORT bool state_get_group_member( /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `member` -- [out] the member info data /// - `session_id` -- [in] null terminated hex string /// @@ -77,8 +76,8 @@ LIBSESSION_EXPORT bool state_get_group_member( /// invalid session_id). LIBSESSION_EXPORT bool state_get_or_construct_group_member( const state_object* state, - const char* pubkey_hex, - config_group_member* member, + const char* group_id, + state_group_member* member, const char* session_id, char* error) __attribute__((warn_unused_result)); @@ -90,7 +89,7 @@ LIBSESSION_EXPORT bool state_get_or_construct_group_member( /// - `state` -- [in, out] Pointer to the mutable state object /// - `member` -- [in] Pointer containing the member info data LIBSESSION_EXPORT void state_set_group_member( - mutable_state_group_object* state, const config_group_member* member); + mutable_group_state_object* state, const state_group_member* member); /// API: groups/state_erase_group_member /// @@ -108,7 +107,7 @@ LIBSESSION_EXPORT void state_set_group_member( /// Outputs: /// - `bool` -- True if erasing was successful LIBSESSION_EXPORT bool state_erase_group_member( - mutable_state_group_object* state, const char* session_id); + mutable_group_state_object* state, const char* session_id); /// API: groups/state_size_group_members /// @@ -116,13 +115,12 @@ LIBSESSION_EXPORT bool state_erase_group_member( /// /// Inputs: /// - `state` -- [in] - Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `size_t` -- number of members in the group (will be 0 if the group doesn't exist or the -/// 'pubkey_hex' is invalid) -LIBSESSION_EXPORT size_t -state_size_group_members(const state_object* state, const char* pubkey_hex); +/// 'group_id' is invalid) +LIBSESSION_EXPORT size_t state_size_group_members(const state_object* state, const char* group_id); typedef struct groups_members_iterator { void* _internals; @@ -145,12 +143,12 @@ typedef struct groups_members_iterator { /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `groups_members_iterator*` -- pointer to the new iterator LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new( - const state_object* state, const char* pubkey_hex); + const state_object* state, const char* group_id); /// API: groups/groups_members_iterator_free /// @@ -167,12 +165,12 @@ LIBSESSION_EXPORT void groups_members_iterator_free(groups_members_iterator* it) /// /// Inputs: /// - `it` -- [in] Pointer to the groups_members_iterator -/// - `m` -- [out] Pointer to the config_group_member, will be populated if false is returned +/// - `m` -- [out] Pointer to the state_group_member, will be populated if false is returned /// /// Outputs: /// - `bool` -- True if iteration has reached the end LIBSESSION_EXPORT bool groups_members_iterator_done( - groups_members_iterator* it, config_group_member* m); + groups_members_iterator* it, state_group_member* m); /// API: groups/groups_members_iterator_advance /// diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..acd5d2d8 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -8,7 +8,7 @@ #include "../namespaces.hpp" #include "../profile_pic.hpp" -struct config_group_member; +struct state_group_member; namespace session::config::groups { @@ -49,7 +49,7 @@ struct member { explicit member(std::string sid); // Internal ctor/method for C API implementations: - explicit member(const config_group_member& c); // From c struct + explicit member(const state_group_member& c); // From c struct /// API: groups/member::session_id /// @@ -246,7 +246,7 @@ struct member { /// /// Inputs: /// - `m` -- Reference to C struct to fill with group member info. - void into(config_group_member& m) const; + void into(state_group_member& m) const; /// API: groups/member::set_name /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 3016d751..30664d7e 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -5,7 +5,6 @@ extern "C" { #endif #include "../state.h" -#include "base.h" #include "notify.h" #include "util.h" @@ -246,7 +245,7 @@ LIBSESSION_EXPORT bool state_get_or_construct_ugroups_legacy_group( /// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a community group info object LIBSESSION_EXPORT void state_set_ugroups_community( - mutable_state_user_object* state, const ugroups_community_info* group); + mutable_user_state_object* state, const ugroups_community_info* group); /// API: user_groups/state_set_ugroups_group /// @@ -256,7 +255,7 @@ LIBSESSION_EXPORT void state_set_ugroups_community( /// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a group info object LIBSESSION_EXPORT void state_set_ugroups_group( - mutable_state_user_object* state, const ugroups_group_info* group); + mutable_user_state_object* state, const ugroups_group_info* group); /// API: user_groups/state_set_ugroups_legacy_group /// @@ -269,7 +268,7 @@ LIBSESSION_EXPORT void state_set_ugroups_group( /// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object LIBSESSION_EXPORT void state_set_ugroups_legacy_group( - mutable_state_user_object* state, const ugroups_legacy_group_info* group); + mutable_user_state_object* state, const ugroups_legacy_group_info* group); /// API: user_groups/state_set_free_ugroups_legacy_group /// @@ -281,7 +280,7 @@ LIBSESSION_EXPORT void state_set_ugroups_legacy_group( /// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object LIBSESSION_EXPORT void state_set_free_ugroups_legacy_group( - mutable_state_user_object* state, ugroups_legacy_group_info* group); + mutable_user_state_object* state, ugroups_legacy_group_info* group); /// API: user_groups/state_erase_ugroups_community /// @@ -297,7 +296,7 @@ LIBSESSION_EXPORT void state_set_free_ugroups_legacy_group( /// Outputs: /// - `bool` -- Returns True if conversation was found and removed LIBSESSION_EXPORT bool state_erase_ugroups_community( - mutable_state_user_object* state, const char* base_url, const char* room); + mutable_user_state_object* state, const char* base_url, const char* room); /// API: user_groups/state_erase_ugroups_group /// @@ -312,7 +311,7 @@ LIBSESSION_EXPORT bool state_erase_ugroups_community( /// Outputs: /// - `bool` -- Returns True if conversation was found and removed LIBSESSION_EXPORT bool state_erase_ugroups_group( - mutable_state_user_object* state, const char* group_id); + mutable_user_state_object* state, const char* group_id); /// API: user_groups/state_erase_ugroups_legacy_group /// @@ -327,7 +326,7 @@ LIBSESSION_EXPORT bool state_erase_ugroups_group( /// Outputs: /// - `bool` -- Returns True if conversation was found and removed LIBSESSION_EXPORT bool state_erase_ugroups_legacy_group( - mutable_state_user_object* state, const char* group_id); + mutable_user_state_object* state, const char* group_id); /// API: user_groups/ugroups_group_set_kicked /// @@ -573,7 +572,7 @@ LIBSESSION_EXPORT size_t state_size_ugroups_groups(const state_object* state); /// Returns the number of legacy group conversations. /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 590ebfa5..d86505eb 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -5,7 +5,6 @@ extern "C" { #endif #include "../state.h" -#include "base.h" #include "profile_pic.h" /// API: state/state_get_profile_name @@ -29,7 +28,7 @@ LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); /// Inputs: /// - `state` -- [in] Pointer to the mutable state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -LIBSESSION_EXPORT void state_set_profile_name(mutable_state_user_object* state, const char* name); +LIBSESSION_EXPORT void state_set_profile_name(mutable_user_state_object* state, const char* name); /// API: state/state_get_profile_pic /// @@ -52,7 +51,7 @@ LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* sta /// - `state` -- [in] Pointer to the mutable state object /// - `pic` -- [in] Pointer to the pic LIBSESSION_EXPORT void state_set_profile_pic( - mutable_state_user_object* state, user_profile_pic pic); + mutable_user_state_object* state, user_profile_pic pic); /// API: state/state_get_profile_nts_priority /// @@ -75,7 +74,7 @@ LIBSESSION_EXPORT int state_get_profile_nts_priority(const state_object* state); /// - `state` -- [in] Pointer to the mutable state object /// - `priority` -- [in] Integer of the priority LIBSESSION_EXPORT void state_set_profile_nts_priority( - mutable_state_user_object* state, int priority); + mutable_user_state_object* state, int priority); /// API: state/state_get_profile_nts_expiry /// @@ -96,7 +95,7 @@ LIBSESSION_EXPORT int state_get_profile_nts_expiry(const state_object* state); /// Inputs: /// - `state` -- [in] Pointer to the state object /// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void state_set_profile_nts_expiry(mutable_state_user_object* state, int expiry); +LIBSESSION_EXPORT void state_set_profile_nts_expiry(mutable_user_state_object* state, int expiry); /// API: state/state_get_profile_blinded_msgreqs /// @@ -120,7 +119,7 @@ LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* stat /// - `state` -- [in] Pointer to the mutable state object /// - `enabled` -- [in] true if they should be enabled, false if disabled LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs( - mutable_state_user_object* state, int enabled); + mutable_user_state_object* state, int enabled); #ifdef __cplusplus } // extern "C" diff --git a/include/session/state.h b/include/session/state.h index 28e1e4f9..c4a60859 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -8,7 +8,7 @@ extern "C" { #include #include -#include "config/base.h" +#include "config.h" #include "config/namespaces.h" #include "config/profile_pic.h" #include "export.h" @@ -25,15 +25,15 @@ typedef struct state_object { char _error_buf[256]; } state_object; -typedef struct mutable_state_user_object { +typedef struct mutable_user_state_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; -} mutable_state_user_object; +} mutable_user_state_object; -typedef struct mutable_state_group_object { +typedef struct mutable_group_state_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; -} mutable_state_group_object; +} mutable_group_state_object; typedef struct state_namespaced_dump { NAMESPACE namespace_; @@ -55,6 +55,13 @@ typedef struct state_send_response { void* internals; } state_send_response; +typedef enum state_log_level { + LOG_LEVEL_DEBUG = 0, + LOG_LEVEL_INFO, + LOG_LEVEL_WARNING, + LOG_LEVEL_ERROR +} state_log_level; + /// API: state/state_init /// /// Constructs a new state which generates it's own random ed25519 key pair. @@ -89,7 +96,7 @@ LIBSESSION_EXPORT bool state_init( /// Frees a state object. /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `conf` -- [in] Pointer to state_object object LIBSESSION_EXPORT void state_free(state_object* state); /// API: state/state_load @@ -122,7 +129,7 @@ LIBSESSION_EXPORT bool state_load( /// /// The logging function must have signature: /// -/// void log(config_log_level lvl, const char* msg, void* ctx); +/// void log(state_log_level lvl, const char* msg, void* ctx); /// /// Can be called with callback set to NULL to clear an existing logger. /// @@ -133,7 +140,7 @@ LIBSESSION_EXPORT bool state_load( /// - `callback` -- [in] Callback function /// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused LIBSESSION_EXPORT void state_set_logger( - state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx); + state_object* state, void (*callback)(state_log_level, const char*, void*), void* ctx); /// API: state/state_set_send_callback /// @@ -225,7 +232,7 @@ LIBSESSION_EXPORT bool state_merge( const char* pubkey_hex_, state_config_message* configs, size_t count, - config_string_list** successful_hashes); + session_string_list** successful_hashes); /// API: state/state_current_hashes /// @@ -238,7 +245,7 @@ LIBSESSION_EXPORT bool state_merge( /// bytes). Required for group hashes. /// - `current_hashes` -- [out] Pointer to an array of the current config hashes LIBSESSION_EXPORT bool state_current_hashes( - state_object* state, const char* pubkey_hex_, config_string_list** current_hashes); + state_object* state, const char* pubkey_hex_, session_string_list** current_hashes); /// API: state/state_current_hashes /// @@ -251,7 +258,7 @@ LIBSESSION_EXPORT bool state_current_hashes( /// bytes). Required for group hashes. /// - `current_hashes` -- [out] Pointer to an array of the current config hashes LIBSESSION_EXPORT bool state_current_hashes( - state_object* state, const char* pubkey_hex_, config_string_list** current_hashes); + state_object* state, const char* pubkey_hex_, session_string_list** current_hashes); /// API: state/state_current_seqno /// @@ -380,14 +387,14 @@ LIBSESSION_EXPORT bool state_get_keys( /// /// Inputs: /// - `state` -- [in] Pointer to the state object -/// - `callback` -- [in] callback to be called with the `mutable_state_user_object` in order to +/// - `callback` -- [in] callback to be called with the `mutable_user_state_object` in order to /// modify the user state. /// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused /// /// Outputs: /// - `bool` -- Whether the mutation succeeded or not LIBSESSION_EXPORT bool state_mutate_user( - state_object* state, void (*callback)(mutable_state_user_object*, void*), void* ctx); + state_object* state, void (*callback)(mutable_user_state_object*, void*), void* ctx); /// API: state/state_mutate_group /// @@ -399,7 +406,7 @@ LIBSESSION_EXPORT bool state_mutate_user( /// Inputs: /// - `state` -- [in] Pointer to the state object /// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) -/// - `callback` -- [in] callback to be called with the `mutable_state_group_object` in order to +/// - `callback` -- [in] callback to be called with the `mutable_group_state_object` in order to /// modify the group state. /// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused /// @@ -408,7 +415,7 @@ LIBSESSION_EXPORT bool state_mutate_user( LIBSESSION_EXPORT bool state_mutate_group( state_object* state, const char* pubkey_hex, - void (*callback)(mutable_state_group_object*, void*), + void (*callback)(mutable_group_state_object*, void*), void* ctx); /// API: state/mutable_state_user_set_error_if_empty @@ -420,7 +427,7 @@ LIBSESSION_EXPORT bool state_mutate_group( /// - `err` -- [in] the error value to store in the state /// - `err_len` -- [in] length of 'err' LIBSESSION_EXPORT void mutable_state_user_set_error_if_empty( - mutable_state_user_object* state, const char* err, size_t err_len); + mutable_user_state_object* state, const char* err, size_t err_len); /// API: state/mutable_state_group_set_error_if_empty /// @@ -431,7 +438,7 @@ LIBSESSION_EXPORT void mutable_state_user_set_error_if_empty( /// - `err` -- [in] the error value to store in the state /// - `err_len` -- [in] length of 'err' LIBSESSION_EXPORT void mutable_state_group_set_error_if_empty( - mutable_state_group_object* state, const char* err, size_t err_len); + mutable_group_state_object* state, const char* err, size_t err_len); #ifdef __cplusplus } // extern "C" diff --git a/include/session/state.hpp b/include/session/state.hpp index d52fda60..698abfd9 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -1,5 +1,6 @@ #pragma once +#include "config.h" #include "config/contacts.hpp" #include "config/convo_info_volatile.hpp" #include "config/groups/info.hpp" @@ -67,11 +68,11 @@ class MutableUserConfigs { class MutableGroupConfigs { private: - State* parent_state; + State& parent_state; public: MutableGroupConfigs( - State* state, + State& state, session::config::groups::Info& info, session::config::groups::Members& members, session::config::groups::Keys& keys, @@ -83,6 +84,13 @@ class MutableGroupConfigs { session::config::groups::Keys& keys; std::optional> set_error; + std::chrono::milliseconds get_network_offset() const; + void manual_send( + std::string pubkey_hex, + ustring payload, + std::function + received_response) const; + ~MutableGroupConfigs(); }; @@ -146,7 +154,7 @@ class State { std::unique_ptr _config_convo_info_volatile; std::unique_ptr _config_user_groups; std::unique_ptr _config_user_profile; - std::map> _config_groups; + std::map> _config_groups; protected: Ed25519PubKey _user_pk; @@ -199,7 +207,7 @@ class State { std::string prefixed_pubkey, uint64_t timestamp_ms, ustring data)> hook) { - _store = hook; + _store = std::move(hook); if (!hook) return; @@ -224,7 +232,7 @@ class State { ustring payload, std::function received_response)> hook) { - _send = hook; + _send = std::move(hook); if (!hook) return; @@ -273,6 +281,24 @@ class State { bool allow_store = true, bool allow_send = true); + /// API: state/State::manual_send + /// + /// This allows for manually triggering the `_send` hook as there are some operations (eg. + /// supplement group keys) which won't be detected as changes and need to be explicitly sent. + /// + /// Inputs: + /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. + /// - `payload` -- payload which should be sent to the API. + /// - `received_response` -- callback which should be called with the response from the send + /// request. + /// + /// Outputs: None + void manual_send( + std::string pubkey_hex, + ustring payload, + std::function + received_response) const; + /// API: state/State::merge /// /// This takes all of the messages pulled down from the server and does whatever is necessary to @@ -377,8 +403,10 @@ class State { std::optional description, std::optional pic, std::vector members, - std::function - callback); + std::function< + void(std::string_view group_id, + ustring_view group_sk, + std::optional error)> callback); void approve_group(std::string_view group_id, std::optional group_sk); @@ -415,7 +443,6 @@ class State { bool success, uint16_t status_code, ustring response); - void validate_group_pubkey(std::string_view pubkey_hex) const; }; inline State& unbox(state_object* state) { @@ -426,11 +453,11 @@ inline const State& unbox(const state_object* state) { assert(state && state->internals); return *static_cast(state->internals); } -inline MutableUserConfigs& unbox(mutable_state_user_object* state) { +inline MutableUserConfigs& unbox(mutable_user_state_object* state) { assert(state && state->internals); return *static_cast(state->internals); } -inline MutableGroupConfigs& unbox(mutable_state_group_object* state) { +inline MutableGroupConfigs& unbox(mutable_group_state_object* state) { assert(state && state->internals); return *static_cast(state->internals); } diff --git a/include/session/state_groups.h b/include/session/state_groups.h index 13522144..72ca5663 100644 --- a/include/session/state_groups.h +++ b/include/session/state_groups.h @@ -8,21 +8,27 @@ extern "C" { #include #include -#include "config/base.h" #include "config/groups/members.h" #include "config/namespaces.h" #include "config/profile_pic.h" #include "export.h" +#include "state.h" LIBSESSION_EXPORT void state_create_group( state_object* state, const char* name, + size_t name_len, const char* description, + size_t description_len, const user_profile_pic pic_, - const config_group_member* members_, + const state_group_member* members_, const size_t members_len, void (*callback)( - bool success, const char* group_id, unsigned const char* group_sk, void* ctx), + const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx), void* ctx); LIBSESSION_EXPORT void state_approve_group( diff --git a/include/session/util.hpp b/include/session/util.hpp index d3e06d4f..b8bc8358 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -73,6 +73,10 @@ inline std::basic_string_view to_sv(const std::array& v) { return {v.data(), N}; } +inline ustring_view operator""_usv(const char* __str, size_t __len) { + return {to_unsigned(__str), __len}; +} + inline uint64_t get_timestamp() { return std::chrono::steady_clock::now().time_since_epoch().count(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f7a482c8..cb957246 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -95,6 +95,7 @@ target_link_libraries(config common libsession::protos PRIVATE + nlohmann_json::nlohmann_json libsodium::sodium-internal libzstd::static ) diff --git a/src/config/base.cpp b/src/config/base.cpp index ae44ffcc..47bf5ff7 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -13,7 +13,6 @@ #include #include "internal.hpp" -#include "session/config/base.h" #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" #include "session/export.h" @@ -597,174 +596,4 @@ std::array ConfigSig::seed_hash(std::string_view key) const { return out; } -void set_error(config_object* conf, std::string e) { - auto& error = unbox(conf).error; - error = std::move(e); - conf->last_error = error.c_str(); -} - } // namespace session::config - -extern "C" { - -using namespace session; -using namespace session::config; - -LIBSESSION_EXPORT void config_free(config_object* conf) { - delete conf; -} - -LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf) { - return static_cast(unbox(conf)->storage_namespace()); -} - -LIBSESSION_EXPORT config_string_list* config_merge( - config_object* conf, - const char** msg_hashes, - const unsigned char** configs, - const size_t* lengths, - size_t count) { - auto& config = *unbox(conf); - std::vector> confs; - confs.reserve(count); - for (size_t i = 0; i < count; i++) - confs.emplace_back(msg_hashes[i], ustring_view{configs[i], lengths[i]}); - - return make_string_list(config.merge(confs)); -} - -LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { - return unbox(conf)->needs_push(); -} - -LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { - auto& config = *unbox(conf); - auto [seqno, data, obs] = config.push(); - - // We need to do one alloc here that holds everything: - // - the returned struct - // - pointers to the obsolete message hash strings - // - the data - // - the message hash strings - size_t buffer_size = sizeof(config_push_data) + obs.size() * sizeof(char*) + data.size(); - for (auto& o : obs) - buffer_size += o.size(); - buffer_size += obs.size(); // obs msg hash string NULL terminators - - auto* ret = static_cast(std::malloc(buffer_size)); - - ret->seqno = seqno; - - static_assert(alignof(config_push_data) >= alignof(char*)); - ret->obsolete = reinterpret_cast(ret + 1); - ret->obsolete_len = obs.size(); - - ret->config = reinterpret_cast(ret->obsolete + ret->obsolete_len); - ret->config_len = data.size(); - - std::memcpy(ret->config, data.data(), data.size()); - char* obsptr = reinterpret_cast(ret->config + ret->config_len); - for (size_t i = 0; i < obs.size(); i++) { - std::memcpy(obsptr, obs[i].c_str(), obs[i].size() + 1); - ret->obsolete[i] = obsptr; - obsptr += obs[i].size() + 1; - } - - return ret; -} - -LIBSESSION_EXPORT void config_confirm_pushed( - config_object* conf, seqno_t seqno, const char* msg_hash) { - unbox(conf)->confirm_pushed(seqno, msg_hash); -} - -LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen) { - assert(out && outlen); - auto data = unbox(conf)->dump(); - *outlen = data.size(); - *out = static_cast(std::malloc(data.size())); - std::memcpy(*out, data.data(), data.size()); -} - -LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { - return unbox(conf)->needs_dump(); -} - -LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf) { - return make_string_list(unbox(conf)->current_hashes()); -} - -LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size_t* len) { - const auto keys = unbox(conf)->get_keys(); - assert(std::count_if(keys.begin(), keys.end(), [](const auto& k) { return k.size() == 32; }) == - keys.size()); - assert(len); - *len = keys.size(); - if (keys.empty()) - return nullptr; - auto* buf = static_cast(std::malloc(32 * keys.size())); - auto* cur = buf; - for (const auto& k : keys) { - std::memcpy(cur, k.data(), 32); - cur += 32; - } - - return buf; -} - -LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}); -} -LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}, /*high_priority=*/false); -} -LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { - return unbox(conf)->clear_keys(); -} -LIBSESSION_EXPORT bool config_remove_key(config_object* conf, const unsigned char* key) { - return unbox(conf)->remove_key({key, 32}); -} -LIBSESSION_EXPORT int config_key_count(const config_object* conf) { - return unbox(conf)->key_count(); -} -LIBSESSION_EXPORT bool config_has_key(const config_object* conf, const unsigned char* key) { - return unbox(conf)->has_key({key, 32}); -} -LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i) { - return unbox(conf)->key(i).data(); -} - -LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf) { - return unbox(conf)->encryption_domain(); -} - -LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret) { - unbox(conf)->set_sig_keys({secret, 64}); -} - -LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - unbox(conf)->set_sig_pubkey({pubkey, 32}); -} - -LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { - const auto& pk = unbox(conf)->get_sig_pubkey(); - if (pk) - return pk->data(); - return nullptr; -} - -LIBSESSION_EXPORT void config_clear_sig_keys(config_object* conf) { - unbox(conf)->clear_sig_keys(); -} - -LIBSESSION_EXPORT void config_set_logger( - config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx) { - if (!callback) - unbox(conf)->logger = nullptr; - else - unbox(conf)->logger = [callback, ctx](LogLevel lvl, std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; -} - -} // extern "C" diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index f6a6ec8a..ca7a1f86 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -346,12 +346,12 @@ LIBSESSION_C_API bool state_get_or_construct_contact( } LIBSESSION_C_API void state_set_contact( - mutable_state_user_object* state, const contacts_contact* contact) { + mutable_user_state_object* state, const contacts_contact* contact) { unbox(state).contacts.set(contact_info{*contact}); } LIBSESSION_C_API bool state_erase_contact( - mutable_state_user_object* state, const char* session_id) { + mutable_user_state_object* state, const char* session_id) { try { return unbox(state).contacts.erase(session_id); } catch (...) { diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 02e83d14..e1f4867d 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -603,27 +603,27 @@ LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_legacy_group( } LIBSESSION_C_API void state_set_convo_info_volatile_1to1( - mutable_state_user_object* state, const convo_info_volatile_1to1* convo) { + mutable_user_state_object* state, const convo_info_volatile_1to1* convo) { unbox(state).convo_info_volatile.set(convo::one_to_one{*convo}); } LIBSESSION_C_API void state_set_convo_info_volatile_community( - mutable_state_user_object* state, const convo_info_volatile_community* convo) { + mutable_user_state_object* state, const convo_info_volatile_community* convo) { unbox(state).convo_info_volatile.set(convo::community{*convo}); } LIBSESSION_C_API void state_set_convo_info_volatile_group( - mutable_state_user_object* state, const convo_info_volatile_group* convo) { + mutable_user_state_object* state, const convo_info_volatile_group* convo) { unbox(state).convo_info_volatile.set(convo::group{*convo}); } LIBSESSION_C_API void state_set_convo_info_volatile_legacy_group( - mutable_state_user_object* state, const convo_info_volatile_legacy_group* convo) { + mutable_user_state_object* state, const convo_info_volatile_legacy_group* convo) { unbox(state).convo_info_volatile.set(convo::legacy_group{*convo}); } LIBSESSION_C_API bool state_erase_convo_info_volatile_1to1( - mutable_state_user_object* state, const char* session_id) { + mutable_user_state_object* state, const char* session_id) { try { return unbox(state).convo_info_volatile.erase_1to1(session_id); } catch (...) { @@ -631,7 +631,7 @@ LIBSESSION_C_API bool state_erase_convo_info_volatile_1to1( } } LIBSESSION_C_API bool state_erase_convo_info_volatile_community( - mutable_state_user_object* state, const char* base_url, const char* room) { + mutable_user_state_object* state, const char* base_url, const char* room) { try { return unbox(state).convo_info_volatile.erase_community(base_url, room); } catch (...) { @@ -639,7 +639,7 @@ LIBSESSION_C_API bool state_erase_convo_info_volatile_community( } } LIBSESSION_C_API bool state_erase_convo_info_volatile_group( - mutable_state_user_object* state, const char* group_id) { + mutable_user_state_object* state, const char* group_id) { try { return unbox(state).convo_info_volatile.erase_group(group_id); } catch (...) { @@ -647,7 +647,7 @@ LIBSESSION_C_API bool state_erase_convo_info_volatile_group( } } LIBSESSION_C_API bool state_erase_convo_info_volatile_legacy_group( - mutable_state_user_object* state, const char* group_id) { + mutable_user_state_object* state, const char* group_id) { try { return unbox(state).convo_info_volatile.erase_legacy_group(group_id); } catch (...) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 76436509..f987424e 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -128,10 +128,10 @@ LIBSESSION_C_API const size_t GROUP_INFO_NAME_MAX_LENGTH = groups::Info::NAME_MA LIBSESSION_C_API const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH = groups::Info::DESCRIPTION_MAX_LENGTH; -LIBSESSION_C_API bool state_get_groups_info_name( - const state_object* state, const char* pubkey_hex, char* name) { +LIBSESSION_C_API bool state_get_group_name( + const state_object* state, const char* group_id, char* name) { try { - if (auto s = unbox(state).config({pubkey_hex, 66}).get_name()) { + if (auto s = unbox(state).config({group_id, 66}).get_name()) { std::string res = {s->data(), s->size()}; if (res.size() > groups::Info::NAME_MAX_LENGTH) res.resize(groups::Info::NAME_MAX_LENGTH); @@ -143,15 +143,14 @@ LIBSESSION_C_API bool state_get_groups_info_name( return false; } -LIBSESSION_C_API void state_set_groups_info_name( - mutable_state_group_object* state, const char* name) { +LIBSESSION_C_API void state_set_group_name(mutable_group_state_object* state, const char* name) { unbox(state).info.set_name(name); } -LIBSESSION_C_API bool state_get_groups_info_description( - const state_object* state, const char* pubkey_hex, char* description) { +LIBSESSION_C_API bool state_get_group_description( + const state_object* state, const char* group_id, char* description) { try { - if (auto s = unbox(state).config({pubkey_hex, 66}).get_description()) { + if (auto s = unbox(state).config({group_id, 66}).get_description()) { std::string res = {s->data(), s->size()}; if (res.size() > groups::Info::DESCRIPTION_MAX_LENGTH) res.resize(groups::Info::DESCRIPTION_MAX_LENGTH); @@ -163,15 +162,15 @@ LIBSESSION_C_API bool state_get_groups_info_description( return false; } -LIBSESSION_C_API void state_set_groups_info_description( - mutable_state_group_object* state, const char* description) { +LIBSESSION_C_API void state_set_group_description( + mutable_group_state_object* state, const char* description) { unbox(state).info.set_description(description); } -LIBSESSION_C_API bool state_get_groups_info_pic( - const state_object* state, const char* pubkey_hex, user_profile_pic* pic) { +LIBSESSION_C_API bool state_get_group_pic( + const state_object* state, const char* group_id, user_profile_pic* pic) { try { - if (auto p = unbox(state).config({pubkey_hex, 66}).get_profile_pic()) { + if (auto p = unbox(state).config({group_id, 66}).get_profile_pic()) { copy_c_str(pic->url, p.url); std::memcpy(pic->key, p.key.data(), 32); return true; @@ -181,8 +180,7 @@ LIBSESSION_C_API bool state_get_groups_info_pic( return false; } -LIBSESSION_C_API void state_set_groups_info_pic( - mutable_state_group_object* state, user_profile_pic pic) { +LIBSESSION_C_API void state_set_group_pic(mutable_group_state_object* state, user_profile_pic pic) { std::string_view url{pic.url}; ustring_view key; if (!url.empty()) @@ -191,11 +189,11 @@ LIBSESSION_C_API void state_set_groups_info_pic( unbox(state).info.set_profile_pic(url, key); } -LIBSESSION_C_API bool state_get_groups_info_expiry_timer( - const state_object* state, const char* pubkey_hex, int* timer) { +LIBSESSION_C_API bool state_get_group_expiry_timer( + const state_object* state, const char* group_id, int* timer) { try { *timer = unbox(state) - .config({pubkey_hex, 66}) + .config({group_id, 66}) .get_expiry_timer() .value_or(0s) .count(); @@ -205,46 +203,44 @@ LIBSESSION_C_API bool state_get_groups_info_expiry_timer( return false; } -LIBSESSION_C_API void state_set_groups_info_expiry_timer( - mutable_state_group_object* state, int expiry) { +LIBSESSION_C_API void state_set_group_expiry_timer(mutable_group_state_object* state, int expiry) { unbox(state).info.set_expiry_timer(std::max(0, expiry) * 1s); } -LIBSESSION_C_API bool state_get_groups_info_created( - const state_object* state, const char* pubkey_hex, int64_t* created) { +LIBSESSION_C_API bool state_get_group_created( + const state_object* state, const char* group_id, int64_t* created) { try { - *created = unbox(state).config({pubkey_hex, 66}).get_created().value_or(0); + *created = unbox(state).config({group_id, 66}).get_created().value_or(0); return true; } catch (...) { } return false; } -LIBSESSION_C_API void groups_info_set_created(mutable_state_group_object* state, int64_t ts) { +LIBSESSION_C_API void state_set_group_created(mutable_group_state_object* state, int64_t ts) { unbox(state).info.set_created(std::max(0, ts)); } -LIBSESSION_C_API bool state_get_groups_info_delete_before( - const state_object* state, const char* pubkey_hex, int64_t* delete_before) { +LIBSESSION_C_API bool state_get_group_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before) { try { *delete_before = - unbox(state).config({pubkey_hex, 66}).get_delete_before().value_or(0); + unbox(state).config({group_id, 66}).get_delete_before().value_or(0); return true; } catch (...) { } return false; } -LIBSESSION_C_API void state_set_groups_info_delete_before( - mutable_state_group_object* state, int64_t ts) { +LIBSESSION_C_API void state_set_group_delete_before(mutable_group_state_object* state, int64_t ts) { unbox(state).info.set_delete_before(std::max(0, ts)); } -LIBSESSION_C_API bool state_get_groups_info_attach_delete_before( - const state_object* state, const char* pubkey_hex, int64_t* delete_before) { +LIBSESSION_C_API bool state_get_group_attach_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before) { try { *delete_before = unbox(state) - .config({pubkey_hex, 66}) + .config({group_id, 66}) .get_delete_attach_before() .value_or(0); return true; @@ -253,15 +249,14 @@ LIBSESSION_C_API bool state_get_groups_info_attach_delete_before( return false; } -LIBSESSION_C_API void state_set_groups_info_attach_delete_before( - mutable_state_group_object* state, int64_t ts) { +LIBSESSION_C_API void state_set_group_attach_delete_before( + mutable_group_state_object* state, int64_t ts) { unbox(state).info.set_delete_attach_before(std::max(0, ts)); } -LIBSESSION_C_API bool state_groups_info_is_destroyed( - const state_object* state, const char* pubkey_hex) { +LIBSESSION_C_API bool state_group_is_destroyed(const state_object* state, const char* group_id) { try { - if (unbox(state).config({pubkey_hex, 66}).is_destroyed()) { + if (unbox(state).config({group_id, 66}).is_destroyed()) { return true; } } catch (...) { @@ -269,7 +264,7 @@ LIBSESSION_C_API bool state_groups_info_is_destroyed( return false; } -LIBSESSION_C_API void state_destroy_group(mutable_state_group_object* state) { +LIBSESSION_C_API void state_destroy_group(mutable_group_state_object* state) { unbox(state).info.destroy_group(); } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3b034ab..f8d3c76a 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -22,6 +23,8 @@ #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" #include "session/multi_encrypt.hpp" +#include "session/state.h" +#include "session/state.hpp" #include "session/xed25519.hpp" using namespace std::literals; @@ -538,6 +541,39 @@ ustring Keys::key_supplement(const std::vector& sids) const { return ustring{to_unsigned_sv(d.view())}; } +std::pair Keys::prepare_supplement_payload( + ustring supplement_msg, std::chrono::milliseconds timestamp) const { + if (!admin()) + throw std::runtime_error{"prepare_supplement_payload: Failed to sign; user is not admin"}; + if (!_sign_pk) + throw std::runtime_error{"prepare_supplement_payload: Missing group pubkey"}; + + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store") + static_cast(storage_namespace()) + + static_cast(timestamp.count()); + + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), _sign_sk.data())) + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + + auto group_id = "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()); + nlohmann::json params{ + {"namespace", storage_namespace()}, + {"pubkey", group_id}, + {"ttl", default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(supplement_msg)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + nlohmann::json request_json{{"method", "store"}, {"params", params}}; + + return {group_id, to_unsigned(request_json.dump())}; +} + // Blinding factor for subaccounts: H(sessionid || groupid) mod L, where H is 64-byte blake2b, using // a hash key derived from the group's seed. std::array Keys::subaccount_blind_factor( @@ -1371,270 +1407,149 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c using namespace session; using namespace session::config; +using namespace session::state; -namespace { -groups::Keys& unbox(config_group_keys* conf) { - assert(conf && conf->internals); - return *static_cast(conf->internals); -} -const groups::Keys& unbox(const config_group_keys* conf) { - assert(conf && conf->internals); - return *static_cast(conf->internals); -} - -void set_error(config_group_keys* conf, std::string_view e) { - if (e.size() > 255) - e.remove_suffix(e.size() - 255); - std::memcpy(conf->_error_buf, e.data(), e.size()); - conf->_error_buf[e.size()] = 0; - conf->last_error = conf->_error_buf; -} -} // namespace - -LIBSESSION_C_API int groups_keys_init( - config_group_keys** conf, - const unsigned char* user_ed25519_secretkey, - const unsigned char* group_ed25519_pubkey, - const unsigned char* group_ed25519_secretkey, - config_object* cinfo, - config_object* cmembers, - const unsigned char* dump, - size_t dumplen, - char* error) { - - assert(user_ed25519_secretkey && group_ed25519_pubkey && cinfo && cmembers); - - ustring_view user_sk{user_ed25519_secretkey, 64}; - ustring_view group_pk{group_ed25519_pubkey, 32}; - std::optional group_sk; - if (group_ed25519_secretkey) - group_sk.emplace(group_ed25519_secretkey, 64); - std::optional dumped; - if (dump && dumplen) - dumped.emplace(dump, dumplen); - - auto& info = *unbox(cinfo); - auto& members = *unbox(cmembers); - auto c_conf = std::make_unique(); +extern "C" { +LIBSESSION_C_API size_t state_size_group_keys(const state_object* state, const char* group_id) { try { - c_conf->internals = new groups::Keys{user_sk, group_pk, group_sk, dumped, info, members}; - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return SESSION_ERR_INVALID_DUMP; + return unbox(state).config(group_id).size(); + } catch (...) { + return 0; } - - c_conf->last_error = nullptr; - *conf = c_conf.release(); - return SESSION_ERR_NONE; -} - -LIBSESSION_C_API size_t groups_keys_size(const config_group_keys* conf) { - return unbox(conf).size(); -} - -LIBSESSION_C_API const unsigned char* group_keys_get_key(const config_group_keys* conf, size_t N) { - auto keys = unbox(conf).group_keys(); - if (N >= keys.size()) - return nullptr; - return keys[N].data(); } -LIBSESSION_C_API bool groups_keys_is_admin(const config_group_keys* conf) { - return unbox(conf).admin(); -} - -LIBSESSION_C_API bool groups_keys_load_admin_key( - config_group_keys* conf, - const unsigned char* secret, - config_object* info, - config_object* members) { +LIBSESSION_C_API const unsigned char* state_get_group_key( + const state_object* state, const char* group_id, size_t N) { try { - unbox(conf).load_admin_key( - ustring_view{secret, 32}, - *unbox(info), - *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + auto keys = unbox(state).config(group_id).group_keys(); + if (N >= keys.size()) + return nullptr; + return keys[N].data(); + } catch (...) { + return nullptr; } - return true; } -LIBSESSION_C_API bool groups_keys_rekey( - config_group_keys* conf, - config_object* info, - config_object* members, - const unsigned char** out, - size_t* outlen) { - assert(info && members && out && outlen); - auto& keys = unbox(conf); - ustring_view to_push; +LIBSESSION_C_API bool state_is_group_admin(const state_object* state, const char* group_id) { try { - to_push = keys.rekey(*unbox(info), *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); + return unbox(state).config(group_id).admin(); + } catch (...) { return false; } - *out = to_push.data(); - *outlen = to_push.size(); - return true; } -LIBSESSION_C_API bool groups_keys_pending_config( - const config_group_keys* conf, const unsigned char** out, size_t* outlen) { - assert(out && outlen); - if (auto pending = unbox(conf).pending_config()) { - *out = pending->data(); - *outlen = pending->size(); - return true; - } - return false; -} - -LIBSESSION_C_API bool groups_keys_load_message( - config_group_keys* conf, - const char* msg_hash, - const unsigned char* data, - size_t datalen, - int64_t timestamp_ms, - config_object* info, - config_object* members) { - assert(data && info && members); +LIBSESSION_C_API bool state_load_group_admin_key( + mutable_group_state_object* state, const unsigned char* secret) { try { - unbox(conf).load_key_message( - msg_hash, - ustring_view{data, datalen}, - timestamp_ms, - *unbox(info), - *unbox(members)); + unbox(state).keys.load_admin_key( + ustring_view{secret, 32}, unbox(state).info, unbox(state).members); + return true; } catch (const std::exception& e) { - set_error(conf, e.what()); + if (auto set_error = unbox(state).set_error; set_error.has_value()) + set_error.value()(e.what()); return false; } - return true; -} - -LIBSESSION_C_API config_string_list* groups_keys_current_hashes(const config_group_keys* conf) { - return make_string_list(unbox(conf).current_hashes()); } -LIBSESSION_C_API bool groups_keys_needs_rekey(const config_group_keys* conf) { - return unbox(conf).needs_rekey(); -} - -LIBSESSION_C_API bool groups_keys_needs_dump(const config_group_keys* conf) { - return unbox(conf).needs_dump(); -} - -LIBSESSION_C_API void groups_keys_dump( - config_group_keys* conf, unsigned char** out, size_t* outlen) { - assert(out && outlen); - auto dump = unbox(conf).dump(); - *out = static_cast(std::malloc(dump.size())); - std::memcpy(*out, dump.data(), dump.size()); - *outlen = dump.size(); -} - -LIBSESSION_C_API void groups_keys_encrypt_message( - const config_group_keys* conf, - const unsigned char* plaintext_in, - size_t plaintext_len, - unsigned char** ciphertext_out, - size_t* ciphertext_len) { - assert(plaintext_in && ciphertext_out && ciphertext_len); - - ustring ciphertext; +LIBSESSION_C_API bool state_group_needs_rekey(const state_object* state, const char* group_id) { try { - ciphertext = unbox(conf).encrypt_message(ustring_view{plaintext_in, plaintext_len}); - *ciphertext_out = static_cast(std::malloc(ciphertext.size())); - std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); - *ciphertext_len = ciphertext.size(); + return unbox(state).config(group_id).needs_rekey(); } catch (...) { - *ciphertext_out = nullptr; - *ciphertext_len = 0; + return false; } } -LIBSESSION_C_API bool groups_keys_decrypt_message( - config_group_keys* conf, - const unsigned char* ciphertext_in, - size_t ciphertext_len, - char* session_id, - unsigned char** plaintext_out, - size_t* plaintext_len) { - assert(ciphertext_in && plaintext_out && plaintext_len); - +LIBSESSION_C_API bool state_rekey_group(mutable_group_state_object* state) { try { - auto [sid, plaintext] = - unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); - std::memcpy(session_id, sid.c_str(), sid.size() + 1); - *plaintext_out = static_cast(std::malloc(plaintext.size())); - std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); - *plaintext_len = plaintext.size(); + unbox(state).keys.rekey(unbox(state).info, unbox(state).members); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); + if (auto set_error = unbox(state).set_error; set_error.has_value()) + set_error.value()(e.what()); + return false; } - return false; } -LIBSESSION_C_API bool groups_keys_key_supplement( - config_group_keys* conf, +LIBSESSION_C_API void state_supplement_group_key( + mutable_group_state_object* state, const char** sids, size_t sids_len, - unsigned char** message, - size_t* message_len) { - assert(sids && message && message_len); - + void (*callback)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* ctx), + void* ctx) { + assert(sids); std::vector session_ids; for (size_t i = 0; i < sids_len; i++) session_ids.emplace_back(sids[i]); + try { - auto msg = unbox(conf).key_supplement(session_ids); - *message = static_cast(malloc(msg.size())); - *message_len = msg.size(); - std::memcpy(*message, msg.data(), msg.size()); - return true; + auto msg = unbox(state).keys.key_supplement(session_ids); + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + unbox(state).get_network_offset()); + auto [pubkey, payload] = unbox(state).keys.prepare_supplement_payload(msg, timestamp); + + unbox(state).manual_send( + pubkey, + payload, + [callback, ctx](bool success, int16_t status_code, ustring response) { + if (callback) + callback(success, status_code, response.data(), response.size(), ctx); + }); } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + if (auto set_error = unbox(state).set_error; set_error.has_value()) + set_error.value()(e.what()); + if (callback) + callback(false, -1, nullptr, 0, ctx); } } -LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { - return unbox(conf).current_generation(); +LIBSESSION_EXPORT int state_get_current_group_generation( + const state_object* state, const char* group_id) { + try { + return unbox(state).config(group_id).current_generation(); + } catch (...) { + return 0; + } } -LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( - config_group_keys* conf, +LIBSESSION_C_API bool state_make_group_swarm_subaccount_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* sign_value) { + unsigned char* sign_value, + char* error) { assert(sign_value); try { - auto val = unbox(conf).swarm_make_subaccount(session_id, write, del); + auto val = unbox(state).config(group_id).swarm_make_subaccount( + session_id, write, del); assert(val.size() == 100); std::memcpy(sign_value, val.data(), val.size()); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_make_subaccount( - config_group_keys* conf, const char* session_id, unsigned char* sign_value) { - return groups_keys_swarm_make_subaccount_flags(conf, session_id, true, false, sign_value); +LIBSESSION_C_API bool state_make_group_swarm_subaccount( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* sign_value, + char* error) { + return state_make_group_swarm_subaccount_flags( + state, group_id, session_id, true, false, sign_value, error); } -LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( +LIBSESSION_C_API bool verify_group_swarm_subaccount_flags( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value, @@ -1652,7 +1567,7 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( } } -LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( +LIBSESSION_C_API bool verify_group_swarm_subaccount( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value) { @@ -1662,18 +1577,20 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( ustring_view{signing_value, 100}); } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( - config_group_keys* conf, +LIBSESSION_C_API bool state_sign_group_swarm_subaccount( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, char* subaccount, char* subaccount_sig, - char* signature) { + char* signature, + char* error) { assert(msg && signing_value && subaccount && subaccount_sig && signature); try { - auto auth = unbox(conf).swarm_subaccount_sign( + auto auth = unbox(state).config(group_id).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); assert(auth.subaccount.size() == 48); assert(auth.subaccount_sig.size() == 88); @@ -1683,23 +1600,24 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( std::memcpy(signature, auth.signature.c_str(), auth.signature.size() + 1); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( - config_group_keys* conf, +LIBSESSION_C_API bool state_sign_group_swarm_subaccount_binary( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, unsigned char* subaccount, unsigned char* subaccount_sig, - unsigned char* signature) { + unsigned char* signature, + char* error) { assert(msg && signing_value && subaccount && subaccount_sig && signature); try { - auto auth = unbox(conf).swarm_subaccount_sign( + auto auth = unbox(state).config(group_id).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}, true); assert(auth.subaccount.size() == 36); assert(auth.subaccount_sig.size() == 64); @@ -1709,29 +1627,83 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( std::memcpy(signature, auth.signature.data(), 64); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( - config_group_keys* conf, +LIBSESSION_C_API bool state_get_group_swarm_subaccount_token_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* token) { + unsigned char* token, + char* error) { try { - auto tok = unbox(conf).swarm_subaccount_token(session_id, write, del); + auto tok = unbox(state).config(group_id).swarm_subaccount_token( + session_id, write, del); assert(tok.size() == 36); std::memcpy(token, tok.data(), 36); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API bool state_get_group_swarm_subaccount_token( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* token, + char* error) { + return state_get_group_swarm_subaccount_token_flags( + state, group_id, session_id, true, false, token, error); +} + +LIBSESSION_C_API void state_encrypt_group_message( + const state_object* state, + const char* group_id, + const unsigned char* plaintext_in, + size_t plaintext_len, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { + assert(plaintext_in && ciphertext_out && ciphertext_len); + + ustring ciphertext; + try { + ciphertext = unbox(state).config(group_id).encrypt_message( + ustring_view{plaintext_in, plaintext_len}); + *ciphertext_out = static_cast(std::malloc(ciphertext.size())); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + *ciphertext_len = ciphertext.size(); + } catch (...) { + *ciphertext_out = nullptr; + *ciphertext_len = 0; } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_token( - config_group_keys* conf, const char* session_id, unsigned char* token) { - return groups_keys_swarm_subaccount_token_flags(conf, session_id, true, false, token); +LIBSESSION_C_API bool state_decrypt_group_message( + const state_object* state, + const char* group_id, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + char* session_id, + unsigned char** plaintext_out, + size_t* plaintext_len, + char* error) { + assert(ciphertext_in && plaintext_out && plaintext_len); + + try { + auto [sid, plaintext] = unbox(state).config(group_id).decrypt_message( + ustring_view{ciphertext_in, ciphertext_len}); + std::memcpy(session_id, sid.c_str(), sid.size() + 1); + *plaintext_out = static_cast(std::malloc(plaintext.size())); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + *plaintext_len = plaintext.size(); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } } + +} // extern "C" diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 204996c4..b033452c 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -134,7 +134,7 @@ member::member(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } -member::member(const config_group_member& m) : session_id{m.session_id, 66} { +member::member(const state_group_member& m) : session_id{m.session_id, 66} { assert(std::strlen(m.name) <= MAX_NAME_LENGTH); name = m.name; assert(std::strlen(m.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); @@ -151,7 +151,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { supplement = m.supplement; } -void member::into(config_group_member& m) const { +void member::into(state_group_member& m) const { std::memcpy(m.session_id, session_id.data(), 67); copy_c_str(m.name, name); if (profile_picture) { @@ -185,12 +185,12 @@ extern "C" { LIBSESSION_C_API bool state_get_group_member( const state_object* state, - const char* pubkey_hex, - config_group_member* member, + const char* group_id, + state_group_member* member, const char* session_id, char* error) { try { - if (auto c = unbox(state).config(pubkey_hex).get(session_id)) { + if (auto c = unbox(state).config(group_id).get(session_id)) { c->into(*member); return true; } @@ -202,12 +202,12 @@ LIBSESSION_C_API bool state_get_group_member( LIBSESSION_C_API bool state_get_or_construct_group_member( const state_object* state, - const char* pubkey_hex, - config_group_member* member, + const char* group_id, + state_group_member* member, const char* session_id, char* error) { try { - unbox(state).config(pubkey_hex).get_or_construct(session_id).into(*member); + unbox(state).config(group_id).get_or_construct(session_id).into(*member); return true; } catch (const std::exception& e) { set_error_value(error, e.what()); @@ -216,12 +216,12 @@ LIBSESSION_C_API bool state_get_or_construct_group_member( } LIBSESSION_C_API void state_set_group_member( - mutable_state_group_object* state, const config_group_member* member) { + mutable_group_state_object* state, const state_group_member* member) { unbox(state).members.set(groups::member{*member}); } LIBSESSION_C_API bool state_erase_group_member( - mutable_state_group_object* state, const char* session_id) { + mutable_group_state_object* state, const char* session_id) { try { return unbox(state).members.erase(session_id); } catch (...) { @@ -229,20 +229,19 @@ LIBSESSION_C_API bool state_erase_group_member( } } -LIBSESSION_C_API size_t -state_size_group_members(const state_object* state, const char* pubkey_hex) { +LIBSESSION_C_API size_t state_size_group_members(const state_object* state, const char* group_id) { try { - return unbox(state).config(pubkey_hex).size(); + return unbox(state).config(group_id).size(); } catch (...) { return 0; } } LIBSESSION_C_API groups_members_iterator* groups_members_iterator_new( - const state_object* state, const char* pubkey_hex) { + const state_object* state, const char* group_id) { auto* it = new groups_members_iterator{}; it->_internals = - new groups::Members::iterator{unbox(state).config(pubkey_hex).begin()}; + new groups::Members::iterator{unbox(state).config(group_id).begin()}; return it; } @@ -252,7 +251,7 @@ LIBSESSION_C_API void groups_members_iterator_free(groups_members_iterator* it) } LIBSESSION_C_API bool groups_members_iterator_done( - groups_members_iterator* it, config_group_member* c) { + groups_members_iterator* it, state_group_member* c) { auto& real = *static_cast(it->_internals); if (real.done()) return true; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 8872ea4c..b0d333c4 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -7,73 +7,13 @@ #include #include -#include "session/config/base.h" +#include "session/config.h" #include "session/config/base.hpp" #include "session/config/error.h" #include "session/types.hpp" namespace session::config { -template -[[nodiscard]] int c_wrapper_init_generic(config_object** conf, char* error, Args&&... args) { - auto c = std::make_unique>(); - auto c_conf = std::make_unique(); - - try { - c->config = std::make_unique(std::forward(args)...); - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return SESSION_ERR_INVALID_DUMP; - } - - c_conf->internals = c.release(); - c_conf->last_error = nullptr; - *conf = c_conf.release(); - return SESSION_ERR_NONE; -} - -template -[[nodiscard]] int c_wrapper_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - assert(ed25519_secretkey_bytes); - ustring_view ed25519_secretkey{ed25519_secretkey_bytes, 64}; - std::optional dump; - if (dumpstr && dumplen) - dump.emplace(dumpstr, dumplen); - return c_wrapper_init_generic(conf, error, ed25519_secretkey, dump); -} - -template -[[nodiscard]] int c_group_wrapper_init( - config_object** conf, - const unsigned char* ed25519_pubkey_bytes, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dump_bytes, - size_t dumplen, - char* error) { - - assert(ed25519_pubkey_bytes); - - ustring_view ed25519_pubkey{ed25519_pubkey_bytes, 32}; - std::optional ed25519_secretkey; - if (ed25519_secretkey_bytes) - ed25519_secretkey.emplace(ed25519_secretkey_bytes, 64); - std::optional dump; - if (dump_bytes && dumplen) - dump.emplace(dump_bytes, dumplen); - - return c_wrapper_init_generic(conf, error, ed25519_pubkey, ed25519_secretkey, dump); -} - template void copy_c_str(char (&dest)[N], std::string_view src) { if (src.size() >= N) @@ -82,34 +22,34 @@ void copy_c_str(char (&dest)[N], std::string_view src) { dest[src.size()] = 0; } -// Copies a container of std::strings into a self-contained malloc'ed config_string_list for +// Copies a container of std::strings into a self-contained malloc'ed session_string_list for // returning to C code with the strings and pointers of the string list in the same malloced space, // hanging off the end (so that everything, including string values, is freed by a single `free()`). template < typename Container, typename = std::enable_if_t>> -config_string_list* make_string_list(Container vals) { - // We malloc space for the config_string_list struct itself, plus the required number of string +session_string_list* make_string_list(Container vals) { + // We malloc space for the session_string_list struct itself, plus the required number of string // pointers to store its strings, and the space to actually contain a copy of the string data. // When we're done, the malloced memory we grab is going to look like this: // - // {config_string_list} + // {session_string_list} // {pointer1}{pointer2}... // {string data 1\0}{string data 2\0}... // - // where config_string_list.value points at the beginning of {pointer1}, and each pointerN + // where session_string_list.value points at the beginning of {pointer1}, and each pointerN // points at the beginning of the {string data N\0} c string. // // Since we malloc it all at once, when the user frees it, they also free the entire thing. - size_t sz = sizeof(config_string_list) + vals.size() * sizeof(char*); + size_t sz = sizeof(session_string_list) + vals.size() * sizeof(char*); // plus, for each string, the space to store it (including the null) for (auto& v : vals) sz += v.size() + 1; - auto* ret = static_cast(std::malloc(sz)); + auto* ret = static_cast(std::malloc(sz)); ret->len = vals.size(); - static_assert(alignof(config_string_list) >= alignof(char*)); + static_assert(alignof(session_string_list) >= alignof(char*)); // value points at the space immediately after the struct itself, which is the first element in // the array of c string pointers. diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index d72b33ad..c2dc96ce 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -723,27 +723,27 @@ LIBSESSION_C_API bool state_get_or_construct_ugroups_legacy_group( } LIBSESSION_C_API void state_set_ugroups_community( - mutable_state_user_object* state, const ugroups_community_info* comm) { + mutable_user_state_object* state, const ugroups_community_info* comm) { unbox(state).user_groups.set(community_info{*comm}); } LIBSESSION_C_API void state_set_ugroups_group( - mutable_state_user_object* state, const ugroups_group_info* group) { + mutable_user_state_object* state, const ugroups_group_info* group) { unbox(state).user_groups.set(group_info{*group}); } LIBSESSION_C_API void state_set_ugroups_legacy_group( - mutable_state_user_object* state, const ugroups_legacy_group_info* group) { + mutable_user_state_object* state, const ugroups_legacy_group_info* group) { unbox(state).user_groups.set(legacy_group_info{*group}); } LIBSESSION_C_API void state_set_free_ugroups_legacy_group( - mutable_state_user_object* state, ugroups_legacy_group_info* group) { + mutable_user_state_object* state, ugroups_legacy_group_info* group) { unbox(state).user_groups.set(legacy_group_info{std::move(*group)}); } LIBSESSION_C_API bool state_erase_ugroups_community( - mutable_state_user_object* state, const char* base_url, const char* room) { + mutable_user_state_object* state, const char* base_url, const char* room) { try { return unbox(state).user_groups.erase_community(base_url, room); } catch (...) { @@ -752,7 +752,7 @@ LIBSESSION_C_API bool state_erase_ugroups_community( } LIBSESSION_C_API bool state_erase_ugroups_group( - mutable_state_user_object* state, const char* group_id) { + mutable_user_state_object* state, const char* group_id) { try { return unbox(state).user_groups.erase_group(group_id); } catch (...) { @@ -761,7 +761,7 @@ LIBSESSION_C_API bool state_erase_ugroups_group( } LIBSESSION_C_API bool state_erase_ugroups_legacy_group( - mutable_state_user_object* state, const char* group_id) { + mutable_user_state_object* state, const char* group_id) { try { return unbox(state).user_groups.erase_legacy_group(group_id); } catch (...) { diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 069a6d8d..5eebd001 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -84,7 +84,7 @@ LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { return nullptr; } -LIBSESSION_C_API void state_set_profile_name(mutable_state_user_object* state, const char* name) { +LIBSESSION_C_API void state_set_profile_name(mutable_user_state_object* state, const char* name) { unbox(state).user_profile.set_name(name); } @@ -100,7 +100,7 @@ LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* stat } LIBSESSION_C_API void state_set_profile_pic( - mutable_state_user_object* state, user_profile_pic pic) { + mutable_user_state_object* state, user_profile_pic pic) { std::string_view url{pic.url}; ustring_view key; if (!url.empty()) @@ -114,7 +114,7 @@ LIBSESSION_C_API int state_get_profile_nts_priority(const state_object* state) { } LIBSESSION_C_API void state_set_profile_nts_priority( - mutable_state_user_object* state, int priority) { + mutable_user_state_object* state, int priority) { unbox(state).user_profile.set_nts_priority(priority); } @@ -122,7 +122,7 @@ LIBSESSION_C_API int state_get_profile_nts_expiry(const state_object* state) { return unbox(state).config().get_nts_expiry().value_or(0s).count(); } -LIBSESSION_C_API void state_set_profile_nts_expiry(mutable_state_user_object* state, int expiry) { +LIBSESSION_C_API void state_set_profile_nts_expiry(mutable_user_state_object* state, int expiry) { unbox(state).user_profile.set_nts_expiry(std::max(0, expiry) * 1s); } @@ -133,7 +133,7 @@ LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state } LIBSESSION_C_API void state_set_profile_blinded_msgreqs( - mutable_state_user_object* state, int enabled) { + mutable_user_state_object* state, int enabled) { std::optional val; if (enabled >= 0) val = static_cast(enabled); diff --git a/src/state.cpp b/src/state.cpp index b25ec06e..7d59f24e 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -133,6 +133,7 @@ void State::load( "Unable to retrieve group " + std::string(pubkey_hex) + " from user_groups config"}; auto pubkey = session_id_pk(pubkey_hex, "03"); + std::string gid = {pubkey_hex.data(), pubkey_hex.size()}; ustring_view pubkey_sv = to_unsigned_sv(pubkey); ustring_view user_ed25519_secretkey = {_user_sk.data(), 64}; std::optional opt_dump = dump; @@ -142,31 +143,30 @@ void State::load( group_ed25519_secretkey = {user_group_info.value().secretkey.data(), 64}; // Create a fresh `GroupConfigs` state - if (!_config_groups.count(pubkey_hex)) { + if (auto [it, b] = _config_groups.try_emplace(gid, nullptr); b) { if (namespace_ == Namespace::GroupKeys) throw std::runtime_error{ "Attempted to load groups_keys config before groups_info or groups_members " "configs"}; - _config_groups[pubkey_hex] = - std::make_unique(pubkey_sv, user_ed25519_secretkey); + _config_groups[gid] = std::make_unique(pubkey_sv, user_ed25519_secretkey); } // Reload the specified namespace with the dump if (namespace_ == Namespace::GroupInfo) { - _config_groups[pubkey_hex]->info = + _config_groups[gid]->info = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[pubkey_hex]->info); + add_child_logger(_config_groups[gid]->info); } else if (namespace_ == Namespace::GroupMembers) { - _config_groups[pubkey_hex]->members = + _config_groups[gid]->members = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[pubkey_hex]->members); + add_child_logger(_config_groups[gid]->members); } else if (namespace_ == Namespace::GroupKeys) { - auto info = _config_groups[pubkey_hex]->info.get(); - auto members = _config_groups[pubkey_hex]->members.get(); + auto info = _config_groups[gid]->info.get(); + auto members = _config_groups[gid]->members.get(); auto keys = std::make_unique( user_ed25519_secretkey, pubkey_sv, group_ed25519_secretkey, dump, *info, *members); - _config_groups[pubkey_hex]->keys = std::move(keys); + _config_groups[gid]->keys = std::move(keys); } else throw std::runtime_error{"Attempted to load unknown namespace"}; } @@ -276,11 +276,23 @@ void State::config_changed( bool success, uint16_t status_code, ustring response) { handle_config_push_response( target_pubkey_hex, push.namespace_seqno, success, status_code, response); + + // Now that we have confirmed the push we need to store the configs again + config_changed(target_pubkey_hex, true, false); }); } log(LogLevel::debug, "config_changed: Complete"); } +void State::manual_send( + std::string pubkey_hex, + ustring payload, + std::function received_response) + const { + if (_send) + _send(pubkey_hex, payload, received_response); +} + PreparedPush State::prepare_push( std::string pubkey_hex, std::chrono::milliseconds timestamp, @@ -292,6 +304,7 @@ PreparedPush State::prepare_push( for (auto& config : configs) { if (!config->needs_push()) continue; + log(LogLevel::debug, "prepare_push: generate push for " + namespace_name(config->storage_namespace()) + ", (" + pubkey_hex + ")"); @@ -517,11 +530,13 @@ std::vector State::merge( auto members = _config_groups[target_pubkey_hex]->members.get(); is_group_merge = true; - if (config.namespace_ == Namespace::GroupInfo) + if (config.namespace_ == Namespace::GroupInfo) { merged_hashes = info->merge(pending_configs); - else if (config.namespace_ == Namespace::GroupMembers) + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + } else if (config.namespace_ == Namespace::GroupMembers) { merged_hashes = members->merge(pending_configs); - else if (config.namespace_ == Namespace::GroupKeys) { + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + } else if (config.namespace_ == Namespace::GroupKeys) { // GroupKeys doesn't support merging multiple messages at once so do them individually if (_config_groups[target_pubkey_hex]->keys->load_key_message( config.hash, config.data, config.timestamp_ms, *info, *members)) { @@ -557,14 +572,12 @@ std::vector State::current_hashes(std::optional p } else { if (pubkey_hex->size() != 66) throw std::invalid_argument{"current_hashes: Invalid pubkey_hex - expected 66 bytes"}; - if (!_config_groups.count(*pubkey_hex)) - throw std::runtime_error{ - "current_hashes: Attempted to retrieve current hashes for group with no config " - "state"}; - auto info_hashes = _config_groups[*pubkey_hex]->info->current_hashes(); - auto members_hashes = _config_groups[*pubkey_hex]->members->current_hashes(); - auto keys_hashes = _config_groups[*pubkey_hex]->keys->current_hashes(); + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); + auto info_hashes = group->info->current_hashes(); + auto members_hashes = group->members->current_hashes(); + auto keys_hashes = group->keys->current_hashes(); result.insert(result.end(), info_hashes.begin(), info_hashes.end()); result.insert(result.end(), members_hashes.begin(), members_hashes.end()); result.insert(result.end(), keys_hashes.begin(), keys_hashes.end()); @@ -616,7 +629,7 @@ ustring State::dump(bool full_dump) { return session::ustring{to_unsigned_sv(to_dump)}; } -ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex_) { +ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex) { switch (namespace_) { case Namespace::Contacts: return _config_contacts->dump(); case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->dump(); @@ -625,22 +638,21 @@ ustring State::dump(config::Namespace namespace_, std::optionalsize() != 64) + if (pubkey_hex->size() != 64) throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; - if (!_config_groups.count(*pubkey_hex_)) - throw std::runtime_error{"Unable to retrieve group"}; // Retrieve the group configs for this pubkey - auto group_configs = _config_groups[*pubkey_hex_].get(); + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); switch (namespace_) { - case Namespace::GroupInfo: return group_configs->info->dump(); - case Namespace::GroupMembers: return group_configs->members->dump(); - case Namespace::GroupKeys: return group_configs->keys->dump(); + case Namespace::GroupInfo: return group->info->dump(); + case Namespace::GroupMembers: return group->members->dump(); + case Namespace::GroupKeys: return group->keys->dump(); default: throw std::runtime_error{"Attempted to load unknown namespace"}; } } @@ -767,14 +779,11 @@ void State::handle_config_push_response( } } - // Now that we have confirmed the push we need to store the configs again - config_changed(pubkey, true, false); - log(LogLevel::debug, "handle_config_push_response: Completed"); } std::vector State::get_keys( - Namespace namespace_, std::optional pubkey_hex_) { + Namespace namespace_, std::optional pubkey_hex) { switch (namespace_) { case Namespace::Contacts: return _config_contacts->get_keys(); case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->get_keys(); @@ -783,22 +792,21 @@ std::vector State::get_keys( default: break; } - // Other namespaces are unique for a given pubkey_hex_ - if (!pubkey_hex_) + // Other namespaces are unique for a given pubkey_hex + if (!pubkey_hex) throw std::invalid_argument{ "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; - if (pubkey_hex_->size() != 64) + if (pubkey_hex->size() != 64) throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; - if (!_config_groups.count(*pubkey_hex_)) - throw std::runtime_error{"Unable to retrieve group"}; // Retrieve the group configs for this pubkey - auto group_configs = _config_groups[*pubkey_hex_].get(); + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); switch (namespace_) { - case Namespace::GroupInfo: return group_configs->info->get_keys(); - case Namespace::GroupMembers: return group_configs->members->get_keys(); - case Namespace::GroupKeys: return group_configs->keys->group_keys(); + case Namespace::GroupInfo: return group->info->get_keys(); + case Namespace::GroupMembers: return group->members->get_keys(); + case Namespace::GroupKeys: return group->keys->group_keys(); default: throw std::runtime_error{"Attempted to load unknown namespace"}; } } @@ -808,24 +816,27 @@ void State::create_group( std::optional description, std::optional pic, std::vector members_, - std::function + std::function error)> callback) { auto key_pair = ed25519::ed25519_key_pair(); auto group_id = "03" + oxenc::to_hex(key_pair.first.begin(), key_pair.first.end()); + ustring ed_pk = {key_pair.first.data(), key_pair.first.size()}; + ustring ed_sk = {key_pair.second.data(), key_pair.second.size()}; std::chrono::milliseconds timestamp = (std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) + network_offset); // Sanity check to avoid group collision - if (_config_groups.count(group_id)) + if (auto [it, b] = _config_groups.try_emplace(group_id, nullptr); b) { + _config_groups[group_id] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk)); + } else { throw std::runtime_error{"create_group: Tried to create group matching an existing group"}; - - ustring_view ed_pk = to_unsigned_sv(key_pair.first); - ustring_view ed_sk = to_unsigned_sv(key_pair.second); - _config_groups[group_id] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk)); + } // Store the group info + assert(_config_groups[group_id]); _config_groups[group_id]->info = std::make_unique(ed_pk, ed_sk, std::nullopt); _config_groups[group_id]->info->set_name(name); _config_groups[group_id]->info->set_created(timestamp.count()); @@ -866,31 +877,28 @@ void State::create_group( std::vector configs = { _config_groups[group_id]->info.get(), _config_groups[group_id]->members.get()}; auto push = prepare_push(group_id, timestamp, configs); + _send(group_id, push.payload, - [this, group_id, push, ed_sk, name, timestamp, callback]( - bool success, int16_t status_code, ustring response) { - // Call through to the default 'handle_config_push_response' first to update it's - // state correctly (this will also result in the configs getting stored to disk) - handle_config_push_response( - group_id, push.namespace_seqno, success, status_code, response); - - // Double check that the group state still exists - if (!_config_groups.count(group_id)) { - log(LogLevel::error, - "create_group: Unable to retrieve group when processing create response"); - callback(false, "", to_unsigned_sv("")); - return; - } - + [this, + gid = std::move(group_id), + namespace_seqno = push.namespace_seqno, + secretkey = std::move(ed_sk), + n = std::move(name), + timestamp, + cb = std::move(callback)](bool success, int16_t status_code, ustring response) { try { + // Call through to the default 'handle_config_push_response' first to update it's + // state correctly (this will also result in the configs getting stored to disk) + handle_config_push_response(gid, namespace_seqno, success, status_code, response); + // Retrieve the group configs for this pubkey and setup an entry in the user - // groups config for it - auto group_configs = _config_groups[group_id].get(); - auto group = _config_user_groups->get_or_construct_group(group_id); - group.name = name; + // groups config for it (the 'at' call will throw if the group doesn't exist) + auto group_configs = _config_groups.at(gid).get(); + auto group = _config_user_groups->get_or_construct_group(gid); + group.name = n; group.joined_at = timestamp.count(); - group.secretkey = ed_sk; + group.secretkey = secretkey; _config_user_groups->set(group); // Manually trigger 'config_changed' because we modified '_config_user_groups' @@ -898,34 +906,45 @@ void State::create_group( // triggered config_changed(); + // Now that we have a `_config_user_groups` entry for the group and have confirmed + // the push we need to store the group configs (we can't do this until after the + // `_config_user_groups` has been updated) + config_changed(gid, true, false); + // Lastly trigger the 'callback' to communicate the group was successfully created - callback(true, group_id, ed_sk); - } catch (...) { - callback(false, "", to_unsigned_sv("")); + cb(gid, secretkey, std::nullopt); + } catch (const std::exception& e) { + cb(""sv, ""_usv, e.what()); } }); } void State::approve_group(std::string_view group_id, std::optional group_sk) { + std::string gid = {group_id.data(), group_id.size()}; + // If we don't already have GroupConfigs then create them - if (!_config_groups[group_id]) { - auto ed_pk = to_unsigned_sv(oxenc::from_hex(group_id.begin() + 2, group_id.end())); - _config_groups[group_id] = + if (auto [it, b] = _config_groups.try_emplace(gid, nullptr); b) { + auto ed_pk_data = oxenc::from_hex(group_id.begin() + 2, group_id.end()); + auto ed_pk = to_unsigned_sv(ed_pk_data); + _config_groups[gid] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); - _config_groups[group_id]->info = - std::make_unique(ed_pk, group_sk, std::nullopt); - _config_groups[group_id]->members = + _config_groups[gid]->info = std::make_unique(ed_pk, group_sk, std::nullopt); + _config_groups[gid]->members = std::make_unique(ed_pk, group_sk, std::nullopt); - auto info = _config_groups[group_id]->info.get(); - auto members = _config_groups[group_id]->members.get(); - _config_groups[group_id]->keys = std::make_unique( + auto info = _config_groups[gid]->info.get(); + auto members = _config_groups[gid]->members.get(); + _config_groups[gid]->keys = std::make_unique( to_unsigned_sv(_user_sk), ed_pk, group_sk, std::nullopt, *info, *members); } // Update the USER_GROUPS config to have the group marked as approved auto group = _config_user_groups->get_or_construct_group(group_id); group.invited = false; + + if (group_sk) + group.secretkey = {group_sk->data(), group_sk->size()}; + _config_user_groups->set(group); // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We @@ -934,13 +953,6 @@ void State::approve_group(std::string_view group_id, std::optional config_changed(); } -void State::validate_group_pubkey(std::string_view pubkey_hex) const { - if (pubkey_hex.size() != 66) - throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; - if (!_config_groups.count(pubkey_hex)) - throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; -} - // Template functions template @@ -980,30 +992,23 @@ const UserProfile& State::config() const { template <> const groups::Info& State::config(std::string_view pubkey_hex) const { - validate_group_pubkey(pubkey_hex); - - if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->info; - - throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->info; }; template <> const groups::Members& State::config(std::string_view pubkey_hex) const { - validate_group_pubkey(pubkey_hex); - - if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->members; - - throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->members; }; template <> const groups::Keys& State::config(std::string_view pubkey_hex) const { - if (auto it = _config_groups.find(pubkey_hex); it != _config_groups.end()) - return *it->second->keys; - - throw std::runtime_error{"config: Attempted to retrieve group configs which doesn't exist"}; + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->keys; }; MutableUserConfigs State::mutable_config( @@ -1024,17 +1029,32 @@ MutableUserConfigs::~MutableUserConfigs() { MutableGroupConfigs State::mutable_config( std::string_view pubkey_hex, std::optional> set_error) { - validate_group_pubkey(pubkey_hex); + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + + std::string gid = {pubkey_hex.data(), pubkey_hex.size()}; return MutableGroupConfigs( - this, - *_config_groups[pubkey_hex]->info, - *_config_groups[pubkey_hex]->members, - *_config_groups[pubkey_hex]->keys, + *this, + *_config_groups[gid]->info, + *_config_groups[gid]->members, + *_config_groups[gid]->keys, set_error); }; +std::chrono::milliseconds MutableGroupConfigs::get_network_offset() const { + return parent_state.network_offset; +}; + +void MutableGroupConfigs::manual_send( + std::string pubkey_hex, + ustring payload, + std::function received_response) + const { + parent_state.manual_send(pubkey_hex, payload, received_response); +}; + MutableGroupConfigs::~MutableGroupConfigs() { - parent_state->config_changed(info.id); + parent_state.config_changed(info.id); }; } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index e00396d8..fa36a1b3 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -104,12 +104,12 @@ LIBSESSION_C_API bool state_load( } LIBSESSION_C_API void state_set_logger( - state_object* state, void (*callback)(config_log_level, const char*, void*), void* ctx) { + state_object* state, void (*callback)(state_log_level, const char*, void*), void* ctx) { if (!callback) unbox(state).logger = nullptr; else { unbox(state).logger = [callback, ctx](session::config::LogLevel lvl, std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); }; } } @@ -235,7 +235,7 @@ LIBSESSION_C_API bool state_merge( const char* pubkey_hex_, state_config_message* configs, size_t count, - config_string_list** successful_hashes) { + session_string_list** successful_hashes) { try { std::optional pubkey_hex; if (pubkey_hex_) @@ -261,7 +261,7 @@ LIBSESSION_C_API bool state_merge( } LIBSESSION_C_API bool state_current_hashes( - state_object* state, const char* pubkey_hex_, config_string_list** current_hashes) { + state_object* state, const char* pubkey_hex_, session_string_list** current_hashes) { try { std::optional pubkey_hex; if (pubkey_hex_) @@ -369,14 +369,25 @@ LIBSESSION_C_API bool state_get_keys( LIBSESSION_C_API void state_create_group( state_object* state, const char* name, - const char* description, + size_t name_len, + const char* description_, + size_t description_len, const user_profile_pic pic_, - const config_group_member* members_, + const state_group_member* members_, const size_t members_len, void (*callback)( - bool success, const char* group_id, unsigned const char* group_sk, void* ctx), + const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx), void* ctx) { + assert(name); try { + std::optional description; + if (description_) + description = {description_, description_len}; + std::string_view url{pic_.url}; ustring_view key; if (!url.empty()) @@ -391,16 +402,20 @@ LIBSESSION_C_API void state_create_group( } unbox(state).create_group( - name, + {name, name_len}, description, pic, members, - [callback, ctx](bool success, std::string_view group_id, ustring_view group_sk) { - callback(success, group_id.data(), group_sk.data(), ctx); + [callback, ctx]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { + callback(group_id.data(), group_sk.data(), error->data(), error->size(), ctx); }); } catch (const std::exception& e) { - set_error(state, e.what()); - callback(false, nullptr, nullptr, ctx); + std::string_view err = e.what(); + set_error(state, err); + callback(nullptr, nullptr, e.what(), err.size(), ctx); } } @@ -418,9 +433,9 @@ LIBSESSION_EXPORT void state_approve_group( } LIBSESSION_C_API bool state_mutate_user( - state_object* state, void (*callback)(mutable_state_user_object*, void*), void* ctx) { + state_object* state, void (*callback)(mutable_user_state_object*, void*), void* ctx) { try { - auto s_object = new mutable_state_user_object(); + auto s_object = new mutable_user_state_object(); auto mutable_state = unbox(state).mutable_config([state](std::string_view e) { // Don't override an existing error if (state->last_error) @@ -439,10 +454,10 @@ LIBSESSION_C_API bool state_mutate_user( LIBSESSION_C_API bool state_mutate_group( state_object* state, const char* pubkey_hex, - void (*callback)(mutable_state_group_object*, void*), + void (*callback)(mutable_group_state_object*, void*), void* ctx) { try { - auto s_object = new mutable_state_group_object(); + auto s_object = new mutable_group_state_object(); auto mutable_state = unbox(state).mutable_config({pubkey_hex, 66}, [state](std::string_view e) { // Don't override an existing error @@ -460,13 +475,13 @@ LIBSESSION_C_API bool state_mutate_group( } LIBSESSION_C_API void mutable_state_user_set_error_if_empty( - mutable_state_user_object* state, const char* err, size_t err_len) { + mutable_user_state_object* state, const char* err, size_t err_len) { if (auto set_error = unbox(state).set_error; set_error.has_value()) set_error.value()({err, err_len}); } LIBSESSION_C_API void mutable_state_group_set_error_if_empty( - mutable_state_group_object* state, const char* err, size_t err_len) { + mutable_group_state_object* state, const char* err, size_t err_len) { if (auto set_error = unbox(state).set_error; set_error.has_value()) set_error.value()({err, err_len}); } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index edddc8cb..927d5c98 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -273,7 +273,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_contact(mutable_state, static_cast(ctx)); }, &c); @@ -306,7 +306,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); state_config_message* merge_data = new state_config_message[1]; - config_string_list* accepted; + session_string_list* accepted; merge_data[0] = { NAMESPACE_CONTACTS, "fakehash1", @@ -347,7 +347,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { state_mutate_user( state2, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_contact(mutable_state, static_cast(ctx)); }, &c4); @@ -414,7 +414,7 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { } state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { auto contacts_to_remove = static_cast*>(ctx); for (auto& cont : *contacts_to_remove) diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index c047ac83..cce1c19b 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -329,7 +329,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { std::pair convos = {&c, &og}; state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { auto convos = static_cast< std::pair*>(ctx); state_set_convo_info_volatile_1to1(mutable_state, convos->first); @@ -393,7 +393,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { std::pair convos2 = {&c2, &cg}; state_mutate_user( state2, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { auto convos = static_cast< std::pair*>( ctx); @@ -415,7 +415,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); state_config_message* merge_data = new state_config_message[1]; - config_string_list* accepted; + session_string_list* accepted; merge_data[0] = { NAMESPACE_CONVO_INFO_VOLATILE, "hash123", @@ -472,7 +472,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_erase_convo_info_volatile_1to1( mutable_state, "052000000000000000000000000000000000000000000000000000000000000000"); @@ -481,7 +481,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK_FALSE(session::state::unbox(state).config().needs_push()); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_erase_convo_info_volatile_1to1( mutable_state, "055000000000000000000000000000000000000000000000000000000000000000"); @@ -667,7 +667,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load .count(); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_convo_info_volatile_1to1( mutable_state, static_cast(ctx)); }, @@ -704,7 +704,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load .count(); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_convo_info_volatile_1to1( mutable_state, static_cast(ctx)); }, @@ -727,7 +727,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load .count(); state_mutate_user( state2, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_convo_info_volatile_1to1( mutable_state, static_cast(ctx)); }, @@ -740,7 +740,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); state_config_message* merge_data = new state_config_message[1]; - config_string_list* accepted; + session_string_list* accepted; merge_data[0] = { NAMESPACE_CONVO_INFO_VOLATILE, "hash5235", @@ -767,7 +767,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load // because of the above dirty->merge->dirty (without an intermediate push) pattern. state_mutate_user( state2, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { REQUIRE_NOTHROW(state_set_convo_info_volatile_1to1( mutable_state, static_cast(ctx))); }, diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 3c5b5247..422f67b0 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -675,7 +675,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { // Non-freeing, so we can keep using `group`; this is less common: state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { auto group = static_cast(ctx); state_set_ugroups_legacy_group(mutable_state, group); @@ -686,7 +686,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { }, group); - config_string_list* hashes; + session_string_list* hashes; REQUIRE(state_current_hashes(state, nullptr, &hashes)); CHECK(hashes->len == 0); free(hashes); @@ -726,7 +726,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); state_config_message* merge_data = new state_config_message[1]; - config_string_list* accepted; + session_string_list* accepted; merge_data[0] = { NAMESPACE_USER_GROUPS, "fakehash1", diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 475ba217..f6e40b3c 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -12,10 +14,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include "utils.hpp" @@ -25,6 +30,7 @@ using namespace oxenc::literals; static constexpr int64_t created_ts = 1680064059; +using namespace session::state; using namespace session::config; static std::array sk_from_seed(ustring_view seed) { @@ -557,47 +563,87 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { struct pseudo_client { + std::string group_id; std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; - std::string session_id{session_id_from_ed(public_key)}; + std::array user_secret_key; + const ustring_view user_public_key{user_secret_key.data() + 32, 32}; + std::string user_session_id{session_id_from_ed(user_public_key)}; - config_group_keys* keys; - config_object* info; - config_object* members; + state_object* state; + std::optional last_store = std::nullopt; + std::optional last_send = std::nullopt; pseudo_client( - ustring seed, - bool is_admin, - unsigned char* gpk, - std::optional 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, + ustring user_seed, + std::optional group_id_ = std::nullopt, + std::optional> group_sk_ = std::nullopt) : + user_secret_key{sk_from_seed(user_seed)} { + char err[256]; + REQUIRE(state_init(&state, user_secret_key.data(), nullptr, 0, err)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + + // If we already have a group then just "approve" it + if (group_id_) { + auto gid = *group_id_; + group_id = gid; + + if (group_sk_) { + auto gsk = *group_sk_; + secret_key = gsk; + state_approve_group(state, gid.c_str(), gsk.data()); + return; + } + + state_approve_group(state, gid.c_str(), nullptr); + return; + } + + pseudo_client* ctx = this; + state_create_group( + state, + "", 0, - NULL); - REQUIRE(rv == 0); + nullptr, + 0, + user_profile_pic(), + nullptr, + 0, + [](const char* group_id, + const unsigned char* group_sk, + const char* error, + const size_t error_len, + void* ctx) { + if (error_len > 0) + REQUIRE(error == ""sv); + + auto client = static_cast(ctx); + + // Now that the group is created store the values + client->group_id = group_id; + memcpy(client->secret_key.data(), group_sk, 64); + + // Clear the 'last_send' and 'last_store' since we don't care about the + // group creation + client->last_send = std::nullopt; + client->last_store = std::nullopt; + }, + ctx); + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200,\"body\":{\"hash\":" + "\"fakehash1\"}}]}"); + last_send->response_cb( + true, + 200, + send_response.data(), + send_response.size(), + last_send->callback_context); } - ~pseudo_client() { - config_free(info); - config_free(members); - } + ~pseudo_client() { state_free(state); } }; - const ustring group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; const ustring admin1_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; const ustring admin2_seed = @@ -609,189 +655,257 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 }; - std::array group_pk; - std::array group_sk; - - crypto_sign_ed25519_seed_keypair( - group_pk.data(), - group_sk.data(), - reinterpret_cast(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 admins; hacky_list 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()); + admins.emplace_back(admin1_seed); + + auto& admin1 = admins[0]; + admins.emplace_back(admin2_seed, admin1.group_id, admin1.secret_key); for (int i = 0; i < 4; ++i) - members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + members.emplace_back(member_seeds[i], admin1.group_id); - REQUIRE(admins[0].session_id == + REQUIRE(admins[0].user_session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); - REQUIRE(admins[1].session_id == + REQUIRE(admins[1].user_session_id == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - REQUIRE(members[0].session_id == + REQUIRE(members[0].user_session_id == "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); - REQUIRE(members[1].session_id == + REQUIRE(members[1].user_session_id == "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); - REQUIRE(members[2].session_id == + REQUIRE(members[2].user_session_id == "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); - REQUIRE(members[3].session_id == + REQUIRE(members[3].user_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())); + auto& admin2 = admins[1]; + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 1); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 0); - 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); - - config_push_data* new_mem_config1 = config_push(admin1.members); - CHECK(new_mem_config1->seqno == 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; - merge_size1[0] = new_info_config1->config_len; - - merge_data1[1] = new_mem_config1->config; - merge_size1[1] = new_mem_config1->config_len; + for (const auto& m : members) + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + + // Add member, re-key, distribute + auto& member1 = members[0]; + state_group_member new_member1; + REQUIRE(state_get_or_construct_group_member( + admin1.state, + admin1.group_id.c_str(), + &new_member1, + member1.user_session_id.c_str(), + nullptr)); + + state_mutate_group( + admin1.state, + admin1.group_id.c_str(), + [](mutable_group_state_object* state, void* ctx) { + state_set_group_member(state, static_cast(ctx)); + CHECK(state_rekey_group(state)); + }, + &new_member1); + + CHECK(session::state::unbox(admin1.state) + .config(admin1.group_id) + .needs_push()); + + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 2); + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 2); + REQUIRE(admin1.last_send.has_value()); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto second_request_data = nlohmann::json::json_pointer("/params/requests/1/params/data"); + auto third_request_data = nlohmann::json::json_pointer("/params/requests/2/params/data"); + auto last_send_json = nlohmann::json::parse(admin1.last_send->payload); + REQUIRE(last_send_json.contains(first_request_data)); + REQUIRE(last_send_json.contains(second_request_data)); + REQUIRE(last_send_json.contains(third_request_data)); + auto last_send_data_0 = session::to_unsigned( + oxenc::from_base64(last_send_json[first_request_data].get())); + auto last_send_data_1 = session::to_unsigned( + oxenc::from_base64(last_send_json[second_request_data].get())); + auto last_send_data_2 = session::to_unsigned( + oxenc::from_base64(last_send_json[third_request_data].get())); + state_config_message* merge_data = new state_config_message[3]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash1", + created_ts, + last_send_data_0.data(), + last_send_data_0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash2", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash3", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; + state_config_message* merge_data_no_keys = new state_config_message[2]; + merge_data_no_keys[0] = { + NAMESPACE_GROUP_INFO, + "fakehash2", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + merge_data_no_keys[1] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash3", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; /* 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, "fakehash1"); - - 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, "fakehash1"); - - REQUIRE(groups_members_size(a.members) == 1); + session_string_list* accepted; + REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash1"sv); + CHECK(accepted->value[1] == "fakehash2"sv); + CHECK(accepted->value[2] == "fakehash3"sv); + free(accepted); + + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash3\"}" + "}]}"); + a.last_send->response_cb( + true, + 200, + send_response.data(), + send_response.size(), + a.last_send->callback_context); + + REQUIRE(state_size_group_members(a.state, a.group_id.c_str()) == 2); } - /* 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 - */ + /* Non-admins */ 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)); - config_string_list* hashes; - REQUIRE_THROWS( - hashes = config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); - REQUIRE_THROWS( - hashes = config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); - - REQUIRE(groups_members_size(m.members) == 0); + // Non-admin members cannot merge without the updated encryption keys + session_string_list* accepted; + REQUIRE_FALSE(state_merge(m.state, m.group_id.c_str(), merge_data_no_keys, 2, &accepted)); + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + m.state->last_error = nullptr; + + // The first member will be able to decrypt the keys (since they are a member), and + // info/member configs once they have the updated keys but the others aren't members so + // should fail + if (m.user_session_id == member1.user_session_id) { + REQUIRE(state_merge(m.state, m.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash1"sv); + CHECK(accepted->value[1] == "fakehash2"sv); + CHECK(accepted->value[2] == "fakehash3"sv); + free(accepted); + + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 2); + } else { + REQUIRE_FALSE(state_merge(m.state, m.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + m.state->last_error = nullptr; + } } - free(new_info_config1); - free(new_mem_config1); + free(merge_data_no_keys); + free(merge_data); - for (int i = 0; i < members.size(); ++i) { - config_group_member new_mem; + std::vector new_members; + new_members.reserve(members.size()); - REQUIRE(groups_members_get_or_construct( - members[i].members, &new_mem, members[i].session_id.c_str())); + for (auto& m : members) { + auto new_mem = state_group_member(); + REQUIRE(state_get_or_construct_group_member( + admin1.state, + admin1.group_id.c_str(), + &new_mem, + m.user_session_id.c_str(), + nullptr)); new_mem.admin = false; - groups_members_set(admin1.members, &new_mem); + new_members.push_back(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"; - - merge_data2[0] = new_info_config2->config; - merge_size2[0] = new_info_config2->config_len; - - merge_data2[1] = new_mem_config2->config; - merge_size2[1] = new_mem_config2->config_len; + state_mutate_group( + admin1.state, + admin1.group_id.c_str(), + [](mutable_group_state_object* state, void* ctx) { + auto new_members = static_cast*>(ctx); + + for (auto new_mem : *new_members) { + state_set_group_member(state, &new_mem); + } + + REQUIRE(state_rekey_group(state)); + }, + &new_members); + + CHECK(session::state::unbox(admin1.state) + .config(admin1.group_id) + .needs_push()); + CHECK(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 3); + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 3); + + last_send_json = nlohmann::json::parse(admin1.last_send->payload); + REQUIRE(last_send_json.contains(second_request_data)); + REQUIRE(last_send_json.contains(third_request_data)); + last_send_data_0 = session::to_unsigned( + oxenc::from_base64(last_send_json[first_request_data].get())); + last_send_data_1 = session::to_unsigned( + oxenc::from_base64(last_send_json[second_request_data].get())); + last_send_data_2 = session::to_unsigned( + oxenc::from_base64(last_send_json[third_request_data].get())); + merge_data = new state_config_message[3]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash4", + created_ts, + last_send_data_0.data(), + last_send_data_0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash5", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash6", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; 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, "fakehash2"); - 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, "fakehash2"); - - REQUIRE(groups_members_size(a.members) == 5); + session_string_list* accepted; + REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash4"sv); + CHECK(accepted->value[1] == "fakehash5"sv); + CHECK(accepted->value[2] == "fakehash6"sv); + free(accepted); + + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash5\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash6\"}" + "}]}"); + a.last_send->response_cb( + true, + 200, + send_response.data(), + send_response.size(), + a.last_send->callback_context); + + REQUIRE(state_size_group_members(a.state, a.group_id.c_str()) == 5); } - free(new_info_config2); - free(new_mem_config2); + free(merge_data); } TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 369a306e..47e08c67 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -148,7 +148,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(state_get_profile_name(state) == nullptr); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_profile_name(mutable_state, "Test Name"); }, nullptr); @@ -157,7 +157,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(strlen(state_get_profile_pic(state).url) == 0); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { auto p = user_profile_pic(); strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < // sizeof(p.url)! @@ -174,7 +174,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(state_get_profile_blinded_msgreqs(state) == -1); state_mutate_user( state, - [](mutable_state_user_object* mutable_state, void* ctx) { + [](mutable_user_state_object* mutable_state, void* ctx) { state_set_profile_blinded_msgreqs(mutable_state, 1); }, nullptr); diff --git a/tests/utils.hpp b/tests/utils.hpp index 0de22dbc..c133d25f 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -10,7 +10,6 @@ #include #include -#include "session/config/base.h" #include "session/config/namespaces.h" #include "session/config/namespaces.hpp" From f7ee549ab74ce2d1c77abd7dad3109efd3ce5c23 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 21 Feb 2024 17:45:32 +1100 Subject: [PATCH 11/24] Fixed a few bugs and added some missing comments Added a 'load_group_admin_key' function to make the relevant changes needed when getting promoted to admin. Fixed an issue where failing to handle a '_send' response wasn't setting the state error correctly. Fixed an issue where the 'prepare_push' wasn't signing group config requests correctly. --- include/session/config/groups/keys.h | 33 +--- include/session/state.h | 58 ++----- include/session/state.hpp | 66 +++++++- include/session/state_groups.h | 66 +++++++- src/config/groups/keys.cpp | 24 +-- src/state.cpp | 244 +++++++++++++++++++-------- src/state_c_wrapper.cpp | 72 ++++---- 7 files changed, 366 insertions(+), 197 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 998a4b37..e02aa796 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -63,24 +63,6 @@ LIBSESSION_EXPORT const unsigned char* state_get_group_key( /// - `true` if we have admin keys, `false` otherwise. LIBSESSION_EXPORT bool state_is_group_admin(const state_object* state, const char* group_id); -/// API: groups/state_load_group_admin_key -/// -/// Loads the admin keys, effectively upgrading this keys object from a member to an admin. -/// -/// This does nothing if the keys object already has admin keys. -/// -/// Inputs: -/// - `state` -- Pointer to the mutable state object -/// - `secret` -- pointer to the 32-byte group seed. (This a 64-byte libsodium "secret key" begins -/// with the seed, this can also be a given a pointer to such a value). -/// -/// Outputs: -/// - `true` if the object has been upgraded to admin status, or was already admin status; `false` -/// if the given seed value does not match the group's public key. If this returns `true` then -/// after the call a call to `state_is_group_admin` would also return `true`. -LIBSESSION_EXPORT bool state_load_group_admin_key( - mutable_group_state_object* state, const unsigned char* secret); - /// API: groups/state_group_needs_rekey /// /// Checks whether a rekey is required (for instance, because of key generation conflict). Note @@ -140,12 +122,7 @@ LIBSESSION_EXPORT void state_supplement_group_key( mutable_group_state_object* state, const char** sids, size_t sids_len, - void (*callback)( - bool success, - int16_t status_code, - const unsigned char* res, - size_t reslen, - void* ctx), + void (*callback)(bool success, void* ctx), void* ctx); /// API: groups/state_get_current_group_generation @@ -348,8 +325,8 @@ LIBSESSION_EXPORT bool state_sign_group_swarm_subaccount_binary( /// /// Constructs the subaccount token for a session id. The main use of this is to submit a swarm /// token revocation; for issuing subaccount tokens you want to use -/// `groups_keys_swarm_make_subaccount` instead. This will produce the same subaccount token that -/// `groups_keys_swarm_make_subaccount` implicitly creates that can be passed to a swarm to add a +/// `state_make_group_swarm_subaccount` instead. This will produce the same subaccount token that +/// `state_make_group_swarm_subaccount` implicitly creates that can be passed to a swarm to add a /// revocation for that subaccount. /// /// This is recommended to be used when removing a non-admin member to prevent their access. @@ -461,8 +438,8 @@ LIBSESSION_EXPORT void state_encrypt_group_message( /// Outputs: /// - `bool` -- True if the message was successfully decrypted, false if decryption (or parsing or /// decompression) failed with all of our known keys. If (and only if) true is returned then -/// `plaintext_out` must be freed when done with it. If false is returned then `conf.last_error` -/// will contain a diagnostic message describing why the decryption failed. +/// `plaintext_out` must be freed when done with it. If false is returned then `error` will +/// contain a diagnostic message describing why the decryption failed. LIBSESSION_EXPORT bool state_decrypt_group_message( const state_object* state, const char* group_id, diff --git a/include/session/state.h b/include/session/state.h index c4a60859..16ba1e06 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -214,6 +214,17 @@ LIBSESSION_EXPORT void state_set_service_node_offset(state_object* state, int64_ /// most recent API response LIBSESSION_EXPORT int64_t state_network_offset(const state_object* state); +/// API: state/state_has_pending_send +/// +/// Returns whether the state currently has local changes which are waiting to be sent. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state object +/// +/// Outputs: +/// - `bool` -- Flag indicating whether the state has local changes which are waiting to be sent. +LIBSESSION_EXPORT bool state_has_pending_send(const state_object* state); + /// API: state/state_merge /// /// Takes an pointer to an array of `state_config_message`, sorts them and merges them into the @@ -247,19 +258,6 @@ LIBSESSION_EXPORT bool state_merge( LIBSESSION_EXPORT bool state_current_hashes( state_object* state, const char* pubkey_hex_, session_string_list** current_hashes); -/// API: state/state_current_hashes -/// -/// The current config hashes; this can be empty if the current hashes are unknown or the current -/// state is not clean (i.e. a push is needed or pending). -/// -/// Inputs: -/// - `state` -- [in] Pointer to state object -/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 -/// bytes). Required for group hashes. -/// - `current_hashes` -- [out] Pointer to an array of the current config hashes -LIBSESSION_EXPORT bool state_current_hashes( - state_object* state, const char* pubkey_hex_, session_string_list** current_hashes); - /// API: state/state_current_seqno /// /// The current config seqno; this will return the updated seqno if there is a pending push. If @@ -327,32 +325,6 @@ LIBSESSION_EXPORT bool state_dump_namespace( unsigned char** out, size_t* outlen); -/// API: state/state_received_send_response -/// -/// Takes the network respons and request context from sending the data from the `send` hook and -/// processes the response updating the state as needed. -/// -/// Inputs: -/// - `state` -- [in] Pointer to state_object object -/// - `request_ctx` -- [in] Pointer to the request context data which was provided by the `send` -/// hook. -/// - `request_ctx_len` -- [in] Length of the `request_ctx`. -/// - `response_data` -- [in] Pointer to the response from the swarm after sending the -/// `payload_data`. -/// - `response_data_len` -- [in] Length of the `response_data`. -// LIBSESSION_EXPORT bool state_received_send_response( -// state_object* state, -// unsigned char* request_ctx, -// size_t request_ctx_len, -// unsigned char* response_data, -// size_t response_data_len); - -LIBSESSION_EXPORT bool state_received_send_response( - state_object* state, - const state_send_response* callback, - const unsigned char* response, - const size_t size); - /// API: state/state_get_keys /// /// Obtains the current group decryption keys. @@ -418,7 +390,7 @@ LIBSESSION_EXPORT bool state_mutate_group( void (*callback)(mutable_group_state_object*, void*), void* ctx); -/// API: state/mutable_state_user_set_error_if_empty +/// API: state/mutable_user_state_set_error_if_empty /// /// Updates the `state->last_error` value to the provided message if it is currently empty. /// @@ -426,10 +398,10 @@ LIBSESSION_EXPORT bool state_mutate_group( /// - `state` -- [in] Pointer to the mutable state object /// - `err` -- [in] the error value to store in the state /// - `err_len` -- [in] length of 'err' -LIBSESSION_EXPORT void mutable_state_user_set_error_if_empty( +LIBSESSION_EXPORT void mutable_user_state_set_error_if_empty( mutable_user_state_object* state, const char* err, size_t err_len); -/// API: state/mutable_state_group_set_error_if_empty +/// API: state/mutable_group_state_set_error_if_empty /// /// Updates the `state->last_error` value to the provided message if it is currently empty. /// @@ -437,7 +409,7 @@ LIBSESSION_EXPORT void mutable_state_user_set_error_if_empty( /// - `state` -- [in] Pointer to the mutable state object /// - `err` -- [in] the error value to store in the state /// - `err_len` -- [in] length of 'err' -LIBSESSION_EXPORT void mutable_state_group_set_error_if_empty( +LIBSESSION_EXPORT void mutable_group_state_set_error_if_empty( mutable_group_state_object* state, const char* err, size_t err_len); #ifdef __cplusplus diff --git a/include/session/state.hpp b/include/session/state.hpp index 698abfd9..d2e3ce30 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -209,7 +209,7 @@ class State { ustring data)> hook) { _store = std::move(hook); - if (!hook) + if (!_store) return; config_changed(std::nullopt, true, false); @@ -234,7 +234,7 @@ class State { received_response)> hook) { _send = std::move(hook); - if (!hook) + if (!_send) return; config_changed(std::nullopt, false, true); @@ -262,6 +262,15 @@ class State { std::optional pubkey_hex, ustring_view dump); + /// API: state/State::has_pending_send + /// + /// Returns whether the state currently has local changes which are waiting to be sent. + /// + /// Outputs: + /// - `bool` -- Flag indicating whether the state has local changes which are waiting to be + /// sent. + bool has_pending_send() const; + /// API: state/State::config_changed /// /// This is called internally whenever a config gets dirtied. This function then validates the @@ -398,6 +407,26 @@ class State { std::vector get_keys( config::Namespace namespace_, std::optional pubkey_hex_); + /// API: groups/State::create_group + /// + /// Creates a new group with the provided values defining the initial state. Triggers the + /// callback upon success or error, if an error occurred the `error` value will be populated, + /// otherwise the `group_id` and `group_sk` will be populated. + /// + /// This function will add the updated group into the user groups config and setup the initial + /// group configs. The '_send' and '_store' hooks will be triggered for the newly + /// created/updated config messages. + /// + /// Note: This function **does not** send invitations to the group members so the clients will + /// still need to do so. Any members provided to this funciton will be included in the initial + /// keys generation. + /// + /// Inputs: + /// - `name` -- the name of the group. + /// - `description` -- optional description for the group. + /// - `pic` -- optional display picture for the group. + /// - `members` -- initial members to be added to the group. + /// - `callback` -- a callback to be triggered upon success/failure of the group creation. void create_group( std::string_view name, std::optional description, @@ -408,8 +437,38 @@ class State { ustring_view group_sk, std::optional error)> callback); + /// API: groups/State::approve_group + /// + /// Approves a group invitation, this will update the 'invited' flag in the user groups config + /// and create the initial group state. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `group_sk` -- optional 64-byte secret key for the group. void approve_group(std::string_view group_id, std::optional group_sk); + /// API: groups/State::load_group_admin_key + /// + /// Loads the admin keys into a group, upgrading the user from a member to an admin within the + /// keys and members objects, and storing the group secret key within the user groups config. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `secret` -- the group's 64-byte secret key or 32-byte seed + /// + /// Outputs: nothing. After a successful call, `admin()` will return true. Throws if the given + /// secret key does not match the group's pubkey. + void load_group_admin_key(std::string_view group_id, ustring_view secret); + + /// API: groups/State::erase_group + /// + /// Removes the group state and, if specified, removes the group from the user groups config. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `remove_user_record` -- flag to indicate whether the user groups entry should be removed. + void erase_group(std::string_view group_id, bool remove_user_record); + // Retrieves a read-only version of the user config template const ConfigType& config() const; @@ -436,7 +495,8 @@ class State { PreparedPush prepare_push( std::string pubkey_hex, std::chrono::milliseconds timestamp, - std::vector configs); + std::vector configs, + std::optional group_sk = std::nullopt); void handle_config_push_response( std::string pubkey, std::vector> namespace_seqnos, diff --git a/include/session/state_groups.h b/include/session/state_groups.h index 72ca5663..54fb8fff 100644 --- a/include/session/state_groups.h +++ b/include/session/state_groups.h @@ -14,14 +14,39 @@ extern "C" { #include "export.h" #include "state.h" +/// API: groups/state_create_group +/// +/// Creates a new group with the provided values defining the initial state. Triggers the callback +/// upon success or error, if an error occurred the `error` value will be populated, otherwise the +/// `group_id` and `group_sk` will be populated. +/// +/// This function will add the updated group into the user groups config and setup the initial group +/// configs. The '_send' and '_store' hooks will be triggered for the newly created/updated config +/// messages. +/// +/// Note: This function **does not** send invitations to the group members so the clients will still +/// need to do so. Any members provided to this funciton will be included in the initial keys +/// generation. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `name` -- the name of the group. +/// - `name_len` -- the length of the 'name' +/// - `description` -- optional description for the group. +/// - `description_len` -- the length of the 'description'. +/// - `pic` -- optional display picture for the group. +/// - `members` -- initial members to be added to the group. +/// - `members_len` -- the length of the 'members' array. +/// - `callback` -- a callback to be triggered upon success/failure of the group creation. +/// - `ctx` -- Pointer to an optional context. Set to NULL if unused LIBSESSION_EXPORT void state_create_group( state_object* state, const char* name, size_t name_len, const char* description, size_t description_len, - const user_profile_pic pic_, - const state_group_member* members_, + const user_profile_pic pic, + const state_group_member* members, const size_t members_len, void (*callback)( const char* group_id, @@ -31,9 +56,46 @@ LIBSESSION_EXPORT void state_create_group( void* ctx), void* ctx); +/// API: groups/state_approve_group +/// +/// Approves a group invitation, this will update the 'invited' flag in the user groups config and +/// create the initial group state. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `group_sk` -- optional 64-byte secret key for the group. LIBSESSION_EXPORT void state_approve_group( state_object* state, const char* group_id, unsigned const char* group_sk); +/// API: groups/state_load_group_admin_key +/// +/// Loads the admin keys into a group, upgrading the user from a member to an admin within the keys +/// and members objects, and storing the group secret key within the user groups config. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `seed` -- pointer to the 32-byte seed. +/// +/// Outputs: +/// - `true` if the member has been upgraded to admin status, or was already admin status; `false` +/// if the given seed value does not match the group's public key. If this returns `true` then +/// after the call a call to `state_is_group_admin` would also return `true`. +LIBSESSION_EXPORT bool state_load_group_admin_key( + state_object* state, const char* group_id, unsigned const char* seed); + +/// API: groups/state_erase_group +/// +/// Removes the group state and, if specified, removes the group from the user groups config. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `remove_user_record` -- flag to indicate whether the user groups entry should be removed. +LIBSESSION_EXPORT void state_erase_group( + state_object* state, const char* group_id, bool remove_user_record); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index f8d3c76a..cc94b20c 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1439,19 +1439,6 @@ LIBSESSION_C_API bool state_is_group_admin(const state_object* state, const char } } -LIBSESSION_C_API bool state_load_group_admin_key( - mutable_group_state_object* state, const unsigned char* secret) { - try { - unbox(state).keys.load_admin_key( - ustring_view{secret, 32}, unbox(state).info, unbox(state).members); - return true; - } catch (const std::exception& e) { - if (auto set_error = unbox(state).set_error; set_error.has_value()) - set_error.value()(e.what()); - return false; - } -} - LIBSESSION_C_API bool state_group_needs_rekey(const state_object* state, const char* group_id) { try { return unbox(state).config(group_id).needs_rekey(); @@ -1475,12 +1462,7 @@ LIBSESSION_C_API void state_supplement_group_key( mutable_group_state_object* state, const char** sids, size_t sids_len, - void (*callback)( - bool success, - int16_t status_code, - const unsigned char* res, - size_t reslen, - void* ctx), + void (*callback)(bool success, void* ctx), void* ctx) { assert(sids); std::vector session_ids; @@ -1500,13 +1482,13 @@ LIBSESSION_C_API void state_supplement_group_key( payload, [callback, ctx](bool success, int16_t status_code, ustring response) { if (callback) - callback(success, status_code, response.data(), response.size(), ctx); + callback(success, ctx); }); } catch (const std::exception& e) { if (auto set_error = unbox(state).set_error; set_error.has_value()) set_error.value()(e.what()); if (callback) - callback(false, -1, nullptr, 0, ctx); + callback(false, ctx); } } diff --git a/src/state.cpp b/src/state.cpp index 7d59f24e..bec0606e 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -85,6 +85,29 @@ State::State(ustring_view ed25519_secretkey, std::vector dumps) _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); add_child_logger(_config_user_profile); } + + // If we have a group in the 'user_groups' that isn't in the 'invited' state but didn't have a + // dump for it then most likely the group has been approved but we haven't completed the initial + // poll - in this case we want to create the group configs because we can assume that the client + // will try to poll and merge the state into the group + for (auto group_it = _config_user_groups->begin_groups(); + group_it != _config_user_groups->end(); + ++group_it) { + if (group_it->invited) + continue; + + std::optional group_sk; + + if (!group_it->secretkey.empty()) + group_sk = {group_it->secretkey.data(), group_it->secretkey.size()}; + + if (auto [it, b] = _config_groups.try_emplace(group_it->id, nullptr); b) { + auto ed_pk_data = oxenc::from_hex(group_it->id.begin() + 2, group_it->id.end()); + auto ed_pk = to_unsigned_sv(ed_pk_data); + _config_groups[group_it->id] = + std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); + } + } } void State::load( @@ -171,6 +194,26 @@ void State::load( throw std::runtime_error{"Attempted to load unknown namespace"}; } +bool State::has_pending_send() const { + bool needs_push = + (_config_contacts->needs_push() || _config_convo_info_volatile->needs_push() || + _config_user_groups->needs_push() || _config_user_profile->needs_push()); + + if (!needs_push) { + for (const auto& it : _config_groups) { + needs_push = + (it.second->keys->admin() && + (it.second->info->needs_push() || it.second->members->needs_push() || + it.second->keys->pending_config())); + + if (needs_push) + break; + } + } + + return needs_push; +} + void State::config_changed( std::optional pubkey_hex, bool allow_store, bool allow_send) { auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); @@ -217,18 +260,15 @@ void State::config_changed( " from user_groups config"}; // Only group admins can push group config changes + auto& group = _config_groups.at(target_pubkey_hex); needs_push = (allow_send && !user_group_info->secretkey.empty() && - (_config_groups[target_pubkey_hex]->info->needs_push() || - _config_groups[target_pubkey_hex]->members->needs_push() || - _config_groups[target_pubkey_hex]->keys->pending_config())); + (group->info->needs_push() || group->members->needs_push() || + group->keys->pending_config())); needs_dump = - (allow_store && (_config_groups[target_pubkey_hex]->info->needs_dump() || - _config_groups[target_pubkey_hex]->members->needs_dump() || - _config_groups[target_pubkey_hex]->keys->needs_dump())); - configs = { - _config_groups[target_pubkey_hex]->info.get(), - _config_groups[target_pubkey_hex]->members.get()}; + (allow_store && (group->info->needs_dump() || group->members->needs_dump() || + group->keys->needs_dump())); + configs = {group->info.get(), group->members.get()}; info_title = "Group configs for " + target_pubkey_hex; } @@ -272,13 +312,13 @@ void State::config_changed( log(LogLevel::debug, "config_changed: Call 'send'"); _send(target_pubkey_hex, push.payload, - [this, target_pubkey_hex, push]( + [this, pubkey = std::move(target_pubkey_hex), push]( bool success, uint16_t status_code, ustring response) { handle_config_push_response( - target_pubkey_hex, push.namespace_seqno, success, status_code, response); + pubkey, push.namespace_seqno, success, status_code, response); // Now that we have confirmed the push we need to store the configs again - config_changed(target_pubkey_hex, true, false); + config_changed(pubkey, true, false); }); } log(LogLevel::debug, "config_changed: Complete"); @@ -296,11 +336,33 @@ void State::manual_send( PreparedPush State::prepare_push( std::string pubkey_hex, std::chrono::milliseconds timestamp, - std::vector configs) { + std::vector configs, + std::optional group_sk) { auto is_group_pubkey = (!pubkey_hex.empty() && pubkey_hex.substr(0, 2) != "05"); std::vector requests; std::vector obsolete_hashes; + std::array seckey; + // Prepare for signing + if (is_group_pubkey) { + // If we were given an explicit secret key then use that, otherwise retrieve it from the + // user groups config + if (group_sk) + memcpy(seckey.data(), group_sk->data(), 64); + else { + auto config = _config_groups[pubkey_hex]->keys.get(); + auto user_group = _config_user_groups->get_group(pubkey_hex); + + if (!config->admin() || !user_group || user_group->secretkey.empty()) + throw std::runtime_error{ + "prepare_push: Only groups admins can push config changes"}; + + memcpy(seckey.data(), user_group->secretkey.data(), 64); + } + } else + memcpy(seckey.data(), _user_sk.data(), 64); + + // Check the configs for changes for (auto& config : configs) { if (!config->needs_push()) continue; @@ -321,27 +383,29 @@ PreparedPush State::prepare_push( to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); verification += to_unsigned_sv(std::to_string(timestamp.count())); - if (0 != - crypto_sign_ed25519_detached( - sig.data(), nullptr, verification.data(), verification.size(), _user_sk.data())) - throw std::runtime_error{ - "config_changed: Failed to sign; perhaps the secret key is invalid?"}; - nlohmann::json params{ {"namespace", static_cast(config->storage_namespace())}, {"pubkey", pubkey_hex}, {"ttl", config->default_ttl().count()}, {"timestamp", timestamp.count()}, {"data", oxenc::to_base64(msg)}, - {"signature", oxenc::to_base64(sig.begin(), sig.end())}, }; + // Sign the request + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), seckey.data())) + throw std::runtime_error{ + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; + + params["signature"] = oxenc::to_base64(sig.begin(), sig.end()); + // For user config storage we also need to add `pubkey_ed25519` if (!is_group_pubkey) params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); // Add the 'seqno' temporarily to the params (this will be removed from the payload - // before sending but is needed to generate the request context) + // before sending but is needed for handling the push result) params["seqno"] = seqno; requests.emplace_back(params); @@ -368,9 +432,9 @@ PreparedPush State::prepare_push( nullptr, verification.data(), verification.size(), - _user_sk.data())) + seckey.data())) throw std::runtime_error{ - "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ {"namespace", config->storage_namespace()}, @@ -383,7 +447,7 @@ PreparedPush State::prepare_push( // The 'GROUP_KEYS' push data doesn't need a 'seqno', but to avoid index // out-of-bounds issues we add one anyway (this will be removed from the payload - // before sending but is needed to generate the request context) + // before sending but is needed for handling the push result) params["seqno"] = 0; requests.emplace_back(params); @@ -421,7 +485,7 @@ PreparedPush State::prepare_push( if (0 != crypto_sign_ed25519_detached( - sig.data(), nullptr, verification.data(), verification.size(), _user_sk.data())) + sig.data(), nullptr, verification.data(), verification.size(), seckey.data())) throw std::runtime_error{ "config_changed: Failed to sign; perhaps the secret key is invalid?"}; @@ -521,13 +585,10 @@ std::vector State::merge( "merge: Invalid pubkey_hex - required for group config namespaces"}; if (target_pubkey_hex.size() != 66) throw std::invalid_argument{"merge: Invalid pubkey_hex - expected 66 bytes"}; - if (!_config_groups.count(target_pubkey_hex)) - throw std::runtime_error{ - "merge: Attempted to merge group configs before for group with no config " - "state"}; - auto info = _config_groups[target_pubkey_hex]->info.get(); - auto members = _config_groups[target_pubkey_hex]->members.get(); + auto& group = _config_groups.at(target_pubkey_hex); + auto info = group->info.get(); + auto members = group->members.get(); is_group_merge = true; if (config.namespace_ == Namespace::GroupInfo) { @@ -538,7 +599,7 @@ std::vector State::merge( good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); } else if (config.namespace_ == Namespace::GroupKeys) { // GroupKeys doesn't support merging multiple messages at once so do them individually - if (_config_groups[target_pubkey_hex]->keys->load_key_message( + if (group->keys->load_key_message( config.hash, config.data, config.timestamp_ms, *info, *members)) { good_hashes.emplace_back(config.hash); } @@ -716,7 +777,7 @@ void State::handle_config_push_response( if (single_status_code == 406 || single_status_code == 425) error = "The user's clock is out of sync with the service node network."; else if (single_status_code == 401) - error = "Unauthorised (sinature verification failed)."; + error = "Unauthorised (signature verification failed)."; if (error_body) error += " Server error: " + *error_body + "."; @@ -763,15 +824,11 @@ void State::handle_config_push_response( } // Other namespaces are unique for a given pubkey - if (!_config_groups.count(pubkey)) - throw std::runtime_error{"handle_config_push_response: Unable to retrieve group"}; - - // Retrieve the group configs for this pubkey - auto group_configs = _config_groups[pubkey].get(); + auto& group = _config_groups.at(pubkey); switch (namespace_) { - case Namespace::GroupInfo: group_configs->info->confirm_pushed(seqno, hash); - case Namespace::GroupMembers: group_configs->members->confirm_pushed(seqno, hash); + case Namespace::GroupInfo: group->info->confirm_pushed(seqno, hash); + case Namespace::GroupMembers: group->members->confirm_pushed(seqno, hash); case Namespace::GroupKeys: continue; // No need to do anything here default: throw std::runtime_error{ @@ -837,20 +894,20 @@ void State::create_group( // Store the group info assert(_config_groups[group_id]); - _config_groups[group_id]->info = std::make_unique(ed_pk, ed_sk, std::nullopt); - _config_groups[group_id]->info->set_name(name); - _config_groups[group_id]->info->set_created(timestamp.count()); + auto& group = _config_groups.at(group_id); + group->info = std::make_unique(ed_pk, ed_sk, std::nullopt); + group->info->set_name(name); + group->info->set_created(timestamp.count()); if (description) - _config_groups[group_id]->info->set_description(*description); + group->info->set_description(*description); if (pic) - _config_groups[group_id]->info->set_profile_pic(*pic); + group->info->set_profile_pic(*pic); // Need to load the members before creating the Keys config to ensure they // are included in the initial key rotation - _config_groups[group_id]->members = - std::make_unique(ed_pk, ed_sk, std::nullopt); + group->members = std::make_unique(ed_pk, ed_sk, std::nullopt); // Insert the current user as a group admin auto admin_member = groups::member{_user_x_pk_hex}; @@ -860,23 +917,24 @@ void State::create_group( if (auto name = _config_user_profile->get_name()) admin_member.name = *name; - _config_groups[group_id]->members->set(admin_member); + group->members->set(admin_member); // Add other members (ignore the current user if they happen to be included) for (auto m : members_) if (m.session_id != _user_x_pk_hex) - _config_groups[group_id]->members->set(m); + group->members->set(m); // Finally create the keys - auto info = _config_groups[group_id]->info.get(); - auto members = _config_groups[group_id]->members.get(); - _config_groups[group_id]->keys = std::make_unique( + auto info = group->info.get(); + auto members = group->members.get(); + group->keys = std::make_unique( to_unsigned_sv(_user_sk), ed_pk, ed_sk, std::nullopt, *info, *members); - // Prepare and trigger the push for the group configs - std::vector configs = { - _config_groups[group_id]->info.get(), _config_groups[group_id]->members.get()}; - auto push = prepare_push(group_id, timestamp, configs); + // Prepare and trigger the push for the group configs (need to explicitly provide the 'ed_sk' + // here as we won't load the group into user groups until after we have successfully pushed the + // group configs) + std::vector configs = {group->info.get(), group->members.get()}; + auto push = prepare_push(group_id, timestamp, configs, ed_sk); _send(group_id, push.payload, @@ -894,12 +952,12 @@ void State::create_group( // Retrieve the group configs for this pubkey and setup an entry in the user // groups config for it (the 'at' call will throw if the group doesn't exist) - auto group_configs = _config_groups.at(gid).get(); - auto group = _config_user_groups->get_or_construct_group(gid); - group.name = n; - group.joined_at = timestamp.count(); - group.secretkey = secretkey; - _config_user_groups->set(group); + _config_groups.at(gid); + auto user_group = _config_user_groups->get_or_construct_group(gid); + user_group.name = n; + user_group.joined_at = timestamp.count(); + user_group.secretkey = secretkey; + _config_user_groups->set(user_group); // Manually trigger 'config_changed' because we modified '_config_user_groups' // directly rather than via the 'MutableUserConfigs' so it won't automatically get @@ -928,14 +986,6 @@ void State::approve_group(std::string_view group_id, std::optional auto ed_pk = to_unsigned_sv(ed_pk_data); _config_groups[gid] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); - _config_groups[gid]->info = std::make_unique(ed_pk, group_sk, std::nullopt); - _config_groups[gid]->members = - std::make_unique(ed_pk, group_sk, std::nullopt); - - auto info = _config_groups[gid]->info.get(); - auto members = _config_groups[gid]->members.get(); - _config_groups[gid]->keys = std::make_unique( - to_unsigned_sv(_user_sk), ed_pk, group_sk, std::nullopt, *info, *members); } // Update the USER_GROUPS config to have the group marked as approved @@ -953,6 +1003,60 @@ void State::approve_group(std::string_view group_id, std::optional config_changed(); } +void State::load_group_admin_key(std::string_view group_id, ustring_view secret) { + if (secret.size() == 64) + secret.remove_suffix(32); + else if (secret.size() != 32) + throw std::invalid_argument{ + "Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"}; + + std::string gid = {group_id.data(), group_id.size()}; + std::array pk; + sodium_cleared> sk; + crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), secret.data()); + + // Load the secret key into the Keys config + auto& group = _config_groups.at(gid); + auto info = group->info.get(); + auto members = group->members.get(); + group->keys->load_admin_key(secret, *info, *members); + + // Update the group member record to flag the current user as an admin + auto member = members->get_or_construct(_user_x_pk_hex); + member.admin = true; + member.invite_status = 0; // Just in case + member.promotion_status = 0; + + // Update the user groups record to include the admin key + auto user_group = _config_user_groups->get_or_construct_group(group_id); + user_group.secretkey = {sk.data(), sk.size()}; + _config_user_groups->set(user_group); + + // Trigger the 'config_changed' callbacks directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(); + config_changed(group_id); +} + +void State::erase_group(std::string_view group_id, bool remove_user_record) { + std::string gid = {group_id.data(), group_id.size()}; + + // Remove the group configs + _config_groups.erase(gid); + + // If we don't want to remove the user record then stop here + if (!remove_user_record) + return; + + _config_user_groups->erase_group(group_id); + + // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(); +} + // Template functions template diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index fa36a1b3..64c3a039 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -116,6 +116,12 @@ LIBSESSION_C_API void state_set_logger( using response_callback_t = std::function; +struct response_callback_info { + state_object* state; + response_callback_t cb; + + response_callback_info(state_object* state, response_callback_t cb) : state{state}, cb{cb} {} +}; LIBSESSION_C_API bool state_set_send_callback( state_object* state, @@ -136,14 +142,14 @@ LIBSESSION_C_API bool state_set_send_callback( if (!callback) unbox(state).on_send(nullptr); else { - unbox(state).on_send([callback, app_ctx]( + unbox(state).on_send([state, callback, app_ctx]( std::string pubkey, ustring data, response_callback_t received_response) { // We leak ownership of this std::function below in the `.release()` call, then we // recapture it inside the inner response callback below. - auto on_response = - std::make_unique(std::move(received_response)); + auto on_response = std::make_unique( + state, std::move(received_response)); callback( pubkey.c_str(), @@ -154,15 +160,15 @@ LIBSESSION_C_API bool state_set_send_callback( const unsigned char* res, size_t reslen, void* callback_context) { + // Recapture the std::function callback here in a unique_ptr so that + // we clean it up at the end of this lambda. + std::unique_ptr info{ + static_cast(callback_context)}; try { - // Recapture the std::function callback here in a unique_ptr so that - // we clean it up at the end of this lambda. - std::unique_ptr cb{ - static_cast(callback_context)}; - (*cb)(success, status_code, {res, reslen}); + info->cb(success, status_code, {res, reslen}); return true; - } catch (...) { - return false; + } catch (const std::exception& e) { + return set_error(info->state, e.what()); } }, app_ctx, @@ -176,22 +182,6 @@ LIBSESSION_C_API bool state_set_send_callback( } } -LIBSESSION_C_API bool state_received_send_response( - state_object* state, - const state_send_response* callback, - const unsigned char* response, - const size_t size) { - try { - assert(callback && callback->internals); - auto received_response = - *static_cast*>(callback->internals); - received_response({response, size}); - return true; - } catch (const std::exception& e) { - return set_error(state, e.what()); - } -} - LIBSESSION_C_API bool state_set_store_callback( state_object* state, void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), @@ -230,6 +220,10 @@ LIBSESSION_C_API int64_t state_network_offset(const state_object* state) { return unbox(state).network_offset.count(); } +LIBSESSION_C_API bool state_has_pending_send(const state_object* state) { + return unbox(state).has_pending_send(); +} + LIBSESSION_C_API bool state_merge( state_object* state, const char* pubkey_hex_, @@ -419,7 +413,7 @@ LIBSESSION_C_API void state_create_group( } } -LIBSESSION_EXPORT void state_approve_group( +LIBSESSION_C_API void state_approve_group( state_object* state, const char* group_id, unsigned const char* group_sk) { try { std::optional ed_sk; @@ -427,8 +421,26 @@ LIBSESSION_EXPORT void state_approve_group( ed_sk = {group_sk, 64}; unbox(state).approve_group({group_id, 66}, ed_sk); + } catch (...) { + } +} + +LIBSESSION_C_API bool state_load_group_admin_key( + state_object* state, const char* group_id, unsigned const char* seed) { + try { + std::string gid = {group_id, 66}; + unbox(state).load_group_admin_key({group_id, 66}, ustring_view{seed, 32}); + return true; } catch (const std::exception& e) { - set_error(state, e.what()); + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_erase_group( + state_object* state, const char* group_id, bool remove_user_record) { + try { + unbox(state).erase_group({group_id, 66}, remove_user_record); + } catch (...) { } } @@ -474,13 +486,13 @@ LIBSESSION_C_API bool state_mutate_group( } } -LIBSESSION_C_API void mutable_state_user_set_error_if_empty( +LIBSESSION_C_API void mutable_user_state_set_error_if_empty( mutable_user_state_object* state, const char* err, size_t err_len) { if (auto set_error = unbox(state).set_error; set_error.has_value()) set_error.value()({err, err_len}); } -LIBSESSION_C_API void mutable_state_group_set_error_if_empty( +LIBSESSION_C_API void mutable_group_state_set_error_if_empty( mutable_group_state_object* state, const char* err, size_t err_len) { if (auto set_error = unbox(state).set_error; set_error.has_value()) set_error.value()({err, err_len}); From bfadd657b4b8823a06d7c64cba0f5cb43e3ad11d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 22 Feb 2024 15:21:24 +1100 Subject: [PATCH 12/24] Fixed a signature generation issue when updating keys --- src/state.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/state.cpp b/src/state.cpp index bec0606e..8a45fefd 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -423,9 +423,10 @@ PreparedPush State::prepare_push( // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; - ustring verification = to_unsigned("store") + - static_cast(config->storage_namespace()) + - static_cast(timestamp.count()); + ustring verification = to_unsigned("store"); + verification += + to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); if (0 != crypto_sign_ed25519_detached( sig.data(), From aab569a7efee14f5ac82f75bfbc397207a2c43e2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 22 Feb 2024 16:47:56 +1100 Subject: [PATCH 13/24] Fixed an issue where the timestamp passed to 'store' could be incorrect --- include/session/state.h | 8 +- include/session/state.hpp | 41 +++------- src/state.cpp | 155 ++++++++++++++++++++++++++------------ src/state_c_wrapper.cpp | 10 +-- 4 files changed, 122 insertions(+), 92 deletions(-) diff --git a/include/session/state.h b/include/session/state.h index 16ba1e06..51bb7f2d 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -236,14 +236,8 @@ LIBSESSION_EXPORT bool state_has_pending_send(const state_object* state); /// bytes). Required for group dumps. /// - `configs` -- [in] Pointer to an array of `state_config_message` objects /// - `count` -- [in] Number of objects in `configs` -/// - `successful_hashes` -- [out] Pointer to an array of message hashes that were successfully -/// merged LIBSESSION_EXPORT bool state_merge( - state_object* state, - const char* pubkey_hex_, - state_config_message* configs, - size_t count, - session_string_list** successful_hashes); + state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count); /// API: state/state_current_hashes /// diff --git a/include/session/state.hpp b/include/session/state.hpp index d2e3ce30..4649bf2c 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -127,20 +127,6 @@ struct config_message { uint64_t timestamp_ms, ustring_view data) : namespace_{namespace_}, hash{hash}, timestamp_ms{timestamp_ms}, data{data} {}; - - config_message() = delete; - config_message(config_message&&) = default; - config_message(const config_message&) = default; - config_message& operator=(config_message&&) = default; - config_message& operator=(const config_message&) = default; - - auto cmpval() const { return std::tie(namespace_, hash, timestamp_ms, data); } - bool operator<(const config_message& b) const { return cmpval() < b.cmpval(); } - bool operator>(const config_message& b) const { return cmpval() > b.cmpval(); } - bool operator<=(const config_message& b) const { return cmpval() <= b.cmpval(); } - bool operator>=(const config_message& b) const { return cmpval() >= b.cmpval(); } - bool operator==(const config_message& b) const { return cmpval() == b.cmpval(); } - bool operator!=(const config_message& b) const { return cmpval() != b.cmpval(); } }; struct PreparedPush { @@ -212,10 +198,10 @@ class State { if (!_store) return; - config_changed(std::nullopt, true, false); + config_changed(std::nullopt, true, false, std::nullopt); for (auto& [key, val] : _config_groups) - config_changed(key, true, false); + config_changed(key, true, false, std::nullopt); }; /// Hook which will be called whenever config messages need to be sent via the API. The hook @@ -237,10 +223,10 @@ class State { if (!_send) return; - config_changed(std::nullopt, false, true); + config_changed(std::nullopt, false, true, std::nullopt); for (auto& [key, val] : _config_groups) - config_changed(key, false, true); + config_changed(key, false, true, std::nullopt); }; /// API: state/State::load @@ -283,12 +269,13 @@ class State { /// bytes). Required for group changes. /// - `allow_store` -- boolean value to specify whether this change can trigger the store hook. /// - `allow_send` -- boolean value to specify whether this change can trigger the send hook. - /// - /// Outputs: None + /// - `server_timestamp_ms` -- timestamp value provided when the change was triggered from a + /// merge rather than a user action. void config_changed( - std::optional pubkey_hex = std::nullopt, - bool allow_store = true, - bool allow_send = true); + std::optional pubkey_hex, + bool allow_store, + bool allow_send, + std::optional server_timestamp_ms); /// API: state/State::manual_send /// @@ -332,13 +319,7 @@ class State { /// Required for group dumps. /// - `configs` -- vector of `config_message` types which include the data needed to properly /// merge. - /// - /// Outputs: - /// - vector of successfully parsed hashes. Note that this does not mean the hash was recent or - /// that it changed the config, merely that the returned hash was properly parsed and - /// processed as a config message, even if it was too old to be useful (or was already known - /// to be included). The hashes will be in the same order as in the input vector. - std::vector merge( + void merge( std::optional pubkey_hex, const std::vector& configs); /// API: state/State::current_hashes diff --git a/src/state.cpp b/src/state.cpp index 8a45fefd..05c1be76 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -215,7 +215,10 @@ bool State::has_pending_send() const { } void State::config_changed( - std::optional pubkey_hex, bool allow_store, bool allow_send) { + std::optional pubkey_hex, + bool allow_store, + bool allow_send, + std::optional server_timestamp_ms) { auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); @@ -281,6 +284,9 @@ void State::config_changed( // Call the hook to store the dump if needed if (_store && needs_dump && allow_store) { + std::chrono::milliseconds store_timestamp = + std::chrono::milliseconds(server_timestamp_ms.value_or(timestamp.count())); + for (auto& config : configs) { if (!config->needs_dump()) continue; @@ -288,7 +294,7 @@ void State::config_changed( "config_changed: call 'store' for " + namespace_name(config->storage_namespace())); _store(config->storage_namespace(), target_pubkey_hex, - timestamp.count(), + store_timestamp.count(), config->dump()); } @@ -300,7 +306,7 @@ void State::config_changed( _store(keys_config->storage_namespace(), target_pubkey_hex, - timestamp.count(), + store_timestamp.count(), keys_config->dump()); } } @@ -318,7 +324,7 @@ void State::config_changed( pubkey, push.namespace_seqno, success, status_code, response); // Now that we have confirmed the push we need to store the configs again - config_changed(pubkey, true, false); + config_changed(pubkey, true, false, std::nullopt); }); } log(LogLevel::debug, "config_changed: Complete"); @@ -509,11 +515,38 @@ PreparedPush State::prepare_push( return {to_unsigned(payload.dump()), namespace_seqnos}; } -std::vector State::merge( +std::optional max_merged_timestamp( + const std::vector& messages, + const std::vector& merged_hashes) { + // Filter messages based on merged_hashes + std::vector merged_messages; + std::copy_if( + messages.begin(), + messages.end(), + std::back_inserter(merged_messages), + [&merged_hashes](const config_message& msg) { + return std::find(merged_hashes.begin(), merged_hashes.end(), msg.hash) != + merged_hashes.end(); + }); + auto max_timestamp_message = std::max_element( + merged_messages.begin(), + merged_messages.end(), + [](const config_message& msg1, const config_message& msg2) { + return msg1.timestamp_ms < msg2.timestamp_ms; + }); + + if (max_timestamp_message != messages.end()) { + return max_timestamp_message->timestamp_ms; + } + + return std::nullopt; +} + +void State::merge( std::optional pubkey_hex, const std::vector& configs) { log(LogLevel::debug, "merge: Called with " + std::to_string(configs.size()) + " configs"); if (configs.empty()) - return {}; + return; // Sort the namespaces based on the order they should be merged in to minimise conflicts between // different config messages @@ -523,8 +556,7 @@ std::vector State::merge( }); bool is_group_merge = false; - std::vector good_hashes; - std::vector> pending_configs; + std::vector pending_configs; auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); @@ -538,7 +570,7 @@ std::vector State::merge( (i > 0 && config.namespace_ != sorted_configs[i - 1].namespace_)) pending_configs.clear(); - pending_configs.emplace_back(config.hash, config.data); + pending_configs.emplace_back(config); // If this is not a GroupKeys config, the last config or the next config is not in the same // namespace then go to the next loop so we can batch-merge the configs in a later loop @@ -550,31 +582,48 @@ std::vector State::merge( log(LogLevel::debug, "merge: Merging " + namespace_name(config.namespace_) + " config (" + std::string(target_pubkey_hex) + ")"); + std::vector> to_merge; + to_merge.reserve(pending_configs.size()); + std::transform( + pending_configs.begin(), + pending_configs.end(), + std::back_inserter(to_merge), + [](const config_message& msg) { + return std::pair{msg.hash, msg.data}; + }); std::vector merged_hashes; switch (config.namespace_) { case Namespace::Contacts: - merged_hashes = _config_contacts->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - config_changed(target_pubkey_hex, true, false); // Immediately store changes + merged_hashes = _config_contacts->merge(to_merge); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); continue; case Namespace::ConvoInfoVolatile: - merged_hashes = _config_convo_info_volatile->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - config_changed(target_pubkey_hex, true, false); // Immediately store changes + merged_hashes = _config_convo_info_volatile->merge(to_merge); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); continue; case Namespace::UserGroups: - merged_hashes = _config_user_groups->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - config_changed(target_pubkey_hex, true, false); // Immediately store changes + merged_hashes = _config_user_groups->merge(to_merge); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); continue; case Namespace::UserProfile: - merged_hashes = _config_user_profile->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - config_changed(target_pubkey_hex, true, false); // Immediately store changes + merged_hashes = _config_user_profile->merge(to_merge); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); continue; default: break; @@ -592,30 +641,42 @@ std::vector State::merge( auto members = group->members.get(); is_group_merge = true; - if (config.namespace_ == Namespace::GroupInfo) { - merged_hashes = info->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - } else if (config.namespace_ == Namespace::GroupMembers) { - merged_hashes = members->merge(pending_configs); - good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); - } else if (config.namespace_ == Namespace::GroupKeys) { - // GroupKeys doesn't support merging multiple messages at once so do them individually - if (group->keys->load_key_message( - config.hash, config.data, config.timestamp_ms, *info, *members)) { - good_hashes.emplace_back(config.hash); - } - } else - throw std::runtime_error{"merge: Attempted to merge from unknown namespace"}; + switch (config.namespace_) { + case Namespace::GroupInfo: + merged_hashes = info->merge(to_merge); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::GroupMembers: + merged_hashes = members->merge(to_merge); - config_changed(target_pubkey_hex, true, false); // Immediately store changes + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::GroupKeys: + // GroupKeys doesn't support merging multiple messages at once so do them + // individually + if (group->keys->load_key_message( + config.hash, config.data, config.timestamp_ms, *info, *members)) { + config_changed(target_pubkey_hex, true, false, config.timestamp_ms); + } + continue; + + default: throw std::runtime_error{"merge: Attempted to merge from unknown namespace"}; + } } // Now that all of the merges have been completed we want to trigger the `send` hook if - // there is a pending push - config_changed(target_pubkey_hex, false, true); + // there is a pending push (the 'server_timestamp_ms' is only needed for the `store` hook + // so no need to pass here) + config_changed(target_pubkey_hex, false, true, std::nullopt); log(LogLevel::debug, "merge: Complete"); - return good_hashes; } std::vector State::current_hashes(std::optional pubkey_hex) { @@ -963,12 +1024,12 @@ void State::create_group( // Manually trigger 'config_changed' because we modified '_config_user_groups' // directly rather than via the 'MutableUserConfigs' so it won't automatically get // triggered - config_changed(); + config_changed(std::nullopt, true, true, std::nullopt); // Now that we have a `_config_user_groups` entry for the group and have confirmed // the push we need to store the group configs (we can't do this until after the // `_config_user_groups` has been updated) - config_changed(gid, true, false); + config_changed(gid, true, false, std::nullopt); // Lastly trigger the 'callback' to communicate the group was successfully created cb(gid, secretkey, std::nullopt); @@ -1001,7 +1062,7 @@ void State::approve_group(std::string_view group_id, std::optional // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We // don't call it for the group config because there is no data so it's likely we are creating // the initial state upon accepting an invite so have no data yet) - config_changed(); + config_changed(std::nullopt, true, true, std::nullopt); } void State::load_group_admin_key(std::string_view group_id, ustring_view secret) { @@ -1036,8 +1097,8 @@ void State::load_group_admin_key(std::string_view group_id, ustring_view secret) // Trigger the 'config_changed' callbacks directly since we aren't using 'MutableUserConfig' (We // don't call it for the group config because there is no data so it's likely we are creating // the initial state upon accepting an invite so have no data yet) - config_changed(); - config_changed(group_id); + config_changed(std::nullopt, true, true, std::nullopt); + config_changed(group_id, true, true, std::nullopt); } void State::erase_group(std::string_view group_id, bool remove_user_record) { @@ -1055,7 +1116,7 @@ void State::erase_group(std::string_view group_id, bool remove_user_record) { // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We // don't call it for the group config because there is no data so it's likely we are creating // the initial state upon accepting an invite so have no data yet) - config_changed(); + config_changed(std::nullopt, true, true, std::nullopt); } // Template functions @@ -1128,7 +1189,7 @@ MutableUserConfigs State::mutable_config( }; MutableUserConfigs::~MutableUserConfigs() { - parent_state->config_changed(); + parent_state->config_changed(std::nullopt, true, true, std::nullopt); }; MutableGroupConfigs State::mutable_config( @@ -1159,7 +1220,7 @@ void MutableGroupConfigs::manual_send( }; MutableGroupConfigs::~MutableGroupConfigs() { - parent_state.config_changed(info.id); + parent_state.config_changed(info.id, true, true, std::nullopt); }; } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 64c3a039..3f5bccac 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -225,11 +225,7 @@ LIBSESSION_C_API bool state_has_pending_send(const state_object* state) { } LIBSESSION_C_API bool state_merge( - state_object* state, - const char* pubkey_hex_, - state_config_message* configs, - size_t count, - session_string_list** successful_hashes) { + state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count) { try { std::optional pubkey_hex; if (pubkey_hex_) @@ -245,9 +241,7 @@ LIBSESSION_C_API bool state_merge( configs[i].timestamp_ms, ustring{configs[i].data, configs[i].datalen}); - auto result = unbox(state).merge(pubkey_hex, confs); - unbox(state).log(LogLevel::info, "Merged " + std::to_string(result.size())); - *successful_hashes = make_string_list(result); + unbox(state).merge(pubkey_hex, confs); return true; } catch (const std::exception& e) { return set_error(state, e.what()); From 5ea15f313eb4d3ba7886f2e28a92a17d02bb37e7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 23 Feb 2024 17:30:15 +1100 Subject: [PATCH 14/24] Added a 'add_group_members' function to handle key rotation internally --- include/session/config/groups/keys.h | 29 -- include/session/config/namespaces.hpp | 2 +- include/session/state.h | 8 +- include/session/state.hpp | 56 ++-- include/session/state_groups.h | 26 ++ src/config/groups/keys.cpp | 34 --- src/state.cpp | 304 ++++++++++++++------- src/state_c_wrapper.cpp | 45 ++- tests/test_state.cpp | 377 ++++++++++++++++++++++---- 9 files changed, 649 insertions(+), 232 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index e02aa796..d8debc3a 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -96,35 +96,6 @@ LIBSESSION_EXPORT bool state_group_needs_rekey(const state_object* state, const LIBSESSION_EXPORT bool state_rekey_group(mutable_group_state_object* state) __attribute__((warn_unused_result)); -/// API: groups/state_supplement_group_key -/// -/// Generates a supplemental key message for one or more session IDs. This is used to distribute -/// existing active keys to a new member so that that member can access existing keys, configs, and -/// messages. Only admins can call this. -/// -/// The recommended order of operations for adding such a member is: -/// - add the member to Members -/// - generate the key supplement -/// - push new members & key supplement (ideally in a batch) -/// - send invite details, auth signature, etc. to the new user -/// -/// To add a member *without* giving them access to old messages you would use groups_keys_rekey() -/// instead of this method. -/// -/// Inputs: -/// - `state` -- [in] - Pointer to the mutable state object -/// - `sids` -- array of session IDs of the members to generate a supplemental key for; each element -/// must be an ordinary (null-terminated) C string containing the 66-character session id. -/// - `sids_len` -- length of the `sids` array -/// - `callback` -- [in] Callback function called once the send process completes -/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused -LIBSESSION_EXPORT void state_supplement_group_key( - mutable_group_state_object* state, - const char** sids, - size_t sids_len, - void (*callback)(bool success, void* ctx), - void* ctx); - /// API: groups/state_get_current_group_generation /// /// Returns the current generation number for the latest keys message. diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 93de4995..76818f53 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -74,7 +74,7 @@ namespace { /// Returns a number indicating the order that the config messages should be sent in, we need to /// send the `GroupKeys` config _before_ the `GroupInfo` and `GroupMembers` configs as they both /// get encrypted with the latest key and we want to avoid weird edge-cases - int namespace_store_order(const Namespace& n) { + int namespace_send_order(const Namespace& n) { if (n == Namespace::GroupKeys) return 0; return 1; diff --git a/include/session/state.h b/include/session/state.h index 51bb7f2d..16ba1e06 100644 --- a/include/session/state.h +++ b/include/session/state.h @@ -236,8 +236,14 @@ LIBSESSION_EXPORT bool state_has_pending_send(const state_object* state); /// bytes). Required for group dumps. /// - `configs` -- [in] Pointer to an array of `state_config_message` objects /// - `count` -- [in] Number of objects in `configs` +/// - `successful_hashes` -- [out] Pointer to an array of message hashes that were successfully +/// merged LIBSESSION_EXPORT bool state_merge( - state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count); + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + session_string_list** successful_hashes); /// API: state/state_current_hashes /// diff --git a/include/session/state.hpp b/include/session/state.hpp index 4649bf2c..c8f40264 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -85,11 +85,6 @@ class MutableGroupConfigs { std::optional> set_error; std::chrono::milliseconds get_network_offset() const; - void manual_send( - std::string pubkey_hex, - ustring payload, - std::function - received_response) const; ~MutableGroupConfigs(); }; @@ -275,25 +270,9 @@ class State { std::optional pubkey_hex, bool allow_store, bool allow_send, - std::optional server_timestamp_ms); - - /// API: state/State::manual_send - /// - /// This allows for manually triggering the `_send` hook as there are some operations (eg. - /// supplement group keys) which won't be detected as changes and need to be explicitly sent. - /// - /// Inputs: - /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. - /// - `payload` -- payload which should be sent to the API. - /// - `received_response` -- callback which should be called with the response from the send - /// request. - /// - /// Outputs: None - void manual_send( - std::string pubkey_hex, - ustring payload, - std::function - received_response) const; + std::optional server_timestamp_ms, + std::optional> + after_send = std::nullopt); /// API: state/State::merge /// @@ -319,7 +298,13 @@ class State { /// Required for group dumps. /// - `configs` -- vector of `config_message` types which include the data needed to properly /// merge. - void merge( + /// + /// Outputs: + /// - vector of successfully parsed hashes. Note that this does not mean the hash was recent or + /// that it changed the config, merely that the returned hash was properly parsed and + /// processed as a config message, even if it was too old to be useful (or was already known + /// to be included). The hashes will be in the same order as in the input vector. + std::vector merge( std::optional pubkey_hex, const std::vector& configs); /// API: state/State::current_hashes @@ -441,6 +426,27 @@ class State { /// secret key does not match the group's pubkey. void load_group_admin_key(std::string_view group_id, ustring_view secret); + /// API: groups/add_group_members + /// + /// Adds members to Members for the group and performs either a key rotation or a key + /// supplement. Only admins can call this. + /// + /// Invite details, auth signature, etc. will still need to be sent separately to the new user. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `supplemental_rotation` -- flag to control whether a supplemental (when true) or full + /// (when false) key rotation should be performed. Doing a supplemental rotation will + /// distributes the existing active keys so that the new members can access existing key, + /// configs and messages. + /// - `members` -- vector of members to add to the group. + /// - `callback` -- Callback function called once the send process completes. + void add_group_members( + std::string_view group_id, + bool supplemental_rotation, + const std::vector members, + std::function error)> callback); + /// API: groups/State::erase_group /// /// Removes the group state and, if specified, removes the group from the user groups config. diff --git a/include/session/state_groups.h b/include/session/state_groups.h index 54fb8fff..7af7c278 100644 --- a/include/session/state_groups.h +++ b/include/session/state_groups.h @@ -85,6 +85,32 @@ LIBSESSION_EXPORT void state_approve_group( LIBSESSION_EXPORT bool state_load_group_admin_key( state_object* state, const char* group_id, unsigned const char* seed); +/// API: groups/state_add_group_members +/// +/// Adds members to Members for the group and performs either a key rotation or a key supplement. +/// Only admins can call this. +/// +/// Invite details, auth signature, etc. will still need to be sent separately to the new user. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object. +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". +/// - `supplemental_rotation` -- [in] flag to control whether a supplemental (when true) or full +/// (when false) key rotation should be performed. Doing a supplemental rotation will distributes +/// the existing active keys so that the new members can access existing key, configs and messages. +/// - `members` -- [in] array of members to add to the group. +/// - `members_len` -- [in] length of the `members` array +/// - `callback` -- [in] Callback function called once the send process completes +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_add_group_members( + state_object* state, + const char* group_id, + const bool supplemental_rotation, + const state_group_member** members, + const size_t members_len, + void (*callback)(const char* error, void* ctx), + void* ctx); + /// API: groups/state_erase_group /// /// Removes the group state and, if specified, removes the group from the user groups config. diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index cc94b20c..566f74dd 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1458,40 +1458,6 @@ LIBSESSION_C_API bool state_rekey_group(mutable_group_state_object* state) { } } -LIBSESSION_C_API void state_supplement_group_key( - mutable_group_state_object* state, - const char** sids, - size_t sids_len, - void (*callback)(bool success, void* ctx), - void* ctx) { - assert(sids); - std::vector session_ids; - for (size_t i = 0; i < sids_len; i++) - session_ids.emplace_back(sids[i]); - - try { - auto msg = unbox(state).keys.key_supplement(session_ids); - std::chrono::milliseconds timestamp = - (std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) + - unbox(state).get_network_offset()); - auto [pubkey, payload] = unbox(state).keys.prepare_supplement_payload(msg, timestamp); - - unbox(state).manual_send( - pubkey, - payload, - [callback, ctx](bool success, int16_t status_code, ustring response) { - if (callback) - callback(success, ctx); - }); - } catch (const std::exception& e) { - if (auto set_error = unbox(state).set_error; set_error.has_value()) - set_error.value()(e.what()); - if (callback) - callback(false, ctx); - } -} - LIBSESSION_EXPORT int state_get_current_group_generation( const state_object* state, const char* group_id) { try { diff --git a/src/state.cpp b/src/state.cpp index 05c1be76..c46fb5fa 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -218,7 +218,9 @@ void State::config_changed( std::optional pubkey_hex, bool allow_store, bool allow_send, - std::optional server_timestamp_ms) { + std::optional server_timestamp_ms, + std::optional> + after_send) { auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); @@ -286,28 +288,30 @@ void State::config_changed( if (_store && needs_dump && allow_store) { std::chrono::milliseconds store_timestamp = std::chrono::milliseconds(server_timestamp_ms.value_or(timestamp.count())); + std::vector> sorted_stores; for (auto& config : configs) { - if (!config->needs_dump()) - continue; - log(LogLevel::debug, - "config_changed: call 'store' for " + namespace_name(config->storage_namespace())); - _store(config->storage_namespace(), - target_pubkey_hex, - store_timestamp.count(), - config->dump()); + if (config->needs_dump()) + sorted_stores.emplace_back(config->storage_namespace(), config->dump()); } // GroupKeys needs special handling as it's not a `ConfigBase` if (is_group_pubkey && _config_groups[target_pubkey_hex]->keys->needs_dump()) { - log(LogLevel::debug, - "config_changed: Group Keys config for " + target_pubkey_hex + " needs_dump"); - auto keys_config = _config_groups[target_pubkey_hex]->keys.get(); + auto config = _config_groups[target_pubkey_hex]->keys.get(); + sorted_stores.emplace_back(config->storage_namespace(), config->dump()); + } + + // Sort the namespaces based on the order they should be merged in to minimise the chance + // that config messages dependant on others are saved before their dependencies + std::sort(sorted_stores.begin(), sorted_stores.end(), [](const auto& a, const auto& b) { + return namespace_merge_order(a.first) < namespace_merge_order(b.first); + }); - _store(keys_config->storage_namespace(), - target_pubkey_hex, - store_timestamp.count(), - keys_config->dump()); + for (auto& info : sorted_stores) { + log(LogLevel::debug, + "config_changed: call 'store' for " + namespace_name(info.first) + " in " + + target_pubkey_hex); + _store(info.first, target_pubkey_hex, store_timestamp.count(), info.second); } } @@ -318,27 +322,25 @@ void State::config_changed( log(LogLevel::debug, "config_changed: Call 'send'"); _send(target_pubkey_hex, push.payload, - [this, pubkey = std::move(target_pubkey_hex), push]( + [this, + pubkey = std::move(target_pubkey_hex), + push, + after_send = std::move(after_send)]( bool success, uint16_t status_code, ustring response) { handle_config_push_response( pubkey, push.namespace_seqno, success, status_code, response); // Now that we have confirmed the push we need to store the configs again config_changed(pubkey, true, false, std::nullopt); + + // Call the 'after_send' callback if provided + if (after_send) + (*after_send)(success, status_code, response); }); } log(LogLevel::debug, "config_changed: Complete"); } -void State::manual_send( - std::string pubkey_hex, - ustring payload, - std::function received_response) - const { - if (_send) - _send(pubkey_hex, payload, received_response); -} - PreparedPush State::prepare_push( std::string pubkey_hex, std::chrono::milliseconds timestamp, @@ -465,8 +467,8 @@ PreparedPush State::prepare_push( // that config messages dependant on others are stored before their dependencies auto sorted_requests = requests; std::sort(sorted_requests.begin(), sorted_requests.end(), [](const auto& a, const auto& b) { - return namespace_store_order(static_cast(a["namespace"])) < - namespace_store_order(static_cast(b["namespace"])); + return namespace_send_order(static_cast(a["namespace"])) < + namespace_send_order(static_cast(b["namespace"])); }); std::vector> namespace_seqnos; @@ -486,7 +488,7 @@ PreparedPush State::prepare_push( // Ed25519 signature of `("delete" || messages...)` std::array sig; ustring verification = to_unsigned("delete"); - log(LogLevel::debug, "config_changed: has obsolete hashes"); + log(LogLevel::debug, "prepare_push: has obsolete hashes"); for (auto& hash : obsolete_hashes) verification += to_unsigned_sv(hash); @@ -494,7 +496,7 @@ PreparedPush State::prepare_push( crypto_sign_ed25519_detached( sig.data(), nullptr, verification.data(), verification.size(), seckey.data())) throw std::runtime_error{ - "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; nlohmann::json params{ {"messages", obsolete_hashes}, @@ -542,11 +544,11 @@ std::optional max_merged_timestamp( return std::nullopt; } -void State::merge( +std::vector State::merge( std::optional pubkey_hex, const std::vector& configs) { log(LogLevel::debug, "merge: Called with " + std::to_string(configs.size()) + " configs"); if (configs.empty()) - return; + return {}; // Sort the namespaces based on the order they should be merged in to minimise conflicts between // different config messages @@ -556,10 +558,15 @@ void State::merge( }); bool is_group_merge = false; + std::vector good_hashes; std::vector pending_configs; auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); + // Sanity check the size of the pubkey + if (target_pubkey_hex.size() != 66) + throw std::invalid_argument{"merge: Invalid pubkey_hex - expected 66 bytes"}; + for (size_t i = 0; i < sorted_configs.size(); ++i) { auto& config = sorted_configs[i]; @@ -596,6 +603,7 @@ void State::merge( switch (config.namespace_) { case Namespace::Contacts: merged_hashes = _config_contacts->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -604,6 +612,7 @@ void State::merge( case Namespace::ConvoInfoVolatile: merged_hashes = _config_convo_info_volatile->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -612,6 +621,7 @@ void State::merge( case Namespace::UserGroups: merged_hashes = _config_user_groups->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -620,6 +630,7 @@ void State::merge( case Namespace::UserProfile: merged_hashes = _config_user_profile->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -633,8 +644,6 @@ void State::merge( if (!pubkey_hex) throw std::invalid_argument{ "merge: Invalid pubkey_hex - required for group config namespaces"}; - if (target_pubkey_hex.size() != 66) - throw std::invalid_argument{"merge: Invalid pubkey_hex - expected 66 bytes"}; auto& group = _config_groups.at(target_pubkey_hex); auto info = group->info.get(); @@ -644,6 +653,7 @@ void State::merge( switch (config.namespace_) { case Namespace::GroupInfo: merged_hashes = info->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -652,6 +662,7 @@ void State::merge( case Namespace::GroupMembers: merged_hashes = members->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); // Immediately store changes if a merge was successful if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) @@ -663,6 +674,7 @@ void State::merge( // individually if (group->keys->load_key_message( config.hash, config.data, config.timestamp_ms, *info, *members)) { + good_hashes.emplace_back(config.hash); config_changed(target_pubkey_hex, true, false, config.timestamp_ms); } continue; @@ -671,12 +683,26 @@ void State::merge( } } - // Now that all of the merges have been completed we want to trigger the `send` hook if - // there is a pending push (the 'server_timestamp_ms' is only needed for the `store` hook - // so no need to pass here) + // If two admins rekeyed for different member changes at the same time then there is a "key + // collision" and the "needs rekey" function will return true to indicate that a 3rd `rekey` + // needs to be made to have a final set of keys which includes all members + if (is_group_merge) { + auto& group = _config_groups.at(target_pubkey_hex); + + if (group->keys->needs_rekey()) { + auto info = group->info.get(); + auto members = group->members.get(); + group->keys->rekey(*info, *members); + } + } + + // Now that all of the merges have been completed we want to trigger the `send` hook just in + // case if there is a pending push (the 'server_timestamp_ms' is only needed for the `store` + // hook so no need to pass here) config_changed(target_pubkey_hex, false, true, std::nullopt); log(LogLevel::debug, "merge: Complete"); + return good_hashes; } std::vector State::current_hashes(std::optional pubkey_hex) { @@ -780,18 +806,79 @@ ustring State::dump(config::Namespace namespace_, std::optional extract_error(int status_code, ustring response) { + if (response.empty()) + return std::nullopt; + + std::string response_string = {from_unsigned(response.data()), response.size()}; + + try { + auto response_json = nlohmann::json::parse(response); + + // If the status code for the root request failed then try to extract the 'reason', + // otherwise just return the response as the error + if (status_code < 200 || status_code > 299) { + if (response_json.contains("reason")) + return response_json["reason"].get(); + else + return response_string; + } + + // If it wasn't a batch/sequence request then assume it was successful and return no error + if (!response_json.contains("results")) + return std::nullopt; + + auto results = response_json["results"]; + + // Check if all of the results has the same status code + int single_status_code = -1; + std::optional error_body; + for (const auto& result : results.items()) { + // Invalid subresponse, just return the response as the error + if (!result.value().contains("code")) + return response_string; + + auto code = result.value()["code"].get(); + + // If the code was different from all former codes then there wasn't a single error (ie. + // it needs specific handling) so return no error + if (single_status_code != -1 && code != single_status_code) + return std::nullopt; + + single_status_code = code; + + if (result.value().contains("body") && result.value()["body"].is_string()) + error_body = result.value()["body"].get(); + } + + // Return the error if all results failed with the same error + if (single_status_code < 200 || single_status_code > 299) { + // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just + // in case) + if (single_status_code == 406 || single_status_code == 425) + return "The user's clock is out of sync with the service node network."; + + return error_body.value_or( + "Failed with status code: " + std::to_string(single_status_code) + "."); + } + + return std::nullopt; + } catch (...) { + return response_string; + } +} + void State::handle_config_push_response( std::string pubkey, std::vector> namespace_seqnos, bool success, uint16_t status_code, ustring response) { - std::string response_string = {from_unsigned(response.data()), response.size()}; - // If the request failed then just error - if (!success || (status_code < 200 && status_code > 299)) - throw std::invalid_argument{ - "handle_config_push_response: Request failed with data - " + response_string}; + if (auto error = extract_error(status_code, response); error) + throw std::runtime_error{*error}; + + log(LogLevel::debug, "handle_config_push_response: No simple error detected."); // Otherwise process the response data auto response_json = nlohmann::json::parse(response); @@ -804,52 +891,9 @@ void State::handle_config_push_response( throw std::invalid_argument{ "handle_config_push_response: Invalid response - 'results' array is empty"}; + // If the response includes a timestamp value then we should update the network offset auto results = response_json["results"]; - // Check if all of the results has the same status code - int single_status_code = -1; - std::optional error_body; - for (const auto& result : results.items()) { - if (!result.value().contains("code")) - throw std::invalid_argument{ - "handle_config_push_response: Invalid result - expected to contain 'code'" + - result.value().dump()}; - - // If the code was different from all former codes then break the loop - auto code = result.value()["code"].get(); - - if (single_status_code != -1 && code != single_status_code) { - single_status_code = 200; - error_body = std::nullopt; - break; - } - - single_status_code = code; - - if (result.value().contains("body") && result.value()["body"].is_string()) - error_body = result.value()["body"].get(); - } - - // Throw if all results failed with the same error - if (single_status_code < 200 || single_status_code > 299) { - auto error = "Failed with status code: " + std::to_string(single_status_code) + "."; - - // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just in - // case) - if (single_status_code == 406 || single_status_code == 425) - error = "The user's clock is out of sync with the service node network."; - else if (single_status_code == 401) - error = "Unauthorised (signature verification failed)."; - - if (error_body) - error += " Server error: " + *error_body + "."; - - throw std::runtime_error{error}; - } - log(LogLevel::debug, - "handle_config_push_response: Doesn't have a consistent error across requests"); - - // If the response includes a timestamp value then we should update the network offset if (auto first_result = results[0]; first_result.contains("body") && first_result["body"].contains("t")) network_offset = @@ -1101,6 +1145,86 @@ void State::load_group_admin_key(std::string_view group_id, ustring_view secret) config_changed(group_id, true, true, std::nullopt); } +void State::add_group_members( + std::string_view group_id, + bool supplemental_rotation, + const std::vector members, + std::function error)> callback) { + if (members.empty()) { + callback(std::nullopt); + return; + } + + std::string gid = {group_id.data(), group_id.size()}; + auto& group = _config_groups.at(gid); + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + // Add the members to Members + for (auto m : members) + group->members->set(m); + + // If it's not a supplemental rotation then do a rekey + if (!supplemental_rotation) { + auto info = _config_groups[gid]->info.get(); + auto members = _config_groups[gid]->members.get(); + group->keys->rekey(*info, *members); + } + + // Prepare the push payload for the group configs + std::vector configs = {group->info.get(), group->members.get()}; + auto push = prepare_push(gid, timestamp, configs); + + // If it is a supplemental rotation then we want to include the key supplement within the batch + // request we are going to send + if (supplemental_rotation) { + std::vector sids; + std::transform( + members.begin(), + members.end(), + std::back_inserter(sids), + [](const groups::member& m) { return m.session_id; }); + auto msg = group->keys->key_supplement(sids); + auto [pubkey, payload] = group->keys->prepare_supplement_payload(msg, timestamp); + + // We need to update the current payload (we want to insert the supplement to the beginning + // of the batch requests) + auto updated_namespace_seqnos = push.namespace_seqno; + auto updated_payload = nlohmann::json::parse(push.payload); + auto payload_json = nlohmann::json::parse(payload); + auto requests_ptr = nlohmann::json::json_pointer("/params/requests"); + + if (!updated_payload.contains(requests_ptr)) + throw std::runtime_error{"add_group_members: Prepared payload structure is invalid."}; + + // No seqno for keys messages + auto updated_requests = updated_payload[requests_ptr]; + updated_requests.insert(updated_requests.begin(), payload_json); + updated_payload[requests_ptr] = updated_requests; + updated_namespace_seqnos.insert( + updated_namespace_seqnos.begin(), {group->keys->storage_namespace(), 0}); + push = {to_unsigned(updated_payload.dump()), updated_namespace_seqnos}; + } + + _send(gid, + push.payload, + [this, + gid = std::move(gid), + namespace_seqno = push.namespace_seqno, + cb = std::move(callback)](bool success, int16_t status_code, ustring response) { + try { + // Call through to the default 'handle_config_push_response' first to update it's + // state correctly (this will also result in the configs getting stored to disk) + handle_config_push_response(gid, namespace_seqno, success, status_code, response); + cb(std::nullopt); + } catch (const std::exception& e) { + cb(e.what()); + } + }); +} + void State::erase_group(std::string_view group_id, bool remove_user_record) { std::string gid = {group_id.data(), group_id.size()}; @@ -1211,14 +1335,6 @@ std::chrono::milliseconds MutableGroupConfigs::get_network_offset() const { return parent_state.network_offset; }; -void MutableGroupConfigs::manual_send( - std::string pubkey_hex, - ustring payload, - std::function received_response) - const { - parent_state.manual_send(pubkey_hex, payload, received_response); -}; - MutableGroupConfigs::~MutableGroupConfigs() { parent_state.config_changed(info.id, true, true, std::nullopt); }; diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 3f5bccac..f743ef86 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -225,7 +225,11 @@ LIBSESSION_C_API bool state_has_pending_send(const state_object* state) { } LIBSESSION_C_API bool state_merge( - state_object* state, const char* pubkey_hex_, state_config_message* configs, size_t count) { + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + session_string_list** successful_hashes) { try { std::optional pubkey_hex; if (pubkey_hex_) @@ -241,7 +245,10 @@ LIBSESSION_C_API bool state_merge( configs[i].timestamp_ms, ustring{configs[i].data, configs[i].datalen}); - unbox(state).merge(pubkey_hex, confs); + auto result = unbox(state).merge(pubkey_hex, confs); + + if (successful_hashes) + *successful_hashes = make_string_list(result); return true; } catch (const std::exception& e) { return set_error(state, e.what()); @@ -430,6 +437,40 @@ LIBSESSION_C_API bool state_load_group_admin_key( } } +LIBSESSION_C_API void state_add_group_members( + state_object* state, + const char* group_id, + const bool supplemental_rotation, + const state_group_member** members_, + const size_t members_len, + void (*callback)(const char* error, void* ctx), + void* ctx) { + assert(members_); + try { + std::vector members; + for (size_t i = 0; i < members_len; i++) + members.emplace_back(groups::member{*members_[i]}); + + unbox(state).add_group_members( + {group_id, 66}, + supplemental_rotation, + members, + [cb = std::move(callback), ctx](std::optional error) { + if (cb) { + if (error) + (*cb)(error->data(), ctx); + else + (*cb)(nullptr, ctx); + } + }); + } catch (const std::exception& e) { + set_error(state, e.what()); + + if (callback) + callback(e.what(), ctx); + } +} + LIBSESSION_C_API void state_erase_group( state_object* state, const char* group_id, bool remove_user_record) { try { diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 47e08c67..0e81f6c4 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -39,66 +39,78 @@ TEST_CASE("State", "[state][state]") { "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto state = State({ed_sk.data(), ed_sk.size()}, {}); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; + std::vector store_records; + std::vector send_records; - state.on_store([&last_store]( + state.on_store([&store_records]( config::Namespace namespace_, std::string pubkey, uint64_t timestamp_ms, ustring data) { - last_store = {namespace_, pubkey, timestamp_ms, data}; + store_records.push_back({namespace_, pubkey, timestamp_ms, data}); }); state.on_send( - [&last_send]( + [&send_records]( std::string pubkey, ustring payload, response_callback_t received_response) { // Replicate the behaviour in the C wrapper auto on_response = std::make_unique(std::move(received_response)); - last_send = { - pubkey, - payload, - [](bool success, - int16_t status_code, - const unsigned char* res, - size_t reslen, - void* callback_context) { - try { - // Recapture the std::function callback here in a unique_ptr so that - // we clean it up at the end of this lambda. - std::unique_ptr cb{ - static_cast(callback_context)}; - (*cb)(success, status_code, {res, reslen}); - return true; - } catch (...) { - return false; - } - }, - nullptr, - on_response.release()}; + send_records.push_back( + {pubkey, + payload, + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so + // that we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + nullptr, + on_response.release()}); }); // Sanity check direct config access CHECK_FALSE(state.config().get_name().has_value()); state.mutable_config().user_profile.set_name("Test Name"); CHECK(state.config().get_name() == "Test Name"); - CHECK(last_store->namespace_ == Namespace::UserProfile); - CHECK(last_store->pubkey == + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].namespace_ == Namespace::UserProfile); + CHECK(store_records[0].pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); - CHECK(oxenc::to_hex(last_store->data.begin(), last_store->data.end()) == + CHECK(oxenc::to_hex(store_records[0].data.begin(), store_records[0].data.end()) == "64313a21693165313a2438343a64313a23693165313a2664313a6e393a54657374204e616d6565313a3c6c6c" "69306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a" "3d64313a6e303a6565313a28303a313a296c6565"); - CHECK(last_send->pubkey == + CHECK(send_records[0].pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" "6"); - auto send_data_no_ts = replace_suffix_between(to_sv(last_send->payload), (13 + 22), 22, "0"); - auto send_data_no_sig = replace_suffix_between(send_data_no_ts, (37 + 88), 37, "sig"); - CHECK(send_data_no_sig == - "{\"method\":\"sequence\",\"params\":{\"requests\":[{\"method\":\"store\",\"params\":{" - "\"data\":" - "\"CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" + auto send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(send_data.value( + nlohmann::json::json_pointer("/params/requests/0/params/pubkey_ed25519"), "") == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == + 2); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") == + "CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" "piV1rPki0Quo5Zp34MwwdZXqMFEwRpKGZJwpFPSre6jln5XlmH8tnq8djJo/" "7QP8kH4m8uUfzsRNgZ1K6agbnGgRolBXgk86/" "yFmmEsyC81rJF1dgqtkmOhA3nIFpk+yaPt5U5BzsELMQj3sydDB+" @@ -106,19 +118,25 @@ TEST_CASE("State", "[state][state]") { "eo0Zovg012oOixj1Uq9I7M9fajgklO+GmE3I3LFGXkmDoDwLYyPavWe68FU8zV9OtFFfUKdIxRJUTZXgU8Kwxzc/" "U3RzIm8Sc7APgIPkJsTmJr+ckYzLEdzbrqae4gxvzFB22lZYt62rg7KVoaBWUcB3NgFhTxMGc37ysti0pfoxO/" "T+zkKertLqX+iWNZLRhy3kLaXhEkqafYQzikepvhzD8/" - "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/" - "yAA\"," - "\"namespace\":2,\"pubkey\":" - "\"0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46\",\"pubkey_" - "ed25519\":\"8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f\"," - "\"signature\":\"sig\",\"timestamp\":0,\"ttl\":2592000000}}]}}"); + "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/yAA"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == + 2592000000); CHECK(state.config().get_seqno() == 1); // Confirm the push ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + REQUIRE(send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context)); + CHECK(store_records.size() == 2); // Should call store after confirming the push + CHECK_FALSE(state.config().needs_push()); // Init with dumps auto dump = state.dump(Namespace::UserProfile); @@ -133,6 +151,274 @@ TEST_CASE("State", "[state][state]") { CHECK_FALSE(state3.config().get_name().has_value()); state3.load(Namespace::UserProfile, std::nullopt, dump); CHECK(state3.config().get_name() == "Test Name"); + + // Creating a group works correctly + session::config::profile_pic p; + { + // These don't stay alive, so we use set_key/set_url to make a local copy: + ustring key = "qwerty78901234567890123456789012"_bytes; + std::string url = "http://example.com/huge.bmp"; + p.set_key(std::move(key)); + p.url = std::move(url); + } + const std::array member_seeds = { + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118", // member3 + }; + std::vector members; + members.reserve(member_seeds.size()); + for (auto i = 0; i < member_seeds.size(); i++) { + auto m = groups::member(member_seeds[i]); + m.set_name("Member " + std::to_string(i)); + members.emplace_back(m); + } + + state.create_group( + "TestName", + "TestDesc", + std::move(p), + members, + [&state]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { + REQUIRE_FALSE(error.has_value()); + + auto g = state.config().get_group(group_id); + REQUIRE(g.has_value()); + CHECK(g->name == "TestName"); + CHECK(g->secretkey == group_sk); + CHECK(state.config(group_id).get_seqno() == 1); + CHECK(state.config(group_id).get_seqno() == 1); + CHECK(state.config(group_id).current_generation() == 0); + CHECK(state.config(group_id).admin()); + }); + + REQUIRE(send_records.size() == 2); + send_data = nlohmann::json::parse(send_records[1].payload); + REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") + .substr(0, 2) == "03"); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == + 12); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") + .size() == 5324); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == + 2592000000); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/pubkey"), "") + .substr(0, 2) == "03"); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/namespace"), 0) == + 13); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/data"), "") + .size() == 684); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/ttl"), 0L) == + 2592000000); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/pubkey"), "") + .substr(0, 2) == "03"); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/namespace"), 0) == + 14); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/data"), "") + .size() == 684); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/ttl"), 0L) == + 2592000000); + + CHECK_FALSE(state.config().needs_push()); + CHECK(store_records.size() == + 2); // Shouldn't store anything until we process a success response + send_response = to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":" + "{\"hash\":\"fakehash3\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}}]}"); + REQUIRE(send_records[1].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[1].callback_context)); + CHECK(store_records.size() == 6); + CHECK(store_records[2].namespace_ == Namespace::UserGroups); + CHECK(store_records[3].namespace_ == Namespace::GroupKeys); + CHECK(store_records[4].namespace_ == Namespace::GroupInfo); + CHECK(store_records[5].namespace_ == Namespace::GroupMembers); + CHECK(state.config().get_seqno() == 1); + CHECK(state.config().needs_push()); + + // Prepare to merge the group data + std::vector to_merge; + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash2", + send_data[nlohmann::json::json_pointer("/params/requests/0/params/timestamp")] + .get(), + to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" + "requests/0/" + "params/data")] + .get()))); + to_merge.emplace_back( + Namespace::GroupInfo, + "fakehash3", + send_data[nlohmann::json::json_pointer("/params/requests/1/params/timestamp")] + .get(), + to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" + "requests/1/" + "params/data")] + .get()))); + to_merge.emplace_back( + Namespace::GroupMembers, + "fakehash4", + send_data[nlohmann::json::json_pointer("/params/requests/2/params/timestamp")] + .get(), + to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" + "requests/2/" + "params/data")] + .get()))); + + // Once the create group 'send' is confirm we add the group to UserGroups and also need to send + // that + REQUIRE(send_records.size() == 3); + send_data = nlohmann::json::parse(send_records[2].payload); + REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(send_data.value( + nlohmann::json::json_pointer("/params/requests/0/params/pubkey_ed25519"), "") == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == + 5); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") + .size() == 576); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == + 2592000000); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash5\"}}]}"); + REQUIRE(send_records[2].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[2].callback_context)); + + REQUIRE(state.config().size_groups() == 1); + auto member4_sid = "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"; + auto group = *state.config().begin_groups(); + CHECK(state.config(group.id).get_seqno() == 1); + CHECK(state.config(group.id).get_seqno() == 1); + CHECK(state.config(group.id).current_generation() == 0); + CHECK_FALSE(state.config().needs_push()); + + // Keys only get loaded when merging so we need to trigger the merge + auto result = state.merge(group.id, to_merge); + REQUIRE(result.size() == 3); + CHECK(result[0] == "fakehash2"); + CHECK(result[1] == "fakehash3"); + CHECK(result[2] == "fakehash4"); + CHECK(send_records.size() == 3); + + // Check that the supplemental rotation calls everything correctly + std::vector supplemental_members; + supplemental_members.emplace_back(member4_sid); + state.add_group_members( + group.id, true, supplemental_members, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 4); + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == + group.id); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == + 12); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") + .size() == 264); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == + 2592000000); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/method"), "") == + "stor" + "e"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/pubkey"), "") == + group.id); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/namespace"), 0) == + 14); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/data"), "") + .size() == 684); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/signature"), "") + .size() == 88); + CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/ttl"), 0L) == + 2592000000); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/method"), "") == + "delete"); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/pubkey"), "") == + group.id); + CHECK_FALSE( + send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/" + "pubkey_ed25519"))); + CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/signature"), "") + .size() == 88); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests/2/params/messages")] + .is_array()); + REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests/2/params/messages")].size() == + 2); + REQUIRE(send_data.value( + nlohmann::json::json_pointer("/params/requests/2/params/messages/0"), "") == + "fakehash3"); + REQUIRE(send_data.value( + nlohmann::json::json_pointer("/params/requests/2/params/messages/1"), "") == + "fakehash4"); } TEST_CASE("State c API", "[state][state][c]") { @@ -188,5 +474,4 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(state_get_profile_name(state2) == nullptr); CHECK(state_load(state2, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); CHECK(state_get_profile_name(state2) == "Test Name"sv); -} - +} \ No newline at end of file From 729ad3ccc77bb77ac7390fb67b6d1eee3768e317 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 23 Feb 2024 17:48:55 +1100 Subject: [PATCH 15/24] Tweak the C API for the add_group_members function --- include/session/state_groups.h | 4 ++-- src/state_c_wrapper.cpp | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/include/session/state_groups.h b/include/session/state_groups.h index 7af7c278..8d6781e7 100644 --- a/include/session/state_groups.h +++ b/include/session/state_groups.h @@ -106,9 +106,9 @@ LIBSESSION_EXPORT void state_add_group_members( state_object* state, const char* group_id, const bool supplemental_rotation, - const state_group_member** members, + const state_group_member* members, const size_t members_len, - void (*callback)(const char* error, void* ctx), + void (*callback)(const char* error, const size_t error_len, void* ctx), void* ctx); /// API: groups/state_erase_group diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index f743ef86..20eb4328 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -441,15 +441,17 @@ LIBSESSION_C_API void state_add_group_members( state_object* state, const char* group_id, const bool supplemental_rotation, - const state_group_member** members_, + const state_group_member* members_, const size_t members_len, - void (*callback)(const char* error, void* ctx), + void (*callback)(const char* error, size_t error_len, void* ctx), void* ctx) { assert(members_); try { std::vector members; + members.reserve(members_len); + for (size_t i = 0; i < members_len; i++) - members.emplace_back(groups::member{*members_[i]}); + members.emplace_back(groups::member{members_[i]}); unbox(state).add_group_members( {group_id, 66}, @@ -458,16 +460,16 @@ LIBSESSION_C_API void state_add_group_members( [cb = std::move(callback), ctx](std::optional error) { if (cb) { if (error) - (*cb)(error->data(), ctx); + (*cb)(error->data(), error->size(), ctx); else - (*cb)(nullptr, ctx); + (*cb)(nullptr, 0, ctx); } }); } catch (const std::exception& e) { - set_error(state, e.what()); + std::string_view error = e.what(); if (callback) - callback(e.what(), ctx); + callback(e.what(), error.size(), ctx); } } From edda5cf8f7c7ed7cfcec78b7d10da4501512351c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 26 Feb 2024 18:17:58 +1100 Subject: [PATCH 16/24] Added more unit tests, fixed a few bugs --- include/session/state.hpp | 14 +- include/session/state_groups.h | 4 +- src/state.cpp | 99 ++-- src/state_c_wrapper.cpp | 36 +- tests/test_config_user_groups.cpp | 2 +- tests/test_group_keys.cpp | 129 ++--- tests/test_state.cpp | 788 ++++++++++++++++++++++-------- 7 files changed, 740 insertions(+), 332 deletions(-) diff --git a/include/session/state.hpp b/include/session/state.hpp index c8f40264..c0688f8a 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -125,8 +125,15 @@ struct config_message { }; struct PreparedPush { + struct Info { + bool is_config_push; + bool requires_response; + config::Namespace namespace_; + seqno_t seqno; + }; + ustring payload; - std::vector> namespace_seqno; + std::vector info; }; class State { @@ -410,8 +417,7 @@ class State { /// /// Inputs: /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". - /// - `group_sk` -- optional 64-byte secret key for the group. - void approve_group(std::string_view group_id, std::optional group_sk); + void approve_group(std::string_view group_id); /// API: groups/State::load_group_admin_key /// @@ -486,7 +492,7 @@ class State { std::optional group_sk = std::nullopt); void handle_config_push_response( std::string pubkey, - std::vector> namespace_seqnos, + std::vector push_info, bool success, uint16_t status_code, ustring response); diff --git a/include/session/state_groups.h b/include/session/state_groups.h index 8d6781e7..685fabbe 100644 --- a/include/session/state_groups.h +++ b/include/session/state_groups.h @@ -64,9 +64,7 @@ LIBSESSION_EXPORT void state_create_group( /// Inputs: /// - `state` -- Pointer to the mutable state object /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". -/// - `group_sk` -- optional 64-byte secret key for the group. -LIBSESSION_EXPORT void state_approve_group( - state_object* state, const char* group_id, unsigned const char* group_sk); +LIBSESSION_EXPORT void state_approve_group(state_object* state, const char* group_id); /// API: groups/state_load_group_admin_key /// diff --git a/src/state.cpp b/src/state.cpp index c46fb5fa..6d99e0b4 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -251,10 +251,6 @@ void State::config_changed( // Other namespaces are unique for a given pubkey_hex_ if (target_pubkey_hex.size() != 66) throw std::invalid_argument{"config_changed: Invalid pubkey_hex - expected 66 bytes"}; - if (!_config_groups.count(target_pubkey_hex)) - throw std::runtime_error{ - "config_changed: Change trigger in group configs with no state: " + - target_pubkey_hex}; // Ensure we have the admin key for the group auto user_group_info = _config_user_groups->get_group(target_pubkey_hex); @@ -327,8 +323,7 @@ void State::config_changed( push, after_send = std::move(after_send)]( bool success, uint16_t status_code, ustring response) { - handle_config_push_response( - pubkey, push.namespace_seqno, success, status_code, response); + handle_config_push_response(pubkey, push.info, success, status_code, response); // Now that we have confirmed the push we need to store the configs again config_changed(pubkey, true, false, std::nullopt); @@ -471,12 +466,12 @@ PreparedPush State::prepare_push( namespace_send_order(static_cast(b["namespace"])); }); - std::vector> namespace_seqnos; + std::vector push_info; nlohmann::json sequence_params; for (auto& request : sorted_requests) { - namespace_seqnos.push_back( - {request["namespace"].get(), request["seqno"].get()}); + push_info.push_back( + {true, true, request["namespace"].get(), request["seqno"].get()}); request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload nlohmann::json request_json{{"method", "store"}, {"params", request}}; @@ -489,6 +484,7 @@ PreparedPush State::prepare_push( std::array sig; ustring verification = to_unsigned("delete"); log(LogLevel::debug, "prepare_push: has obsolete hashes"); + for (auto& hash : obsolete_hashes) verification += to_unsigned_sv(hash); @@ -510,11 +506,14 @@ PreparedPush State::prepare_push( nlohmann::json request_json{{"method", "delete"}, {"params", params}}; sequence_params["requests"].push_back(request_json); + + // Not strictly needed but means the request count will match + push_info.push_back({false, false, Namespace::UserProfile, 0}); } nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; - return {to_unsigned(payload.dump()), namespace_seqnos}; + return {to_unsigned(payload.dump()), push_info}; } std::optional max_merged_timestamp( @@ -870,7 +869,7 @@ std::optional extract_error(int status_code, ustring response) { void State::handle_config_push_response( std::string pubkey, - std::vector> namespace_seqnos, + std::vector push_info, bool success, uint16_t status_code, ustring response) { @@ -901,12 +900,19 @@ void State::handle_config_push_response( std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch())); - if (results.size() < namespace_seqnos.size()) + size_t required_response_count = + std::count_if(push_info.begin(), push_info.end(), [](const PreparedPush::Info& info) { + return info.requires_response; + }); + if (results.size() != required_response_count) throw std::invalid_argument{ "handle_config_push_response: Invalid response - Number of responses doesn't match " - "the number of requests."}; + "the number of requests requiring responses."}; for (int i = 0, n = results.size(); i < n; ++i) { + if (!push_info[i].is_config_push) + continue; + auto result_code = results[i]["code"].get(); if (result_code < 200 || result_code > 299 || !results[i].contains("body") || @@ -914,17 +920,19 @@ void State::handle_config_push_response( continue; auto hash = results[i]["body"]["hash"].get(); - auto seqno = namespace_seqnos[i].second; - auto namespace_ = namespace_seqnos[i].first; - switch (namespace_) { - case Namespace::Contacts: _config_contacts->confirm_pushed(seqno, hash); continue; + switch (push_info[i].namespace_) { + case Namespace::Contacts: + _config_contacts->confirm_pushed(push_info[i].seqno, hash); + continue; case Namespace::ConvoInfoVolatile: - _config_convo_info_volatile->confirm_pushed(seqno, hash); + _config_convo_info_volatile->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::UserGroups: + _config_user_groups->confirm_pushed(push_info[i].seqno, hash); continue; - case Namespace::UserGroups: _config_user_groups->confirm_pushed(seqno, hash); continue; case Namespace::UserProfile: - _config_user_profile->confirm_pushed(seqno, hash); + _config_user_profile->confirm_pushed(push_info[i].seqno, hash); continue; default: break; } @@ -932,9 +940,13 @@ void State::handle_config_push_response( // Other namespaces are unique for a given pubkey auto& group = _config_groups.at(pubkey); - switch (namespace_) { - case Namespace::GroupInfo: group->info->confirm_pushed(seqno, hash); - case Namespace::GroupMembers: group->members->confirm_pushed(seqno, hash); + switch (push_info[i].namespace_) { + case Namespace::GroupInfo: + group->info->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::GroupMembers: + group->members->confirm_pushed(push_info[i].seqno, hash); + continue; case Namespace::GroupKeys: continue; // No need to do anything here default: throw std::runtime_error{ @@ -1046,7 +1058,7 @@ void State::create_group( push.payload, [this, gid = std::move(group_id), - namespace_seqno = push.namespace_seqno, + push_info = push.info, secretkey = std::move(ed_sk), n = std::move(name), timestamp, @@ -1054,7 +1066,7 @@ void State::create_group( try { // Call through to the default 'handle_config_push_response' first to update it's // state correctly (this will also result in the configs getting stored to disk) - handle_config_push_response(gid, namespace_seqno, success, status_code, response); + handle_config_push_response(gid, push_info, success, status_code, response); // Retrieve the group configs for this pubkey and setup an entry in the user // groups config for it (the 'at' call will throw if the group doesn't exist) @@ -1083,7 +1095,7 @@ void State::create_group( }); } -void State::approve_group(std::string_view group_id, std::optional group_sk) { +void State::approve_group(std::string_view group_id) { std::string gid = {group_id.data(), group_id.size()}; // If we don't already have GroupConfigs then create them @@ -1091,16 +1103,12 @@ void State::approve_group(std::string_view group_id, std::optional auto ed_pk_data = oxenc::from_hex(group_id.begin() + 2, group_id.end()); auto ed_pk = to_unsigned_sv(ed_pk_data); _config_groups[gid] = - std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); + std::make_unique(ed_pk, to_unsigned_sv(_user_sk), std::nullopt); } // Update the USER_GROUPS config to have the group marked as approved auto group = _config_user_groups->get_or_construct_group(group_id); group.invited = false; - - if (group_sk) - group.secretkey = {group_sk->data(), group_sk->size()}; - _config_user_groups->set(group); // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We @@ -1132,6 +1140,7 @@ void State::load_group_admin_key(std::string_view group_id, ustring_view secret) member.admin = true; member.invite_status = 0; // Just in case member.promotion_status = 0; + group->members->set(member); // Update the user groups record to include the admin key auto user_group = _config_user_groups->get_or_construct_group(group_id); @@ -1166,8 +1175,13 @@ void State::add_group_members( for (auto m : members) group->members->set(m); - // If it's not a supplemental rotation then do a rekey - if (!supplemental_rotation) { + // Don't bother rotating the keys if there are only admins + size_t non_admin_count = std::count_if(members.begin(), members.end(), [](const groups::member& m) { + return !m.admin; + }); + + // If there are non-admins and it's not a supplemental rotation then do a rekey + if (non_admin_count > 0 && !supplemental_rotation) { auto info = _config_groups[gid]->info.get(); auto members = _config_groups[gid]->members.get(); group->keys->rekey(*info, *members); @@ -1177,9 +1191,9 @@ void State::add_group_members( std::vector configs = {group->info.get(), group->members.get()}; auto push = prepare_push(gid, timestamp, configs); - // If it is a supplemental rotation then we want to include the key supplement within the batch + // If there are non-admins and it's a supplemental rotation then we want to include the key supplement within the batch // request we are going to send - if (supplemental_rotation) { + if (non_admin_count > 0 && supplemental_rotation) { std::vector sids; std::transform( members.begin(), @@ -1191,7 +1205,7 @@ void State::add_group_members( // We need to update the current payload (we want to insert the supplement to the beginning // of the batch requests) - auto updated_namespace_seqnos = push.namespace_seqno; + auto updated_push_info = push.info; auto updated_payload = nlohmann::json::parse(push.payload); auto payload_json = nlohmann::json::parse(payload); auto requests_ptr = nlohmann::json::json_pointer("/params/requests"); @@ -1203,21 +1217,18 @@ void State::add_group_members( auto updated_requests = updated_payload[requests_ptr]; updated_requests.insert(updated_requests.begin(), payload_json); updated_payload[requests_ptr] = updated_requests; - updated_namespace_seqnos.insert( - updated_namespace_seqnos.begin(), {group->keys->storage_namespace(), 0}); - push = {to_unsigned(updated_payload.dump()), updated_namespace_seqnos}; + updated_push_info.insert(updated_push_info.begin(), {false, true, Namespace::UserProfile, 0}); + push = {to_unsigned(updated_payload.dump()), updated_push_info}; } _send(gid, push.payload, - [this, - gid = std::move(gid), - namespace_seqno = push.namespace_seqno, - cb = std::move(callback)](bool success, int16_t status_code, ustring response) { + [this, gid = std::move(gid), push_info = push.info, cb = std::move(callback)]( + bool success, int16_t status_code, ustring response) { try { // Call through to the default 'handle_config_push_response' first to update it's // state correctly (this will also result in the configs getting stored to disk) - handle_config_push_response(gid, namespace_seqno, success, status_code, response); + handle_config_push_response(gid, push_info, success, status_code, response); cb(std::nullopt); } catch (const std::exception& e) { cb(e.what()); diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index 20eb4328..c21bfad5 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -377,7 +377,7 @@ LIBSESSION_C_API void state_create_group( const size_t error_len, void* ctx), void* ctx) { - assert(name); + assert(name && callback); try { std::optional description; if (description_) @@ -405,7 +405,15 @@ LIBSESSION_C_API void state_create_group( std::string_view group_id, ustring_view group_sk, std::optional error) { - callback(group_id.data(), group_sk.data(), error->data(), error->size(), ctx); + if (error) + callback( + group_id.data(), + group_sk.data(), + error->data(), + error->size(), + ctx); + else + callback(group_id.data(), group_sk.data(), nullptr, 0, ctx); }); } catch (const std::exception& e) { std::string_view err = e.what(); @@ -414,14 +422,9 @@ LIBSESSION_C_API void state_create_group( } } -LIBSESSION_C_API void state_approve_group( - state_object* state, const char* group_id, unsigned const char* group_sk) { +LIBSESSION_C_API void state_approve_group(state_object* state, const char* group_id) { try { - std::optional ed_sk; - if (group_sk) - ed_sk = {group_sk, 64}; - - unbox(state).approve_group({group_id, 66}, ed_sk); + unbox(state).approve_group({group_id, 66}); } catch (...) { } } @@ -429,7 +432,6 @@ LIBSESSION_C_API void state_approve_group( LIBSESSION_C_API bool state_load_group_admin_key( state_object* state, const char* group_id, unsigned const char* seed) { try { - std::string gid = {group_id, 66}; unbox(state).load_group_admin_key({group_id, 66}, ustring_view{seed, 32}); return true; } catch (const std::exception& e) { @@ -445,7 +447,7 @@ LIBSESSION_C_API void state_add_group_members( const size_t members_len, void (*callback)(const char* error, size_t error_len, void* ctx), void* ctx) { - assert(members_); + assert(members_ && callback); try { std::vector members; members.reserve(members_len); @@ -457,13 +459,11 @@ LIBSESSION_C_API void state_add_group_members( {group_id, 66}, supplemental_rotation, members, - [cb = std::move(callback), ctx](std::optional error) { - if (cb) { - if (error) - (*cb)(error->data(), error->size(), ctx); - else - (*cb)(nullptr, 0, ctx); - } + [callback, ctx](std::optional error) { + if (error) + callback(error->data(), error->size(), ctx); + else + callback(nullptr, 0, ctx); }); } catch (const std::exception& e) { std::string_view error = e.what(); diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 422f67b0..9cdf505f 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -733,7 +733,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { created_ts, last_send_data.data(), last_send_data.size()}; - CHECK(state_merge(state2, nullptr, merge_data, 1, &accepted)); + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "fakehash1"sv); free(accepted); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index f6e40b3c..906ec650 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -591,11 +591,12 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { if (group_sk_) { auto gsk = *group_sk_; secret_key = gsk; - state_approve_group(state, gid.c_str(), gsk.data()); + state_approve_group(state, gid.c_str()); + state_load_group_admin_key(state, gid.c_str(), gsk.data()); return; } - state_approve_group(state, gid.c_str(), nullptr); + state_approve_group(state, gid.c_str()); return; } @@ -614,8 +615,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { const char* error, const size_t error_len, void* ctx) { - if (error_len > 0) - REQUIRE(error == ""sv); + REQUIRE(error_len == 0); auto client = static_cast(ctx); @@ -631,8 +631,8 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { ctx); ustring send_response = session::to_unsigned( "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," - "\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200,\"body\":{\"hash\":" - "\"fakehash1\"}}]}"); + "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":" + "\"fakehash3\"}}]}"); last_send->response_cb( true, 200, @@ -682,12 +682,12 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { auto& admin2 = admins[1]; REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 1); - REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 0); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 1); for (const auto& m : members) REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); - // Add member, re-key, distribute + // Manually add member, re-key, distribute auto& member1 = members[0]; state_group_member new_member1; REQUIRE(state_get_or_construct_group_member( @@ -713,6 +713,16 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 2); CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 2); REQUIRE(admin1.last_send.has_value()); + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash3\"}" + "}]}"); + admin1.last_send->response_cb( + true, + 200, + send_response.data(), + send_response.size(), + admin1.last_send->callback_context); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); auto second_request_data = nlohmann::json::json_pointer("/params/requests/1/params/data"); @@ -763,6 +773,9 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { /* 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. + + We also need to merge for admin1 here because the Keys config won't update it's state + until it actually merges the updated keys */ for (auto& a : admins) { session_string_list* accepted; @@ -772,20 +785,11 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { CHECK(accepted->value[1] == "fakehash2"sv); CHECK(accepted->value[2] == "fakehash3"sv); free(accepted); - - ustring send_response = session::to_unsigned( - "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," - "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash3\"}" - "}]}"); - a.last_send->response_cb( - true, - 200, - send_response.data(), - send_response.size(), - a.last_send->callback_context); - - REQUIRE(state_size_group_members(a.state, a.group_id.c_str()) == 2); } + + // Due to the 'load_admin_key' behaviour admin2 will contain both admin1 and admin2 + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 2); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 3); /* Non-admins */ for (auto& m : members) { @@ -817,6 +821,10 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { free(merge_data_no_keys); free(merge_data); + CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).pending_config().has_value()); + std::vector new_members; new_members.reserve(members.size()); @@ -832,37 +840,38 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { new_members.push_back(new_mem); } - state_mutate_group( + // Add members via 'add_member' using supplemental rotation instead of manually + state_add_group_members( admin1.state, admin1.group_id.c_str(), - [](mutable_group_state_object* state, void* ctx) { - auto new_members = static_cast*>(ctx); - - for (auto new_mem : *new_members) { - state_set_group_member(state, &new_mem); - } - - REQUIRE(state_rekey_group(state)); + true, + new_members.data(), + new_members.size(), + [](const char* error, size_t error_len, void* ctx) { + REQUIRE(error_len == 0); }, - &new_members); + nullptr); + + send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash5\"}}]}"); + admin1.last_send->response_cb( + true, + 200, + send_response.data(), + send_response.size(), + admin1.last_send->callback_context); - CHECK(session::state::unbox(admin1.state) - .config(admin1.group_id) - .needs_push()); - CHECK(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); - CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 3); CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 3); last_send_json = nlohmann::json::parse(admin1.last_send->payload); + REQUIRE(last_send_json.contains(first_request_data)); REQUIRE(last_send_json.contains(second_request_data)); - REQUIRE(last_send_json.contains(third_request_data)); last_send_data_0 = session::to_unsigned( oxenc::from_base64(last_send_json[first_request_data].get())); last_send_data_1 = session::to_unsigned( oxenc::from_base64(last_send_json[second_request_data].get())); - last_send_data_2 = session::to_unsigned( - oxenc::from_base64(last_send_json[third_request_data].get())); - merge_data = new state_config_message[3]; + merge_data = new state_config_message[2]; merge_data[0] = { NAMESPACE_GROUP_KEYS, "fakehash4", @@ -870,39 +879,35 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { last_send_data_0.data(), last_send_data_0.size()}; merge_data[1] = { - NAMESPACE_GROUP_INFO, + NAMESPACE_GROUP_MEMBERS, "fakehash5", created_ts, last_send_data_1.data(), last_send_data_1.size()}; - merge_data[2] = { - NAMESPACE_GROUP_MEMBERS, - "fakehash6", - created_ts, - last_send_data_2.data(), - last_send_data_2.size()}; + /* Admins will store the hash for supplemental keys messages but won't actually consider them merged so the 'accepted' array won't contain the hash */ for (auto& a : admins) { session_string_list* accepted; - REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 3, &accepted)); - REQUIRE(accepted->len == 3); + REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 2, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash5"sv); + free(accepted); + } + + // Due to the 'load_admin_key' behaviour admin2 will contain both admin1 and admin2 + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 5); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 6); + + /* Non-admins */ + for (auto& m : members) { + session_string_list* accepted; + REQUIRE(state_merge(m.state, m.group_id.c_str(), merge_data, 2, &accepted)); + REQUIRE(accepted->len == 2); CHECK(accepted->value[0] == "fakehash4"sv); CHECK(accepted->value[1] == "fakehash5"sv); - CHECK(accepted->value[2] == "fakehash6"sv); free(accepted); - ustring send_response = session::to_unsigned( - "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," - "\"body\":{\"hash\":\"fakehash5\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash6\"}" - "}]}"); - a.last_send->response_cb( - true, - 200, - send_response.data(), - send_response.size(), - a.last_send->callback_context); - - REQUIRE(state_size_group_members(a.state, a.group_id.c_str()) == 5); + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 5); } free(merge_data); diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 0e81f6c4..51142680 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "session/config/contacts.h" #include "session/config/namespaces.hpp" @@ -16,31 +17,43 @@ using namespace oxenc::literals; using namespace session; using namespace session::state; using namespace session::config; +using json_ptr = nlohmann::json::json_pointer; static constexpr int64_t created_ts = 1680064059; using response_callback_t = std::function; -std::string replace_suffix_between( - std::string_view value, - int suffix_start_distance_from_end, - int suffix_end_distance_from_end, - std::string_view replacement = "") { - auto start_index = (value.size() - suffix_start_distance_from_end); - auto end_index = (value.size() - suffix_end_distance_from_end); +static std::array sk_from_seed(ustring_view seed) { + std::array ignore; + std::array sk; + crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); + return sk; +} + +static ustring send_response(std::vector hashes) { + std::string result = "{\"results\":["; + + for (auto& hash : hashes) + result += "{\"code\":200,\"body\":{\"hash\":\"" + std::string(hash) + "\"}},"; + + if (!hashes.empty()) + result.pop_back(); // Remove last comma - return std::string(value.substr(0, start_index)) + std::string(replacement) + - std::string(value.substr(end_index, value.size() - end_index)); + result += "]}"; + return to_unsigned(result); } TEST_CASE("State", "[state][state]") { auto ed_sk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; auto state = State({ed_sk.data(), ed_sk.size()}, {}); std::vector store_records; std::vector send_records; + std::vector keys_messages; state.on_store([&store_records]( config::Namespace namespace_, @@ -93,23 +106,18 @@ TEST_CASE("State", "[state][state]") { "69306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a" "3d64313a6e303a6565313a28303a313a296c6565"); CHECK(send_records[0].pubkey == - "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f4" - "6"); + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); auto send_data = nlohmann::json::parse(send_records[0].payload); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 1); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); CHECK(send_data.value("method", "") == "sequence"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); - CHECK(send_data.value( - nlohmann::json::json_pointer("/params/requests/0/params/pubkey_ed25519"), "") == + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == - 2); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") == + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserProfile)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "") == "CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" "piV1rPki0Quo5Zp34MwwdZXqMFEwRpKGZJwpFPSre6jln5XlmH8tnq8djJo/" "7QP8kH4m8uUfzsRNgZ1K6agbnGgRolBXgk86/" @@ -119,21 +127,18 @@ TEST_CASE("State", "[state][state]") { "U3RzIm8Sc7APgIPkJsTmJr+ckYzLEdzbrqae4gxvzFB22lZYt62rg7KVoaBWUcB3NgFhTxMGc37ysti0pfoxO/" "T+zkKertLqX+iWNZLRhy3kLaXhEkqafYQzikepvhzD8/" "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/yAA"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == - 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); CHECK(state.config().get_seqno() == 1); // Confirm the push - ustring send_response = - to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + ustring send_res = send_response({"fakehash1"}); REQUIRE(send_records[0].response_cb( true, 200, - send_response.data(), - send_response.size(), + send_res.data(), + send_res.size(), send_records[0].callback_context)); CHECK(store_records.size() == 2); // Should call store after confirming the push CHECK_FALSE(state.config().needs_push()); @@ -165,14 +170,15 @@ TEST_CASE("State", "[state][state]") { "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118", // member3 + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d", // member4 + "052222222222222222222222222222222222222222222222222222222222222222", // member5 }; std::vector members; - members.reserve(member_seeds.size()); - for (auto i = 0; i < member_seeds.size(); i++) { - auto m = groups::member(member_seeds[i]); - m.set_name("Member " + std::to_string(i)); - members.emplace_back(m); - } + members.reserve(2); + members.emplace_back(groups::member(member_seeds[0])); + members.emplace_back(groups::member(member_seeds[1])); + members[0].set_name("Member 0"); + members[1].set_name("Member 1"); state.create_group( "TestName", @@ -197,73 +203,43 @@ TEST_CASE("State", "[state][state]") { REQUIRE(send_records.size() == 2); send_data = nlohmann::json::parse(send_records[1].payload); - REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 3); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); CHECK(send_data.value("method", "") == "sequence"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") - .substr(0, 2) == "03"); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == - 12); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") - .size() == 5324); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == - 2592000000); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/pubkey"), "") - .substr(0, 2) == "03"); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/namespace"), 0) == - 13); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/data"), "") - .size() == 684); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/ttl"), 0L) == - 2592000000); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/pubkey"), "") - .substr(0, 2) == "03"); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/namespace"), 0) == - 14); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/data"), "") - .size() == 684); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/ttl"), 0L) == - 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), 0L) == 2592000000); CHECK_FALSE(state.config().needs_push()); - CHECK(store_records.size() == - 2); // Shouldn't store anything until we process a success response - send_response = to_unsigned( - "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":" - "{\"hash\":\"fakehash3\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}}]}"); + CHECK(store_records.size() == 2); // Not stored until we process a success response + send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); REQUIRE(send_records[1].response_cb( true, 200, - send_response.data(), - send_response.size(), + send_res.data(), + send_res.size(), send_records[1].callback_context)); CHECK(store_records.size() == 6); CHECK(store_records[2].namespace_ == Namespace::UserGroups); @@ -278,62 +254,46 @@ TEST_CASE("State", "[state][state]") { to_merge.emplace_back( Namespace::GroupKeys, "fakehash2", - send_data[nlohmann::json::json_pointer("/params/requests/0/params/timestamp")] - .get(), - to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" - "requests/0/" - "params/data")] - .get()))); + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); to_merge.emplace_back( Namespace::GroupInfo, "fakehash3", - send_data[nlohmann::json::json_pointer("/params/requests/1/params/timestamp")] - .get(), - to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" - "requests/1/" - "params/data")] - .get()))); + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))); to_merge.emplace_back( Namespace::GroupMembers, "fakehash4", - send_data[nlohmann::json::json_pointer("/params/requests/2/params/timestamp")] - .get(), - to_unsigned(oxenc::from_base64(send_data[nlohmann::json::json_pointer("/params/" - "requests/2/" - "params/data")] - .get()))); + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))); // Once the create group 'send' is confirm we add the group to UserGroups and also need to send - // that + // that updated config REQUIRE(send_records.size() == 3); send_data = nlohmann::json::parse(send_records[2].payload); - REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 1); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); CHECK(send_data.value("method", "") == "sequence"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); - CHECK(send_data.value( - nlohmann::json::json_pointer("/params/requests/0/params/pubkey_ed25519"), "") == + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == - 5); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") - .size() == 576); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == - 2592000000); - send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash5\"}}]}"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + send_res = send_res = send_response({"fakehash5"}); REQUIRE(send_records[2].response_cb( true, 200, - send_response.data(), - send_response.size(), + send_res.data(), + send_res.size(), send_records[2].callback_context)); REQUIRE(state.config().size_groups() == 1); @@ -345,16 +305,16 @@ TEST_CASE("State", "[state][state]") { CHECK_FALSE(state.config().needs_push()); // Keys only get loaded when merging so we need to trigger the merge - auto result = state.merge(group.id, to_merge); - REQUIRE(result.size() == 3); - CHECK(result[0] == "fakehash2"); - CHECK(result[1] == "fakehash3"); - CHECK(result[2] == "fakehash4"); + auto merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash2"); + CHECK(merge_result[1] == "fakehash3"); + CHECK(merge_result[2] == "fakehash4"); CHECK(send_records.size() == 3); // Check that the supplemental rotation calls everything correctly std::vector supplemental_members; - supplemental_members.emplace_back(member4_sid); + supplemental_members.emplace_back(member_seeds[2]); state.add_group_members( group.id, true, supplemental_members, [](std::optional error) { REQUIRE_FALSE(error.has_value()); @@ -362,63 +322,491 @@ TEST_CASE("State", "[state][state]") { REQUIRE(send_records.size() == 4); send_data = nlohmann::json::parse(send_records[3].payload); - REQUIRE(send_data.contains(nlohmann::json::json_pointer("/params/requests"))); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].is_array()); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests")].size() == 3); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/2/params/messages/0"), "") == "fakehash4"); + keys_messages.emplace_back(config_message{ + Namespace::GroupKeys, + "fakehash5", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}); + + send_res = send_res = send_response({"fakehash5", "fakehash6"}); + REQUIRE(send_records[3].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[3].callback_context)); + auto last_keys = config_message{ + Namespace::GroupKeys, + "fakehash5", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}; + + // When 2 admins rekey a group at the same time (and create a conflict) the merge process will perform + // another rekey to resolve the conflict + std::vector new_admin; + new_admin.emplace_back("05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + new_admin[0].admin = true; + state.add_group_members( + group.id, false, new_admin, [](std::optional error) { + CHECK(error.value_or("") == ""sv); + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 5); + send_data = nlohmann::json::parse(send_records[4].payload); + send_res = send_response({"fakehash7"}); + REQUIRE(send_records[4].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[4].callback_context)); +} + +TEST_CASE("State", "[state][state][merge key conflict]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + const std::string admin2_sid = "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"; + const std::array member_seeds = { + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118", // member3 + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d", // member4 + }; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + std::vector keys_messages; + + state.on_store([&store_records]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + store_records.push_back({namespace_, pubkey, timestamp_ms, data}); + }); + state.on_send( + [&send_records]( + std::string pubkey, ustring payload, response_callback_t received_response) { + // Replicate the behaviour in the C wrapper + auto on_response = + std::make_unique(std::move(received_response)); + + send_records.push_back( + {pubkey, + payload, + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so + // that we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + nullptr, + on_response.release()}); + }); + auto admin2_sk = sk_from_seed({admin2_seed.data(), admin2_seed.size()}); + auto state_admin_2 = State({admin2_sk.data(), admin2_sk.size()}, {}); + std::vector store_records_2; + std::vector send_records_2; + + state_admin_2.on_store([&store_records_2]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + store_records_2.push_back({namespace_, pubkey, timestamp_ms, data}); + }); + state_admin_2.on_send( + [&send_records_2]( + std::string pubkey, ustring payload, response_callback_t received_response) { + // Replicate the behaviour in the C wrapper + auto on_response = + std::make_unique(std::move(received_response)); + + send_records_2.push_back( + {pubkey, + payload, + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so + // that we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + nullptr, + on_response.release()}); + }); + + // Create the initial group + std::vector members; + members.reserve(3); + members.emplace_back(groups::member(member_seeds[0])); + members.emplace_back(groups::member(member_seeds[1])); + members.emplace_back(groups::member{admin2_sid}); + members[0].set_name("Member 0"); + members[1].set_name("Member 1"); + members[2].set_name("Admin 2"); + + state.create_group( + "TestName", + "TestDesc", + profile_pic(), + members, + [&state]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 1); + auto send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + ustring send_res = send_response({"fakehash1", "fakehash2", "fakehash3"}); + REQUIRE(send_records[0].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[0].callback_context)); + REQUIRE(send_records.size() == 2); // Group added to UserGroups + send_data = nlohmann::json::parse(send_records[1].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + send_res = send_response({"fakehash4"}); + REQUIRE(send_records[1].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[1].callback_context)); + REQUIRE(state.config().size_groups() == 1); + auto group = *state.config().begin_groups(); + + // Group keys aren't finalised until they have been retrieved and merged in + std::vector to_merge; + send_data = nlohmann::json::parse(send_records[0].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash1", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + auto merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash1"); + CHECK(send_records.size() == 2); // Unchanged + + // Load the group for admin2 + state_admin_2.approve_group(group.id); + REQUIRE(send_records_2.size() == 1); + send_data = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + send_res = send_response({"fakehash5"}); + REQUIRE(send_records_2[0].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[0].callback_context)); + REQUIRE(send_records_2.size() == 1); // Unchanged + + send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + to_merge.clear(); + to_merge.emplace_back(config_message{ + Namespace::GroupKeys, + "fakehash1", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}); + to_merge.emplace_back(config_message{ + Namespace::GroupInfo, + "fakehash2", + send_data.value(json_ptr("/params/requests/1/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))}); + to_merge.emplace_back(config_message{ + Namespace::GroupMembers, + "fakehash3", + send_data.value(json_ptr("/params/requests/2/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))}); + merge_result = state_admin_2.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash1"); + CHECK(merge_result[1] == "fakehash2"); + CHECK(merge_result[2] == "fakehash3"); + + // Promote to admin + state_admin_2.load_group_admin_key(group.id, group.secretkey); + REQUIRE(send_records_2.size() == 3); + + // UserGroups gets the admin key + send_data = nlohmann::json::parse(send_records_2[1].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); + send_res = send_response({"fakehash6"}); + REQUIRE(send_records_2[1].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[1].callback_context)); + + // Member flagged as an admin + send_data = nlohmann::json::parse(send_records_2[2].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); CHECK(send_data.value("method", "") == "sequence"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/pubkey"), "") == - group.id); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/namespace"), 0) == - 12); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/data"), "") - .size() == 264); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/0/params/ttl"), 0L) == - 2592000000); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/method"), "") == - "stor" - "e"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/pubkey"), "") == - group.id); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/namespace"), 0) == - 14); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/data"), "") - .size() == 684); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/signature"), "") - .size() == 88); - CHECK(send_data.contains(nlohmann::json::json_pointer("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/1/params/ttl"), 0L) == - 2592000000); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/method"), "") == - "delete"); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/pubkey"), "") == - group.id); - CHECK_FALSE( - send_data.contains(nlohmann::json::json_pointer("/params/requests/2/params/" - "pubkey_ed25519"))); - CHECK(send_data.value(nlohmann::json::json_pointer("/params/requests/2/params/signature"), "") - .size() == 88); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests/2/params/messages")] - .is_array()); - REQUIRE(send_data[nlohmann::json::json_pointer("/params/requests/2/params/messages")].size() == - 2); - REQUIRE(send_data.value( - nlohmann::json::json_pointer("/params/requests/2/params/messages/0"), "") == - "fakehash3"); - REQUIRE(send_data.value( - nlohmann::json::json_pointer("/params/requests/2/params/messages/1"), "") == - "fakehash4"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash3"); + send_res = send_response({"fakehash7"}); + REQUIRE(send_records_2[2].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[2].callback_context)); + REQUIRE(send_records_2.size() == 3); // Unchanged + REQUIRE(state_admin_2.config(group.id).admin()); + + // Merge the member change into admin1 + to_merge.clear(); + to_merge.emplace_back(config_message{ + Namespace::GroupMembers, + "fakehash7", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}); + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash7"); + REQUIRE(send_records.size() == 2); // Unchanged + + // Create a conflict between the members/keys + std::vector conflict_members_1, conflict_members_2; + conflict_members_1.emplace_back(member_seeds[2]); + conflict_members_2.emplace_back(member_seeds[3]); + state.add_group_members( + group.id, false, conflict_members_1, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + state_admin_2.add_group_members( + group.id, false, conflict_members_2, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 3); + send_data = nlohmann::json::parse(send_records[2].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 3); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash7"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/2"), "") == "fakehash3"); + send_res = send_response({"fakehash8", "fakehash9", "fakehash10"}); + REQUIRE(send_records[2].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[2].callback_context)); + + // Group keys aren't finalised until they have been retrieved and merged in + to_merge.clear(); + send_data = nlohmann::json::parse(send_records[2].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash8", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash8"); + CHECK(send_records.size() == 3); // Unchanged + + REQUIRE(send_records_2.size() == 4); + send_data = nlohmann::json::parse(send_records_2[3].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 2); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash7"); + send_res = send_response({"fakehash11", "fakehash12", "fakehash13"}); + REQUIRE(send_records_2[3].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[3].callback_context)); + + // Group keys aren't finalised until they have been retrieved and merged in + to_merge.clear(); + send_data = nlohmann::json::parse(send_records_2[3].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash11", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + merge_result = state_admin_2.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash11"); + CHECK(send_records_2.size() == 4); // Unchanged + + // Both configs are one the same generation (with a conflict) + REQUIRE(state.config(group.id).current_generation() == 1); + REQUIRE(state_admin_2.config(group.id).current_generation() == 1); + + // Merge the changes from admin2 across to admin1 (the merge function should handle the conflict) + send_data = nlohmann::json::parse(send_records_2[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + to_merge.clear(); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash11", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupInfo, + "fakehash12", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupMembers, + "fakehash13", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))); + + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash11"); + CHECK(merge_result[1] == "fakehash12"); + CHECK(merge_result[2] == "fakehash13"); + + // Admin1 should have performed a rekey as part of the merge (updating each of the group configs) + REQUIRE(send_records.size() == 4); + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash9"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash12"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/2"), "") == "fakehash13"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/3"), "") == "fakehash10"); } TEST_CASE("State c API", "[state][state][c]") { From 8e213cc940d5a7bc01c7e45b18a2e568ba305860 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 27 Feb 2024 10:50:45 +1100 Subject: [PATCH 17/24] Added unit tests for complicated state group functions --- src/state.cpp | 3 + tests/test_config_contacts.cpp | 35 +- tests/test_config_convo_info_volatile.cpp | 79 ++-- tests/test_config_user_groups.cpp | 28 +- tests/test_group_keys.cpp | 33 +- tests/test_state.cpp | 547 +++++++++++++++++----- tests/utils.hpp | 9 +- 7 files changed, 528 insertions(+), 206 deletions(-) diff --git a/src/state.cpp b/src/state.cpp index 6d99e0b4..92ce240e 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -519,6 +519,9 @@ PreparedPush State::prepare_push( std::optional max_merged_timestamp( const std::vector& messages, const std::vector& merged_hashes) { + if (messages.empty() || merged_hashes.empty()) + return std::nullopt; + // Filter messages based on merged_hashes std::vector merged_messages; std::copy_if( diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 927d5c98..88d7f9a0 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -240,13 +240,13 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { char err[256]; state_object* state; REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; - std::optional last_store_2 = std::nullopt; - std::optional last_send_2 = std::nullopt; + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); const char* const definitely_real_id = "050000000000000000000000000000000000000000000000000000000000000000"; @@ -288,20 +288,22 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { CHECK_FALSE(c2.blocked); CHECK(strlen(c2.profile_pic.url) == 0); - CHECK((*last_store).pubkey == + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); - CHECK((*last_send).pubkey == + CHECK(send_records[0].pubkey == "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" "f46"); CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONTACTS) == 1); state_object* state2; REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); - state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); - state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->payload); + auto last_send_json = nlohmann::json::parse(send_records[0].payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -321,8 +323,8 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + send_records[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); contacts_contact c3; REQUIRE(state_get_contact(state2, &c3, definitely_real_id, nullptr)); @@ -352,7 +354,8 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { }, &c4); - auto last_send_json_2 = nlohmann::json::parse(last_send_2->payload); + REQUIRE(send_records_2.size() == 1); + auto last_send_json_2 = nlohmann::json::parse(send_records_2[0].payload); REQUIRE(last_send_json_2.contains(first_request_data)); auto last_send_data_2 = to_unsigned( oxenc::from_base64(last_send_json_2[first_request_data].get())); @@ -370,8 +373,8 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { free(merge_data); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); - last_send_2->response_cb( - true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); + send_records_2[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records_2[0].callback_context); auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); REQUIRE(last_send_json_2.contains(messages_key)); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index cce1c19b..fb1d7def 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -253,13 +253,13 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { memset(err, 0, 255); state_object* state; REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; - std::optional last_store_2 = std::nullopt; - std::optional last_send_2 = std::nullopt; + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); const char* const definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; @@ -345,24 +345,26 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); // Pretend we uploaded it + REQUIRE(send_records.size() == 1); ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash1\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + send_records[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); CHECK_FALSE(session::state::unbox(state).config().needs_dump()); + REQUIRE(store_records.size() == 2); state_namespaced_dump* dumps = new state_namespaced_dump[1]; dumps[0] = { - static_cast((*last_store).namespace_), - (*last_store).pubkey.c_str(), - (*last_store).data.data(), - (*last_store).data.size()}; + static_cast(store_records[1].namespace_), + store_records[1].pubkey.c_str(), + store_records[1].data.data(), + store_records[1].data.size()}; state_object* state2; REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); - state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); - state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); free(dumps); CHECK_FALSE(session::state::unbox(state2).config().needs_push()); @@ -409,8 +411,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(session::state::unbox(state2).config().needs_push()); CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); + REQUIRE(send_records_2.size() == 1); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send_2->payload); + auto last_send_json = nlohmann::json::parse(send_records_2[0].payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -429,8 +432,8 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { free(merge_data); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash123\"}}]}"); - last_send_2->response_cb( - true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); + send_records_2[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records_2[0].callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); std::vector seen; @@ -651,13 +654,13 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load char err[256]; state_object* state; REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; - std::optional last_store_2 = std::nullopt; - std::optional last_send_2 = std::nullopt; + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); convo_info_volatile_1to1 c; CHECK(state_get_or_construct_convo_info_volatile_1to1( @@ -674,23 +677,25 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load &c); // Fake push: + REQUIRE(send_records.size() == 1); CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"somehash\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + send_records[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); // Load the dump: + REQUIRE(store_records.size() == 2); state_namespaced_dump* dumps = new state_namespaced_dump[1]; dumps[0] = { - static_cast((*last_store).namespace_), - (*last_store).pubkey.c_str(), - (*last_store).data.data(), - (*last_store).data.size()}; + static_cast(store_records[1].namespace_), + store_records[1].pubkey.c_str(), + store_records[1].data.data(), + store_records[1].data.size()}; state_object* state2; REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); - state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); - state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); free(dumps); // Change the original again, then push it for conf2: @@ -710,10 +715,11 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load }, &c); + REQUIRE(send_records.size() == 2); CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash5235\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + send_records[1].response_cb( + true, 200, send_response.data(), send_response.size(), send_records[1].callback_context); // But *before* we load the push make a dirtying change to conf2 that we *don't* push (so that // we'll be merging into a dirty-state config): @@ -735,7 +741,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load // And now, *before* we push the dirty config, also merge the incoming push from `state`: auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->payload); + auto last_send_json = nlohmann::json::parse(send_records[1].payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); @@ -773,11 +779,12 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load }, &c1); + REQUIRE(send_records_2.size() == 3); CHECK(session::state::unbox(state2).config().needs_push()); CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 4); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hashz\"}}]}"); - last_send_2->response_cb( - true, 200, send_response.data(), send_response.size(), last_send_2->callback_context); + send_records_2[2].response_cb( + true, 200, send_response.data(), send_response.size(), send_records_2[2].callback_context); CHECK_FALSE(session::state::unbox(state2).config().needs_push()); CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 9cdf505f..4045836b 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -581,13 +581,13 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { char err[256]; state_object* state; REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; - std::optional last_store_2 = std::nullopt; - std::optional last_send_2 = std::nullopt; + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; @@ -691,17 +691,19 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { CHECK(hashes->len == 0); free(hashes); - CHECK((*last_store).pubkey == + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].pubkey == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK((*last_send).pubkey == + CHECK(send_records[0].pubkey == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3" "a72"); CHECK(state_current_seqno(state, nullptr, NAMESPACE_USER_GROUPS) == 1); ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); - last_send->response_cb( - true, 200, send_response.data(), send_response.size(), last_send->callback_context); + send_records[0].response_cb( + true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); REQUIRE(state_current_hashes(state, nullptr, &hashes)); REQUIRE(hashes); @@ -717,11 +719,11 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { state_object* state2; REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); - state_set_store_callback(state2, c_store_callback, reinterpret_cast(&last_store_2)); - state_set_send_callback(state2, c_send_callback, reinterpret_cast(&last_send_2)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); - auto last_send_json = nlohmann::json::parse(last_send->payload); + auto last_send_json = nlohmann::json::parse(send_records[0].payload); REQUIRE(last_send_json.contains(first_request_data)); auto last_send_data = to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 906ec650..4ea74e45 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -570,8 +570,8 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { std::string user_session_id{session_id_from_ed(user_public_key)}; state_object* state; - std::optional last_store = std::nullopt; - std::optional last_send = std::nullopt; + std::vector store_records; + std::vector send_records; pseudo_client( ustring user_seed, @@ -580,8 +580,8 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { user_secret_key{sk_from_seed(user_seed)} { char err[256]; REQUIRE(state_init(&state, user_secret_key.data(), nullptr, 0, err)); - state_set_store_callback(state, c_store_callback, reinterpret_cast(&last_store)); - state_set_send_callback(state, c_send_callback, reinterpret_cast(&last_send)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); // If we already have a group then just "approve" it if (group_id_) { @@ -622,23 +622,19 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { // Now that the group is created store the values client->group_id = group_id; memcpy(client->secret_key.data(), group_sk, 64); - - // Clear the 'last_send' and 'last_store' since we don't care about the - // group creation - client->last_send = std::nullopt; - client->last_store = std::nullopt; }, ctx); + REQUIRE(send_records.size() == 1); ustring send_response = session::to_unsigned( "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":" "\"fakehash3\"}}]}"); - last_send->response_cb( + send_records[0].response_cb( true, 200, send_response.data(), send_response.size(), - last_send->callback_context); + send_records[0].callback_context); } ~pseudo_client() { state_free(state); } @@ -710,24 +706,24 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { .config(admin1.group_id) .needs_push()); + REQUIRE(admin1.send_records.size() == 3); CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 2); CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 2); - REQUIRE(admin1.last_send.has_value()); ustring send_response = session::to_unsigned( "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash3\"}" "}]}"); - admin1.last_send->response_cb( + admin1.send_records[2].response_cb( true, 200, send_response.data(), send_response.size(), - admin1.last_send->callback_context); + admin1.send_records[2].callback_context); auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); auto second_request_data = nlohmann::json::json_pointer("/params/requests/1/params/data"); auto third_request_data = nlohmann::json::json_pointer("/params/requests/2/params/data"); - auto last_send_json = nlohmann::json::parse(admin1.last_send->payload); + auto last_send_json = nlohmann::json::parse(admin1.send_records[2].payload); REQUIRE(last_send_json.contains(first_request_data)); REQUIRE(last_send_json.contains(second_request_data)); REQUIRE(last_send_json.contains(third_request_data)); @@ -852,19 +848,20 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { }, nullptr); + REQUIRE(admin1.send_records.size() == 4); send_response = session::to_unsigned( "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," "\"body\":{\"hash\":\"fakehash5\"}}]}"); - admin1.last_send->response_cb( + admin1.send_records[3].response_cb( true, 200, send_response.data(), send_response.size(), - admin1.last_send->callback_context); + admin1.send_records[3].callback_context); CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 3); - last_send_json = nlohmann::json::parse(admin1.last_send->payload); + last_send_json = nlohmann::json::parse(admin1.send_records[3].payload); REQUIRE(last_send_json.contains(first_request_data)); REQUIRE(last_send_json.contains(second_request_data)); last_send_data_0 = session::to_unsigned( diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 51142680..625d0c44 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -8,7 +8,10 @@ #include "session/config/namespaces.hpp" #include "session/config/user_profile.h" #include "session/config/user_profile.hpp" +#include "session/config/user_groups.h" +#include "session/config/groups/members.h" #include "session/state.h" +#include "session/state_groups.h" #include "session/state.hpp" #include "utils.hpp" @@ -43,18 +46,7 @@ static ustring send_response(std::vector hashes) { return to_unsigned(result); } -TEST_CASE("State", "[state][state]") { - auto ed_sk = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - const ustring admin2_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; - - auto state = State({ed_sk.data(), ed_sk.size()}, {}); - std::vector store_records; - std::vector send_records; - std::vector keys_messages; - +static void setup_store_hook(State& state, std::vector& store_records) { state.on_store([&store_records]( config::Namespace namespace_, std::string pubkey, @@ -62,6 +54,9 @@ TEST_CASE("State", "[state][state]") { ustring data) { store_records.push_back({namespace_, pubkey, timestamp_ms, data}); }); +} + +static void setup_send_hook(State& state, std::vector& send_records) { state.on_send( [&send_records]( std::string pubkey, ustring payload, response_callback_t received_response) { @@ -91,6 +86,20 @@ TEST_CASE("State", "[state][state]") { nullptr, on_response.release()}); }); +} + +TEST_CASE("State", "[state][state]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); // Sanity check direct config access CHECK_FALSE(state.config().get_name().has_value()); @@ -288,7 +297,7 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); - send_res = send_res = send_response({"fakehash5"}); + send_res = send_response({"fakehash5"}); REQUIRE(send_records[2].response_cb( true, 200, @@ -349,47 +358,126 @@ TEST_CASE("State", "[state][state]") { REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].size() == 1); CHECK(send_data.value(json_ptr("/params/requests/2/params/messages/0"), "") == "fakehash4"); - keys_messages.emplace_back(config_message{ - Namespace::GroupKeys, - "fakehash5", - send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), - to_unsigned(oxenc::from_base64( - send_data[json_ptr("/params/requests/0/params/data")].get()))}); - send_res = send_res = send_response({"fakehash5", "fakehash6"}); + send_res = send_response({"fakehash5", "fakehash6"}); REQUIRE(send_records[3].response_cb( true, 200, send_res.data(), send_res.size(), send_records[3].callback_context)); - auto last_keys = config_message{ - Namespace::GroupKeys, - "fakehash5", - send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), +} + +TEST_CASE("State", "[state][state][merge failure behaviour]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + auto state2 = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); + setup_store_hook(state2, store_records_2); + setup_send_hook(state2, send_records_2); + + // Setup an initial state + state.mutable_config().user_profile.set_name("Test Name1"); + REQUIRE(send_records.size() == 1); + CHECK(store_records.size() == 1); + ustring send_res = send_response({"fakehash1"}); + REQUIRE(send_records[0].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[0].callback_context)); + CHECK(store_records.size() == 2); + + // Merge into state2 so they are consistent + std::vector to_merge; + auto send_data = nlohmann::json::parse(send_records[0].payload); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash1", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), to_unsigned(oxenc::from_base64( - send_data[json_ptr("/params/requests/0/params/data")].get()))}; + send_data[json_ptr("/params/requests/0/params/data")].get()))); + auto merge_result = state2.merge(std::nullopt, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash1"); - // When 2 admins rekey a group at the same time (and create a conflict) the merge process will perform - // another rekey to resolve the conflict - std::vector new_admin; - new_admin.emplace_back("05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - new_admin[0].admin = true; - state.add_group_members( - group.id, false, new_admin, [](std::optional error) { - CHECK(error.value_or("") == ""sv); - REQUIRE_FALSE(error.has_value()); - }); + // Modify state2 to generate valid data to merge + state2.mutable_config().user_profile.set_name("Test Name"); + REQUIRE(send_records_2.size() == 1); + CHECK(store_records_2.size() == 2); + send_res = send_response({"fakehash2"}); + REQUIRE(send_records_2[0].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[0].callback_context)); + CHECK(store_records.size() == 2); - REQUIRE(send_records.size() == 5); - send_data = nlohmann::json::parse(send_records[4].payload); - send_res = send_response({"fakehash7"}); - REQUIRE(send_records[4].response_cb( + state2.mutable_config().user_profile.set_name("Test Name2"); + REQUIRE(send_records_2.size() == 2); + CHECK(store_records_2.size() == 4); + send_res = send_response({"fakehash3"}); + REQUIRE(send_records_2[1].response_cb( true, 200, send_res.data(), send_res.size(), - send_records[4].callback_context)); + send_records_2[1].callback_context)); + CHECK(store_records_2.size() == 5); + REQUIRE(state2.config().get_name().has_value()); + CHECK(*state2.config().get_name() == "Test Name2"); + + // Generate a valid and an invalid config for the merge (only the valid one should be merged) + send_data = nlohmann::json::parse(send_records_2[0].payload); + auto valid_timestamp = send_data[json_ptr("/params/requests/0/params/timestamp")].get(); + auto invalid_send_data = nlohmann::json::parse(send_records_2[1].payload); + auto invalid_payload = oxenc::from_base64( + invalid_send_data[json_ptr("/params/requests/0/params/data")].get()); + invalid_payload.replace(invalid_payload.begin(), invalid_payload.begin() + 4, "RAWR"); + to_merge.clear(); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash2", + valid_timestamp, + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash3", + 3000000000000, + to_unsigned(invalid_payload)); + merge_result = state.merge(std::nullopt, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash2"); + CHECK(send_records.size() == 1); // Unchanged + REQUIRE(state.config().get_name().has_value()); + CHECK(*state.config().get_name() == "Test Name"); + REQUIRE(store_records.size() == 3); + CHECK(store_records[2].timestamp == valid_timestamp); + + // Now try to merge solely invalid data, the send/store hooks shouldn't get called + CHECK(send_records.size() == 1); + CHECK(store_records.size() == 3); + to_merge.clear(); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash3", + 3000000000000, + to_unsigned(invalid_payload)); + merge_result = state.merge(std::nullopt, to_merge); + CHECK(merge_result.size() == 0); + CHECK(send_records.size() == 1); // Unchanged + CHECK(store_records.size() == 3); // Unchanged } TEST_CASE("State", "[state][state][merge key conflict]") { @@ -409,85 +497,15 @@ TEST_CASE("State", "[state][state][merge key conflict]") { auto state = State({ed_sk.data(), ed_sk.size()}, {}); std::vector store_records; std::vector send_records; - std::vector keys_messages; - - state.on_store([&store_records]( - config::Namespace namespace_, - std::string pubkey, - uint64_t timestamp_ms, - ustring data) { - store_records.push_back({namespace_, pubkey, timestamp_ms, data}); - }); - state.on_send( - [&send_records]( - std::string pubkey, ustring payload, response_callback_t received_response) { - // Replicate the behaviour in the C wrapper - auto on_response = - std::make_unique(std::move(received_response)); + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); - send_records.push_back( - {pubkey, - payload, - [](bool success, - int16_t status_code, - const unsigned char* res, - size_t reslen, - void* callback_context) { - try { - // Recapture the std::function callback here in a unique_ptr so - // that we clean it up at the end of this lambda. - std::unique_ptr cb{ - static_cast(callback_context)}; - (*cb)(success, status_code, {res, reslen}); - return true; - } catch (...) { - return false; - } - }, - nullptr, - on_response.release()}); - }); auto admin2_sk = sk_from_seed({admin2_seed.data(), admin2_seed.size()}); auto state_admin_2 = State({admin2_sk.data(), admin2_sk.size()}, {}); std::vector store_records_2; std::vector send_records_2; - - state_admin_2.on_store([&store_records_2]( - config::Namespace namespace_, - std::string pubkey, - uint64_t timestamp_ms, - ustring data) { - store_records_2.push_back({namespace_, pubkey, timestamp_ms, data}); - }); - state_admin_2.on_send( - [&send_records_2]( - std::string pubkey, ustring payload, response_callback_t received_response) { - // Replicate the behaviour in the C wrapper - auto on_response = - std::make_unique(std::move(received_response)); - - send_records_2.push_back( - {pubkey, - payload, - [](bool success, - int16_t status_code, - const unsigned char* res, - size_t reslen, - void* callback_context) { - try { - // Recapture the std::function callback here in a unique_ptr so - // that we clean it up at the end of this lambda. - std::unique_ptr cb{ - static_cast(callback_context)}; - (*cb)(success, status_code, {res, reslen}); - return true; - } catch (...) { - return false; - } - }, - nullptr, - on_response.release()}); - }); + setup_store_hook(state_admin_2, store_records_2); + setup_send_hook(state_admin_2, send_records_2); // Create the initial group std::vector members; @@ -813,10 +831,23 @@ TEST_CASE("State c API", "[state][state][c]") { auto ed_sk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; char err[256]; state_object* state; + state_object* state2; + auto admin2_sk = sk_from_seed({admin2_seed.data(), admin2_seed.size()}); REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + REQUIRE(state_init(&state2, admin2_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); // User Profile forwarding CHECK(state_get_profile_name(state) == nullptr); @@ -857,9 +888,287 @@ TEST_CASE("State c API", "[state][state][c]") { unsigned char* dump1; size_t dump1len; state_dump_namespace(state, NAMESPACE_USER_PROFILE, nullptr, &dump1, &dump1len); - state_object* state2; - REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, err)); - CHECK(state_get_profile_name(state2) == nullptr); - CHECK(state_load(state2, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); - CHECK(state_get_profile_name(state2) == "Test Name"sv); + state_object* state3; + REQUIRE(state_init(&state3, ed_sk.data(), nullptr, 0, err)); + CHECK(state_get_profile_name(state3) == nullptr); + CHECK(state_load(state3, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); + CHECK(state_get_profile_name(state3) == "Test Name"sv); + + // Creating a group works correctly + auto pic = user_profile_pic(); + strcpy(pic.url, "http://example.com/huge.bmp"); + memcpy(pic.key, "qwerty78901234567890123456789012", 32); + auto members = new state_group_member[3]; + members[0] = state_group_member{"05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", "Member 0"}; + members[1] = state_group_member{"053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", "Member 1"}; + members[2] = state_group_member{"05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e", "Admin 2"}; + + state_create_group( + state, + "TestName", + 8, + "TestDesc", + 8, + pic, + members, + 3, + [](const char* group_id, unsigned const char* group_sk, const char* error, const size_t error_len, void* ctx) { + REQUIRE(error_len == 0); + }, + nullptr + ); + free(members); + + REQUIRE(send_records.size() == 4); + auto send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), 0L) == 2592000000); + + CHECK_FALSE(unbox(state).config().needs_push()); + CHECK(store_records.size() == 3); // Not stored until we process a success response + auto send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); + REQUIRE(send_records[3].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[3].callback_context)); + CHECK(store_records.size() == 7); + CHECK(store_records[3].namespace_ == Namespace::UserGroups); + CHECK(store_records[4].namespace_ == Namespace::GroupKeys); + CHECK(store_records[5].namespace_ == Namespace::GroupInfo); + CHECK(store_records[6].namespace_ == Namespace::GroupMembers); + CHECK(unbox(state).config().get_seqno() == 1); + CHECK(unbox(state).config().needs_push()); + + auto gid = send_data.value(json_ptr("/params/requests/0/params/pubkey"), ""); + auto g = ugroups_group_info(); + REQUIRE(state_size_ugroups(state) == 1); + REQUIRE(state_get_ugroups_group(state, &g, gid.c_str(), nullptr)); + CHECK(g.name == "TestName"sv); + CHECK(unbox(state).config(gid).get_seqno() == 1); + CHECK(unbox(state).config(gid).get_seqno() == 1); + CHECK(unbox(state).config(gid).current_generation() == 0); + CHECK(unbox(state).config(gid).admin()); + + // Keys only get loaded when merging so we need to trigger the merge + state_config_message* merge_data = new state_config_message[3]; + session_string_list* accepted; + auto payload0 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get())); + auto payload1 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get())); + auto payload2 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get())); + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash2", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + payload0.data(), + payload0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash3", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + payload1.data(), + payload1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash4", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + payload2.data(), + payload2.size()}; + REQUIRE(state_merge(state, gid.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash2"sv); + CHECK(accepted->value[1] == "fakehash3"sv); + CHECK(accepted->value[2] == "fakehash4"sv); + CHECK(send_records.size() == 5); + free(accepted); + free(merge_data); + + // Check that the supplemental rotation calls everything correctly + members = new state_group_member[1]; + members[0] = state_group_member{"05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"}; + state_add_group_members(state, gid.c_str(), true, members, 1, [](const char* error, size_t error_len, void* ctx){ + REQUIRE(error_len == 0); + }, nullptr); + free(members); + + REQUIRE(send_records.size() == 6); + send_data = nlohmann::json::parse(send_records[5].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 1024); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/2/params/messages/0"), "") == "fakehash4"); + + send_res = send_response({"fakehash5", "fakehash6"}); + REQUIRE(send_records[5].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records[5].callback_context)); + + // Load the group for admin2 + state_approve_group(state2, gid.c_str()); + REQUIRE(send_records_2.size() == 1); + send_data = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + send_res = send_response({"fakehash5"}); + REQUIRE(send_records_2[0].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[0].callback_context)); + REQUIRE(send_records_2.size() == 1); // Unchanged + + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + merge_data = new state_config_message[3]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash2", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + payload0.data(), + payload0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash3", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + payload1.data(), + payload1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash4", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + payload2.data(), + payload2.size()}; + REQUIRE(state_merge(state2, gid.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash2"sv); + CHECK(accepted->value[1] == "fakehash3"sv); + CHECK(accepted->value[2] == "fakehash4"sv); + CHECK(send_records_2.size() == 1); + free(accepted); + free(merge_data); + + // Promote to admin + state_load_group_admin_key(state2, gid.c_str(), g.secretkey); + REQUIRE(send_records_2.size() == 3); + + // UserGroups gets the admin key + send_data = nlohmann::json::parse(send_records_2[1].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); + send_res = send_response({"fakehash6"}); + REQUIRE(send_records_2[1].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[1].callback_context)); + + // Member flagged as an admin + send_data = nlohmann::json::parse(send_records_2[2].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 1024); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash4"); + send_res = send_response({"fakehash7"}); + REQUIRE(send_records_2[2].response_cb( + true, + 200, + send_res.data(), + send_res.size(), + send_records_2[2].callback_context)); + REQUIRE(send_records_2.size() == 3); // Unchanged + REQUIRE(unbox(state2).config(gid).admin()); } \ No newline at end of file diff --git a/tests/utils.hpp b/tests/utils.hpp index c133d25f..cb17921a 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -116,11 +116,12 @@ inline void c_store_callback( const unsigned char* data, size_t data_len, void* ctx) { - *static_cast*>(ctx) = last_store_data{ + static_cast*>(ctx)->emplace_back( + last_store_data{ static_cast(namespace_), {pubkey, 66}, timestamp_ms, - {data, data_len}}; + {data, data_len}}); } inline void c_send_callback( @@ -135,6 +136,6 @@ inline void c_send_callback( void* callback_context), void* app_ctx, void* callback_context) { - *static_cast*>(app_ctx) = - last_send_data{{pubkey, 66}, {data, data_len}, response_cb, app_ctx, callback_context}; + static_cast*>(app_ctx)->emplace_back( + last_send_data{{pubkey, 66}, {data, data_len}, response_cb, app_ctx, callback_context}); } From 422097923ba5d27934e253bd05ba036a873869eb Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 27 Feb 2024 11:15:09 +1100 Subject: [PATCH 18/24] Fixed a request handling bug --- src/state.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state.cpp b/src/state.cpp index 92ce240e..3398f9cd 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -907,9 +907,9 @@ void State::handle_config_push_response( std::count_if(push_info.begin(), push_info.end(), [](const PreparedPush::Info& info) { return info.requires_response; }); - if (results.size() != required_response_count) + if (results.size() < required_response_count) throw std::invalid_argument{ - "handle_config_push_response: Invalid response - Number of responses doesn't match " + "handle_config_push_response: Invalid response - Number of responses smaller than " "the number of requests requiring responses."}; for (int i = 0, n = results.size(); i < n; ++i) { From 702a703fa67ed7e5916003522d2c606d96651c35 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 27 Feb 2024 12:13:49 +1100 Subject: [PATCH 19/24] Fixed a couple of bugs and formatting Fixed key supplement request verification data Fixed an issue where functions which triggered the send hook and had their own callbacks weren't reporting send failures correctly --- src/config/groups/keys.cpp | 5 +- src/onionreq/response_parser.cpp | 2 +- src/state.cpp | 19 +- tests/test_config_contacts.cpp | 12 +- tests/test_config_convo_info_volatile.cpp | 30 +- tests/test_config_user_groups.cpp | 6 +- tests/test_group_keys.cpp | 26 +- tests/test_state.cpp | 332 ++++++++++------------ tests/utils.hpp | 3 +- 9 files changed, 223 insertions(+), 212 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 566f74dd..fe185a65 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -551,8 +551,9 @@ std::pair Keys::prepare_supplement_payload( // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and // `timestamp` are the base10 expression of the namespace and `timestamp` values std::array sig; - ustring verification = to_unsigned("store") + static_cast(storage_namespace()) + - static_cast(timestamp.count()); + ustring verification = to_unsigned("store"); + verification += to_unsigned_sv(std::to_string(static_cast(storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); if (0 != crypto_sign_ed25519_detached( diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 2ace3237..c0a917d8 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -38,7 +38,7 @@ ustring ResponseParser::decrypt(ustring ciphertext) const { ciphertext, destination_x25519_public_key_); else - throw e; + throw; } } diff --git a/src/state.cpp b/src/state.cpp index 3398f9cd..100c4bd5 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -471,7 +471,10 @@ PreparedPush State::prepare_push( for (auto& request : sorted_requests) { push_info.push_back( - {true, true, request["namespace"].get(), request["seqno"].get()}); + {true, + true, + request["namespace"].get(), + request["seqno"].get()}); request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload nlohmann::json request_json{{"method", "store"}, {"params", request}}; @@ -1094,6 +1097,7 @@ void State::create_group( cb(gid, secretkey, std::nullopt); } catch (const std::exception& e) { cb(""sv, ""_usv, e.what()); + throw; } }); } @@ -1179,9 +1183,8 @@ void State::add_group_members( group->members->set(m); // Don't bother rotating the keys if there are only admins - size_t non_admin_count = std::count_if(members.begin(), members.end(), [](const groups::member& m) { - return !m.admin; - }); + size_t non_admin_count = std::count_if( + members.begin(), members.end(), [](const groups::member& m) { return !m.admin; }); // If there are non-admins and it's not a supplemental rotation then do a rekey if (non_admin_count > 0 && !supplemental_rotation) { @@ -1194,8 +1197,8 @@ void State::add_group_members( std::vector configs = {group->info.get(), group->members.get()}; auto push = prepare_push(gid, timestamp, configs); - // If there are non-admins and it's a supplemental rotation then we want to include the key supplement within the batch - // request we are going to send + // If there are non-admins and it's a supplemental rotation then we want to include the key + // supplement within the batch request we are going to send if (non_admin_count > 0 && supplemental_rotation) { std::vector sids; std::transform( @@ -1220,7 +1223,8 @@ void State::add_group_members( auto updated_requests = updated_payload[requests_ptr]; updated_requests.insert(updated_requests.begin(), payload_json); updated_payload[requests_ptr] = updated_requests; - updated_push_info.insert(updated_push_info.begin(), {false, true, Namespace::UserProfile, 0}); + updated_push_info.insert( + updated_push_info.begin(), {false, true, Namespace::UserProfile, 0}); push = {to_unsigned(updated_payload.dump()), updated_push_info}; } @@ -1235,6 +1239,7 @@ void State::add_group_members( cb(std::nullopt); } catch (const std::exception& e) { cb(e.what()); + throw; } }); } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 88d7f9a0..59485e0d 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -324,7 +324,11 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); send_records[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); contacts_contact c3; REQUIRE(state_get_contact(state2, &c3, definitely_real_id, nullptr)); @@ -374,7 +378,11 @@ TEST_CASE("State contacts (C API)", "[state][contacts][c]") { send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); send_records_2[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records_2[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[0].callback_context); auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); REQUIRE(last_send_json_2.contains(messages_key)); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index fb1d7def..157fa52f 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -349,7 +349,11 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash1\"}}]}"); send_records[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); CHECK_FALSE(session::state::unbox(state).config().needs_dump()); @@ -433,7 +437,11 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash123\"}}]}"); send_records_2[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records_2[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[0].callback_context); CHECK_FALSE(session::state::unbox(state).config().needs_push()); std::vector seen; @@ -682,7 +690,11 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"somehash\"}}]}"); send_records[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); // Load the dump: REQUIRE(store_records.size() == 2); @@ -719,7 +731,11 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash5235\"}}]}"); send_records[1].response_cb( - true, 200, send_response.data(), send_response.size(), send_records[1].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records[1].callback_context); // But *before* we load the push make a dirtying change to conf2 that we *don't* push (so that // we'll be merging into a dirty-state config): @@ -784,7 +800,11 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 4); send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hashz\"}}]}"); send_records_2[2].response_cb( - true, 200, send_response.data(), send_response.size(), send_records_2[2].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[2].callback_context); CHECK_FALSE(session::state::unbox(state2).config().needs_push()); CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 4045836b..78071588 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -703,7 +703,11 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { ustring send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); send_records[0].response_cb( - true, 200, send_response.data(), send_response.size(), send_records[0].callback_context); + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); REQUIRE(state_current_hashes(state, nullptr, &hashes)); REQUIRE(hashes); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 4ea74e45..25ac6178 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -580,7 +580,8 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { user_secret_key{sk_from_seed(user_seed)} { char err[256]; REQUIRE(state_init(&state, user_secret_key.data(), nullptr, 0, err)); - state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_store_callback( + state, c_store_callback, reinterpret_cast(&store_records)); state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); // If we already have a group then just "approve" it @@ -782,7 +783,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { CHECK(accepted->value[2] == "fakehash3"sv); free(accepted); } - + // Due to the 'load_admin_key' behaviour admin2 will contain both admin1 and admin2 REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 2); REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 3); @@ -817,9 +818,15 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { free(merge_data_no_keys); free(merge_data); - CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); - CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); - CHECK_FALSE(session::state::unbox(admin1.state).config(admin1.group_id).pending_config().has_value()); + CHECK_FALSE( + session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state) + .config(admin1.group_id) + .needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state) + .config(admin1.group_id) + .pending_config() + .has_value()); std::vector new_members; new_members.reserve(members.size()); @@ -843,11 +850,9 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { true, new_members.data(), new_members.size(), - [](const char* error, size_t error_len, void* ctx) { - REQUIRE(error_len == 0); - }, + [](const char* error, size_t error_len, void* ctx) { REQUIRE(error_len == 0); }, nullptr); - + REQUIRE(admin1.send_records.size() == 4); send_response = session::to_unsigned( "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," @@ -882,7 +887,8 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { last_send_data_1.data(), last_send_data_1.size()}; - /* Admins will store the hash for supplemental keys messages but won't actually consider them merged so the 'accepted' array won't contain the hash */ + /* Admins will store the hash for supplemental keys messages but won't actually consider them + * merged so the 'accepted' array won't contain the hash */ for (auto& a : admins) { session_string_list* accepted; REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 2, &accepted)); diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 625d0c44..5fdc392c 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -1,18 +1,18 @@ #include +#include #include #include -#include #include "session/config/contacts.h" +#include "session/config/groups/members.h" #include "session/config/namespaces.hpp" +#include "session/config/user_groups.h" #include "session/config/user_profile.h" #include "session/config/user_profile.hpp" -#include "session/config/user_groups.h" -#include "session/config/groups/members.h" #include "session/state.h" -#include "session/state_groups.h" #include "session/state.hpp" +#include "session/state_groups.h" #include "utils.hpp" using namespace std::literals; @@ -38,7 +38,7 @@ static ustring send_response(std::vector hashes) { for (auto& hash : hashes) result += "{\"code\":200,\"body\":{\"hash\":\"" + std::string(hash) + "\"}},"; - + if (!hashes.empty()) result.pop_back(); // Remove last comma @@ -125,7 +125,8 @@ TEST_CASE("State", "[state][state]") { "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserProfile)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserProfile)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "") == "CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" "piV1rPki0Quo5Zp34MwwdZXqMFEwRpKGZJwpFPSre6jln5XlmH8tnq8djJo/" @@ -144,11 +145,7 @@ TEST_CASE("State", "[state][state]") { // Confirm the push ustring send_res = send_response({"fakehash1"}); REQUIRE(send_records[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); CHECK(store_records.size() == 2); // Should call store after confirming the push CHECK_FALSE(state.config().needs_push()); @@ -219,7 +216,8 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -227,7 +225,8 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); @@ -235,7 +234,8 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); @@ -245,11 +245,7 @@ TEST_CASE("State", "[state][state]") { CHECK(store_records.size() == 2); // Not stored until we process a success response send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); REQUIRE(send_records[1].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[1].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[1].callback_context)); CHECK(store_records.size() == 6); CHECK(store_records[2].namespace_ == Namespace::UserGroups); CHECK(store_records[3].namespace_ == Namespace::GroupKeys); @@ -292,18 +288,15 @@ TEST_CASE("State", "[state][state]") { "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records[2].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[2].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[2].callback_context)); REQUIRE(state.config().size_groups() == 1); auto member4_sid = "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"; @@ -338,7 +331,8 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -346,7 +340,8 @@ TEST_CASE("State", "[state][state]") { CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == group.id); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); @@ -361,11 +356,7 @@ TEST_CASE("State", "[state][state]") { send_res = send_response({"fakehash5", "fakehash6"}); REQUIRE(send_records[3].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[3].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[3].callback_context)); } TEST_CASE("State", "[state][state][merge failure behaviour]") { @@ -390,11 +381,7 @@ TEST_CASE("State", "[state][state][merge failure behaviour]") { CHECK(store_records.size() == 1); ustring send_res = send_response({"fakehash1"}); REQUIRE(send_records[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); CHECK(store_records.size() == 2); // Merge into state2 so they are consistent @@ -416,11 +403,7 @@ TEST_CASE("State", "[state][state][merge failure behaviour]") { CHECK(store_records_2.size() == 2); send_res = send_response({"fakehash2"}); REQUIRE(send_records_2[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); CHECK(store_records.size() == 2); state2.mutable_config().user_profile.set_name("Test Name2"); @@ -428,11 +411,7 @@ TEST_CASE("State", "[state][state][merge failure behaviour]") { CHECK(store_records_2.size() == 4); send_res = send_response({"fakehash3"}); REQUIRE(send_records_2[1].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[1].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); CHECK(store_records_2.size() == 5); REQUIRE(state2.config().get_name().has_value()); CHECK(*state2.config().get_name() == "Test Name2"); @@ -452,14 +431,11 @@ TEST_CASE("State", "[state][state][merge failure behaviour]") { to_unsigned(oxenc::from_base64( send_data[json_ptr("/params/requests/0/params/data")].get()))); to_merge.emplace_back( - Namespace::UserProfile, - "fakehash3", - 3000000000000, - to_unsigned(invalid_payload)); + Namespace::UserProfile, "fakehash3", 3000000000000, to_unsigned(invalid_payload)); merge_result = state.merge(std::nullopt, to_merge); REQUIRE(merge_result.size() == 1); CHECK(merge_result[0] == "fakehash2"); - CHECK(send_records.size() == 1); // Unchanged + CHECK(send_records.size() == 1); // Unchanged REQUIRE(state.config().get_name().has_value()); CHECK(*state.config().get_name() == "Test Name"); REQUIRE(store_records.size() == 3); @@ -470,14 +446,11 @@ TEST_CASE("State", "[state][state][merge failure behaviour]") { CHECK(store_records.size() == 3); to_merge.clear(); to_merge.emplace_back( - Namespace::UserProfile, - "fakehash3", - 3000000000000, - to_unsigned(invalid_payload)); + Namespace::UserProfile, "fakehash3", 3000000000000, to_unsigned(invalid_payload)); merge_result = state.merge(std::nullopt, to_merge); CHECK(merge_result.size() == 0); - CHECK(send_records.size() == 1); // Unchanged - CHECK(store_records.size() == 3); // Unchanged + CHECK(send_records.size() == 1); // Unchanged + CHECK(store_records.size() == 3); // Unchanged } TEST_CASE("State", "[state][state][merge key conflict]") { @@ -486,7 +459,8 @@ TEST_CASE("State", "[state][state][merge key conflict]") { "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; const ustring admin2_seed = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; - const std::string admin2_sid = "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"; + const std::string admin2_sid = + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"; const std::array member_seeds = { "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 @@ -525,36 +499,30 @@ TEST_CASE("State", "[state][state][merge key conflict]") { [&state]( std::string_view group_id, ustring_view group_sk, - std::optional error) { - REQUIRE_FALSE(error.has_value()); - }); + std::optional error) { REQUIRE_FALSE(error.has_value()); }); REQUIRE(send_records.size() == 1); auto send_data = nlohmann::json::parse(send_records[0].payload); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); ustring send_res = send_response({"fakehash1", "fakehash2", "fakehash3"}); REQUIRE(send_records[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); REQUIRE(send_records.size() == 2); // Group added to UserGroups send_data = nlohmann::json::parse(send_records[1].payload); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); send_res = send_response({"fakehash4"}); REQUIRE(send_records[1].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[1].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[1].callback_context)); REQUIRE(state.config().size_groups() == 1); auto group = *state.config().begin_groups(); @@ -570,7 +538,7 @@ TEST_CASE("State", "[state][state][merge key conflict]") { auto merge_result = state.merge(group.id, to_merge); REQUIRE(merge_result.size() == 1); CHECK(merge_result[0] == "fakehash1"); - CHECK(send_records.size() == 2); // Unchanged + CHECK(send_records.size() == 2); // Unchanged // Load the group for admin2 state_admin_2.approve_group(group.id); @@ -581,20 +549,19 @@ TEST_CASE("State", "[state][state][merge key conflict]") { REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); CHECK(send_data.value("method", "") == "sequence"); CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records_2[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); REQUIRE(send_records_2.size() == 1); // Unchanged send_data = nlohmann::json::parse(send_records[0].payload); @@ -637,9 +604,12 @@ TEST_CASE("State", "[state][state][merge key conflict]") { REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); CHECK(send_data.value("method", "") == "sequence"); CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -649,11 +619,7 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); send_res = send_response({"fakehash6"}); REQUIRE(send_records_2[1].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[1].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); // Member flagged as an admin send_data = nlohmann::json::parse(send_records_2[2].payload); @@ -664,7 +630,8 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -674,11 +641,7 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash3"); send_res = send_response({"fakehash7"}); REQUIRE(send_records_2[2].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[2].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[2].callback_context)); REQUIRE(send_records_2.size() == 3); // Unchanged REQUIRE(state_admin_2.config(group.id).admin()); @@ -712,9 +675,12 @@ TEST_CASE("State", "[state][state][merge key conflict]") { send_data = nlohmann::json::parse(send_records[2].payload); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 3); CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); @@ -722,12 +688,8 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/2"), "") == "fakehash3"); send_res = send_response({"fakehash8", "fakehash9", "fakehash10"}); REQUIRE(send_records[2].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[2].callback_context)); - + true, 200, send_res.data(), send_res.size(), send_records[2].callback_context)); + // Group keys aren't finalised until they have been retrieved and merged in to_merge.clear(); send_data = nlohmann::json::parse(send_records[2].payload); @@ -740,27 +702,26 @@ TEST_CASE("State", "[state][state][merge key conflict]") { merge_result = state.merge(group.id, to_merge); REQUIRE(merge_result.size() == 1); CHECK(merge_result[0] == "fakehash8"); - CHECK(send_records.size() == 3); // Unchanged + CHECK(send_records.size() == 3); // Unchanged REQUIRE(send_records_2.size() == 4); send_data = nlohmann::json::parse(send_records_2[3].payload); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 2); CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash7"); send_res = send_response({"fakehash11", "fakehash12", "fakehash13"}); REQUIRE(send_records_2[3].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[3].callback_context)); - + true, 200, send_res.data(), send_res.size(), send_records_2[3].callback_context)); + // Group keys aren't finalised until they have been retrieved and merged in to_merge.clear(); send_data = nlohmann::json::parse(send_records_2[3].payload); @@ -773,13 +734,14 @@ TEST_CASE("State", "[state][state][merge key conflict]") { merge_result = state_admin_2.merge(group.id, to_merge); REQUIRE(merge_result.size() == 1); CHECK(merge_result[0] == "fakehash11"); - CHECK(send_records_2.size() == 4); // Unchanged + CHECK(send_records_2.size() == 4); // Unchanged // Both configs are one the same generation (with a conflict) REQUIRE(state.config(group.id).current_generation() == 1); REQUIRE(state_admin_2.config(group.id).current_generation() == 1); - // Merge the changes from admin2 across to admin1 (the merge function should handle the conflict) + // Merge the changes from admin2 across to admin1 (the merge function should handle the + // conflict) send_data = nlohmann::json::parse(send_records_2[3].payload); REQUIRE(send_data.contains(json_ptr("/params/requests"))); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); @@ -809,16 +771,20 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK(merge_result[0] == "fakehash11"); CHECK(merge_result[1] == "fakehash12"); CHECK(merge_result[2] == "fakehash13"); - - // Admin1 should have performed a rekey as part of the merge (updating each of the group configs) + + // Admin1 should have performed a rekey as part of the merge (updating each of the group + // configs) REQUIRE(send_records.size() == 4); send_data = nlohmann::json::parse(send_records[3].payload); REQUIRE(send_data.contains(json_ptr("/params/requests"))); REQUIRE(send_data[json_ptr("/params/requests")].is_array()); REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 4); CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash9"); @@ -899,24 +865,28 @@ TEST_CASE("State c API", "[state][state][c]") { strcpy(pic.url, "http://example.com/huge.bmp"); memcpy(pic.key, "qwerty78901234567890123456789012", 32); auto members = new state_group_member[3]; - members[0] = state_group_member{"05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", "Member 0"}; - members[1] = state_group_member{"053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", "Member 1"}; - members[2] = state_group_member{"05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e", "Admin 2"}; + members[0] = state_group_member{ + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", "Member 0"}; + members[1] = state_group_member{ + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", "Member 1"}; + members[2] = state_group_member{ + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e", "Admin 2"}; state_create_group( - state, - "TestName", - 8, - "TestDesc", - 8, - pic, - members, - 3, - [](const char* group_id, unsigned const char* group_sk, const char* error, const size_t error_len, void* ctx) { - REQUIRE(error_len == 0); - }, - nullptr - ); + state, + "TestName", + 8, + "TestDesc", + 8, + pic, + members, + 3, + [](const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx) { REQUIRE(error_len == 0); }, + nullptr); free(members); REQUIRE(send_records.size() == 4); @@ -928,7 +898,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -936,7 +907,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); @@ -944,7 +916,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); @@ -954,11 +927,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(store_records.size() == 3); // Not stored until we process a success response auto send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); REQUIRE(send_records[3].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[3].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[3].callback_context)); CHECK(store_records.size() == 7); CHECK(store_records[3].namespace_ == Namespace::UserGroups); CHECK(store_records[4].namespace_ == Namespace::GroupKeys); @@ -1015,10 +984,16 @@ TEST_CASE("State c API", "[state][state][c]") { // Check that the supplemental rotation calls everything correctly members = new state_group_member[1]; - members[0] = state_group_member{"05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"}; - state_add_group_members(state, gid.c_str(), true, members, 1, [](const char* error, size_t error_len, void* ctx){ - REQUIRE(error_len == 0); - }, nullptr); + members[0] = state_group_member{ + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"}; + state_add_group_members( + state, + gid.c_str(), + true, + members, + 1, + [](const char* error, size_t error_len, void* ctx) { REQUIRE(error_len == 0); }, + nullptr); free(members); REQUIRE(send_records.size() == 6); @@ -1030,7 +1005,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -1038,7 +1014,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == gid); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 1024); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); @@ -1053,11 +1030,7 @@ TEST_CASE("State c API", "[state][state][c]") { send_res = send_response({"fakehash5", "fakehash6"}); REQUIRE(send_records[5].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records[5].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records[5].callback_context)); // Load the group for admin2 state_approve_group(state2, gid.c_str()); @@ -1068,20 +1041,19 @@ TEST_CASE("State c API", "[state][state][c]") { REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); CHECK(send_data.value("method", "") == "sequence"); CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records_2[0].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[0].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); REQUIRE(send_records_2.size() == 1); // Unchanged send_data = nlohmann::json::parse(send_records[3].payload); @@ -1127,9 +1099,12 @@ TEST_CASE("State c API", "[state][state][c]") { REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); CHECK(send_data.value("method", "") == "sequence"); CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -1139,11 +1114,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); send_res = send_response({"fakehash6"}); REQUIRE(send_records_2[1].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[1].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); // Member flagged as an admin send_data = nlohmann::json::parse(send_records_2[2].payload); @@ -1154,7 +1125,8 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 1024); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); @@ -1164,11 +1136,7 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash4"); send_res = send_response({"fakehash7"}); REQUIRE(send_records_2[2].response_cb( - true, - 200, - send_res.data(), - send_res.size(), - send_records_2[2].callback_context)); + true, 200, send_res.data(), send_res.size(), send_records_2[2].callback_context)); REQUIRE(send_records_2.size() == 3); // Unchanged REQUIRE(unbox(state2).config(gid).admin()); } \ No newline at end of file diff --git a/tests/utils.hpp b/tests/utils.hpp index cb17921a..423f00c2 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -116,8 +116,7 @@ inline void c_store_callback( const unsigned char* data, size_t data_len, void* ctx) { - static_cast*>(ctx)->emplace_back( - last_store_data{ + static_cast*>(ctx)->emplace_back(last_store_data{ static_cast(namespace_), {pubkey, 66}, timestamp_ms, From c2f8a4000283e6dcc7fb31518c9dabda6aa79319 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 27 Feb 2024 13:01:40 +1100 Subject: [PATCH 20/24] Expose an error string so iOS can catch and handle it --- include/session/errors.h | 13 +++++++++++++ include/session/errors.hpp | 11 +++++++++++ src/CMakeLists.txt | 1 + src/config/base.cpp | 3 ++- src/errors.cpp | 9 +++++++++ tests/test_group_info.cpp | 7 ++++--- 6 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 include/session/errors.h create mode 100644 include/session/errors.hpp create mode 100644 src/errors.cpp diff --git a/include/session/errors.h b/include/session/errors.h new file mode 100644 index 00000000..899fd0ec --- /dev/null +++ b/include/session/errors.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "export.h" + +LIBSESSION_EXPORT extern const char* SESSION_ERROR_READ_ONLY_CONFIG; + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/errors.hpp b/include/session/errors.hpp new file mode 100644 index 00000000..23f6db9d --- /dev/null +++ b/include/session/errors.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace session { + +struct Error { + static constexpr const char* READ_ONLY_CONFIG = "Unable to make changes to a read-only config object"; +}; + +} // namespace session diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cb957246..506acee7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,7 @@ add_libsession_util_library(config config/protos.cpp config/user_groups.cpp config/user_profile.cpp + errors.cpp fields.cpp ) diff --git a/src/config/base.cpp b/src/config/base.cpp index 47bf5ff7..aecac1d5 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -15,6 +15,7 @@ #include "internal.hpp" #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" +#include "session/errors.hpp" #include "session/export.h" #include "session/util.hpp" @@ -24,7 +25,7 @@ namespace session::config { void ConfigBase::set_state(ConfigState s) { if (s == ConfigState::Dirty && is_readonly()) - throw std::runtime_error{"Unable to make changes to a read-only config object"}; + throw std::runtime_error{Error::READ_ONLY_CONFIG}; if (_state == ConfigState::Clean && !_curr_hash.empty()) { _old_hashes.insert(std::move(_curr_hash)); diff --git a/src/errors.cpp b/src/errors.cpp new file mode 100644 index 00000000..e38ae97f --- /dev/null +++ b/src/errors.cpp @@ -0,0 +1,9 @@ +#include "session/errors.hpp" + +#include "session/export.h" + +namespace session { + +LIBSESSION_C_API const char* SESSION_ERROR_READ_ONLY_CONFIG = Error::READ_ONLY_CONFIG; + +} // extern "C" diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 1896b47f..0ab226bb 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/errors.hpp" #include "utils.hpp" using namespace std::literals; @@ -162,9 +163,9 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { ginfo.add_key(k, false); REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); + ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); + ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: @@ -202,7 +203,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.get_name() == "Super Group!!"); REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group11"), "Unable to make changes to a read-only config object"); + ginfo.set_name("Super Group11"), session::Error::READ_ONLY_CONFIG); // 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!!"); From 1f4ba98164475eec2e6db3e04888e9a1f0733ee9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 28 Feb 2024 18:47:37 +1100 Subject: [PATCH 21/24] Fixed a couple of bugs with error handling --- include/session/errors.hpp | 3 +- include/session/state.hpp | 16 ++++----- src/config/contacts.cpp | 2 ++ src/config/groups/keys.cpp | 4 +-- src/errors.cpp | 2 +- src/state.cpp | 73 ++++++++++++++++++++++---------------- src/state_c_wrapper.cpp | 32 +++++++++++------ tests/test_group_info.cpp | 9 ++--- 8 files changed, 81 insertions(+), 60 deletions(-) diff --git a/include/session/errors.hpp b/include/session/errors.hpp index 23f6db9d..f7036cd2 100644 --- a/include/session/errors.hpp +++ b/include/session/errors.hpp @@ -5,7 +5,8 @@ namespace session { struct Error { - static constexpr const char* READ_ONLY_CONFIG = "Unable to make changes to a read-only config object"; + static constexpr const char* READ_ONLY_CONFIG = + "Unable to make changes to a read-only config object"; }; } // namespace session diff --git a/include/session/state.hpp b/include/session/state.hpp index c0688f8a..f84876b9 100644 --- a/include/session/state.hpp +++ b/include/session/state.hpp @@ -49,19 +49,19 @@ class MutableUserConfigs { session::config::ConvoInfoVolatile& convo_info_volatile, session::config::UserGroups& user_groups, session::config::UserProfile& user_profile, - std::optional> set_error) : + std::optional> on_error) : parent_state(state), contacts(contacts), convo_info_volatile(convo_info_volatile), user_groups(user_groups), user_profile(user_profile), - set_error(set_error) {} + on_error(on_error) {} session::config::Contacts& contacts; session::config::ConvoInfoVolatile& convo_info_volatile; session::config::UserGroups& user_groups; session::config::UserProfile& user_profile; - std::optional> set_error; + std::optional> on_error; ~MutableUserConfigs(); }; @@ -76,13 +76,13 @@ class MutableGroupConfigs { session::config::groups::Info& info, session::config::groups::Members& members, session::config::groups::Keys& keys, - std::optional> set_error) : - parent_state(state), info(info), members(members), keys(keys), set_error(set_error) {} + std::optional> on_error) : + parent_state(state), info(info), members(members), keys(keys), on_error(on_error) {} session::config::groups::Info& info; session::config::groups::Members& members; session::config::groups::Keys& keys; - std::optional> set_error; + std::optional> on_error; std::chrono::milliseconds get_network_offset() const; @@ -473,13 +473,13 @@ class State { // Retrieves an editable version of the user config. Once the returned value is deconstructed it // will trigger the `send` and `store` hooks. MutableUserConfigs mutable_config( - std::optional> set_error = std::nullopt); + std::optional> on_error = std::nullopt); // Retrieves an editable version of the group config for the given public key. Once the returned // value is deconstructed it will trigger the `send` and `store` hooks. MutableGroupConfigs mutable_config( std::string_view pubkey_hex, - std::optional> set_error = std::nullopt); + std::optional> on_error = std::nullopt); private: template diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index ca7a1f86..0b1129e1 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -120,6 +120,7 @@ void contact_info::into(contacts_contact& c) const { c.blocked = blocked; c.priority = priority; c.notifications = static_cast(notifications); + c.mute_until = mute_until; c.exp_mode = static_cast(exp_mode); c.exp_seconds = exp_timer.count(); if (c.exp_seconds <= 0 && c.exp_mode != CONVO_EXPIRATION_NONE) @@ -142,6 +143,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, blocked = c.blocked; priority = c.priority; notifications = static_cast(c.notifications); + mute_until = c.mute_until; exp_mode = static_cast(c.exp_mode); exp_timer = exp_mode == expiration_mode::none ? 0s : std::chrono::seconds{c.exp_seconds}; if (exp_timer <= 0s && exp_mode != expiration_mode::none) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index fe185a65..5a355d44 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1453,8 +1453,8 @@ LIBSESSION_C_API bool state_rekey_group(mutable_group_state_object* state) { unbox(state).keys.rekey(unbox(state).info, unbox(state).members); return true; } catch (const std::exception& e) { - if (auto set_error = unbox(state).set_error; set_error.has_value()) - set_error.value()(e.what()); + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)(e.what()); return false; } } diff --git a/src/errors.cpp b/src/errors.cpp index e38ae97f..677a7c71 100644 --- a/src/errors.cpp +++ b/src/errors.cpp @@ -6,4 +6,4 @@ namespace session { LIBSESSION_C_API const char* SESSION_ERROR_READ_ONLY_CONFIG = Error::READ_ONLY_CONFIG; -} // extern "C" +} // namespace session diff --git a/src/state.cpp b/src/state.cpp index 100c4bd5..ccf2c25f 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -177,19 +177,19 @@ void State::load( // Reload the specified namespace with the dump if (namespace_ == Namespace::GroupInfo) { - _config_groups[gid]->info = + _config_groups.at(gid)->info = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[gid]->info); + add_child_logger(_config_groups.at(gid)->info); } else if (namespace_ == Namespace::GroupMembers) { - _config_groups[gid]->members = + _config_groups.at(gid)->members = std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); - add_child_logger(_config_groups[gid]->members); + add_child_logger(_config_groups.at(gid)->members); } else if (namespace_ == Namespace::GroupKeys) { - auto info = _config_groups[gid]->info.get(); - auto members = _config_groups[gid]->members.get(); + auto info = _config_groups.at(gid)->info.get(); + auto members = _config_groups.at(gid)->members.get(); auto keys = std::make_unique( user_ed25519_secretkey, pubkey_sv, group_ed25519_secretkey, dump, *info, *members); - _config_groups[gid]->keys = std::move(keys); + _config_groups.at(gid)->keys = std::move(keys); } else throw std::runtime_error{"Attempted to load unknown namespace"}; } @@ -292,8 +292,8 @@ void State::config_changed( } // GroupKeys needs special handling as it's not a `ConfigBase` - if (is_group_pubkey && _config_groups[target_pubkey_hex]->keys->needs_dump()) { - auto config = _config_groups[target_pubkey_hex]->keys.get(); + if (is_group_pubkey && _config_groups.at(target_pubkey_hex)->keys->needs_dump()) { + auto config = _config_groups.at(target_pubkey_hex)->keys.get(); sorted_stores.emplace_back(config->storage_namespace(), config->dump()); } @@ -353,7 +353,7 @@ PreparedPush State::prepare_push( if (group_sk) memcpy(seckey.data(), group_sk->data(), 64); else { - auto config = _config_groups[pubkey_hex]->keys.get(); + auto config = _config_groups.at(pubkey_hex)->keys.get(); auto user_group = _config_user_groups->get_group(pubkey_hex); if (!config->admin() || !user_group || user_group->secretkey.empty()) @@ -416,7 +416,7 @@ PreparedPush State::prepare_push( // GroupKeys needs special handling as it's not a `ConfigBase` if (is_group_pubkey) { - auto config = _config_groups[pubkey_hex]->keys.get(); + auto config = _config_groups.at(pubkey_hex)->keys.get(); auto pending = config->pending_config(); if (pending) { @@ -811,9 +811,12 @@ ustring State::dump(config::Namespace namespace_, std::optional extract_error(int status_code, ustring response) { - if (response.empty()) - return std::nullopt; +std::optional extract_error(bool success, int status_code, ustring response) { + // If we have an explicit failure and there is no response data then return an error + if (!success && response.empty()) + return "Failed with status code: " + std::to_string(status_code) + "."; + else if (response.empty()) + return std::nullopt; // An empty response might be valid but we can't parse std::string response_string = {from_unsigned(response.data()), response.size()}; @@ -880,7 +883,7 @@ void State::handle_config_push_response( uint16_t status_code, ustring response) { // If the request failed then just error - if (auto error = extract_error(status_code, response); error) + if (auto error = extract_error(success, status_code, response); error) throw std::runtime_error{*error}; log(LogLevel::debug, "handle_config_push_response: No simple error detected."); @@ -916,7 +919,7 @@ void State::handle_config_push_response( "the number of requests requiring responses."}; for (int i = 0, n = results.size(); i < n; ++i) { - if (!push_info[i].is_config_push) + if (push_info.size() <= i || !push_info[i].is_config_push) continue; auto result_code = results[i]["code"].get(); @@ -1017,7 +1020,6 @@ void State::create_group( } // Store the group info - assert(_config_groups[group_id]); auto& group = _config_groups.at(group_id); group->info = std::make_unique(ed_pk, ed_sk, std::nullopt); group->info->set_name(name); @@ -1188,8 +1190,8 @@ void State::add_group_members( // If there are non-admins and it's not a supplemental rotation then do a rekey if (non_admin_count > 0 && !supplemental_rotation) { - auto info = _config_groups[gid]->info.get(); - auto members = _config_groups[gid]->members.get(); + auto info = _config_groups.at(gid)->info.get(); + auto members = _config_groups.at(gid)->members.get(); group->keys->rekey(*info, *members); } @@ -1321,33 +1323,35 @@ const groups::Keys& State::config(std::string_view pubkey_hex) const { }; MutableUserConfigs State::mutable_config( - std::optional> set_error) { + std::optional> on_error) { return MutableUserConfigs( this, *_config_contacts, *_config_convo_info_volatile, *_config_user_groups, *_config_user_profile, - set_error); + on_error); }; MutableUserConfigs::~MutableUserConfigs() { - parent_state->config_changed(std::nullopt, true, true, std::nullopt); + try { + parent_state->config_changed(std::nullopt, true, true, std::nullopt); + } catch (const std::exception& e) { + if (!on_error) + throw; + + (*on_error)(e.what()); + } }; MutableGroupConfigs State::mutable_config( std::string_view pubkey_hex, - std::optional> set_error) { + std::optional> on_error) { if (pubkey_hex.size() != 66) throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; - std::string gid = {pubkey_hex.data(), pubkey_hex.size()}; - return MutableGroupConfigs( - *this, - *_config_groups[gid]->info, - *_config_groups[gid]->members, - *_config_groups[gid]->keys, - set_error); + auto& group = _config_groups.at({pubkey_hex.data(), pubkey_hex.size()}); + return MutableGroupConfigs(*this, *group->info, *group->members, *group->keys, on_error); }; std::chrono::milliseconds MutableGroupConfigs::get_network_offset() const { @@ -1355,7 +1359,14 @@ std::chrono::milliseconds MutableGroupConfigs::get_network_offset() const { }; MutableGroupConfigs::~MutableGroupConfigs() { - parent_state.config_changed(info.id, true, true, std::nullopt); + try { + parent_state.config_changed(info.id, true, true, std::nullopt); + } catch (const std::exception& e) { + if (!on_error) + throw; + + (*on_error)(e.what()); + } }; } // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp index c21bfad5..d5586271 100644 --- a/src/state_c_wrapper.cpp +++ b/src/state_c_wrapper.cpp @@ -392,9 +392,8 @@ LIBSESSION_C_API void state_create_group( std::vector members = {}; members.reserve(members_len); - for (size_t i = 0; i < members_len; i++) { + for (size_t i = 0; i < members_len; i++) members.emplace_back(groups::member{members_[i]}); - } unbox(state).create_group( {name, name_len}, @@ -417,7 +416,6 @@ LIBSESSION_C_API void state_create_group( }); } catch (const std::exception& e) { std::string_view err = e.what(); - set_error(state, err); callback(nullptr, nullptr, e.what(), err.size(), ctx); } } @@ -494,10 +492,16 @@ LIBSESSION_C_API bool state_mutate_user( }); s_object->internals = &mutable_state; callback(s_object, ctx); - return true; } catch (const std::exception& e) { - return set_error(state, e.what()); + set_error(state, e.what()); } + + // If the state has an error the it was most likely set above (even if not then it means the + // state has an unhandled error which should be handled then cleared by the caller) + if (state->last_error) + return false; + + return true; } LIBSESSION_C_API bool state_mutate_group( @@ -517,22 +521,28 @@ LIBSESSION_C_API bool state_mutate_group( }); s_object->internals = &mutable_state; callback(s_object, ctx); - return true; } catch (const std::exception& e) { - return set_error(state, e.what()); + set_error(state, e.what()); } + + // If the state has an error the it was most likely set above (even if not then it means the + // state has an unhandled error which should be handled then cleared by the caller) + if (state->last_error) + return false; + + return true; } LIBSESSION_C_API void mutable_user_state_set_error_if_empty( mutable_user_state_object* state, const char* err, size_t err_len) { - if (auto set_error = unbox(state).set_error; set_error.has_value()) - set_error.value()({err, err_len}); + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)({err, err_len}); } LIBSESSION_C_API void mutable_group_state_set_error_if_empty( mutable_group_state_object* state, const char* err, size_t err_len) { - if (auto set_error = unbox(state).set_error; set_error.has_value()) - set_error.value()({err, err_len}); + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)({err, err_len}); } } // extern "C" \ No newline at end of file diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 0ab226bb..459fb294 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -162,10 +162,8 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { for (const auto& k : enc_keys1) // Just for testing, as above. ginfo.add_key(k, false); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: @@ -202,8 +200,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.get_name() == "Super Group!!"); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group11"), session::Error::READ_ONLY_CONFIG); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group11"), session::Error::READ_ONLY_CONFIG); // 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!!"); From de5d182f9c61a8174fe25a8561d24611f5098c02 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 29 Feb 2024 09:00:02 +1100 Subject: [PATCH 22/24] Attempt to fix CI build errors --- include/session/config/namespaces.hpp | 2 ++ src/CMakeLists.txt | 1 + src/errors.cpp | 1 + src/state.cpp | 13 +++++-------- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 76818f53..6b3b827b 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -39,6 +39,8 @@ namespace { case Namespace::RevokedRetrievableGroupMessages: return "RevokedRetrievableGroupMessages"; + + default: return "Invalid"; } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 506acee7..3771a2ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ target_link_libraries(state PUBLIC crypto common + config libsession::protos PRIVATE nlohmann_json::nlohmann_json diff --git a/src/errors.cpp b/src/errors.cpp index 677a7c71..67f97b11 100644 --- a/src/errors.cpp +++ b/src/errors.cpp @@ -1,5 +1,6 @@ #include "session/errors.hpp" +#include "session/errors.h" #include "session/export.h" namespace session { diff --git a/src/state.cpp b/src/state.cpp index ccf2c25f..83704a7a 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -15,6 +15,7 @@ #include "session/config/contacts.hpp" #include "session/config/convo_info_volatile.hpp" #include "session/config/groups/members.hpp" +#include "session/config/groups/keys.hpp" #include "session/config/namespaces.h" #include "session/config/namespaces.hpp" #include "session/config/user_groups.hpp" @@ -1337,10 +1338,8 @@ MutableUserConfigs::~MutableUserConfigs() { try { parent_state->config_changed(std::nullopt, true, true, std::nullopt); } catch (const std::exception& e) { - if (!on_error) - throw; - - (*on_error)(e.what()); + if (on_error) + (*on_error)(e.what()); } }; @@ -1362,10 +1361,8 @@ MutableGroupConfigs::~MutableGroupConfigs() { try { parent_state.config_changed(info.id, true, true, std::nullopt); } catch (const std::exception& e) { - if (!on_error) - throw; - - (*on_error)(e.what()); + if (on_error) + (*on_error)(e.what()); } }; From 209b4b499204e2bc36c76bcb333e80122a235eb0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 29 Feb 2024 12:25:22 +1100 Subject: [PATCH 23/24] Moved the 'state' stuff from it's own lib into 'config' --- src/CMakeLists.txt | 18 +----------------- tests/CMakeLists.txt | 1 - 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3771a2ec..f38af186 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -74,15 +74,11 @@ add_libsession_util_library(config config/user_groups.cpp config/user_profile.cpp errors.cpp - fields.cpp -) - -add_libsession_util_library(state state.cpp state_c_wrapper.cpp + fields.cpp ) - target_link_libraries(crypto PUBLIC common @@ -101,18 +97,6 @@ target_link_libraries(config libzstd::static ) -target_link_libraries(state - PUBLIC - crypto - common - config - libsession::protos - PRIVATE - nlohmann_json::nlohmann_json - libsodium::sodium-internal - libzstd::static -) - if(ENABLE_ONIONREQ) add_libsession_util_library(onionreq onionreq/builder.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 550bd2f4..6a2f9bd2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,7 +29,6 @@ add_executable(testAll target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq - libsession::state libsodium::sodium-internal nlohmann_json::nlohmann_json Catch2::Catch2WithMain) From d77458c3df78b506cfcb4cee5399f4cf9806d2a6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 29 Feb 2024 13:13:20 +1100 Subject: [PATCH 24/24] PR comment fixes, ran the formatter, test fixes --- include/session/config/namespaces.hpp | 4 +- src/state.cpp | 11 ++--- tests/test_state.cpp | 70 +++++++++++++-------------- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 6b3b827b..ac969df3 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -39,9 +39,9 @@ namespace { case Namespace::RevokedRetrievableGroupMessages: return "RevokedRetrievableGroupMessages"; - - default: return "Invalid"; } + + return "Invalid"; } /// Returns a number indicating the order that the config dumps should be loaded in, we need to diff --git a/src/state.cpp b/src/state.cpp index 83704a7a..341a8f22 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -14,8 +14,8 @@ #include "session/config/base.hpp" #include "session/config/contacts.hpp" #include "session/config/convo_info_volatile.hpp" -#include "session/config/groups/members.hpp" #include "session/config/groups/keys.hpp" +#include "session/config/groups/members.hpp" #include "session/config/namespaces.h" #include "session/config/namespaces.hpp" #include "session/config/user_groups.hpp" @@ -319,10 +319,7 @@ void State::config_changed( log(LogLevel::debug, "config_changed: Call 'send'"); _send(target_pubkey_hex, push.payload, - [this, - pubkey = std::move(target_pubkey_hex), - push, - after_send = std::move(after_send)]( + [this, pubkey = target_pubkey_hex, push, after_send = std::move(after_send)]( bool success, uint16_t status_code, ustring response) { handle_config_push_response(pubkey, push.info, success, status_code, response); @@ -1066,7 +1063,7 @@ void State::create_group( _send(group_id, push.payload, [this, - gid = std::move(group_id), + gid = group_id, push_info = push.info, secretkey = std::move(ed_sk), n = std::move(name), @@ -1233,7 +1230,7 @@ void State::add_group_members( _send(gid, push.payload, - [this, gid = std::move(gid), push_info = push.info, cb = std::move(callback)]( + [this, gid = gid, push_info = push.info, cb = std::move(callback)]( bool success, int16_t status_code, ustring response) { try { // Call through to the default 'handle_config_push_response' first to update it's diff --git a/tests/test_state.cpp b/tests/test_state.cpp index 5fdc392c..99d49cd3 100644 --- a/tests/test_state.cpp +++ b/tests/test_state.cpp @@ -139,7 +139,7 @@ TEST_CASE("State", "[state][state]") { "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/yAA"); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); CHECK(state.config().get_seqno() == 1); // Confirm the push @@ -218,28 +218,28 @@ TEST_CASE("State", "[state][state]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), uint64_t(0)) == 2592000000); CHECK_FALSE(state.config().needs_push()); CHECK(store_records.size() == 2); // Not stored until we process a success response @@ -290,10 +290,10 @@ TEST_CASE("State", "[state][state]") { "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records[2].response_cb( true, 200, send_res.data(), send_res.size(), send_records[2].callback_context)); @@ -333,19 +333,19 @@ TEST_CASE("State", "[state][state]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == group.id); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == group.id); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); @@ -555,10 +555,10 @@ TEST_CASE("State", "[state][state][merge key conflict]") { "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records_2[0].response_cb( true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); @@ -610,10 +610,10 @@ TEST_CASE("State", "[state][state][merge key conflict]") { "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); @@ -632,10 +632,10 @@ TEST_CASE("State", "[state][state][merge key conflict]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash3"); @@ -900,28 +900,28 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 5324); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupInfo)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/2/params/data"), "").size() == 684); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), uint64_t(0)) == 2592000000); CHECK_FALSE(unbox(state).config().needs_push()); CHECK(store_records.size() == 3); // Not stored until we process a success response @@ -1007,19 +1007,19 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupKeys)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 264); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == gid); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/1/params/data"), "").size() == 1024); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == gid); CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); @@ -1047,10 +1047,10 @@ TEST_CASE("State c API", "[state][state][c]") { "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); send_res = send_response({"fakehash5"}); REQUIRE(send_records_2[0].response_cb( true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); @@ -1105,10 +1105,10 @@ TEST_CASE("State c API", "[state][state][c]") { "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::UserGroups)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 576); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); @@ -1127,10 +1127,10 @@ TEST_CASE("State c API", "[state][state][c]") { CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == static_cast(Namespace::GroupMembers)); - CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "").size() == 1024); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); - CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), 0L) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash4");