From 5d10667082ca258f3c1bdcb6223d374285357859 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 2 Aug 2023 21:15:35 -0300 Subject: [PATCH 001/572] Group metadata; signed & read-only configs support Adds a group info config type, which tracks distributed info for v2 groups. This requires introducing/using a few new concepts not currently used for user config messages: - Multiple decryption keys. User config doesn't do this at all (rather it generates a single decryption key from the private key for each namespace). This doesn't yet add support for sharing and distributing those keys, just for having being able to load a config with a list of multiple possible keys. - Config signing and verification. For user configs this isn't done, since only the owner can actually encrypt/decrypt a config message, just being able to decrypt it is authentication enough. This required various modifications to make the config library properly prevent modifications when we can't modify, and to properly follow that through in terms of merging, updates, etc. --- external/oxen-encoding | 2 +- include/session/config.hpp | 4 +- include/session/config/base.h | 51 +++++ include/session/config/base.hpp | 116 ++++++++++- include/session/config/groups/info.h | 269 +++++++++++++++++++++++++ include/session/config/groups/info.hpp | 267 ++++++++++++++++++++++++ include/session/config/namespaces.hpp | 3 + src/CMakeLists.txt | 1 + src/config.cpp | 44 +++- src/config/base.cpp | 129 ++++++++++-- src/config/encrypt.cpp | 3 +- src/config/groups/info.cpp | 107 ++++++++++ tests/CMakeLists.txt | 1 + tests/test_group_info.cpp | 187 +++++++++++++++++ tests/utils.hpp | 15 ++ 15 files changed, 1173 insertions(+), 26 deletions(-) create mode 100644 include/session/config/groups/info.h create mode 100644 include/session/config/groups/info.hpp create mode 100644 src/config/groups/info.cpp create mode 100644 tests/test_group_info.cpp diff --git a/external/oxen-encoding b/external/oxen-encoding index fc85dfd3..3bca3ac2 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit fc85dfd352e8474bc7195b0ba881838bd72ebea6 +Subproject commit 3bca3ac22dac31258a4dd158e1e6568aa2315c75 diff --git a/include/session/config.hpp b/include/session/config.hpp index 45044615..f02fe961 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -87,7 +87,7 @@ class ConfigMessage { /// (so that they can return a reference to it). seqno_hash_t seqno_hash_{0, {0}}; - bool verified_signature_ = false; + std::optional> verified_signature_; // This will be set during construction from configs based on the merge result: // -1 means we had to merge one or more configs together into a new merged config @@ -214,7 +214,7 @@ class ConfigMessage { /// Returns true if this message contained a valid, verified signature when it was parsed. /// Returns false otherwise (e.g. not loaded from verification at all; loaded without a /// verification function; or had no signature and a signature wasn't required). - bool verified_signature() const { return verified_signature_; } + bool verified_signature() const { return verified_signature_.has_value(); } /// Constructs a new MutableConfigMessage from this config message with an incremented seqno. /// The new config message's diff will reflect changes made after this construction. diff --git a/include/session/config/base.h b/include/session/config/base.h index de9806e3..522445f1 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -446,6 +446,57 @@ LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, siz /// - `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. +/// +/// 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 987e587d..1c230b60 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,8 @@ class ConfigBase { // Tracks our current state ConfigState _state = ConfigState::Clean; + void init_from_dump(std::string_view dump); + static constexpr size_t KEY_SIZE = 32; // Contains the base key(s) we use to encrypt/decrypt messages. If non-empty, the .front() @@ -65,6 +68,15 @@ class ConfigBase { size_t _keys_size = 0; size_t _keys_capacity = 0; + // Contains an optional signing keypair; if the public key is set then incoming messages must + // contain a valid signature from that key to be loaded. If the private key is set then a + // signature will be added to the message signed by that key. (Note that if a public key is set + // but not a private key then this config object cannot push config changes!) + using Ed25519PubKey = std::array; + using Ed25519Secret = std::array; + std::optional _sign_pk = std::nullopt; + Ed25519Secret* _sign_sk = nullptr; + // Contains the current active message hash, as fed into us in `confirm_pushed()`. Empty if we // don't know it yet. When we dirty the config this value gets moved into `old_hashes_` to be // removed by the next push. @@ -78,7 +90,15 @@ class ConfigBase { // 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. - explicit ConfigBase(std::optional dump = std::nullopt); + // + // Can optionally be passed a pubkey or secretkey (or both, but the pubkey can be obtained from + // the secretkey automatically): if either is given, the config object is set up to require + // 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 dump = std::nullopt, + std::optional ed25519_pubkey = std::nullopt, + std::optional ed25519_secretkey = std::nullopt); // Tracks whether we need to dump again; most mutating methods should set this to true (unless // calling set_state, which sets to to true implicitly). @@ -684,6 +704,13 @@ class ConfigBase { public: virtual ~ConfigBase(); + // Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to + // be managed. + ConfigBase(ConfigBase&&) = delete; + ConfigBase(const ConfigBase&) = delete; + ConfigBase& operator=(ConfigBase&&) = delete; + ConfigBase& operator=(const ConfigBase&) = delete; + // Proxy class providing read and write access to the contained config data. const DictFieldRoot data{*this}; @@ -794,6 +821,43 @@ class ConfigBase { /// - `bool` -- Returns true if changes have been serialized bool is_clean() const { return _state == ConfigState::Clean; } + /// API: base/ConfigBase::is_readonly + /// + /// Returns true if this config object is in read-only mode: specifically that means that this + /// config object can only absorb new config entries but is incapable of producing new entries, + /// and thus cannot modify or merge configs. + /// + /// This currently happens for config messages that require verification of a signature but do + /// not have the private keys required to *produce* a signature. For private config types, such + /// as single-user configs, this will never be the case (as those can only be decrypted in the + /// first place if you possess the private key). Note, however, that additional conditions for + /// read-only could be added in the future, so this being true should not *strictly* be + /// interpreted as a cannot-sign issue. + /// + /// There are some consequences of being readonly: + /// + /// - any attempt to modify config values will throw an exception. + /// - when multiple conflicting config objects are loaded only the "best" (i.e. higher seqno, + /// with ties determined by hashed value) config is loaded; if values need to be merged this + /// config will ignore the alternate values until someone who can produce a signature produces + /// a merged config that properly incorporates (and signs) the updated config. + /// - read-only configurations never have anything to push, that is, `needs_push()` will always + /// be false. + /// - it is still possible to `push()` a config anyway, but this only returns the current config + /// and signature of the message currently being used, and *never* returns any obsolete + /// hashes. Typically this is unlikely to be useful, as it is expected that only signers (who + /// can update and merge) are likely also the only ones who can actually push new configs to + /// the swarm. + /// - read-only configurations do not reliably track obsolete hashes as the obsolesence logic + /// depends on the results of merging, which read-only configs do not support. (If you do + /// call `push()`, you'll always just get back an empty list of obsolete hashes). + /// + /// Inputs: None + /// + /// Outputs: + /// - `bool` true if this config object is read-only + bool is_readonly() const { return _config->verifier && !_config->signer; } + /// API: base/ConfigBase::current_hashes /// /// The current config hash(es); this can be empty if the current hash is unknown or the current @@ -996,6 +1060,56 @@ class ConfigBase { assert(i < _keys_size); return {_keys[i].data(), _keys[i].size()}; } + + /// API: base/ConfigBase::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` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey + /// concatenated together) that sets both the secret key and public key. + void set_sig_keys(ustring_view secret); + + /// API: base/ConfigBase::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 `set_sig_keys()` to set both + /// secret and pubkey keys together). + /// + /// Inputs: + /// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages + void set_sig_pubkey(ustring_view pubkey); + + /// API: base/ConfigBase::get_sig_pubkey + /// + /// Returns a const reference to the 32-byte Ed25519 signing pubkey, if set. + /// + /// Outputs: + /// - reference to the 32-byte pubkey, or `std::nullopt` if not set. + const std::optional>& get_sig_pubkey() const { return _sign_pk; } + + /// API: base/ConfigBase::clear_sig_keys + /// + /// Drops the signature pubkey and/or secret key, if the object has them. + /// + /// Inputs: none. + void clear_sig_keys(); }; // 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 new file mode 100644 index 00000000..d5326192 --- /dev/null +++ b/include/session/config/groups/info.h @@ -0,0 +1,269 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../base.h" +#include "../profile_pic.h" +#include "../util.h" + +/// API: groups/group_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`). +/// +/// Declaration: +/// ```cpp +/// INT group_info_init( +/// [out] config_object** conf, +/// [in] const unsigned char** keys, +/// [in] size_t keylen, +/// [in] const unsigned char* dump, +/// [in] size_t dumplen, +/// [out] char* error +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [out] Pointer to the config object +/// - `keys` -- pointer to the beginning of an array of 32-byte encryption/decryption keys for this +/// group info. These should be specified in most-recent-to-least-recent order; the *first* key +/// will be the one used for encryption when pushing an update. +/// - `keylen` -- the number of the `keys` array +/// - `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); + +/// API: contacts/contacts_iterator_done +/// +/// 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 +/// +/// Outputs: +/// - `bool` -- True if iteration has reached the end +LIBSESSION_EXPORT bool contacts_iterator_done(contacts_iterator* it, contacts_contact* c); + +/// API: contacts/contacts_iterator_advance +/// +/// 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); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp new file mode 100644 index 00000000..5276bdaf --- /dev/null +++ b/include/session/config/groups/info.hpp @@ -0,0 +1,267 @@ +#pragma once + +#include +#include +#include + +#include "../base.hpp" +#include "../namespaces.hpp" +#include "../profile_pic.hpp" + +namespace session::config::groups { + +using namespace std::literals; + +/// keys used in this config, either currently or in the past (so that we don't reuse): +/// +/// ! - set to true if the group has been destroyed (and should be removed from receiving clients) +/// c - creation unix timestamp (seconds) +/// d - delete before timestamp: this instructs receiving clients that they should delete all +/// messages with a timestamp < the set value. +/// D - delete attachments before - same as above, but specific to attachments. +/// E - disappearing message timer (seconds) if the delete-after-send disappearing messages mode is +/// enabled for the group. Omitted if disappearing messages is disabled. +/// n - utf8 group name (human-readable) +/// p - group profile url +/// q - group profile decryption key (binary) + +class Info final : public ConfigBase { + + public: + // No default constructor + Info() = delete; + + /// API: groups/Info::Info + /// + /// Constructs a group info config object from existing data (stored from `dump()`) and a list + /// of encryption keys for encrypting new and decrypting existing messages. + /// + /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass + /// `std::nullopt` as the second argument. + /// + /// Inputs: + /// - `keys` -- contains the possible 32-byte en/decryption keys that may be used for incoming + /// messages. These are *not* Ed25519 secret keys, but rather symmetric encryption keys used + /// for encryption (generally generated using a cryptographically secure random generator). + /// The *first* key in this list will be used to encrypt outgoing config messages (and so, in + /// general, should be the most current key). There must always be at least one key present + /// (either provided at construction or via add_keys) before you can push a config. + /// Post-construction you can add or remove keys via add_key/remove_key/clear_keys from + /// ConfigBase. + /// - `ed25519_pubkey` is the public key of this group, used to validate config messages. + /// Config messages not signed with this key will be rejected. + /// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages. + /// This is only possessed by the group admin(s), and must be provided in order to make and + /// push config changes. + /// - `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()`. + Info(const std::vector& keys, + ustring_view ed25519_pubkey, + std::optional ed25519_secretkey, + std::optional dumped); + + /// API: groups/Info::storage_namespace + /// + /// Returns the Info namespace. Is constant, will always return Namespace::GroupInfo + /// + /// Inputs: None + /// + /// Outputs: + /// - `Namespace` - Will return Namespace::GroupInfo + Namespace storage_namespace() const override { return Namespace::GroupInfo; } + + /// API: groups/Info::encryption_domain + /// + /// Returns the encryption domain used when encrypting messages of this type. + /// + /// Inputs: None + /// + /// Outputs: + /// - `const char*` - Will return "groups::Info" + const char* encryption_domain() const override { return "groups::Info"; } + + /// API: groups/Info::get_name + /// + /// Returns the group name, or std::nullopt if there is no group name set. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` - Returns the group name if it is set + std::optional get_name() const; + + /// API: groups/Info::set_name + /// + /// Sets the group name; if given an empty string then the name is removed. + /// + /// Declaration: + /// ```cpp + /// void set_name(std::string_view new_name); + /// ``` + /// + /// Inputs: + /// - `new_name` -- The name to be put into the group Info + void set_name(std::string_view new_name); + + /// API: groups/Info::get_profile_pic + /// + /// Gets the group's current profile pic URL and decryption key. The returned object will + /// evaluate as false if the URL and/or key are not set. + /// + /// Declaration: + /// ```cpp + /// profile_pic get_group_pic() const; + /// ``` + /// + /// Inputs: None + /// + /// Outputs: + /// - `profile_pic` - Returns the group's profile pic + profile_pic get_profile_pic() const; + + /// API: groups/Info::set_profile_pic + /// + /// Sets the group'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); + void set_profile_pic(profile_pic pic); + + /// API: groups/Info::set_expiry_timer + /// + /// Sets (or clears) the group's message expiry timer. If > 0s the setting becomes the + /// delete-after-send value; if omitted or given a 0 or negative duration then the expiring + /// message timer is disabled for the group. + /// + /// Inputs: + /// - `expiration_timer` -- how long the expiration timer should be, defaults to zero (disabling + /// message expiration) if the argument is omitted. + void set_expiry_timer(std::chrono::seconds expiration_timer = 0min); + + /// API: groups/Info::get_expiry_timer + /// + /// Returns the group's current message expiry timer, or `std::nullopt` if no expiry timer is + /// set. If not nullopt then the expiry will always be >= 1s. + /// + /// Note that groups only support expire-after-send expiry timers and so there is no separate + /// expiry type setting. + /// + /// Inputs: none + /// + /// Outputs: + /// - `std::chrono::seconds` -- the expiry timer duration + std::optional get_expiry_timer() const; + + /// API: groups/Info::set_created + /// + /// Sets the created timestamp. It's recommended (but not required) that you only set this if + /// not already set. + /// + /// Inputs: + /// - `session_id` -- hex string of the session id + /// - `timestamp` -- standard unix timestamp when the group was created + void set_created(int64_t timestamp); + + /// API: groups/Info::get_created + /// + /// Returns the creation timestamp, if set/known. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `std::optional` -- the unix timestamp when the group was created, or nullopt if + /// the creation timestamp is not set. + std::optional get_created() const; + + /// API: groups/Info::set_delete_before + /// + /// Sets a "delete before" unix timestamp: this instructs clients to delete all messages from + /// the closed group history with a timestamp earlier than this value. Returns nullopt if no + /// delete-before timestamp is set. + /// + /// The given value is not checked for sanity (e.g. if you pass milliseconds it will be + /// interpreted as deleting everything for the next 50000+ years). Be careful! + /// + /// Inputs: + /// - `timestamp` -- the new unix timestamp before which clients should delete messages. Pass 0 + /// (or negative) to disable the delete-before timestamp. + void set_delete_before(int64_t timestamp); + + /// API: groups/Info::get_delete_before + /// + /// Returns the delete-before unix timestamp (seconds) for the group; clients should delete all + /// messages from the closed group with timestamps earlier than this value, if set. + /// + /// Returns std::nullopt if no delete-before timestamp is set. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `int64_t` -- the unix timestamp for which all older messages shall be delete + std::optional get_delete_before() const; + + /// API: groups/Info::set_delete_attach_before + /// + /// Sets a "delete attachments before" unix timestamp: this instructs clients to drop the + /// attachments (though not necessarily the messages themselves; see `get_delete_before` for + /// that) from any messages older than the given timestamp. Returns nullopt if no + /// delete-attachments-before timestamp is set. + /// + /// The given value is not checked for sanity (e.g. if you pass milliseconds it will be + /// interpreted as deleting all attachments for the next 50000+ years). Be careful! + /// + /// Inputs: + /// - `timestamp` -- the new unix timestamp before which clients should delete attachments. Pass + /// 0 + /// (or negative) to disable the delete-attachment-before timestamp. + void set_delete_attach_before(int64_t timestamp); + + /// API: groups/Info::get_delete_before + /// + /// Returns the delete-before unix timestamp (seconds) for the group; clients should delete all + /// messages from the closed group with timestamps earlier than this value, if set. + /// + /// Returns std::nullopt if no delete-before timestamp is set. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `int64_t` -- the unix timestamp for which all older messages shall be delete + std::optional get_delete_attach_before() const; + + /// API: groups/Info::destroy_group() + /// + /// Sets the group as permanently deleted, and set this status in the group's config. Receiving + /// clients are supposed to remove the conversation from their conversation list when this + /// happens. + /// + /// This change is permanent; the flag cannot be unset once set! + /// + /// Inputs: + /// + /// None: this call is destructive and permanent. Be careful! + void destroy_group(); + + /// API: groups/Info::is_destroyed() + /// + /// Returns true if this group has been marked destroyed; the receiving client is expected to + /// delete it. + /// + /// Outputs: + /// - `true` if the group has been destroyed, `false` otherwise. + bool is_destroyed() const; +}; + +} // namespace session::config::groups diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 394617c0..c80957f9 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -9,6 +9,9 @@ enum class Namespace : std::int16_t { Contacts = 3, ConvoInfoVolatile = 4, UserGroups = 5, + + // Groups namespaces (i.e. for config of the group itself, not one user's group settings) + GroupInfo = 11, }; } // namespace session::config diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f4f836b5..1b5dcdeb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(config config/convo_info_volatile.cpp config/encrypt.cpp config/error.c + config/groups/info.cpp config/internal.cpp config/user_groups.cpp config/user_profile.cpp diff --git a/src/config.cpp b/src/config.cpp index 2410908c..83abde0a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -593,9 +593,12 @@ ConfigMessage::ConfigMessage( if (sig.empty()) { if (!signature_optional) throw missing_signature{"Config signature is missing"}; - } else if (verified_signature_ = verifier(to_verify, sig); !verified_signature_) { + } else if (sig.size() != 64) + throw signature_error{"Config signature is invalid (not 64B)"}; + else if (!verifier(to_verify, sig)) throw signature_error{"Config signature failed verification"}; - } + else + std::memcpy(verified_signature_.emplace().data(), sig.data(), 64); } } catch (const oxenc::bt_deserialize_invalid& err) { throw config_parse_error{"Failed to parse config file: "s + err.what()}; @@ -657,7 +660,7 @@ ConfigMessage::ConfigMessage( assert(curr_confs >= 1); if (curr_confs == 1) { - // We have just one config left after all that, so we become it directly as-is + // We have just one non-redundant config left after all that, so we become it directly as-is for (int i = 0; i < configs.size(); i++) { if (!configs[i].second) { *this = std::move(configs[i].first); @@ -665,12 +668,32 @@ ConfigMessage::ConfigMessage( return; } } - assert(false); + assert(!"we counted one good config but couldn't find it?!"); + } + + // Otherwise we have more than one valid config, so have to merge them. + + // ... Unless we require signature verification but can't sign, in which case we can't actually + // produce a proper merge, so we will just keep the highest (highest seqno, hash) config and use + // that, dropping the rest. Someone else (with signing power) will have to merge and push the + // merge out to us. + if (verifier && !signer && !signature_optional) { + auto best_it = + std::max_element(configs.begin(), configs.end(), [](const auto& a, const auto& b) { + if (a.second != b.second) // Exactly one of the two is redundant + return a.second; // a < b iff a is redundant + return a.first.seqno_hash_ < b.first.seqno_hash_; + }); + *this = std::move(best_it->first); + unmerged_ = std::distance(configs.begin(), best_it); + return; } + unmerged_ = -1; - // Clear any redundant messages + // Clear any redundant messages. (we do it *here* rather than above because, in the + // single-good-config case, above, we need the index of the good config for `unmerged_`). configs.erase( std::remove_if(configs.begin(), configs.end(), [](const auto& c) { return c.second; }), configs.end()); @@ -750,6 +773,7 @@ const oxenc::bt_dict& ConfigMessage::diff() { } const oxenc::bt_dict& MutableConfigMessage::diff() { + verified_signature_.reset(); prune(); diff_ = diff_impl(orig_data_, data_).value_or(oxenc::bt_dict{}); return diff_; @@ -792,7 +816,15 @@ ustring ConfigMessage::serialize_impl(const oxenc::bt_dict& curr_diff, bool enab unknown_it = append_unknown(outer, unknown_it, unknown_.end(), "~"); assert(unknown_it == unknown_.end()); - if (signer && enable_signing) { + if (verified_signature_) { + // We have the signature attached to the current message, so use it. (This will get cleared + // if we do anything that changes the config). + outer.append( + "~", + std::string_view{ + reinterpret_cast(verified_signature_->data()), + verified_signature_->size()}); + } else if (signer && enable_signing) { auto to_sign = to_unsigned_sv(outer.view()); // The view contains the trailing "e", but we don't sign it (we are going to append the // signature there instead): diff --git a/src/config/base.cpp b/src/config/base.cpp index 3f80d5e2..5364b810 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -19,6 +20,10 @@ using namespace std::literals; 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"}; + if (_state == ConfigState::Clean && !_curr_hash.empty()) { _old_hashes.insert(std::move(_curr_hash)); _curr_hash.clear(); @@ -63,12 +68,21 @@ int ConfigBase::merge(const std::vector>& c std::vector all_confs; all_hashes.reserve(configs.size() + 1); all_confs.reserve(configs.size() + 1); + // We serialize our current config and include it in the list of configs to be merged, as if it // had already been pushed to the server (so that this code will be identical whether or not the // value was pushed). - auto mine = _config->serialize(); - all_hashes.emplace_back(_curr_hash); - all_confs.emplace_back(mine); + // + // (We skip this for seqno=0, but that's just a default-constructed, nothing-in-the-config case + // for which we also can't have or produce a signature, so there's no point in even trying to + // merge it). + + ustring mine; + if (old_seqno != 0 || is_dirty()) { + mine = _config->serialize(); + all_hashes.emplace_back(_curr_hash); + all_confs.emplace_back(mine); + } std::vector> plaintexts; @@ -165,8 +179,8 @@ int ConfigBase::merge(const std::vector>& c auto new_conf = make_config_message( _state == ConfigState::Dirty, all_confs, - nullptr, /* FIXME for signed messages: verifier */ - nullptr, /* FIXME for signed messages: signer */ + _config->verifier, + _config->signer, config_lags(), false, /* signature not optional (if we have a verifier) */ [&](size_t i, const config_error& e) { @@ -219,7 +233,7 @@ int ConfigBase::merge(const std::vector>& c } return all_confs.size() - bad_confs.size() - - 1; // -1 because we don't count the first one (reparsing ourself). + (mine.empty() ? 0 : 1); // -1 because we don't count the first one (reparsing ourself). } std::vector ConfigBase::current_hashes() const { @@ -271,8 +285,9 @@ std::tuple> ConfigBase::push() { if (is_dirty()) set_state(ConfigState::Waiting); - for (auto& old : _old_hashes) - obs.push_back(std::move(old)); + if (!is_readonly()) + for (auto& old : _old_hashes) + obs.push_back(std::move(old)); _old_hashes.clear(); return ret; @@ -291,6 +306,10 @@ ustring ConfigBase::dump() { auto data = _config->serialize(false /* disable signing for local storage */); auto data_sv = from_unsigned_sv(data); oxenc::bt_list old_hashes; + + if (is_readonly()) + _old_hashes.clear(); + for (auto& old : _old_hashes) old_hashes.emplace_back(old); oxenc::bt_dict d{ @@ -307,15 +326,30 @@ ustring ConfigBase::dump() { return ustring{to_unsigned_sv(dumped)}; } -ConfigBase::ConfigBase(std::optional dump) { +ConfigBase::ConfigBase( + std::optional dump, + std::optional ed25519_pubkey, + std::optional ed25519_secretkey) { + if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; - if (!dump) { + + if (dump) + init_from_dump(from_unsigned_sv(*dump)); + else _config = std::make_unique(); - return; + + if (ed25519_secretkey) { + if (ed25519_pubkey) + assert(*ed25519_pubkey == ed25519_secretkey->substr(32)); + set_sig_keys(*ed25519_secretkey); + } else if (ed25519_pubkey) { + set_sig_pubkey(*ed25519_pubkey); } +} - oxenc::bt_dict_consumer d{from_unsigned_sv(*dump)}; +void ConfigBase::init_from_dump(std::string_view dump) { + oxenc::bt_dict_consumer d{dump}; if (!d.skip_until("!")) throw std::runtime_error{"Unable to parse dumped config data: did not find '!' state key"}; _state = static_cast(d.consume_integer()); @@ -330,9 +364,8 @@ ConfigBase::ConfigBase(std::optional dump) { // could store a dump. _config = std::make_unique( to_unsigned_sv(d.consume_string_view()), - nullptr, // FIXME: verifier; but maybe want to delay setting this since it - // shouldn't be signed? - nullptr, // FIXME: signer + nullptr, // verifier, set later + nullptr, // signer, set later config_lags(), true /* signature optional because we don't sign the dump */); else @@ -354,6 +387,8 @@ ConfigBase::ConfigBase(std::optional dump) { ConfigBase::~ConfigBase() { sodium_free(_keys); + if (_sign_sk) + sodium_free(_sign_sk); } int ConfigBase::key_count() const { @@ -455,6 +490,51 @@ void ConfigBase::load_key(ustring_view ed25519_secretkey) { add_key(ed25519_secretkey.substr(0, 32)); } +void ConfigBase::set_sig_keys(ustring_view secret) { + if (secret.size() != 64) + throw std::invalid_argument{"Invalid sodium secret: expected 64 bytes"}; + clear_sig_keys(); + _sign_sk = static_cast(sodium_malloc(sizeof(Ed25519Secret))); + std::memcpy(_sign_sk->data(), secret.data(), secret.size()); + _sign_pk.emplace(); + crypto_sign_ed25519_sk_to_pk(_sign_pk->data(), _sign_sk->data()); + + _config->verifier = [this](ustring_view data, ustring_view sig) { + return 0 == crypto_sign_ed25519_verify_detached( + sig.data(), data.data(), data.size(), _sign_pk->data()); + }; + _config->signer = [this](ustring_view data) { + ustring sig; + sig.resize(64); + if (0 != crypto_sign_ed25519_detached( + sig.data(), nullptr, data.data(), data.size(), _sign_sk->data())) + throw std::runtime_error{"Internal error: config signing failed!"}; + return sig; + }; +} + +void ConfigBase::set_sig_pubkey(ustring_view pubkey) { + if (pubkey.size() != 32) + throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; + _sign_pk.emplace(); + std::memcpy(_sign_pk->data(), pubkey.data(), 32); + + _config->verifier = [this](ustring_view data, ustring_view sig) { + return 0 == crypto_sign_ed25519_verify_detached( + sig.data(), data.data(), data.size(), _sign_pk->data()); + }; +} + +void ConfigBase::clear_sig_keys() { + _sign_pk.reset(); + if (_sign_sk) { + sodium_free(_sign_sk); + _sign_sk = nullptr; + } + _config->signer = nullptr; + _config->verifier = nullptr; +} + void set_error(config_object* conf, std::string e) { auto& error = unbox(conf).error; error = std::move(e); @@ -596,6 +676,25 @@ 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) diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 0694fa09..8729131b 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -40,7 +40,8 @@ static std::array ma // We hash the key because we're using a deterministic nonce: the `key_base` value is expected // to be a long-term value for which nonce reuse (via hash collision) would be bad: by // incorporating the domain and message size we at least vary the key to further restrict the - // nonce reuse concern to messages of identical sizes and identical domain. + // nonce reuse concern so that you would not only have to hash collide but also have it happen + // on messages of identical sizes and identical domain. std::array key{0}; crypto_generichash_blake2b_state state; crypto_generichash_blake2b_init(&state, nullptr, 0, key.size()); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp new file mode 100644 index 00000000..6f90b83c --- /dev/null +++ b/src/config/groups/info.cpp @@ -0,0 +1,107 @@ +#include "session/config/groups/info.hpp" + +#include +#include + +#include + +#include "../internal.hpp" +//#include "session/config/groups/info.h" +#include "session/config/error.h" +#include "session/export.h" +#include "session/types.hpp" +#include "session/util.hpp" + +namespace session::config::groups { + +using namespace std::literals; +using session::ustring_view; + +Info::Info( + const std::vector& keys, + ustring_view ed25519_pubkey, + std::optional ed25519_secretkey, + std::optional dumped) : + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} { + for (const auto& k : keys) + add_key(k); +} + +std::optional Info::get_name() const { + if (auto* s = data["n"].string(); s && !s->empty()) + return *s; + return std::nullopt; +} + +void Info::set_name(std::string_view new_name) { + set_nonempty_str(data["n"], new_name); +} + +profile_pic Info::get_profile_pic() const { + profile_pic pic{}; + if (auto* url = data["p"].string(); url && !url->empty()) + pic.url = *url; + if (auto* key = data["q"].string(); key && key->size() == 32) + pic.key = {reinterpret_cast(key->data()), 32}; + return pic; +} + +void Info::set_profile_pic(std::string_view url, ustring_view key) { + set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); +} + +void Info::set_profile_pic(profile_pic pic) { + set_profile_pic(pic.url, pic.key); +} + +std::optional Info::get_expiry_timer() const { + if (auto exp = data["E"].integer()) + return *exp * 1s; + return std::nullopt; +} + +void Info::set_expiry_timer(std::chrono::seconds expiration_timer) { + set_positive_int(data["E"], expiration_timer.count()); +} + +void Info::set_created(int64_t timestamp) { + set_positive_int(data["c"], timestamp); +} + +std::optional Info::get_created() const { + if (auto* ts = data["c"].integer()) + return *ts; + return std::nullopt; +} + +void Info::set_delete_before(int64_t timestamp) { + set_positive_int(data["d"], timestamp); +} + +std::optional Info::get_delete_before() const { + if (auto* ts = data["d"].integer()) + return *ts; + return std::nullopt; +} + +void Info::set_delete_attach_before(int64_t timestamp) { + set_positive_int(data["D"], timestamp); +} + +std::optional Info::get_delete_attach_before() const { + if (auto* ts = data["D"].integer()) + return *ts; + return std::nullopt; +} + +void Info::destroy_group() { + set_flag(data["!"], true); +} + +bool Info::is_destroyed() const { + if (auto* ts = data["!"].integer(); ts && *ts > 0) + return true; + return false; +} + +} // namespace session::config::groups diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dcc4dfa8..a7cdefb3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(testAll test_config_contacts.cpp test_config_convo_info_volatile.cpp test_encrypt.cpp + test_group_info.cpp test_xed25519.cpp ) diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp new file mode 100644 index 00000000..db2d7ed8 --- /dev/null +++ b/tests/test_group_info.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +static constexpr int64_t created_ts = 1680064059; + +using namespace session::config; + +TEST_CASE("Verify-only Group Info", "[config][verify-only]") { + + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair( + ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); + CHECK(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); + + std::vector enc_keys; + enc_keys.push_back("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); + // This Info object has only the public key, not the priv key, and so cannot modify things: + groups::Info ginfo{view_vec(enc_keys), to_usv(ed_pk), std::nullopt, std::nullopt}; + + REQUIRE_THROWS_WITH( + ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); + REQUIRE_THROWS_WITH( + ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); + CHECK(!ginfo.is_dirty()); + + // This one is good and has the right signature: + groups::Info ginfo_rw{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + ginfo_rw.set_name("Super Group!!"); + CHECK(ginfo_rw.is_dirty()); + CHECK(ginfo_rw.needs_push()); + CHECK(ginfo_rw.needs_dump()); + + auto [seqno, to_push, obs] = ginfo_rw.push(); + + CHECK(seqno == 1); + + ginfo_rw.confirm_pushed(seqno, "fakehash1"); + CHECK(ginfo_rw.needs_dump()); + CHECK_FALSE(ginfo_rw.needs_push()); + + std::vector> merge_configs; + merge_configs.emplace_back("fakehash1", to_push); + CHECK(ginfo.merge(merge_configs) == 1); + CHECK_FALSE(ginfo.needs_push()); + + groups::Info ginfo_rw2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + CHECK(ginfo_rw2.merge(merge_configs) == 1); + CHECK_FALSE(ginfo.needs_push()); + + CHECK(ginfo.get_name() == "Super Group!!"); + + REQUIRE_THROWS_WITH( + ginfo.set_name("Super Group11"), "Unable to make changes to a read-only config object"); + // This shouldn't throw because it isn't *actually* changing a config value (i.e. re-setting the + // same value does not dirty the config). It isn't clear why you'd need to do this, but still. + ginfo.set_name("Super Group!!"); + + // Deliberately use the wrong signing key so that what we produce encrypts successfully but + // doesn't verify + const auto seed_bad1 = + "0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + std::array ed_pk_bad1; + std::array ed_sk_bad1; + crypto_sign_ed25519_seed_keypair( + ed_pk_bad1.data(), + ed_sk_bad1.data(), + reinterpret_cast(seed_bad1.data())); + + groups::Info ginfo_bad1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + ginfo_bad1.merge(merge_configs); + ginfo_bad1.set_sig_keys(to_usv(ed_sk_bad1)); + ginfo_bad1.set_name("Bad name, BAD!"); + auto [s_bad, p_bad, o_bad] = ginfo_bad1.push(); + + merge_configs.clear(); + merge_configs.emplace_back("badhash1", p_bad); + + CHECK(ginfo.merge(merge_configs) == 0); + CHECK_FALSE(ginfo.needs_push()); + + // Now let's get more complicated: we will have *two* valid signers who submit competing updates + ginfo_rw2.set_name("Super Group 2"); + ginfo_rw2.set_created(12345); + ginfo_rw.set_name("Super Group 3"); + ginfo_rw.set_expiry_timer(365 * 24h); + + CHECK(ginfo_rw.needs_push()); + CHECK(ginfo_rw2.needs_push()); + + auto [s2, tp2, o2] = ginfo_rw2.push(); + auto [s3, tp3, o3] = ginfo_rw.push(); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash2", tp2); + merge_configs.emplace_back("fakehash3", tp3); + + CHECK(ginfo.merge(merge_configs) == 2); + CHECK(ginfo.is_clean()); + + CHECK(s2 == 2); + CHECK(s3 == 2); + CHECK_FALSE(ginfo.needs_push()); + + CHECK(ginfo_rw.merge(merge_configs) == 2); + CHECK(ginfo_rw2.merge(merge_configs) == 2); + + CHECK(ginfo_rw.needs_push()); + CHECK(ginfo_rw2.needs_push()); + + auto [s23, t23, o23] = ginfo_rw.push(); + auto [s32, t32, o32] = ginfo_rw2.push(); + + CHECK(s23 == s32); + CHECK(t23 == t32); + CHECK(o23 == o32); + + ginfo_rw.confirm_pushed(s23, "fakehash23"); + ginfo_rw2.confirm_pushed(s32, "fakehash23"); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash23", t23); + + CHECK(ginfo.merge(merge_configs) == 1); + CHECK(ginfo_rw.merge(merge_configs) == 1); + CHECK(ginfo_rw2.merge(merge_configs) == 1); + + CHECK_FALSE(ginfo.needs_push()); + CHECK_FALSE(ginfo_rw.needs_push()); + CHECK_FALSE(ginfo_rw2.needs_push()); + + auto test = [](groups::Info& g) { + auto n = g.get_name(); + REQUIRE(n); + CHECK(*n == "Super Group 2"); + auto c = g.get_created(); + REQUIRE(c); + CHECK(*c == 12345); + auto et = g.get_expiry_timer(); + REQUIRE(et); + CHECK(*et == 365 * 24h); + }; + SECTION("read-only group info") { + test(ginfo); + } + SECTION("group writer 1") { + test(ginfo_rw); + } + SECTION("group writer 2") { + test(ginfo_rw2); + } + + + CHECK(ginfo.needs_dump()); + auto dump = ginfo.dump(); + groups::Info ginfo2{view_vec(enc_keys), to_usv(ed_pk), std::nullopt, dump}; + + CHECK(!ginfo.needs_dump()); + CHECK(!ginfo2.needs_dump()); + + auto [s4, t4, o4] = ginfo.push(); + auto [s5, t5, o5] = ginfo.push(); + CHECK(s4 == s23); + CHECK(s4 == s5); + CHECK(t4 == t23); + CHECK(t4 == t5); + CHECK(o4.empty()); + CHECK(o5.empty()); +} diff --git a/tests/utils.hpp b/tests/utils.hpp index aff40513..3a73a139 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -38,6 +39,10 @@ inline std::string_view to_sv(ustring_view x) { inline ustring_view to_usv(std::string_view x) { return {reinterpret_cast(x.data()), x.size()}; } +template +ustring_view to_usv(const std::array& data) { + return {data.data(), N}; +} inline std::string printable(ustring_view x) { std::string p; @@ -74,3 +79,13 @@ template std::set> make_set(T&&... args) { return {std::forward(args)...}; } + +template +std::vector> view_vec(std::vector>&& v) = delete; +template +std::vector> view_vec(const std::vector>& v) { + std::vector> vv; + vv.reserve(v.size()); + std::copy(v.begin(), v.end(), std::back_inserter(vv)); + return vv; +} From 80f4d1428456cceefee71532c72ab87b8bc89ba3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 3 Aug 2023 12:47:38 -0300 Subject: [PATCH 002/572] Add multi-key encryption tests Fixes a bug where construction-provided keys were loaded in reversed priority. --- src/config/groups/info.cpp | 2 +- tests/test_group_info.cpp | 71 +++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 6f90b83c..484a2e73 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -24,7 +24,7 @@ Info::Info( std::optional dumped) : ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} { for (const auto& k : keys) - add_key(k); + add_key(k, false); } std::optional Info::get_name() const { diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index db2d7ed8..2b532c17 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -30,10 +30,17 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector enc_keys; - enc_keys.push_back("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); + std::vector enc_keys1; + enc_keys1.push_back( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); + std::vector enc_keys2; + enc_keys2.push_back( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + enc_keys2.push_back( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); + // This Info object has only the public key, not the priv key, and so cannot modify things: - groups::Info ginfo{view_vec(enc_keys), to_usv(ed_pk), std::nullopt, std::nullopt}; + groups::Info ginfo{view_vec(enc_keys1), to_usv(ed_pk), std::nullopt, std::nullopt}; REQUIRE_THROWS_WITH( ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); @@ -42,7 +49,7 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: - groups::Info ginfo_rw{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; ginfo_rw.set_name("Super Group!!"); CHECK(ginfo_rw.is_dirty()); @@ -62,7 +69,7 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { CHECK(ginfo.merge(merge_configs) == 1); CHECK_FALSE(ginfo.needs_push()); - groups::Info ginfo_rw2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw2{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; CHECK(ginfo_rw2.merge(merge_configs) == 1); CHECK_FALSE(ginfo.needs_push()); @@ -85,7 +92,7 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { ed_sk_bad1.data(), reinterpret_cast(seed_bad1.data())); - groups::Info ginfo_bad1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_bad1{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; ginfo_bad1.merge(merge_configs); ginfo_bad1.set_sig_keys(to_usv(ed_sk_bad1)); ginfo_bad1.set_name("Bad name, BAD!"); @@ -158,20 +165,13 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { REQUIRE(et); CHECK(*et == 365 * 24h); }; - SECTION("read-only group info") { - test(ginfo); - } - SECTION("group writer 1") { - test(ginfo_rw); - } - SECTION("group writer 2") { - test(ginfo_rw2); - } - + test(ginfo); + test(ginfo_rw); + test(ginfo_rw2); CHECK(ginfo.needs_dump()); auto dump = ginfo.dump(); - groups::Info ginfo2{view_vec(enc_keys), to_usv(ed_pk), std::nullopt, dump}; + groups::Info ginfo2{view_vec(enc_keys1), to_usv(ed_pk), std::nullopt, dump}; CHECK(!ginfo.needs_dump()); CHECK(!ginfo2.needs_dump()); @@ -184,4 +184,41 @@ TEST_CASE("Verify-only Group Info", "[config][verify-only]") { CHECK(t4 == t5); CHECK(o4.empty()); CHECK(o5.empty()); + + // This account has a different primary decryption key + groups::Info ginfo_rw3{view_vec(enc_keys2), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + CHECK(ginfo_rw3.merge(merge_configs) == 1); + CHECK(ginfo_rw3.get_name() == "Super Group 2"); + + auto [s6, t6, o6] = ginfo_rw3.push(); + CHECK(to_hex(ginfo_rw3.key(0)) == + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + REQUIRE(ginfo_rw3.key_count() == 2); + CHECK(to_hex(ginfo_rw3.key(1)) == + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + CHECK(s6 == s5); + CHECK(t6.size() == t23.size()); + CHECK(t6 != t23); + + ginfo_rw3.set_profile_pic( + "http://example.com/12345", + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(ginfo_rw3.needs_push()); + auto [s7, t7, o7] = ginfo_rw3.push(); + CHECK(s7 == s6 + 1); + CHECK(t7 != t6); + CHECK(o7 == std::vector{{"fakehash23"s}}); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash7", t7); + // If we don't have the new "bbb" key loaded yet, this will fail: + CHECK(ginfo.merge(merge_configs) == 0); + + ginfo.add_key(enc_keys2.front()); + CHECK(ginfo.merge(merge_configs) == 1); + + auto pic = ginfo.get_profile_pic(); + CHECK_FALSE(pic.empty()); + CHECK(pic.url == "http://example.com/12345"); + CHECK(pic.key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); } From 83c9dec70f8382a5bc780e74e11c172e415a4f30 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 4 Aug 2023 14:42:49 -0300 Subject: [PATCH 003/572] Remove duplicate implementation --- src/config/contacts.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 6e973504..85b011f1 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -30,22 +30,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)); -namespace { - -void check_session_id(std::string_view session_id) { - if (session_id.size() != 66 || !oxenc::is_hex(session_id)) - throw std::invalid_argument{ - "Invalid pubkey: expected 66 hex digits, got " + std::to_string(session_id.size()) + - " and/or not hex"}; -} - -std::string session_id_to_bytes(std::string_view session_id) { - check_session_id(session_id); - return oxenc::from_hex(session_id); -} - -} // namespace - 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); } From 12d601b9db3aaf433569f3839bac1daae5a1fe43 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 4 Aug 2023 14:43:10 -0300 Subject: [PATCH 004/572] Add group info fields test --- tests/test_group_info.cpp | 103 +++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 2b532c17..c85315ea 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -17,7 +17,108 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -TEST_CASE("Verify-only Group Info", "[config][verify-only]") { +TEST_CASE("Group Info settings", "[config][groups]") { + + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair( + ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); + CHECK(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); + + std::vector enc_keys{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; + + groups::Info ginfo1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + enc_keys.insert(enc_keys.begin(), "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); + enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); + groups::Info ginfo2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + ginfo1.set_name("GROUP Name"); + CHECK(ginfo1.is_dirty()); + CHECK(ginfo1.needs_push()); + CHECK(ginfo1.needs_dump()); + + auto [s1, p1, o1] = ginfo1.push(); + + CHECK(s1 == 1); + CHECK(p1.size() == 256); + CHECK(o1.empty()); + + ginfo1.confirm_pushed(s1, "fakehash1"); + CHECK(ginfo1.needs_dump()); + CHECK_FALSE(ginfo1.needs_push()); + + std::vector> merge_configs; + merge_configs.emplace_back("fakehash1", p1); + CHECK(ginfo2.merge(merge_configs) == 1); + CHECK_FALSE(ginfo2.needs_push()); + + CHECK(ginfo2.get_name() == "GROUP Name"); + + ginfo2.set_profile_pic( + "http://example.com/12345", + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + ginfo2.set_expiry_timer(1h); + constexpr int64_t create_time{1682529839}; + ginfo2.set_created(create_time); + ginfo2.set_delete_before(create_time + 50*86400); + ginfo2.set_delete_attach_before(create_time + 70*86400); + ginfo2.destroy_group(); + + auto [s2, p2, o2] = ginfo2.push(); + CHECK(s2 == 2); + CHECK(p2.size() == 512); + CHECK(o2 == std::vector{"fakehash1"s}); + + ginfo2.confirm_pushed(s2, "fakehash2"); + + ginfo1.set_name("Better name!"); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash2", p2); + + // This fails because ginfo1 doesn't yet have the new key that ginfo2 used (bbb...) + CHECK(ginfo1.merge(merge_configs) == 0); + + ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + ginfo1.add_key("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes, /*prepend=*/ false); + + CHECK(ginfo1.merge(merge_configs) == 1); + + CHECK(ginfo1.needs_push()); + auto [s3, p3, o3] = ginfo1.push(); + + CHECK(ginfo1.get_name() == "Better name!"); + CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); + CHECK(ginfo1.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(ginfo1.get_expiry_timer() == 1h); + CHECK(ginfo1.get_created() == create_time); + CHECK(ginfo1.get_delete_before() == create_time + 50*86400); + CHECK(ginfo1.get_delete_attach_before() == create_time + 70*86400); + CHECK(ginfo1.is_destroyed()); + + ginfo1.confirm_pushed(s3, "fakehash3"); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash3", p3); + CHECK(ginfo2.merge(merge_configs) == 1); + CHECK(ginfo2.get_name() == "Better name!"); + CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345"); + CHECK(ginfo2.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(ginfo2.get_expiry_timer() == 1h); + CHECK(ginfo2.get_created() == create_time); + CHECK(ginfo2.get_delete_before() == create_time + 50*86400); + CHECK(ginfo2.get_delete_attach_before() == create_time + 70*86400); + CHECK(ginfo2.is_destroyed()); +} + +TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; std::array ed_pk; From 6f1964611bd0b6a6bad7518f87cffa4f48794575 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 4 Aug 2023 16:43:48 -0300 Subject: [PATCH 005/572] Add group members config --- include/session/config/groups/members.hpp | 356 ++++++++++++++++++++++ include/session/config/namespaces.hpp | 1 + src/CMakeLists.txt | 1 + src/config/contacts.cpp | 3 + src/config/groups/members.cpp | 129 ++++++++ tests/CMakeLists.txt | 1 + tests/test_group_info.cpp | 2 +- tests/test_group_members.cpp | 240 +++++++++++++++ 8 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 include/session/config/groups/members.hpp create mode 100644 src/config/groups/members.cpp create mode 100644 tests/test_group_members.cpp diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp new file mode 100644 index 00000000..92f6f9d8 --- /dev/null +++ b/include/session/config/groups/members.hpp @@ -0,0 +1,356 @@ +#pragma once + +#include +#include +#include + +#include "../base.hpp" +#include "../namespaces.hpp" +#include "../profile_pic.hpp" + +namespace session::config::groups { + +using namespace std::literals; + +/// keys used in this config, either currently or in the past (so that we don't reuse): +/// +/// m - dict of members; each key is the member session id (33 bytes), each value is a dict +/// containing subkeys: +/// n - member name; this will always be set in the encoded message to prevent dict pruning, but +/// will be an empty string if there is no name. +/// p - member profile pic url +/// q - member profile pic decryption key (binary) +/// I - invite status; this will be one of: +/// - 1 if the invite has been issued but not yet accepted. +/// - 2 if an invite was created but failed to send for some reason (and thus can be resent) +/// - omitted once an invite is accepted. (This also gets omitted if the `A` admin flag gets +/// set). +/// A - flag set to 1 if the member is an admin, omitted otherwise. +/// P - promotion (to admin) status; this will be one of: +/// - 1 if a promotion has been sent. +/// - 2 if a promotion was created but failed to send for some reason (and thus should be +/// resent) +/// - omitted once the promotion is accepted (i.e. once `A` gets set). + +constexpr int INVITE_SENT = 1, INVITE_FAILED = 2; + +/// Struct containing member details +struct member { + static constexpr size_t MAX_NAME_LENGTH = 100; + + explicit member(std::string sid); + + // Internal ctor/method for C API implementations: + member(const struct config_group_member& c); // From c struct + + /// API: groups/member::session_id + /// + /// The member's session ID, in hex. + std::string session_id; + + /// API: groups/member::name + /// + /// The member's human-readable name. Optional. This is used by other members of the group to + /// display a member's details before having seen a message from that member. + std::string name; + + /// API: groups/member::profile_picture + /// + /// The member's profile picture (URL & decryption key). Optional. This is used by other + /// members of the group to display a member's details before having seen a message from that + /// member. + profile_pic profile_picture; + + /// API: groups/member::admin + /// + /// Flag that is set to indicate to the group that this member is an admin. + /// + /// Note that this is only informative but isn't a permission gate: someone could still possess + /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could + /// have lost (or never had) the keys even if this is set. + /// + /// See also `promoted()` if you want to check for either an admin or someone being promoted to + /// admin. + bool admin = 0; + + // Flags to track an invited user. This value is typically not used directly, but rather via + // the `set_invited()`, `invite_pending()` and similar methods. + int invite_status = 0; + + /// API: groups/member::set_invited + /// + /// Sets the "invited" flag for this user. This marks the user as having a pending invitation + /// to the group. The optional `failed` parameter can be specified as true if the invitation + /// was issued but failed to send for some reason (this is intended as a signal to other clients + /// that the invitation should be reissued). + void set_invited(bool failed = false) { invite_status = failed ? INVITE_FAILED : INVITE_SENT; } + + /// API: groups/members::set_accepted + /// + /// This clears the "invited" flag for this user, thus indicating that the user has accepted an + /// invitation and is now a regular member of the group. + void set_accepted() { invite_status = 0; } + + /// API: groups/member::invite_pending + /// + /// Returns whether the user currently has a pending invitation. Returns true if so (whether or + /// not that invitation has failed). + /// + /// Outputs: + /// - `bool` -- true if the user has a pending invitation, false otherwise. + bool invite_pending() const { return invite_status > 0; } + + /// API: groups/member::invite_failed + /// + /// Returns true if the user has a pending invitation that is marked as failed (and thus should + /// be re-sent). + /// + /// Outputs: + /// - `bool` -- true if the user has a failed pending invitation + bool invite_failed() const { return invite_status == INVITE_FAILED; } + + // Flags to track a promoted-to-admin user. This value is typically not used directly, but + // rather via the `set_promoted()`, `promotion_pending()` and similar methods. + int promotion_status = 0; + + /// API: groups/member::set_promoted + /// + /// Sets the "promoted" flag for this user. This marks the user as having a pending + /// promotion-to-admin in the group. The optional `failed` parameter can be specified as true + /// if the promotion was issued but failed to send for some reason (this is intended as a signal + /// to other clients that the promotion should be reissued). + /// + /// Note that this flag is ignored when the `admin` field is set to true. + void set_promoted(bool failed = false) { + promotion_status = failed ? INVITE_FAILED : INVITE_SENT; + } + + /// API: groups/member::promotion_pending + /// + /// Returns whether the user currently has a pending invitation/promotion to admin status. + /// Returns true if so (whether or not that invitation has failed). + /// + /// Outputs: + /// - `bool` -- true if the user has a pending promotion, false otherwise. + bool promotion_pending() const { return !admin && promotion_status > 0; } + + /// API: groups/member::promotion_failed + /// + /// Returns true if the user has a pending promotion-to-admin that is marked as failed (and thus + /// should be re-sent). + /// + /// Outputs: + /// - `bool` -- true if the user has a failed pending promotion + bool promotion_failed() const { return !admin && promotion_status == INVITE_FAILED; } + + /// API: groups/member::promoted + /// + /// Returns true if the user is already an admin *or* has a pending promotion to admin. + bool promoted() const { return admin || promotion_pending(); } + + /// API: groups/member::info + /// + /// converts the member info into a c struct + /// + /// Inputs: + /// - `c` -- Return Parameter that will be filled with data in contact_info + void into(config_group_member& c) const; + + /// API: groups/member::set_name + /// + /// Sets a name; this is exactly the same as assigning to .name directly, except that we throw + /// an exception if the given name is longer than MAX_NAME_LENGTH. + /// + /// Note that you can set a longer name directly into the `.name` member, but it will be + /// truncated when serializing the record. + /// + /// Inputs: + /// - `name` -- Name to assign to the contact + void set_name(std::string name); + + private: + friend class Members; + void load(const dict& info_dict); +}; + +class Members final : public ConfigBase { + + public: + // No default constructor + Members() = delete; + + /// API: groups/Members::Members + /// + /// Constructs a group members config object from existing data (stored from `dump()`) and a + /// list of encryption keys for encrypting new and decrypting existing messages. + /// + /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass + /// `std::nullopt` as the second argument. + /// + /// Inputs: + /// - `keys` -- contains the possible 32-byte en/decryption keys that may be used for incoming + /// messages. These are *not* Ed25519 secret keys, but rather symmetric encryption keys used + /// for encryption (generally generated using a cryptographically secure random generator). + /// The *first* key in this list will be used to encrypt outgoing config messages (and so, in + /// general, should be the most current key). There must always be at least one key present + /// (either provided at construction or via add_keys) before you can push a config. + /// Post-construction you can add or remove keys via add_key/remove_key/clear_keys from + /// ConfigBase. + /// - `ed25519_pubkey` is the public key of this group, used to validate config messages. + /// Config messages not signed with this key will be rejected. + /// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages. + /// This is only possessed by the group admin(s), and must be provided in order to make and + /// push config changes. + /// - `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()`. + Members(const std::vector& keys, + ustring_view ed25519_pubkey, + std::optional ed25519_secretkey, + std::optional dumped); + + /// API: groups/Members::storage_namespace + /// + /// Returns the Members namespace. Is constant, will always return Namespace::GroupMembers + /// + /// Inputs: None + /// + /// Outputs: + /// - `Namespace` - Will return Namespace::GroupMembers + Namespace storage_namespace() const override { return Namespace::GroupMembers; } + + /// API: groups/Members::encryption_domain + /// + /// Returns the encryption domain used when encrypting messages of this type. + /// + /// Inputs: None + /// + /// Outputs: + /// - `const char*` - Will return "groups::Members" + const char* encryption_domain() const override { return "groups::Members"; } + + /// API: groups/Members::get + /// + /// Looks up and returns a member by hex session ID. Returns nullopt if the session ID was + /// not found, otherwise returns a filled out `member`. + /// + /// Inputs: + /// - `pubkey_hex` -- hex string of the session id + /// + /// Outputs: + /// - `std::optional` - Returns nullopt if session ID was not found, otherwise a + /// filled out `member` struct. + std::optional get(std::string_view pubkey_hex) const; + + /// API: groups/Members::get_or_construct + /// + /// Similar to get(), but if the session ID does not exist this returns a filled-out member + /// containing the session_id (all other fields will be empty/defaulted). This is intended to + /// be combined with `set` to set-or-create a record. + /// + /// NB: calling this does *not* add the session id to the member list when called: that requires + /// also calling `set` with this value. + /// + /// Inputs: + /// - `pubkey_hex` -- hex string of the session id + /// + /// Outputs: + /// - `member` - Returns a filled out member struct + member get_or_construct(std::string_view pubkey_hex) const; + + /// API: groups/Members::set + /// + /// Sets or updates the various values associated with a member with the given info. The usual + /// use is to access the current info, change anything desired, then pass it back into set, + /// e.g.: + /// + /// ```cpp + /// auto m = members.get_or_construct(pubkey); + /// c.name = "Session User 42"; + /// members.set(c); + /// ``` + /// + /// Inputs: + /// - `member` -- member value to set + void set(const member& member); + + /// API: groups/Members::erase + /// + /// Removes a session ID from the member list, if present. + /// + /// Inputs: + /// - `session_id` the hex session ID of the member to remove + /// + /// Outputs: + /// - true if the member was found (and removed); false if the member was not in the list. + bool erase(std::string_view session_id); + + struct iterator; + /// API: groups/Members::begin + /// + /// Iterators for iterating through all members. Typically you access this implicit via a for + /// loop over the `Members` object: + /// + ///```cpp + /// for (auto& member : members) { + /// // use member.session_id, member.name, etc. + /// } + ///``` + /// + /// This iterates in sorted order through the session_ids. + /// + /// It is NOT permitted to add/modify/remove records while iterating; instead such modifications + /// require two passes: an iterator loop to collect the required modifications, then a second + /// pass to apply the modifications. + /// + /// Inputs: None + /// + /// Outputs: + /// - `iterator` - Returns an iterator for the beginning of the members + iterator begin() const { return iterator{data["m"].dict()}; } + + /// API: groups/Members::end + /// + /// Iterator for passing the end of the members + /// + /// Inputs: None + /// + /// Outputs: + /// - `iterator` - Returns an iterator for the end of the members + iterator end() const { return iterator{nullptr}; } + + using iterator_category = std::input_iterator_tag; + using value_type = member; + using reference = value_type&; + using pointer = value_type*; + using difference_type = std::ptrdiff_t; + + struct iterator { + private: + std::shared_ptr _val; + dict::const_iterator _it; + const dict* _members; + void _load_info(); + iterator(const dict* members) : _members{members} { + if (_members) { + _it = _members->begin(); + _load_info(); + } + } + friend class Members; + + public: + bool operator==(const iterator& other) const; + bool operator!=(const iterator& other) const { return !(*this == other); } + bool done() const; // Equivalent to comparing against the end iterator + member& operator*() const { return *_val; } + member* operator->() const { return _val.get(); } + iterator& operator++(); + iterator operator++(int) { + auto copy{*this}; + ++*this; + return copy; + } + }; +}; + +} // namespace session::config::groups diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index c80957f9..7b1db95a 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -12,6 +12,7 @@ enum class Namespace : std::int16_t { // Groups namespaces (i.e. for config of the group itself, not one user's group settings) GroupInfo = 11, + GroupMembers = 12, }; } // namespace session::config diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1b5dcdeb..1e96f27d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(config config/encrypt.cpp config/error.c config/groups/info.cpp + config/groups/members.cpp config/internal.cpp config/user_groups.cpp config/user_profile.cpp diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 85b011f1..2b9e5875 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -356,6 +356,9 @@ bool Contacts::iterator::operator==(const iterator& other) const { if (!other._contacts) // other is an "end" tombstone: return whether we are at the end return _it == _contacts->end(); + if (!_contacts) + // we are an "end" tombstone: return whether the other one is at the end + return other._it == other._contacts->end(); return _it == other._it; } diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp new file mode 100644 index 00000000..2c37be88 --- /dev/null +++ b/src/config/groups/members.cpp @@ -0,0 +1,129 @@ +#include "session/config/groups/members.hpp" +#include + +#include "../internal.hpp" + +namespace session::config::groups { + +Members::Members( + const std::vector& keys, + ustring_view ed25519_pubkey, + std::optional ed25519_secretkey, + std::optional dumped) : + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} { + for (const auto& k : keys) + add_key(k, false); +} + +std::optional Members::get(std::string_view pubkey_hex) const { + std::string pubkey = session_id_to_bytes(pubkey_hex); + + auto* info_dict = data["m"][pubkey].dict(); + if (!info_dict) + return std::nullopt; + + auto result = std::make_optional(std::string{pubkey_hex}); + result->load(*info_dict); + return result; +} + +member Members::get_or_construct(std::string_view pubkey_hex) const { + if (auto maybe = get(pubkey_hex)) + return *std::move(maybe); + + return member{std::string{pubkey_hex}}; +} + +void Members::set(const member& mem) { + + std::string pk = session_id_to_bytes(mem.session_id); + auto info = data["m"][pk]; + + // Always set the name, even if empty, to keep the dict from getting pruned if there are no + // other entries. + info["n"] = mem.name.substr(0, member::MAX_NAME_LENGTH); + + set_pair_if( + mem.profile_picture, + info["p"], + mem.profile_picture.url, + info["q"], + mem.profile_picture.key); + + set_flag(info["A"], mem.admin); + set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); + set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); +} + +void member::load(const dict& info_dict) { + name = maybe_string(info_dict, "n").value_or(""); + + auto url = maybe_string(info_dict, "p"); + auto key = maybe_ustring(info_dict, "q"); + if (url && key && !url->empty() && key->size() == 32) { + profile_picture.url = std::move(*url); + profile_picture.key = std::move(*key); + } else { + profile_picture.clear(); + } + + admin = maybe_int(info_dict, "A").value_or(0); + invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); + promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); +} + +/// 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 Members::iterator::_load_info() { + while (_it != _members->end()) { + if (_it->first.size() == 33) { + if (auto* info_dict = std::get_if(&_it->second)) { + _val = std::make_shared(oxenc::to_hex(_it->first)); + auto hex = oxenc::to_hex(_it->first); + _val->load(*info_dict); + return; + } + } + + // We found something we don't understand (wrong pubkey size, or not a dict value) so skip + // it. + ++_it; + } +} + +bool Members::iterator::operator==(const iterator& other) const { + if (!_members && !other._members) + return true; // Both are end tombstones + if (!other._members) + // other is an "end" tombstone: return whether we are at the end + return _it == _members->end(); + if (!_members) + // we are an "end" tombstone: return whether the other one is at the end + return other._it == other._members->end(); + return _it == other._it; +} + +bool Members::iterator::done() const { + return !_members || _it == _members->end(); +} + +Members::iterator& Members::iterator::operator++() { + ++_it; + _load_info(); + return *this; +} + +bool Members::erase(std::string_view session_id) { + std::string pk = session_id_to_bytes(session_id); + auto info = data["m"][pk]; + bool ret = info.exists(); + info.erase(); + return ret; +} + + +member::member(std::string sid) : session_id{std::move(sid)} { + check_session_id(session_id); +} + +} // namespace session::config::groups diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7cdefb3..1cba0814 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(testAll test_config_convo_info_volatile.cpp test_encrypt.cpp test_group_info.cpp + test_group_members.cpp test_xed25519.cpp ) diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index c85315ea..798aa2e7 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -17,7 +17,7 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -TEST_CASE("Group Info settings", "[config][groups]") { +TEST_CASE("Group Info settings", "[config][groups][info]") { const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; std::array ed_pk; diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp new file mode 100644 index 00000000..974afb94 --- /dev/null +++ b/tests/test_group_members.cpp @@ -0,0 +1,240 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +static constexpr int64_t created_ts = 1680064059; + +using namespace session::config; + +constexpr bool is_prime100(int i) { + constexpr std::array p100 = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}; + for (auto p : p100) + if (p >= i) + return p == i; + return false; +} + +TEST_CASE("Group Members", "[config][groups][members]") { + + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair( + ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); + CHECK(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); + + std::vector enc_keys{ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; + + groups::Members gmem1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + enc_keys.insert( + enc_keys.begin(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); + enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); + groups::Members gmem2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + std::vector sids; + while (sids.size() < 256) { + std::array sid; + for (auto& s : sid) + s = sids.size(); + sid[0] = 0x05; + sids.push_back(oxenc::to_hex(sid.begin(), sid.end())); + } + + // 10 admins: + for (int i = 0; i < 10; i++) { + auto m = gmem1.get_or_construct(sids[i]); + m.admin = true; + m.name = "Admin " + std::to_string(i); + m.profile_picture.url = "http://example.com/" + std::to_string(i); + m.profile_picture.key = + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + gmem1.set(m); + } + // 10 members: + for (int i = 10; i < 20; i++) { + auto m = gmem1.get_or_construct(sids[i]); + m.name = "Member " + std::to_string(i); + m.profile_picture.url = "http://example.com/" + std::to_string(i); + m.profile_picture.key = + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + gmem1.set(m); + } + // 5 members with no attributes (not even a name): + for (int i = 20; i < 25; i++) { + auto m = gmem1.get_or_construct(sids[i]); + gmem1.set(m); + } + + CHECK(gmem1.needs_push()); + auto [s1, p1, o1] = gmem1.push(); + CHECK(p1.size() == 768); + + gmem1.confirm_pushed(s1, "fakehash1"); + CHECK(gmem1.needs_dump()); + CHECK_FALSE(gmem1.needs_push()); + + std::vector> merge_configs; + merge_configs.emplace_back("fakehash1", p1); + CHECK(gmem2.merge(merge_configs) == 1); + CHECK_FALSE(gmem2.needs_push()); + + for (int i = 0; i < 25; i++) + CHECK(gmem2.get(sids[i]).has_value()); + + { + int i = 0; + for (auto& m : gmem2) { + CHECK(m.session_id == sids[i]); + CHECK_FALSE(m.invite_pending()); + CHECK_FALSE(m.invite_failed()); + CHECK_FALSE(m.promotion_pending()); + CHECK_FALSE(m.promotion_failed()); + if (i < 10) { + CHECK(m.admin); + CHECK(m.name == "Admin " + std::to_string(i)); + CHECK_FALSE(m.profile_picture.empty()); + CHECK(m.promoted()); + } else { + CHECK_FALSE(m.admin); + CHECK_FALSE(m.promoted()); + if (i < 20) { + CHECK(m.name == "Member " + std::to_string(i)); + CHECK_FALSE(m.profile_picture.empty()); + } else { + CHECK(m.name.empty()); + CHECK(m.profile_picture.empty()); + } + } + i++; + } + CHECK(i == 25); + } + + for (int i = 22; i < 50; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.name = "Member " + std::to_string(i); + gmem2.set(m); + } + for (int i = 50; i < 55; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.set_invited(); + gmem2.set(m); + } + for (int i = 55; i < 58; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.set_invited(true); + gmem2.set(m); + } + for (int i = 58; i < 62; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.set_promoted(i >= 60); + gmem2.set(m); + } + + CHECK(gmem2.get(sids[23]).value().name == "Member 23"); + + auto [s2, p2, o2] = gmem2.push(); + gmem2.confirm_pushed(s2, "fakehash2"); + merge_configs.emplace_back("fakehash2", p2); // not clearing it first! + CHECK(gmem1.merge(merge_configs) == 1); + gmem1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + CHECK(gmem1.merge(merge_configs) == 2); + + CHECK(gmem1.get(sids[23]).value().name == "Member 23"); + + { + int i = 0; + for (auto& m : gmem1) { + CHECK(m.session_id == sids[i]); + CHECK(m.admin == i < 10); + CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" + : i < 10 ? "Admin " + std::to_string(i) + : "Member " + std::to_string(i))); + CHECK(m.profile_picture.key == + (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes + : ""_hexbytes)); + CHECK(m.profile_picture.url == + (i < 20 ? "http://example.com/" + std::to_string(i) : "")); + CHECK(m.invite_pending() == (50 <= i && i < 58)); + CHECK(m.invite_failed() == (55 <= i && i < 58)); + CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); + CHECK(m.promotion_pending() == (i >= 58 && i < 62)); + CHECK(m.promotion_failed() == (i >= 60 && i < 62)); + i++; + } + CHECK(i == 62); + } + + for (int i = 0; i < 100; i++) { + if (is_prime100(i)) + gmem1.erase(sids[i]); + else if (i >= 50 && i <= 56) { + auto m = gmem1.get(sids[i]).value(); + if (i >= 55) + m.set_invited(); + else + m.set_accepted(); + gmem1.set(m); + } else if (i == 58) { + auto m = gmem1.get(sids[i]).value(); + m.admin = true; + gmem1.set(m); + } else if (i == 59) { + auto m = gmem1.get(sids[i]).value(); + m.set_promoted(); + gmem1.set(m); + } + } + + auto [s3, p3, o3] = gmem1.push(); + gmem1.confirm_pushed(s3, "fakehash3"); + merge_configs.clear(); + merge_configs.emplace_back("fakehash3", p3); + CHECK(gmem2.merge(merge_configs) == 1); + + { + int i = 0; + for (auto& m : gmem2) { + CHECK(m.session_id == sids[i]); + CHECK(m.admin == (i < 10 || i == 58)); + CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" + : i < 10 ? "Admin " + std::to_string(i) + : "Member " + std::to_string(i))); + CHECK(m.profile_picture.key == + (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes + : ""_hexbytes)); + CHECK(m.profile_picture.url == + (i < 20 ? "http://example.com/" + std::to_string(i) : "")); + CHECK(m.invite_pending() == (55 <= i && i < 58)); + CHECK(m.invite_failed() == (i == 57)); + CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); + CHECK(m.promotion_pending() == (i >= 59 && i <= 61)); + CHECK(m.promotion_failed() == (i >= 60 && i <= 61)); + do + i++; + while (is_prime100(i)); + } + CHECK(i == 62); + } +} From 8f142c0a35b1158cccf3afacf4219e69e4e75fbc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 4 Aug 2023 19:18:01 -0300 Subject: [PATCH 006/572] Fix bad doc name --- include/session/config/groups/info.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 5276bdaf..5d417655 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -228,7 +228,7 @@ class Info final : public ConfigBase { /// (or negative) to disable the delete-attachment-before timestamp. void set_delete_attach_before(int64_t timestamp); - /// API: groups/Info::get_delete_before + /// API: groups/Info::get_delete_attach_before /// /// Returns the delete-before unix timestamp (seconds) for the group; clients should delete all /// messages from the closed group with timestamps earlier than this value, if set. From bf3df15913871fd369330951d85f267ee0f8efea Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 8 Aug 2023 13:52:35 -0300 Subject: [PATCH 007/572] More API doc fixes --- include/session/config/groups/info.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 5d417655..6d598f42 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -241,7 +241,7 @@ class Info final : public ConfigBase { /// - `int64_t` -- the unix timestamp for which all older messages shall be delete std::optional get_delete_attach_before() const; - /// API: groups/Info::destroy_group() + /// API: groups/Info::destroy_group /// /// Sets the group as permanently deleted, and set this status in the group's config. Receiving /// clients are supposed to remove the conversation from their conversation list when this @@ -254,7 +254,7 @@ class Info final : public ConfigBase { /// None: this call is destructive and permanent. Be careful! void destroy_group(); - /// API: groups/Info::is_destroyed() + /// API: groups/Info::is_destroyed /// /// Returns true if this group has been marked destroyed; the receiving client is expected to /// delete it. From 7e3001d90f2d720afbf7f1161928305704fe4891 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 18 Aug 2023 19:41:54 -0300 Subject: [PATCH 008/572] Group encryption keys Tests to follow. --- include/session/config.hpp | 47 +++ include/session/config/base.hpp | 190 +++++---- include/session/config/groups/info.hpp | 9 + include/session/config/groups/keys.hpp | 298 ++++++++++++++ include/session/config/groups/members.hpp | 31 +- include/session/config/namespaces.hpp | 1 + include/session/util.hpp | 225 +++++++++++ src/CMakeLists.txt | 2 + src/config.cpp | 117 +++--- src/config/base.cpp | 206 ++++++---- src/config/groups/info.cpp | 5 +- src/config/groups/keys.cpp | 451 ++++++++++++++++++++++ src/config/groups/members.cpp | 2 +- src/config/internal.cpp | 48 +++ src/config/internal.hpp | 21 + src/util.cpp | 16 + tests/test_group_info.cpp | 29 +- 17 files changed, 1453 insertions(+), 245 deletions(-) create mode 100644 include/session/config/groups/keys.hpp create mode 100644 src/config/groups/keys.cpp create mode 100644 src/util.cpp diff --git a/include/session/config.hpp b/include/session/config.hpp index f02fe961..0a27103f 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -60,6 +60,11 @@ struct missing_signature : signature_error { struct config_parse_error : config_error { using config_error::config_error; }; +/// Type thrown for some bad value in a config (e.g. missing required key, or key with an +/// unexpected/unhandled value). +struct config_value_error : config_parse_error { + using config_parse_error::config_parse_error; +}; /// Class for a parsed, read-only config message; also serves as the base class of a /// MutableConfigMessage which allows setting values. @@ -342,6 +347,48 @@ class MutableConfigMessage : public ConfigMessage { void increment_impl(); }; +/// API: base/verify_config_sig +/// +/// Verifies a config message signature, throwing a missing_signature or signature_error exception +/// if the signature is missing or invalid. +/// +/// A config message signature is always in the "~" key of a config message, which must be the +/// very last key of the message, and signs the config value up to (but not including) the ~ +/// key-value pair in the serialized config message. +/// +/// For instance, for a config message of: +/// +/// d[...configdata...]1:~64:[sigdata]e +/// +/// the signature signs the value `d[...configdata...]` (i.e. the `1:~64:[sigdata]` signature +/// pair, and the final closing `e` of the config message, are not included). No keys may +/// follow the signature key/value. +/// +/// Inputs: +/// - `dict` -- a `bt_dict_consumer` positioned at or before the "~" key where the signature is +/// expected. (If the bt_dict_consumer has already consumed the "~" key then this call will fail +/// as if the signature was missing). +/// - `config_msg` -- the full config message; this must be a view of the same data in memory that +/// `dict` is parsing (i.e. it cannot be a copy). +/// - `verifier` -- a callback to invoke to verify the signature of the message. If the callback is +/// empty then the signature will be ignored (it is neither required nor verified). +/// - `signature_optional` -- true if the message is allowed to omit a signature; in such a case a +/// message will be accepted with no signature, or with a valid signature, but not with an invalid +/// signature. (Ignored if `verifier` is empty) +/// - `verified_signature` is a pointer to a std::optional array of signature data; if this is +/// specified and not nullptr then the optional with be emplaced with the signature bytes if the +/// signature successfully validates. +/// +/// Outputs: +/// - returns with no value on success +/// - throws on failure +void verify_config_sig( + oxenc::bt_dict_consumer dict, + ustring_view config_msg, + const ConfigMessage::verify_callable& verifier, + bool signature_optional, + std::optional>* verified_signature = nullptr); + } // namespace session::config namespace oxenc::detail { diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 1c230b60..66c0e307 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -44,9 +45,95 @@ enum class ConfigState : int { Waiting = 2, }; +using Ed25519PubKey = std::array; +using Ed25519Secret = sodium_array; + +// Helper base class for holding a config signing keypair +class ConfigSig { + protected: + // Contains an optional signing keypair; if the public key is set then incoming messages must + // contain a valid signature from that key to be loaded. If the private key is set then a + // signature will be added to the message signed by that key. (Note that if a public key is set + // but not a private key then this config object cannot push config changes!) + std::optional _sign_pk = std::nullopt; + Ed25519Secret _sign_sk; + + ConfigSig() = default; + + // Returns a blake2b 32-byte hash of the config signing seed using hash key `key`. `key` must + // be 64 bytes or less, and should generally be unique for each key use case. + // + // Throws if a secret key hasn't been set via `set_sig_keys`. + std::array seed_hash(std::string_view key) const; + + virtual void set_verifier(ConfigMessage::verify_callable v) = 0; + virtual void set_signer(ConfigMessage::sign_callable v) = 0; + + // Meant to be called from the subclass constructor after other necessary initialization; calls + // set_sig_keys, set_sig_pubkey, or clear_sig_keys() for you, based on which are non-nullopt. + // + // Throws if given invalid data (i.e. wrong key size, or mismatched pubkey/secretkey). + void init_sig_keys( + std::optional ed25519_pubkey, + std::optional ed25519_secretkey); + + public: + virtual ~ConfigSig() = default; + + /// API: base/ConfigSig::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` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey + /// concatenated together) that sets both the secret key and public key. + void set_sig_keys(ustring_view secret); + + /// API: base/ConfigSig::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 `set_sig_keys()` to set both + /// secret and pubkey keys together). + /// + /// Inputs: + /// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages + void set_sig_pubkey(ustring_view pubkey); + + /// API: base/ConfigSig::get_sig_pubkey + /// + /// Returns a const reference to the 32-byte Ed25519 signing pubkey, if set. + /// + /// Outputs: + /// - reference to the 32-byte pubkey, or `std::nullopt` if not set. + const std::optional>& get_sig_pubkey() const { return _sign_pk; } + + /// API: base/ConfigSig::clear_sig_keys + /// + /// Drops the signature pubkey and/or secret key, if the object has them. + /// + /// Inputs: none. + void clear_sig_keys(); +}; + /// Base config type for client-side configs containing common functionality needed by all config /// sub-types. -class ConfigBase { +class ConfigBase : public ConfigSig { private: // 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 @@ -64,18 +151,7 @@ class ConfigBase { // element will be used when encrypting a new message to push. When decrypting, we attempt each // of them, starting with .front(), until decryption succeeds. using Key = std::array; - Key* _keys = nullptr; - size_t _keys_size = 0; - size_t _keys_capacity = 0; - - // Contains an optional signing keypair; if the public key is set then incoming messages must - // contain a valid signature from that key to be loaded. If the private key is set then a - // signature will be added to the message signed by that key. (Note that if a public key is set - // but not a private key then this config object cannot push config changes!) - using Ed25519PubKey = std::array; - using Ed25519Secret = std::array; - std::optional _sign_pk = std::nullopt; - Ed25519Secret* _sign_sk = nullptr; + sodium_vector _keys; // Contains the current active message hash, as fed into us in `confirm_pushed()`. Empty if we // don't know it yet. When we dirty the config this value gets moved into `old_hashes_` to be @@ -119,6 +195,9 @@ class ConfigBase { // already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter. MutableConfigMessage& dirty(); + void set_verifier(ConfigMessage::verify_callable v) override; + void set_signer(ConfigMessage::sign_callable s) override; + public: // class for proxying subfield access; this class should never be stored but only used // ephemerally (most of its methods are rvalue-qualified). This lets constructs such as @@ -702,7 +781,7 @@ class ConfigBase { void load_key(ustring_view ed25519_secretkey); public: - virtual ~ConfigBase(); + virtual ~ConfigBase() = default; // Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to // be managed. @@ -983,19 +1062,24 @@ class ConfigBase { /// Inputs: /// - `ustring_view key` -- 32 byte binary key /// - `high_priority` -- Whether to add to front or back of key list. If true then key is added - /// to beginning and replace highest-priority key for encryption - void add_key(ustring_view key, bool high_priority = true); + /// to beginning and replace highest-priority key for encryption + /// - `dirty_config` -- if true then mark the config as dirty (incrementing seqno and needing a + /// push) if the first key (i.e. the key used for encryption) is changed as a result of this + /// call. Ignored if the config is not modifiable. + void add_key(ustring_view key, bool high_priority = true, bool dirty_config = false); /// API: base/ConfigBase::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. /// - /// Inputs: None + /// Inputs: + /// - `dirty_config` -- if this removes a key then mark the config as dirty (incrementing seqno + /// and requiring a push). Only has an effect if the config is modifiable. /// /// Outputs: /// - `int` -- Returns number of keys removed - int clear_keys(); + int clear_keys(bool dirty_config = false); /// API: base/ConfigBase::remove_key /// @@ -1008,10 +1092,26 @@ class ConfigBase { /// Inputs: /// - `key` -- the key to remove from the key list /// - `from` -- optional agrument to specify which position to remove from, usually omitted + /// - `dirty_config` -- if true, and the *first* key (the encryption key) is removed from the + /// list then mark the config as dirty (incrementing seqno and requiring a re-push). Ignored + /// if the config is not modifiable. /// /// Outputs: /// - `bool` -- Returns true if found and removed - bool remove_key(ustring_view key, size_t from = 0); + bool remove_key(ustring_view key, size_t from = 0, bool dirty_config = false); + + /// API: base/ConfigBase::replace_keys + /// + /// Replaces the full set of keys with the given vector of keys. This is equivalent to calling + /// `clear_keys()` and then `add_key` with the keys, in order (and so the first key in the + /// vector becomes the highest priority, i.e. the key used for encryption). + /// + /// Inputs: + /// - `new_keys` -- the new decryption keys; the first key becomes the new encryption key + /// - `dirty_config` -- if true then set the config status to dirty (incrementing seqno and + /// requiring a repush) if the old and new first key are not the same. Ignored if the config + /// is not modifiable. + void replace_keys(const std::vector& new_keys, bool dirty_config = false); /// API: base/ConfigBase::get_keys /// @@ -1057,59 +1157,9 @@ class ConfigBase { /// Outputs: /// - `ustring_view` -- binary data of the key ustring_view key(size_t i = 0) const { - assert(i < _keys_size); + assert(i < _keys.size()); return {_keys[i].data(), _keys[i].size()}; } - - /// API: base/ConfigBase::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` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey - /// concatenated together) that sets both the secret key and public key. - void set_sig_keys(ustring_view secret); - - /// API: base/ConfigBase::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 `set_sig_keys()` to set both - /// secret and pubkey keys together). - /// - /// Inputs: - /// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages - void set_sig_pubkey(ustring_view pubkey); - - /// API: base/ConfigBase::get_sig_pubkey - /// - /// Returns a const reference to the 32-byte Ed25519 signing pubkey, if set. - /// - /// Outputs: - /// - reference to the 32-byte pubkey, or `std::nullopt` if not set. - const std::optional>& get_sig_pubkey() const { return _sign_pk; } - - /// API: base/ConfigBase::clear_sig_keys - /// - /// Drops the signature pubkey and/or secret key, if the object has them. - /// - /// Inputs: none. - void clear_sig_keys(); }; // 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.hpp b/include/session/config/groups/info.hpp index 6d598f42..c46bb83a 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -80,6 +80,15 @@ class Info final : public ConfigBase { /// - `const char*` - Will return "groups::Info" const char* encryption_domain() const override { return "groups::Info"; } + /// Returns the subaccount masking value. This is based on the group's seed and thus is only + /// obtainable by an admin account. + /// + /// Inputs: none + /// + /// Outputs: + /// - `ustring_view` - the 32-byte masking value. + std::array subaccount_mask() const; + /// API: groups/Info::get_name /// /// Returns the group name, or std::nullopt if there is no group name set. diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp new file mode 100644 index 00000000..dde05a43 --- /dev/null +++ b/include/session/config/groups/keys.hpp @@ -0,0 +1,298 @@ +#pragma once + +#include +#include + +#include "../../config.hpp" +#include "../base.hpp" +#include "../namespaces.hpp" +#include "../profile_pic.hpp" +#include "members.hpp" + +namespace session::config::groups { + +class Members; +class Info; + +using namespace std::literals; + +/// This "config" isn't exactly a regular config type that inherits from ConfigBase; in particular: +/// - it doesn't encrypt the message (but merely contains encrypted elements within it) +/// - it doesn't merge +/// - it does have a concept analogous to the message seqno +/// - conflict resolution involves regenerating and distributing new keys; nothing gets merged. +/// - it cares strongly about when new configs were pushed (configs expire after having been +/// replaced for a certain amount of time, not by being updated). +/// - its internal state isn't fully serialized when pushing updates +/// - messages don't contain the outer layer of config messages (where config metadata, references +/// to other objects, etc.) that ConfigBase-derived type hold. +/// - it isn't compressed (since most of the data fields are encrypted or random, compression +/// reduction would be minimal). +/// +/// Fields used (in ascii order): +/// # -- 24-byte nonce used for all the encrypted values in this message; required. +/// + -- set to 1 if this is a supplemental key message; omitted for a full key message. (It's +/// important that this key sort earlier than any fields that can differ between +/// supplemental/non-supplemental fields so we can identify the message type while parsing it). +/// G -- monotonically incrementing counter identifying key generation changes +/// K -- encrypted copy of the key for admins (omitted for `+` incremental key messages) +/// k -- packed bytes of encrypted keys for non-admin members; this is a single byte string in which +/// each 48 bytes is a separate encrypted value. +/// ~ -- signature of the message signed by the group's master keypair, signing the message value up +/// to but not including the ~ keypair. The signature must be the last key in the dict (thus +/// `~` since it is the largest 7-bit ascii character value). Note that this signature +/// mechanism works exactly the same as the signature on regular config messages. +/// +/// Some extra details: +/// +/// - each copy of the encryption key uses xchacha20_poly1305 using the `n` nonce +/// - the `k` members list gets padded with junk entries up to the next multiple of 75 (for +/// non-supplemental messages). +/// - the decryption key for the admin version of the key is H(admin_seed, +/// key="SessionGroupKeyAdmin") +/// - the encryption key for a member is H(a'B || A' || B, key="SessionGroupKeyMember") where a'/A' +/// is the group Ed25519 master key converted to X25519, and b/B is the member's X25519 keypair +/// (i.e. B is the non-05-prefixed session_id). +/// - the decryption key is calculated by the member using `bA' || A' || B` +/// - A new key and nonce is created from a 56-byte H(M0 || M1 || ... || Mn || g || S, +/// key="SessionGroupKeyGen"), where S = H(group_seed, key="SessionGroupKeySeed"). + +class Keys final : public ConfigSig { + + Ed25519Secret user_ed25519_sk; + + struct key_info { + std::array key; + std::chrono::system_clock::time_point timestamp; + int64_t generation; + + auto cmpval() const { return std::tie(generation, timestamp, key); } + bool operator<(const key_info& b) const { return cmpval() < b.cmpval(); } + bool operator==(const key_info& b) const { return cmpval() == b.cmpval(); } + bool operator!=(const key_info& b) const { return !(*this == b); } + }; + + /// Vector of keys that is kept sorted by generation/timestamp/key. This gets pruned as keys + /// have been superceded by another key for a sufficient amount of time (see KEY_EXPIRY). + sodium_vector keys_; + + sodium_cleared> pending_key_; + sodium_vector pending_key_config_; + int64_t pending_gen_ = -1; + + ConfigMessage::verify_callable verifier_; + ConfigMessage::sign_callable signer_; + + void set_verifier(ConfigMessage::verify_callable v) override { verifier_ = std::move(v); } + void set_signer(ConfigMessage::sign_callable s) override { signer_ = std::move(s); } + + // Checks for and drops expired keys. + void remove_expired(); + + public: + /// The multiple of members keys we include in the message; we add junk entries to the key list + /// to reach a multiple of this. 75 is chosen because it's a decently large human-round number + /// that should still fit within 4kiB page size on the storage server (allowing for some extra + /// row field storage). + static constexpr int MESSAGE_KEY_MULTIPLE = 75; + + // 75 because: + // 2 // for the 'de' delimiters of the outer dict + // + 3 + 2 + 12 // for the `1:g` and `iNNNNNNNNNNe` generation keypair + // + 3 + 3 + 24 // for the `1:n`, `24:`, and 24 byte nonce + // + 3 + 3 + 48 // for the `1:K`, `48:`, and 48 byte ciphertexted key + // + 3 + 6 // for the `1:k` and `NNNNN:` key and prefix of the keys pair + // + N * 48 // for the packed encryption keys + // + 3 + 3 + 64; // for the `1:~` and `64:` and 64 byte signature + // = 177 + 48N + // + // and N=75 puts us a little bit under 4kiB (which is sqlite's default page size). + + /// A key expires when it has been surpassed by another key for at least this amount of time. + /// We default this to double the 30 days that we strictly need to avoid race conditions with + /// 30-day old config messages that might need the key for a client that is only very rarely + /// online. + static constexpr auto KEY_EXPIRY = 2 * 30 * 24h; + + // No default constructor + Keys() = delete; + + /// API: groups/Keys::Keys + /// + /// Constructs a group members config object from existing data (stored from `dump()`) and a + /// list of encryption keys for encrypting new and decrypting existing messages. + /// + /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass + /// `std::nullopt` as the second argument. + /// + /// Inputs: + /// - `user_ed25519_secretkey` is the ed25519 secret key backing the current user's session ID, + /// and is used to decrypt incoming keys. It is required. + /// - `group_ed25519_pubkey` is the public key of the group, used to verify message signatures + /// on key updates. It is required. + /// - `group_ed25519_secretkey` is the secret key of the group, used to encrypt, decrypt, and + /// sign config messages. This is only possessed by the group admin(s), and must be provided + /// in order to make and push config changes. + /// - `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()`. + Keys(ustring_view user_ed25519_secretkey, + ustring_view group_ed25519_pubkey, + std::optional group_ed25519_secretkey, + std::optional dumped); + + /// API: groups/Keys::storage_namespace + /// + /// Returns the Keys namespace. Is constant, will always return Namespace::GroupKeys + /// + /// Inputs: None + /// + /// Outputs: + /// - `Namespace` - Will return Namespace::GroupKeys + Namespace storage_namespace() const { return Namespace::GroupKeys; } + + /// API: groups/Keys::encryption_domain + /// + /// Returns the encryption domain used when encrypting messages of this type. + /// + /// Inputs: None + /// + /// Outputs: + /// - `const char*` - Will return "groups::Keys" + const char* encryption_domain() const { return "groups::Keys"; } + + /// API: groups/Keys::group_keys + /// + /// Returns all the unexpired decryption keys that we know about. Keys are returned ordered + /// from most-recent to least-recent (and so the first one is meant to be used as the encryption + /// key), including a pending key if this object is in the process of pushing a new keys + /// message. + /// + /// Outputs: + /// - `std::vector` - vector of encryption keys. + std::vector group_keys() const; + + /// API: groups/Keys::rekey + /// + /// Generate 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 given + /// config::groups::Members object. This can only be done by an admin account (i.e. we must + /// have the group's private key). + /// + /// This method is intended to be called in two situations: + /// - potentially after loading new keys config messages (see `needs_rekey()`) + /// - when removing a member to switch to a new encryption key for the group that excludes that + /// member. + /// + /// This method is closely coupled to the group's Info and Members configs: it updates their + /// encryption keys and sets them as dirty, requiring a re-push to re-encrypt each of them. + /// Typically a rekey is performed as follows: + /// + /// - `rekey()` is called, returning the new keys config. + /// - `info.push()` is called to get the new info config (re-encrypted with the new key) + /// - `members.push()` is called to get the new members config (using the new key) + /// - all three new configs are pushed (ideally all at once, in a single batch request). + /// + /// Inputs: + /// - `Info` - the group's Info; it will be dirtied after the rekey and will require a push. + /// - `Members` - the current Members config for the group. When removing one or more members + /// this should be the list of members with the specific members already removed. The members + /// config will be dirtied after the rekey and will require a push. + /// + /// Outputs: + /// - `ustring_view` containing the data that needs to be pushed to the config keys namespace + /// for the group. (This can be re-obtained from `push()` if needed until it has been + /// confirmed or superceded). + ustring_view rekey(Info& info, Members& members); + + /// API: groups/Keys::pending_push + /// + /// If a rekey has been performed but not yet confirmed then this will contain the config + /// message to be pushed to the swarm. If there is no push current pending then this returns + /// nullopt. The value should be used immediately (i.e. the ustring_view may not remain valid + /// if other calls to the config object are made). + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` -- returns a populated config message that should be pushed, + /// if not yet confirmed, otherwise when no pending update is present this returns nullopt. + std::optional pending_config() const; + + /// API: groups/Keys::pending_key + /// + /// After calling rekey() this contains the new group encryption key *before* it is confirmed + /// pushed into the swarm. This is primarily used to allow a rekey + member list update using + /// the new key in the same swarm upload sequence. + /// + /// The pending key is dropped when an incoming keys message is successfully loaded with either + /// the pending key itself, or a keys message with a higher generation. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` the encryption key generated by the last `rekey()` call. + /// This is set to a new key when `rekey()` is called, and is cleared when any config message + /// is successfully loaded by `load_key`. + std::optional pending_key() const; + + /// API: groups/Keys::load_key + /// + /// Loads a key pulled down from the swarm into this Keys object. + /// + /// A Session client must process messages from the keys namespace *before* other group config + /// messages as new key messages may contain encryption keys needed to decrypt the other group + /// config message types. + /// + /// It is safe to load the same config multiple times, and to load expired configs; such cases + /// would typically not change the keys, but are allowed anyway. + /// + /// This method should always be wrapped in a `try/catch`: if the given configuration data is + /// malformed or is not properly signed an exception will be raised. + /// + /// Inputs: + /// - `msg` - the full stored config message value + /// - `hash` - the storage message hash (used to track current config messages) + /// - `timestamp` - the timestamp (from the swarm) when this message was stored (used to track + /// when other keys expire). + /// - `members` - the given group::Members object's en/decryption key list will be updated to + /// match this object's key list. + /// - `info` - the given group::Info object's en/decryption key list will be updated to match + /// this object's key list. + /// + /// Outputs: + /// - throws `std::runtime_error` (typically a subclass thereof) on failure to parse. + void load_key_message( + ustring_view data, + ustring_view msgid, + int64_t timestamp_ms, + Members& members, + Info& info); + + /// API: groups/Keys::needs_rekey + /// + /// Returns true if the key list requires a new key to be generated and pushed to the server (by + /// calling `rekey()`). This will only be true for admin accounts (as only admin accounts can + /// call rekey()). Note that this value will also remain true until the pushed data is fetched + /// and loaded via `load_key_message`. + /// + /// Note that this not only tracks when an automatic `rekey()` is needed because of a key + /// collision (such as two admins removing different members at the same time); there are other + /// situations in which rekey() should also be called (such as when kicking a member) that are + /// not reflected by this flag. + /// + /// The recommended use of this method is to call it immediately after fetching messages from + /// the group config namespace of the swarm, whether or not new configs were retrieved, but + /// after processing incoming new config messages that were pulled down. + /// + /// Unlike regular config messages, there is no need to confirm the push: confirmation (and + /// adoption of the new keys) happens when the new keys arrived back down from the swarm in the + /// next fetch. + /// + /// Inputs: None + /// + /// Outputs: + /// - `true` if a rekey is needed, `false` otherwise. + bool needs_rekey() const; +}; + +} // namespace session::config::groups diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 92f6f9d8..7eefa6d9 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -189,13 +189,13 @@ class Members final : public ConfigBase { /// /// Inputs: /// - `keys` -- contains the possible 32-byte en/decryption keys that may be used for incoming - /// messages. These are *not* Ed25519 secret keys, but rather symmetric encryption keys used - /// for encryption (generally generated using a cryptographically secure random generator). - /// The *first* key in this list will be used to encrypt outgoing config messages (and so, in - /// general, should be the most current key). There must always be at least one key present - /// (either provided at construction or via add_keys) before you can push a config. - /// Post-construction you can add or remove keys via add_key/remove_key/clear_keys from - /// ConfigBase. + /// messages (both config messages and group messages). These are *not* Ed25519 secret keys, + /// but rather symmetric encryption keys used for encryption (generally generated using a + /// cryptographically secure random generator). The *first* key in this list will be used to + /// encrypt outgoing config messages (and so, in general, should be the most current key). + /// There must always be at least one key present (either provided at construction or via + /// add_keys) before you can push a config. Post-construction you can add or remove keys via + /// add_key/remove_key/clear_keys from ConfigBase. /// - `ed25519_pubkey` is the public key of this group, used to validate config messages. /// Config messages not signed with this key will be rejected. /// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages. @@ -277,6 +277,23 @@ class Members final : public ConfigBase { /// /// Removes a session ID from the member list, if present. /// + /// Typically this call should be coupled with a re-key of the group's encryption key so that + /// the removed member cannot read the group. For example: + /// + /// bool removed = members.erase("050123456789abcdef..."); + /// // You can remove more than one at a time, if needed: + /// removed |= members.erase("050000111122223333..."); + /// + /// if (removed) { + /// auto new_keys_conf = keys.rekey(members); + /// members.add_key(*keys.pending_key(), true); + /// auto [seqno, new_memb_conf, obs] = members.push(); + /// + /// // Send the two new configs to the swarm (via a seqence of two `store`s): + /// // - new_keys_conf goes into the keys namespace + /// // - new_memb_conf goes into the members namespace + /// } + /// /// Inputs: /// - `session_id` the hex session ID of the member to remove /// diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 7b1db95a..1fc58c42 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -13,6 +13,7 @@ enum class Namespace : std::int16_t { // Groups namespaces (i.e. for config of the group itself, not one user's group settings) GroupInfo = 11, GroupMembers = 12, + GroupKeys = 13, }; } // namespace session::config diff --git a/include/session/util.hpp b/include/session/util.hpp index 6cfa77e2..cdb6fccb 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,4 +1,11 @@ #pragma once +#include +#include +#include +#include +#include +#include + #include "types.hpp" namespace session { @@ -41,4 +48,222 @@ inline constexpr bool end_with(std::string_view str, std::string_view suffix) { return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix; } +// Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure +void* sodium_buffer_allocate(size_t size); +// Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. +void sodium_buffer_deallocate(void* p); +// Calls sodium_memzero to zero a buffer +void sodium_zero_buffer(void* ptr, size_t size); + +// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but +// more secure for sensitive data). +template +struct sodium_ptr { + private: + T* x; + + public: + sodium_ptr() : x{nullptr} {} + sodium_ptr(std::nullptr_t) : sodium_ptr{} {} + ~sodium_ptr() { reset(x); } + + // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` + // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly + // to destruct and deallocate the existing object. + template + T& emplace(Args&&... args) { + if (x) + reset(); + x = static_cast(sodium_buffer_allocate(sizeof(T))); + new (x) T(std::forward(args)...); + return *x; + } + + void reset() { + if (x) { + x->~T(); + sodium_buffer_deallocate(x); + x = nullptr; + } + } + void operator=(std::nullptr_t) { reset(); } + + T& operator*() { return *x; } + const T& operator*() const { return *x; } + + T* operator->() { return x; } + const T* operator->() const { return x; } + + explicit operator bool() const { return x != nullptr; } +}; + +// Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only +// be used with trivially destructible types. +template >> +struct sodium_cleared : T { + using T::T; + + ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } +}; + +// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation +// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes +// extra precautions, intended for storing sensitive values. +template +struct sodium_array { + private: + T* buf; + size_t len; + + public: + // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). + sodium_array() : buf{nullptr}, len{0} {} + + // Constructs an array with a given size, default-constructing the individual elements. + template >> + explicit sodium_array(size_t length) : + buf{length == 0 ? nullptr + : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, + len{0} { + + if (length > 0) { + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else if constexpr (std::is_nothrow_default_constructible_v) { + for (; len < length; len++) + new (buf[len]) T(); + } else { + try { + for (; len < length; len++) + new (buf[len]) T(); + } catch (...) { + reset(); + throw; + } + } + } + } + + ~sodium_array() { reset(); } + + // Moveable: ownership is transferred to the new object and the old object becomes empty. + sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { + other.buf = nullptr; + other.len = 0; + } + sodium_array& operator=(sodium_array&& other) { + sodium_buffer_deallocate(buf); + buf = other.buf; + len = other.len; + other.buf = nullptr; + other.len = 0; + } + + // Non-copyable + sodium_array(const sodium_array&) = delete; + sodium_array& operator=(const sodium_array&) = delete; + + // Destroys the held array; after destroying elements the allocated space is overwritten with + // 0s before being deallocated. + void reset() { + if (buf) { + if constexpr (!std::is_trivially_destructible_v) + while (len > 0) + buf[--len].~T(); + + sodium_buffer_deallocate(buf); + } + buf = nullptr; + len = 0; + } + + // Calls reset() to destroy the current value (if any) and then allocates a new + // default-constructed one of the given size. + template >> + void reset(size_t length) { + reset(); + if (length > 0) { + buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else { + for (; len < length; len++) + new (buf[len]) T(); + } + } + } + + // Loads the array from a pointer and size; this first resets a value (if present), allocates a + // new array of the given size, the copies the given value(s) into the new buffer. T must be + // copyable. This is *not* safe to use if `buf` points into the currently allocated data. + template >> + void load(const T* data, size_t length) { + reset(length); + if (length == 0) + return; + + if constexpr (std::is_trivially_copyable_v) + std::memcpy(buf, data, sizeof(T) * length); + else + for (; len < length; len++) + new (buf[len]) T(data[len]); + } + + const T& operator[](size_t i) const { + assert(i < len); + return buf[i]; + } + T& operator[](size_t i) { + assert(i < len); + return buf[i]; + } + + T* data() { return buf; } + const T* data() const { return buf; } + + size_t size() const { return len; } + bool empty() const { return len == 0; } + explicit operator bool() const { return !empty(); } + + T* begin() { return buf; } + const T* begin() const { return buf; } + T* end() { return buf + len; } + const T* end() const { return buf + len; } + + using difference_type = ptrdiff_t; + using value_type = T; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::random_access_iterator_tag; +}; + +// sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with +// sensitive data. It is as a result slower and has more overhead than a standard allocator and +// intended for use with a container (such as std::vector) when storing keys. +template +struct sodium_allocator { + using value_type = T; + + [[nodiscard]] static T* allocate(std::size_t n) { + return static_cast(sodium_buffer_allocate(n * sizeof(T))); + } + + static void deallocate(T* p, std::size_t) { sodium_buffer_deallocate(p); } + + template + bool operator==(const sodium_allocator&) const noexcept { + return true; + } + template + bool operator!=(const sodium_allocator&) const noexcept { + return false; + } +}; + +/// Vector that uses sodium's secure (but heavy) memory allocations +template +using sodium_vector = std::vector>; + } // namespace session diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e96f27d..041afbc8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,10 +24,12 @@ add_library(config config/error.c config/groups/info.cpp config/groups/members.cpp + config/groups/keys.cpp config/internal.cpp config/user_groups.cpp config/user_profile.cpp fields.cpp + util.cpp ) set_target_properties( config diff --git a/src/config.cpp b/src/config.cpp index 83abde0a..d7062cb6 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -15,6 +15,7 @@ #include #include +#include "config/internal.hpp" #include "session/bt_merge.hpp" #include "session/util.hpp" @@ -347,44 +348,6 @@ namespace { return std::string_view{reinterpret_cast(hash.data()), hash.size()}; } - oxenc::bt_dict::iterator append_unknown( - oxenc::bt_dict_producer& out, - oxenc::bt_dict::iterator it, - oxenc::bt_dict::iterator end, - std::string_view until) { - for (; it != end && it->first < until; ++it) - out.append_bt(it->first, it->second); - - assert(!(it != end && it->first == until)); - return it; - } - - /// Extracts and unknown keys in the top-level dict into `unknown` that have keys (strictly) - /// between previous and until. - void load_unknowns( - oxenc::bt_dict& unknown, - oxenc::bt_dict_consumer& in, - std::string_view previous, - std::string_view until) { - while (!in.is_finished() && in.key() < until) { - std::string key{in.key()}; - if (key <= previous || (!unknown.empty() && key <= unknown.rbegin()->first)) - throw oxenc::bt_deserialize_invalid{"top-level keys are out of order"}; - if (in.is_string()) - unknown.emplace_hint(unknown.end(), std::move(key), in.consume_string()); - else if (in.is_negative_integer()) - unknown.emplace_hint(unknown.end(), std::move(key), in.consume_integer()); - else if (in.is_integer()) - unknown.emplace_hint(unknown.end(), std::move(key), in.consume_integer()); - else if (in.is_list()) - unknown.emplace_hint(unknown.end(), std::move(key), in.consume_list()); - else if (in.is_dict()) - unknown.emplace_hint(unknown.end(), std::move(key), in.consume_dict()); - else - throw oxenc::bt_deserialize_invalid{"invalid bencoded value type"}; - } - } - hash_t& hash_msg(hash_t& into, ustring_view serialized) { crypto_generichash_blake2b( into.data(), into.size(), serialized.data(), serialized.size(), nullptr, 0); @@ -466,6 +429,50 @@ namespace { } } // namespace +void verify_config_sig( + oxenc::bt_dict_consumer dict, + ustring_view config_msg, + const ConfigMessage::verify_callable& verifier, + bool signature_optional, + std::optional>* verified_signature) { + ustring_view to_verify, sig; + dict.skip_until("~"); + if (!dict.is_finished() && dict.key() == "~") { + // We get the key string_view here because it points into the buffer that we need. + // Currently it will be pointing at the "~", i.e.: + // + // [...previousdata...]1:~64:[sigdata] + // ^-- here + // + // but what we need is the data up to the end of `]`, so we subtract 2 off that to + // figure out the range of the full serialized data that should have been signed: + + auto key = dict.key(); + assert(to_unsigned(key.data()) > config_msg.data() && + to_unsigned(key.data()) < config_msg.data() + config_msg.size()); + to_verify = config_msg.substr(0, to_unsigned(key.data()) - config_msg.data() - 2); + sig = to_unsigned_sv(dict.consume_string_view()); + } + + if (!dict.is_finished()) + throw config_parse_error{"Invalid config: dict has invalid key(s) after \"~\""}; + + if (verifier) { + if (sig.empty()) { + if (!signature_optional) + throw missing_signature{"Config signature is missing"}; + } else if (sig.size() != 64) + throw signature_error{"Config signature is invalid (not 64B)"}; + else if (!verifier(to_verify, sig)) + throw signature_error{"Config signature failed verification"}; + else if (verified_signature) { + if (!*verified_signature) + verified_signature->emplace(); + std::memcpy((*verified_signature)->data(), sig.data(), 64); + } + } +} + bool MutableConfigMessage::prune() { return prune_(data_).second; } @@ -568,38 +575,7 @@ ConfigMessage::ConfigMessage( load_unknowns(unknown_, dict, "=", "~"); - ustring_view to_verify, sig; - if (!dict.is_finished() && dict.key() == "~") { - // We get the key string_view here because it points into the buffer that we need. - // Currently it will be pointing at the "~", i.e.: - // - // [...previousdata...]1:~64:[sigdata] - // ^-- here - // - // but what we need is the data up to the end of `]`, so we subtract 2 off that to - // figure out the range of the full serialized data that should have been signed: - - auto key = dict.key(); - assert(to_unsigned(key.data()) > serialized.data() && - to_unsigned(key.data()) < serialized.data() + serialized.size()); - to_verify = serialized.substr(0, to_unsigned(key.data()) - serialized.data() - 2); - sig = to_unsigned_sv(dict.consume_string_view()); - } - - if (!dict.is_finished()) - throw config_parse_error{"Invalid config: dict has invalid key(s) after \"~\""}; - - if (verifier) { - if (sig.empty()) { - if (!signature_optional) - throw missing_signature{"Config signature is missing"}; - } else if (sig.size() != 64) - throw signature_error{"Config signature is invalid (not 64B)"}; - else if (!verifier(to_verify, sig)) - throw signature_error{"Config signature failed verification"}; - else - std::memcpy(verified_signature_.emplace().data(), sig.data(), 64); - } + verify_config_sig(dict, serialized, verifier, signature_optional, &verified_signature_); } catch (const oxenc::bt_deserialize_invalid& err) { throw config_parse_error{"Failed to parse config file: "s + err.what()}; } @@ -689,7 +665,6 @@ ConfigMessage::ConfigMessage( return; } - unmerged_ = -1; // Clear any redundant messages. (we do it *here* rather than above because, in the diff --git a/src/config/base.cpp b/src/config/base.cpp index 5364b810..7ea0fbc9 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -21,8 +22,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{"Unable to make changes to a read-only config object"}; if (_state == ConfigState::Clean && !_curr_hash.empty()) { _old_hashes.insert(std::move(_curr_hash)); @@ -60,7 +60,7 @@ std::unique_ptr make_config_message(bool from_dirty, Args&&... ar int ConfigBase::merge(const std::vector>& configs) { - if (_keys_size == 0) + if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; const auto old_seqno = _config->seqno(); @@ -96,9 +96,8 @@ int ConfigBase::merge(const std::vector>& c // - element 4 is a chunk of the data. for (size_t ci = 0; ci < configs.size(); ci++) { auto& [hash, conf] = configs[ci]; - std::optional plaintext; bool decrypted = false; - for (size_t i = 0; !decrypted && i < _keys_size; i++) { + for (size_t i = 0; !decrypted && i < _keys.size(); i++) { try { plaintexts.emplace_back(hash, decrypt(conf, key(i), encryption_domain())); decrypted = true; @@ -267,7 +266,7 @@ void compress_message(ustring& msg, int level) { } std::tuple> ConfigBase::push() { - if (_keys_size == 0) + if (_keys.empty()) throw std::logic_error{"Cannot push data without an encryption key!"}; std::tuple> ret{ @@ -276,6 +275,7 @@ std::tuple> ConfigBase::push() { auto& [seqno, msg, obs] = ret; if (auto lvl = compression_level()) compress_message(msg, *lvl); + pad_message(msg); // Prefix pad with nulls encrypt_inplace(msg, key(), encryption_domain()); @@ -339,12 +339,19 @@ ConfigBase::ConfigBase( else _config = std::make_unique(); + init_sig_keys(ed25519_pubkey, ed25519_secretkey); +} + +void ConfigSig::init_sig_keys( + std::optional ed25519_pubkey, std::optional ed25519_secretkey) { if (ed25519_secretkey) { - if (ed25519_pubkey) - assert(*ed25519_pubkey == ed25519_secretkey->substr(32)); + if (ed25519_pubkey && *ed25519_pubkey != ed25519_secretkey->substr(32)) + throw std::invalid_argument{"Invalid signing keys: secret key and pubkey do not match"}; set_sig_keys(*ed25519_secretkey); } else if (ed25519_pubkey) { set_sig_pubkey(*ed25519_pubkey); + } else { + clear_sig_keys(); } } @@ -385,14 +392,8 @@ void ConfigBase::init_from_dump(std::string_view dump) { load_extra_data(std::move(extra)); } -ConfigBase::~ConfigBase() { - sodium_free(_keys); - if (_sign_sk) - sodium_free(_sign_sk); -} - int ConfigBase::key_count() const { - return _keys_size; + return _keys.size(); } bool ConfigBase::has_key(ustring_view key) const { @@ -400,86 +401,104 @@ bool ConfigBase::has_key(ustring_view key) const { throw std::invalid_argument{"invalid key given to has_key(): not 32-bytes"}; auto* keyptr = key.data(); - for (size_t i = 0; i < _keys_size; i++) - if (sodium_memcmp(keyptr, _keys[i].data(), KEY_SIZE) == 0) + for (const auto& key : _keys) + if (sodium_memcmp(keyptr, key.data(), KEY_SIZE) == 0) return true; return false; } std::vector ConfigBase::get_keys() const { std::vector ret; - ret.reserve(_keys_size); - for (size_t i = 0; i < _keys_size; i++) - ret.emplace_back(_keys[i].data(), _keys[i].size()); + ret.reserve(_keys.size()); + for (const auto& key : _keys) + ret.emplace_back(key.data(), key.size()); return ret; } -void ConfigBase::add_key(ustring_view key, bool high_priority) { +void ConfigBase::add_key(ustring_view key, bool high_priority, bool dirty_config) { static_assert( sizeof(Key) == KEY_SIZE, "std::array appears to have some overhead which seems bad"); if (key.size() != KEY_SIZE) throw std::invalid_argument{"add_key failed: key size must be 32 bytes"}; - if (_keys_size > 0 && sodium_memcmp(_keys[0].data(), key.data(), KEY_SIZE) == 0) + if (!_keys.empty() && sodium_memcmp(_keys.front().data(), key.data(), KEY_SIZE) == 0) return; else if (!high_priority && has_key(key)) return; - if (_keys_capacity == 0) { + if (_keys.capacity() == 0) // There's not a lot of point in starting this off really small: sodium is likely going to // use at least a page size anyway. - _keys_capacity = 16; - _keys = static_cast(sodium_allocarray(_keys_capacity, KEY_SIZE)); - } - - if (_keys_size >= _keys_capacity) { - _keys_capacity *= 2; - auto new_keys = static_cast(sodium_allocarray(_keys_capacity, 32)); - if (high_priority) { - std::memcpy(new_keys[0].data(), key.data(), KEY_SIZE); - std::memcpy(&new_keys[1], _keys, _keys_size * KEY_SIZE); - } else { - std::memcpy(&new_keys[0], _keys, _keys_size * KEY_SIZE); - std::memcpy(new_keys[_keys_size].data(), key.data(), KEY_SIZE); - } - sodium_free(_keys); - _keys = new_keys; - } else if (high_priority) { - // shift everything up so we can insert at beginning - std::memmove(&_keys[1], &_keys[0], _keys_size * KEY_SIZE); - std::memcpy(_keys[0].data(), key.data(), KEY_SIZE); - } else { - // add at the end - std::memcpy(_keys[_keys_size].data(), key.data(), KEY_SIZE); - } - _keys_size++; + _keys.reserve(64); - // *Slightly* suboptimal in that we might change buffers above even when we didn't need to, but - // not worth worrying about optimizing. if (high_priority) remove_key(key, 1); + + auto& newkey = *_keys.emplace(high_priority ? _keys.begin() : _keys.end()); + std::memcpy(newkey.data(), key.data(), KEY_SIZE); + + if (dirty_config && !is_readonly() && (_keys.size() == 1 || high_priority)) + dirty(); } -int ConfigBase::clear_keys() { - int ret = _keys_size; - _keys_size = 0; +int ConfigBase::clear_keys(bool dirty_config) { + int ret = _keys.size(); + _keys.clear(); + _keys.shrink_to_fit(); + + if (dirty_config && !is_readonly() && ret > 0) + dirty(); + return ret; } -bool ConfigBase::remove_key(ustring_view key, size_t from) { - bool removed = false; - - for (size_t i = from; i < _keys_size; i++) { - if (sodium_memcmp(key.data(), _keys[i].data(), KEY_SIZE) == 0) { - if (i + 1 < _keys_size) - std::memmove(&_keys[i], &_keys[i + 1], (_keys_size - i - 1) * KEY_SIZE); - _keys_size--; - removed = true; - // Don't break, in case there are somehow duplicates in here - } +void ConfigBase::replace_keys(const std::vector& new_keys, bool dirty_config) { + if (new_keys.empty()) { + if (_keys.empty()) + return; + clear_keys(dirty_config); + return; } - return removed; + + for (auto& k : new_keys) + if (k.size() != KEY_SIZE) + throw std::invalid_argument{"replace_keys failed: keys must be 32 bytes"}; + + dirty_config = dirty_config && !is_readonly() && + (_keys.empty() || + sodium_memcmp(_keys.front().data(), new_keys.front().data(), KEY_SIZE) != 0); + + _keys.clear(); + for (auto& k : new_keys) + add_key(k, /*high_priority=*/false); // The first key gets the high priority spot even + // with `false` since we just emptied the list + + if (dirty_config) + dirty(); +} + +bool ConfigBase::remove_key(ustring_view key, size_t from, bool dirty_config) { + auto starting_size = _keys.size(); + if (from >= starting_size) + return false; + + dirty_config = dirty_config && !is_readonly() && + sodium_memcmp(key.data(), _keys.front().data(), KEY_SIZE) == 0; + + _keys.erase( + std::remove_if( + _keys.begin() + from, + _keys.end(), + [&key](const auto& k) { + return sodium_memcmp(key.data(), k.data(), KEY_SIZE) == 0; + }), + _keys.end()); + + if (dirty_config) + dirty(); + + return _keys.size() < starting_size; } void ConfigBase::load_key(ustring_view ed25519_secretkey) { @@ -490,49 +509,68 @@ void ConfigBase::load_key(ustring_view ed25519_secretkey) { add_key(ed25519_secretkey.substr(0, 32)); } -void ConfigBase::set_sig_keys(ustring_view secret) { +void ConfigSig::set_sig_keys(ustring_view secret) { if (secret.size() != 64) throw std::invalid_argument{"Invalid sodium secret: expected 64 bytes"}; clear_sig_keys(); - _sign_sk = static_cast(sodium_malloc(sizeof(Ed25519Secret))); - std::memcpy(_sign_sk->data(), secret.data(), secret.size()); + _sign_sk.reset(64); + std::memcpy(_sign_sk.data(), secret.data(), secret.size()); _sign_pk.emplace(); - crypto_sign_ed25519_sk_to_pk(_sign_pk->data(), _sign_sk->data()); + crypto_sign_ed25519_sk_to_pk(_sign_pk->data(), _sign_sk.data()); - _config->verifier = [this](ustring_view data, ustring_view sig) { + set_verifier([this](ustring_view data, ustring_view sig) { return 0 == crypto_sign_ed25519_verify_detached( sig.data(), data.data(), data.size(), _sign_pk->data()); - }; - _config->signer = [this](ustring_view data) { + }); + set_signer([this](ustring_view data) { ustring sig; sig.resize(64); if (0 != crypto_sign_ed25519_detached( - sig.data(), nullptr, data.data(), data.size(), _sign_sk->data())) + sig.data(), nullptr, data.data(), data.size(), _sign_sk.data())) throw std::runtime_error{"Internal error: config signing failed!"}; return sig; - }; + }); } -void ConfigBase::set_sig_pubkey(ustring_view pubkey) { +void ConfigSig::set_sig_pubkey(ustring_view pubkey) { if (pubkey.size() != 32) throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; _sign_pk.emplace(); std::memcpy(_sign_pk->data(), pubkey.data(), 32); - _config->verifier = [this](ustring_view data, ustring_view sig) { + set_verifier([this](ustring_view data, ustring_view sig) { return 0 == crypto_sign_ed25519_verify_detached( sig.data(), data.data(), data.size(), _sign_pk->data()); - }; + }); } -void ConfigBase::clear_sig_keys() { +void ConfigSig::clear_sig_keys() { _sign_pk.reset(); - if (_sign_sk) { - sodium_free(_sign_sk); - _sign_sk = nullptr; - } - _config->signer = nullptr; - _config->verifier = nullptr; + _sign_sk.reset(); + set_signer(nullptr); + set_verifier(nullptr); +} + +void ConfigBase::set_verifier(ConfigMessage::verify_callable v) { + _config->verifier = std::move(v); +} + +void ConfigBase::set_signer(ConfigMessage::sign_callable s) { + _config->signer = std::move(s); +} + +std::array ConfigSig::seed_hash(std::string_view key) const { + if (!_sign_sk) + throw std::runtime_error{"Cannot make a seed hash without a signing secret key"}; + std::array out; + crypto_generichash_blake2b( + out.data(), + out.size(), + _sign_sk.data(), + 32, // Just the seed part of the value, not the last half (which is just the pubkey) + reinterpret_cast(key.data()), + std::min(key.size(), 64)); + return out; } void set_error(config_object* conf, std::string e) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 484a2e73..066c6225 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -6,7 +6,6 @@ #include #include "../internal.hpp" -//#include "session/config/groups/info.h" #include "session/config/error.h" #include "session/export.h" #include "session/types.hpp" @@ -27,6 +26,10 @@ Info::Info( add_key(k, false); } +std::array Info::subaccount_mask() const { + return seed_hash("SessionGroupSubaccountMask"); +} + std::optional Info::get_name() const { if (auto* s = data["n"].string(); s && !s->empty()) return *s; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp new file mode 100644 index 00000000..88da86e8 --- /dev/null +++ b/src/config/groups/keys.cpp @@ -0,0 +1,451 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../internal.hpp" + +namespace session::config::groups { + +Keys::Keys( + ustring_view user_ed25519_secretkey, + ustring_view group_ed25519_pubkey, + std::optional group_ed25519_secretkey, + std::optional dumped) { + + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + + if (user_ed25519_secretkey.size() != 64) + throw std::invalid_argument{"Invalid Keys construction: invalid user ed25519 secret key"}; + if (group_ed25519_pubkey.size() != 32) + throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 public key"}; + if (group_ed25519_secretkey && group_ed25519_secretkey->size() != 64) + throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 secret key"}; + + init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); + + user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); +} + +std::vector Keys::group_keys() const { + std::vector ret; + ret.reserve(keys_.size() + !pending_key_config_.empty()); + + if (pending_key_config_.empty()) + ret.emplace_back(pending_key_.data(), 32); + + for (auto it = keys_.rbegin(); it != keys_.rend(); ++it) + ret.emplace_back(it->key.data(), 32); + + return ret; +} + +static std::array compute_xpk(const unsigned char* ed25519_pk) { + std::array xpk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to X25519; " + "is the pubkey valid?"}; + return xpk; +} + +static constexpr auto seed_hash_key = "SessionGroupKeySeed"sv; +static const ustring_view enc_key_hash_key = to_unsigned_sv("SessionGroupKeyGen"sv); +static constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; +static const ustring_view enc_key_member_hash_key = to_unsigned_sv("SessionGroupKeyMemberKey"sv); +static const ustring_view junk_seed_hash_key = to_unsigned_sv("SessionGroupJunkMembers"sv); + +ustring_view Keys::rekey(Info& info, Members& members) { + if (!_sign_sk || !_sign_pk) + throw std::logic_error{ + "Unable to issue a new group encryption key without the main group keys"}; + + // For members we calculate the outer encryption key as H(aB || A || B). But because we only + // have `B` (the session id) as an x25519 pubkey, we do this in x25519 space, which means we + // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. + auto group_xpk = compute_xpk(_sign_pk->data()); + + sodium_cleared> group_xsk; + crypto_sign_ed25519_sk_to_curve25519(group_xsk.data(), _sign_sk.data()); + + // We need quasi-randomness: full secure random would be great, except that different admins + // encrypting for the same update would always create different keys, but we want it + // deterministic so that that doesn't happen. + // + // So we use: + // + // H1(member0 || member1 || ... || memberN || generation || H2(group_secret_key)) + // + // where: + // - H1(.) = 56-byte BLAKE2b keyed hash with key "SessionGroupKeyGen" + // - memberI is each members full session ID, expressed in hex (66 chars), in sorted order (note + // that this includes *all* members, not only non-admins). + // - generation is the new generation value, expressed as a base 10 string (e.g. "123") + // - H2(.) = 32-byte BLAKE2b keyed hash of the sodium group secret key seed (just the 32 byte, + // not the full 64 byte with the pubkey in the second half), key "SessionGroupKeySeed" + // + // And then from this 56-byte hash we use the first 32 bytes as the new group key and the last + // 24 bytes as the encryption nonce. + // + // If we have to append junk member keys (for padding) them we reuse H1 with H(H1 || a) to + // produce a sodium pseudo-RNG seed for deterministic junk value generation. + // + // To encrypt this we have one key encrypted for all admins, plus one encryption per non-admin + // member. For admins we encrypt using a 32-byte blake2b keyed hash of the group secret key + // seed, just like H2, but with key "SessionGroupKeyAdminKey". + + std::array h2 = seed_hash(seed_hash_key); + + std::array h1; + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( + &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); + for (const auto& m : members) + crypto_generichash_blake2b_update( + &st, to_unsigned(m.session_id.data()), m.session_id.size()); + + auto gen = keys_.empty() ? 0 : keys_.back().generation + 1; + auto gen_str = std::to_string(gen); + crypto_generichash_blake2b_update(&st, to_unsigned(gen_str.data()), gen_str.size()); + + crypto_generichash_blake2b_update(&st, h2.data(), 32); + + crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); + + ustring_view enc_key{h1.data(), 32}; + ustring_view nonce{h1.data() + 32, 24}; + + oxenc::bt_dict_producer d{}; + + d.append("#", from_unsigned_sv(nonce)); + // d.append("+", 0); // Not supplemental, so leave off + + static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); + static_assert(crypto_aead_xchacha20poly1305_ietf_ABYTES == 16); + std::array< + unsigned char, + crypto_aead_xchacha20poly1305_ietf_KEYBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES> + encrypted; + std::string_view enc_sv{reinterpret_cast(encrypted.data()), encrypted.size()}; + + // Shared key for admins + auto member_k = seed_hash(enc_key_admin_hash_key); + static_assert(member_k.size() == crypto_aead_xchacha20poly1305_ietf_KEYBYTES); + crypto_aead_xchacha20poly1305_ietf_encrypt( + encrypted.data(), + nullptr, + enc_key.data(), + enc_key.size(), + nullptr, + 0, + nullptr, + nonce.data(), + member_k.data()); + + d.append("G", gen); + d.append("K", enc_sv); + + auto member_keys = d.append_list("k"); + + int member_count = 0; + for (const auto& m : members) { + auto m_xpk = session_id_xpk(m.session_id); + // Calculate the encryption key: H(aB || A || B) + if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) + continue; // The scalarmult failed; maybe a bad session id? + + crypto_generichash_blake2b_init( + &st, + enc_key_member_hash_key.data(), + enc_key_member_hash_key.size(), + member_k.size()); + crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); + crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); + crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); + crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); + + crypto_aead_xchacha20poly1305_ietf_encrypt( + encrypted.data(), + nullptr, + enc_key.data(), + enc_key.size(), + nullptr, + 0, + nullptr, + nonce.data(), + member_k.data()); + + member_keys.append(enc_sv); + member_count++; + } + + // Pad it out with junk entries to the next MESSAGE_KEY_MULTIPLE + if (member_count % MESSAGE_KEY_MULTIPLE) { + int n_junk = MESSAGE_KEY_MULTIPLE - (member_count % MESSAGE_KEY_MULTIPLE); + std::vector junk_data; + junk_data.resize(encrypted.size() * n_junk); + + std::array rng_seed; + crypto_generichash_blake2b_init( + &st, junk_seed_hash_key.data(), junk_seed_hash_key.size(), rng_seed.size()); + crypto_generichash_blake2b_update(&st, h1.data(), h1.size()); + crypto_generichash_blake2b_update(&st, _sign_sk.data(), _sign_sk.size()); + crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); + + randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); + std::string_view junk_view{ + reinterpret_cast(junk_data.data()), junk_data.size()}; + while (!junk_view.empty()) { + member_keys.append(junk_view.substr(0, encrypted.size())); + junk_view.remove_prefix(encrypted.size()); + } + } + + // Finally we sign the message at put it as the ~ key (which is 0x7f, and thus comes later than + // any other ascii key). + auto to_sign = to_unsigned_sv(d.view()); + // The view contains the trailing "e", but we don't sign it (we are going to append the + // signature there instead): + to_sign.remove_suffix(1); + auto sig = signer_(to_sign); + if (sig.size() != 64) + throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; + + d.append("~", from_unsigned_sv(sig)); + + // Load this key/config/gen into our pending variables + pending_gen_ = gen; + std::memcpy(pending_key_.data(), enc_key.data(), pending_key_.size()); + pending_key_config_.clear(); + auto conf = d.view(); + pending_key_config_.resize(conf.size()); + std::memcpy(pending_key_config_.data(), conf.data(), conf.size()); + + auto new_key_list = group_keys(); + // We want to dirty the member/info lists so that they get re-encrypted and re-pushed with the + // new key: + members.replace_keys(new_key_list, /*dirty=*/true); + info.replace_keys(new_key_list, /*dirty=*/true); + + return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; +} + +std::optional Keys::pending_config() const { + if (pending_key_config_.empty()) + return std::nullopt; + return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; +} + +void Keys::load_key_message( + ustring_view data, ustring_view msgid, int64_t timestamp_ms, Members& members, Info& info) { + + oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; + + if (!_sign_pk || !verifier_) + throw std::logic_error{"Group pubkey is not set; unable to load config message"}; + + auto group_xpk = compute_xpk(_sign_pk->data()); + + if (!d.skip_until("#")) + throw config_value_error{"Key message has no nonce"}; + auto nonce = to_unsigned_sv(d.consume_string_view()); + + bool supplemental = false; + if (d.skip_until("+")) { + auto supp = d.consume_integer(); + if (supp == 0 || supp == 1) + supplemental = static_cast(supp); + else + throw config_value_error{ + "Unexpected value " + std::to_string(supp) + " for '+' key (expected 0/1)"}; + } + + bool found_key = false; + sodium_cleared new_key{}; + new_key.timestamp = std::chrono::system_clock::from_time_t(timestamp_ms / 1000) + + 1ms * (timestamp_ms % 1000); + + if (!d.skip_until("G")) + throw config_value_error{"Key message missing required generation (G) field"}; + + new_key.generation = d.consume_integer(); + if (new_key.generation < 0) + throw config_value_error{"Key message contains invalid negative generation"}; + + if (!supplemental) { + if (!d.skip_until("K")) + throw config_value_error{ + "Non-supplemental key message is missing required admin key (K)"}; + + auto admin_key = to_unsigned_sv(d.consume_string_view()); + if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{"Key message has invalid admin key length"}; + + if (_sign_sk) { + auto k = seed_hash(enc_key_admin_hash_key); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + new_key.key.data(), + nullptr, + nullptr, + admin_key.data(), + admin_key.size(), + nullptr, + 0, + nonce.data(), + k.data())) + throw config_value_error{"Failed to decrypt admin key from key message"}; + + found_key = true; + } + } + + sodium_cleared> member_dec_key; + if (!found_key) { + sodium_cleared> member_xsk; + crypto_sign_ed25519_sk_to_curve25519(member_xsk.data(), user_ed25519_sk.data()); + auto member_xpk = compute_xpk(user_ed25519_sk.data() + 32); + + // Calculate the encryption key: H(bA || A || B) [A = group, B = member] + if (0 != crypto_scalarmult_curve25519( + member_dec_key.data(), member_xsk.data(), group_xpk.data())) + throw std::runtime_error{ + "Unable to compute member decryption key; invalid group or member keys?"}; + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( + &st, + enc_key_member_hash_key.data(), + enc_key_member_hash_key.size(), + member_dec_key.size()); + crypto_generichash_blake2b_update(&st, member_dec_key.data(), member_dec_key.size()); + crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); + crypto_generichash_blake2b_update(&st, member_xpk.data(), member_xpk.size()); + crypto_generichash_blake2b_final(&st, member_dec_key.data(), member_dec_key.size()); + } + + // Even if we're already found a key we still parse these, so that admins and all users have the + // same error conditions for rejecting an invalid config message. + if (!d.skip_until("k")) + throw config_value_error{"Config is missing member keys list (k)"}; + auto key_list = d.consume_list_consumer(); + + int member_key_count = 0; + for (; !key_list.is_finished(); member_key_count++) { + auto member_key = to_unsigned_sv(key_list.consume_string_view()); + if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{ + "Key message has invalid member key length at index " + + std::to_string(member_key_count)}; + + if (found_key) + continue; + + if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + new_key.key.data(), + nullptr, + nullptr, + member_key.data(), + member_key.size(), + nullptr, + 0, + nonce.data(), + member_dec_key.data())) { + // Decryption success, we found our key! + found_key = true; + } + } + + if (!supplemental && member_key_count % MESSAGE_KEY_MULTIPLE != 0) + throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; + + verify_config_sig(d, data, verifier_, false); + + if (found_key) { + auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); + if (it != keys_.end() && new_key == *it) { + // We found a key we already had, so just ignore it. + found_key = false; + } else { + keys_.insert(it, new_key); + + remove_expired(); + } + } + + // If this is our pending config or this has a later generation than our pending config then + // drop our pending status. + if (!pending_key_config_.empty() && + (new_key.generation > pending_gen_ || new_key.key == pending_key_)) + pending_key_config_.clear(); + + auto new_key_list = group_keys(); + members.replace_keys(new_key_list, /*dirty=*/false); + info.replace_keys(new_key_list, /*dirty=*/false); +} + +void Keys::remove_expired() { + if (keys_.size() < 2) + return; + + auto lapsed_end = keys_.begin(); + + for (auto it = keys_.begin(); it != keys_.end();) { + // Advance `it` if the next element is an alternate key (with a later timestamp) from the + // same generation. When we finish this loop, `it` is the last element of this generation + // and `it2` is the first element of the next generation. + auto it2 = std::next(it); + while (it2 != keys_.end() && it2->generation == it->generation) + it = it2++; + if (it2 == keys_.end()) + break; + + // it2 points at the lowest-timestamp value of the next-largest generation: if there is + // something more than 30 days newer than it2, then that tells us that `it`'s generation is + // no longer needed since a newer generation passed it more than 30 days ago. (We actually + // use 60 days for paranoid safety, but the logic is the same). + // + // NB: We don't trust the local system clock here (and the `timestamp` values are + // swarm-provided), because devices are notoriously imprecise, which means that since we + // only invalidate keys when new keys come in, we can hold onto one obsolete generation + // indefinitely (but this is a tiny overhead and not worth trying to build a + // system-clock-is-broken workaround to avoid). + if (it2->timestamp + KEY_EXPIRY < keys_.back().timestamp) + lapsed_end = it2; + else + break; + it = it2; + } + + if (lapsed_end != keys_.begin()) + keys_.erase(keys_.begin(), lapsed_end); +} + +bool Keys::needs_rekey() const { + if (!_sign_sk || !_sign_pk || keys_.size() < 2) + return false; + + // We rekey if the max generation value is being used across multiple keys (which indicates some + // sort of rekey collision, somewhat analagous to merge configs in regular config messages). + auto last_it = std::prev(keys_.end()); + auto second_it = std::prev(last_it); + return last_it->generation == second_it->generation; +} + +std::optional Keys::pending_key() const { + if (!pending_key_config_.empty()) + return ustring_view{pending_key_.data(), pending_key_.size()}; + return std::nullopt; +} + +} // namespace session::config::groups diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 2c37be88..bfbdc1b3 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -1,4 +1,5 @@ #include "session/config/groups/members.hpp" + #include #include "../internal.hpp" @@ -121,7 +122,6 @@ bool Members::erase(std::string_view session_id) { return ret; } - member::member(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 48ba1ec2..a6ac18bb 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -22,6 +23,13 @@ std::string session_id_to_bytes(std::string_view session_id) { return oxenc::from_hex(session_id); } +std::array session_id_xpk(std::string_view session_id) { + check_session_id(session_id); + std::array xpk; + oxenc::from_hex(session_id.begin(), session_id.end(), xpk.begin()); + return xpk; +} + void check_encoded_pubkey(std::string_view pk) { if (!((pk.size() == 64 && oxenc::is_hex(pk)) || ((pk.size() == 43 || (pk.size() == 44 && pk.back() == '=')) && oxenc::is_base64(pk)) || @@ -125,4 +133,44 @@ void set_nonempty_str(ConfigBase::DictFieldProxy&& field, std::string_view val) field.erase(); } +/// Writes all the dict elements in `[it, E)` into `out`; E is whichever of `end` or an element with +/// a key >= `until` comes first. +oxenc::bt_dict::iterator append_unknown( + oxenc::bt_dict_producer& out, + oxenc::bt_dict::iterator it, + oxenc::bt_dict::iterator end, + std::string_view until) { + for (; it != end && it->first < until; ++it) + out.append_bt(it->first, it->second); + + assert(!(it != end && it->first == until)); + return it; +} + +/// Extracts and unknown keys in the top-level dict into `unknown` that have keys (strictly) +/// between previous and until. +void load_unknowns( + oxenc::bt_dict& unknown, + oxenc::bt_dict_consumer& in, + std::string_view previous, + std::string_view until) { + while (!in.is_finished() && in.key() < until) { + std::string key{in.key()}; + if (key <= previous || (!unknown.empty() && key <= unknown.rbegin()->first)) + throw oxenc::bt_deserialize_invalid{"top-level keys are out of order"}; + if (in.is_string()) + unknown.emplace_hint(unknown.end(), std::move(key), in.consume_string()); + else if (in.is_negative_integer()) + unknown.emplace_hint(unknown.end(), std::move(key), in.consume_integer()); + else if (in.is_integer()) + unknown.emplace_hint(unknown.end(), std::move(key), in.consume_integer()); + else if (in.is_list()) + unknown.emplace_hint(unknown.end(), std::move(key), in.consume_list()); + else if (in.is_dict()) + unknown.emplace_hint(unknown.end(), std::move(key), in.consume_dict()); + else + throw oxenc::bt_deserialize_invalid{"invalid bencoded value type"}; + } +} + } // namespace session::config diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 5bbab837..7b9d92e7 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -1,7 +1,10 @@ #pragma once +#include + #include #include +#include #include "session/config/base.hpp" #include "session/config/error.h" @@ -56,6 +59,10 @@ void check_session_id(std::string_view session_id); // Checks the session_id (throwing if invalid) then returns it as bytes std::string session_id_to_bytes(std::string_view session_id); +// Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 prefix +// (which is the x25519 pubkey). +std::array session_id_xpk(std::string_view session_id); + // Validates an open group pubkey; we accept it in hex, base32z, or base64 (padded or unpadded). // Throws std::invalid_argument if invalid. void check_encoded_pubkey(std::string_view pk); @@ -113,4 +120,18 @@ void set_pair_if( } } +oxenc::bt_dict::iterator append_unknown( + oxenc::bt_dict_producer& out, + oxenc::bt_dict::iterator it, + oxenc::bt_dict::iterator end, + std::string_view until); + +/// Extracts and unknown keys in the top-level dict into `unknown` that have keys (strictly) +/// between previous and until. +void load_unknowns( + oxenc::bt_dict& unknown, + oxenc::bt_dict_consumer& in, + std::string_view previous, + std::string_view until); + } // namespace session::config diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 00000000..29cec1df --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,16 @@ +#include + +#include + +namespace session { +void* sodium_buffer_allocate(size_t length) { + if (auto* p = sodium_malloc(length)) + return p; + throw std::bad_alloc{}; +} + +void sodium_buffer_deallocate(void* p) { + if (p) + sodium_free(p); +} +} // namespace session diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 798aa2e7..b8013d0f 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -30,11 +30,14 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector enc_keys{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; + std::vector enc_keys{ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; groups::Info ginfo1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - enc_keys.insert(enc_keys.begin(), "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + enc_keys.insert( + enc_keys.begin(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); groups::Info ginfo2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; @@ -67,8 +70,8 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { ginfo2.set_expiry_timer(1h); constexpr int64_t create_time{1682529839}; ginfo2.set_created(create_time); - ginfo2.set_delete_before(create_time + 50*86400); - ginfo2.set_delete_attach_before(create_time + 70*86400); + ginfo2.set_delete_before(create_time + 50 * 86400); + ginfo2.set_delete_attach_before(create_time + 70 * 86400); ginfo2.destroy_group(); auto [s2, p2, o2] = ginfo2.push(); @@ -87,7 +90,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.merge(merge_configs) == 0); ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); - ginfo1.add_key("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes, /*prepend=*/ false); + ginfo1.add_key( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes, + /*prepend=*/false); CHECK(ginfo1.merge(merge_configs) == 1); @@ -96,11 +101,12 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.get_name() == "Better name!"); CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); - CHECK(ginfo1.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(ginfo1.get_profile_pic().key == + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo1.get_expiry_timer() == 1h); CHECK(ginfo1.get_created() == create_time); - CHECK(ginfo1.get_delete_before() == create_time + 50*86400); - CHECK(ginfo1.get_delete_attach_before() == create_time + 70*86400); + CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400); + CHECK(ginfo1.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo1.is_destroyed()); ginfo1.confirm_pushed(s3, "fakehash3"); @@ -110,11 +116,12 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.merge(merge_configs) == 1); CHECK(ginfo2.get_name() == "Better name!"); CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345"); - CHECK(ginfo2.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(ginfo2.get_profile_pic().key == + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo2.get_expiry_timer() == 1h); CHECK(ginfo2.get_created() == create_time); - CHECK(ginfo2.get_delete_before() == create_time + 50*86400); - CHECK(ginfo2.get_delete_attach_before() == create_time + 70*86400); + CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); + CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo2.is_destroyed()); } From fdc664ef6ba17a7593c978e14d1d1cabada484f2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 14:05:25 -0300 Subject: [PATCH 009/572] Address ftrget review comments --- include/session/config/groups/info.hpp | 8 ++++---- include/session/config/groups/members.hpp | 10 +++++----- src/config/contacts.cpp | 1 - src/config/groups/members.cpp | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index c46bb83a..c278e00a 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -239,15 +239,15 @@ class Info final : public ConfigBase { /// API: groups/Info::get_delete_attach_before /// - /// Returns the delete-before unix timestamp (seconds) for the group; clients should delete all - /// messages from the closed group with timestamps earlier than this value, if set. + /// Returns the delete-attachments-before unix timestamp (seconds) for the group; clients should + /// delete all messages from the closed group with timestamps earlier than this value, if set. /// - /// Returns std::nullopt if no delete-before timestamp is set. + /// Returns std::nullopt if no delete-attachments-before timestamp is set. /// /// Inputs: none. /// /// Outputs: - /// - `int64_t` -- the unix timestamp for which all older messages shall be delete + /// - `int64_t` -- the unix timestamp for which all older message attachments shall be deleted std::optional get_delete_attach_before() const; /// API: groups/Info::destroy_group diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 7eefa6d9..55e8a3d3 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -150,11 +150,11 @@ struct member { /// API: groups/member::info /// - /// converts the member info into a c struct + /// Converts the member info into a C struct. /// /// Inputs: - /// - `c` -- Return Parameter that will be filled with data in contact_info - void into(config_group_member& c) const; + /// - `m` -- Reference to C struct to fill with group member info. + void into(config_group_member& m) const; /// API: groups/member::set_name /// @@ -265,8 +265,8 @@ class Members final : public ConfigBase { /// /// ```cpp /// auto m = members.get_or_construct(pubkey); - /// c.name = "Session User 42"; - /// members.set(c); + /// m.name = "Session User 42"; + /// members.set(m); /// ``` /// /// Inputs: diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 2b9e5875..6c2ca1ce 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -338,7 +338,6 @@ void Contacts::iterator::_load_info() { if (_it->first.size() == 33) { if (auto* info_dict = std::get_if(&_it->second)) { _val = std::make_shared(oxenc::to_hex(_it->first)); - auto hex = oxenc::to_hex(_it->first); _val->load(*info_dict); return; } diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index bfbdc1b3..32980bac 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -80,7 +80,6 @@ void Members::iterator::_load_info() { if (_it->first.size() == 33) { if (auto* info_dict = std::get_if(&_it->second)) { _val = std::make_shared(oxenc::to_hex(_it->first)); - auto hex = oxenc::to_hex(_it->first); _val->load(*info_dict); return; } From 6e3cbf8c4498dc600c1145ceb987fc11244d2682 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 15:01:51 -0300 Subject: [PATCH 010/572] Fix broken assert The first part of this assert shouldn't fail if we are starting from a fresh, blank config (in which case we don't actually serialize our own message anymore). --- src/config/base.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 7ea0fbc9..98ef19ab 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -221,7 +221,8 @@ int ConfigBase::merge(const std::vector>& c /* do nothing */ } else { _config = std::move(new_conf); - assert(_config->unmerged_index() >= 1 && _config->unmerged_index() < all_hashes.size()); + assert(((old_seqno == 0 && mine.empty()) || _config->unmerged_index() >= 1) && + _config->unmerged_index() < all_hashes.size()); set_state(ConfigState::Clean); _curr_hash = all_hashes[_config->unmerged_index()]; } From 0fcc07c197d5833c1f94f899c3fd9a6deba710eb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 17:15:14 -0300 Subject: [PATCH 011/572] Remove `signature_optional` parameter It doesn't really make sense to have this as we either want signatures (for shared messages) or don't (for personally encrypted messages). The only place we were passing it as `true` was in a place that also didn't pass a verifier or signer (during dumping), and so already wasn't adding/checking signatures. --- include/session/config.hpp | 16 ++-------------- src/config.cpp | 24 ++++++++---------------- src/config/base.cpp | 10 ++++------ src/config/groups/keys.cpp | 2 +- tests/test_configdata.cpp | 16 +--------------- 5 files changed, 16 insertions(+), 52 deletions(-) diff --git a/include/session/config.hpp b/include/session/config.hpp index 0a27103f..9eb7b8d5 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -127,8 +127,7 @@ class ConfigMessage { ustring_view serialized, verify_callable verifier = nullptr, sign_callable signer = nullptr, - int lag = DEFAULT_DIFF_LAGS, - bool signature_optional = false); + int lag = DEFAULT_DIFF_LAGS); /// Constructs a new ConfigMessage by loading and potentially merging multiple serialized /// ConfigMessages together, according to the config conflict resolution rules. The result @@ -152,10 +151,6 @@ class ConfigMessage { /// diffs that exceeding this lag value will have those early lagged diffs dropping during /// loading. /// - /// signature_optional - if true then accept a message with no signature even when a verifier is - /// set, thus allowing unsigned messages (though messages with an invalid signature are still - /// not allowed). This option is ignored when verifier is not set. - /// /// error_handler - if set then any config message parsing error will be passed to this function /// for handling with the index of `configs` that failed and the error exception: the callback /// typically warns and, if the overall construction should abort, rethrows the error. If this @@ -168,7 +163,6 @@ class ConfigMessage { verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, - bool signature_optional = false, std::function error_handler = nullptr); /// Returns a read-only reference to the contained data. (To get a mutable config object use @@ -288,7 +282,6 @@ class MutableConfigMessage : public ConfigMessage { verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, - bool signature_optional = false, std::function error_handler = nullptr); /// Wrapper around the above that takes a single string view to load a single message, doesn't @@ -298,8 +291,7 @@ class MutableConfigMessage : public ConfigMessage { ustring_view config, verify_callable verifier = nullptr, sign_callable signer = nullptr, - int lag = DEFAULT_DIFF_LAGS, - bool signature_optional = false); + int lag = DEFAULT_DIFF_LAGS); /// Does the same as the base incrementing, but also records any diff info from the current /// MutableConfigMessage. *this* object gets pruned and signed as part of this call. If the @@ -372,9 +364,6 @@ class MutableConfigMessage : public ConfigMessage { /// `dict` is parsing (i.e. it cannot be a copy). /// - `verifier` -- a callback to invoke to verify the signature of the message. If the callback is /// empty then the signature will be ignored (it is neither required nor verified). -/// - `signature_optional` -- true if the message is allowed to omit a signature; in such a case a -/// message will be accepted with no signature, or with a valid signature, but not with an invalid -/// signature. (Ignored if `verifier` is empty) /// - `verified_signature` is a pointer to a std::optional array of signature data; if this is /// specified and not nullptr then the optional with be emplaced with the signature bytes if the /// signature successfully validates. @@ -386,7 +375,6 @@ void verify_config_sig( oxenc::bt_dict_consumer dict, ustring_view config_msg, const ConfigMessage::verify_callable& verifier, - bool signature_optional, std::optional>* verified_signature = nullptr); } // namespace session::config diff --git a/src/config.cpp b/src/config.cpp index d7062cb6..77b95e6e 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -433,7 +433,6 @@ void verify_config_sig( oxenc::bt_dict_consumer dict, ustring_view config_msg, const ConfigMessage::verify_callable& verifier, - bool signature_optional, std::optional>* verified_signature) { ustring_view to_verify, sig; dict.skip_until("~"); @@ -458,10 +457,9 @@ void verify_config_sig( throw config_parse_error{"Invalid config: dict has invalid key(s) after \"~\""}; if (verifier) { - if (sig.empty()) { - if (!signature_optional) - throw missing_signature{"Config signature is missing"}; - } else if (sig.size() != 64) + if (sig.empty()) + throw missing_signature{"Config signature is missing"}; + else if (sig.size() != 64) throw signature_error{"Config signature is invalid (not 64B)"}; else if (!verifier(to_verify, sig)) throw signature_error{"Config signature failed verification"}; @@ -547,8 +545,7 @@ ConfigMessage::ConfigMessage( ustring_view serialized, verify_callable verifier_, sign_callable signer_, - int lag, - bool signature_optional) : + int lag) : verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} { oxenc::bt_dict_consumer dict{from_unsigned_sv(serialized)}; @@ -575,7 +572,7 @@ ConfigMessage::ConfigMessage( load_unknowns(unknown_, dict, "=", "~"); - verify_config_sig(dict, serialized, verifier, signature_optional, &verified_signature_); + verify_config_sig(dict, serialized, verifier, &verified_signature_); } catch (const oxenc::bt_deserialize_invalid& err) { throw config_parse_error{"Failed to parse config file: "s + err.what()}; } @@ -586,7 +583,6 @@ ConfigMessage::ConfigMessage( verify_callable verifier_, sign_callable signer_, int lag, - bool signature_optional, std::function error_handler) : verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} { @@ -594,7 +590,7 @@ ConfigMessage::ConfigMessage( for (size_t i = 0; i < serialized_confs.size(); i++) { const auto& data = serialized_confs[i]; try { - ConfigMessage m{data, verifier, signer, lag, signature_optional}; + ConfigMessage m{data, verifier, signer, lag}; configs.emplace_back(std::move(m), false); } catch (const config_error& e) { if (error_handler) @@ -653,7 +649,7 @@ ConfigMessage::ConfigMessage( // produce a proper merge, so we will just keep the highest (highest seqno, hash) config and use // that, dropping the rest. Someone else (with signing power) will have to merge and push the // merge out to us. - if (verifier && !signer && !signature_optional) { + if (verifier && !signer) { auto best_it = std::max_element(configs.begin(), configs.end(), [](const auto& a, const auto& b) { if (a.second != b.second) // Exactly one of the two is redundant @@ -716,14 +712,12 @@ MutableConfigMessage::MutableConfigMessage( verify_callable verifier, sign_callable signer, int lag, - bool signature_optional, std::function error_handler) : ConfigMessage{ serialized_confs, std::move(verifier), std::move(signer), lag, - signature_optional, std::move(error_handler)} { if (!merged()) increment_impl(); @@ -733,14 +727,12 @@ MutableConfigMessage::MutableConfigMessage( ustring_view config, verify_callable verifier, sign_callable signer, - int lag, - bool signature_optional) : + int lag) : MutableConfigMessage{ std::vector{{config}}, std::move(verifier), std::move(signer), lag, - signature_optional, [](size_t, const config_error& e) { throw e; }} {} const oxenc::bt_dict& ConfigMessage::diff() { diff --git a/src/config/base.cpp b/src/config/base.cpp index 98ef19ab..6b6d2e70 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -181,7 +181,6 @@ int ConfigBase::merge(const std::vector>& c _config->verifier, _config->signer, config_lags(), - false, /* signature not optional (if we have a verifier) */ [&](size_t i, const config_error& e) { log(LogLevel::warning, e.what()); assert(i > 0); // i == 0 means we can't deserialize our own serialization @@ -372,13 +371,12 @@ void ConfigBase::init_from_dump(std::string_view dump) { // could store a dump. _config = std::make_unique( to_unsigned_sv(d.consume_string_view()), - nullptr, // verifier, set later - nullptr, // signer, set later - config_lags(), - true /* signature optional because we don't sign the dump */); + nullptr, // We omit verifier and signer for now because we don't want this dump to + nullptr, // be signed (since it's just a dump). + config_lags()); else _config = std::make_unique( - to_unsigned_sv(d.consume_string_view()), nullptr, nullptr, config_lags(), true); + to_unsigned_sv(d.consume_string_view()), nullptr, nullptr, config_lags()); if (d.skip_until("(")) { _curr_hash = d.consume_string(); diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 88da86e8..689db1a5 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -369,7 +369,7 @@ void Keys::load_key_message( if (!supplemental && member_key_count % MESSAGE_KEY_MULTIPLE != 0) throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; - verify_config_sig(d, data, verifier_, false); + verify_config_sig(d, data, verifier_); if (found_key) { auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 82fbbe68..0a2b1eed 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -397,6 +397,7 @@ TEST_CASE("config message signature", "[config][signing]") { CHECK(msg.hash() == m.hash()); CHECK(printable(msg.serialize()) == printable(m_expected)); + // Deliberately modify the signature to break it: auto m_broken = m_expected; REQUIRE(m_broken[m_broken.size() - 2] == 0x07); m_broken[m_broken.size() - 2] = 0x17; @@ -422,7 +423,6 @@ TEST_CASE("config message signature", "[config][signing]") { verifier, nullptr, ConfigMessage::DEFAULT_DIFF_LAGS, - false, [](size_t, const auto& exc) { throw exc; }), config::config_error, Message("Config signature failed verification")); @@ -432,20 +432,6 @@ TEST_CASE("config message signature", "[config][signing]") { ConfigMessage(m_unsigned, verifier), config::missing_signature, Message("Config signature is missing")); - - ConfigMessage m_no_sig{m_unsigned, verifier, nullptr, ConfigMessage::DEFAULT_DIFF_LAGS, true}; - CHECK(m_no_sig.seqno() == 10); - CHECK(m_no_sig.data() == m.data()); - // The hash will differ because of the lack of signature - CHECK(m_no_sig.hash() != m.hash()); - - CHECK(printable(m_no_sig.serialize()) == printable(m_unsigned)); - - // If we set a signer and serialize again, we're going to get the *signed* message. (This is - // not something that should be done, really, because this message does not agree with the - // hash). - m_no_sig.signer = signer; - CHECK(printable(m_no_sig.serialize()) == printable(m_expected)); } const config::dict data118{ From c6fd4713aabfc3eb8df0b0ed48d51395de5b6593 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 20:11:17 -0300 Subject: [PATCH 012/572] Move all key management into `Keys`; make Keys dumpable This makes Keys construction, loading, and rekeying take the Info and Members object to update their keys, thus removing the requirement for application code to worry about key management at all (they just feed the keys in, and they propagate to info/members). Also adds a state dump (similar to base config `dump()`) to Keys. --- include/session/config/groups/info.hpp | 19 ++-- include/session/config/groups/keys.hpp | 62 +++++++++-- include/session/config/groups/members.hpp | 16 +-- include/session/util.hpp | 8 ++ src/config/groups/info.cpp | 6 +- src/config/groups/keys.cpp | 119 +++++++++++++++++++++- src/config/groups/members.cpp | 6 +- src/util.cpp | 6 ++ tests/CMakeLists.txt | 1 + tests/test_group_info.cpp | 45 ++++++-- tests/test_group_keys.cpp | 75 ++++++++++++++ tests/test_group_members.cpp | 13 ++- 12 files changed, 318 insertions(+), 58 deletions(-) create mode 100644 tests/test_group_keys.cpp diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index c278e00a..6568a616 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -33,21 +33,15 @@ class Info final : public ConfigBase { /// API: groups/Info::Info /// - /// Constructs a group info config object from existing data (stored from `dump()`) and a list - /// of encryption keys for encrypting new and decrypting existing messages. + /// Constructs a group info config object from existing data (stored from `dump()`). /// /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass - /// `std::nullopt` as the second argument. + /// `std::nullopt` as the third argument. + /// + /// Encryption keys must be loaded before the Info object can be modified or parse other Info + /// messages, and are typically loaded by providing the `Info` object to the `Keys` class. /// /// Inputs: - /// - `keys` -- contains the possible 32-byte en/decryption keys that may be used for incoming - /// messages. These are *not* Ed25519 secret keys, but rather symmetric encryption keys used - /// for encryption (generally generated using a cryptographically secure random generator). - /// The *first* key in this list will be used to encrypt outgoing config messages (and so, in - /// general, should be the most current key). There must always be at least one key present - /// (either provided at construction or via add_keys) before you can push a config. - /// Post-construction you can add or remove keys via add_key/remove_key/clear_keys from - /// ConfigBase. /// - `ed25519_pubkey` is the public key of this group, used to validate config messages. /// Config messages not signed with this key will be rejected. /// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages. @@ -55,8 +49,7 @@ class Info final : public ConfigBase { /// push config changes. /// - `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()`. - Info(const std::vector& keys, - ustring_view ed25519_pubkey, + Info(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, std::optional dumped); diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index dde05a43..b3f05179 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -63,13 +63,16 @@ class Keys final : public ConfigSig { struct key_info { std::array key; - std::chrono::system_clock::time_point timestamp; + std::chrono::system_clock::time_point timestamp; // millisecond precision int64_t generation; auto cmpval() const { return std::tie(generation, timestamp, key); } bool operator<(const key_info& b) const { return cmpval() < b.cmpval(); } + bool operator>(const key_info& b) const { return cmpval() > b.cmpval(); } + bool operator<=(const key_info& b) const { return cmpval() <= b.cmpval(); } + bool operator>=(const key_info& b) const { return cmpval() >= b.cmpval(); } bool operator==(const key_info& b) const { return cmpval() == b.cmpval(); } - bool operator!=(const key_info& b) const { return !(*this == b); } + bool operator!=(const key_info& b) const { return cmpval() != b.cmpval(); } }; /// Vector of keys that is kept sorted by generation/timestamp/key. This gets pruned as keys @@ -80,6 +83,8 @@ class Keys final : public ConfigSig { sodium_vector pending_key_config_; int64_t pending_gen_ = -1; + bool needs_dump_ = false; + ConfigMessage::verify_callable verifier_; ConfigMessage::sign_callable signer_; @@ -89,6 +94,9 @@ class Keys final : public ConfigSig { // Checks for and drops expired keys. void remove_expired(); + // Loads existing state from a previous dump of keys data + void load_dump(ustring_view dump); + public: /// The multiple of members keys we include in the message; we add junk entries to the key list /// to reach a multiple of this. 75 is chosen because it's a decently large human-round number @@ -123,22 +131,29 @@ class Keys final : public ConfigSig { /// list of encryption keys for encrypting new and decrypting existing messages. /// /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass - /// `std::nullopt` as the second argument. + /// `std::nullopt` as the last argument. /// /// Inputs: /// - `user_ed25519_secretkey` is the ed25519 secret key backing the current user's session ID, /// and is used to decrypt incoming keys. It is required. /// - `group_ed25519_pubkey` is the public key of the group, used to verify message signatures - /// on key updates. It is required. + /// on key updates. Required. Should not include the `03` prefix. /// - `group_ed25519_secretkey` is the secret key of the group, used to encrypt, decrypt, and /// sign config messages. This is only possessed by the group admin(s), and must be provided /// in order to make and push config changes. /// - `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()`. + /// - `info` and `members` -- will be loaded with the group keys, if present in the dump. + /// Otherwise, if this is an admin Keys object, with a new one constructed for the initial + /// Keys object; or with no keys loaded at all if this is a non-admin, non-dump construction. + /// (Keys will also be loaded later into this and the info/members objects, when rekey()ing or + /// loading keys via received config messages). Keys(ustring_view user_ed25519_secretkey, ustring_view group_ed25519_pubkey, std::optional group_ed25519_secretkey, - std::optional dumped); + std::optional dumped, + Info& info, + Members& members); /// API: groups/Keys::storage_namespace /// @@ -167,6 +182,9 @@ class Keys final : public ConfigSig { /// key), including a pending key if this object is in the process of pushing a new keys /// message. /// + /// This isn't typically directly needed: this object manages the key lists in the `info` and + /// `members` objects itself. + /// /// Outputs: /// - `std::vector` - vector of encryption keys. std::vector group_keys() const; @@ -247,7 +265,8 @@ class Keys final : public ConfigSig { /// would typically not change the keys, but are allowed anyway. /// /// This method should always be wrapped in a `try/catch`: if the given configuration data is - /// malformed or is not properly signed an exception will be raised. + /// malformed or is not properly signed an exception will be raised (but the Keys object remains + /// usable). /// /// Inputs: /// - `msg` - the full stored config message value @@ -265,8 +284,8 @@ class Keys final : public ConfigSig { ustring_view data, ustring_view msgid, int64_t timestamp_ms, - Members& members, - Info& info); + Info& info, + Members& members); /// API: groups/Keys::needs_rekey /// @@ -293,6 +312,33 @@ class Keys final : public ConfigSig { /// Outputs: /// - `true` if a rekey is needed, `false` otherwise. bool needs_rekey() const; + + /// API: groups/Keys::needs_dump + /// + /// Returns true if this Keys config has changes, either made directly or from incoming configs, + /// that need to be dumped to the database (made since the last call to `dump()`), false if no + /// changes have been made. + /// + /// Inputs: None + /// + /// Outputs: + /// - `true` if state needs to be dumped, `false` if state hasn't changed since the last + /// call to `dump()`. + bool needs_dump() const; + + /// API: groups/Keys::dump + /// + /// Returns a dump of the current state of this keys config that allows the Keys object to be + /// reinstantiated from scratch. + /// + /// Although this can be called at any time, it is recommended to only do so when + /// `needs_dump()` returns true. + /// + /// Inputs: None + /// + /// Outputs: opaque binary data containing the group keys and other Keys config data that can be + /// passed to the `Keys` constructor to reinitialize a Keys object with the current state. + ustring dump(); }; } // namespace session::config::groups diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 55e8a3d3..43f3ee0e 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -185,17 +185,12 @@ class Members final : public ConfigBase { /// list of encryption keys for encrypting new and decrypting existing messages. /// /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass - /// `std::nullopt` as the second argument. + /// `std::nullopt` as the third argument. + /// + /// Encryption keys must be loaded before the Info object can be modified or parse other Info + /// messages, and are typically loaded by providing the `Info` object to the `Keys` class. /// /// Inputs: - /// - `keys` -- contains the possible 32-byte en/decryption keys that may be used for incoming - /// messages (both config messages and group messages). These are *not* Ed25519 secret keys, - /// but rather symmetric encryption keys used for encryption (generally generated using a - /// cryptographically secure random generator). The *first* key in this list will be used to - /// encrypt outgoing config messages (and so, in general, should be the most current key). - /// There must always be at least one key present (either provided at construction or via - /// add_keys) before you can push a config. Post-construction you can add or remove keys via - /// add_key/remove_key/clear_keys from ConfigBase. /// - `ed25519_pubkey` is the public key of this group, used to validate config messages. /// Config messages not signed with this key will be rejected. /// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages. @@ -203,8 +198,7 @@ class Members final : public ConfigBase { /// push config changes. /// - `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()`. - Members(const std::vector& keys, - ustring_view ed25519_pubkey, + Members(ustring_view ed25519_pubkey, std::optional ed25519_secretkey, std::optional dumped); diff --git a/include/session/util.hpp b/include/session/util.hpp index cdb6fccb..322756c9 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -30,6 +30,14 @@ inline ustring_view to_unsigned_sv(std::string_view v) { inline std::string_view from_unsigned_sv(ustring_view v) { return {from_unsigned(v.data()), v.size()}; } +template +inline std::string_view from_unsigned_sv(const std::array& v) { + return {from_unsigned(v.data()), v.size()}; +} +template +inline std::string_view from_unsigned_sv(const std::vector& v) { + return {from_unsigned(v.data()), v.size()}; +} /// 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) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 066c6225..b9ecaa28 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -17,14 +17,10 @@ using namespace std::literals; using session::ustring_view; Info::Info( - const std::vector& keys, ustring_view ed25519_pubkey, std::optional ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} { - for (const auto& k : keys) - add_key(k, false); -} + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} {} std::array Info::subaccount_mask() const { return seed_hash("SessionGroupSubaccountMask"); diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 689db1a5..6a78b6fd 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -15,11 +16,17 @@ namespace session::config::groups { +static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { + return std::chrono::system_clock::time_point{milliseconds_since_epoch * 1ms}; +} + Keys::Keys( ustring_view user_ed25519_secretkey, ustring_view group_ed25519_pubkey, std::optional group_ed25519_secretkey, - std::optional dumped) { + std::optional dumped, + Info& info, + Members& members) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; @@ -34,6 +41,103 @@ Keys::Keys( init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); + + if (dumped) { + load_dump(*dumped); + } else if (_sign_sk) { + rekey(info, members); + } +} + +bool Keys::needs_dump() const { + return needs_dump_; +} + +ustring Keys::dump() { + oxenc::bt_dict_producer d; + { + auto keys = d.append_list("keys"); + for (auto& k : keys_) { + auto ki = keys.append_dict(); + // NB: Keys must be in sorted order + ki.append("g", k.generation); + ki.append("k", from_unsigned_sv(k.key)); + ki.append( + "t", + std::chrono::duration_cast( + k.timestamp.time_since_epoch()) + .count()); + } + } + + if (!pending_key_config_.empty()) { + auto pending = d.append_dict("pending"); + // NB: Keys must be in sorted order + pending.append("c", from_unsigned_sv(pending_key_config_)); + pending.append("g", pending_gen_); + pending.append("k", from_unsigned_sv(pending_key_)); + } + + needs_dump_ = false; + return ustring{to_unsigned_sv(d.view())}; +} + +void Keys::load_dump(ustring_view dump) { + oxenc::bt_dict_consumer d{from_unsigned_sv(dump)}; + + if (d.skip_until("keys")) { + auto keys = d.consume_list_consumer(); + while (!keys.is_finished()) { + auto kd = keys.consume_dict_consumer(); + auto& key = keys_.emplace_back(); + + if (!kd.skip_until("g")) + throw config_value_error{"Invalid Keys dump: found key without generation (g)"}; + key.generation = kd.consume_integer(); + + if (!kd.skip_until("k")) + throw config_value_error{"Invalid Keys dump: found key without key bytes (k)"}; + auto key_bytes = kd.consume_string_view(); + if (key_bytes.size() != key.key.size()) + throw config_value_error{ + "Invalid Keys dump: found key with invalid size (" + + std::to_string(key_bytes.size()) + ")"}; + std::memcpy(key.key.data(), key_bytes.data(), key.key.size()); + + if (!kd.skip_until("t")) + throw config_value_error{"Invalid Keys dump: found key without timestamp (t)"}; + key.timestamp = sys_time_from_ms(kd.consume_integer()); + + if (keys_.size() > 1 && *std::prev(keys_.end(), 2) >= key) + throw config_value_error{"Invalid Keys dump: keys are not in proper sorted order"}; + } + } else { + throw config_value_error{"Invalid Keys dump: `keys` not found"}; + } + + if (d.skip_until("pending")) { + auto pending = d.consume_dict_consumer(); + + if (!pending.skip_until("c")) + throw config_value_error{"Invalid Keys dump: found pending without config (c)"}; + auto pc = pending.consume_string_view(); + pending_key_config_.clear(); + pending_key_config_.resize(pc.size()); + std::memcpy(pending_key_config_.data(), pc.data(), pc.size()); + + if (!pending.skip_until("g")) + throw config_value_error{"Invalid Keys dump: found pending without generation (g)"}; + pending_gen_ = pending.consume_integer(); + + if (!pending.skip_until("k")) + throw config_value_error{"Invalid Keys dump: found pending without key (k)"}; + auto pk = pending.consume_string_view(); + if (pk.size() != pending_key_.size()) + throw config_value_error{ + "Invalid Keys dump: found pending key (k) with invalid size (" + + std::to_string(pk.size()) + ")"}; + std::memcpy(pending_key_.data(), pk.data(), pending_key_.size()); + } } std::vector Keys::group_keys() const { @@ -237,6 +341,8 @@ 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; + return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } @@ -247,7 +353,7 @@ std::optional Keys::pending_config() const { } void Keys::load_key_message( - ustring_view data, ustring_view msgid, int64_t timestamp_ms, Members& members, Info& info) { + ustring_view data, ustring_view msgid, int64_t timestamp_ms, Info& info, Members& members) { oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; @@ -272,8 +378,7 @@ void Keys::load_key_message( bool found_key = false; sodium_cleared new_key{}; - new_key.timestamp = std::chrono::system_clock::from_time_t(timestamp_ms / 1000) + - 1ms * (timestamp_ms % 1000); + new_key.timestamp = sys_time_from_ms(timestamp_ms); if (!d.skip_until("G")) throw config_value_error{"Key message missing required generation (G) field"}; @@ -380,14 +485,18 @@ void Keys::load_key_message( keys_.insert(it, new_key); remove_expired(); + + needs_dump_ = true; } } // If this is our pending config or this has a later generation than our pending config then // drop our pending status. if (!pending_key_config_.empty() && - (new_key.generation > pending_gen_ || new_key.key == pending_key_)) + (new_key.generation > pending_gen_ || new_key.key == pending_key_)) { pending_key_config_.clear(); + needs_dump_ = true; + } auto new_key_list = group_keys(); members.replace_keys(new_key_list, /*dirty=*/false); diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 32980bac..fffbcf99 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -7,14 +7,10 @@ namespace session::config::groups { Members::Members( - const std::vector& keys, ustring_view ed25519_pubkey, std::optional ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} { - for (const auto& k : keys) - add_key(k, false); -} + 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/util.cpp b/src/util.cpp index 29cec1df..8a4d5b44 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -13,4 +13,10 @@ void sodium_buffer_deallocate(void* p) { if (p) sodium_free(p); } + +void sodium_zero_buffer(void* ptr, size_t size) { + if (ptr) + sodium_memzero(ptr, size); +} + } // namespace session diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1cba0814..dbf39f45 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(testAll test_config_contacts.cpp test_config_convo_info_volatile.cpp test_encrypt.cpp + test_group_keys.cpp test_group_info.cpp test_group_members.cpp test_xed25519.cpp diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index b8013d0f..eac73373 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -33,14 +33,22 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { std::vector enc_keys{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; - groups::Info ginfo1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + // This is just for testing: normally you don't load keys manually but just make a groups::Keys + // object that loads the keys into the Members object for you. + for (const auto& k : enc_keys) + ginfo1.add_key(k, false); enc_keys.insert( enc_keys.begin(), "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Info ginfo2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys) // Just for testing, as above. + ginfo2.add_key(k, false); ginfo1.set_name("GROUP Name"); CHECK(ginfo1.is_dirty()); @@ -148,7 +156,10 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); // This Info object has only the public key, not the priv key, and so cannot modify things: - groups::Info ginfo{view_vec(enc_keys1), to_usv(ed_pk), std::nullopt, std::nullopt}; + groups::Info ginfo{to_usv(ed_pk), std::nullopt, std::nullopt}; + + for (const auto& k : enc_keys1) // Just for testing, as above. + ginfo.add_key(k, false); REQUIRE_THROWS_WITH( ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); @@ -157,7 +168,10 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: - groups::Info ginfo_rw{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys1) // Just for testing, as above. + ginfo_rw.add_key(k, false); ginfo_rw.set_name("Super Group!!"); CHECK(ginfo_rw.is_dirty()); @@ -177,7 +191,11 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.merge(merge_configs) == 1); CHECK_FALSE(ginfo.needs_push()); - groups::Info ginfo_rw2{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys1) // Just for testing, as above. + ginfo_rw2.add_key(k, false); + CHECK(ginfo_rw2.merge(merge_configs) == 1); CHECK_FALSE(ginfo.needs_push()); @@ -200,7 +218,11 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { ed_sk_bad1.data(), reinterpret_cast(seed_bad1.data())); - groups::Info ginfo_bad1{view_vec(enc_keys1), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_bad1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys1) // Just for testing, as above. + ginfo_bad1.add_key(k, false); + ginfo_bad1.merge(merge_configs); ginfo_bad1.set_sig_keys(to_usv(ed_sk_bad1)); ginfo_bad1.set_name("Bad name, BAD!"); @@ -279,7 +301,10 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.needs_dump()); auto dump = ginfo.dump(); - groups::Info ginfo2{view_vec(enc_keys1), to_usv(ed_pk), std::nullopt, dump}; + groups::Info ginfo2{to_usv(ed_pk), std::nullopt, dump}; + + for (const auto& k : enc_keys1) // Just for testing, as above. + ginfo2.add_key(k, false); CHECK(!ginfo.needs_dump()); CHECK(!ginfo2.needs_dump()); @@ -294,7 +319,11 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(o5.empty()); // This account has a different primary decryption key - groups::Info ginfo_rw3{view_vec(enc_keys2), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw3{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys2) // Just for testing, as above. + ginfo_rw3.add_key(k, false); + CHECK(ginfo_rw3.merge(merge_configs) == 1); CHECK(ginfo_rw3.get_name() == "Super Group 2"); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp new file mode 100644 index 00000000..2750e547 --- /dev/null +++ b/tests/test_group_keys.cpp @@ -0,0 +1,75 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +static constexpr int64_t created_ts = 1680064059; + +using namespace session::config; + +TEST_CASE("Group Keys", "[config][groups][keys]") { + + const std::array seeds = { + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes, + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes, + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes, + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes}; + std::array, seeds.size()> ed_pk; + std::array, seeds.size()> ed_sk; + for (size_t i = 0; i < seeds.size(); i++) { + crypto_sign_ed25519_seed_keypair( + ed_pk[i].data(), + ed_sk[i].data(), + reinterpret_cast(seeds[i].data())); + CHECK(oxenc::to_hex(seeds[i].begin(), seeds[i].end()) == + oxenc::to_hex(ed_sk[i].begin(), ed_sk[i].begin() + 32)); + } + + constexpr size_t ADMIN1 = 0, ADMIN2 = 1, MEMBER1 = 2, MEMBER2 = 3, GROUP = 4; + + REQUIRE(oxenc::to_hex(ed_pk[ADMIN1].begin(), ed_pk[ADMIN1].end()) == + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); + REQUIRE(oxenc::to_hex(ed_pk[ADMIN2].begin(), ed_pk[ADMIN2].end()) == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + REQUIRE(oxenc::to_hex(ed_pk[MEMBER1].begin(), ed_pk[MEMBER1].end()) == + "8b79719da06ee8a14823f0c8d740aabb134ab7cbc174b8c1a022a27c0964abfd"); + REQUIRE(oxenc::to_hex(ed_pk[MEMBER2].begin(), ed_pk[MEMBER2].end()) == + "d813a070116a8c74e6fcbb3f53d5698a14b6236fcca9bb3136acff749dacdcc4"); + REQUIRE(oxenc::to_hex(ed_pk[GROUP].begin(), ed_pk[GROUP].end()) == + "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); + + std::array, seeds.size() - 1> info; + std::array, seeds.size() - 1> members; + std::array, seeds.size() - 1> keys; + for (size_t i = 0; i < GROUP; i++) { + info[i] = std::make_unique( + to_usv(ed_pk[GROUP]), + i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + std::nullopt); + members[i] = std::make_unique( + to_usv(ed_pk[GROUP]), + i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + std::nullopt); + keys[i] = std::make_unique( + to_usv(ed_sk[i]), + to_usv(ed_pk[GROUP]), + i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + std::nullopt, + *info[i], + *members[i] + ); + } +} diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 974afb94..24afaf49 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -43,14 +42,22 @@ TEST_CASE("Group Members", "[config][groups][members]") { std::vector enc_keys{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; - groups::Members gmem1{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Members gmem1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + // This is just for testing: normally you don't load keys manually but just make a groups::Keys + // object that loads the keys into the Members object for you. + for (const auto& k : enc_keys) + gmem1.add_key(k, false); enc_keys.insert( enc_keys.begin(), "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Members gmem2{view_vec(enc_keys), to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Members gmem2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + + for (const auto& k : enc_keys) // Just for testing, as above. + gmem2.add_key(k, false); std::vector sids; while (sids.size() < 256) { From dddc5b332cf0bb6d335c81b95582620b0d3e4de4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 21:42:31 -0300 Subject: [PATCH 013/572] Doc CI fix --- .drone.jsonnet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 4876ce13..e33f6d4a 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -285,9 +285,9 @@ local static_build(name, 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', apt_get_quiet + ' install -y python3-requests rsync', - 'npm i docsify-cli -g', - 'npm i docsify -g', + 'npm i docsify-cli docsify-themeable docsify-katex@1.4.4 katex marked@4', 'cd docs/api/', + 'export NODE_PATH=node_modules', 'make', '../../utils/ci/drone-docs-upload.sh', ], From f929e79ae1b873347fd6e72dae48e66d264ca019 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 21 Aug 2023 23:08:01 -0300 Subject: [PATCH 014/572] Doc fixes --- docs/api/api-to-markdown.py | 12 +++++++-- docs/api/static/sidebar.md | 1 + external/oxen-encoding | 2 +- include/session/config/base.h | 2 ++ include/session/config/base.hpp | 2 ++ include/session/config/groups/info.hpp | 2 ++ include/session/config/groups/keys.hpp | 7 +++-- include/session/config/groups/members.hpp | 31 +++++++++++++++++++++++ 8 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/api/api-to-markdown.py b/docs/api/api-to-markdown.py index 4a6aa9c0..14ec7ef3 100755 --- a/docs/api/api-to-markdown.py +++ b/docs/api/api-to-markdown.py @@ -47,6 +47,7 @@ # # "Inputs: none." # "Outputs: none." +# "Member variable." # "Inputs:" followed by markdown (typically an unordered list) until the next match from this list. # "Outputs:" followed by markdown # "Example input:" followed by a code block (i.e. containing json) @@ -91,6 +92,7 @@ DEV_RPC_START = re.compile(r"^Dev-API:\s*([\w/:]+)(.*)$") IN_NONE = re.compile(r"^Inputs?: *[nN]one\.?$") IN_SOME = re.compile(r"^Inputs?:\s*$") +MEMBER_VAR = re.compile(r"^Member +[vV]ar(?:iable)?\.?$") DECL_SOME = re.compile(r"^Declaration?:\s*$") OUT_SOME = re.compile(r"^Outputs?:\s*$") EXAMPLE_IN = re.compile(r"^Example [iI]nputs?:\s*$") @@ -159,6 +161,7 @@ class Parsing(Enum): description, decl, inputs, outputs = "", "", "", "" done_desc = False no_inputs = False + member_var = False examples = [] cur_ex_in = None old_names = [] @@ -194,6 +197,9 @@ class Parsing(Enum): error("found multiple Inputs:") inputs, no_inputs, mode = MD_NO_INPUT, True, Parsing.NONE + elif re.search(MEMBER_VAR, line): + member_var, no_inputs, mode = True, True, Parsing.DESC + elif re.search(DECL_SOME, line): if inputs: error("found multiple Syntax:") @@ -285,7 +291,7 @@ class Parsing(Enum): # We hit the end of the commented section if not description or inputs.isspace(): problems.append("endpoint has no description") - if not inputs or inputs.isspace(): + if (not inputs or inputs.isspace()) and not member_var: problems.append( "endpoint has no inputs description; perhaps you need to add 'Inputs: none.'?" ) @@ -321,7 +327,9 @@ class Parsing(Enum): {MD_DECL_HEADER} {decl} - +""" + if not member_var: + md = md + f""" {MD_INPUT_HEADER} {inputs} diff --git a/docs/api/static/sidebar.md b/docs/api/static/sidebar.md index e622fe5b..569af33b 100644 --- a/docs/api/static/sidebar.md +++ b/docs/api/static/sidebar.md @@ -4,6 +4,7 @@ - [Convo Info Volatile](convo_info_volatile.md) - [Encrypt](encrypt.md) - [Error](error.md) +- [Groups](groups.md) - [User Groups](user_groups.md) - [User Profile](user_profile.md) - [Utils](util.md) diff --git a/external/oxen-encoding b/external/oxen-encoding index 3bca3ac2..fc85dfd3 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit 3bca3ac22dac31258a4dd158e1e6568aa2315c75 +Subproject commit fc85dfd352e8474bc7195b0ba881838bd72ebea6 diff --git a/include/session/config/base.h b/include/session/config/base.h index 522445f1..27e108d5 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -486,6 +486,8 @@ LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned /// 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); diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 66c0e307..b4f9511b 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -119,6 +119,8 @@ class ConfigSig { /// /// Returns a const reference to the 32-byte Ed25519 signing pubkey, if set. /// + /// Inputs: none. + /// /// Outputs: /// - reference to the 32-byte pubkey, or `std::nullopt` if not set. const std::optional>& get_sig_pubkey() const { return _sign_pk; } diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 6568a616..f02d4ef1 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -261,6 +261,8 @@ class Info final : public ConfigBase { /// Returns true if this group has been marked destroyed; the receiving client is expected to /// delete it. /// + /// Inputs: none. + /// /// Outputs: /// - `true` if the group has been destroyed, `false` otherwise. bool is_destroyed() const; diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index b3f05179..71c35445 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -185,6 +185,8 @@ class Keys final : public ConfigSig { /// This isn't typically directly needed: this object manages the key lists in the `info` and /// `members` objects itself. /// + /// Inputs: none. + /// /// Outputs: /// - `std::vector` - vector of encryption keys. std::vector group_keys() const; @@ -336,8 +338,9 @@ class Keys final : public ConfigSig { /// /// Inputs: None /// - /// Outputs: opaque binary data containing the group keys and other Keys config data that can be - /// passed to the `Keys` constructor to reinitialize a Keys object with the current state. + /// Outputs: + /// - opaque binary data containing the group keys and other Keys config data that can be passed + /// to the `Keys` constructor to reinitialize a Keys object with the current state. ustring dump(); }; diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 43f3ee0e..7513ab72 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -45,17 +45,23 @@ struct member { /// API: groups/member::session_id /// + /// Member variable + /// /// The member's session ID, in hex. std::string session_id; /// API: groups/member::name /// + /// Member variable + /// /// The member's human-readable name. Optional. This is used by other members of the group to /// display a member's details before having seen a message from that member. std::string name; /// API: groups/member::profile_picture /// + /// Member variable + /// /// The member's profile picture (URL & decryption key). Optional. This is used by other /// members of the group to display a member's details before having seen a message from that /// member. @@ -63,6 +69,8 @@ struct member { /// API: groups/member::admin /// + /// Member variable + /// /// Flag that is set to indicate to the group that this member is an admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess @@ -83,12 +91,18 @@ struct member { /// to the group. The optional `failed` parameter can be specified as true if the invitation /// was issued but failed to send for some reason (this is intended as a signal to other clients /// that the invitation should be reissued). + /// + /// Inputs: + /// - `failed` can be specified and set to `true` to the invite status to "failed-to-send"; + /// otherwise omitting it or giving as `false` sets the invite status to "sent." void set_invited(bool failed = false) { invite_status = failed ? INVITE_FAILED : INVITE_SENT; } /// API: groups/members::set_accepted /// /// This clears the "invited" flag for this user, thus indicating that the user has accepted an /// invitation and is now a regular member of the group. + /// + /// Inputs: none void set_accepted() { invite_status = 0; } /// API: groups/member::invite_pending @@ -96,6 +110,8 @@ struct member { /// Returns whether the user currently has a pending invitation. Returns true if so (whether or /// not that invitation has failed). /// + /// Inputs: none + /// /// Outputs: /// - `bool` -- true if the user has a pending invitation, false otherwise. bool invite_pending() const { return invite_status > 0; } @@ -105,6 +121,8 @@ struct member { /// Returns true if the user has a pending invitation that is marked as failed (and thus should /// be re-sent). /// + /// Inputs: none + /// /// Outputs: /// - `bool` -- true if the user has a failed pending invitation bool invite_failed() const { return invite_status == INVITE_FAILED; } @@ -121,6 +139,10 @@ struct member { /// to other clients that the promotion should be reissued). /// /// Note that this flag is ignored when the `admin` field is set to true. + /// + /// Inputs: + /// - `failed`: can be specified as true to mark the promotion status as "failed-to-send". If + /// omitted or false then the promotion status is set to "sent". void set_promoted(bool failed = false) { promotion_status = failed ? INVITE_FAILED : INVITE_SENT; } @@ -130,6 +152,8 @@ struct member { /// Returns whether the user currently has a pending invitation/promotion to admin status. /// Returns true if so (whether or not that invitation has failed). /// + /// Inputs: None + /// /// Outputs: /// - `bool` -- true if the user has a pending promotion, false otherwise. bool promotion_pending() const { return !admin && promotion_status > 0; } @@ -139,6 +163,8 @@ struct member { /// Returns true if the user has a pending promotion-to-admin that is marked as failed (and thus /// should be re-sent). /// + /// Inputs: None + /// /// Outputs: /// - `bool` -- true if the user has a failed pending promotion bool promotion_failed() const { return !admin && promotion_status == INVITE_FAILED; } @@ -146,6 +172,11 @@ struct member { /// API: groups/member::promoted /// /// Returns true if the user is already an admin *or* has a pending promotion to admin. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `bool` -- true if the member is promoted (or promotion-in-progress) bool promoted() const { return admin || promotion_pending(); } /// API: groups/member::info From cb40a14c9d9a20a1c5db7c0e1d6a8b0d1cb922cd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 22 Aug 2023 13:21:07 -0300 Subject: [PATCH 015/572] Add dedicated namespace for messages; rearrange config namespace values Messages to a new group can't go into 0 (since that is publicly depositable), so reserve a namespace (11) for messages. --- include/session/config/namespaces.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 1fc58c42..f7d48469 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -10,10 +10,12 @@ enum class Namespace : std::int16_t { ConvoInfoVolatile = 4, UserGroups = 5, - // Groups namespaces (i.e. for config of the group itself, not one user's group settings) - GroupInfo = 11, - GroupMembers = 12, - GroupKeys = 13, + // Messages sent to a closed group: + GroupMessages = 11, + // Groups config namespaces (i.e. for shared config of the group itself, not one user's group settings) + GroupKeys = 12, + GroupInfo = 13, + GroupMembers = 14, }; } // namespace session::config From 60cbeca4b42c9ecc1363585e2ae399b47962fd99 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 22 Aug 2023 16:29:15 -0300 Subject: [PATCH 016/572] Add group message encryption + compression Adds method for encrypting/decrypting a message. This supports both compressed+encrypted and just plain encrypted. Abstracts the zstd compression implementation from base.cpp into internal.cpp, and uses it inside the new group message encryption Compression is only used if beneficial (that is, only if compression actually reduces the message size). --- include/session/config/groups/keys.hpp | 62 ++++++++++++++++ src/config/base.cpp | 41 ++--------- src/config/groups/keys.cpp | 97 ++++++++++++++++++++++++++ src/config/internal.cpp | 53 ++++++++++++++ src/config/internal.hpp | 7 ++ 5 files changed, 226 insertions(+), 34 deletions(-) diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 71c35445..bd271468 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -191,6 +191,20 @@ class Keys final : public ConfigSig { /// - `std::vector` - vector of encryption keys. std::vector group_keys() const; + /// API: groups/Keys::encryption_key + /// + /// Accesses the current encryption key: that is, the most current group decryption key. Throws + /// if there are no encryption keys at all. (This is essentially the same as `group_keys()[0]`, + /// except for the throwing and avoiding needing to constructor a vector). + /// + /// You normally don't need to call this; you can just use encrypt_message() instead. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `ustring_view` of the most current group encryption key. + ustring_view group_enc_key() const; + /// API: groups/Keys::rekey /// /// Generate a new encryption key for the group and returns an encrypted key message to be @@ -342,6 +356,54 @@ class Keys final : public ConfigSig { /// - opaque binary data containing the group keys and other Keys config data that can be passed /// to the `Keys` constructor to reinitialize a Keys object with the current state. ustring dump(); + + /// API: groups/Keys::encrypt_message + /// + /// Encrypts group message content; this is passed a binary value to encrypt and + /// encodes/encrypts it for the group using the latest encryption key this object knows about. + /// Such encrypted messages are intended to be passed to `decrypt_message` to decrypt them. + /// + /// The current implementation uses XChaCha20-Poly1305 and returns an encoded value where the + /// first byte indicates the encryption type ('x', or 'X' currently for uncompressed or + /// compressed XChaCha20), the next 24 bytes are the encryption nonce, and the remainder is the + /// ciphertext. The returned value will be 41 bytes larger than the plaintext, at most + /// (potentially less if compression is permitted). + /// + /// When compression is enabled (by omitting the `compress` argument or specifying it as true) + /// then ZSTD compression will be *attempted* on the plaintext message and will be used if the + /// compressed data is smaller than the uncompressed data. If disabled, or if compression does + /// not reduce the size (i.e. because it is not compressible), then the message will not be + /// compressed. + /// + /// Future versions may change this to support other encryption algorithms. + /// + /// This method will throw if there no encryption keys are available at all (which should not + /// occur in normal use). + /// + /// Inputs: + /// - `plaintext` -- the binary message to encrypt. + /// - `compress` -- can be specified as `false` to forcibly disable compression. Normally + /// omitted, to use compression if and only if it reduces the size. + /// + /// Outputs: + /// - `ciphertext` -- the encrypted ciphertext of the message + ustring encrypt_message(ustring_view plaintext, bool compress = true) const; + + /// API: groups/Keys::decrypt_message + /// + /// Decrypts group message content that was presumably encrypted with `encrypt_message`. This + /// will attempt decryption using *all* of the known group encryption keys and, if necessary, + /// decompressing the message. + /// + /// Inputs: + /// - `ciphertext` -- a encoded, encrypted, (possibly) compressed message as produced by + /// `encrypt_message()`. + /// + /// Outputs: + /// - `std::optional` -- the decrypted, decompressed plaintext message if encryption + /// and decompression succeeds; otherwise returns `std::nullopt` if parsing, decryption, or + /// decompression fails. + std::optional decrypt_message(ustring_view ciphertext) const; }; } // namespace session::config::groups diff --git a/src/config/base.cpp b/src/config/base.cpp index 6b6d2e70..3fac5f09 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -5,12 +5,12 @@ #include #include #include -#include #include #include #include +#include "internal.hpp" #include "session/config/base.h" #include "session/config/encrypt.hpp" #include "session/export.h" @@ -133,33 +133,13 @@ int ConfigBase::merge(const std::vector>& c // 'z' prefix indicates zstd-compressed data: if (plain[0] == 'z') { - struct zstd_decomp_freer { - void operator()(ZSTD_DStream* z) const { ZSTD_freeDStream(z); } - }; - std::unique_ptr z_decompressor{ZSTD_createDStream()}; - auto* zds = z_decompressor.get(); - - ZSTD_initDStream(zds); - ZSTD_inBuffer input{/*.src=*/plain.data() + 1, /*.size=*/plain.size() - 1, /*.pos=*/0}; - unsigned char out_buf[4096]; - ZSTD_outBuffer output{/*.dst=*/out_buf, /*.size=*/sizeof(out_buf)}; - bool failed = false; - size_t ret; - ustring decompressed; - do { - output.pos = 0; - ret = ZSTD_decompressStream(zds, &output, &input); - if (ZSTD_isError(ret)) { - failed = true; - break; - } - decompressed += ustring_view{out_buf, output.pos}; - } while (ret > 0 || input.pos < input.size); - if (failed || decompressed.empty()) { + if (auto decompressed = zstd_decompress(ustring_view{plain.data() + 1, plain.size() - 1}); + decompressed && !decompressed->empty()) + plain = std::move(*decompressed); + else { log(LogLevel::warning, "Invalid config message: decompression failed"); continue; } - plain = std::move(decompressed); } if (plain[0] != 'd') @@ -252,15 +232,8 @@ bool ConfigBase::needs_push() const { void compress_message(ustring& msg, int level) { if (!level) return; - ustring compressed; - compressed.resize(1 + ZSTD_compressBound(msg.size())); - compressed[0] = 'z'; // our zstd compression marker prefix byte - auto size = ZSTD_compress( - compressed.data() + 1, compressed.size() - 1, msg.data(), msg.size(), level); - if (ZSTD_isError(size)) - throw std::runtime_error{ - "Unable to compress message: " + std::string{ZSTD_getErrorName(size)}}; - compressed.resize(size + 1); + // "z" is our zstd compression marker prefix byte + ustring compressed = zstd_compress(msg, level, to_unsigned_sv("z"sv)); if (compressed.size() < msg.size()) msg = std::move(compressed); } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 6a78b6fd..c1fe603c 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -153,6 +153,14 @@ std::vector Keys::group_keys() const { return ret; } +ustring_view Keys::group_enc_key() const { + if (keys_.empty()) + throw std::runtime_error{"group_enc_key failed: Keys object has no keys at all!"}; + + auto& key = keys_.back().key; + return {key.data(), key.size()}; +} + static std::array compute_xpk(const unsigned char* ed25519_pk) { std::array xpk; if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) @@ -557,4 +565,93 @@ std::optional Keys::pending_key() const { return std::nullopt; } +static constexpr size_t OVERHEAD = 1 // encryption type indicator + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + + crypto_aead_xchacha20poly1305_ietf_ABYTES; + +ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { + ustring _compressed; + if (compress) { + _compressed = zstd_compress(plaintext); + if (_compressed.size() < plaintext.size()) + plaintext = _compressed; + else { + _compressed.clear(); + compress = false; + } + } + + ustring ciphertext; + ciphertext.resize(OVERHEAD + plaintext.size()); + ciphertext[0] = compress ? 'X' : 'x'; + randombytes_buf(ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ustring_view nonce{ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( + ciphertext.data() + 1 + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, + plaintext.data(), + plaintext.size(), + nullptr, + 0, + nullptr, + nonce.data(), + group_enc_key().data())) + throw std::runtime_error{"Encryption failed"}; + + return ciphertext; +} + +std::optional Keys::decrypt_message(ustring_view ciphertext) const { + if (ciphertext.size() < OVERHEAD) + return std::nullopt; + + ustring plain; + + bool success = false; + bool compressed = false; + char type = static_cast(ciphertext[0]); + ciphertext.remove_prefix(1); + switch (type) { + case 'X': compressed = true; [[fallthrough]]; + case 'x': { + auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext.remove_prefix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + plain.resize(ciphertext.size() - OVERHEAD); + for (auto& k : keys_) { + if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + plain.data(), + nullptr, + nullptr, + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, + nonce.data(), + k.key.data())) { + success = true; + break; + } + } + break; + } + + default: + // Don't know how to handle this type (or it's garbage) + return std::nullopt; + } + + if (!success) // none of the keys worked + return std::nullopt; + + if (compressed) { + if (auto decomp = zstd_decompress(plain)) + plain = std::move(*decomp); + else + // Decompression failed + return std::nullopt; + } + + return std::move(plain); +} + } // namespace session::config::groups diff --git a/src/config/internal.cpp b/src/config/internal.cpp index a6ac18bb..a84d3689 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -173,4 +174,56 @@ void load_unknowns( } } +namespace { + struct zstd_decomp_freer { + void operator()(ZSTD_DStream* z) const { ZSTD_freeDStream(z); } + }; + + using zstd_decomp_ptr = std::unique_ptr; +} // namespace + +ustring zstd_compress(ustring_view data, int level, ustring_view prefix) { + ustring compressed; + if (prefix.empty()) + compressed.resize(ZSTD_compressBound(data.size())); + else { + compressed.resize(prefix.size() + ZSTD_compressBound(data.size())); + compressed.replace(0, prefix.size(), prefix); + } + auto size = ZSTD_compress( + compressed.data() + prefix.size(), + compressed.size() - prefix.size(), + data.data(), + data.size(), + level); + if (ZSTD_isError(size)) + throw std::runtime_error{"Compression failed: " + std::string{ZSTD_getErrorName(size)}}; + + compressed.resize(prefix.size() + size); + return compressed; +} + +std::optional zstd_decompress(ustring_view data) { + zstd_decomp_ptr z_decompressor{ZSTD_createDStream()}; + auto* zds = z_decompressor.get(); + + ZSTD_initDStream(zds); + ZSTD_inBuffer input{/*.src=*/data.data(), /*.size=*/data.size(), /*.pos=*/0}; + std::array out_buf; + ZSTD_outBuffer output{/*.dst=*/out_buf.data(), /*.size=*/out_buf.size()}; + + ustring decompressed; + + size_t ret; + do { + output.pos = 0; + if (ret = ZSTD_decompressStream(zds, &output, &input); ZSTD_isError(ret)) + return std::nullopt; + + decompressed.append(out_buf.data(), output.pos); + } while (ret > 0 || input.pos < input.size); + + return decompressed; +} + } // namespace session::config diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 7b9d92e7..5be9a601 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -134,4 +134,11 @@ void load_unknowns( std::string_view previous, std::string_view until); +/// ZSTD-compresses a value. `prefix` can be prepended on the returned value, if needed. Throws on +/// serious error. +ustring zstd_compress(ustring_view data, int level = 1, ustring_view prefix = {}); + +/// ZSTD-decompresses a value. Returns nullopt if decompression fails. +std::optional zstd_decompress(ustring_view data); + } // namespace session::config From 286243cc22f0901b75d5a06957bc4f494cce3836 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 22 Aug 2023 16:31:20 -0300 Subject: [PATCH 017/572] Revert me -- disable broken group keys test --- tests/test_group_keys.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 2750e547..07897171 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -19,6 +19,7 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; +/* TEST_CASE("Group Keys", "[config][groups][keys]") { const std::array seeds = { @@ -73,3 +74,4 @@ TEST_CASE("Group Keys", "[config][groups][keys]") { ); } } +*/ From 95aeea6cf4733f9e266ef09f426e05c533871652 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 22 Aug 2023 17:25:00 -0300 Subject: [PATCH 018/572] groups::Info C API --- include/session/config/groups/info.h | 282 ++++++++++----------------- src/config/groups/info.cpp | 261 ++++++++++++++++++++++++- 2 files changed, 365 insertions(+), 178 deletions(-) diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index d5326192..9fb4129b 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -8,31 +8,18 @@ extern "C" { #include "../profile_pic.h" #include "../util.h" -/// API: groups/group_info_init +/// 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`). /// -/// Declaration: -/// ```cpp -/// INT group_info_init( -/// [out] config_object** conf, -/// [in] const unsigned char** keys, -/// [in] size_t keylen, -/// [in] const unsigned char* dump, -/// [in] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// /// Inputs: /// - `conf` -- [out] Pointer to the config object -/// - `keys` -- pointer to the beginning of an array of 32-byte encryption/decryption keys for this -/// group info. These should be specified in most-recent-to-least-recent order; the *first* key -/// will be the one used for encryption when pushing an update. -/// - `keylen` -- the number of the `keys` array +/// - `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. @@ -43,226 +30,171 @@ extern "C" { /// 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( +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: 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. +/// API: groups_info/groups_info_get_name /// -/// Declaration: -/// ```cpp -/// BOOL contacts_get( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` +/// 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 -/// - `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)); +/// +/// 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); -/// API: contacts/contacts_get_or_construct +/// API: groups_info/groups_info_set_name /// -/// 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. +/// 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). /// -/// 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. +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `name` -- [in] Pointer to the name as a null-terminated C string /// -/// 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(). +/// Outputs: +/// - `int` -- Returns 0 on success, non-zero on error +LIBSESSION_EXPORT int groups_info_set_name(config_object* conf, const char* name); + +/// API: groups_info/groups_info_get_pic /// -/// Declaration: -/// ```cpp -/// BOOL contacts_get_or_construct( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` +/// 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 -/// - `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. +/// 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); + +/// API: groups_info/groups_info_set_pic /// -/// Declaration: -/// ```cpp -/// VOID contacts_set( -/// [in, out] config_object* conf, -/// [in] const contacts_contact* contact -/// ); -/// ``` +/// Sets a user profile /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object -/// - `contact` -- [in] Pointer containing the contact info data +/// - `conf` -- [in] Pointer to the config object +/// - `pic` -- [in] Pointer to the pic /// -/// 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! -// } +/// Outputs: +/// - `int` -- Returns 0 on success, non-zero on error +LIBSESSION_EXPORT int groups_info_set_pic(config_object* conf, user_profile_pic pic); -/// 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. +/// API: groups_info/groups_info_get_expiry_timer /// -/// Declaration: -/// ```cpp -/// BOOL contacts_erase( -/// [in, out] config_object* conf, -/// [in] const char* session_id -/// ); -/// ``` +/// Gets the group's message expiry timer (seconds). Returns 0 if not set. /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object -/// - `session_id` -- [in] Text containing null terminated hex string +/// - `conf` -- [in] Pointer to the config object /// /// Outputs: -/// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool contacts_erase(config_object* conf, const char* session_id); +/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set +LIBSESSION_EXPORT int groups_info_get_expiry_timer(const config_object* conf); -/// API: contacts/contacts_size +/// 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. /// -/// Returns the number of contacts. +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `expiry` -- [in] Integer of the expiry timer in seconds +LIBSESSION_EXPORT void groups_info_set_expiry_timer(config_object* conf, int expiry); + +/// API: groups_info/groups_info_get_created /// -/// Declaration: -/// ```cpp -/// SIZE_T contacts_size( -/// [in] const config_object* conf -/// ); -/// ``` +/// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. /// /// Inputs: -/// - `conf` -- input - Pointer to the config object +/// - `conf` -- [in] Pointer to the config object /// /// Outputs: -/// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); +/// - `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); -typedef struct contacts_iterator { - void* _internals; -} contacts_iterator; +/// 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_EXPORT void groups_info_set_created(config_object* conf, int64_t ts); -/// API: contacts/contacts_iterator_new +/// API: groups_info/groups_info_get_delete_before /// -/// Starts a new iterator. +/// 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. /// -/// Functions for iterating through the entire contact list, in sorted order. Intended use is: +/// Inputs: +/// - `conf` -- [in] Pointer to the config object /// -/// 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); +/// 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); + +/// API: groups_info/groups_info_set_delete_before /// -/// It is NOT permitted to add/remove/modify records while iterating. +/// Sets the delete-before time (unix timestamp, in seconds) before which messages should be delete. +/// Setting 0 clears the value. /// -/// Declaration: -/// ```cpp -/// CONTACTS_ITERATOR* contacts_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` +/// Inputs: +/// - `conf` -- [in] Pointer to the config 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); + +/// 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: -/// - `contacts_iterator*` -- pointer to the iterator -LIBSESSION_EXPORT contacts_iterator* contacts_iterator_new(const config_object* conf); +/// - `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); -/// API: contacts/contacts_iterator_free +/// API: groups_info/groups_info_set_attach_delete_before /// -/// Frees an iterator once no longer needed. -/// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_free( -/// [in] contacts_iterator* it -/// ); -/// ``` +/// 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: -/// - `it` -- [in] Pointer to the contacts_iterator -LIBSESSION_EXPORT void contacts_iterator_free(contacts_iterator* it); +/// - `conf` -- [in] Pointer to the config 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); -/// API: contacts/contacts_iterator_done +/// API: groups_info/groups_info_is_destroyed(const config_object* conf); /// -/// 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 -/// ); -/// ``` +/// 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: -/// - `it` -- [in] Pointer to the contacts_iterator -/// - `c` -- [out] Pointer to the contact, will be populated if false +/// - `conf` -- [in] Pointer to the config object /// /// Outputs: -/// - `bool` -- True if iteration has reached the end -LIBSESSION_EXPORT bool contacts_iterator_done(contacts_iterator* it, contacts_contact* c); +/// - `true` if the group has been nuked, `false` otherwise. +LIBSESSION_EXPORT bool groups_info_is_destroyed(const config_object* conf); -/// API: contacts/contacts_iterator_advance +/// API: groups_info/groups_info_destroy_group(const config_object* conf); /// -/// Advances the iterator. -/// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_advance( -/// [in] contacts_iterator* it -/// ); -/// ``` +/// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). /// /// Inputs: -/// - `it` -- [in] Pointer to the contacts_iterator -LIBSESSION_EXPORT void contacts_iterator_advance(contacts_iterator* it); +/// - `conf` -- [in] Pointer to the config object +LIBSESSION_EXPORT void groups_info_destroy_group(config_object* conf); #ifdef __cplusplus } // extern "C" diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index b9ecaa28..f1a67ff2 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -7,14 +7,14 @@ #include "../internal.hpp" #include "session/config/error.h" +#include "session/config/groups/info.h" #include "session/export.h" #include "session/types.hpp" #include "session/util.hpp" -namespace session::config::groups { - using namespace std::literals; -using session::ustring_view; + +namespace session::config::groups { Info::Info( ustring_view ed25519_pubkey, @@ -104,3 +104,258 @@ bool Info::is_destroyed() const { } } // namespace session::config::groups + +using namespace session; +using namespace session::config; + +LIBSESSION_C_API int groups_info_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, 32); + std::optional dump; + if (dump_bytes && dumplen) + dump.emplace(dump_bytes, dumplen); + + auto c_conf = std::make_unique(); + auto c = std::make_unique>(); + + try { + c->config = std::make_unique(ed25519_pubkey, ed25519_secretkey, dump); + } 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; +} + +/// 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) { + try { + unbox(conf)->set_name(name); + } catch (const std::exception& e) { + return set_error(conf, SESSION_ERR_BAD_VALUE, e); + } + 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 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) { + 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; +} + +/// 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(); +} From a2dc2e9e147937d85d41e22cf5cdbd159f26229d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 23 Aug 2023 10:55:55 -0300 Subject: [PATCH 019/572] Add Group Members C wrappers --- include/session/config/groups/members.h | 199 ++++++++++++++++++++++ include/session/config/groups/members.hpp | 16 +- src/config/groups/info.cpp | 37 +--- src/config/groups/members.cpp | 117 +++++++++++++ src/config/internal.hpp | 55 ++++-- 5 files changed, 375 insertions(+), 49 deletions(-) create mode 100644 include/session/config/groups/members.h diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h new file mode 100644 index 00000000..3e03ffcb --- /dev/null +++ b/include/session/config/groups/members.h @@ -0,0 +1,199 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../base.h" +#include "../util.h" +#include "../profile_pic.h" + +enum groups_members_invite_status { + INVITE_SENT = 1, + INVITE_FAILED = 2 +}; + +typedef struct config_group_member { + char session_id[67]; // in hex; 66 hex chars + null terminator. + + // These two will be 0-length strings when unset: + char name[101]; + user_profile_pic profile_pic; + + bool admin; + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int promoted; // same value as `invited`, but for promotion-to-admin + +} 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 +/// +/// 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 +/// - `member` -- [out] the member info data +/// - `session_id` -- [in] null terminated hex string +/// +/// Output: +/// - `bool` -- Returns true if member exsts +LIBSESSION_EXPORT bool groups_members_get( + config_object* conf, config_group_member* member, const char* session_id) + __attribute__((warn_unused_result)); + +/// API: groups/groups_members_get_or_construct +/// +/// 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. +/// +/// 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(). +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `member` -- [out] the member info data +/// - `session_id` -- [in] null terminated hex string +/// +/// Output: +/// - `bool` -- Returns true if the member exists, false if not (`member` is always filled +/// regardless). +LIBSESSION_EXPORT bool groups_members_get_or_construct( + config_object* conf, config_group_member* member, const char* session_id) + __attribute__((warn_unused_result)); + + + +/// API: groups/groups_members_set +/// +/// Adds or updates a member from the given member info struct. +/// +/// Inputs: +/// - `conf` -- [in, out] Pointer to the config object +/// - `member` -- [in] Pointer containing the member info data +LIBSESSION_EXPORT void groups_members_set( + config_object* conf, const config_group_member* member); + + +/// API: groups/groups_members_erase +/// +/// 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 +/// iteration; see details below. +/// +/// Typically this should be followed by a group rekey (so that the removed member cannot read the +/// group). +/// +/// 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 groups_members_erase(config_object* conf, const char* session_id); + +/// API: groups/groups_members_size +/// +/// Returns the number of group members. +/// +/// 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 groups_members_iterator { + void* _internals; +} groups_members_iterator; + +/// API: groups/groups_members_iterator_new +/// +/// Starts a new 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); +/// for (; !groups_members_iterator_done(it, &c); groups_members_iterator_advance(it)) { +/// // c.session_id, c.name, etc. are loaded +/// } +/// groups_members_iterator_free(it); +/// +/// It is NOT permitted to add/remove/modify members while iterating. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `groups_members_iterator*` -- pointer to the new iterator +LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new(const config_object* conf); + +/// API: groups/groups_members_iterator_free +/// +/// Frees an iterator once no longer needed. +/// +/// Inputs: +/// - `it` -- [in] Pointer to the groups_members_iterator +LIBSESSION_EXPORT void groups_members_iterator_free(groups_members_iterator* it); + +/// API: groups/groups_members_iterator_done +/// +/// Returns true if iteration has reached the end. Otherwise `m` is populated and false is +/// returned. +/// +/// Inputs: +/// - `it` -- [in] Pointer to the groups_members_iterator +/// - `m` -- [out] Pointer to the config_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); + +/// API: groups/groups_members_iterator_advance +/// +/// Advances the iterator. +/// +/// Inputs: +/// - `it` -- [in] Pointer to the groups_members_iterator +LIBSESSION_EXPORT void groups_members_iterator_advance(groups_members_iterator* it); + + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 7513ab72..34a06eb3 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -8,6 +8,8 @@ #include "../namespaces.hpp" #include "../profile_pic.hpp" +struct config_group_member; + namespace session::config::groups { using namespace std::literals; @@ -41,7 +43,7 @@ struct member { explicit member(std::string sid); // Internal ctor/method for C API implementations: - member(const struct config_group_member& c); // From c struct + explicit member(const config_group_member& c); // From c struct /// API: groups/member::session_id /// @@ -179,7 +181,7 @@ struct member { /// - `bool` -- true if the member is promoted (or promotion-in-progress) bool promoted() const { return admin || promotion_pending(); } - /// API: groups/member::info + /// API: groups/member::into /// /// Converts the member info into a C struct. /// @@ -326,6 +328,16 @@ class Members final : public ConfigBase { /// - true if the member was found (and removed); false if the member was not in the list. bool erase(std::string_view session_id); + /// API: groups/Members::size + /// + /// Returns the number of members in the group. + /// + /// Inputs: None + /// + /// Outputs: + /// - `size_t` - number of members + size_t size() const; + struct iterator; /// API: groups/Members::begin /// diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index f1a67ff2..2ba43e8f 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -110,41 +110,12 @@ using namespace session::config; LIBSESSION_C_API int groups_info_init( config_object** conf, - const unsigned char* ed25519_pubkey_bytes, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dump_bytes, + const unsigned char* ed25519_pubkey, + const unsigned char* ed25519_secretkey, + const unsigned char* dump, 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, 32); - std::optional dump; - if (dump_bytes && dumplen) - dump.emplace(dump_bytes, dumplen); - - auto c_conf = std::make_unique(); - auto c = std::make_unique>(); - - try { - c->config = std::make_unique(ed25519_pubkey, ed25519_secretkey, dump); - } 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; + return c_group_wrapper_init(conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); } /// API: groups_info/groups_info_get_name diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index fffbcf99..c0938038 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -3,6 +3,7 @@ #include #include "../internal.hpp" +#include "session/config/groups/members.h" namespace session::config::groups { @@ -117,8 +118,124 @@ bool Members::erase(std::string_view session_id) { return ret; } +size_t Members::size() const { + if (auto d = data["m"].dict()) + return d->size(); + return 0; +} + 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} { + assert(std::strlen(m.name) <= MAX_NAME_LENGTH); + name = m.name; + assert(std::strlen(m.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); + if (std::strlen(m.profile_pic.url)) { + profile_picture.url = m.profile_pic.url; + profile_picture.key = {m.profile_pic.key, 32}; + } + admin = m.admin; + invite_status = (m.invited == INVITE_SENT || m.invited == INVITE_FAILED) ? m.invited : 0; + promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED) ? m.promoted : 0; +} + +void member::into(config_group_member& m) const { + std::memcpy(m.session_id, session_id.data(), 67); + copy_c_str(m.name, name); + if (profile_picture) { + copy_c_str(m.profile_pic.url, profile_picture.url); + std::memcpy(m.profile_pic.key, profile_picture.key.data(), 32); + } else { + copy_c_str(m.profile_pic.url, ""); + } + m.admin = admin; + static_assert(groups::INVITE_SENT == ::INVITE_SENT); + static_assert(groups::INVITE_FAILED == ::INVITE_FAILED); + m.invited = invite_status; + m.promoted = promotion_status; +} + } // namespace session::config::groups + +using namespace session; +using namespace session::config; + +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); +} + +LIBSESSION_C_API bool groups_members_get( + config_object* conf, config_group_member* member, const char* session_id) { + try { + conf->last_error = nullptr; + if (auto c = unbox(conf)->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; + } + return false; +} + +LIBSESSION_C_API bool groups_members_get_or_construct( + config_object* conf, config_group_member* member, const char* session_id) { + try { + conf->last_error = nullptr; + unbox(conf)->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; + 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 bool groups_members_erase(config_object* conf, const char* session_id) { + try { + return unbox(conf)->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 groups_members_iterator* groups_members_iterator_new(const config_object* conf) { + auto* it = new groups_members_iterator{}; + it->_internals = new groups::Members::iterator{unbox(conf)->begin()}; + return it; +} + +LIBSESSION_C_API void groups_members_iterator_free(groups_members_iterator* it) { + delete static_cast(it->_internals); + delete it; +} + +LIBSESSION_C_API bool groups_members_iterator_done(groups_members_iterator* it, config_group_member* c) { + auto& real = *static_cast(it->_internals); + if (real.done()) + return true; + real->into(*c); + return false; +} + +LIBSESSION_C_API void groups_members_iterator_advance(groups_members_iterator* it) { + ++*static_cast(it->_internals); +} diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 5be9a601..dab43707 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -12,23 +12,13 @@ namespace session::config { -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, 32}; - auto c_conf = std::make_unique(); +template +[[nodiscard]] int c_wrapper_init_generic(config_object** conf, char* error, Args&&... args) { auto c = std::make_unique>(); - std::optional dump; - if (dumpstr && dumplen) - dump.emplace(dumpstr, dumplen); + auto c_conf = std::make_unique(); try { - c->config = std::make_unique(ed25519_secretkey, dump); + c->config = std::make_unique(std::forward(args)...); } catch (const std::exception& e) { if (error) { std::string msg = e.what(); @@ -45,6 +35,43 @@ template 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, 32}; + 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, 32); + 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) From c454e35b04d55906a539c9c8b4e768e176e5fd96 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 23 Aug 2023 19:27:17 -0300 Subject: [PATCH 020/572] Add safety limit to decompressed group decryption size --- include/session/config/groups/keys.hpp | 14 ++++++++++++-- src/config/groups/keys.cpp | 11 +++++++---- src/config/internal.cpp | 5 ++++- src/config/internal.hpp | 5 +++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index bd271468..31c290f0 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -122,6 +122,9 @@ class Keys final : public ConfigSig { /// online. static constexpr auto KEY_EXPIRY = 2 * 30 * 24h; + /// The maximum uncompressed message size we allow in message decryption/encryption. + static constexpr size_t MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; + // No default constructor Keys() = delete; @@ -377,8 +380,12 @@ class Keys final : public ConfigSig { /// /// Future versions may change this to support other encryption algorithms. /// - /// This method will throw if there no encryption keys are available at all (which should not - /// occur in normal use). + /// This method will throw on failure, which can happen in two cases: + /// - if there no encryption keys are available at all (which should not occur in normal use). + /// - if given a plaintext buffer larger than 1MB (even if the compressed version would be much + /// smaller). It is recommended that clients impose their own limits much smaller than this; + /// this limited is here to match the `decrypt_message` limit which is merely intended to + /// guard against decompression memory exhaustion attacks. /// /// Inputs: /// - `plaintext` -- the binary message to encrypt. @@ -395,6 +402,9 @@ class Keys final : public ConfigSig { /// will attempt decryption using *all* of the known group encryption keys and, if necessary, /// decompressing the message. /// + /// To prevent against memory exhaustion attacks, this method will fail if the value is + /// a compressed value that would decompress to a value larger than 1MB. + /// /// Inputs: /// - `ciphertext` -- a encoded, encrypted, (possibly) compressed message as produced by /// `encrypt_message()`. diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index c1fe603c..f1b89253 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1,3 +1,5 @@ +#include "session/config/groups/keys.hpp" + #include #include #include @@ -7,12 +9,11 @@ #include #include -#include -#include -#include #include #include "../internal.hpp" +#include "session/config/groups/info.hpp" +#include "session/config/groups/members.hpp" namespace session::config::groups { @@ -570,6 +571,8 @@ static constexpr size_t OVERHEAD = 1 // encryption type indicator crypto_aead_xchacha20poly1305_ietf_ABYTES; ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { + if (plaintext.size() > MAX_PLAINTEXT_MESSAGE_SIZE) + throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; ustring _compressed; if (compress) { _compressed = zstd_compress(plaintext); @@ -644,7 +647,7 @@ std::optional Keys::decrypt_message(ustring_view ciphertext) const { return std::nullopt; if (compressed) { - if (auto decomp = zstd_decompress(plain)) + if (auto decomp = zstd_decompress(plain, MAX_PLAINTEXT_MESSAGE_SIZE)) plain = std::move(*decomp); else // Decompression failed diff --git a/src/config/internal.cpp b/src/config/internal.cpp index a84d3689..4b75f35a 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -203,7 +203,7 @@ ustring zstd_compress(ustring_view data, int level, ustring_view prefix) { return compressed; } -std::optional zstd_decompress(ustring_view data) { +std::optional zstd_decompress(ustring_view data, size_t max_size) { zstd_decomp_ptr z_decompressor{ZSTD_createDStream()}; auto* zds = z_decompressor.get(); @@ -220,6 +220,9 @@ std::optional zstd_decompress(ustring_view data) { if (ret = ZSTD_decompressStream(zds, &output, &input); ZSTD_isError(ret)) return std::nullopt; + if (max_size > 0 && decompressed.size() + output.pos > max_size) + return std::nullopt; + decompressed.append(out_buf.data(), output.pos); } while (ret > 0 || input.pos < input.size); diff --git a/src/config/internal.hpp b/src/config/internal.hpp index dab43707..f9f1f2f9 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -165,7 +165,8 @@ void load_unknowns( /// serious error. ustring zstd_compress(ustring_view data, int level = 1, ustring_view prefix = {}); -/// ZSTD-decompresses a value. Returns nullopt if decompression fails. -std::optional zstd_decompress(ustring_view data); +/// ZSTD-decompresses a value. Returns nullopt if decompression fails. If max_size is non-zero +/// then this returns nullopt if the decompressed size would exceed that limit. +std::optional zstd_decompress(ustring_view data, size_t max_size = 0); } // namespace session::config From ae2f1ba9478d95e2d55362f2695bdae7c1be438a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 23 Aug 2023 19:32:07 -0300 Subject: [PATCH 021/572] C wrapper API for group keys --- include/session/config/groups/info.h | 12 +- include/session/config/groups/keys.h | 246 ++++++++++++++++++++++++ include/session/config/groups/keys.hpp | 31 ++- include/session/config/groups/members.h | 22 +-- include/session/config/namespaces.hpp | 3 +- src/config.cpp | 10 +- src/config/base.cpp | 5 +- src/config/groups/info.cpp | 3 +- src/config/groups/keys.cpp | 194 +++++++++++++++++-- src/config/groups/members.cpp | 3 +- tests/test_group_info.cpp | 14 +- tests/test_group_members.cpp | 2 +- 12 files changed, 475 insertions(+), 70 deletions(-) create mode 100644 include/session/config/groups/keys.h diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 9fb4129b..1efc4a75 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -134,8 +134,8 @@ LIBSESSION_EXPORT void groups_info_set_created(config_object* conf, int64_t 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. +/// 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 @@ -146,8 +146,8 @@ LIBSESSION_EXPORT int64_t groups_info_get_delete_before(const config_object* con /// 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. +/// 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 @@ -168,8 +168,8 @@ LIBSESSION_EXPORT int64_t groups_info_get_attach_delete_before(const config_obje /// 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. +/// 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 diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h new file mode 100644 index 00000000..a1e64775 --- /dev/null +++ b/include/session/config/groups/keys.h @@ -0,0 +1,246 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../base.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_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 +/// - `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 new keys, if needed +/// - `members` -- [in] the members config object to update with new keys, if needed +/// +/// 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 unsigned char* data, + size_t datalen, + int64_t timestamp_ms, + config_object* info, + config_object* members) __attribute__((warn_unused_result)); + +/// API: groups/groups_keys_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 +/// manage), but mergely whether a rekey is needed after loading one or more config messages. +/// +/// See the C++ Keys::needs_rekey and Keys::rekey descriptions for more details. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `bool` -- `true` if `rekey()` needs to be called, `false` otherwise. +LIBSESSION_EXPORT bool groups_keys_needs_rekey(const config_group_keys* conf) + __attribute__((warn_unused_result)); + +/// API: groups/groups_keys_needs_dump +/// +/// 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). +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `bool` -- `true` if a dump is needed, `false` otherwise. +LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) + __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_encrypt_message +/// +/// Encrypts a message using the most recent group encryption key of this object. The message will +/// be compressed (if that reduces the size) before being encrypted. Decryption (and decompression, +/// if compression was applied) is performed by passing such a message into +/// groups_keys_decrypt_message. +/// +/// Note: this method can fail if there are no encryption keys at all, or if the incoming message +/// decompresses to a huge value (more than 1MB). If it fails then `ciphertext_out` is set to NULL +/// and should not be read or free()d. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `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, + const unsigned char* plaintext_in, + size_t plaintext_len, + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: groups/groups_keys_decrypt_message +/// +/// Attempts to decrypt a message using all of the known active encryption keys of this object. The +/// message will be decompressed after decryption, if required. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `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` +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted/decompressed 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! +/// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption (or parsing or +/// decompression) failed with all of our known keys. +LIBSESSION_EXPORT bool groups_keys_decrypt_message( + const config_group_keys* conf, + const unsigned char* cipherext_in, + size_t cipherext_len, + unsigned char** plaintext_out, + size_t* plaintext_len); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 31c290f0..2acbbed7 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -63,7 +63,7 @@ class Keys final : public ConfigSig { struct key_info { std::array key; - std::chrono::system_clock::time_point timestamp; // millisecond precision + std::chrono::system_clock::time_point timestamp; // millisecond precision int64_t generation; auto cmpval() const { return std::tie(generation, timestamp, key); } @@ -215,10 +215,13 @@ class Keys final : public ConfigSig { /// config::groups::Members object. This can only be done by an admin account (i.e. we must /// have the group's private key). /// - /// This method is intended to be called in two situations: + /// This method is intended to be called in these situations: /// - potentially after loading new keys config messages (see `needs_rekey()`) /// - when removing a member to switch to a new encryption key for the group that excludes that /// member. + /// - when adding a member *and* switching to a new encryption key (without making the old key + /// available to the member) so that the new member cannot decipher pre-existing configs and + /// messages. /// /// This method is closely coupled to the group's Info and Members configs: it updates their /// encryption keys and sets them as dirty, requiring a re-push to re-encrypt each of them. @@ -241,7 +244,7 @@ class Keys final : public ConfigSig { /// confirmed or superceded). ustring_view rekey(Info& info, Members& members); - /// API: groups/Keys::pending_push + /// API: groups/Keys::pending_config /// /// If a rekey has been performed but not yet confirmed then this will contain the config /// message to be pushed to the swarm. If there is no push current pending then this returns @@ -258,8 +261,8 @@ class Keys final : public ConfigSig { /// API: groups/Keys::pending_key /// /// After calling rekey() this contains the new group encryption key *before* it is confirmed - /// pushed into the swarm. This is primarily used to allow a rekey + member list update using - /// the new key in the same swarm upload sequence. + /// pushed into the swarm. This is primarily intended for internal use as this key is generally + /// already propagated to the member/info lists when rekeying occurs. /// /// The pending key is dropped when an incoming keys message is successfully loaded with either /// the pending key itself, or a keys message with a higher generation. @@ -288,23 +291,17 @@ class Keys final : public ConfigSig { /// usable). /// /// Inputs: - /// - `msg` - the full stored config message value - /// - `hash` - the storage message hash (used to track current config messages) - /// - `timestamp` - the timestamp (from the swarm) when this message was stored (used to track - /// when other keys expire). - /// - `members` - the given group::Members object's en/decryption key list will be updated to - /// match this object's key list. + /// - `data` - the full stored config message value + /// - `timestamp_ms` - the timestamp (from the swarm) when this message was stored (used to + /// track when other keys expire). /// - `info` - the given group::Info object's en/decryption key list will be updated to match /// this object's key list. + /// - `members` - the given group::Members object's en/decryption key list will be updated to + /// match this object's key list. /// /// Outputs: /// - throws `std::runtime_error` (typically a subclass thereof) on failure to parse. - void load_key_message( - ustring_view data, - ustring_view msgid, - int64_t timestamp_ms, - Info& info, - Members& members); + void load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members); /// API: groups/Keys::needs_rekey /// diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 3e03ffcb..dcaabda4 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -5,13 +5,10 @@ extern "C" { #endif #include "../base.h" -#include "../util.h" #include "../profile_pic.h" +#include "../util.h" -enum groups_members_invite_status { - INVITE_SENT = 1, - INVITE_FAILED = 2 -}; +enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2 }; typedef struct config_group_member { char session_id[67]; // in hex; 66 hex chars + null terminator. @@ -21,8 +18,8 @@ typedef struct config_group_member { user_profile_pic profile_pic; bool admin; - int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send - int promoted; // same value as `invited`, but for promotion-to-admin + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int promoted; // same value as `invited`, but for promotion-to-admin } config_group_member; @@ -96,8 +93,6 @@ LIBSESSION_EXPORT bool groups_members_get_or_construct( config_object* conf, config_group_member* member, const char* session_id) __attribute__((warn_unused_result)); - - /// API: groups/groups_members_set /// /// Adds or updates a member from the given member info struct. @@ -105,9 +100,7 @@ LIBSESSION_EXPORT bool groups_members_get_or_construct( /// Inputs: /// - `conf` -- [in, out] Pointer to the config 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 groups_members_set(config_object* conf, const config_group_member* member); /// API: groups/groups_members_erase /// @@ -137,7 +130,6 @@ LIBSESSION_EXPORT bool groups_members_erase(config_object* conf, const char* ses /// - `size_t` -- number of contacts LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); - typedef struct groups_members_iterator { void* _internals; } groups_members_iterator; @@ -183,7 +175,8 @@ LIBSESSION_EXPORT void groups_members_iterator_free(groups_members_iterator* it) /// /// Outputs: /// - `bool` -- True if iteration has reached the end -LIBSESSION_EXPORT bool groups_members_iterator_done(groups_members_iterator* it, config_group_member* m); +LIBSESSION_EXPORT bool groups_members_iterator_done( + groups_members_iterator* it, config_group_member* m); /// API: groups/groups_members_iterator_advance /// @@ -193,7 +186,6 @@ LIBSESSION_EXPORT bool groups_members_iterator_done(groups_members_iterator* it, /// - `it` -- [in] Pointer to the groups_members_iterator LIBSESSION_EXPORT void groups_members_iterator_advance(groups_members_iterator* it); - #ifdef __cplusplus } // extern "C" #endif diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index f7d48469..c5c29ec5 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -12,7 +12,8 @@ enum class Namespace : std::int16_t { // Messages sent to a closed group: GroupMessages = 11, - // Groups config namespaces (i.e. for shared config of the group itself, not one user's group settings) + // Groups config namespaces (i.e. for shared config of the group itself, not one user's group + // settings) GroupKeys = 12, GroupInfo = 13, GroupMembers = 14, diff --git a/src/config.cpp b/src/config.cpp index 77b95e6e..f60d554f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -542,10 +542,7 @@ ConfigMessage::ConfigMessage() { } ConfigMessage::ConfigMessage( - ustring_view serialized, - verify_callable verifier_, - sign_callable signer_, - int lag) : + ustring_view serialized, verify_callable verifier_, sign_callable signer_, int lag) : verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} { oxenc::bt_dict_consumer dict{from_unsigned_sv(serialized)}; @@ -724,10 +721,7 @@ MutableConfigMessage::MutableConfigMessage( } MutableConfigMessage::MutableConfigMessage( - ustring_view config, - verify_callable verifier, - sign_callable signer, - int lag) : + ustring_view config, verify_callable verifier, sign_callable signer, int lag) : MutableConfigMessage{ std::vector{{config}}, std::move(verifier), diff --git a/src/config/base.cpp b/src/config/base.cpp index 3fac5f09..c3d03edd 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -133,8 +133,9 @@ int ConfigBase::merge(const std::vector>& c // 'z' prefix indicates zstd-compressed data: if (plain[0] == 'z') { - if (auto decompressed = zstd_decompress(ustring_view{plain.data() + 1, plain.size() - 1}); - decompressed && !decompressed->empty()) + if (auto decompressed = + zstd_decompress(ustring_view{plain.data() + 1, plain.size() - 1}); + decompressed && !decompressed->empty()) plain = std::move(*decompressed); else { log(LogLevel::warning, "Invalid config message: decompression failed"); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 2ba43e8f..9c34a9d0 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -115,7 +115,8 @@ LIBSESSION_C_API int groups_info_init( const unsigned char* dump, size_t dumplen, char* error) { - return c_group_wrapper_init(conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); + return c_group_wrapper_init( + conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); } /// API: groups_info/groups_info_get_name diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index f1b89253..0965d333 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -13,6 +13,7 @@ #include "../internal.hpp" #include "session/config/groups/info.hpp" +#include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" namespace session::config::groups { @@ -361,8 +362,7 @@ std::optional Keys::pending_config() const { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } -void Keys::load_key_message( - ustring_view data, ustring_view msgid, int64_t timestamp_ms, Info& info, Members& members) { +void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members) { oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; @@ -590,15 +590,15 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { randombytes_buf(ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); ustring_view nonce{ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data() + 1 + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - nullptr, - plaintext.data(), - plaintext.size(), - nullptr, - 0, - nullptr, - nonce.data(), - group_enc_key().data())) + ciphertext.data() + 1 + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, + plaintext.data(), + plaintext.size(), + nullptr, + 0, + nullptr, + nonce.data(), + group_enc_key().data())) throw std::runtime_error{"Encryption failed"}; return ciphertext; @@ -658,3 +658,175 @@ std::optional Keys::decrypt_message(ustring_view ciphertext) const { } } // namespace session::config::groups + +using namespace session; +using namespace session::config; + +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(); + + try { + c_conf->internals = new groups::Keys{user_sk, group_pk, group_sk, dump, 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; + } + + c_conf->last_error = nullptr; + *conf = c_conf.release(); + return SESSION_ERR_NONE; +} + +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; + try { + to_push = keys.rekey(*unbox(info), *unbox(members)); + } catch (const std::exception& e) { + set_error(conf, e.what()); + 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 unsigned char* data, + size_t datalen, + int64_t timestamp_ms, + config_object* info, + config_object* members) { + assert(data && info && members); + try { + unbox(conf).load_key_message( + ustring_view{data, datalen}, + timestamp_ms, + *unbox(info), + *unbox(members)); + } catch (const std::exception& e) { + set_error(conf, e.what()); + return false; + } + return true; +} + +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; + 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(); + } catch (...) { + *ciphertext_out = nullptr; + *ciphertext_len = 0; + } +} + +LIBSESSION_C_API bool groups_keys_decrypt_message( + const config_group_keys* conf, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + unsigned char** plaintext_out, + size_t* plaintext_len) { + assert(ciphertext_in && plaintext_out && plaintext_len); + + auto plaintext = unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); + if (!plaintext) + return false; + + *plaintext_out = static_cast(std::malloc(plaintext->size())); + std::memcpy(*plaintext_out, plaintext->data(), plaintext->size()); + *plaintext_len = plaintext->size(); + return true; +} diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index c0938038..9d330d7f 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -228,7 +228,8 @@ LIBSESSION_C_API void groups_members_iterator_free(groups_members_iterator* it) delete it; } -LIBSESSION_C_API bool groups_members_iterator_done(groups_members_iterator* it, config_group_member* c) { +LIBSESSION_C_API bool groups_members_iterator_done( + groups_members_iterator* it, config_group_member* c) { auto& real = *static_cast(it->_internals); if (real.done()) return true; diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index eac73373..ce21534a 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -47,7 +47,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); groups::Info ginfo2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys) // Just for testing, as above. + for (const auto& k : enc_keys) // Just for testing, as above. ginfo2.add_key(k, false); ginfo1.set_name("GROUP Name"); @@ -158,7 +158,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // This Info object has only the public key, not the priv key, and so cannot modify things: groups::Info ginfo{to_usv(ed_pk), std::nullopt, std::nullopt}; - for (const auto& k : enc_keys1) // Just for testing, as above. + for (const auto& k : enc_keys1) // Just for testing, as above. ginfo.add_key(k, false); REQUIRE_THROWS_WITH( @@ -170,7 +170,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // This one is good and has the right signature: groups::Info ginfo_rw{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys1) // Just for testing, as above. + for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw.add_key(k, false); ginfo_rw.set_name("Super Group!!"); @@ -193,7 +193,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_rw2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys1) // Just for testing, as above. + for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw2.add_key(k, false); CHECK(ginfo_rw2.merge(merge_configs) == 1); @@ -220,7 +220,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_bad1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys1) // Just for testing, as above. + for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_bad1.add_key(k, false); ginfo_bad1.merge(merge_configs); @@ -303,7 +303,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { auto dump = ginfo.dump(); groups::Info ginfo2{to_usv(ed_pk), std::nullopt, dump}; - for (const auto& k : enc_keys1) // Just for testing, as above. + for (const auto& k : enc_keys1) // Just for testing, as above. ginfo2.add_key(k, false); CHECK(!ginfo.needs_dump()); @@ -321,7 +321,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // This account has a different primary decryption key groups::Info ginfo_rw3{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys2) // Just for testing, as above. + for (const auto& k : enc_keys2) // Just for testing, as above. ginfo_rw3.add_key(k, false); CHECK(ginfo_rw3.merge(merge_configs) == 1); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 24afaf49..e78d6723 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -56,7 +56,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); groups::Members gmem2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; - for (const auto& k : enc_keys) // Just for testing, as above. + for (const auto& k : enc_keys) // Just for testing, as above. gmem2.add_key(k, false); std::vector sids; From a44567e876f393b0645fa1264b355866ae44d51b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 24 Aug 2023 00:40:19 -0300 Subject: [PATCH 022/572] oxen-encoding update to fix llvm compile error --- external/oxen-encoding | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-encoding b/external/oxen-encoding index fc85dfd3..462be41b 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit fc85dfd352e8474bc7195b0ba881838bd72ebea6 +Subproject commit 462be41bd481b331dabeb3c220b349ef35c89e56 From 995364577cad13db87214b78870a79964b0673dd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 24 Aug 2023 15:06:46 -0300 Subject: [PATCH 023/572] Add new group storage to UserGroups config --- include/session/config/user_groups.h | 22 +++- include/session/config/user_groups.hpp | 155 ++++++++++++++++++----- src/config/internal.cpp | 10 +- src/config/internal.hpp | 7 +- src/config/user_groups.cpp | 164 +++++++++++++++++++++---- 5 files changed, 295 insertions(+), 63 deletions(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index e7187a44..9410add1 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -27,7 +27,7 @@ typedef struct ugroups_legacy_group_info { unsigned char enc_seckey[32]; // If `have_enc_keys`, this is the 32-byte secret key (no NULL // terminator). - int64_t disappearing_timer; // Minutes. 0 == disabled. + int64_t disappearing_timer; // Seconds. 0 == disabled. int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned // (with higher meaning pinned higher). int64_t joined_at; // unix timestamp when joined (or re-joined) @@ -40,6 +40,26 @@ typedef struct ugroups_legacy_group_info { void* _internal; // Internal storage, do not touch. } ugroups_legacy_group_info; +/// Struct holding (non-legacy) group info; this struct owns allocated memory and *must* be freed +/// via either `ugroups_group_free()` or `ugroups_set_free_group()` when finished with it. +typedef struct ugroups_group_info { + char id[67]; // in hex; 66 hex chars + null terminator + + bool have_secretkey; // Will be true if the `secretkey` is populated + unsigned char secretkey[64]; // If `have_secretkey` is set then this is the libsodium-style + // "secret key" for the group (i.e. 32 byte seed + 32 byte pubkey) + bool have_auth_sig; // Will be true if the `auth_sig` is populated + unsigned char auth_sig[64]; // If `have_auth_sig` is set then this is the authentication + // signature that can be used to access the swarm. + + int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned + // (with higher meaning pinned higher). + int64_t joined_at; // unix timestamp when joined (or re-joined) + CONVO_NOTIFY_MODE notifications; // When the user wants notifications + int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` + // setting until the timestamp) +} ugroups_group_info; + typedef struct ugroups_community_info { char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case, // only has port if non-default, has trailing / removed) diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 0809df50..b9684348 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -12,6 +12,7 @@ #include "notify.hpp" extern "C" { +struct ugroups_group_info; struct ugroups_legacy_group_info; struct ugroups_community_info; } @@ -20,25 +21,23 @@ namespace session::config { /// keys used in this config, either currently or in the past (so that we don't reuse): /// -/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and -/// value is a dict containing keys: +/// g - dict of groups (AKA closed groups) for new-style closed groups (i.e. not legacy closed +/// groups; see below for those). Each key is the group's public key (without 0x03 prefix). /// -/// n - name (string). Always set, even if empty. -/// k - encryption public key (32 bytes). Optional. -/// K - encryption secret key (32 bytes). Optional. -/// m - set of member session ids (each 33 bytes). -/// a - set of admin session ids (each 33 bytes). -/// E - disappearing messages duration, in seconds, > 0. Omitted if disappearing messages is -/// disabled. (Note that legacy groups only support expire after-read) +/// K - group seed, if known (i.e. an admin). This is just the seed, which is just the first +/// half (32 bytes) of the 64-byte libsodium-style Ed25519 secret key value (i.e. it omits +/// the cached public key in the second half). This field is always set, but will be empty +/// if the seed is not known. +/// s - authentication signature; this is used by non-admins to authenticate /// @ - notification setting (int). Omitted = use default setting; 1 = all, 2 = disabled, 3 = /// mentions-only. /// ! - mute timestamp: if set then don't show notifications for this contact's messages until /// this unix timestamp (i.e. overriding the current notification setting until the given /// time). -/// + - the conversation priority, for pinned/hidden messages. Integer. Omitted means not -/// pinned; -1 means hidden, and a positive value is a pinned message for which higher -/// priority values means the conversation is meant to appear earlier in the pinned -/// conversation list. +/// + - the conversation priority, for pinning/hiding this group in the conversation list. +/// Integer. Omitted means not pinned; -1 means hidden, and a positive value is a pinned +/// message for which higher priority values means the conversation is meant to appear +/// earlier in the pinned conversation list. /// j - joined at unix timestamp. Omitted if 0. /// /// o - dict of communities (AKA open groups); within this dict (which deliberately has the same @@ -51,14 +50,25 @@ namespace session::config { /// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in /// the outer key, with the capitalization variation in use ("SudokuSolvers") in this /// key. This key is *always* present (to keep the room dict non-empty). -/// @ - notification setting (see above). +/// @ - notification setting (same values as groups, above). /// ! - mute timestamp (see above). -/// + - the conversation priority, for pinned messages. Omitted means not pinned; -1 means -/// hidden; otherwise an integer value >0, where a higher priority means the -/// conversation is meant to appear earlier in the pinned conversation list. +/// + - the conversation priority, for pinning/hiding this community room. See above. /// j - joined at unix timestamp. Omitted if 0. /// -/// c - reserved for future storage of new-style group info. +/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and +/// value is a dict containing keys: +/// +/// n - name (string). Always set, even if empty. +/// k - encryption public key (32 bytes). Optional. +/// K - encryption secret key (32 bytes). Optional. +/// m - set of member session ids (each 33 bytes). +/// a - set of admin session ids (each 33 bytes). +/// E - disappearing messages duration, in seconds, > 0. Omitted if disappearing messages is +/// disabled. (Note that legacy groups only support expire after-read) +/// @ - notification setting (int). Same as above. +/// ! - mute timestamp (see above). +/// + - the conversation priority, for pinned/hidden conversations. See above. +/// j - joined at unix timestamp. Omitted if 0. /// Common base type with fields shared by all the groups struct base_group_info { @@ -162,6 +172,32 @@ struct legacy_group_info : base_group_info { void load(const dict& info_dict); }; +/// Struct containing new group info (aka "closed groups v2"). +struct group_info : base_group_info { + std::string id; // The group pubkey (66 hex digits); this is an ed25519 key, prefixed with "03" + // (to distinguish it from a 05 x25519 pubkey session id). + + /// Group secret key (64 bytes); this is only possessed by admins. + ustring secretkey; + + /// Group authentication signature; this is possessed by non-admins. (This value will be + /// dropped when serializing if secretkey is non-empty, and so does not need to be explicitly + /// cleared when being promoted to admin) + ustring auth_sig; + + /// Constructs a new group info from an hex id (03 + pubkey). Throws if id is invalid. + explicit group_info(std::string gid); + + // Internal ctor/method for C API implementations: + group_info(const struct ugroups_group_info& c); // From c struct + void into(struct ugroups_group_info& c) const; // Into c struct + + private: + friend class UserGroups; + + void load(const dict& info_dict); +}; + /// Community (aka open group) info struct community_info : base_group_info, community { // Note that *changing* url/room/pubkey and then doing a set inserts a new room under the given @@ -181,7 +217,7 @@ struct community_info : base_group_info, community { friend class comm_iterator_helper; }; -using any_group_info = std::variant; +using any_group_info = std::variant; class UserGroups : public ConfigBase { @@ -209,12 +245,12 @@ class UserGroups : public ConfigBase { /// API: user_groups/UserGroups::storage_namespace /// - /// Returns the Contacts namespace. Is constant, will always return 5 + /// Returns the Contacts namespace. /// /// Inputs: None /// /// Outputs: - /// - `Namespace` - Returns 5 + /// - `Namespace` - Returns Namespace::UserGroups Namespace storage_namespace() const override { return Namespace::UserGroups; } /// API: user_groups/UserGroups::encryption_domain @@ -264,6 +300,20 @@ class UserGroups : public ConfigBase { /// found std::optional get_legacy_group(std::string_view pubkey_hex) const; + /// API: user_groups/UserGroups::get_group + /// + /// Looks up and returns a group (aka new closed group) by group ID (hex, looks like a Session + /// ID but starting with 03). Returns nullopt if the group was not found, otherwise returns a + /// filled out `group_info`. + /// + /// Inputs: + /// - `pubkey_hex` -- group ID (hex, looks like a session ID but starting 03 instead of 05) + /// + /// Outputs: + /// - `std::optional` - Returns the filled out group_info struct if found, nullopt + /// if not found. + std::optional get_group(std::string_view pubkey_hex) const; + /// API: user_groups/UserGroups::get_or_construct_community /// /// Same as `get_community`, except if the community isn't found a new blank one is created for @@ -324,6 +374,30 @@ class UserGroups : public ConfigBase { /// - `legacy_group_info` - Returns the filled out legacy_group_info struct legacy_group_info get_or_construct_legacy_group(std::string_view pubkey_hex) const; + /// API: user_groups/UserGroups::get_or_construct_group + /// + /// Gets or constructs a blank group_info for the given group id. + /// + /// Inputs: + /// - `pubkey_hex` -- group ID (hex, looks like a session ID) + /// + /// Outputs: + /// - `group_info` - Returns the filled out group_info struct + group_info get_or_construct_group(std::string_view pubkey_hex) const; + + /// API: user_groups/UserGroups::create_group + /// + /// Constructs a `group_info` object with newly generated (random) keys and returns the + /// group_info containing these keys. The group will have the id and secretkey populated; other + /// fields are defaulted. You still need to pass this to `set()` to store it, after setting any + /// other fields as desired. + /// + /// Inputs: None + /// + /// Outputs: + /// - `group_info` - Returns a filled out group_info struct for a new, randomly generated group. + group_info create_group() const; + /// API: user_groups/UserGroups::set /// /// Inserts or replaces existing group info. For example, to update the info for a community @@ -336,12 +410,15 @@ class UserGroups : public ConfigBase { /// /// Declaration: /// ```cpp + /// void set(const group_info& info); /// void set(const community_info& info); /// void set(const legacy_group_info& info); /// ``` /// /// Inputs: - /// - `info` -- group info struct to insert. Can be either community_info or legacy_group_info + /// - `info` -- group info struct to insert. Can be `community_info`, `group_info`, or + /// `legacy_group_info`. + void set(const group_info& info); void set(const community_info& info); void set(const legacy_group_info& info); @@ -380,11 +457,12 @@ class UserGroups : public ConfigBase { /// API: user_groups/UserGroups::erase /// - /// Removes a conversation taking the community_info or legacy_group_info instance (rather than - /// the pubkey/url) for convenience. + /// Removes a conversation taking the community_info, group_info, or legacy_group_info instance + /// (rather than the pubkey/url) for convenience. /// /// Declaration: /// ```cpp + /// bool erase(const group_info& g); /// bool erase(const community_info& g); /// bool erase(const legacy_group_info& c); /// bool erase(const any_group_info& info); @@ -395,6 +473,7 @@ class UserGroups : public ConfigBase { /// /// Outputs: /// - `bool` - Returns true if found and removed, false otherwise + bool erase(const group_info& g); bool erase(const community_info& g); bool erase(const legacy_group_info& c); bool erase(const any_group_info& info); @@ -409,6 +488,16 @@ class UserGroups : public ConfigBase { /// - `size_t` - Returns the number of groups size_t size() const; + /// API: user_groups/UserGroups::size_groups + /// + /// Returns the number of (non-legacy) groups + /// + /// Inputs: None + /// + /// Outputs: + /// - `size_t` - Returns the number of groups + size_t size_groups() const; + /// API: user_groups/UserGroups::size_communities /// /// Returns the number of communities @@ -419,7 +508,7 @@ class UserGroups : public ConfigBase { /// - `size_t` - Returns the number of groups size_t size_communities() const; - /// API: user_groups/UserGroups::size_communities + /// API: user_groups/UserGroups::size_legacy_groups /// /// Returns the number of legacy groups /// @@ -454,8 +543,8 @@ class UserGroups : public ConfigBase { /// } /// ``` /// - /// This iterates through all groups in sorted order (sorted first by convo type, then by - /// id within the type). + /// This iterates through all groups in sorted order (sorted first by convo type [groups, + /// communities, legacy groups], then by id within the type). /// /// It is NOT permitted to add/remove/modify records while iterating. If such is needed it must /// be done in two passes: once to collect the modifications, then a loop applying the collected @@ -482,11 +571,12 @@ class UserGroups : public ConfigBase { /// Returns an iterator that iterates only through one type of conversations. (The regular /// `.end()` iterator is valid for testing the end of these iterations). + subtype_iterator legacy_groups() const { return {data}; } subtype_iterator begin_communities() const { return {data}; } subtype_iterator begin_legacy_groups() const { return {data}; } using iterator_category = std::input_iterator_tag; - using value_type = std::variant; + using value_type = std::variant; using reference = value_type&; using pointer = value_type*; using difference_type = std::ptrdiff_t; @@ -494,13 +584,19 @@ class UserGroups : public ConfigBase { struct iterator { protected: std::shared_ptr _val; + std::optional _it_group, _end_group; std::optional _it_comm; std::optional _it_legacy, _end_legacy; void _load_val(); iterator() = default; // Constructs an end tombstone explicit iterator( - const DictFieldRoot& data, bool communities = true, bool legacy_closed = true); + const DictFieldRoot& data, + bool communities = true, + bool legacy_closed = true, + bool groups = true); friend class UserGroups; + template + bool check_it(); public: bool operator==(const iterator& other) const; @@ -522,6 +618,7 @@ class UserGroups : public ConfigBase { subtype_iterator(const DictFieldRoot& data) : iterator( data, + std::is_same_v, std::is_same_v, std::is_same_v) {} friend class UserGroups; diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 4b75f35a..1b2035d2 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -11,16 +11,16 @@ namespace session::config { -void check_session_id(std::string_view session_id) { - if (!(session_id.size() == 66 && oxenc::is_hex(session_id) && session_id[0] == '0' && - session_id[1] == '5')) +void check_session_id(std::string_view session_id, unsigned char prefix) { + if (!(session_id.size() == 66 && oxenc::is_hex(session_id) && + session_id[0] == ('0' + (prefix >> 4)) && session_id[1] == ('0' + (prefix & 0xf)))) throw std::invalid_argument{ "Invalid session ID: expected 66 hex digits starting with 05; got " + std::string{session_id}}; } -std::string session_id_to_bytes(std::string_view session_id) { - check_session_id(session_id); +std::string session_id_to_bytes(std::string_view session_id, unsigned char prefix) { + check_session_id(session_id, prefix); return oxenc::from_hex(session_id); } diff --git a/src/config/internal.hpp b/src/config/internal.hpp index f9f1f2f9..d94faabc 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -80,11 +80,12 @@ void copy_c_str(char (&dest)[N], std::string_view src) { dest[src.size()] = 0; } -// Throws std::invalid_argument if session_id doesn't look valid -void check_session_id(std::string_view session_id); +// Throws std::invalid_argument if session_id doesn't look valid. Can optionally be passed a prefix +// byte for id's that aren't starting with 0x05 (e.g. 0x03 for non-legacy group ids). +void check_session_id(std::string_view session_id, unsigned char prefix = 0x05); // Checks the session_id (throwing if invalid) then returns it as bytes -std::string session_id_to_bytes(std::string_view session_id); +std::string session_id_to_bytes(std::string_view session_id, unsigned char prefix = 0x05); // Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 prefix // (which is the x25519 pubkey). diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index aa689653..bace4bf3 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,10 @@ static void base_from(base_group_info& self, const T& c) { self.mute_until = c.mute_until; } +group_info::group_info(std::string sid) : id{std::move(sid)} { + check_session_id(id, 0x03); +} + legacy_group_info::legacy_group_info(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } @@ -190,6 +195,38 @@ bool legacy_group_info::erase(const std::string& session_id) { return members_.erase(session_id); } +group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { + base_from(*this, c); + if (c.have_secretkey) + secretkey.assign(c.secretkey, 64); + if (c.have_auth_sig) + auth_sig.assign(c.auth_sig, 64); +} + +void group_info::into(ugroups_group_info& c) const { + assert(id.size() == 66); + base_into(*this, c); + copy_c_str(c.id, id); + if ((c.have_secretkey = secretkey.size() == 64)) + std::memcpy(c.secretkey, secretkey.data(), 64); + if ((c.have_auth_sig = auth_sig.size() == 64)) + std::memcpy(c.auth_sig, auth_sig.data(), 64); +} + +void group_info::load(const dict& info_dict) { + base_group_info::load(info_dict); + + if (auto seed = maybe_ustring(info_dict, "K"); seed && seed->size() == 32) { + std::array pk; + secretkey.resize(64); + crypto_sign_seed_keypair(pk.data(), secretkey.data(), seed->data()); + if (id != oxenc::to_hex(pk.begin(), pk.end())) + secretkey.clear(); + } + if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 64) + auth_sig = std::move(*sig); +} + void community_info::load(const dict& info_dict) { base_group_info::load(info_dict); @@ -277,6 +314,40 @@ legacy_group_info UserGroups::get_or_construct_legacy_group(std::string_view pub return legacy_group_info{std::string{pubkey_hex}}; } +std::optional UserGroups::get_group(std::string_view pubkey_hex) const { + std::string pubkey = session_id_to_bytes(pubkey_hex, 0x03); + + auto* info_dict = data["g"][pubkey].dict(); + if (!info_dict) + return std::nullopt; + + auto result = std::make_optional(std::string{pubkey_hex}); + result->load(*info_dict); + return result; +} + +group_info UserGroups::get_or_construct_group(std::string_view pubkey_hex) const { + if (auto maybe = get_group(pubkey_hex)) + return *std::move(maybe); + + return group_info{std::string{pubkey_hex}}; +} + +group_info UserGroups::create_group() const { + std::array pk; + ustring sk; + sk.resize(64); + crypto_sign_keypair(pk.data(), sk.data()); + std::string pk_hex; + pk_hex.reserve(66); + pk_hex += "03"; + oxenc::to_hex(pk.begin(), pk.end(), std::back_inserter(pk_hex)); + + group_info gr{std::move(pk_hex)}; + gr.secretkey = std::move(sk); + return gr; +} + void UserGroups::set(const community_info& c) { data["o"][c.base_url()]["#"] = c.pubkey(); auto info = community_field(c); // data["o"][base]["R"][lc_room] @@ -316,6 +387,17 @@ void UserGroups::set(const legacy_group_info& g) { set_positive_int(info["E"], g.disappearing_timer.count()); } +void UserGroups::set(const group_info& g) { + auto info = data["g"][session_id_to_bytes(g.id, 0x03)]; + set_base(g, info); + + if (g.secretkey.size() == 64) + info["K"] = ustring_view{g.secretkey.data(), 32}; + + else if (g.auth_sig.size() == 64) + info["s"] = g.auth_sig; +} + template static bool erase_impl(Field convo) { bool ret = convo.exists(); @@ -337,6 +419,9 @@ bool UserGroups::erase(const community_info& c) { } return gone; } +bool UserGroups::erase(const group_info& c) { + return erase_impl(data["g"][session_id_to_bytes(c.id, 0x03)]); +} bool UserGroups::erase(const legacy_group_info& c) { return erase_impl(data["C"][session_id_to_bytes(c.session_id)]); } @@ -373,11 +458,23 @@ size_t UserGroups::size_legacy_groups() const { return 0; } +size_t UserGroups::size_groups() const { + if (auto* d = data["g"].dict()) + return d->size(); + return 0; +} + size_t UserGroups::size() const { - return size_communities() + size_legacy_groups(); + return size_communities() + size_legacy_groups() + size_groups(); } -UserGroups::iterator::iterator(const DictFieldRoot& data, bool communities, bool legacy_groups) { +UserGroups::iterator::iterator( + const DictFieldRoot& data, bool groups, bool communities, bool legacy_groups) { + if (groups) + if (auto* d = data["g"].dict()) { + _it_group = d->begin(); + _end_group = d->end(); + } if (communities) if (auto* d = data["o"].dict()) _it_comm.emplace(d->begin(), d->end()); @@ -389,38 +486,55 @@ UserGroups::iterator::iterator(const DictFieldRoot& data, bool communities, bool _load_val(); } -/// 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). We also span across three different iterators: first -/// we exhaust communities, then legacy groups. -/// -/// We *always* call this after incrementing the iterators (and after iterator initialization), and -/// this is responsible for making sure that the the _it variables are set up as required. -void UserGroups::iterator::_load_val() { - if (_it_comm) { - if (_it_comm->load(_val)) - return; - else - _it_comm.reset(); - } +template +bool UserGroups::iterator::check_it() { + static_assert( + std::is_same_v || std::is_same_v); + constexpr bool legacy = std::is_same_v; + auto& it = legacy ? _it_legacy : _it_group; + auto& end = legacy ? _end_legacy : _end_group; - while (_it_legacy) { - if (*_it_legacy == *_end_legacy) { - _it_legacy.reset(); - _end_legacy.reset(); + constexpr char prefix = legacy ? 0x05 : 0x03; + while (it) { + if (*it == *end) { + it.reset(); + end.reset(); break; } - auto& [k, v] = **_it_legacy; + auto& [k, v] = **it; - if (k.size() == 33 && k[0] == 0x05) { + if (k.size() == 33 && k[0] == prefix) { if (auto* info_dict = std::get_if(&v)) { - _val = std::make_shared(legacy_group_info{oxenc::to_hex(k)}); - std::get(*_val).load(*info_dict); - return; + _val = std::make_shared(GroupInfo{oxenc::to_hex(k)}); + std::get(*_val).load(*info_dict); + return true; } } - ++*_it_legacy; + ++*it; } + return false; +} + +/// 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). We also span across three different iterators: +/// first we exhaust communities, then legacy groups. +/// +/// We *always* call this after incrementing the iterators (and after iterator initialization), +/// and this is responsible for making sure that the the _it variables are set up as required. +void UserGroups::iterator::_load_val() { + if (check_it()) + return; + + if (_it_comm) { + if (_it_comm->load(_val)) + return; + else + _it_comm.reset(); + } + + if (check_it()) + return; } bool UserGroups::iterator::operator==(const iterator& other) const { From d3b902f657083da3f37315327e2722d4fbda411f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 24 Aug 2023 15:10:54 -0300 Subject: [PATCH 024/572] Fix unclosed list in key config generation --- src/config/groups/keys.cpp | 105 +++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 0965d333..bc76e731 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -269,59 +269,60 @@ ustring_view Keys::rekey(Info& info, Members& members) { d.append("G", gen); d.append("K", enc_sv); - auto member_keys = d.append_list("k"); - - int member_count = 0; - for (const auto& m : members) { - auto m_xpk = session_id_xpk(m.session_id); - // Calculate the encryption key: H(aB || A || B) - if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) - continue; // The scalarmult failed; maybe a bad session id? - - crypto_generichash_blake2b_init( - &st, - enc_key_member_hash_key.data(), - enc_key_member_hash_key.size(), - member_k.size()); - crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); - crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); - crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); - crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); - - crypto_aead_xchacha20poly1305_ietf_encrypt( - encrypted.data(), - nullptr, - enc_key.data(), - enc_key.size(), - nullptr, - 0, - nullptr, - nonce.data(), - member_k.data()); - - member_keys.append(enc_sv); - member_count++; - } - - // Pad it out with junk entries to the next MESSAGE_KEY_MULTIPLE - if (member_count % MESSAGE_KEY_MULTIPLE) { - int n_junk = MESSAGE_KEY_MULTIPLE - (member_count % MESSAGE_KEY_MULTIPLE); - std::vector junk_data; - junk_data.resize(encrypted.size() * n_junk); + { + auto member_keys = d.append_list("k"); + int member_count = 0; + for (const auto& m : members) { + auto m_xpk = session_id_xpk(m.session_id); + // Calculate the encryption key: H(aB || A || B) + if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) + continue; // The scalarmult failed; maybe a bad session id? + + crypto_generichash_blake2b_init( + &st, + enc_key_member_hash_key.data(), + enc_key_member_hash_key.size(), + member_k.size()); + crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); + crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); + crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); + crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); + + crypto_aead_xchacha20poly1305_ietf_encrypt( + encrypted.data(), + nullptr, + enc_key.data(), + enc_key.size(), + nullptr, + 0, + nullptr, + nonce.data(), + member_k.data()); + + member_keys.append(enc_sv); + member_count++; + } - std::array rng_seed; - crypto_generichash_blake2b_init( - &st, junk_seed_hash_key.data(), junk_seed_hash_key.size(), rng_seed.size()); - crypto_generichash_blake2b_update(&st, h1.data(), h1.size()); - crypto_generichash_blake2b_update(&st, _sign_sk.data(), _sign_sk.size()); - crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); - - randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); - std::string_view junk_view{ - reinterpret_cast(junk_data.data()), junk_data.size()}; - while (!junk_view.empty()) { - member_keys.append(junk_view.substr(0, encrypted.size())); - junk_view.remove_prefix(encrypted.size()); + // Pad it out with junk entries to the next MESSAGE_KEY_MULTIPLE + if (member_count % MESSAGE_KEY_MULTIPLE) { + int n_junk = MESSAGE_KEY_MULTIPLE - (member_count % MESSAGE_KEY_MULTIPLE); + std::vector junk_data; + junk_data.resize(encrypted.size() * n_junk); + + std::array rng_seed; + crypto_generichash_blake2b_init( + &st, junk_seed_hash_key.data(), junk_seed_hash_key.size(), rng_seed.size()); + crypto_generichash_blake2b_update(&st, h1.data(), h1.size()); + crypto_generichash_blake2b_update(&st, _sign_sk.data(), _sign_sk.size()); + crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); + + randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); + std::string_view junk_view{ + reinterpret_cast(junk_data.data()), junk_data.size()}; + while (!junk_view.empty()) { + member_keys.append(junk_view.substr(0, encrypted.size())); + junk_view.remove_prefix(encrypted.size()); + } } } From 455125717c6b3b92fa7c4c9c07f3af28060d8c84 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 03:17:53 -0300 Subject: [PATCH 025/572] Fix broken x25519 extraction --- src/config/internal.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 1b2035d2..3b9ef85e 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -27,6 +27,7 @@ std::string session_id_to_bytes(std::string_view session_id, unsigned char prefi std::array session_id_xpk(std::string_view session_id) { check_session_id(session_id); std::array xpk; + session_id.remove_prefix(2); oxenc::from_hex(session_id.begin(), session_id.end(), xpk.begin()); return xpk; } From 8d9ce6e30153a785b13354c99a9a210d5e8fc1a7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 03:30:56 -0300 Subject: [PATCH 026/572] Don't truncate secretkey in C API --- src/config/internal.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/internal.hpp b/src/config/internal.hpp index d94faabc..e1a0dfc9 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -43,7 +43,7 @@ template size_t dumplen, char* error) { assert(ed25519_secretkey_bytes); - ustring_view ed25519_secretkey{ed25519_secretkey_bytes, 32}; + ustring_view ed25519_secretkey{ed25519_secretkey_bytes, 64}; std::optional dump; if (dumpstr && dumplen) dump.emplace(dumpstr, dumplen); @@ -64,7 +64,7 @@ template ustring_view ed25519_pubkey{ed25519_pubkey_bytes, 32}; std::optional ed25519_secretkey; if (ed25519_secretkey_bytes) - ed25519_secretkey.emplace(ed25519_secretkey_bytes, 32); + ed25519_secretkey.emplace(ed25519_secretkey_bytes, 64); std::optional dump; if (dump_bytes && dumplen) dump.emplace(dump_bytes, dumplen); From 744b25eae61dbdc60c472ec9b60854df73a045c6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 11:57:30 -0300 Subject: [PATCH 027/572] Fix broken logic in group_keys --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index bc76e731..6d077506 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -146,7 +146,7 @@ std::vector Keys::group_keys() const { std::vector ret; ret.reserve(keys_.size() + !pending_key_config_.empty()); - if (pending_key_config_.empty()) + if (!pending_key_config_.empty()) ret.emplace_back(pending_key_.data(), 32); for (auto it = keys_.rbegin(); it != keys_.rend(); ++it) From 3c5f74b470487769b54f7eda66c348adc0bb2741 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 19:03:36 -0300 Subject: [PATCH 028/572] Add supplemental key messages --- include/session/config/groups/keys.hpp | 61 ++++- src/config/groups/keys.cpp | 329 ++++++++++++++++++++----- 2 files changed, 321 insertions(+), 69 deletions(-) diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 2acbbed7..f1cb0841 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -31,13 +31,25 @@ using namespace std::literals; /// /// Fields used (in ascii order): /// # -- 24-byte nonce used for all the encrypted values in this message; required. -/// + -- set to 1 if this is a supplemental key message; omitted for a full key message. (It's -/// important that this key sort earlier than any fields that can differ between -/// supplemental/non-supplemental fields so we can identify the message type while parsing it). +/// +/// For non-supplemental messages: +/// /// G -- monotonically incrementing counter identifying key generation changes /// K -- encrypted copy of the key for admins (omitted for `+` incremental key messages) /// k -- packed bytes of encrypted keys for non-admin members; this is a single byte string in which /// each 48 bytes is a separate encrypted value. +/// +/// For supplemental messages: +/// + -- encrypted supplemental key info list; this is a list of encrypted values, encrypted for +/// each member to whom keys are being disclosed. The *decrypted* value of these entries are +/// the same value (encrypted separately for each member) which is a bt-encoded list of dicts +/// where each dict contains keys: +/// - g -- the key generation +/// - k -- the key itself (32 bytes). +/// - t -- the storage timestamp of the key (so that recipients know when keys expire) +/// +/// And finally, for both types: +/// /// ~ -- signature of the message signed by the group's master keypair, signing the message value up /// to but not including the ~ keypair. The signature must be the last key in the dict (thus /// `~` since it is the largest 7-bit ascii character value). Note that this signature @@ -45,7 +57,7 @@ using namespace std::literals; /// /// Some extra details: /// -/// - each copy of the encryption key uses xchacha20_poly1305 using the `n` nonce +/// - each copy of the encryption key uses xchacha20_poly1305 using the `#` nonce /// - the `k` members list gets padded with junk entries up to the next multiple of 75 (for /// non-supplemental messages). /// - the decryption key for the admin version of the key is H(admin_seed, @@ -97,6 +109,10 @@ class Keys final : public ConfigSig { // Loads existing state from a previous dump of keys data void load_dump(ustring_view dump); + // Inserts a key into the correct place in `keys_`. Returns true if the key was inserted, false + // if it already existed. + bool insert_key(const key_info& key); + public: /// The multiple of members keys we include in the message; we add junk entries to the key list /// to reach a multiple of this. 75 is chosen because it's a decently large human-round number @@ -208,6 +224,16 @@ class Keys final : public ConfigSig { /// - `ustring_view` of the most current group encryption key. ustring_view group_enc_key() const; + /// API: groups/Keys::is_admin + /// + /// True if we have admin permissions (i.e. we know the group's master secret key). + /// + /// Inputs: none. + /// + /// Outputs: + /// - `true` if this object knows the group's master key + bool admin() const { return _sign_sk && _sign_pk; } + /// API: groups/Keys::rekey /// /// Generate a new encryption key for the group and returns an encrypted key message to be @@ -244,6 +270,33 @@ class Keys final : public ConfigSig { /// confirmed or superceded). ustring_view rekey(Info& info, Members& members); + /// API: groups/Keys::key_supplement + /// + /// Generates a supplemental key message for one or more session IDs. This is used to + /// distribute an existing key to a new member so that that member can access existing keys 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 + /// + /// Inputs: + /// - `sid` or `sids` -- session ID(s) of the members to generate a supplemental key for (there + /// are two versions of this function, one taking a single ID and one taking a vector). + /// Session IDs are specified in hex. + /// - `all` -- if true (the default) then generate a supplemental message for *all* current + /// keys; if false then only generate one for the most recent key. + /// + /// Outputs: + /// - `ustring` containing the message that should be pushed to the swarm containing encrypted + /// keys for the given user(s). + ustring key_supplement(std::vector sids, bool all = true) const; + ustring key_supplement(std::string sid, bool all = true) const { + return key_supplement(std::vector{{std::move(sid)}}, all); + } + /// API: groups/Keys::pending_config /// /// If a rekey has been performed but not yet confirmed then this will contain the config diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 6d077506..d9f2f0f6 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -46,7 +46,7 @@ Keys::Keys( if (dumped) { load_dump(*dumped); - } else if (_sign_sk) { + } else if (admin()) { rekey(info, members); } } @@ -179,7 +179,7 @@ static const ustring_view enc_key_member_hash_key = to_unsigned_sv("SessionGroup static const ustring_view junk_seed_hash_key = to_unsigned_sv("SessionGroupJunkMembers"sv); ustring_view Keys::rekey(Info& info, Members& members) { - if (!_sign_sk || !_sign_pk) + if (!admin()) throw std::logic_error{ "Unable to issue a new group encryption key without the main group keys"}; @@ -222,6 +222,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { std::array h1; crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); for (const auto& m : members) @@ -357,12 +358,164 @@ ustring_view Keys::rekey(Info& info, Members& members) { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } +ustring Keys::key_supplement(std::vector sids, bool all) const { + if (!admin()) + throw std::logic_error{ + "Unable to issue supplemental group encryption keys without the main group keys"}; + + if (keys_.empty()) + throw std::logic_error{ + "Unable to create supplemental keys: this object has no keys at all"}; + + // For members we calculate the outer encryption key as H(aB || A || B). But because we only + // have `B` (the session id) as an x25519 pubkey, we do this in x25519 space, which means we + // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. + auto group_xpk = compute_xpk(_sign_pk->data()); + + sodium_cleared> group_xsk; + crypto_sign_ed25519_sk_to_curve25519(group_xsk.data(), _sign_sk.data()); + + // We need quasi-randomness here for the nonce: full secure random would be great, except that + // different admins encrypting for the same update would always create different keys, but we + // want it deterministic so that that doesn't happen. + // + // So we use a nonce of: + // + // H1(member0 || member1 || ... || memberN || keysdata || H2(group_secret_key)) + // + // where: + // - H1(.) = 24-byte BLAKE2b keyed hash with key "SessionGroupKeyGen" + // - memberI is the full session ID of each member included in this key update, expressed in hex + // (66 chars), in sorted order. + // - keysdata is the unencrypted inner value that we are encrypting for each supplemental member + // - H2(.) = 32-byte BLAKE2b keyed hash of the sodium group secret key seed (just the 32 byte, + // not the full 64 byte with the pubkey in the second half), key "SessionGroupKeySeed" + + std::string supp_keys; + { + oxenc::bt_list_producer supp; + for (auto& ki : keys_) { + auto d = supp.append_dict(); + d.append("g", ki.generation); + d.append("k", from_unsigned_sv(ki.key)); + d.append( + "t", + std::chrono::duration_cast( + ki.timestamp.time_since_epoch()) + .count()); + } + supp_keys = std::move(supp).str(); + } + + std::array h1; + + crypto_generichash_blake2b_state st; + + crypto_generichash_blake2b_init( + &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); + + for (const auto& sid : sids) + crypto_generichash_blake2b_update(&st, to_unsigned(sid.data()), sid.size()); + + crypto_generichash_blake2b_update(&st, to_unsigned(supp_keys.data()), supp_keys.size()); + + std::array h2 = seed_hash(seed_hash_key); + crypto_generichash_blake2b_update(&st, h2.data(), h2.size()); + + crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); + + ustring_view nonce{h1.data(), h1.size()}; + + oxenc::bt_dict_producer d{}; + + d.append("#", from_unsigned_sv(nonce)); + + { + auto list = d.append_list("+"); + std::vector encrypted; + encrypted.resize(supp_keys.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + size_t member_count = 0; + + for (auto& sid : sids) { + auto m_xpk = session_id_xpk(sid); + + // Calculate the encryption key: H(aB || A || B) + std::array member_k; + if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) + continue; // The scalarmult failed; maybe a bad session id? + + crypto_generichash_blake2b_init( + &st, + enc_key_member_hash_key.data(), + enc_key_member_hash_key.size(), + member_k.size()); + crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); + crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); + crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); + crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); + + crypto_aead_xchacha20poly1305_ietf_encrypt( + encrypted.data(), + nullptr, + to_unsigned(supp_keys.data()), + supp_keys.size(), + nullptr, + 0, + nullptr, + nonce.data(), + member_k.data()); + + list.append(from_unsigned_sv(encrypted)); + + member_count++; + } + + if (member_count == 0) + throw std::runtime_error{ + "Unable to construct supplemental messages: invalid session ids given"}; + } + + // Finally we sign the message at put it as the ~ key (which is 0x7f, and thus comes later than + // any other ascii key). + auto to_sign = to_unsigned_sv(d.view()); + // The view contains the trailing "e", but we don't sign it (we are going to append the + // signature there instead): + to_sign.remove_suffix(1); + auto sig = signer_(to_sign); + if (sig.size() != 64) + throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; + + d.append("~", from_unsigned_sv(sig)); + + return ustring{to_unsigned_sv(d.view())}; +} + std::optional Keys::pending_config() const { if (pending_key_config_.empty()) return std::nullopt; return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } +bool Keys::insert_key(const key_info& new_key) { + auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); + if (it != keys_.end() && new_key == *it) + // We found a key we already had, so just ignore it. + return false; + + if (it == keys_.begin() && new_key.generation < keys_.front().generation && + keys_.front().timestamp + KEY_EXPIRY < keys_.back().timestamp) + // The new one is older than the front one, and the front one is already more than + // KEY_EXPIRY before the last one, so this new one is stale. + return false; + + keys_.insert(it, new_key); + remove_expired(); + needs_dump_ = true; + + return true; +} + void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members) { oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; @@ -376,57 +529,11 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, throw config_value_error{"Key message has no nonce"}; auto nonce = to_unsigned_sv(d.consume_string_view()); - bool supplemental = false; - if (d.skip_until("+")) { - auto supp = d.consume_integer(); - if (supp == 0 || supp == 1) - supplemental = static_cast(supp); - else - throw config_value_error{ - "Unexpected value " + std::to_string(supp) + " for '+' key (expected 0/1)"}; - } - bool found_key = false; sodium_cleared new_key{}; - new_key.timestamp = sys_time_from_ms(timestamp_ms); - - if (!d.skip_until("G")) - throw config_value_error{"Key message missing required generation (G) field"}; - - new_key.generation = d.consume_integer(); - if (new_key.generation < 0) - throw config_value_error{"Key message contains invalid negative generation"}; - - if (!supplemental) { - if (!d.skip_until("K")) - throw config_value_error{ - "Non-supplemental key message is missing required admin key (K)"}; - - auto admin_key = to_unsigned_sv(d.consume_string_view()); - if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw config_value_error{"Key message has invalid admin key length"}; - - if (_sign_sk) { - auto k = seed_hash(enc_key_admin_hash_key); - - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - new_key.key.data(), - nullptr, - nullptr, - admin_key.data(), - admin_key.size(), - nullptr, - 0, - nonce.data(), - k.data())) - throw config_value_error{"Failed to decrypt admin key from key message"}; - - found_key = true; - } - } sodium_cleared> member_dec_key; - if (!found_key) { + if (!admin()) { sodium_cleared> member_xsk; crypto_sign_ed25519_sk_to_curve25519(member_xsk.data(), user_ed25519_sk.data()); auto member_xpk = compute_xpk(user_ed25519_sk.data() + 32); @@ -449,6 +556,111 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, crypto_generichash_blake2b_final(&st, member_dec_key.data(), member_dec_key.size()); } + if (d.skip_until("+")) { + // This is a supplemental keys message, not a full one + auto supp = d.consume_list_consumer(); + + while (!supp.is_finished()) { + + int member_key_count = 0; + for (; !supp.is_finished(); member_key_count++) { + auto encrypted = to_unsigned_sv(supp.consume_string_view()); + // Expect an encrypted message like this, which has a minimum valid size (if both g + // and t are 0 for some reason) of: + // d -- 1 + // 1:k 32:... -- +38 + // 1:g i1e -- + 6 + // 1:t iXe -- + 6 + // e + 1 + // --- + // 52 + if (encrypted.size() < 52 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{ + "Supplemental key message has invalid key info size at index " + + std::to_string(member_key_count)}; + + if (found_key || admin()) + continue; + + ustring plaintext; + plaintext.resize(encrypted.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + plaintext.data(), + nullptr, + nullptr, + encrypted.data(), + encrypted.size(), + nullptr, + 0, + nonce.data(), + member_dec_key.data())) { + // Decryption success, we found our key list! + found_key = true; + + oxenc::bt_list_consumer key_infos{from_unsigned_sv(plaintext)}; + while (!key_infos.is_finished()) { + auto keyinf = key_infos.consume_dict_consumer(); + if (!keyinf.skip_until("g")) + throw config_value_error{ + "Invalid supplemental key message: no `g` generation"}; + new_key.generation = keyinf.consume_integer(); + if (!keyinf.skip_until("k")) + throw config_value_error{ + "Invalid supplemental key message: no `k` key data"}; + auto key_val = keyinf.consume_string_view(); + if (key_val.size() != 32) + throw config_value_error{ + "Invalid supplemental key message: `k` key has wrong size"}; + std::memcpy(new_key.key.data(), key_val.data(), 32); + if (!keyinf.skip_until("t")) + throw config_value_error{ + "Invalid supplemental key message: no `t` timestamp"}; + new_key.timestamp = sys_time_from_ms(keyinf.consume_integer()); + + insert_key(new_key); + } + } + } + } + + return; + } + + new_key.timestamp = sys_time_from_ms(timestamp_ms); + + if (!d.skip_until("G")) + throw config_value_error{"Key message missing required generation (G) field"}; + + new_key.generation = d.consume_integer(); + if (new_key.generation < 0) + throw config_value_error{"Key message contains invalid negative generation"}; + + if (!d.skip_until("K")) + throw config_value_error{"Non-supplemental key message is missing required admin key (K)"}; + + auto admin_key = to_unsigned_sv(d.consume_string_view()); + if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{"Key message has invalid admin key length"}; + + if (admin()) { + auto k = seed_hash(enc_key_admin_hash_key); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + new_key.key.data(), + nullptr, + nullptr, + admin_key.data(), + admin_key.size(), + nullptr, + 0, + nonce.data(), + k.data())) + throw config_value_error{"Failed to decrypt admin key from key message"}; + + found_key = true; + } + // Even if we're already found a key we still parse these, so that admins and all users have the // same error conditions for rejecting an invalid config message. if (!d.skip_until("k")) @@ -478,28 +690,15 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, member_dec_key.data())) { // Decryption success, we found our key! found_key = true; + insert_key(new_key); } } - if (!supplemental && member_key_count % MESSAGE_KEY_MULTIPLE != 0) + if (member_key_count % MESSAGE_KEY_MULTIPLE != 0) throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; verify_config_sig(d, data, verifier_); - if (found_key) { - auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); - if (it != keys_.end() && new_key == *it) { - // We found a key we already had, so just ignore it. - found_key = false; - } else { - keys_.insert(it, new_key); - - remove_expired(); - - needs_dump_ = true; - } - } - // If this is our pending config or this has a later generation than our pending config then // drop our pending status. if (!pending_key_config_.empty() && @@ -551,7 +750,7 @@ void Keys::remove_expired() { } bool Keys::needs_rekey() const { - if (!_sign_sk || !_sign_pk || keys_.size() < 2) + if (!admin() || keys_.size() < 2) return false; // We rekey if the max generation value is being used across multiple keys (which indicates some From e83f479aaa43970cf154e3d1031af434b655a707 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 19:29:26 -0300 Subject: [PATCH 029/572] fix admin key loading --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index d9f2f0f6..e75bd48b 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -243,7 +243,6 @@ ustring_view Keys::rekey(Info& info, Members& members) { oxenc::bt_dict_producer d{}; d.append("#", from_unsigned_sv(nonce)); - // d.append("+", 0); // Not supplemental, so leave off static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); static_assert(crypto_aead_xchacha20poly1305_ietf_ABYTES == 16); @@ -659,6 +658,7 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, throw config_value_error{"Failed to decrypt admin key from key message"}; found_key = true; + insert_key(new_key); } // Even if we're already found a key we still parse these, so that admins and all users have the From cb89c0fd70825a8005b599565de400286a5cc3f5 Mon Sep 17 00:00:00 2001 From: dr7ana Date: Thu, 24 Aug 2023 13:03:06 -0700 Subject: [PATCH 030/572] off by 2 error (Jason) --- include/session/config/user_groups.h | 16 ++-- include/session/config/user_groups.hpp | 2 +- tests/CMakeLists.txt | 8 ++ tests/test_group_keys.cpp | 121 ++++++++++++++++++++----- 4 files changed, 113 insertions(+), 34 deletions(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 9410add1..2728b029 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -43,14 +43,14 @@ typedef struct ugroups_legacy_group_info { /// Struct holding (non-legacy) group info; this struct owns allocated memory and *must* be freed /// via either `ugroups_group_free()` or `ugroups_set_free_group()` when finished with it. typedef struct ugroups_group_info { - char id[67]; // in hex; 66 hex chars + null terminator - - bool have_secretkey; // Will be true if the `secretkey` is populated - unsigned char secretkey[64]; // If `have_secretkey` is set then this is the libsodium-style - // "secret key" for the group (i.e. 32 byte seed + 32 byte pubkey) - bool have_auth_sig; // Will be true if the `auth_sig` is populated - unsigned char auth_sig[64]; // If `have_auth_sig` is set then this is the authentication - // signature that can be used to access the swarm. + char id[67]; // in hex; 66 hex chars + null terminator + + bool have_secretkey; // Will be true if the `secretkey` is populated + unsigned char secretkey[64]; // If `have_secretkey` is set then this is the libsodium-style + // "secret key" for the group (i.e. 32 byte seed + 32 byte pubkey) + bool have_auth_sig; // Will be true if the `auth_sig` is populated + unsigned char auth_sig[64]; // If `have_auth_sig` is set then this is the authentication + // signature that can be used to access the swarm. int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned // (with higher meaning pinned higher). diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index b9684348..3549e55c 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -190,7 +190,7 @@ struct group_info : base_group_info { // Internal ctor/method for C API implementations: group_info(const struct ugroups_group_info& c); // From c struct - void into(struct ugroups_group_info& c) const; // Into c struct + void into(struct ugroups_group_info& c) const; // Into c struct private: friend class UserGroups; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dbf39f45..e73548bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,14 @@ add_executable(testAll test_xed25519.cpp ) +add_executable(testKeys + test_group_keys.cpp +) + +target_link_libraries(testKeys PRIVATE + config + Catch2::Catch2WithMain) + target_link_libraries(testAll PRIVATE config Catch2::Catch2WithMain) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 07897171..bda24ce4 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -19,28 +19,48 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -/* TEST_CASE("Group Keys", "[config][groups][keys]") { + const ustring group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; + const std::array seeds = { - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes, - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes, - "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes, - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes}; + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes, // admin1 + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes, // admin2 + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 + "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 + "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 + }; + + size_t n_seeds = seeds.size(); + std::array, seeds.size()> ed_pk; std::array, seeds.size()> ed_sk; - for (size_t i = 0; i < seeds.size(); i++) { + + std::array group_pk; + std::array group_sk; + + for (size_t i = 0; i < n_seeds; i++) { crypto_sign_ed25519_seed_keypair( ed_pk[i].data(), ed_sk[i].data(), reinterpret_cast(seeds[i].data())); - CHECK(oxenc::to_hex(seeds[i].begin(), seeds[i].end()) == - oxenc::to_hex(ed_sk[i].begin(), ed_sk[i].begin() + 32)); + REQUIRE(oxenc::to_hex(seeds[i].begin(), seeds[i].end()) == + oxenc::to_hex(ed_sk[i].begin(), ed_sk[i].begin() + 32)); } - constexpr size_t ADMIN1 = 0, ADMIN2 = 1, MEMBER1 = 2, MEMBER2 = 3, GROUP = 4; + 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)); + + constexpr size_t ADMIN1 = 0, ADMIN2 = 1, MEMBER1 = 2, MEMBER2 = 3, MEMBER3 = 4, MEMBER4 = 5; + REQUIRE(oxenc::to_hex(group_pk.begin(), group_pk.end()) == + "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); REQUIRE(oxenc::to_hex(ed_pk[ADMIN1].begin(), ed_pk[ADMIN1].end()) == "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); REQUIRE(oxenc::to_hex(ed_pk[ADMIN2].begin(), ed_pk[ADMIN2].end()) == @@ -48,30 +68,81 @@ TEST_CASE("Group Keys", "[config][groups][keys]") { REQUIRE(oxenc::to_hex(ed_pk[MEMBER1].begin(), ed_pk[MEMBER1].end()) == "8b79719da06ee8a14823f0c8d740aabb134ab7cbc174b8c1a022a27c0964abfd"); REQUIRE(oxenc::to_hex(ed_pk[MEMBER2].begin(), ed_pk[MEMBER2].end()) == + "a2b000e46c13859c0eecea72af9db9e06b22cad767ccf487b004b7592628a595"); + REQUIRE(oxenc::to_hex(ed_pk[MEMBER3].begin(), ed_pk[MEMBER3].end()) == + "dee285469b5ae983e03749aa41ff5b723f2bcad4f31d0de6515275f40e7b32cb"); + REQUIRE(oxenc::to_hex(ed_pk[MEMBER4].begin(), ed_pk[MEMBER4].end()) == "d813a070116a8c74e6fcbb3f53d5698a14b6236fcca9bb3136acff749dacdcc4"); - REQUIRE(oxenc::to_hex(ed_pk[GROUP].begin(), ed_pk[GROUP].end()) == - "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); - std::array, seeds.size() - 1> info; - std::array, seeds.size() - 1> members; - std::array, seeds.size() - 1> keys; - for (size_t i = 0; i < GROUP; i++) { + std::array, seeds.size()> info; + std::array, seeds.size()> members; + std::array, seeds.size()> keys; + + for (size_t i = 0; i < n_seeds; i++) { info[i] = std::make_unique( - to_usv(ed_pk[GROUP]), - i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + to_usv(group_pk), + i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, std::nullopt); members[i] = std::make_unique( - to_usv(ed_pk[GROUP]), - i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + to_usv(group_pk), + i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, std::nullopt); keys[i] = std::make_unique( to_usv(ed_sk[i]), - to_usv(ed_pk[GROUP]), - i <= ADMIN2 ? std::make_optional(to_usv(ed_sk[GROUP])) : std::nullopt, + to_usv(group_pk), + i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, std::nullopt, *info[i], - *members[i] - ); + *members[i]); + } + + std::vector sids; + + for (int i = 0; i < n_seeds; ++i) { + std::array sid; + memcpy(&sid[1], &ed_pk[i], 32); + sid[0] = 0x05; + sids.push_back(oxenc::to_hex(sid.begin(), sid.end())); } + + for (const auto& m : members) + REQUIRE(m->size() == 0); + + std::vector> info_configs; + std::vector> mem_configs; + + SECTION("Add members and re-key") { + for (int i = MEMBER1; i < MEMBER4; ++i) { + auto m = members[ADMIN1]->get_or_construct(sids[i]); + m.admin = false; + members[ADMIN1]->set(m); + } + + CHECK(members[ADMIN1]->needs_push()); + + // get new configs + auto new_keys_config = keys[ADMIN1]->rekey(*info[ADMIN1], *members[ADMIN1]); + auto [iseq, new_info_config, iobs] = info[ADMIN1]->push(); + info[ADMIN1]->confirm_pushed(iseq, "fakehash1"); + auto [mseq, new_mem_config, mobs] = members[ADMIN1]->push(); + members[ADMIN1]->confirm_pushed(mseq, "fakehash1"); + + info_configs.emplace_back("fakehash1", new_info_config); + mem_configs.emplace_back("fakehash1", new_mem_config); + + for (int i = MEMBER1; i < MEMBER4; ++i) { + auto n = info[i]->merge(info_configs); + auto m = members[i]->merge(mem_configs); + } + } + + // SECTION("Remove member 4 and re-key") { + // REQUIRE(members[ADMIN1]->erase(sids[MEMBER4])); + + // // get new configs + // auto new_keys_config = keys[ADMIN1]->rekey(*info[ADMIN1], *members[ADMIN1]); + // auto [iseq, new_info_config, iobs] = info[ADMIN1]->push(); + // auto [mseq, new_members_config, mobs] = members[ADMIN1]->push(); + + // } } -*/ From b4cf7e2a9994965453e227064064dc3f0d53624f Mon Sep 17 00:00:00 2001 From: dr7ana Date: Fri, 25 Aug 2023 07:49:58 -0700 Subject: [PATCH 031/572] Config keys unit tests - new group keys unit tests runs through relevant functionalities - add/remove users, change group info, add admin, verify encryption compression --- tests/test_group_keys.cpp | 327 +++++++++++++++++++++++++++----------- tests/utils.hpp | 4 + 2 files changed, 241 insertions(+), 90 deletions(-) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index bda24ce4..cf27b330 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -21,35 +21,69 @@ using namespace session::config; TEST_CASE("Group Keys", "[config][groups][keys]") { + struct pseudo_client { + const bool is_admin; + + const ustring seed; + std::string session_id; + + std::array public_key; + std::array secret_key; + + std::unique_ptr keys; + std::unique_ptr info; + std::unique_ptr members; + + pseudo_client(ustring s, bool a, unsigned char* gpk, std::optional gsk) : + seed{s}, is_admin{a} { + crypto_sign_ed25519_seed_keypair( + public_key.data(), + secret_key.data(), + reinterpret_cast(seed.data())); + + REQUIRE(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(secret_key.begin(), secret_key.begin() + 32)); + + std::array sid; + int rc = crypto_sign_ed25519_pk_to_curve25519(&sid[1], public_key.data()); + REQUIRE(rc == 0); + sid[0] = 0x05; + session_id = oxenc::to_hex(sid.begin(), sid.end()); + + info = std::make_unique( + ustring_view{gpk, 32}, + is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt); + members = std::make_unique( + ustring_view{gpk, 32}, + is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt); + keys = std::make_unique( + to_usv(secret_key), + ustring_view{gpk, 32}, + is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt, + *info, + *members); + } + }; + const ustring group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - - const std::array seeds = { - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes, // admin1 - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes, // admin2 + const ustring admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + const std::array member_seeds = { "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 }; - size_t n_seeds = seeds.size(); - - std::array, seeds.size()> ed_pk; - std::array, seeds.size()> ed_sk; - std::array group_pk; std::array group_sk; - for (size_t i = 0; i < n_seeds; i++) { - crypto_sign_ed25519_seed_keypair( - ed_pk[i].data(), - ed_sk[i].data(), - reinterpret_cast(seeds[i].data())); - REQUIRE(oxenc::to_hex(seeds[i].begin(), seeds[i].end()) == - oxenc::to_hex(ed_sk[i].begin(), ed_sk[i].begin() + 32)); - } - crypto_sign_ed25519_seed_keypair( group_pk.data(), group_sk.data(), @@ -57,92 +91,205 @@ TEST_CASE("Group Keys", "[config][groups][keys]") { REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - constexpr size_t ADMIN1 = 0, ADMIN2 = 1, MEMBER1 = 2, MEMBER2 = 3, MEMBER3 = 4, MEMBER4 = 5; - - REQUIRE(oxenc::to_hex(group_pk.begin(), group_pk.end()) == - "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); - REQUIRE(oxenc::to_hex(ed_pk[ADMIN1].begin(), ed_pk[ADMIN1].end()) == - "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); - REQUIRE(oxenc::to_hex(ed_pk[ADMIN2].begin(), ed_pk[ADMIN2].end()) == - "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); - REQUIRE(oxenc::to_hex(ed_pk[MEMBER1].begin(), ed_pk[MEMBER1].end()) == - "8b79719da06ee8a14823f0c8d740aabb134ab7cbc174b8c1a022a27c0964abfd"); - REQUIRE(oxenc::to_hex(ed_pk[MEMBER2].begin(), ed_pk[MEMBER2].end()) == - "a2b000e46c13859c0eecea72af9db9e06b22cad767ccf487b004b7592628a595"); - REQUIRE(oxenc::to_hex(ed_pk[MEMBER3].begin(), ed_pk[MEMBER3].end()) == - "dee285469b5ae983e03749aa41ff5b723f2bcad4f31d0de6515275f40e7b32cb"); - REQUIRE(oxenc::to_hex(ed_pk[MEMBER4].begin(), ed_pk[MEMBER4].end()) == - "d813a070116a8c74e6fcbb3f53d5698a14b6236fcca9bb3136acff749dacdcc4"); - - std::array, seeds.size()> info; - std::array, seeds.size()> members; - std::array, seeds.size()> keys; - - for (size_t i = 0; i < n_seeds; i++) { - info[i] = std::make_unique( - to_usv(group_pk), - i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, - std::nullopt); - members[i] = std::make_unique( - to_usv(group_pk), - i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, - std::nullopt); - keys[i] = std::make_unique( - to_usv(ed_sk[i]), - to_usv(group_pk), - i <= ADMIN2 ? std::make_optional(to_usv(group_sk)) : std::nullopt, - std::nullopt, - *info[i], - *members[i]); - } + std::vector admins; + std::vector members; - std::vector sids; + // Initialize admin and member objects + admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); + admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); - for (int i = 0; i < n_seeds; ++i) { - std::array sid; - memcpy(&sid[1], &ed_pk[i], 32); - sid[0] = 0x05; - sids.push_back(oxenc::to_hex(sid.begin(), sid.end())); - } + for (int i = 0; i < 4; ++i) + members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + REQUIRE(admins[0].session_id == + "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); + REQUIRE(admins[1].session_id == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + REQUIRE(members[0].session_id == + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); + REQUIRE(members[1].session_id == + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); + REQUIRE(members[2].session_id == + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); + REQUIRE(members[3].session_id == + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); + + for (const auto& a : admins) + REQUIRE(a.members->size() == 0); for (const auto& m : members) - REQUIRE(m->size() == 0); + REQUIRE(m.members->size() == 0); std::vector> info_configs; std::vector> mem_configs; - SECTION("Add members and re-key") { - for (int i = MEMBER1; i < MEMBER4; ++i) { - auto m = members[ADMIN1]->get_or_construct(sids[i]); - m.admin = false; - members[ADMIN1]->set(m); - } + // add admin account, re-key, distribute + auto& admin1 = admins[0]; - CHECK(members[ADMIN1]->needs_push()); + auto m = admin1.members->get_or_construct(admin1.session_id); + m.admin = true; + m.name = "Admin1"; + admin1.members->set(m); - // get new configs - auto new_keys_config = keys[ADMIN1]->rekey(*info[ADMIN1], *members[ADMIN1]); - auto [iseq, new_info_config, iobs] = info[ADMIN1]->push(); - info[ADMIN1]->confirm_pushed(iseq, "fakehash1"); - auto [mseq, new_mem_config, mobs] = members[ADMIN1]->push(); - members[ADMIN1]->confirm_pushed(mseq, "fakehash1"); + CHECK(admin1.members->needs_push()); - info_configs.emplace_back("fakehash1", new_info_config); - mem_configs.emplace_back("fakehash1", new_mem_config); + auto new_keys_config1 = admin1.keys->rekey(*admin1.info, *admin1.members); + CHECK(not new_keys_config1.empty()); - for (int i = MEMBER1; i < MEMBER4; ++i) { - auto n = info[i]->merge(info_configs); - auto m = members[i]->merge(mem_configs); - } + auto [iseq1, new_info_config1, iobs1] = admin1.info->push(); + admin1.info->confirm_pushed(iseq1, "fakehash1"); + info_configs.emplace_back("fakehash1", new_info_config1); + + auto [mseq1, new_mem_config1, mobs1] = admin1.members->push(); + admin1.members->confirm_pushed(mseq1, "fakehash1"); + mem_configs.emplace_back("fakehash1", new_mem_config1); + + /* Even though we have only added one admin, admin2 will still be able to see group info + like group size and merge all configs. This is because they have loaded the key config + message, which they can decrypt with the group secret key. + */ + for (auto& a : admins) { + a.keys->load_key_message(new_keys_config1, get_timestamp(), *a.info, *a.members); + CHECK(a.info->merge(info_configs) == 1); + CHECK(a.members->merge(mem_configs) == 1); + CHECK(a.members->size() == 1); } - // SECTION("Remove member 4 and re-key") { - // REQUIRE(members[ADMIN1]->erase(sids[MEMBER4])); + /* All attempts to merge non-admin members will throw, as none of the non admin members + will be able to decrypt the new info/member configs using the updated keys + */ + for (auto& m : members) { + m.keys->load_key_message(new_keys_config1, get_timestamp(), *m.info, *m.members); + CHECK_THROWS(m.info->merge(info_configs)); + CHECK_THROWS(m.members->merge(mem_configs)); + CHECK(m.members->size() == 0); + } + + info_configs.clear(); + mem_configs.clear(); + + // add non-admin members, re-key, distribute + for (int i = 0; i < members.size(); ++i) { + auto m = admin1.members->get_or_construct(members[i].session_id); + m.admin = false; + m.name = "Member" + std::to_string(i); + admin1.members->set(m); + } + + CHECK(admin1.members->needs_push()); + + auto new_keys_config2 = admin1.keys->rekey(*admin1.info, *admin1.members); + CHECK(not new_keys_config2.empty()); + + auto [iseq2, new_info_config2, iobs2] = admin1.info->push(); + admin1.info->confirm_pushed(iseq2, "fakehash2"); + info_configs.emplace_back("fakehash2", new_info_config2); + + auto [mseq2, new_mem_config2, mobs2] = admin1.members->push(); + admin1.members->confirm_pushed(mseq2, "fakehash2"); + mem_configs.emplace_back("fakehash2", new_mem_config2); + + for (auto& a : admins) { + a.keys->load_key_message(new_keys_config2, get_timestamp(), *a.info, *a.members); + CHECK(a.info->merge(info_configs) == 1); + CHECK(a.members->merge(mem_configs) == 1); + CHECK(a.members->size() == 5); + } + + for (auto& m : members) { + m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); + CHECK(m.info->merge(info_configs) == 1); + CHECK(m.members->merge(mem_configs) == 1); + CHECK(m.members->size() == 5); + } + + info_configs.clear(); + mem_configs.clear(); + + // change group info, re-key, distribute + admin1.info->set_name("tomatosauce"s); + + CHECK(admin1.info->needs_push()); + + auto new_keys_config3 = admin1.keys->rekey(*admin1.info, *admin1.members); + CHECK(not new_keys_config3.empty()); + + auto [iseq3, new_info_config3, iobs3] = admin1.info->push(); + admin1.info->confirm_pushed(iseq3, "fakehash3"); + info_configs.emplace_back("fakehash3", new_info_config3); + + auto [mseq3, new_mem_config3, mobs3] = admin1.members->push(); + admin1.members->confirm_pushed(mseq3, "fakehash3"); + mem_configs.emplace_back("fakehash3", new_mem_config3); + + for (auto& a : admins) { + a.keys->load_key_message(new_keys_config3, get_timestamp(), *a.info, *a.members); + CHECK(a.info->merge(info_configs) == 1); + CHECK(a.members->merge(mem_configs) == 1); + CHECK(a.info->get_name() == "tomatosauce"s); + } + + for (auto& m : members) { + m.keys->load_key_message(new_keys_config3, get_timestamp(), *m.info, *m.members); + CHECK(m.info->merge(info_configs) == 1); + CHECK(m.members->merge(mem_configs) == 1); + CHECK(m.info->get_name() == "tomatosauce"s); + } + + info_configs.clear(); + mem_configs.clear(); + + // remove members, re-key, distribute + CHECK(admin1.members->erase(members[3].session_id)); + CHECK(admin1.members->erase(members[2].session_id)); + + CHECK(admin1.members->needs_push()); + + auto new_keys_config4 = admin1.keys->rekey(*admin1.info, *admin1.members); + CHECK(not new_keys_config4.empty()); + + auto [iseq4, new_info_config4, iobs4] = admin1.info->push(); + admin1.info->confirm_pushed(iseq4, "fakehash4"); + info_configs.emplace_back("fakehash4", new_info_config4); + + auto [mseq4, new_mem_config4, mobs4] = admin1.members->push(); + admin1.members->confirm_pushed(mseq4, "fakehash4"); + mem_configs.emplace_back("fakehash4", new_mem_config4); + + for (auto& a : admins) { + a.keys->load_key_message(new_keys_config4, get_timestamp(), *a.info, *a.members); + CHECK(a.info->merge(info_configs) == 1); + CHECK(a.members->merge(mem_configs) == 1); + CHECK(a.members->size() == 3); + } + + for (int i = 0; i < 2; ++i) { + auto& m = members[i]; + m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); + CHECK(m.info->merge(info_configs) == 1); + CHECK(m.members->merge(mem_configs) == 1); + CHECK(m.members->size() == 3); + } + + for (int i = 2; i < 4; ++i) { + auto& m = members[i]; + m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); + CHECK(m.info->merge(info_configs) == 0); + CHECK(m.members->merge(mem_configs) == 0); + CHECK(m.members->size() == 5); + } + + info_configs.clear(); + mem_configs.clear(); + + // middle-out time + auto msg = "hello to all my friends sitting in the tomato sauce"s; + + for (int i = 0; i < 5; ++i) + msg += msg; - // // get new configs - // auto new_keys_config = keys[ADMIN1]->rekey(*info[ADMIN1], *members[ADMIN1]); - // auto [iseq, new_info_config, iobs] = info[ADMIN1]->push(); - // auto [mseq, new_members_config, mobs] = members[ADMIN1]->push(); + auto compressed = admin1.keys->encrypt_message(to_usv(msg), true); + auto uncompressed = admin1.keys->encrypt_message(to_usv(msg), false); - // } + CHECK(compressed.size() < msg.size()); + CHECK(compressed.size() < uncompressed.size()); } diff --git a/tests/utils.hpp b/tests/utils.hpp index 3a73a139..f842a076 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -33,6 +33,10 @@ inline constexpr auto operator""_kiB(unsigned long long kiB) { return kiB * 1024; } +inline int64_t get_timestamp() { + return std::chrono::steady_clock::now().time_since_epoch().count(); +} + inline std::string_view to_sv(ustring_view x) { return {reinterpret_cast(x.data()), x.size()}; } From 9f447b6c32811a8af76a4c8d97997387f259b3bf Mon Sep 17 00:00:00 2001 From: dr7ana Date: Fri, 25 Aug 2023 15:28:40 -0700 Subject: [PATCH 032/572] review --- tests/test_group_keys.cpp | 131 +++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index cf27b330..4870fa79 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -6,8 +6,11 @@ #include #include #include +#include #include +#include #include +#include #include #include "utils.hpp" @@ -19,7 +22,7 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -TEST_CASE("Group Keys", "[config][groups][keys]") { +TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { struct pseudo_client { const bool is_admin; @@ -132,8 +135,9 @@ TEST_CASE("Group Keys", "[config][groups][keys]") { CHECK(admin1.members->needs_push()); - auto new_keys_config1 = admin1.keys->rekey(*admin1.info, *admin1.members); - CHECK(not new_keys_config1.empty()); + auto maybe_key_config = admin1.keys->pending_config(); + REQUIRE(maybe_key_config); + auto new_keys_config1 = *maybe_key_config; auto [iseq1, new_info_config1, iobs1] = admin1.info->push(); admin1.info->confirm_pushed(iseq1, "fakehash1"); @@ -293,3 +297,124 @@ TEST_CASE("Group Keys", "[config][groups][keys]") { CHECK(compressed.size() < msg.size()); CHECK(compressed.size() < uncompressed.size()); } + +TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") +{ + struct pseudo_client { + const bool is_admin; + + const ustring seed; + std::string session_id; + + std::array public_key; + std::array secret_key; + + config_group_keys* keys; + config_object* info; + config_object* members; + + pseudo_client(ustring s, bool a, unsigned char* gpk, std::optional gsk) : + seed{s}, is_admin{a} { + crypto_sign_ed25519_seed_keypair( + public_key.data(), + secret_key.data(), + reinterpret_cast(seed.data())); + + REQUIRE(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(secret_key.begin(), secret_key.begin() + 32)); + + std::array sid; + int rc = crypto_sign_ed25519_pk_to_curve25519(&sid[1], public_key.data()); + REQUIRE(rc == 0); + sid[0] = 0x05; + session_id = oxenc::to_hex(sid.begin(), sid.end()); + + int rv = groups_members_init( + &members, + gpk, + is_admin ? *gsk : NULL, + NULL, + 0, + NULL); + REQUIRE(rv == 0); + + rv = groups_info_init( + &info, + gpk, + is_admin ? *gsk : NULL, + NULL, + 0, + NULL); + REQUIRE(rv == 0); + + rv = groups_keys_init( + &keys, + secret_key.data(), + gpk, + is_admin ? *gsk : NULL, + info, + members, + NULL, + 0, + NULL); + REQUIRE(rv == 0); + } + + ~pseudo_client() + { + config_free(info); + config_free(members); + } + }; + + const ustring group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; + const ustring admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + const std::array member_seeds = { + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 + "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 + "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 + "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)); + + std::vector admins; + std::vector members; + + // Initialize admin and member objects + admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); + // admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); + + // for (int i = 0; i < 4; ++i) + // members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + + // REQUIRE(admins[0].session_id == + // "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); + // REQUIRE(admins[1].session_id == + // "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + // REQUIRE(members[0].session_id == + // "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); + // REQUIRE(members[1].session_id == + // "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); + // REQUIRE(members[2].session_id == + // "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); + // REQUIRE(members[3].session_id == + // "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); + + // for (const auto& a : admins) + // REQUIRE(contacts_size(a.members) == 0); + // for (const auto& m : members) + // REQUIRE(contacts_size(m.members) == 0); +} From d1218641b78dac3a3ab0b3b35b8e8d8d778c0638 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 19:59:11 -0300 Subject: [PATCH 033/572] Fix crash when keys_ is empty --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index e75bd48b..37877ae2 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -502,7 +502,7 @@ bool Keys::insert_key(const key_info& new_key) { // We found a key we already had, so just ignore it. return false; - if (it == keys_.begin() && new_key.generation < keys_.front().generation && + if (keys_.size() >= 2 && it == keys_.begin() && new_key.generation < keys_.front().generation && keys_.front().timestamp + KEY_EXPIRY < keys_.back().timestamp) // The new one is older than the front one, and the front one is already more than // KEY_EXPIRY before the last one, so this new one is stale. From c431f125d9a55490bade5b473257fcc29952ef99 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 20:25:51 -0300 Subject: [PATCH 034/572] Fix crash in C API keys init when no dump given --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 37877ae2..2bd4b2fd 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -908,7 +908,7 @@ LIBSESSION_C_API int groups_keys_init( auto c_conf = std::make_unique(); try { - c_conf->internals = new groups::Keys{user_sk, group_pk, group_sk, dump, info, members}; + 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(); From 883710340a14b74ffbac482b8b4b41aadcad446e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Aug 2023 22:33:43 -0300 Subject: [PATCH 035/572] Supplemental config fixes and tests --- include/session/config/groups/keys.hpp | 20 +- src/config/groups/keys.cpp | 183 +++++++------ tests/test_group_keys.cpp | 355 +++++++++++++++---------- 3 files changed, 325 insertions(+), 233 deletions(-) diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index f1cb0841..e8ca92d3 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -273,8 +273,8 @@ class Keys final : public ConfigSig { /// API: groups/Keys::key_supplement /// /// Generates a supplemental key message for one or more session IDs. This is used to - /// distribute an existing key to a new member so that that member can access existing keys and - /// messages. Only admins can call this. + /// 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 @@ -282,19 +282,19 @@ class Keys final : public ConfigSig { /// - 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 you would use rekey() instead of this method. + /// /// Inputs: /// - `sid` or `sids` -- session ID(s) of the members to generate a supplemental key for (there /// are two versions of this function, one taking a single ID and one taking a vector). /// Session IDs are specified in hex. - /// - `all` -- if true (the default) then generate a supplemental message for *all* current - /// keys; if false then only generate one for the most recent key. /// /// Outputs: /// - `ustring` containing the message that should be pushed to the swarm containing encrypted /// keys for the given user(s). - ustring key_supplement(std::vector sids, bool all = true) const; - ustring key_supplement(std::string sid, bool all = true) const { - return key_supplement(std::vector{{std::move(sid)}}, all); + ustring key_supplement(std::vector sids) const; + ustring key_supplement(std::string sid) const { + return key_supplement(std::vector{{std::move(sid)}}); } /// API: groups/Keys::pending_config @@ -354,7 +354,11 @@ class Keys final : public ConfigSig { /// /// Outputs: /// - throws `std::runtime_error` (typically a subclass thereof) on failure to parse. - void load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members); + /// - returns true if we found a key for us in the message, false if we did not. Note that this + /// is mainly informative and does not signal an error: false could mean, for instance, be a + /// supplemental message that wasn't for us. Note also that true doesn't mean keys changed: + /// it could mean we decrypted one for us, but already had it. + bool load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members); /// API: groups/Keys::needs_rekey /// diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 2bd4b2fd..c17062ab 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,8 @@ #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" +using namespace std::literals; + namespace session::config::groups { static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { @@ -156,6 +159,8 @@ std::vector Keys::group_keys() const { } ustring_view Keys::group_enc_key() const { + if (!pending_key_config_.empty()) + return {pending_key_.data(), 32}; if (keys_.empty()) throw std::runtime_error{"group_enc_key failed: Keys object has no keys at all!"}; @@ -357,7 +362,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } -ustring Keys::key_supplement(std::vector sids, bool all) const { +ustring Keys::key_supplement(std::vector sids) const { if (!admin()) throw std::logic_error{ "Unable to issue supplemental group encryption keys without the main group keys"}; @@ -497,10 +502,18 @@ std::optional Keys::pending_config() const { } bool Keys::insert_key(const key_info& new_key) { + // Find all keys with the same generation and see if our key is in there (that is: we are + // deliberately ignoring timestamp so that we don't add the same key with slight timestamp + // variations). + const auto [gen_begin, gen_end] = + std::equal_range(keys_.begin(), keys_.end(), new_key, [](const auto& a, const auto& b) { + return a.generation < b.generation; + }); + for (auto it = gen_begin; it != gen_end; ++it) + if (it->key == new_key.key) + return false; + auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); - if (it != keys_.end() && new_key == *it) - // We found a key we already had, so just ignore it. - return false; if (keys_.size() >= 2 && it == keys_.begin() && new_key.generation < keys_.front().generation && keys_.front().timestamp + KEY_EXPIRY < keys_.back().timestamp) @@ -515,7 +528,7 @@ bool Keys::insert_key(const key_info& new_key) { return true; } -void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members) { +bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members) { oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; @@ -528,8 +541,7 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, throw config_value_error{"Key message has no nonce"}; auto nonce = to_unsigned_sv(d.consume_string_view()); - bool found_key = false; - sodium_cleared new_key{}; + sodium_vector new_keys; sodium_cleared> member_dec_key; if (!admin()) { @@ -578,8 +590,8 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, "Supplemental key message has invalid key info size at index " + std::to_string(member_key_count)}; - if (found_key || admin()) - continue; + if (!new_keys.empty() || admin()) + continue; // Keep parsing, just to ensure validity of the whole message ustring plaintext; plaintext.resize(encrypted.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); @@ -595,10 +607,10 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, nonce.data(), member_dec_key.data())) { // Decryption success, we found our key list! - found_key = true; oxenc::bt_list_consumer key_infos{from_unsigned_sv(plaintext)}; while (!key_infos.is_finished()) { + auto& new_key = new_keys.emplace_back(); auto keyinf = key_infos.consume_dict_consumer(); if (!keyinf.skip_until("g")) throw config_value_error{ @@ -616,100 +628,109 @@ void Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, throw config_value_error{ "Invalid supplemental key message: no `t` timestamp"}; new_key.timestamp = sys_time_from_ms(keyinf.consume_integer()); - - insert_key(new_key); } } } } + } else { + // Full message (i.e. not supplemental) - return; - } - - new_key.timestamp = sys_time_from_ms(timestamp_ms); - - if (!d.skip_until("G")) - throw config_value_error{"Key message missing required generation (G) field"}; + bool found_key = false; + auto& new_key = new_keys.emplace_back(); + new_key.timestamp = sys_time_from_ms(timestamp_ms); - new_key.generation = d.consume_integer(); - if (new_key.generation < 0) - throw config_value_error{"Key message contains invalid negative generation"}; + if (!d.skip_until("G")) + throw config_value_error{"Key message missing required generation (G) field"}; - if (!d.skip_until("K")) - throw config_value_error{"Non-supplemental key message is missing required admin key (K)"}; + new_key.generation = d.consume_integer(); + if (new_key.generation < 0) + throw config_value_error{"Key message contains invalid negative generation"}; - auto admin_key = to_unsigned_sv(d.consume_string_view()); - if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw config_value_error{"Key message has invalid admin key length"}; + if (!d.skip_until("K")) + throw config_value_error{ + "Non-supplemental key message is missing required admin key (K)"}; + + auto admin_key = to_unsigned_sv(d.consume_string_view()); + if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{"Key message has invalid admin key length"}; + + if (admin()) { + auto k = seed_hash(enc_key_admin_hash_key); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + new_key.key.data(), + nullptr, + nullptr, + admin_key.data(), + admin_key.size(), + nullptr, + 0, + nonce.data(), + k.data())) + throw config_value_error{"Failed to decrypt admin key from key message"}; - if (admin()) { - auto k = seed_hash(enc_key_admin_hash_key); + found_key = true; + } - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - new_key.key.data(), - nullptr, - nullptr, - admin_key.data(), - admin_key.size(), - nullptr, - 0, - nonce.data(), - k.data())) - throw config_value_error{"Failed to decrypt admin key from key message"}; + // Even if we're already found a key we still parse these, so that admins and all users have + // the same error conditions for rejecting an invalid config message. + if (!d.skip_until("k")) + throw config_value_error{"Config is missing member keys list (k)"}; + auto key_list = d.consume_list_consumer(); - found_key = true; - insert_key(new_key); - } + int member_key_count = 0; + for (; !key_list.is_finished(); member_key_count++) { + auto member_key = to_unsigned_sv(key_list.consume_string_view()); + if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{ + "Key message has invalid member key length at index " + + std::to_string(member_key_count)}; + + if (found_key) + continue; + + if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + new_key.key.data(), + nullptr, + nullptr, + member_key.data(), + member_key.size(), + nullptr, + 0, + nonce.data(), + member_dec_key.data())) { + // Decryption success, we found our key! + found_key = true; + } + } - // Even if we're already found a key we still parse these, so that admins and all users have the - // same error conditions for rejecting an invalid config message. - if (!d.skip_until("k")) - throw config_value_error{"Config is missing member keys list (k)"}; - auto key_list = d.consume_list_consumer(); + if (member_key_count % MESSAGE_KEY_MULTIPLE != 0) + throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; - int member_key_count = 0; - for (; !key_list.is_finished(); member_key_count++) { - auto member_key = to_unsigned_sv(key_list.consume_string_view()); - if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw config_value_error{ - "Key message has invalid member key length at index " + - std::to_string(member_key_count)}; - - if (found_key) - continue; - - if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - new_key.key.data(), - nullptr, - nullptr, - member_key.data(), - member_key.size(), - nullptr, - 0, - nonce.data(), - member_dec_key.data())) { - // Decryption success, we found our key! - found_key = true; - insert_key(new_key); - } + if (!found_key) + new_keys.pop_back(); } - if (member_key_count % MESSAGE_KEY_MULTIPLE != 0) - throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; - verify_config_sig(d, data, verifier_); // If this is our pending config or this has a later generation than our pending config then // drop our pending status. - if (!pending_key_config_.empty() && - (new_key.generation > pending_gen_ || new_key.key == pending_key_)) { + 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; } - auto new_key_list = group_keys(); - members.replace_keys(new_key_list, /*dirty=*/false); - info.replace_keys(new_key_list, /*dirty=*/false); + if (!new_keys.empty()) { + for (auto& k : new_keys) + insert_key(k); + + auto new_key_list = group_keys(); + members.replace_keys(new_key_list, /*dirty=*/false); + info.replace_keys(new_key_list, /*dirty=*/false); + return true; + } + return false; } void Keys::remove_expired() { diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 4870fa79..633d0eff 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -1,16 +1,18 @@ #include #include +#include +#include +#include #include +#include #include #include #include +#include #include -#include #include -#include #include -#include #include #include "utils.hpp" @@ -22,53 +24,62 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { - - struct pseudo_client { - const bool is_admin; - - const ustring seed; - std::string session_id; - - std::array public_key; - std::array secret_key; +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; +} - std::unique_ptr keys; - std::unique_ptr info; - std::unique_ptr members; +static std::string session_id_from_ed(ustring_view ed_pk) { + std::string sid; + std::array xpk; + int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); + REQUIRE(rc == 0); + sid.reserve(66); + sid += "05"; + oxenc::to_hex(xpk.begin(), xpk.end(), std::back_inserter(sid)); + return sid; +} - pseudo_client(ustring s, bool a, unsigned char* gpk, std::optional gsk) : - seed{s}, is_admin{a} { - crypto_sign_ed25519_seed_keypair( - public_key.data(), - secret_key.data(), - reinterpret_cast(seed.data())); +// Hacky little class that implements `[n]` on a std::list. This is inefficient (since it access +// has to iterate n times through the list) but we only use it on small lists in this test code so +// convenience wins over efficiency. (Why not just use a vector? Because vectors requires `T` to +// be moveable, so we'd either have to use std::unique_ptr for members, which is also annoying). +template +struct hacky_list : std::list { + T& operator[](size_t n) { return *std::next(std::begin(*this), n); } +}; - REQUIRE(oxenc::to_hex(seed.begin(), seed.end()) == - oxenc::to_hex(secret_key.begin(), secret_key.begin() + 32)); +TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { - std::array sid; - int rc = crypto_sign_ed25519_pk_to_curve25519(&sid[1], public_key.data()); - REQUIRE(rc == 0); - sid[0] = 0x05; - session_id = oxenc::to_hex(sid.begin(), sid.end()); - - info = std::make_unique( - ustring_view{gpk, 32}, - is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt); - members = std::make_unique( - ustring_view{gpk, 32}, - is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt); - keys = std::make_unique( - to_usv(secret_key), - ustring_view{gpk, 32}, - is_admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt, - *info, - *members); - } + struct pseudo_client { + std::array secret_key; + const ustring_view public_key{secret_key.data() + 32, 32}; + std::string session_id{session_id_from_ed(public_key)}; + + groups::Info info; + groups::Members members; + groups::Keys keys; + + pseudo_client( + ustring_view seed, + bool a, + const unsigned char* gpk, + std::optional gsk) : + secret_key{sk_from_seed(seed)}, + info{ustring_view{gpk, 32}, + a ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + members{ustring_view{gpk, 32}, + a ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + keys{to_usv(secret_key), + ustring_view{gpk, 32}, + a ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt, + info, + members} {} }; const ustring group_seed = @@ -81,7 +92,9 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes, // member4 + "3333333333333333333333333333333333333333333333333333333333333333"_hexbytes, // member3b + "4444444444444444444444444444444444444444444444444444444444444444"_hexbytes, // member4b }; std::array group_pk; @@ -94,8 +107,11 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - std::vector admins; - std::vector members; + // Using list instead of vector so that `psuedo_client` doesn't have to be moveable, which lets + // us put the Info/Member/Keys directly inside it (rather than having to use a unique_ptr, which + // would also be annoying). + hacky_list admins; + hacky_list members; // Initialize admin and member objects admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); @@ -118,9 +134,9 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); for (const auto& a : admins) - REQUIRE(a.members->size() == 0); + REQUIRE(a.members.size() == 0); for (const auto& m : members) - REQUIRE(m.members->size() == 0); + REQUIRE(m.members.size() == 0); std::vector> info_configs; std::vector> mem_configs; @@ -128,23 +144,23 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // add admin account, re-key, distribute auto& admin1 = admins[0]; - auto m = admin1.members->get_or_construct(admin1.session_id); + auto m = admin1.members.get_or_construct(admin1.session_id); m.admin = true; m.name = "Admin1"; - admin1.members->set(m); + admin1.members.set(m); - CHECK(admin1.members->needs_push()); + CHECK(admin1.members.needs_push()); - auto maybe_key_config = admin1.keys->pending_config(); + auto maybe_key_config = admin1.keys.pending_config(); REQUIRE(maybe_key_config); auto new_keys_config1 = *maybe_key_config; - auto [iseq1, new_info_config1, iobs1] = admin1.info->push(); - admin1.info->confirm_pushed(iseq1, "fakehash1"); + auto [iseq1, new_info_config1, iobs1] = admin1.info.push(); + admin1.info.confirm_pushed(iseq1, "fakehash1"); info_configs.emplace_back("fakehash1", new_info_config1); - auto [mseq1, new_mem_config1, mobs1] = admin1.members->push(); - admin1.members->confirm_pushed(mseq1, "fakehash1"); + auto [mseq1, new_mem_config1, mobs1] = admin1.members.push(); + admin1.members.confirm_pushed(mseq1, "fakehash1"); mem_configs.emplace_back("fakehash1", new_mem_config1); /* Even though we have only added one admin, admin2 will still be able to see group info @@ -152,20 +168,20 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { message, which they can decrypt with the group secret key. */ for (auto& a : admins) { - a.keys->load_key_message(new_keys_config1, get_timestamp(), *a.info, *a.members); - CHECK(a.info->merge(info_configs) == 1); - CHECK(a.members->merge(mem_configs) == 1); - CHECK(a.members->size() == 1); + a.keys.load_key_message(new_keys_config1, get_timestamp(), a.info, a.members); + CHECK(a.info.merge(info_configs) == 1); + CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.members.size() == 1); } /* All attempts to merge non-admin members will throw, as none of the non admin members will be able to decrypt the new info/member configs using the updated keys */ for (auto& m : members) { - m.keys->load_key_message(new_keys_config1, get_timestamp(), *m.info, *m.members); - CHECK_THROWS(m.info->merge(info_configs)); - CHECK_THROWS(m.members->merge(mem_configs)); - CHECK(m.members->size() == 0); + m.keys.load_key_message(new_keys_config1, get_timestamp(), m.info, m.members); + CHECK_THROWS(m.info.merge(info_configs)); + CHECK_THROWS(m.members.merge(mem_configs)); + CHECK(m.members.size() == 0); } info_configs.clear(); @@ -173,114 +189,124 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // add non-admin members, re-key, distribute for (int i = 0; i < members.size(); ++i) { - auto m = admin1.members->get_or_construct(members[i].session_id); + auto m = admin1.members.get_or_construct(members[i].session_id); m.admin = false; m.name = "Member" + std::to_string(i); - admin1.members->set(m); + admin1.members.set(m); } - CHECK(admin1.members->needs_push()); + CHECK(admin1.members.needs_push()); - auto new_keys_config2 = admin1.keys->rekey(*admin1.info, *admin1.members); + auto new_keys_config2 = admin1.keys.rekey(admin1.info, admin1.members); CHECK(not new_keys_config2.empty()); - auto [iseq2, new_info_config2, iobs2] = admin1.info->push(); - admin1.info->confirm_pushed(iseq2, "fakehash2"); + auto [iseq2, new_info_config2, iobs2] = admin1.info.push(); + admin1.info.confirm_pushed(iseq2, "fakehash2"); info_configs.emplace_back("fakehash2", new_info_config2); - auto [mseq2, new_mem_config2, mobs2] = admin1.members->push(); - admin1.members->confirm_pushed(mseq2, "fakehash2"); + auto [mseq2, new_mem_config2, mobs2] = admin1.members.push(); + admin1.members.confirm_pushed(mseq2, "fakehash2"); mem_configs.emplace_back("fakehash2", new_mem_config2); for (auto& a : admins) { - a.keys->load_key_message(new_keys_config2, get_timestamp(), *a.info, *a.members); - CHECK(a.info->merge(info_configs) == 1); - CHECK(a.members->merge(mem_configs) == 1); - CHECK(a.members->size() == 5); + a.keys.load_key_message(new_keys_config2, get_timestamp(), a.info, a.members); + CHECK(a.info.merge(info_configs) == 1); + CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.members.size() == 5); } for (auto& m : members) { - m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); - CHECK(m.info->merge(info_configs) == 1); - CHECK(m.members->merge(mem_configs) == 1); - CHECK(m.members->size() == 5); + m.keys.load_key_message(new_keys_config2, get_timestamp(), m.info, m.members); + CHECK(m.info.merge(info_configs) == 1); + CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.members.size() == 5); } info_configs.clear(); mem_configs.clear(); // change group info, re-key, distribute - admin1.info->set_name("tomatosauce"s); + admin1.info.set_name("tomatosauce"s); - CHECK(admin1.info->needs_push()); + CHECK(admin1.info.needs_push()); - auto new_keys_config3 = admin1.keys->rekey(*admin1.info, *admin1.members); + auto new_keys_config3 = admin1.keys.rekey(admin1.info, admin1.members); CHECK(not new_keys_config3.empty()); - auto [iseq3, new_info_config3, iobs3] = admin1.info->push(); - admin1.info->confirm_pushed(iseq3, "fakehash3"); + auto [iseq3, new_info_config3, iobs3] = admin1.info.push(); + admin1.info.confirm_pushed(iseq3, "fakehash3"); info_configs.emplace_back("fakehash3", new_info_config3); - auto [mseq3, new_mem_config3, mobs3] = admin1.members->push(); - admin1.members->confirm_pushed(mseq3, "fakehash3"); + auto [mseq3, new_mem_config3, mobs3] = admin1.members.push(); + admin1.members.confirm_pushed(mseq3, "fakehash3"); mem_configs.emplace_back("fakehash3", new_mem_config3); for (auto& a : admins) { - a.keys->load_key_message(new_keys_config3, get_timestamp(), *a.info, *a.members); - CHECK(a.info->merge(info_configs) == 1); - CHECK(a.members->merge(mem_configs) == 1); - CHECK(a.info->get_name() == "tomatosauce"s); + a.keys.load_key_message(new_keys_config3, get_timestamp(), a.info, a.members); + CHECK(a.info.merge(info_configs) == 1); + CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.get_name() == "tomatosauce"s); } for (auto& m : members) { - m.keys->load_key_message(new_keys_config3, get_timestamp(), *m.info, *m.members); - CHECK(m.info->merge(info_configs) == 1); - CHECK(m.members->merge(mem_configs) == 1); - CHECK(m.info->get_name() == "tomatosauce"s); + m.keys.load_key_message(new_keys_config3, get_timestamp(), m.info, m.members); + CHECK(m.info.merge(info_configs) == 1); + CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.info.get_name() == "tomatosauce"s); } info_configs.clear(); mem_configs.clear(); // remove members, re-key, distribute - CHECK(admin1.members->erase(members[3].session_id)); - CHECK(admin1.members->erase(members[2].session_id)); + CHECK(admin1.members.size() == 5); + CHECK(admin1.members.erase(members[3].session_id)); + CHECK(admin1.members.erase(members[2].session_id)); + CHECK(admin1.members.size() == 3); - CHECK(admin1.members->needs_push()); + CHECK(admin1.members.needs_push()); - auto new_keys_config4 = admin1.keys->rekey(*admin1.info, *admin1.members); + ustring old_key{admin1.keys.group_enc_key()}; + auto new_keys_config4 = admin1.keys.rekey(admin1.info, admin1.members); CHECK(not new_keys_config4.empty()); - auto [iseq4, new_info_config4, iobs4] = admin1.info->push(); - admin1.info->confirm_pushed(iseq4, "fakehash4"); + CHECK(old_key != admin1.keys.group_enc_key()); + + auto [iseq4, new_info_config4, iobs4] = admin1.info.push(); + admin1.info.confirm_pushed(iseq4, "fakehash4"); info_configs.emplace_back("fakehash4", new_info_config4); - auto [mseq4, new_mem_config4, mobs4] = admin1.members->push(); - admin1.members->confirm_pushed(mseq4, "fakehash4"); + auto [mseq4, new_mem_config4, mobs4] = admin1.members.push(); + admin1.members.confirm_pushed(mseq4, "fakehash4"); mem_configs.emplace_back("fakehash4", new_mem_config4); for (auto& a : admins) { - a.keys->load_key_message(new_keys_config4, get_timestamp(), *a.info, *a.members); - CHECK(a.info->merge(info_configs) == 1); - CHECK(a.members->merge(mem_configs) == 1); - CHECK(a.members->size() == 3); + CHECK(a.keys.load_key_message(new_keys_config4, get_timestamp(), a.info, a.members)); + CHECK(a.info.merge(info_configs) == 1); + CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.members.size() == 3); } - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < members.size(); i++) { auto& m = members[i]; - m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); - CHECK(m.info->merge(info_configs) == 1); - CHECK(m.members->merge(mem_configs) == 1); - CHECK(m.members->size() == 3); + bool found_key = + m.keys.load_key_message(new_keys_config2, get_timestamp(), m.info, m.members); + + if (i < 2) { // We should still be in the group + CHECK(found_key); + CHECK(m.info.merge(info_configs) == 1); + CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.members.size() == 3); + } else { + CHECK_FALSE(found_key); + CHECK(m.info.merge(info_configs) == 0); + CHECK(m.members.merge(mem_configs) == 0); + CHECK(m.members.size() == 5); + } } - for (int i = 2; i < 4; ++i) { - auto& m = members[i]; - m.keys->load_key_message(new_keys_config2, get_timestamp(), *m.info, *m.members); - CHECK(m.info->merge(info_configs) == 0); - CHECK(m.members->merge(mem_configs) == 0); - CHECK(m.members->size() == 5); - } + members.pop_back(); + members.pop_back(); info_configs.clear(); mem_configs.clear(); @@ -291,15 +317,69 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (int i = 0; i < 5; ++i) msg += msg; - auto compressed = admin1.keys->encrypt_message(to_usv(msg), true); - auto uncompressed = admin1.keys->encrypt_message(to_usv(msg), false); + auto compressed = admin1.keys.encrypt_message(to_usv(msg)); + auto uncompressed = admin1.keys.encrypt_message(to_usv(msg), false); CHECK(compressed.size() < msg.size()); CHECK(compressed.size() < uncompressed.size()); + + // Add two new members and send them supplemental keys + for (int i = 0; i < 2; ++i) { + auto& m = members.emplace_back(member_seeds[4 + i], false, group_pk.data(), std::nullopt); + + auto memb = admin1.members.get_or_construct(m.session_id); + memb.set_invited(); + admin1.members.set(memb); + + CHECK_FALSE(m.keys.admin()); + } + + REQUIRE(members[2].session_id == + "054eb4fafee2bd3018a24e310de8106333c2b364eaed029a7f05d7b45ccc77683a"); + REQUIRE(members[3].session_id == + "057ce31baa9a04b5cfb83ab7ccdd7b669b911a082d29883d6aad3256294a0a5e0c"); + + // We actually send supplemental keys to members 1, as well, by mistake just to make sure it + // doesn't do or hurt anything to get a supplemental key you already have. + std::vector supp_sids; + std::transform( + std::next(members.begin()), members.end(), std::back_inserter(supp_sids), [](auto& m) { + return m.session_id; + }); + auto supp = admin1.keys.key_supplement(supp_sids); + CHECK(admin1.members.needs_push()); + CHECK_FALSE(admin1.info.needs_push()); + auto [mseq5, mpush5, mobs5] = admin1.members.push(); + mem_configs.emplace_back("fakehash5", mpush5); + admin1.members.confirm_pushed(mseq5, "fakehash5"); + info_configs.emplace_back("fakehash4", new_info_config4); + + for (size_t i = 0; i < members.size(); i++) { + DYNAMIC_SECTION("supp key load " << i) { + auto& m = members[i]; + bool found_key = m.keys.load_key_message(supp, get_timestamp(), m.info, m.members); + + if (i < 1) { + // This supp key wasn't for us + CHECK_FALSE(found_key); + CHECK(m.keys.group_keys().size() == 3); + } else { + CHECK(found_key); + // new_keys_config1 never went to the initial members, but did go out in the + // supplement, which is why we have the extra key here. + CHECK(m.keys.group_keys().size() == 4); + } + + CHECK(m.info.merge(info_configs) == 1); + CHECK(m.members.merge(mem_configs) == 1); + REQUIRE(m.info.get_name()); + CHECK(*m.info.get_name() == "tomatosauce"sv); + CHECK(m.members.size() == 5); + } + } } -TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") -{ +TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") { struct pseudo_client { const bool is_admin; @@ -326,25 +406,13 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") std::array sid; int rc = crypto_sign_ed25519_pk_to_curve25519(&sid[1], public_key.data()); REQUIRE(rc == 0); - sid[0] = 0x05; - session_id = oxenc::to_hex(sid.begin(), sid.end()); + session_id += "\x05"; + oxenc::to_hex(sid.begin(), sid.end(), std::back_inserter(session_id)); - int rv = groups_members_init( - &members, - gpk, - is_admin ? *gsk : NULL, - NULL, - 0, - NULL); + 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); + rv = groups_info_init(&info, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL); REQUIRE(rv == 0); rv = groups_keys_init( @@ -360,8 +428,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") REQUIRE(rv == 0); } - ~pseudo_client() - { + ~pseudo_client() { config_free(info); config_free(members); } From 517a61a455d31cd9363198d1b3d02f20093a5811 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Aug 2023 15:53:58 -0300 Subject: [PATCH 036/572] Updates for user groups -> groups - add test - add missing `erase_group` to erase by id - fix bug where `K` wasn't set for non-admins (it should be set but empty in such a case so that we always have a key). - make `check_session_id` take a string view prefix instead of a char to make it a little easier, and to include it in the error message. --- include/session/config/user_groups.hpp | 15 ++++- src/config/internal.cpp | 12 ++-- src/config/internal.hpp | 4 +- src/config/user_groups.cpp | 19 ++++-- tests/test_config_user_groups.cpp | 90 ++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 16 deletions(-) diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 3549e55c..3ca6cb8a 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -28,7 +28,8 @@ namespace session::config { /// half (32 bytes) of the 64-byte libsodium-style Ed25519 secret key value (i.e. it omits /// the cached public key in the second half). This field is always set, but will be empty /// if the seed is not known. -/// s - authentication signature; this is used by non-admins to authenticate +/// s - authentication signature; this is used by non-admins to authenticate. Omitted when K is +/// non-empty. /// @ - notification setting (int). Omitted = use default setting; 1 = all, 2 = disabled, 3 = /// mentions-only. /// ! - mute timestamp: if set then don't show notifications for this contact's messages until @@ -443,6 +444,18 @@ class UserGroups : public ConfigBase { /// - `bool` - Returns true if found and removed, false otherwise bool erase_community(std::string_view base_url, std::string_view room); + /// API: user_groups/UserGroups::erase_group + /// + /// Removes a (new, closed) group conversation. Returns true if found and removed, false if not + /// present. + /// + /// Inputs: + /// - `pubkey_hex` -- group ID (hex, looks like a session ID, but starts with 03) + /// + /// Outputs: + /// - `bool` - Returns true if found and removed, false otherwise + bool erase_group(std::string_view pubkey_hex); + /// API: user_groups/UserGroups::erase_legacy_group /// /// Removes a legacy group conversation. Returns true if found and removed, false if not diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 3b9ef85e..979137da 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -11,15 +11,15 @@ namespace session::config { -void check_session_id(std::string_view session_id, unsigned char prefix) { - if (!(session_id.size() == 66 && oxenc::is_hex(session_id) && - session_id[0] == ('0' + (prefix >> 4)) && session_id[1] == ('0' + (prefix & 0xf)))) +void check_session_id(std::string_view session_id, std::string_view prefix) { + if (!(session_id.size() == 64 + prefix.size() && oxenc::is_hex(session_id) && + session_id.substr(0, prefix.size()) == prefix)) throw std::invalid_argument{ - "Invalid session ID: expected 66 hex digits starting with 05; got " + - std::string{session_id}}; + "Invalid session ID: expected 66 hex digits starting with " + std::string{prefix} + + "; got " + std::string{session_id}}; } -std::string session_id_to_bytes(std::string_view session_id, unsigned char prefix) { +std::string session_id_to_bytes(std::string_view session_id, std::string_view prefix) { check_session_id(session_id, prefix); return oxenc::from_hex(session_id); } diff --git a/src/config/internal.hpp b/src/config/internal.hpp index e1a0dfc9..7a749bec 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -82,10 +82,10 @@ void copy_c_str(char (&dest)[N], std::string_view src) { // Throws std::invalid_argument if session_id doesn't look valid. Can optionally be passed a prefix // byte for id's that aren't starting with 0x05 (e.g. 0x03 for non-legacy group ids). -void check_session_id(std::string_view session_id, unsigned char prefix = 0x05); +void check_session_id(std::string_view session_id, std::string_view prefix = "05"); // Checks the session_id (throwing if invalid) then returns it as bytes -std::string session_id_to_bytes(std::string_view session_id, unsigned char prefix = 0x05); +std::string session_id_to_bytes(std::string_view session_id, std::string_view prefix = "05"); // Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 prefix // (which is the x25519 pubkey). diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index bace4bf3..7c6f7126 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -50,7 +50,7 @@ static void base_from(base_group_info& self, const T& c) { } group_info::group_info(std::string sid) : id{std::move(sid)} { - check_session_id(id, 0x03); + check_session_id(id, "03"); } legacy_group_info::legacy_group_info(std::string sid) : session_id{std::move(sid)} { @@ -315,7 +315,7 @@ legacy_group_info UserGroups::get_or_construct_legacy_group(std::string_view pub } std::optional UserGroups::get_group(std::string_view pubkey_hex) const { - std::string pubkey = session_id_to_bytes(pubkey_hex, 0x03); + std::string pubkey = session_id_to_bytes(pubkey_hex, "03"); auto* info_dict = data["g"][pubkey].dict(); if (!info_dict) @@ -388,14 +388,16 @@ void UserGroups::set(const legacy_group_info& g) { } void UserGroups::set(const group_info& g) { - auto info = data["g"][session_id_to_bytes(g.id, 0x03)]; + auto info = data["g"][session_id_to_bytes(g.id, "03")]; set_base(g, info); if (g.secretkey.size() == 64) info["K"] = ustring_view{g.secretkey.data(), 32}; - - else if (g.auth_sig.size() == 64) - info["s"] = g.auth_sig; + else { + info["K"] = ustring_view{}; + if (g.auth_sig.size() == 64) + info["s"] = g.auth_sig; + } } template @@ -420,7 +422,7 @@ bool UserGroups::erase(const community_info& c) { return gone; } bool UserGroups::erase(const group_info& c) { - return erase_impl(data["g"][session_id_to_bytes(c.id, 0x03)]); + return erase_impl(data["g"][session_id_to_bytes(c.id, "03")]); } bool UserGroups::erase(const legacy_group_info& c) { return erase_impl(data["C"][session_id_to_bytes(c.session_id)]); @@ -435,6 +437,9 @@ bool UserGroups::erase_community(std::string_view base_url, std::string_view roo bool UserGroups::erase_legacy_group(std::string_view id) { return erase(legacy_group_info{std::string{id}}); } +bool UserGroups::erase_group(std::string_view id) { + return erase(group_info{std::string{id}}); +} size_t UserGroups::size_communities() const { size_t count = 0; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index cccf18bf..4fe91cac 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -408,6 +408,96 @@ TEST_CASE("User Groups", "[config][groups]") { } } +TEST_CASE("User Groups (new)", "[config][groups][new]") { + + 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)); + + session::config::UserGroups groups{ustring_view{seed}, std::nullopt}; + + constexpr auto definitely_real_id = + "035000000000000000000000000000000000000000000000000000000000000000"sv; + + int64_t now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + CHECK_FALSE(groups.get_group(definitely_real_id)); + + CHECK(groups.empty()); + CHECK(groups.size() == 0); + + auto c = groups.get_or_construct_group(definitely_real_id); + + CHECK(c.id == definitely_real_id); + CHECK(c.priority == 0); + CHECK(c.joined_at == 0); + CHECK(c.notifications == session::config::notify_mode::defaulted); + CHECK(c.mute_until == 0); + + groups.set(c); + + CHECK(groups.needs_push()); + CHECK(groups.needs_dump()); + + auto [seqno, to_push, obs] = groups.push(); + groups.confirm_pushed(seqno, "fakehash1"); + + auto d1 = groups.dump(); + + session::config::UserGroups g2{ustring_view{seed}, d1}; + + auto c2 = g2.get_group(definitely_real_id); + REQUIRE(c2.has_value()); + + CHECK(c2->id == definitely_real_id); + CHECK(c2->priority == 0); + CHECK(c2->joined_at == 0); + CHECK(c2->notifications == session::config::notify_mode::defaulted); + CHECK(c2->mute_until == 0); + + c2->priority = 123; + c2->joined_at = 1234567890; + c2->notifications = session::config::notify_mode::mentions_only; + c2->mute_until = 456789012; + + g2.set(*c2); + + std::tie(seqno, to_push, obs) = g2.push(); + g2.confirm_pushed(seqno, "fakehash2"); + + std::vector> to_merge; + to_merge.emplace_back("fakehash2", to_push); + groups.merge(to_merge); + + auto c3 = groups.get_group(definitely_real_id); + REQUIRE(c3.has_value()); + CHECK(c3->id == definitely_real_id); + CHECK(c3->priority == 123); + CHECK(c3->joined_at == 1234567890); + CHECK(c3->notifications == session::config::notify_mode::mentions_only); + CHECK(c3->mute_until == 456789012); + + groups.erase(*c3); + + auto gg = groups.get_or_construct_group("030303030303030303030303030303030303030303030303030303030303030303"); + groups.set(gg); + CHECK(groups.erase_group("030303030303030303030303030303030303030303030303030303030303030303")); + CHECK_FALSE(groups.erase_group("030303030303030303030303030303030303030303030303030303030303030303")); +} + TEST_CASE("User Groups members C API", "[config][groups][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; From 390faa8a27c6a7c60086f850c7094314baa7fa0c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Aug 2023 23:08:15 -0300 Subject: [PATCH 037/572] Remove temporary testKeys binary It was temporary for simpler keys testing. --- tests/CMakeLists.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e73548bd..dbf39f45 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,14 +16,6 @@ add_executable(testAll test_xed25519.cpp ) -add_executable(testKeys - test_group_keys.cpp -) - -target_link_libraries(testKeys PRIVATE - config - Catch2::Catch2WithMain) - target_link_libraries(testAll PRIVATE config Catch2::Catch2WithMain) From 8cb26be50d29c9ef4962b5e6cbaaec208c9d2632 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 30 Aug 2023 16:31:03 -0300 Subject: [PATCH 038/572] Swarm subaccount authentication This adds methods to `Keys` that generates subaccount tokens and signatures as needed to do storage server subaccount authentication (which currently requires testnet as the subaccount code is not yet active on mainnet), along with test code to test it. Also adds a tests/swarm-auth-test binary that spits out storage requests for store/retrieve testing. --- include/session/config/groups/info.hpp | 12 +- include/session/config/groups/keys.hpp | 143 ++++++++++++ include/session/config/user_groups.h | 7 +- include/session/config/user_groups.hpp | 11 +- include/session/xed25519.hpp | 14 ++ src/config/groups/info.cpp | 7 +- src/config/groups/keys.cpp | 306 ++++++++++++++++++++++++- src/config/internal.cpp | 10 +- src/config/internal.hpp | 6 +- src/config/user_groups.cpp | 16 +- src/xed25519.cpp | 13 -- tests/CMakeLists.txt | 3 + tests/swarm-auth-test.cpp | 162 +++++++++++++ tests/test_config_user_groups.cpp | 8 +- tests/test_config_userprofile.cpp | 8 + tests/test_group_keys.cpp | 163 ++++++++++--- tests/utils.hpp | 11 +- 17 files changed, 807 insertions(+), 93 deletions(-) create mode 100644 tests/swarm-auth-test.cpp diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index f02d4ef1..9ebdfd7d 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -73,14 +73,16 @@ class Info final : public ConfigBase { /// - `const char*` - Will return "groups::Info" const char* encryption_domain() const override { return "groups::Info"; } - /// Returns the subaccount masking value. This is based on the group's seed and thus is only - /// obtainable by an admin account. + /// API: groups/Info::id /// - /// Inputs: none + /// Contains the (read-only) id of this group, that is, 03 followed by the pubkey in hex. (This + /// is equivalent to a 05-prefixed session_id, but is the group-specific identifier). + /// + /// Inputs: None /// /// Outputs: - /// - `ustring_view` - the 32-byte masking value. - std::array subaccount_mask() const; + /// - `std::string` containing the hex group id/pubkey + const std::string id; /// API: groups/Info::get_name /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index e8ca92d3..718a6422 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -113,6 +113,11 @@ class Keys final : public ConfigSig { // if it already existed. bool insert_key(const key_info& key); + // Returned the blinding factor for a given session X25519 pubkey. This depends on the group's + // seed and thus is only obtainable by an admin account. + std::array subaccount_blind_factor( + const std::array& session_xpk) const; + public: /// The multiple of members keys we include in the message; we add junk entries to the key list /// to reach a multiple of this. 75 is chosen because it's a decently large human-round number @@ -297,6 +302,144 @@ class Keys final : public ConfigSig { return key_supplement(std::vector{{std::move(sid)}}); } + /// API: groups/Keys::swarm_make_subaccount + /// + /// Constructs a swarm subaccount signing value that a member can use to access messages in the + /// swarm. Requires group admins keys. + /// + /// Inputs: + /// - `session_id` -- the session ID of the member (in hex) + /// - `write` -- if true (which is the default if omitted) 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 false then the user can only retrieve + /// messages. + /// - `del` -- if true (default is false) then the user shall be allowed to delete messages + /// from the swarm. This permission can be used to appoint a sort of "moderator" who can + /// delete messages without having the full admin group keys. + /// + /// Outputs: + /// - `ustring` -- contains a subaccount swarm signing value; this can be passed (by the user) + /// into `swarm_subaccount_sign` to sign a value suitable for swarm authentication. + /// (Internally this packs the flags, blinding factor, and group admin signature together and + /// will be 4 + 32 + 64 = 100 bytes long). + /// + /// This value must be provided to the user so that they can authentication. The user should + /// call `swarm_verify_subaccount` to verify that the signing value was indeed signed by a + /// group admin before using/storing it. + /// + /// The signing value produced will be the same (for a given `session_id`/`write`/`del` + /// values) when constructed by any admin of the group. + ustring swarm_make_subaccount( + std::string_view session_id, bool write = true, bool del = false) const; + + /// API: groups/Keys::swarm_verify_subaccount + /// + /// Verifies that a received subaccount signing value (allegedly produced by + /// swarm_make_subaccount) is a valid subaccount signing value for the given group pubkey, + /// including a proper signature by an admin of the group. The signing value must have read + /// permission, but parameters can be given to also require write or delete permissions. A + /// subaccount signing value should always be checked for validity using this before creating a + /// group that would depend on it. + /// + /// There are two versions of this function: a static one callable without having a Keys + /// instance that takes the group id and user's session Ed25519 secret key as arguments; and a + /// member function that omits these first two arguments (using the ones from the Keys + /// instance). + /// + /// Inputs: + /// - `groupid` -- the group id/pubkey, in hex, beginning with "03". + /// - `session_ed25519_secretkey` -- the user's Session ID secret key. + /// - `signing_value` -- the subaccount signing value to validate + /// - `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). + /// + /// Outputs: + /// - `true` if `signing_value` is a valid subaccount signing value for `groupid` with read (and + /// possible write and/or del permissions, if requested). `false` if the signing value does + /// not validate or does not meet the requirements. + static bool swarm_verify_subaccount( + std::string group_id, + ustring_view session_ed25519_secretkey, + ustring_view signing_value, + bool write = false, + bool del = false); + bool swarm_verify_subaccount( + ustring_view signing_value, bool write = false, bool del = false) const; + + /// API: groups/Keys::swarm_auth + /// + /// This struct containing the storage server authentication values for subaccount + /// authentication. The three strings in this struct may be either raw bytes, or hex/base64 + /// encoded, depending on the `binary` parameter passed to `swarm_subaccount_sign`. + /// + /// `.subaccount` is the value to be passed as the "subaccount" authentication parameter. (It + /// consists of permission flags followed by a blinded public key.) + /// + /// `.subaccount_sig` is the value to be passed as the "subaccount_sig" authentication + /// parameter. (It consists of an admin-produced signature of the subaccount, providing + /// permission for that token to be used for authentication). + /// + /// `.signature` is the value to be passed as the "signature" authentication parameter. (It is + /// an Ed25519 signature that validates using the blinded public key inside `subaccount`). + /// + /// Inputs: none. + struct swarm_auth { + std::string subaccount; + std::string subaccount_sig; + std::string signature; + }; + + /// API: groups/Keys::swarm_subaccount_sign + /// + /// 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 + /// `swarm_auth_key`). + /// + /// Storage server subaccount authentication requires passing the three values in the returned + /// struct in the storage server request. (See Keys::swarm_auth for details). + /// + /// Inputs: + /// - `msg` -- the 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). + /// - `signing_value` -- the 100-byte subaccount signing value, as produced by an admin's + /// `swarm_make_subaccount` and provided to this member. + /// - `binary` -- if set to true then the returned values will be binary. If omitted, the + /// returned struct values will be hex-encoded (subaccount token) or base64-encoded + /// (signatures) suitable for direct passing as JSON values to the storage server. + /// + /// Outputs: + /// - struct containing three binary values enabling swarm authentication (see description + /// above). + swarm_auth swarm_subaccount_sign( + ustring_view msg, ustring_view signing_value, bool binary = false) const; + + /// API: groups/Keys::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 `swarm_make_subaccount` + /// instead. This will produce the same subaccount token that `swarm_make_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. + /// (Note, however, that there are circumstances where this can fail to prevent access, and so + /// should be combined with proper member removal and key rotation so that even if the member + /// gains access to messages, they cannot read them). + /// + /// Inputs: + /// - `session_id` -- the session ID of the member (in hex) + /// - `write`, `del` -- optional; see `swarm_make_subaccount`. The same arguments should be + /// provided (or omitted) as were used in `swarm_make_subaccount`. + /// + /// Outputs: + /// - 36 byte token that can be used for swarm token revocation. + ustring swarm_subaccount_token( + std::string_view session_id, bool write = true, bool del = false) const; + /// API: groups/Keys::pending_config /// /// If a rekey has been performed but not yet confirmed then this will contain the config diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 2728b029..2e9be8ce 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -48,9 +48,10 @@ typedef struct ugroups_group_info { bool have_secretkey; // Will be true if the `secretkey` is populated unsigned char secretkey[64]; // If `have_secretkey` is set then this is the libsodium-style // "secret key" for the group (i.e. 32 byte seed + 32 byte pubkey) - bool have_auth_sig; // Will be true if the `auth_sig` is populated - unsigned char auth_sig[64]; // If `have_auth_sig` is set then this is the authentication - // signature that can be used to access the swarm. + bool have_auth_data; // Will be true if the `auth_data` is populated + unsigned char auth_data[100]; // If `have_auth_data` is set then this is the authentication + // signing value that can be used to produce signature values to + // access the swarm. int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned // (with higher meaning pinned higher). diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 3ca6cb8a..16ba58d9 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -181,10 +181,13 @@ struct group_info : base_group_info { /// Group secret key (64 bytes); this is only possessed by admins. ustring secretkey; - /// Group authentication signature; this is possessed by non-admins. (This value will be - /// dropped when serializing if secretkey is non-empty, and so does not need to be explicitly - /// cleared when being promoted to admin) - ustring auth_sig; + /// Group authentication signing value (100 bytes); this is used by non-admins to authenticate + /// (using the swarm key generation functions in config::groups::Keys). This value will be + /// dropped when serializing an updated config message if `secretkey` is non-empty (i.e. if it + /// is an admin), and so does not need to be explicitly cleared when being promoted to admin. + /// + /// Producing and using this value is done with the groups::Keys `swarm` methods. + ustring auth_data; /// Constructs a new group info from an hex id (03 + pubkey). Throws if id is invalid. explicit group_info(std::string gid); diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index 9889113c..2c55679f 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -35,4 +35,18 @@ std::array pubkey(ustring_view curve25519_pubkey); /// "Softer" version that takes/returns strings of regular chars std::string pubkey(std::string_view curve25519_pubkey); +/// Utility function that provides a constant-time `if (b) f = g;` implementation for byte arrays. +template +void constant_time_conditional_assign( + std::array& f, const std::array& g, bool b) { + std::array x; + for (size_t i = 0; i < x.size(); i++) + x[i] = f[i] ^ g[i]; + unsigned char mask = (unsigned char)(-(signed char)b); + for (size_t i = 0; i < x.size(); i++) + x[i] &= mask; + for (size_t i = 0; i < x.size(); i++) + f[i] ^= x[i]; +} + } // namespace session::xed25519 diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 9c34a9d0..be29e581 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -20,11 +20,8 @@ Info::Info( ustring_view ed25519_pubkey, std::optional ed25519_secretkey, std::optional dumped) : - ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey} {} - -std::array Info::subaccount_mask() const { - return seed_hash("SessionGroupSubaccountMask"); -} + ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey}, + id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} {} std::optional Info::get_name() const { if (auto* s = data["n"].string(); s && !s->empty()) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index c17062ab..0e6a81ae 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1,10 +1,13 @@ #include "session/config/groups/keys.hpp" +#include +#include #include #include #include #include #include +#include #include #include #include @@ -16,6 +19,7 @@ #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" +#include "session/xed25519.hpp" using namespace std::literals; @@ -278,7 +282,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { auto member_keys = d.append_list("k"); int member_count = 0; for (const auto& m : members) { - auto m_xpk = session_id_xpk(m.session_id); + auto m_xpk = session_id_pk(m.session_id); // Calculate the encryption key: H(aB || A || B) if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) continue; // The scalarmult failed; maybe a bad session id? @@ -442,7 +446,7 @@ ustring Keys::key_supplement(std::vector sids) const { size_t member_count = 0; for (auto& sid : sids) { - auto m_xpk = session_id_xpk(sid); + auto m_xpk = session_id_pk(sid); // Calculate the encryption key: H(aB || A || B) std::array member_k; @@ -495,6 +499,304 @@ ustring Keys::key_supplement(std::vector sids) const { return ustring{to_unsigned_sv(d.view())}; } +// 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( + const std::array& session_xpk) const { + + auto mask = seed_hash("SessionGroupSubaccountMask"); + static_assert(mask.size() == crypto_generichash_blake2b_KEYBYTES); + + std::array h; + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, mask.data(), mask.size(), h.size()); + crypto_generichash_blake2b_update(&st, to_unsigned("\x05"), 1); + crypto_generichash_blake2b_update(&st, session_xpk.data(), session_xpk.size()); + crypto_generichash_blake2b_update(&st, to_unsigned("\x03"), 1); + crypto_generichash_blake2b_update(&st, _sign_pk->data(), _sign_pk->size()); + crypto_generichash_blake2b_final(&st, h.data(), h.size()); + + std::array out; + crypto_core_ed25519_scalar_reduce(out.data(), h.data()); + return out; +} + +namespace { + + // These constants are defined and explains in more detail in oxen-storage-server + constexpr unsigned char SUBACC_FLAG_READ = 0b0001; + constexpr unsigned char SUBACC_FLAG_WRITE = 0b0010; + constexpr unsigned char SUBACC_FLAG_DEL = 0b0100; + constexpr unsigned char SUBACC_FLAG_ANY_PREFIX = 0b1000; + + constexpr unsigned char subacc_flags(bool write, bool del) { + return SUBACC_FLAG_READ | (write ? SUBACC_FLAG_WRITE : 0) | (del ? SUBACC_FLAG_DEL : 0); + } + +} // namespace + +ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, bool del) const { + if (!admin()) + throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; + + // This gets a wee bit complicated because we only have a session_id, but we really need an + // Ed25519 pubkey. So we do the signal-style XEd25519 thing here where we start with the + // positive alternative behind their x25519 pubkey and work from there. This means, + // unfortunately, that making a signature needs to muck around since this is the proper public + // only half the time. + + // Terminology/variables (a/A indicates private/public keys) + // - s/S are the Ed25519 underlying Session keys (neither is observed in this context) + // - x/X are the X25519 conversions of s/S (x, similarly, is not observed, but X is: it's in the + // session_id). + // - T = |S|, i.e. the positive of the two alternatives we get from inverting the Ed -> X + // pubkey. + // - c/C is the group's Ed25519 + // - k is the blinding factor, which is: H(\x05...[sessionid]\x03...[groupid], key=M) mod L, + // where: H is 64-byte blake2b; M is `subaccount_blind_factor` (see above). + // - p is the account network prefix (03) + // - f are the flag bits, determined by `write` and `del` arguments + + auto X = session_id_pk(session_id); + auto& c = _sign_sk; + auto& C = *_sign_pk; + + auto k = subaccount_blind_factor(X); + + // T = |S| + auto T = xed25519::pubkey(ustring_view{X.data(), X.size()}); + + // kT is the user's Ed25519 blinded pubkey: + std::array kT; + + if (0 != crypto_scalarmult_ed25519_noclamp(kT.data(), k.data(), T.data())) + throw std::runtime_error{"scalarmult failed: perhaps an invalid session id?"}; + + ustring out; + out.resize(4 + 32 + 64); + out[0] = 0x03; // network prefix + out[1] = subacc_flags(write, del); // permission flags + out[2] = 0; // reserved 1 + out[3] = 0; // reserved 2 + // The next 32 bytes are k (NOT kT; the user can go make kT themselves): + std::memcpy(&out[4], k.data(), k.size()); + + // And then finally, we append a group signature of: p || f || 0 || 0 || kT + std::array to_sign; + std::memcpy(&to_sign[0], out.data(), 4); // first 4 bytes are the same as out + std::memcpy(&to_sign[4], kT.data(), 32); // but then we have kT instead of k + crypto_sign_ed25519_detached(&out[36], nullptr, to_sign.data(), to_sign.size(), c.data()); + + return out; +} + +ustring Keys::swarm_subaccount_token(std::string_view session_id, bool write, bool del) const { + if (!admin()) + throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; + + // Similar to the above, but we only care about getting flags || kT + + auto X = session_id_pk(session_id); + auto& c = _sign_sk; + auto& C = *_sign_pk; + + auto k = subaccount_blind_factor(X); + + // T = |S| + auto T = xed25519::pubkey(ustring_view{X.data(), X.size()}); + + ustring out; + out.resize(4 + 32); + out[0] = 0x03; // network prefix + out[1] = subacc_flags(write, del); // permission flags + out[2] = 0; // reserved 1 + out[3] = 0; // reserved 2 + if (0 != crypto_scalarmult_ed25519_noclamp(&out[4], k.data(), T.data())) + throw std::runtime_error{"scalarmult failed: perhaps an invalid session id?"}; + return out; +} + +Keys::swarm_auth Keys::swarm_subaccount_sign( + ustring_view msg, ustring_view sign_val, bool binary) const { + if (sign_val.size() != 100) + throw std::logic_error{"Invalid signing value: size is wrong"}; + + if (!_sign_pk) + throw std::logic_error{"Unable to verify: group pubkey is not set (!?)"}; + + Keys::swarm_auth result; + auto& [token, sub_sig, sig] = result; + + // (see above for variable/crypto notation) + + ustring_view k = sign_val.substr(4, 32); + + // our token is the first 4 bytes of `sign_val` (flags, etc.), followed by kT which we have to + // compute: + token.resize(36); + std::memcpy(token.data(), sign_val.data(), 4); + + // T = |S|, i.e. we have to clear the sign bit from our pubkey + std::array T; + crypto_sign_ed25519_sk_to_pk(T.data(), user_ed25519_sk.data()); + bool neg = T[31] & 0x80; + T[31] &= 0x7f; + if (0 != crypto_scalarmult_ed25519_noclamp(to_unsigned(token.data() + 4), k.data(), T.data())) + throw std::runtime_error{"scalarmult failed: perhaps an invalid session id or seed?"}; + + // token is now set: flags || kT + ustring_view kT{to_unsigned(token.data() + 4), 32}; + + // sub_sig is just the admin's signature, sitting at the end of sign_val (after 4f || k): + sub_sig = from_unsigned_sv(sign_val.substr(36)); + + // Our signing private scalar is kt, where t = ±s according to whether we had to negate S to + // make T + std::array s, s_neg; + crypto_sign_ed25519_sk_to_curve25519(s.data(), user_ed25519_sk.data()); + crypto_core_ed25519_scalar_negate(s_neg.data(), s.data()); + xed25519::constant_time_conditional_assign(s, s_neg, neg); + + auto& t = s; + + std::array kt; + crypto_core_ed25519_scalar_mul(kt.data(), k.data(), t.data()); + + // We now have kt, kT, our privkey/public. (Note that kt is a scalar, not a seed). + + // We're going to get *close* to standard Ed25519 here, except: + // + // where Ed25519 uses + // + // r = SHA512(SHA512(seed)[32:64] || M) mod L + // + // we're instead going to use: + // + // r = H64(H32(seed, key="SubaccountSeed") || kT || M, key="SubaccountSig") mod L + // + // where H64 and H32 are BLAKE2b keyed hashes of 64 and 32 bytes, respectively, thus + // differentiating the signature for both different seeds and different blinded kT pubkeys. + // + // From there, we follow the standard EdDSA construction: + // + // R = rB + // S = r + H(R || kT || M) kt (mod L) + // + // (using the standard Ed25519 SHA-512 here for H) + + constexpr auto seed_hash_key = "SubaccountSeed"sv; + constexpr auto r_hash_key = "SubaccountSig"sv; + std::array hseed; + crypto_generichash_blake2b( + hseed.data(), + hseed.size(), + user_ed25519_sk.data(), + 32, + reinterpret_cast(seed_hash_key.data()), + seed_hash_key.size()); + + std::array tmp; + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( + &st, + reinterpret_cast(r_hash_key.data()), + r_hash_key.size(), + tmp.size()); + crypto_generichash_blake2b_update(&st, hseed.data(), hseed.size()); + crypto_generichash_blake2b_update(&st, kT.data(), kT.size()); + crypto_generichash_blake2b_update(&st, msg.data(), msg.size()); + crypto_generichash_blake2b_final(&st, tmp.data(), tmp.size()); + + std::array r; + crypto_core_ed25519_scalar_reduce(r.data(), tmp.data()); + + sig.resize(64); + unsigned char* R = to_unsigned(sig.data()); + unsigned char* S = to_unsigned(sig.data() + 32); + // R = rB + crypto_scalarmult_ed25519_base_noclamp(R, r.data()); + + // Compute S = r + H(R || A || M) a mod L: (with A = kT, a = kt) + crypto_hash_sha512_state shast; + crypto_hash_sha512_init(&shast); + crypto_hash_sha512_update(&shast, R, 32); + crypto_hash_sha512_update(&shast, kT.data(), kT.size()); // A = pubkey, that is, kT + crypto_hash_sha512_update(&shast, msg.data(), msg.size()); + std::array hram; + crypto_hash_sha512_final(&shast, hram.data()); // S = H(R||A||M) + crypto_core_ed25519_scalar_reduce(S, hram.data()); // S %= L + crypto_core_ed25519_scalar_mul(S, S, kt.data()); // S *= a + crypto_core_ed25519_scalar_add(S, S, r.data()); // S += r + + // sig is now set to the desired R || S, with S = r + H(R || A || M)a (all mod L) + + if (!binary) { + token = oxenc::to_hex(token); + sub_sig = oxenc::to_base64(sub_sig); + sig = oxenc::to_base64(sig); + } + + return result; +} + +bool Keys::swarm_verify_subaccount(ustring_view sign_val, bool write, bool del) const { + if (!_sign_pk) + return false; + return swarm_verify_subaccount( + "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()), + ustring_view{user_ed25519_sk.data(), user_ed25519_sk.size()}, + sign_val, + write, + del); +} + +bool Keys::swarm_verify_subaccount( + std::string group_id, + ustring_view user_ed_sk, + ustring_view sign_val, + bool write, + bool del) { + auto group_pk = session_id_pk(group_id, "03"); + + if (sign_val.size() != 100) + return false; + + ustring_view prefix = sign_val.substr(0, 4); + if (prefix[0] != 0x03 && !(prefix[1] & SUBACC_FLAG_ANY_PREFIX)) + return false; // require either 03 prefix match, or the "any prefix" flag + + if (!(prefix[1] & SUBACC_FLAG_READ)) + return false; // missing the read flag + + if (write && !(prefix[1] & SUBACC_FLAG_WRITE)) + return false; // we require write, but it isn't set + // + if (del && !(prefix[1] & SUBACC_FLAG_DEL)) + return false; // we require delete, but it isn't set + + ustring_view k = sign_val.substr(4, 32); + ustring_view sig = sign_val.substr(36); + + // T = |S|, i.e. we have to clear the sign bit from our pubkey + std::array T; + crypto_sign_ed25519_sk_to_pk(T.data(), user_ed_sk.data()); + T[31] &= 0x7f; + + // Compute kT, then reconstruct the `flags || kT` value the admin should have provided a + // signature for + std::array kT; + if (0 != crypto_scalarmult_ed25519_noclamp(kT.data(), k.data(), T.data())) + throw std::runtime_error{"scalarmult failed: perhaps an invalid session id or seed?"}; + + std::array to_verify; + std::memcpy(&to_verify[0], sign_val.data(), 4); // prefix, flags, 2x future use bytes + std::memcpy(&to_verify[4], kT.data(), 32); + + // Verify it! + return 0 == crypto_sign_ed25519_verify_detached( + sig.data(), to_verify.data(), to_verify.size(), group_pk.data()); +} + std::optional Keys::pending_config() const { if (pending_key_config_.empty()) return std::nullopt; diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 979137da..c5b4421c 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -24,12 +24,12 @@ std::string session_id_to_bytes(std::string_view session_id, std::string_view pr return oxenc::from_hex(session_id); } -std::array session_id_xpk(std::string_view session_id) { - check_session_id(session_id); - std::array xpk; +std::array session_id_pk(std::string_view session_id, std::string_view prefix) { + check_session_id(session_id, prefix); + std::array pk; session_id.remove_prefix(2); - oxenc::from_hex(session_id.begin(), session_id.end(), xpk.begin()); - return xpk; + oxenc::from_hex(session_id.begin(), session_id.end(), pk.begin()); + return pk; } void check_encoded_pubkey(std::string_view pk) { diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 7a749bec..ca37f3a0 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -87,9 +87,9 @@ void check_session_id(std::string_view session_id, std::string_view prefix = "05 // Checks the session_id (throwing if invalid) then returns it as bytes std::string session_id_to_bytes(std::string_view session_id, std::string_view prefix = "05"); -// Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 prefix -// (which is the x25519 pubkey). -std::array session_id_xpk(std::string_view session_id); +// Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 (or +// whatever) prefix, which is a pubkey (x25519 for 05 session_ids, ed25519 for other prefixes). +std::array session_id_pk(std::string_view session_id, std::string_view prefix = "05"); // Validates an open group pubkey; we accept it in hex, base32z, or base64 (padded or unpadded). // Throws std::invalid_argument if invalid. diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 7c6f7126..3a79972c 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -199,8 +199,8 @@ group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { base_from(*this, c); if (c.have_secretkey) secretkey.assign(c.secretkey, 64); - if (c.have_auth_sig) - auth_sig.assign(c.auth_sig, 64); + if (c.have_auth_data) + auth_data.assign(c.auth_data, sizeof(c.auth_data)); } void group_info::into(ugroups_group_info& c) const { @@ -209,8 +209,8 @@ void group_info::into(ugroups_group_info& c) const { copy_c_str(c.id, id); if ((c.have_secretkey = secretkey.size() == 64)) std::memcpy(c.secretkey, secretkey.data(), 64); - if ((c.have_auth_sig = auth_sig.size() == 64)) - std::memcpy(c.auth_sig, auth_sig.data(), 64); + if ((c.have_auth_data = auth_data.size() == 100)) + std::memcpy(c.auth_data, auth_data.data(), 100); } void group_info::load(const dict& info_dict) { @@ -223,8 +223,8 @@ void group_info::load(const dict& info_dict) { if (id != oxenc::to_hex(pk.begin(), pk.end())) secretkey.clear(); } - if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 64) - auth_sig = std::move(*sig); + if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 100) + auth_data = std::move(*sig); } void community_info::load(const dict& info_dict) { @@ -395,8 +395,8 @@ void UserGroups::set(const group_info& g) { info["K"] = ustring_view{g.secretkey.data(), 32}; else { info["K"] = ustring_view{}; - if (g.auth_sig.size() == 64) - info["s"] = g.auth_sig; + if (g.auth_data.size() == 100) + info["s"] = g.auth_data; } } diff --git a/src/xed25519.cpp b/src/xed25519.cpp index d5c69fcc..e885ff1a 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -20,19 +20,6 @@ using bytes = std::array; namespace { - // constant time `if (b) f = g;` implementation - template - void constant_time_conditional_assign(bytes& f, const bytes& g, bool b) { - bytes x; - for (size_t i = 0; i < x.size(); i++) - x[i] = f[i] ^ g[i]; - unsigned char mask = (unsigned char)(-(signed char)b); - for (size_t i = 0; i < x.size(); i++) - x[i] &= mask; - for (size_t i = 0; i < x.size(); i++) - f[i] ^= x[i]; - } - void fe25519_montx_to_edy(fe25519 y, const fe25519 u) { fe25519 one; crypto_internal_fe25519_1(one); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dbf39f45..cce4b155 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,3 +21,6 @@ target_link_libraries(testAll PRIVATE Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) + +add_executable(swarm-auth-test swarm-auth-test.cpp) +target_link_libraries(swarm-auth-test PRIVATE config) diff --git a/tests/swarm-auth-test.cpp b/tests/swarm-auth-test.cpp new file mode 100644 index 00000000..3d235f15 --- /dev/null +++ b/tests/swarm-auth-test.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +static constexpr int64_t created_ts = 1680064059; + +using namespace session::config; + +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 std::string session_id_from_ed(ustring_view ed_pk) { + std::string sid; + std::array xpk; + int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); + assert(rc == 0); + sid.reserve(66); + sid += "05"; + oxenc::to_hex(xpk.begin(), xpk.end(), std::back_inserter(sid)); + return sid; +} + +// Hacky little class that implements `[n]` on a std::list. This is inefficient (since it access +// has to iterate n times through the list) but we only use it on small lists in this test code so +// convenience wins over efficiency. (Why not just use a vector? Because vectors requires `T` to +// be moveable, so we'd either have to use std::unique_ptr for members, which is also annoying). +template +struct hacky_list : std::list { + T& operator[](size_t n) { return *std::next(std::begin(*this), n); } +}; + +struct pseudo_client { + std::array secret_key; + const ustring_view public_key{secret_key.data() + 32, 32}; + std::string session_id{session_id_from_ed(public_key)}; + + groups::Info info; + groups::Members members; + groups::Keys keys; + + pseudo_client( + ustring_view seed, + bool admin, + const unsigned char* gpk, + std::optional gsk) : + secret_key{sk_from_seed(seed)}, + info{ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + members{ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + keys{to_usv(secret_key), + ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt, + info, + members} {} +}; + +int main() { + + const ustring group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; + const ustring admin_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + const ustring member_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + + std::array group_pk; + std::array group_sk; + + crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + + pseudo_client admin{admin_seed, true, group_pk.data(), group_sk.data()}; + pseudo_client member{member_seed, false, group_pk.data(), std::nullopt}; + session::config::UserGroups member_groups{member_seed, std::nullopt}; + + auto auth_data = admin.keys.swarm_make_subaccount(member.session_id); + { + auto g = member_groups.get_or_construct_group(member.info.id); + g.auth_data = auth_data; + member_groups.set(g); + } + + session::config::UserGroups member_gr2{member_seed, std::nullopt}; + auto [seqno, push, obs] = member_groups.push(); + + std::vector> gr_conf; + gr_conf.emplace_back("fakehash1", push); + + member_gr2.merge(gr_conf); + + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto msg = to_usv("hello world"); + std::array store_sig; + ustring store_to_sign; + store_to_sign += to_usv("store999"); + store_to_sign += to_usv(std::to_string(now)); + crypto_sign_ed25519_detached( + store_sig.data(), nullptr, store_to_sign.data(), store_to_sign.size(), group_sk.data()); + + nlohmann::json store{ + {"method", "store"}, + {"params", + {{"pubkey", member.info.id}, + {"namespace", 999}, + {"timestamp", now}, + {"ttl", 3600'000}, + {"data", oxenc::to_base64(msg)}, + {"signature", oxenc::to_base64(store_sig.begin(), store_sig.end())}}}}; + + std::cout << "STORE:\n\n" << store.dump() << "\n\n"; + + ustring retrieve_to_sign; + retrieve_to_sign += to_usv("retrieve999"); + retrieve_to_sign += to_usv(std::to_string(now)); + auto subauth = member.keys.swarm_subaccount_sign(retrieve_to_sign, auth_data); + + nlohmann::json retrieve{ + {"method", "retrieve"}, + {"params", + { + {"pubkey", member.info.id}, + {"namespace", 999}, + {"timestamp", now}, + {"subaccount", subauth.subaccount}, + {"subaccount_sig", subauth.subaccount_sig}, + {"signature", subauth.signature}, + }}}; + + std::cout << "RETRIEVE:\n\n" << retrieve.dump() << "\n\n"; +} diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 4fe91cac..054fa334 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/config/notify.hpp" #include "utils.hpp" using namespace std::literals; @@ -492,10 +493,13 @@ TEST_CASE("User Groups (new)", "[config][groups][new]") { groups.erase(*c3); - auto gg = groups.get_or_construct_group("030303030303030303030303030303030303030303030303030303030303030303"); + auto gg = groups.get_or_construct_group( + "030303030303030303030303030303030303030303030303030303030303030303"); groups.set(gg); CHECK(groups.erase_group("030303030303030303030303030303030303030303030303030303030303030303")); - CHECK_FALSE(groups.erase_group("030303030303030303030303030303030303030303030303030303030303030303")); + CHECK_FALSE( + groups.erase_group("03030303030303030303030303030303030303030303030303030303030303030" + "3")); } TEST_CASE("User Groups members C API", "[config][groups][c]") { diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 47c547b2..79e2941a 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -12,6 +12,14 @@ 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" + : "debug") + << ": " << msg); +} + TEST_CASE("user profile C API", "[config][user_profile][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 633d0eff..b52d1084 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -7,12 +8,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include "utils.hpp" @@ -51,36 +54,36 @@ struct hacky_list : std::list { T& operator[](size_t n) { return *std::next(std::begin(*this), n); } }; -TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { +struct pseudo_client { + std::array secret_key; + const ustring_view public_key{secret_key.data() + 32, 32}; + std::string session_id{session_id_from_ed(public_key)}; + + groups::Info info; + groups::Members members; + groups::Keys keys; + + pseudo_client( + ustring_view seed, + bool admin, + const unsigned char* gpk, + std::optional gsk) : + secret_key{sk_from_seed(seed)}, + info{ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + members{ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt}, + keys{to_usv(secret_key), + ustring_view{gpk, 32}, + admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::nullopt, + info, + members} {} +}; - struct pseudo_client { - std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; - std::string session_id{session_id_from_ed(public_key)}; - - groups::Info info; - groups::Members members; - groups::Keys keys; - - pseudo_client( - ustring_view seed, - bool a, - const unsigned char* gpk, - std::optional gsk) : - secret_key{sk_from_seed(seed)}, - info{ustring_view{gpk, 32}, - a ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt}, - members{ustring_view{gpk, 32}, - a ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt}, - keys{to_usv(secret_key), - ustring_view{gpk, 32}, - a ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt, - info, - members} {} - }; +TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { const ustring group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; @@ -100,10 +103,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { std::array group_pk; std::array group_sk; - crypto_sign_ed25519_seed_keypair( - group_pk.data(), - group_sk.data(), - reinterpret_cast(group_seed.data())); + crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); @@ -379,7 +379,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { } } -TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") { +TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { struct pseudo_client { const bool is_admin; @@ -485,3 +485,98 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][c]") { // for (const auto& m : members) // REQUIRE(contacts_size(m.members) == 0); } + +TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { + + const ustring group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; + const ustring admin_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + const ustring member_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + + std::array group_pk; + std::array group_sk; + + crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == + oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); + + CHECK(oxenc::to_hex(group_pk.begin(), group_pk.end()) == + "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); + + pseudo_client admin{admin_seed, true, group_pk.data(), group_sk.data()}; + pseudo_client member{member_seed, false, group_pk.data(), std::nullopt}; + session::config::UserGroups member_groups{member_seed, std::nullopt}; + + CHECK(admin.session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); + + CHECK(member.session_id == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5" + "e"); + CHECK(oxenc::to_hex(group_pk.begin(), group_pk.end()) == + "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); + CHECK(member.info.id == "03c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); + + auto auth_data = admin.keys.swarm_make_subaccount(member.session_id); + { + auto g = member_groups.get_or_construct_group(member.info.id); + g.auth_data = auth_data; + member_groups.set(g); + } + + session::config::UserGroups member_gr2{member_seed, std::nullopt}; + auto [seqno, push, obs] = member_groups.push(); + + std::vector> gr_conf; + gr_conf.emplace_back("fakehash1", push); + + member_gr2.merge(gr_conf); + + auto g = member_groups.get_group(member.info.id); + REQUIRE(g); + CHECK(g->id == member.info.id); + CHECK(g->auth_data == auth_data); + + auto to_sign = to_usv("retrieve9991693340111000"); + auto subauth_b64 = member.keys.swarm_subaccount_sign(to_sign, auth_data); + + CHECK(subauth_b64.subaccount == + "0303000085af311da72570859cae7e84d6a135e716a8c0b7f7f4d554b8da4778a636e839"); + CHECK(subauth_b64.subaccount_sig == + "6brvv/" + "2jfciBAJeRKMGSepNJLullyrVVHijyVDE+8GC5Oc89UNxjNrq1kVV1P+pkUIRDOew24gSLFgLZfdl+BQ=="); + CHECK(subauth_b64.signature == + "c3PJ4g29v5RivKm8Tdg49vGU2/" + "6kVd0yONnpz5U5zePMYptqW3iYQ0TYf2rEzv3qqkPhS5p67M5GAccHoBHGDQ=="); + + auto subauth = member.keys.swarm_subaccount_sign(to_sign, auth_data, true); + CHECK(oxenc::to_hex(subauth.subaccount) == subauth_b64.subaccount); + CHECK(oxenc::to_base64(subauth.subaccount_sig) == subauth_b64.subaccount_sig); + CHECK(oxenc::to_base64(subauth.signature) == subauth_b64.signature); + + CHECK(0 == + crypto_sign_ed25519_verify_detached( + reinterpret_cast(subauth.signature.data()), + to_sign.data(), + to_sign.size(), + reinterpret_cast(subauth.subaccount.substr(4).data()))); + + CHECK(member.keys.swarm_verify_subaccount(auth_data)); + CHECK(session::config::groups::Keys::swarm_verify_subaccount( + member.info.id, to_usv(member.secret_key), auth_data)); + + // Try flipping a bit in each position of the auth data and make sure it fails to validate: + for (int i = 0; i < auth_data.size(); i++) { + for (int b = 0; b < 8; b++) { + if (i == 35 && b == 7) // This is the sign bit of k, which can be flipped but gets + // flipped back when dealing with the missing X->Ed conversion + // sign bit, so won't actually change anything if it flips. + continue; + auto auth_data2 = auth_data; + auth_data2[i] ^= 1 << b; + CHECK_FALSE(session::config::groups::Keys::swarm_verify_subaccount( + member.info.id, to_usv(member.secret_key), auth_data2)); + } + } +} diff --git a/tests/utils.hpp b/tests/utils.hpp index f842a076..7d45d3d4 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -3,11 +3,12 @@ #include #include -#include +#include #include #include #include #include +#include #include "session/config/base.h" @@ -66,14 +67,6 @@ inline std::string printable(const unsigned char* x, size_t n) { return printable({x, n}); } -inline 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" - : "debug") - << ": " << msg); -} - template std::set as_set(const Container& c) { return {c.begin(), c.end()}; From 18d3df2d4376d820f6725177a3bba351cb052de0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 30 Aug 2023 19:58:46 -0300 Subject: [PATCH 039/572] Don't build swarm-auth-test by default It adds a nlohmann::json dependency, and isn't really part of the normal test suite. --- include/session/config/groups/keys.h | 35 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index a1e64775..4f4ff496 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -190,6 +190,41 @@ LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) LIBSESSION_EXPORT void groups_keys_dump( config_group_keys* conf, unsigned char** out, size_t* outlen); +/// API: grous/groups_keys_key_supplement +/// +/// 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: +/// - `conf` -- pointer to the keys config 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, + const char** sids, + size_t sids_len, + unsigned char** message, + size_t* message_len); + /// API: groups/groups_keys_encrypt_message /// /// Encrypts a message using the most recent group encryption key of this object. The message will diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cce4b155..3bf47602 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,5 +22,5 @@ target_link_libraries(testAll PRIVATE add_custom_target(check COMMAND testAll) -add_executable(swarm-auth-test swarm-auth-test.cpp) +add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) From 224dda91e1f0a4430e7d0f4bcc09dd82a2fbf4d9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 30 Aug 2023 19:59:30 -0300 Subject: [PATCH 040/572] Format fix --- src/config/internal.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/internal.hpp b/src/config/internal.hpp index ca37f3a0..e78ba640 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -89,7 +89,8 @@ std::string session_id_to_bytes(std::string_view session_id, std::string_view pr // Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 (or // whatever) prefix, which is a pubkey (x25519 for 05 session_ids, ed25519 for other prefixes). -std::array session_id_pk(std::string_view session_id, std::string_view prefix = "05"); +std::array session_id_pk( + std::string_view session_id, std::string_view prefix = "05"); // Validates an open group pubkey; we accept it in hex, base32z, or base64 (padded or unpadded). // Throws std::invalid_argument if invalid. From 9b0cdcdc0cfc788b4e46c1b337ceddfbc1deee0f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 30 Aug 2023 21:15:53 -0300 Subject: [PATCH 041/572] C API updates, and related tweaks - Adds C wrappers for all the swarm authentication methods - Adds C wrapper for querying if the keys object has admin permission - Adds C wrapper for key_supplement to generate supplemental key messages - Changes the swarm `subaccount` value to be base64 instead of hex (since it isn't really a pubkey) - Makes the vector passed into key_supplement const& --- include/session/config/groups/keys.h | 219 ++++++++++++++++++++++++- include/session/config/groups/keys.hpp | 12 +- src/config/groups/keys.cpp | 156 +++++++++++++++++- tests/test_group_keys.cpp | 5 +- 4 files changed, 380 insertions(+), 12 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 4f4ff496..b6e29a41 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -69,6 +69,18 @@ LIBSESSION_EXPORT int groups_keys_init( size_t dumplen, char* error) __attribute__((warn_unused_result)); +/// API: groups/groups_keys_is_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 +/// +/// Outputs: +/// - `true` if we have admin keys, `false` otherwise. +LIBSESSION_EXPORT bool groups_keys_is_admin(const config_group_keys* conf); + /// API: groups/groups_keys_rekey /// /// Generates a new encryption key for the group and returns an encrypted key message to be pushed @@ -190,7 +202,7 @@ LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) LIBSESSION_EXPORT void groups_keys_dump( config_group_keys* conf, unsigned char** out, size_t* outlen); -/// API: grous/groups_keys_key_supplement +/// API: groups/groups_keys_key_supplement /// /// 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 @@ -225,6 +237,211 @@ LIBSESSION_EXPORT bool groups_keys_key_supplement( unsigned char** message, size_t* message_len); +/// API: groups/groups_keys_swarm_make_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 +/// - `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 +/// authentication. +/// +/// 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); + +/// API: groups/groups_keys_swarm_make_subaccount_flags +/// +/// Same as groups_keys_swarm_make_subaccount, but lets you specify whether the write/del flags are +/// present. +/// +/// +/// Inputs: +/// - `conf` -- the config object +/// - `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 +/// false then the user can only retrieve messages. Typically this is true. +/// - `del` -- if true (default is false) then the user shall be allowed to delete messages from the +/// swarm. This permission can be used to appoint a sort of "moderator" who can delete messages +/// 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. +/// +/// Outputs: +/// - `bool` - same as groups_keys_swarm_make_subaccount +LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( + config_group_keys* conf, + const char* session_id, + bool write, + bool del, + unsigned char* sign_value); + +/// API: groups/groups_keys_swarm_verify_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 +/// pubkey, including a proper signature by an admin of the group. The signing value must have read +/// permission, but parameters can be given to also require write or delete permissions. A +/// subaccount signing value should always be checked for validity using this before creating a +/// group that would depend on it. +/// +/// 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". +/// - `session_ed25519_secretkey` -- the user's Session ID secret key (64 bytes). +/// - `signing_value` -- the 100-byte subaccount signing value to validate +/// +/// The key will require read and write access to be acceptable. (See the _flags version if you +/// need something else). +/// +/// Outputs: +/// - `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( + const char* group_id, + const unsigned char* session_ed25519_secretkey, + const unsigned char* signing_value); + +/// API: groups/groups_keys_swarm_verify_subaccount_flags +/// +/// Same as groups_keys_swarm_verify_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 +/// - `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( + 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 +/// +/// 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`). +/// +/// Storage server subaccount authentication requires passing the three values in the returned +/// struct in the storage server request. +/// +/// This version of the function writes base64-encoded values to the output parameters; there is +/// also a `_binary` version that writes raw values. +/// +/// Inputs: +/// - `conf` -- the keys config object +/// - `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). +/// - `msg_len` -- the length of the `msg` buffer +/// - `signing_value` -- the 100-byte subaccount signing value, as produced by an admin's +/// `swarm_make_subaccount` and provided to this member. +/// - `subaccount` -- [out] a C string buffer of *at least* 49 bytes where the null-terminated +/// 48-byte base64-encoded subaccount value will be written. This is the value to pass as +/// `subaccount` for storage server subaccount authentication. +/// - `subaccount_sig` -- [out] a C string buffer of *at least* 89 bytes where the null-terminated, +/// 88-ascii-character base64-encoded version of the 64-byte admin signature authorizing this +/// subaccount will be written. This is the value to be passed as `subaccount_sig` for storage +/// server subaccount authentication. +/// - `signature` -- [out] a C string buffer of *at least* 89 bytes where the null-terminated, +/// 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. +/// +/// 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, + const unsigned char* msg, + size_t msg_len, + const unsigned char* signing_value, + + char* subaccount, + char* subaccount_sig, + char* signature); + +/// API: groups/groups_keys_swarm_subaccount_sign_binary +/// +/// Does exactly the same as groups_keys_swarm_subaccount_sign 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 +/// - `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, + const unsigned char* msg, + size_t msg_len, + const unsigned char* signing_value, + + unsigned char* subaccount, + unsigned char* subaccount_sig, + unsigned char* signature); + +/// API: groups/groups_keys_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 +/// `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 +/// revocation for that subaccount. +/// +/// This is recommended to be used when removing a non-admin member to prevent their access. +/// (Note, however, that there are circumstances where this can fail to prevent access, and so +/// should be combined with proper member removal and key rotation so that even if the member +/// gains access to messages, they cannot read them). +/// +/// Inputs: +/// - `conf` -- the keys config object +/// - `session_id` -- the session ID of the member (in hex) +/// - `token` -- [out] a 36-byte buffer into which to write the subaccount token. +/// +/// 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); + +/// API: groups/groups_keys_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. +/// +/// Inputs: +/// - `conf` -- the keys config object +/// - `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. +/// +/// Outputs: +/// - true if the call succeeded, false if an error occured. +LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( + config_group_keys* conf, + const char* session_id, + bool write, + bool del, + unsigned char* token); + /// API: groups/groups_keys_encrypt_message /// /// Encrypts a message using the most recent group encryption key of this object. The message will diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 718a6422..26719cf6 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -297,7 +297,7 @@ class Keys final : public ConfigSig { /// Outputs: /// - `ustring` containing the message that should be pushed to the swarm containing encrypted /// keys for the given user(s). - ustring key_supplement(std::vector sids) const; + ustring key_supplement(const std::vector& sids) const; ustring key_supplement(std::string sid) const { return key_supplement(std::vector{{std::move(sid)}}); } @@ -373,7 +373,7 @@ class Keys final : public ConfigSig { /// API: groups/Keys::swarm_auth /// /// This struct containing the storage server authentication values for subaccount - /// authentication. The three strings in this struct may be either raw bytes, or hex/base64 + /// authentication. The three strings in this struct may be either raw bytes, or base64 /// encoded, depending on the `binary` parameter passed to `swarm_subaccount_sign`. /// /// `.subaccount` is the value to be passed as the "subaccount" authentication parameter. (It @@ -397,7 +397,7 @@ class Keys final : public ConfigSig { /// /// 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 - /// `swarm_auth_key`). + /// `swarm_make_subaccount`). /// /// Storage server subaccount authentication requires passing the three values in the returned /// struct in the storage server request. (See Keys::swarm_auth for details). @@ -408,9 +408,9 @@ class Keys final : public ConfigSig { /// 999 made at unix time 1234567890.123; see storage server RPC documentation for details). /// - `signing_value` -- the 100-byte subaccount signing value, as produced by an admin's /// `swarm_make_subaccount` and provided to this member. - /// - `binary` -- if set to true then the returned values will be binary. If omitted, the - /// returned struct values will be hex-encoded (subaccount token) or base64-encoded - /// (signatures) suitable for direct passing as JSON values to the storage server. + /// - `binary` -- if set to true then the returned values will be binary. If omitted (or + /// explicitly false), the returned struct values will be base64-encoded suitable for direct + /// passing as JSON values to the storage server without further encoding/modification. /// /// Outputs: /// - struct containing three binary values enabling swarm authentication (see description diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 0e6a81ae..a3dcd2e6 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -366,7 +366,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } -ustring Keys::key_supplement(std::vector sids) const { +ustring Keys::key_supplement(const std::vector& sids) const { if (!admin()) throw std::logic_error{ "Unable to issue supplemental group encryption keys without the main group keys"}; @@ -731,7 +731,7 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // sig is now set to the desired R || S, with S = r + H(R || A || M)a (all mod L) if (!binary) { - token = oxenc::to_hex(token); + token = oxenc::to_base64(token); sub_sig = oxenc::to_base64(sub_sig); sig = oxenc::to_base64(sig); } @@ -1247,6 +1247,10 @@ LIBSESSION_C_API int groups_keys_init( return SESSION_ERR_NONE; } +LIBSESSION_C_API bool groups_keys_is_admin(const config_group_keys* conf) { + return unbox(conf).admin(); +} + LIBSESSION_C_API bool groups_keys_rekey( config_group_keys* conf, config_object* info, @@ -1353,3 +1357,151 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( *plaintext_len = plaintext->size(); return true; } + +LIBSESSION_C_API bool groups_keys_key_supplement( + config_group_keys* conf, + const char** sids, + size_t sids_len, + unsigned char** message, + size_t* message_len) { + assert(sids && message && message_len); + + 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; + } catch (const std::exception& e) { + set_error(conf, e.what()); + return false; + } +} + +LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( + config_group_keys* conf, + const char* session_id, + bool write, + bool del, + unsigned char* sign_value) { + assert(sign_value); + try { + auto val = unbox(conf).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; + } +} + +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 groups_keys_swarm_verify_subaccount_flags( + const char* group_id, + const unsigned char* session_ed25519_secretkey, + const unsigned char* signing_value, + bool write, + bool del) { + try { + return groups::Keys::swarm_verify_subaccount( + group_id, + ustring_view{session_ed25519_secretkey, 64}, + ustring_view{signing_value, 100}, + write, + del); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( + const char* group_id, + const unsigned char* session_ed25519_secretkey, + const unsigned char* signing_value) { + return groups::Keys::swarm_verify_subaccount( + group_id, + ustring_view{session_ed25519_secretkey, 64}, + ustring_view{signing_value, 100}); +} + +LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( + config_group_keys* conf, + const unsigned char* msg, + size_t msg_len, + const unsigned char* signing_value, + + char* subaccount, + char* subaccount_sig, + char* signature) { + assert(msg && signing_value && subaccount && subaccount_sig && signature); + try { + auto auth = unbox(conf).swarm_subaccount_sign( + ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); + assert(auth.subaccount.size() == 48); + assert(auth.subaccount_sig.size() == 88); + assert(auth.signature.size() == 88); + std::memcpy(subaccount, auth.subaccount.c_str(), auth.subaccount.size() + 1); + std::memcpy(subaccount_sig, auth.subaccount_sig.c_str(), auth.subaccount_sig.size() + 1); + 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; + } +} + +LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( + config_group_keys* conf, + const unsigned char* msg, + size_t msg_len, + const unsigned char* signing_value, + + unsigned char* subaccount, + unsigned char* subaccount_sig, + unsigned char* signature) { + assert(msg && signing_value && subaccount && subaccount_sig && signature); + try { + auto auth = unbox(conf).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); + assert(auth.signature.size() == 64); + std::memcpy(subaccount, auth.subaccount.data(), 36); + std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 36); + std::memcpy(signature, auth.signature.data(), 36); + return true; + } catch (const std::exception& e) { + set_error(conf, e.what()); + return false; + } +} + +LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( + config_group_keys* conf, + const char* session_id, + bool write, + bool del, + unsigned char* token) { + try { + auto tok = unbox(conf).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; + } +} + +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); +} diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index b52d1084..cce81204 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -541,8 +541,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") auto to_sign = to_usv("retrieve9991693340111000"); auto subauth_b64 = member.keys.swarm_subaccount_sign(to_sign, auth_data); - CHECK(subauth_b64.subaccount == - "0303000085af311da72570859cae7e84d6a135e716a8c0b7f7f4d554b8da4778a636e839"); + CHECK(subauth_b64.subaccount == "AwMAAIWvMR2nJXCFnK5+hNahNecWqMC39/TVVLjaR3imNug5"); CHECK(subauth_b64.subaccount_sig == "6brvv/" "2jfciBAJeRKMGSepNJLullyrVVHijyVDE+8GC5Oc89UNxjNrq1kVV1P+pkUIRDOew24gSLFgLZfdl+BQ=="); @@ -551,7 +550,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") "6kVd0yONnpz5U5zePMYptqW3iYQ0TYf2rEzv3qqkPhS5p67M5GAccHoBHGDQ=="); auto subauth = member.keys.swarm_subaccount_sign(to_sign, auth_data, true); - CHECK(oxenc::to_hex(subauth.subaccount) == subauth_b64.subaccount); + CHECK(oxenc::to_base64(subauth.subaccount) == subauth_b64.subaccount); CHECK(oxenc::to_base64(subauth.subaccount_sig) == subauth_b64.subaccount_sig); CHECK(oxenc::to_base64(subauth.signature) == subauth_b64.signature); From e30122b524907a2ad724e3fc236770714dc164c7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 15:39:32 -0300 Subject: [PATCH 042/572] Add missing user_groups C API for new groups --- include/session/config/user_groups.h | 135 +++++++++++++++++++++++-- include/session/config/user_groups.hpp | 27 ++++- src/config/user_groups.cpp | 52 ++++++++++ 3 files changed, 203 insertions(+), 11 deletions(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 2e9be8ce..c58f5584 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -108,12 +108,46 @@ LIBSESSION_EXPORT int user_groups_init( size_t dumplen, char* error) __attribute__((warn_unused_result)); +/// API: user_groups/user_groups_get_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 +/// `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") +/// +/// 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); + +/// API: user_groups/user_groups_get_or_construct_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 +/// `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") +/// +/// 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); + /// API: user_groups/user_groups_get_community /// /// Gets community conversation info into `comm`, if the community info was found. `base_url` and -/// `room` are null-terminated c strings; pubkey is 32 bytes. base_url will be -/// normalized/lower-cased; room is case-insensitive for the lookup: note that this may well return -/// a community info with a different room capitalization than the one provided to the call. +/// `room` are null-terminated c strings. base_url will be normalized/lower-cased; room is +/// case-insensitive for the lookup: note that this may well return a community info with a +/// 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 @@ -277,6 +311,16 @@ LIBSESSION_EXPORT void ugroups_legacy_group_free(ugroups_legacy_group_info* grou LIBSESSION_EXPORT void user_groups_set_community( config_object* conf, const ugroups_community_info* group); +/// API: user_groups/user_groups_set_group +/// +/// Adds or updates a (non-legacy) group conversation from the given group info +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_object object +/// - `group` -- [in] Pointer to a group info object +LIBSESSION_EXPORT void user_groups_set_group( + config_object* conf, const ugroups_group_info* group); + /// API: user_groups/user_groups_set_legacy_group /// /// Adds or updates a legacy group conversation from the into. This version of the method should @@ -343,6 +387,29 @@ LIBSESSION_EXPORT void user_groups_set_free_legacy_group( LIBSESSION_EXPORT bool user_groups_erase_community( config_object* conf, const char* base_url, const char* room); +/// API: user_groups/user_groups_erase_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 +/// - `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); + /// API: user_groups/user_groups_erase_legacy_group /// /// Erases a conversation from the conversation list. Returns true if the conversation was found @@ -561,7 +628,7 @@ LIBSESSION_EXPORT size_t user_groups_size(const config_object* conf); /// API: user_groups/user_groups_size_communities /// -/// Returns the number of conversations of the specific type. +/// Returns the number of community conversations. /// /// Declaration: /// ```cpp @@ -577,9 +644,27 @@ LIBSESSION_EXPORT size_t user_groups_size(const config_object* conf); /// - `size_t` -- Returns the number of conversations LIBSESSION_EXPORT size_t user_groups_size_communities(const config_object* conf); +/// API: user_groups/user_groups_size_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 +/// +/// Outputs: +/// - `size_t` -- Returns the number of conversations +LIBSESSION_EXPORT size_t user_groups_size_groups(const config_object* conf); + /// API: user_groups/user_groups_size_legacy_groups /// -/// Returns the number of conversations of the specific type. +/// Returns the number of legacy group conversations. /// /// Declaration: /// ```cpp @@ -605,12 +690,15 @@ typedef struct user_groups_iterator user_groups_iterator; /// ```cpp /// ugroups_community_info c2; /// ugroups_legacy_group_info c3; +/// ugroups_group_info c4; /// user_groups_iterator *it = user_groups_iterator_new(my_groups); /// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) { /// if (user_groups_it_is_community(it, &c2)) { /// // use c2.whatever /// } else if (user_groups_it_is_legacy_group(it, &c3)) { /// // use c3.whatever +/// } else if (user_groups_it_is_group(it, &c4)) { +/// // use c4.whatever /// } /// } /// user_groups_iterator_free(it); @@ -678,6 +766,22 @@ LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_communities( LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_legacy_groups( const config_object* conf); +/// API: user_groups/user_groups_iterator_new_groups +/// +/// The same as `user_groups_iterator_new` 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 +/// +/// Outputs: +/// - `user_groups_iterator*` -- The Iterator +LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_groups( + const config_object* conf); + + /// API: user_groups/user_groups_iterator_free /// /// Frees an iterator once no longer needed. @@ -740,7 +844,7 @@ LIBSESSION_EXPORT void user_groups_iterator_advance(user_groups_iterator* it); /// ``` /// /// Inputs: -/// - `it` -- [in, out] The Iterator +/// - `it` -- [in] The iterator /// - `c` -- [out] sets details of community into here if true /// /// Outputs: @@ -748,7 +852,22 @@ LIBSESSION_EXPORT void user_groups_iterator_advance(user_groups_iterator* it); LIBSESSION_EXPORT bool user_groups_it_is_community( user_groups_iterator* it, ugroups_community_info* c); -/// API: user_groups/user_groups_it_is_community +/// API: user_groups/user_groups_it_is_group +/// +/// If the current iterator record is a non-legacy group conversation this sets the details into +/// `group` and returns true. Otherwise it returns false. +/// +/// Inputs: +/// - `it` -- [in] The Iterator +/// - `group` -- [out] sets details of the group into here (if true is returned) +/// +/// Outputs: +/// - `bool` -- Returns `true` and sets `group` if the group is a non-legacy group (aka closed group). +LIBSESSION_EXPORT bool user_groups_it_is_group( + user_groups_iterator* it, ugroups_group_info* group); + + +/// API: user_groups/user_groups_it_is_legacy_group /// /// If the current iterator record is a legacy group conversation this sets the details into /// `c` and returns true. Otherwise it returns false. @@ -762,7 +881,7 @@ LIBSESSION_EXPORT bool user_groups_it_is_community( /// ``` /// /// Inputs: -/// - `it` -- [in, out] The Iterator +/// - `it` -- [in] The iterator /// - `c` -- [out] sets details of legacy group into here if true /// /// Outputs: diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 16ba58d9..4a3d7d7c 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -585,10 +585,31 @@ class UserGroups : public ConfigBase { template struct subtype_iterator; - /// Returns an iterator that iterates only through one type of conversations. (The regular - /// `.end()` iterator is valid for testing the end of these iterations). - subtype_iterator legacy_groups() const { return {data}; } + /// API: user_groups/UserGroups::begin_groups + /// + /// Inputs: None + /// + /// Outputs: + /// - an iterator that iterates only through group conversations. (The regular `.end()` + /// iterator is valid for testing the end of this iterator). + subtype_iterator begin_groups() const { return {data}; } + + /// API: user_groups/UserGroups::begin_communities + /// + /// Inputs: None + /// + /// Outputs: + /// - an iterator that iterates only through community conversations. (The regular `.end()` + /// iterator is valid for testing the end of this iterator). subtype_iterator begin_communities() const { return {data}; } + + /// API: user_groups/UserGroups::begin_legacy_groups + /// + /// Inputs: None + /// + /// Outputs: + /// - an iterator that iterates only through legacy group conversations. (The regular `.end()` + /// iterator is valid for testing the end of this iterator). subtype_iterator begin_legacy_groups() const { return {data}; } using iterator_category = std::input_iterator_tag; diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 3a79972c..e5c827b1 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -613,6 +613,34 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( return false; } } +LIBSESSION_C_API bool user_groups_get_group( + config_object* conf, ugroups_group_info* group, const char* group_id){ + try { + conf->last_error = nullptr; + if (auto g = unbox(conf)->get_group(group_id)) { + g->into(*group); + return true; + } + } catch (const std::exception& e) { + set_error(conf, 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) { + try { + conf->last_error = nullptr; + unbox(conf) + ->get_or_construct_group(group_id) + .into(*group); + return true; + } catch (const std::exception& e) { + set_error(conf, e.what()); + return false; + } +} LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { if (group && group->_internal) { @@ -657,6 +685,10 @@ 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 user_groups_set_group( + config_object* conf, const ugroups_group_info* group) { + unbox(conf)->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}); @@ -674,6 +706,14 @@ LIBSESSION_C_API bool user_groups_erase_community( return false; } } +LIBSESSION_C_API bool user_groups_erase_group( + config_object* conf, const char* group_id) { + try { + return unbox(conf)->erase_group(group_id); + } catch (...) { + return false; + } +} LIBSESSION_C_API bool user_groups_erase_legacy_group(config_object* conf, const char* group_id) { try { return unbox(conf)->erase_legacy_group(group_id); @@ -772,6 +812,9 @@ LIBSESSION_C_API size_t user_groups_size(const config_object* conf) { LIBSESSION_C_API size_t user_groups_size_communities(const config_object* conf) { return unbox(conf)->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 user_groups_size_legacy_groups(const config_object* conf) { return unbox(conf)->size_legacy_groups(); } @@ -784,6 +827,10 @@ LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_communities( const config_object* conf) { return new user_groups_iterator{{unbox(conf)->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_legacy_groups( const config_object* conf) { return new user_groups_iterator{{unbox(conf)->begin_legacy_groups()}}; @@ -818,6 +865,11 @@ LIBSESSION_C_API bool user_groups_it_is_community( return user_groups_it_is_impl(it, c); } +LIBSESSION_C_API bool user_groups_it_is_group( + user_groups_iterator* it, ugroups_group_info* g) { + return user_groups_it_is_impl(it, g); +} + 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); From 2ad96d55f04d5a615cdbc7b9a409aec5f4415027 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 16:02:20 -0300 Subject: [PATCH 043/572] Keys.size(); updates to Keys C API - Add keys.size() return the number of keys in the object. - Add C API for retrieving keys & key size - Fix copy-and-paste error in groups_members_size C API name --- include/session/config/groups/keys.h | 39 +++++++++++++++++++++++++ include/session/config/groups/keys.hpp | 11 +++++++ include/session/config/groups/members.h | 2 +- src/config/groups/keys.cpp | 17 ++++++++++- tests/test_group_keys.cpp | 2 ++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index b6e29a41..6d7933cc 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -69,6 +69,45 @@ LIBSESSION_EXPORT int groups_keys_init( size_t dumplen, char* error) __attribute__((warn_unused_result)); +/// API: groups/groups_keys_size +/// +/// Returns the number of decryption keys stored in this Keys object. Mainly for +/// debugging/information purposes. +/// +/// Inputs: +/// - `conf` -- keys config object +/// +/// Outputs: +/// - `size_t` number of keys +LIBSESSION_EXPORT size_t groups_keys_size(const config_group_keys* conf); + +/// API: groups/groups_keys_get_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 +/// key). +/// +/// This function is not particularly efficient and is not typically needed except for diagnostics: +/// instead encryption/decryption should be performed used the dedicated functions which +/// automatically manage the decryption keys. +/// +/// This function can be used to obtain all decryption keys by calling it with an incrementing value +/// until it returns nullptr (or alternatively, looping over `0 <= i < groups_keys_size`). +/// +/// Returns nullptr if N is >= the current number of decryption keys. +/// +/// The returned pointer points at a 32-byte binary value containing the key; it should be copied or +/// used at once as it may not remain valid past other calls to the keys object. It should *not* be +/// freed. +/// +/// Inputs: +/// - `conf` -- keys config object +/// - `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 /// /// Returns true if this object has the group private keys, i.e. the user is an all-powerful diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 26719cf6..faddd57a 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -215,6 +215,17 @@ class Keys final : public ConfigSig { /// - `std::vector` - vector of encryption keys. std::vector group_keys() const; + /// API: groups/Keys::size + /// + /// Returns the number of distinct decryption keys that we know about. Mainly for + /// debugging/information purposes. + /// + /// Inputs: none + /// + /// Outputs: + /// - `size_t` of the number of keys we know about + size_t size() const; + /// API: groups/Keys::encryption_key /// /// Accesses the current encryption key: that is, the most current group decryption key. Throws diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index dcaabda4..d65b3245 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -128,7 +128,7 @@ LIBSESSION_EXPORT bool groups_members_erase(config_object* conf, const char* ses /// /// Outputs: /// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); +LIBSESSION_EXPORT size_t groups_members_size(const config_object* conf); typedef struct groups_members_iterator { void* _internals; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3dcd2e6..ba064792 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -149,9 +149,13 @@ void Keys::load_dump(ustring_view dump) { } } +size_t Keys::size() const { + return keys_.size() + !pending_key_config_.empty(); +} + std::vector Keys::group_keys() const { std::vector ret; - ret.reserve(keys_.size() + !pending_key_config_.empty()); + ret.reserve(size()); if (!pending_key_config_.empty()) ret.emplace_back(pending_key_.data(), 32); @@ -1247,6 +1251,17 @@ LIBSESSION_C_API int groups_keys_init( return SESSION_ERR_NONE; } +LIBSESSION_C_API size_t group_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(); } diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index cce81204..be4fd860 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -362,11 +362,13 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { if (i < 1) { // This supp key wasn't for us CHECK_FALSE(found_key); + CHECK(m.keys.size() == 3); CHECK(m.keys.group_keys().size() == 3); } else { CHECK(found_key); // new_keys_config1 never went to the initial members, but did go out in the // supplement, which is why we have the extra key here. + CHECK(m.keys.size() == 4); CHECK(m.keys.group_keys().size() == 4); } From 8a9d8accfa27e1ab843427f7f2ad9f12bf0e8d05 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 16:24:20 -0300 Subject: [PATCH 044/572] Fix doc typos/mistakes --- include/session/config/groups/members.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index d65b3245..fa9ad181 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -65,7 +65,7 @@ LIBSESSION_EXPORT int groups_members_init( /// - `session_id` -- [in] null terminated hex string /// /// Output: -/// - `bool` -- Returns true if member exsts +/// - `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)); @@ -87,8 +87,8 @@ LIBSESSION_EXPORT bool groups_members_get( /// - `session_id` -- [in] null terminated hex string /// /// Output: -/// - `bool` -- Returns true if the member exists, false if not (`member` is always filled -/// regardless). +/// - `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)); From 24ed15885efe1598cef2574cbc0495ff95b8f80d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 17:16:07 -0300 Subject: [PATCH 045/572] Add groups to convo info volatile --- include/session/config/convo_info_volatile.h | 198 ++++++++++++++++-- .../session/config/convo_info_volatile.hpp | 102 ++++++++- src/config/convo_info_volatile.cpp | 68 +++++- tests/test_config_convo_info_volatile.cpp | 25 ++- 4 files changed, 364 insertions(+), 29 deletions(-) diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index fc94ab1a..cd3f6f43 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -24,6 +24,12 @@ typedef struct convo_info_volatile_community { bool unread; // true if marked unread } convo_info_volatile_community; +typedef struct convo_info_volatile_group { + char group_id[67]; // in hex; 66 hex chars + null terminator. Begins with "03". + int64_t last_read; // ms since unix epoch + bool unread; // true if marked unread +} convo_info_volatile_group; + typedef struct convo_info_volatile_legacy_group { char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID, // though isn't really one. @@ -189,7 +195,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_community( /// /// Declaration: /// ```cpp -/// BOOL convo_info_volatile_get_or_constructcommunity( +/// BOOL convo_info_volatile_get_or_construct_community( /// [in] config_object* conf, /// [out] convo_info_volatile_community* comm, /// [in] const char* base_url, @@ -206,7 +212,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_community( /// - `pubkey` -- [in] 32 byte binary data of the pubkey /// /// Outputs: -/// - `bool` - Returns true if the community exists +/// - `bool` - Returns true if the call succeeds LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( config_object* conf, convo_info_volatile_community* convo, @@ -214,6 +220,67 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( const char* room, unsigned const char* pubkey) __attribute__((warn_unused_result)); +/// API: convo_info_volatile/convo_info_volatile_get_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 +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `convo` -- [out] Pointer to group +/// - `id` -- [in] Null terminated hex string (66 chars, beginning with 03) specifying the ID of the +/// group +/// +/// 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) + __attribute__((warn_unused_result)); + +/// API: convo_info_volatile/convo_info_volatile_get_or_construct_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`. +/// +/// 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 +/// - `convo` -- [out] Pointer to group +/// - `id` -- [in] Null terminated hex string specifying the ID of the group +/// +/// 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) + __attribute__((warn_unused_result)); + + /// API: convo_info_volatile/convo_info_volatile_get_legacy_group /// /// Fills `convo` with the conversation info given a legacy group ID (specified as a null-terminated @@ -233,10 +300,10 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [out] Pointer to legacy group -/// - `id` -- [in] Null terminated jex string specifying the ID of the legacy group +/// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group /// /// Outputs: -/// - `bool` - Returns true if the community exists +/// - `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)); @@ -265,10 +332,10 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_legacy_group( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [out] Pointer to legacy group -/// - `id` -- [in] Null terminated jex string specifying the ID of the legacy group +/// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group /// /// Outputs: -/// - `bool` - Returns true if the community exists +/// - `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)); @@ -309,6 +376,24 @@ LIBSESSION_EXPORT void convo_info_volatile_set_1to1( LIBSESSION_EXPORT void convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo); +/// API: convo_info_volatile/convo_info_volatile_set_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 +/// - `convo` -- [in] Pointer to group info structure +LIBSESSION_EXPORT void convo_info_volatile_set_group( + config_object* conf, const convo_info_volatile_group* convo); + /// API: convo_info_volatile/convo_info_volatile_set_legacy_group /// /// Adds or updates a legacy group from the given convo info @@ -323,7 +408,7 @@ LIBSESSION_EXPORT void convo_info_volatile_set_community( /// /// Inputs: /// - `conf` -- [in] Pointer to the config object -/// - `convo` -- [in] Pointer to community info structure +/// - `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); @@ -374,6 +459,29 @@ LIBSESSION_EXPORT bool convo_info_volatile_erase_1to1(config_object* conf, const LIBSESSION_EXPORT bool convo_info_volatile_erase_community( config_object* conf, const char* base_url, const char* room); +/// API: convo_info_volatile/convo_info_volatile_erase_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 +/// - `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); + + /// API: convo_info_volatile/convo_info_volatile_erase_legacy_group /// /// Erases a legacy group. Returns true if the group was found @@ -451,6 +559,24 @@ LIBSESSION_EXPORT size_t convo_info_volatile_size_1to1(const config_object* conf /// - `size_t` -- number of communities LIBSESSION_EXPORT size_t convo_info_volatile_size_communities(const config_object* conf); +/// API: convo_info_volatile/convo_info_volatile_size_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 +/// +/// Outputs: +/// - `size_t` -- number of groups +LIBSESSION_EXPORT size_t convo_info_volatile_size_groups(const config_object* conf); + /// API: convo_info_volatile/convo_info_volatile_size_legacy_groups /// /// Returns the number of legacy groups. @@ -479,15 +605,18 @@ typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// ```cpp /// convo_info_volatile_1to1 c1; /// convo_info_volatile_community c2; -/// convo_info_volatile_legacy_group c3; +/// convo_info_volatile_group c3; +/// convo_info_volatile_legacy_group c4; /// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos); /// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { /// if (convo_info_volatile_it_is_1to1(it, &c1)) { /// // use c1.whatever /// } else if (convo_info_volatile_it_is_community(it, &c2)) { /// // use c2.whatever -/// } else if (convo_info_volatile_it_is_legacy_group(it, &c3)) { +/// } else if (convo_info_volatile_it_is_group(it, &c3)) { /// // use c3.whatever +/// } else if (convo_info_volatile_it_is_legacy_group(it, &c4)) { +/// // use c4.whatever /// } /// } /// convo_info_volatile_iterator_free(it); @@ -557,6 +686,29 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities( const config_object* conf); +/// API: convo_info_volatile/convo_info_volatile_iterator_new_groups +/// +/// The same as `convo_info_volatile_iterator_new` except that this iterates *only* over one type of +/// conversation. You still need to use `convo_info_volatile_it_is_group` (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 +/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_groups( +/// [in] const config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `convo_info_volatile_iterator*` -- Iterator +LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( + const config_object* conf); + /// API: convo_info_volatile/convo_info_volatile_iterator_new_legacy_groups /// /// The same as `convo_info_volatile_iterator_new` except that this iterates *only* over one type of @@ -656,7 +808,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_it_is_1to1( /// ```cpp /// BOOL convo_info_volatile_it_is_community( /// [in] convo_info_volatile_iterator* it, -/// [out] convo_info_volatile_1to1* c +/// [out] convo_info_volatile_community* c /// ); /// ``` /// @@ -672,6 +824,28 @@ LIBSESSION_EXPORT bool convo_info_volatile_it_is_1to1( LIBSESSION_EXPORT bool convo_info_volatile_it_is_community( convo_info_volatile_iterator* it, convo_info_volatile_community* c); +/// API: convo_info_volatile/convo_info_volatile_it_is_group +/// +/// If the current iterator record is a group conversation this sets the details into `g` and +/// returns true. Otherwise it returns false. +/// +/// Declaration: +/// ```cpp +/// BOOL convo_info_volatile_it_is_group( +/// [in] convo_info_volatile_iterator* it, +/// [out] convo_info_volatile_group* g +/// ); +/// ``` +/// +/// Inputs: +/// - `it` -- [in] The convo_info_volatile_iterator +/// - `c` -- [out] Pointer to the convo_info_volatile, will be populated if true +/// +/// Outputs: +/// - `bool` -- True if the record is a group conversation +LIBSESSION_EXPORT bool convo_info_volatile_it_is_group( + convo_info_volatile_iterator* it, convo_info_volatile_group* c); + /// API: convo_info_volatile/convo_info_volatile_it_is_legacy_group /// /// If the current iterator record is a legacy group conversation this sets the details into `c` and @@ -680,8 +854,8 @@ LIBSESSION_EXPORT bool convo_info_volatile_it_is_community( /// Declaration: /// ```cpp /// BOOL convo_info_volatile_it_is_legacy_group( -/// [in] convo_info_volatile_iterator* it, -/// [out] convo_info_volatile_1to1* c +/// [in] convo_info_volatile_iterator* it, +/// [out] convo_info_volatile_legacy_group* c /// ); /// ``` /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 482906af..28b3e822 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -14,6 +14,7 @@ using namespace std::literals; extern "C" { struct convo_info_volatile_1to1; struct convo_info_volatile_community; +struct convo_info_volatile_group; struct convo_info_volatile_legacy_group; } @@ -41,14 +42,18 @@ class ConvoInfoVolatile; /// included, but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. /// +/// g - group conversations (aka new, non-legacy closed groups). The key is the group identifier +/// (beginning with 03). Values are dicts with keys: +/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always +/// included, but will be 0 if no messages are read. +/// u - will be present and set to 1 if this conversation is specifically marked unread. +/// /// C - legacy group conversations (aka closed groups). The key is the group identifier (which /// looks indistinguishable from a Session ID, but isn't really a proper Session ID). Values /// are dicts with keys: /// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, /// but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. -/// -/// c - reserved for future tracking of new group conversations. namespace convo { @@ -103,6 +108,26 @@ namespace convo { friend struct session::config::comm_iterator_helper; }; + struct group : base { + std::string id; // 66 hex digits starting with "03" + + /// API: convo_info_volatile/group::group + /// + /// Constructs an empty group from an id + /// + /// Inputs: + /// - `group_id` -- hex string of group_id, 66 hex bytes starting with "03" + explicit group(std::string&& group_id); + explicit group(std::string_view group_id); + + // Internal ctor/method for C API implementations: + group(const struct convo_info_volatile_group& c); // From c struct + void into(convo_info_volatile_group& c) const; // Into c struct + + private: + friend class session::config::ConvoInfoVolatile; + }; + struct legacy_group : base { std::string id; // in hex, indistinguishable from a Session ID @@ -129,7 +154,7 @@ namespace convo { friend class session::config::ConvoInfoVolatile; }; - using any = std::variant; + using any = std::variant; } // namespace convo class ConvoInfoVolatile : public ConfigBase { @@ -249,6 +274,18 @@ class ConvoInfoVolatile : public ConfigBase { /// - `std::optional` - Returns a community std::optional get_community(std::string_view partial_url) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_group + /// + /// Looks up and returns a group conversation by ID. The ID is a 66-character hex string + /// beginning with "03". Returns nullopt if there is no record of the group conversation. + /// + /// Inputs: + /// - `pubkey_hex` -- Hex string of the group ID + /// + /// Outputs: + /// - `std::optional` - Returns a group + std::optional get_group(std::string_view pubkey_hex) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_legacy_group /// /// Looks up and returns a legacy group conversation by ID. The ID looks like a hex Session ID, @@ -275,6 +312,19 @@ class ConvoInfoVolatile : public ConfigBase { /// - `convo::one_to_one` - Returns a contact convo::one_to_one get_or_construct_1to1(std::string_view session_id) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_group + /// + /// These are the same as the above `get` methods (without "_or_construct" in the name), except + /// that when the conversation doesn't exist a new one is created, prefilled with the + /// pubkey/url/etc. + /// + /// Inputs: + /// - `pubkey_hex` -- Hex string pubkey + /// + /// Outputs: + /// - `convo::group` - Returns a group + convo::group get_or_construct_group(std::string_view pubkey_hex) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_legacy_group /// /// These are the same as the above `get` methods (without "_or_construct" in the name), except @@ -347,6 +397,7 @@ class ConvoInfoVolatile : public ConfigBase { /// Declaration: /// ```cpp /// void set(const convo::one_to_one& c); + /// void set(const convo::group& c); /// void set(const convo::legacy_group& c); /// void set(const convo::community& c); /// void set(const convo::any& c); // Variant which can be any of the above @@ -356,6 +407,7 @@ class ConvoInfoVolatile : public ConfigBase { /// - `c` -- struct containing any contact, community or group void set(const convo::one_to_one& c); void set(const convo::legacy_group& c); + void set(const convo::group& c); void set(const convo::community& c); void set(const convo::any& c); // Variant which can be any of the above @@ -392,6 +444,17 @@ class ConvoInfoVolatile : public ConfigBase { /// - `bool` - Returns true if found and removed, otherwise false bool erase_community(std::string_view base_url, std::string_view room); + /// API: convo_info_volatile/ConvoInfoVolatile::erase_group + /// + /// Removes a group conversation. Returns true if found and removed, false if not present. + /// + /// Inputs: + /// - `pubkey_hex` -- String of the group pubkey + /// + /// Outputs: + /// - `bool` - Returns true if found and removed, otherwise false + bool erase_group(std::string_view pubkey_hex); + /// API: convo_info_volatile/ConvoInfoVolatile::erase_legacy_group /// /// Removes a legacy group conversation. Returns true if found and removed, false if not @@ -423,6 +486,7 @@ class ConvoInfoVolatile : public ConfigBase { /// - `bool` - Returns true if found and removed, otherwise false bool erase(const convo::one_to_one& c); bool erase(const convo::community& c); + bool erase(const convo::group& c); bool erase(const convo::legacy_group& c); bool erase(const convo::any& c); // Variant of any of them @@ -438,6 +502,7 @@ class ConvoInfoVolatile : public ConfigBase { /// size_t size() const; /// size_t size_1to1() const; /// size_t size_communities() const; + /// size_t size_groups() const; /// size_t size_legacy_groups() const; /// ``` /// @@ -447,9 +512,11 @@ class ConvoInfoVolatile : public ConfigBase { /// - `size_t` - Returns the number of conversations size_t size() const; - /// Returns the number of 1-to-1, community, and legacy group conversations, respectively. + /// Returns the number of 1-to-1, community, group, and legacy group conversations, + /// respectively. size_t size_1to1() const; size_t size_communities() const; + size_t size_groups() const; size_t size_legacy_groups() const; /// API: convo_info_volatile/ConvoInfoVolatile::empty @@ -474,6 +541,8 @@ class ConvoInfoVolatile : public ConfigBase { /// // use dm->session_id, dm->last_read, etc. /// } else if (const auto* og = std::get_if(&convo)) { /// // use og->base_url, og->room, om->last_read, etc. + /// } else if (const auto* cg = std::get_if(&convo)) { + /// // use cg->id, cg->last_read /// } else if (const auto* lcg = std::get_if(&convo)) { /// // use lcg->id, lcg->last_read /// } @@ -483,6 +552,9 @@ class ConvoInfoVolatile : public ConfigBase { /// This iterates through all conversations in sorted order (sorted first by convo type, then by /// id within the type). /// + /// The `begin_TYPE()` versions of the iterator return an iterator that loops only through the + /// given `TYPE` of conversations. (The .end() iterator works for all the iterator variations). + /// /// It is NOT permitted to add/modify/remove records while iterating; performing modifications /// based on a condition requires two passes: one to collect the required changes, and another /// to apply them key by key. @@ -492,6 +564,7 @@ class ConvoInfoVolatile : public ConfigBase { /// iterator begin() const; /// subtype_iterator begin_1to1() const; /// subtype_iterator begin_communities() const; + /// subtype_iterator begin_groups() const; /// subtype_iterator begin_legacy_groups() const; /// ``` /// @@ -503,7 +576,8 @@ class ConvoInfoVolatile : public ConfigBase { /// API: convo_info_volatile/ConvoInfoVolatile::end /// - /// Iterator for passing the end of the conversations + /// Iterator for passing the end of the conversations. This works for both the all-convo + /// iterator (`begin()`) and the type-specific iterators (e.g. `begin_groups()`). /// /// Inputs: None /// @@ -517,10 +591,12 @@ class ConvoInfoVolatile : public ConfigBase { /// Returns an iterator that iterates only through one type of conversations subtype_iterator begin_1to1() const { return {data}; } subtype_iterator begin_communities() const { return {data}; } + subtype_iterator begin_groups() const { return {data}; } subtype_iterator begin_legacy_groups() const { return {data}; } using iterator_category = std::input_iterator_tag; - using value_type = std::variant; + using value_type = + std::variant; using reference = value_type&; using pointer = value_type*; using difference_type = std::ptrdiff_t; @@ -528,15 +604,18 @@ class ConvoInfoVolatile : public ConfigBase { struct iterator { protected: std::shared_ptr _val; - std::optional _it_11, _end_11, _it_lgroup, _end_lgroup; + std::optional _it_11, _end_11, _it_group, _end_group, _it_lgroup, + _end_lgroup; std::optional _it_comm; void _load_val(); iterator() = default; // Constructs an end tombstone - explicit iterator( + iterator( const DictFieldRoot& data, - bool oneto1 = true, - bool communities = true, - bool legacy_groups = true); + bool oneto1, + bool communities, + bool groups, + bool legacy_groups); + explicit iterator(const DictFieldRoot& data) : iterator(data, true, true, true, true) {} friend class ConvoInfoVolatile; public: @@ -561,6 +640,7 @@ class ConvoInfoVolatile : public ConfigBase { data, std::is_same_v, std::is_same_v, + std::is_same_v, std::is_same_v) {} friend class ConvoInfoVolatile; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 27d40b19..d4eac2c1 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -31,7 +31,7 @@ namespace convo { one_to_one::one_to_one(std::string_view sid) : session_id{sid} { check_session_id(session_id); } - one_to_one::one_to_one(const struct convo_info_volatile_1to1& c) : + one_to_one::one_to_one(const convo_info_volatile_1to1& c) : base{c.last_read, c.unread}, session_id{c.session_id, 66} {} void one_to_one::into(convo_info_volatile_1to1& c) const { @@ -54,13 +54,29 @@ namespace convo { c.unread = unread; } + group::group(std::string&& cgid) : id{std::move(cgid)} { + check_session_id(id, "03"); + } + group::group(std::string_view cgid) : id{cgid} { + check_session_id(id, "03"); + } + group::group(const convo_info_volatile_group& c) : + base{c.last_read, c.unread}, id{c.group_id, 66} {} + + void group::into(convo_info_volatile_group& c) const { + std::memcpy(c.group_id, id.c_str(), 67); + c.last_read = last_read; + c.unread = unread; + } + + legacy_group::legacy_group(std::string&& cgid) : id{std::move(cgid)} { check_session_id(id); } legacy_group::legacy_group(std::string_view cgid) : id{cgid} { check_session_id(id); } - legacy_group::legacy_group(const struct convo_info_volatile_legacy_group& c) : + legacy_group::legacy_group(const convo_info_volatile_legacy_group& c) : base{c.last_read, c.unread}, id{c.group_id, 66} {} void legacy_group::into(convo_info_volatile_legacy_group& c) const { @@ -158,6 +174,28 @@ convo::community ConvoInfoVolatile::get_or_construct_community( return result; } +std::optional ConvoInfoVolatile::get_group( + std::string_view pubkey_hex) const { + std::string pubkey = session_id_to_bytes(pubkey_hex, "03"); + + auto* info_dict = data["g"][pubkey].dict(); + if (!info_dict) + return std::nullopt; + + auto result = std::make_optional(std::string{pubkey_hex}); + result->load(*info_dict); + return result; +} + +convo::group ConvoInfoVolatile::get_or_construct_group( + std::string_view pubkey_hex) const { + if (auto maybe = get_group(pubkey_hex)) + return *std::move(maybe); + + return convo::group{std::string{pubkey_hex}}; +} + + std::optional ConvoInfoVolatile::get_legacy_group( std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); @@ -242,6 +280,11 @@ void ConvoInfoVolatile::set(const convo::community& c) { set_base(c, info); } +void ConvoInfoVolatile::set(const convo::group& c) { + auto info = data["g"][session_id_to_bytes(c.id, "03")]; + set_base(c, info); +} + void ConvoInfoVolatile::set(const convo::legacy_group& c) { auto info = data["C"][session_id_to_bytes(c.id)]; set_base(c, info); @@ -270,6 +313,9 @@ bool ConvoInfoVolatile::erase(const convo::community& c) { } return gone; } +bool ConvoInfoVolatile::erase(const convo::group& c) { + return erase_impl(data["g"][session_id_to_bytes(c.id, "03")]); +} bool ConvoInfoVolatile::erase(const convo::legacy_group& c) { return erase_impl(data["C"][session_id_to_bytes(c.id)]); } @@ -283,6 +329,9 @@ bool ConvoInfoVolatile::erase_1to1(std::string_view session_id) { bool ConvoInfoVolatile::erase_community(std::string_view base_url, std::string_view room) { return erase(convo::community{base_url, room}); } +bool ConvoInfoVolatile::erase_group(std::string_view id) { + return erase(convo::group{id}); +} bool ConvoInfoVolatile::erase_legacy_group(std::string_view id) { return erase(convo::legacy_group{id}); } @@ -309,6 +358,12 @@ size_t ConvoInfoVolatile::size_communities() const { return count; } +size_t ConvoInfoVolatile::size_groups() const { + if (auto* d = data["g"].dict()) + return d->size(); + return 0; +} + size_t ConvoInfoVolatile::size_legacy_groups() const { if (auto* d = data["C"].dict()) return d->size(); @@ -316,11 +371,11 @@ size_t ConvoInfoVolatile::size_legacy_groups() const { } size_t ConvoInfoVolatile::size() const { - return size_1to1() + size_communities() + size_legacy_groups(); + return size_1to1() + size_communities() + size_legacy_groups() + size_groups(); } ConvoInfoVolatile::iterator::iterator( - const DictFieldRoot& data, bool oneto1, bool communities, bool legacy_groups) { + const DictFieldRoot& data, bool oneto1, bool communities, bool groups, bool legacy_groups) { if (oneto1) if (auto* d = data["1"].dict()) { _it_11 = d->begin(); @@ -329,6 +384,11 @@ ConvoInfoVolatile::iterator::iterator( if (communities) if (auto* d = data["o"].dict()) _it_comm.emplace(d->begin(), d->end()); + if (groups) + if (auto* d = data["g"].dict()) { + _it_group = d->begin(); + _end_group = d->end(); + } if (legacy_groups) if (auto* d = data["C"].dict()) { _it_lgroup = d->begin(); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 0edc9720..a7d6095b 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -35,6 +35,9 @@ TEST_CASE("Conversations", "[config][conversations]") { constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"sv; + constexpr auto benders_nightmare_group = + "030111101001001000101010011011010010101010111010000110100001210000"sv; + CHECK_FALSE(convos.get_1to1(definitely_real_id)); CHECK(convos.empty()); @@ -79,6 +82,18 @@ TEST_CASE("Conversations", "[config][conversations]") { // The new data doesn't get stored until we call this: convos.set(og); + CHECK_FALSE(convos.get_group(benders_nightmare_group)); + + auto g = convos.get_or_construct_group(benders_nightmare_group); + CHECK(g.id == benders_nightmare_group); + CHECK(g.last_read == 0); + CHECK_FALSE(g.unread); + + g.last_read = now_ms; + g.unread = true; + convos.set(g); + + auto [seqno, to_push, obs] = convos.push(); CHECK(seqno == 1); @@ -111,6 +126,11 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(x2->pubkey_hex() == to_hex(open_group_pubkey)); CHECK(x2->unread); + auto x3 = convos2.get_group(benders_nightmare_group); + REQUIRE(x3); + CHECK(x3->last_read == now_ms); + CHECK(x3->unread); + auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"sv; auto c2 = convos.get_or_construct_1to1(another_id); c2.unread = true; @@ -143,7 +163,7 @@ TEST_CASE("Conversations", "[config][conversations]") { for (auto* conv : {&convos, &convos2}) { // Iterate through and make sure we got everything we expected seen.clear(); - CHECK(conv->size() == 4); + CHECK(conv->size() == 5); CHECK(conv->size_1to1() == 2); CHECK(conv->size_communities() == 1); CHECK(conv->size_legacy_groups() == 1); @@ -174,8 +194,9 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK_FALSE(convos.needs_push()); convos.erase_1to1("055000000000000000000000000000000000000000000000000000000000000000"); CHECK(convos.needs_push()); - CHECK(convos.size() == 3); + CHECK(convos.size() == 4); CHECK(convos.size_1to1() == 1); + CHECK(convos.size_groups() == 1); // Check the single-type iterators: seen.clear(); From 4d0c6e412e248547f66a1392a5987d67e46be5ea Mon Sep 17 00:00:00 2001 From: dr7ana Date: Thu, 31 Aug 2023 13:39:17 -0700 Subject: [PATCH 046/572] Completed group keys C api unit test --- include/session/config/convo_info_volatile.h | 5 +- include/session/config/user_groups.h | 25 +-- src/config/convo_info_volatile.cpp | 8 +- src/config/user_groups.cpp | 22 +-- tests/test_config_convo_info_volatile.cpp | 1 - tests/test_group_keys.cpp | 195 ++++++++++++++----- 6 files changed, 167 insertions(+), 89 deletions(-) diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index cd3f6f43..6ddba3eb 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -280,7 +280,6 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_group( config_object* conf, convo_info_volatile_group* convo, const char* id) __attribute__((warn_unused_result)); - /// API: convo_info_volatile/convo_info_volatile_get_legacy_group /// /// Fills `convo` with the conversation info given a legacy group ID (specified as a null-terminated @@ -478,9 +477,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_erase_community( /// /// 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 convo_info_volatile_erase_group(config_object* conf, const char* group_id); /// API: convo_info_volatile/convo_info_volatile_erase_legacy_group /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index c58f5584..65529fe0 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -121,9 +121,7 @@ LIBSESSION_EXPORT int user_groups_init( /// 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); + config_object* conf, ugroups_group_info* group, const char* group_id); /// API: user_groups/user_groups_get_or_construct_group /// @@ -138,9 +136,7 @@ LIBSESSION_EXPORT bool user_groups_get_group( /// 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); + config_object* conf, ugroups_group_info* group, const char* group_id); /// API: user_groups/user_groups_get_community /// @@ -318,8 +314,7 @@ LIBSESSION_EXPORT void user_groups_set_community( /// Inputs: /// - `conf` -- [in] Pointer to config_object 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 user_groups_set_group(config_object* conf, const ugroups_group_info* group); /// API: user_groups/user_groups_set_legacy_group /// @@ -407,8 +402,7 @@ LIBSESSION_EXPORT bool user_groups_erase_community( /// /// 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 user_groups_erase_group(config_object* conf, const char* group_id); /// API: user_groups/user_groups_erase_legacy_group /// @@ -778,9 +772,7 @@ LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_legacy_groups( /// /// 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 config_object* conf); /// API: user_groups/user_groups_iterator_free /// @@ -862,10 +854,9 @@ LIBSESSION_EXPORT bool user_groups_it_is_community( /// - `group` -- [out] sets details of the group into here (if true is returned) /// /// Outputs: -/// - `bool` -- Returns `true` and sets `group` if the group is a non-legacy group (aka closed group). -LIBSESSION_EXPORT bool user_groups_it_is_group( - user_groups_iterator* it, ugroups_group_info* group); - +/// - `bool` -- Returns `true` and sets `group` if the group is a non-legacy group (aka closed +/// group). +LIBSESSION_EXPORT bool user_groups_it_is_group(user_groups_iterator* it, ugroups_group_info* group); /// API: user_groups/user_groups_it_is_legacy_group /// diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index d4eac2c1..3ce3dae1 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -69,7 +69,6 @@ namespace convo { c.unread = unread; } - legacy_group::legacy_group(std::string&& cgid) : id{std::move(cgid)} { check_session_id(id); } @@ -174,8 +173,7 @@ convo::community ConvoInfoVolatile::get_or_construct_community( return result; } -std::optional ConvoInfoVolatile::get_group( - std::string_view pubkey_hex) const { +std::optional ConvoInfoVolatile::get_group(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex, "03"); auto* info_dict = data["g"][pubkey].dict(); @@ -187,15 +185,13 @@ std::optional ConvoInfoVolatile::get_group( return result; } -convo::group ConvoInfoVolatile::get_or_construct_group( - std::string_view pubkey_hex) const { +convo::group ConvoInfoVolatile::get_or_construct_group(std::string_view pubkey_hex) const { if (auto maybe = get_group(pubkey_hex)) return *std::move(maybe); return convo::group{std::string{pubkey_hex}}; } - std::optional ConvoInfoVolatile::get_legacy_group( 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 e5c827b1..c84a4893 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -614,7 +614,7 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( } } LIBSESSION_C_API bool user_groups_get_group( - config_object* conf, ugroups_group_info* group, const char* group_id){ + config_object* conf, ugroups_group_info* group, const char* group_id) { try { conf->last_error = nullptr; if (auto g = unbox(conf)->get_group(group_id)) { @@ -627,14 +627,10 @@ LIBSESSION_C_API bool user_groups_get_group( return false; } LIBSESSION_C_API bool user_groups_get_or_construct_group( - config_object* conf, - ugroups_group_info* group, - const char* group_id) { + config_object* conf, ugroups_group_info* group, const char* group_id) { try { conf->last_error = nullptr; - unbox(conf) - ->get_or_construct_group(group_id) - .into(*group); + unbox(conf)->get_or_construct_group(group_id).into(*group); return true; } catch (const std::exception& e) { set_error(conf, e.what()); @@ -685,8 +681,7 @@ 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 user_groups_set_group( - config_object* conf, const ugroups_group_info* group) { +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 user_groups_set_legacy_group( @@ -706,8 +701,7 @@ LIBSESSION_C_API bool user_groups_erase_community( return false; } } -LIBSESSION_C_API bool user_groups_erase_group( - config_object* conf, const char* group_id) { +LIBSESSION_C_API bool user_groups_erase_group(config_object* conf, const char* group_id) { try { return unbox(conf)->erase_group(group_id); } catch (...) { @@ -827,8 +821,7 @@ LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_communities( const config_object* conf) { return new user_groups_iterator{{unbox(conf)->begin_communities()}}; } -LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_groups( - const config_object* conf) { +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_legacy_groups( @@ -865,8 +858,7 @@ LIBSESSION_C_API bool user_groups_it_is_community( return user_groups_it_is_impl(it, c); } -LIBSESSION_C_API bool user_groups_it_is_group( - user_groups_iterator* it, ugroups_group_info* g) { +LIBSESSION_C_API bool user_groups_it_is_group(user_groups_iterator* it, ugroups_group_info* g) { return user_groups_it_is_impl(it, g); } diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index a7d6095b..9e4ddf22 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -93,7 +93,6 @@ TEST_CASE("Conversations", "[config][conversations]") { g.unread = true; convos.set(g); - auto [seqno, to_push, obs] = convos.push(); CHECK(seqno == 1); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index be4fd860..d5a867c9 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -383,34 +384,20 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { struct pseudo_client { - const bool is_admin; - - const ustring seed; - std::string session_id; - - std::array public_key; std::array secret_key; + const ustring_view public_key{secret_key.data() + 32, 32}; + std::string session_id{session_id_from_ed(public_key)}; config_group_keys* keys; config_object* info; config_object* members; - pseudo_client(ustring s, bool a, unsigned char* gpk, std::optional gsk) : - seed{s}, is_admin{a} { - crypto_sign_ed25519_seed_keypair( - public_key.data(), - secret_key.data(), - reinterpret_cast(seed.data())); - - REQUIRE(oxenc::to_hex(seed.begin(), seed.end()) == - oxenc::to_hex(secret_key.begin(), secret_key.begin() + 32)); - - std::array sid; - int rc = crypto_sign_ed25519_pk_to_curve25519(&sid[1], public_key.data()); - REQUIRE(rc == 0); - session_id += "\x05"; - oxenc::to_hex(sid.begin(), sid.end(), std::back_inserter(session_id)); - + 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); @@ -459,33 +446,149 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - std::vector admins; - std::vector members; + 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()); - - // for (int i = 0; i < 4; ++i) - // members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); - - // REQUIRE(admins[0].session_id == - // "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); - // REQUIRE(admins[1].session_id == - // "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - // REQUIRE(members[0].session_id == - // "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); - // REQUIRE(members[1].session_id == - // "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); - // REQUIRE(members[2].session_id == - // "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); - // REQUIRE(members[3].session_id == - // "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); - - // for (const auto& a : admins) - // REQUIRE(contacts_size(a.members) == 0); - // for (const auto& m : members) - // REQUIRE(contacts_size(m.members) == 0); + admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); + + for (int i = 0; i < 4; ++i) + members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + + REQUIRE(admins[0].session_id == + "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); + REQUIRE(admins[1].session_id == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + REQUIRE(members[0].session_id == + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); + REQUIRE(members[1].session_id == + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); + REQUIRE(members[2].session_id == + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); + REQUIRE(members[3].session_id == + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); + + for (const auto& a : admins) + REQUIRE(groups_members_size(a.members) == 0); + for (const auto& m : members) + REQUIRE(groups_members_size(m.members) == 0); + + // add admin account, re-key, distribute + auto& admin1 = admins[0]; + config_group_member new_admin1; + + REQUIRE(groups_members_get_or_construct( + admin1.members, &new_admin1, admin1.session_id.c_str())); + + new_admin1.admin = true; + groups_members_set(admin1.members, &new_admin1); + + CHECK(config_needs_push(admin1.members)); + + const unsigned char* new_keys_config_1; + size_t key_len1; + REQUIRE(groups_keys_pending_config(admin1.keys, &new_keys_config_1, &key_len1)); + + config_push_data* new_info_config1 = config_push(admin1.info); + CHECK(new_info_config1->seqno == 1); + + 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; + + /* 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, new_keys_config_1, key_len1, get_timestamp(), a.info, a.members)); + REQUIRE(config_merge(a.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); + config_confirm_pushed(a.info, new_info_config1->seqno, "fakehash1"); + + REQUIRE(config_merge(a.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); + config_confirm_pushed(a.members, new_mem_config1->seqno, "fakehash1"); + + REQUIRE(groups_members_size(a.members) == 1); + } + + /* All attempts to merge non-admin members will throw, as none of the non admin members + will be able to decrypt the new info/member configs using the updated keys + */ + for (auto& m : members) { + // this will return true if the message was parsed successfully, NOT if the keys were + // decrypted + REQUIRE(groups_keys_load_message( + m.keys, new_keys_config_1, key_len1, get_timestamp(), m.info, m.members)); + REQUIRE_THROWS(config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); + REQUIRE_THROWS(config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); + + REQUIRE(groups_members_size(m.members) == 0); + } + + free(new_info_config1); + free(new_mem_config1); + + for (int i = 0; i < members.size(); ++i) { + config_group_member new_mem; + + REQUIRE(groups_members_get_or_construct( + members[i].members, &new_mem, members[i].session_id.c_str())); + new_mem.admin = false; + groups_members_set(admin1.members, &new_mem); + } + + CHECK(config_needs_push(admin1.members)); + + const unsigned char* new_keys_config_2; + size_t key_len2; + REQUIRE(groups_keys_rekey( + admin1.keys, admin1.info, admin1.members, &new_keys_config_2, &key_len2)); + + config_push_data* new_info_config2 = config_push(admin1.info); + CHECK(new_info_config2->seqno == 2); + + config_push_data* new_mem_config2 = config_push(admin1.members); + CHECK(new_mem_config2->seqno == 2); + + const char* merge_hash2[1]; + const unsigned char* merge_data2[2]; + size_t merge_size2[2]; + + merge_hash2[0] = "fakehash2"; + + 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; + + for (auto& a : admins) { + REQUIRE(groups_keys_load_message( + a.keys, new_keys_config_2, key_len2, get_timestamp(), a.info, a.members)); + REQUIRE(config_merge(a.info, merge_hash2, &merge_data2[0], &merge_size2[0], 1)); + config_confirm_pushed(a.info, new_info_config2->seqno, "fakehash2"); + + REQUIRE(config_merge(a.members, merge_hash2, &merge_data2[1], &merge_size2[1], 1)); + config_confirm_pushed(a.members, new_mem_config2->seqno, "fakehash2"); + + REQUIRE(groups_members_size(a.members) == 5); + } + + free(new_info_config2); + free(new_mem_config2); } TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { From 3ed91d53a56333a7cf8a991e50b3c623a4182fdd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 18:25:38 -0300 Subject: [PATCH 047/572] Fix propagation of secret key values Also adds tests to verify that auth_data and secretkey data propagate as expected. Many thanks to Harris for identifying and tracking this down. --- src/config/user_groups.cpp | 15 +++++++++++---- tests/test_config_user_groups.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index c84a4893..b0157136 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -217,9 +217,10 @@ void group_info::load(const dict& info_dict) { base_group_info::load(info_dict); if (auto seed = maybe_ustring(info_dict, "K"); seed && seed->size() == 32) { - std::array pk; + std::array pk; + pk[0] = 0x03; secretkey.resize(64); - crypto_sign_seed_keypair(pk.data(), secretkey.data(), seed->data()); + crypto_sign_seed_keypair(pk.data() + 1, secretkey.data(), seed->data()); if (id != oxenc::to_hex(pk.begin(), pk.end())) secretkey.clear(); } @@ -388,10 +389,16 @@ void UserGroups::set(const legacy_group_info& g) { } void UserGroups::set(const group_info& g) { - auto info = data["g"][session_id_to_bytes(g.id, "03")]; + auto pk_bytes = session_id_to_bytes(g.id, "03"); + auto info = data["g"][pk_bytes]; set_base(g, info); - if (g.secretkey.size() == 64) + if (g.secretkey.size() == 64 && + // Make sure the secretkey's embedded pubkey matches the group id: + ustring_view{g.secretkey.data() + 32, 32} == + ustring_view{ + reinterpret_cast(pk_bytes.data() + 1), + pk_bytes.size() - 1}) info["K"] = ustring_view{g.secretkey.data(), 32}; else { info["K"] = ustring_view{}; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 054fa334..b04e329e 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -409,7 +409,7 @@ TEST_CASE("User Groups", "[config][groups]") { } } -TEST_CASE("User Groups (new)", "[config][groups][new]") { +TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk, curve_pk; @@ -442,12 +442,20 @@ TEST_CASE("User Groups (new)", "[config][groups][new]") { auto c = groups.get_or_construct_group(definitely_real_id); + CHECK(c.secretkey.empty()); CHECK(c.id == definitely_real_id); CHECK(c.priority == 0); CHECK(c.joined_at == 0); CHECK(c.notifications == session::config::notify_mode::defaulted); CHECK(c.mute_until == 0); + c.secretkey = to_usv(ed_sk); // This *isn't* the right secret key for the group, so won't + // propagate, and so auth data will: + c.auth_data = + "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000"_hexbytes; + groups.set(c); CHECK(groups.needs_push()); @@ -476,6 +484,14 @@ TEST_CASE("User Groups (new)", "[config][groups][new]") { g2.set(*c2); + auto c2b = g2.get_or_construct_group("03" + oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + c2b.secretkey = to_usv(ed_sk); // This one does match the group ID, so should propagate + c2b.auth_data = // should get ignored, since we have a valid secret key set: + "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000"_hexbytes; + g2.set(c2b); + std::tie(seqno, to_push, obs) = g2.push(); g2.confirm_pushed(seqno, "fakehash2"); @@ -485,6 +501,11 @@ TEST_CASE("User Groups (new)", "[config][groups][new]") { auto c3 = groups.get_group(definitely_real_id); REQUIRE(c3.has_value()); + CHECK(c3->secretkey.empty()); + CHECK(to_hex(c3->auth_data) == + "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000"); CHECK(c3->id == definitely_real_id); CHECK(c3->priority == 123); CHECK(c3->joined_at == 1234567890); @@ -493,6 +514,11 @@ TEST_CASE("User Groups (new)", "[config][groups][new]") { groups.erase(*c3); + auto c3b = groups.get_group("03" + oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + REQUIRE(c3b); + CHECK(c3b->auth_data.empty()); + CHECK(to_hex(c3b->secretkey) == to_hex(seed) + oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + auto gg = groups.get_or_construct_group( "030303030303030303030303030303030303030303030303030303030303030303"); groups.set(gg); From 5854c4f168f45599d49ba65457cad74890db6b87 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 31 Aug 2023 20:59:07 -0300 Subject: [PATCH 048/572] Fix name typo in groups_keys_size --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index ba064792..cd6b365b 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1251,7 +1251,7 @@ LIBSESSION_C_API int groups_keys_init( return SESSION_ERR_NONE; } -LIBSESSION_C_API size_t group_keys_size(const config_group_keys* conf) { +LIBSESSION_C_API size_t groups_keys_size(const config_group_keys* conf) { return unbox(conf).size(); } From 194f972d161a57dae07430f92eac44e95c208c84 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 1 Sep 2023 00:16:41 -0300 Subject: [PATCH 049/572] Fix user_groups iterators with new closed groups --- src/config/user_groups.cpp | 8 +++++--- tests/test_config_user_groups.cpp | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index b0157136..e8e6cbe8 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -550,15 +550,17 @@ void UserGroups::iterator::_load_val() { } bool UserGroups::iterator::operator==(const iterator& other) const { - return _it_comm == other._it_comm && _it_legacy == other._it_legacy; + return _it_group == other._it_group && _it_comm == other._it_comm && _it_legacy == other._it_legacy; } bool UserGroups::iterator::done() const { - return !_it_comm && !_it_legacy; + return !_it_group && !_it_comm && !_it_legacy; } UserGroups::iterator& UserGroups::iterator::operator++() { - if (_it_comm) + if (_it_group) + ++*_it_group; + else if (_it_comm) _it_comm->advance(); else { assert(_it_legacy); diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index b04e329e..5e759e01 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -198,6 +198,10 @@ TEST_CASE("User Groups", "[config][groups]") { // The new data doesn't get stored until we call this: groups.set(og); + auto fake_group_id = "030101010101010101010101010101010101010101010101010101010101010101"s; + auto ggg = groups.get_or_construct_group(fake_group_id); + groups.set(ggg); + auto [seqno, to_push, obs] = groups.push(); auto to_push1 = to_push; @@ -226,9 +230,10 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(obs.empty()); CHECK(g2.current_hashes() == std::vector{{"fakehash1"s}}); - CHECK(g2.size() == 2); + CHECK(g2.size() == 3); CHECK(g2.size_communities() == 1); CHECK(g2.size_legacy_groups() == 1); + CHECK(g2.size_groups() == 1); auto x1 = g2.get_legacy_group(definitely_real_id); REQUIRE(x1); @@ -260,12 +265,15 @@ TEST_CASE("User Groups", "[config][groups]") { std::to_string(members) + " members"); } else if (auto* og = std::get_if(&group)) { seen.push_back("community: " + og->base_url() + "/r/" + og->room()); + } else if (auto* g = std::get_if(&group)) { + seen.push_back("group: " + g->id); } else { seen.push_back("unknown"); } } CHECK(seen == std::vector{ + "group: " + fake_group_id, "community: http://example.org:5678/r/SudokuRoom", "legacy: Englishmen, 1 admins, 2 members", }); @@ -311,9 +319,10 @@ TEST_CASE("User Groups", "[config][groups]") { REQUIRE(x3.has_value()); CHECK(x3->room() == "sudokuRoom"); // We picked up the capitalization change - CHECK(groups.size() == 2); + CHECK(groups.size() == 3); CHECK(groups.size_communities() == 1); CHECK(groups.size_legacy_groups() == 1); + CHECK(groups.size_groups() == 1); CHECK(c1.insert(users[4], false)); CHECK(c1.insert(users[5], true)); @@ -348,9 +357,10 @@ TEST_CASE("User Groups", "[config][groups]") { to_merge.clear(); to_merge.emplace_back("fakehash3", to_push); groups.merge(to_merge); - CHECK(groups.size() == 1); + CHECK(groups.size() == 2); CHECK(groups.size_communities() == 0); CHECK(groups.size_legacy_groups() == 1); + CHECK(groups.size_groups() == 1); int prio = 0; auto beanstalk_pubkey = "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"; @@ -361,9 +371,10 @@ TEST_CASE("User Groups", "[config][groups]") { groups.set(g); } - CHECK(groups.size() == 5); + CHECK(groups.size() == 6); CHECK(groups.size_communities() == 4); CHECK(groups.size_legacy_groups() == 1); + CHECK(groups.size_groups() == 1); std::tie(seqno, to_push, obs) = groups.push(); groups.confirm_pushed(seqno, "fakehash4"); @@ -394,12 +405,15 @@ TEST_CASE("User Groups", "[config][groups]") { std::to_string(members) + " members"); } else if (auto* og = std::get_if(&group)) { seen.push_back("community: " + og->base_url() + "/r/" + og->room()); + } else if (auto* g = std::get_if(&group)) { + seen.push_back("group: " + g->id); } else { seen.push_back("unknown"); } } CHECK(seen == std::vector{ + "group: " + fake_group_id, "community: http://jacksbeanstalk.org/r/fee", "community: http://jacksbeanstalk.org/r/fi", "community: http://jacksbeanstalk.org/r/fo", From 364f8d32fc42396355ca43cdb61b0a4a4deea0e8 Mon Sep 17 00:00:00 2001 From: dr7ana Date: Fri, 1 Sep 2023 05:27:03 -0700 Subject: [PATCH 050/572] formatter --- src/config/user_groups.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index e8e6cbe8..957afc10 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -550,7 +550,8 @@ void UserGroups::iterator::_load_val() { } bool UserGroups::iterator::operator==(const iterator& other) const { - return _it_group == other._it_group && _it_comm == other._it_comm && _it_legacy == other._it_legacy; + return _it_group == other._it_group && _it_comm == other._it_comm && + _it_legacy == other._it_legacy; } bool UserGroups::iterator::done() const { From 8ed090e669577131405f2f136bd859bcfb6cba32 Mon Sep 17 00:00:00 2001 From: dr7ana Date: Fri, 1 Sep 2023 06:09:05 -0700 Subject: [PATCH 051/572] C method to return groups keys - Added config_groups_keys to config/base.h - Looped in test call into config user groups C api unit test --- include/session/config/base.h | 13 +++++++++++++ src/config/base.cpp | 23 +++++++++++++++++++++++ tests/test_config_user_groups.cpp | 4 ++++ 3 files changed, 40 insertions(+) diff --git a/include/session/config/base.h b/include/session/config/base.h index 27e108d5..ee4404a8 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -281,6 +281,19 @@ typedef struct config_string_list { /// - `config_string_list*` -- point to the list of hashes, pointer belongs to the caller LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf); +/// API: base/config_groups_keys +/// +/// Obtains the current group decryption keys. +/// +/// The returned pointer belongs to the caller and must be freed via `free()` when done with it. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config_object object +/// +/// Outputs: +/// - `config_string_list*` -- pointer to the list of keys, pointer belongs to the caller +LIBSESSION_EXPORT config_string_list* config_groups_keys(const config_object* conf); + /// 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). diff --git a/src/config/base.cpp b/src/config/base.cpp index c3d03edd..cdad946e 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -661,6 +661,29 @@ LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* return ret; } +LIBSESSION_EXPORT config_string_list* config_groups_keys(const config_object* conf) { + auto keys = unbox(conf)->get_keys(); + size_t sz = sizeof(config_string_list) + keys.size() * sizeof(char*); + for (auto& k : keys) + sz += k.size() + 1; + void* buf = std::malloc(sz); + auto* ret = static_cast(buf); + ret->len = keys.size(); + + static_assert(alignof(config_string_list) >= alignof(char*)); + ret->value = reinterpret_cast(ret + 1); + char** next_ptr = ret->value; + char* next_str = reinterpret_cast(next_ptr + ret->len); + + for (size_t i = 0; i < ret->len; i++) { + *(next_ptr++) = next_str; + std::memcpy(next_str, keys[i].data(), keys[i].size() + 1); + next_str += keys[i].size() + 1; + } + + return ret; +} + LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { unbox(conf)->add_key({key, 32}); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 5e759e01..e389df53 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -674,6 +674,10 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { CHECK(hashes->value[0] == "fakehash1"sv); free(hashes); + config_string_list* keys = config_groups_keys(conf); + REQUIRE(keys); + REQUIRE(keys->len == 1); + session::config::UserGroups c2{ustring_view{seed}, std::nullopt}; std::vector> to_merge; From bb7a2cfa1bbbfe94db7a69ffc4875cdf48330432 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 2 Sep 2023 12:00:54 -0300 Subject: [PATCH 052/572] Add current hash tracking Adds current hash tracking to groups::Keys; this adds a `current_hashes` method to group::Keys that works similarly to the base config current_hashes (though returns a set instead of vector). (The C version is `groups_keys_current_hashes`) Testing this also triggered a bug in that we weren't probably re-loading the verified-signature state on a dump->load cycle, which caused an assertion failure on merge (because the current state couldn't be successfully serialized-then-deserialized), also fixed here. Also renames the recently added C config_group_keys to config_get_keys to better reflect its purpose. --- external/oxen-encoding | 2 +- include/session/config.hpp | 20 ++- include/session/config/base.h | 16 +- include/session/config/base.hpp | 4 + include/session/config/groups/keys.h | 20 ++- include/session/config/groups/keys.hpp | 34 +++- src/config.cpp | 22 ++- src/config/base.cpp | 88 ++++------ src/config/groups/keys.cpp | 155 +++++++++++++----- src/config/internal.hpp | 46 ++++++ tests/test_config_user_groups.cpp | 5 +- tests/test_group_keys.cpp | 216 ++++++++++++++++++++----- tests/utils.hpp | 6 +- 13 files changed, 469 insertions(+), 165 deletions(-) diff --git a/external/oxen-encoding b/external/oxen-encoding index 462be41b..867d0797 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit 462be41bd481b331dabeb3c220b349ef35c89e56 +Subproject commit 867d0797a08361eee613b91060a2ef447d2f9f4d diff --git a/include/session/config.hpp b/include/session/config.hpp index 9eb7b8d5..170019c0 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -127,7 +127,8 @@ class ConfigMessage { ustring_view serialized, verify_callable verifier = nullptr, sign_callable signer = nullptr, - int lag = DEFAULT_DIFF_LAGS); + int lag = DEFAULT_DIFF_LAGS, + bool trust_signature = false); /// Constructs a new ConfigMessage by loading and potentially merging multiple serialized /// ConfigMessages together, according to the config conflict resolution rules. The result @@ -210,10 +211,13 @@ class ConfigMessage { /// data), this will return -1. int unmerged_index() const { return unmerged_; } - /// Returns true if this message contained a valid, verified signature when it was parsed. - /// Returns false otherwise (e.g. not loaded from verification at all; loaded without a - /// verification function; or had no signature and a signature wasn't required). - bool verified_signature() const { return verified_signature_.has_value(); } + /// Read-only access to the optional verified signature if this message contained a valid, + /// verified signature when it was parsed. Returns nullopt otherwise (e.g. not loaded from + /// verification at all; loaded without a verification function; or had no signature and a + /// signature wasn't required). + const std::optional>& verified_signature() { + return verified_signature_; + } /// Constructs a new MutableConfigMessage from this config message with an incremented seqno. /// The new config message's diff will reflect changes made after this construction. @@ -367,6 +371,9 @@ class MutableConfigMessage : public ConfigMessage { /// - `verified_signature` is a pointer to a std::optional array of signature data; if this is /// specified and not nullptr then the optional with be emplaced with the signature bytes if the /// signature successfully validates. +/// - `trust_signature` bypasses the verification and signature requirements, blinding trusting a +/// signature if present. This is intended for use when restoring from a dump (along with a +/// nullptr verifier). /// /// Outputs: /// - returns with no value on success @@ -375,7 +382,8 @@ void verify_config_sig( oxenc::bt_dict_consumer dict, ustring_view config_msg, const ConfigMessage::verify_callable& verifier, - std::optional>* verified_signature = nullptr); + std::optional>* verified_signature = nullptr, + bool trust_signature = false); } // namespace session::config diff --git a/include/session/config/base.h b/include/session/config/base.h index ee4404a8..a14ba528 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -281,18 +281,26 @@ typedef struct config_string_list { /// - `config_string_list*` -- point to the list of hashes, pointer belongs to the caller LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf); -/// API: base/config_groups_keys +/// API: base/config_get_keys /// /// Obtains the current group decryption keys. /// -/// The returned pointer belongs to the caller and must be freed via `free()` when done with it. +/// 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: -/// - `config_string_list*` -- pointer to the list of keys, pointer belongs to the caller -LIBSESSION_EXPORT config_string_list* config_groups_keys(const config_object* conf); +/// - `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). diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index b4f9511b..73516a4d 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1120,6 +1120,10 @@ class ConfigBase : public ConfigSig { /// 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: None /// /// Outputs: diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 6d7933cc..25592c26 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -179,23 +179,39 @@ LIBSESSION_EXPORT bool groups_keys_pending_config( /// /// 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 new keys, if needed -/// - `members` -- [in] the members config object to update with new keys, if needed +/// - `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 /// /// Checks whether a rekey is required (for instance, because of key generation conflict). Note diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index faddd57a..cab57089 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "../../config.hpp" #include "../base.hpp" @@ -47,6 +48,8 @@ using namespace std::literals; /// - g -- the key generation /// - k -- the key itself (32 bytes). /// - t -- the storage timestamp of the key (so that recipients know when keys expire) +/// G -- the maximum generation of the keys included in this message; this is used to track when +/// this message can be allowed to expire. /// /// And finally, for both types: /// @@ -91,6 +94,9 @@ class Keys final : public ConfigSig { /// have been superceded by another key for a sufficient amount of time (see KEY_EXPIRY). sodium_vector keys_; + /// Hashes of messages we have successfully parsed; used for deciding what needs to be renewed. + std::map> active_msgs_; + sodium_cleared> pending_key_; sodium_vector pending_key_config_; int64_t pending_gen_ = -1; @@ -109,9 +115,8 @@ class Keys final : public ConfigSig { // Loads existing state from a previous dump of keys data void load_dump(ustring_view dump); - // Inserts a key into the correct place in `keys_`. Returns true if the key was inserted, false - // if it already existed. - bool insert_key(const key_info& key); + // Inserts a key into the correct place in `keys_`. + void insert_key(std::string_view message_hash, key_info&& key); // Returned the blinding factor for a given session X25519 pubkey. This depends on the group's // seed and thus is only obtainable by an admin account. @@ -283,7 +288,8 @@ class Keys final : public ConfigSig { /// Outputs: /// - `ustring_view` containing the data that needs to be pushed to the config keys namespace /// for the group. (This can be re-obtained from `push()` if needed until it has been - /// confirmed or superceded). + /// confirmed or superceded). This data must be consumed or copied from the returned + /// string_view immediately: it will not be valid past other calls on the Keys config object. ustring_view rekey(Info& info, Members& members); /// API: groups/Keys::key_supplement @@ -498,6 +504,7 @@ class Keys final : public ConfigSig { /// usable). /// /// Inputs: + /// - `hash` - the message hash from the swarm /// - `data` - the full stored config message value /// - `timestamp_ms` - the timestamp (from the swarm) when this message was stored (used to /// track when other keys expire). @@ -512,7 +519,24 @@ class Keys final : public ConfigSig { /// is mainly informative and does not signal an error: false could mean, for instance, be a /// supplemental message that wasn't for us. Note also that true doesn't mean keys changed: /// it could mean we decrypted one for us, but already had it. - bool load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members); + bool load_key_message( + std::string_view hash, + ustring_view data, + int64_t timestamp_ms, + Info& info, + Members& members); + + /// API: groups/Keys::current_hashes + /// + /// Returns a set of message hashes of messages that contain currently active decryption keys. + /// These are the messages that should be periodically renewed by clients with write access to + /// keep them alive for other accounts (or devices) who might need them in the future. + /// + /// Inputs: none + /// + /// Outputs: + /// - vector of message hashes + std::unordered_set current_hashes() const; /// API: groups/Keys::needs_rekey /// diff --git a/src/config.cpp b/src/config.cpp index f60d554f..99957bad 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -433,7 +433,8 @@ void verify_config_sig( oxenc::bt_dict_consumer dict, ustring_view config_msg, const ConfigMessage::verify_callable& verifier, - std::optional>* verified_signature) { + std::optional>* verified_signature, + bool trust_signature) { ustring_view to_verify, sig; dict.skip_until("~"); if (!dict.is_finished() && dict.key() == "~") { @@ -456,12 +457,13 @@ void verify_config_sig( if (!dict.is_finished()) throw config_parse_error{"Invalid config: dict has invalid key(s) after \"~\""}; - if (verifier) { - if (sig.empty()) - throw missing_signature{"Config signature is missing"}; - else if (sig.size() != 64) + if (verifier || trust_signature) { + if (sig.empty()) { + if (!trust_signature) + throw missing_signature{"Config signature is missing"}; + } else if (sig.size() != 64) throw signature_error{"Config signature is invalid (not 64B)"}; - else if (!verifier(to_verify, sig)) + else if (verifier && !verifier(to_verify, sig)) throw signature_error{"Config signature failed verification"}; else if (verified_signature) { if (!*verified_signature) @@ -542,7 +544,11 @@ ConfigMessage::ConfigMessage() { } ConfigMessage::ConfigMessage( - ustring_view serialized, verify_callable verifier_, sign_callable signer_, int lag) : + ustring_view serialized, + verify_callable verifier_, + sign_callable signer_, + int lag, + bool trust_signature) : verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} { oxenc::bt_dict_consumer dict{from_unsigned_sv(serialized)}; @@ -569,7 +575,7 @@ ConfigMessage::ConfigMessage( load_unknowns(unknown_, dict, "=", "~"); - verify_config_sig(dict, serialized, verifier, &verified_signature_); + verify_config_sig(dict, serialized, verifier, &verified_signature_, trust_signature); } catch (const oxenc::bt_deserialize_invalid& err) { throw config_parse_error{"Failed to parse config file: "s + err.what()}; } diff --git a/src/config/base.cpp b/src/config/base.cpp index cdad946e..d22c3b14 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -1,5 +1,7 @@ #include "session/config/base.hpp" +#include +#include #include #include #include @@ -281,23 +283,20 @@ ustring ConfigBase::dump() { auto data_sv = from_unsigned_sv(data); oxenc::bt_list old_hashes; + oxenc::bt_dict_producer d; + d.append("!", static_cast(_state)); + d.append("$", data_sv); + d.append("(", _curr_hash); + if (is_readonly()) _old_hashes.clear(); + d.append_list(")").append(_old_hashes.begin(), _old_hashes.end()); - for (auto& old : _old_hashes) - old_hashes.emplace_back(old); - oxenc::bt_dict d{ - {"!", static_cast(_state)}, - {"$", data_sv}, - {"(", _curr_hash}, - {")", std::move(old_hashes)}, - }; if (auto extra = extra_data(); !extra.empty()) - d.emplace("+", std::move(extra)); + d.append_bt("+", std::move(extra)); _needs_dump = false; - auto dumped = oxenc::bt_serialize(d); - return ustring{to_unsigned_sv(dumped)}; + return ustring{to_unsigned_sv(d.view())}; } ConfigBase::ConfigBase( @@ -337,6 +336,7 @@ void ConfigBase::init_from_dump(std::string_view dump) { if (!d.skip_until("$")) throw std::runtime_error{"Unable to parse dumped config data: did not find '$' data key"}; + auto data = to_unsigned_sv(d.consume_string_view()); if (_state == ConfigState::Dirty) // If we dumped dirty data then we need to reload it as a mutable config message so that the // seqno gets incremented. This "wastes" one seqno value (since we didn't send the old @@ -344,13 +344,17 @@ void ConfigBase::init_from_dump(std::string_view dump) { // is a little more robust against failure if we actually sent it but got killed before we // could store a dump. _config = std::make_unique( - to_unsigned_sv(d.consume_string_view()), + data, nullptr, // We omit verifier and signer for now because we don't want this dump to nullptr, // be signed (since it's just a dump). config_lags()); else _config = std::make_unique( - to_unsigned_sv(d.consume_string_view()), nullptr, nullptr, config_lags()); + data, + nullptr, + nullptr, + config_lags(), + /*trust_signature=*/true); if (d.skip_until("(")) { _curr_hash = d.consume_string(); @@ -639,49 +643,25 @@ LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { } LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf) { - auto hashes = unbox(conf)->current_hashes(); - size_t sz = sizeof(config_string_list) + hashes.size() * sizeof(char*); - for (auto& h : hashes) - sz += h.size() + 1; - void* buf = std::malloc(sz); - auto* ret = static_cast(buf); - ret->len = hashes.size(); - - static_assert(alignof(config_string_list) >= alignof(char*)); - ret->value = reinterpret_cast(ret + 1); - char** next_ptr = ret->value; - char* next_str = reinterpret_cast(next_ptr + ret->len); - - for (size_t i = 0; i < ret->len; i++) { - *(next_ptr++) = next_str; - std::memcpy(next_str, hashes[i].c_str(), hashes[i].size() + 1); - next_str += hashes[i].size() + 1; + 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 ret; -} - -LIBSESSION_EXPORT config_string_list* config_groups_keys(const config_object* conf) { - auto keys = unbox(conf)->get_keys(); - size_t sz = sizeof(config_string_list) + keys.size() * sizeof(char*); - for (auto& k : keys) - sz += k.size() + 1; - void* buf = std::malloc(sz); - auto* ret = static_cast(buf); - ret->len = keys.size(); - - static_assert(alignof(config_string_list) >= alignof(char*)); - ret->value = reinterpret_cast(ret + 1); - char** next_ptr = ret->value; - char* next_str = reinterpret_cast(next_ptr + ret->len); - - for (size_t i = 0; i < ret->len; i++) { - *(next_ptr++) = next_str; - std::memcpy(next_str, keys[i].data(), keys[i].size() + 1); - next_str += keys[i].size() + 1; - } - - return ret; + return buf; } LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index cd6b365b..66776473 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "../internal.hpp" #include "session/config/groups/info.hpp" @@ -64,6 +65,16 @@ bool Keys::needs_dump() const { ustring Keys::dump() { oxenc::bt_dict_producer d; + { + auto active = d.append_list("active"); + for (const auto& [gen, hashes] : active_msgs_) { + auto lst = active.append_list(); + lst.append(gen); + for (const auto& h : hashes) + lst.append(h); + } + } + { auto keys = d.append_list("keys"); for (auto& k : keys_) { @@ -94,6 +105,18 @@ ustring Keys::dump() { void Keys::load_dump(ustring_view dump) { oxenc::bt_dict_consumer d{from_unsigned_sv(dump)}; + if (d.skip_until("active")) { + auto active = d.consume_list_consumer(); + while (!active.is_finished()) { + auto lst = active.consume_list_consumer(); + auto& hashes = active_msgs_[lst.consume_integer()]; + while (!lst.is_finished()) + hashes.insert(lst.consume_string()); + } + } else { + throw config_value_error{"Invalid Keys dump: `active` not found"}; + } + if (d.skip_until("keys")) { auto keys = d.consume_list_consumer(); while (!keys.is_finished()) { @@ -488,6 +511,8 @@ ustring Keys::key_supplement(const std::vector& sids) const { "Unable to construct supplemental messages: invalid session ids given"}; } + d.append("G", keys_.back().generation); + // Finally we sign the message at put it as the ~ key (which is 0x7f, and thus comes later than // any other ascii key). auto to_sign = to_unsigned_sv(d.view()); @@ -807,7 +832,7 @@ std::optional Keys::pending_config() const { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } -bool Keys::insert_key(const key_info& new_key) { +void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { // Find all keys with the same generation and see if our key is in there (that is: we are // deliberately ignoring timestamp so that we don't add the same key with slight timestamp // variations). @@ -816,8 +841,10 @@ bool Keys::insert_key(const key_info& new_key) { return a.generation < b.generation; }); for (auto it = gen_begin; it != gen_end; ++it) - if (it->key == new_key.key) - return false; + if (it->key == new_key.key) { + active_msgs_[new_key.generation].emplace(msg_hash); + return; + } auto it = std::lower_bound(keys_.begin(), keys_.end(), new_key); @@ -825,16 +852,20 @@ bool Keys::insert_key(const key_info& new_key) { keys_.front().timestamp + KEY_EXPIRY < keys_.back().timestamp) // The new one is older than the front one, and the front one is already more than // KEY_EXPIRY before the last one, so this new one is stale. - return false; + return; - keys_.insert(it, new_key); + active_msgs_[new_key.generation].emplace(msg_hash); + keys_.insert(it, std::move(new_key)); remove_expired(); needs_dump_ = true; - - return true; } -bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, Members& members) { +bool Keys::load_key_message( + std::string_view hash, + ustring_view data, + int64_t timestamp_ms, + Info& info, + Members& members) { oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; @@ -848,6 +879,8 @@ bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, auto nonce = to_unsigned_sv(d.consume_string_view()); sodium_vector new_keys; + std::optional max_gen; // If set then associate the message with this generation + // value, even if we didn't find a key for us. sodium_cleared> member_dec_key; if (!admin()) { @@ -938,8 +971,13 @@ bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, } } } - } else { - // Full message (i.e. not supplemental) + + if (!d.skip_until("G")) + throw config_value_error{ + "Supplemental key message missing required max generation field (G)"}; + max_gen = d.consume_integer(); + + } else { // Full message (i.e. not supplemental) bool found_key = false; auto& new_key = new_keys.emplace_back(); @@ -1013,8 +1051,10 @@ bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, if (member_key_count % MESSAGE_KEY_MULTIPLE != 0) throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; - if (!found_key) + if (!found_key) { + max_gen = new_key.generation; new_keys.pop_back(); + } } verify_config_sig(d, data, verifier_); @@ -1029,51 +1069,74 @@ bool Keys::load_key_message(ustring_view data, int64_t timestamp_ms, Info& info, if (!new_keys.empty()) { for (auto& k : new_keys) - insert_key(k); + insert_key(hash, std::move(k)); auto new_key_list = group_keys(); members.replace_keys(new_key_list, /*dirty=*/false); info.replace_keys(new_key_list, /*dirty=*/false); return true; + } else if (max_gen) { + active_msgs_[*max_gen].emplace(hash); + remove_expired(); + needs_dump_ = true; } + return false; } -void Keys::remove_expired() { - if (keys_.size() < 2) - return; - - auto lapsed_end = keys_.begin(); +std::unordered_set Keys::current_hashes() const { + std::unordered_set hashes; + for (const auto& [g, hash] : active_msgs_) + hashes.insert(hash.begin(), hash.end()); + return hashes; +} - for (auto it = keys_.begin(); it != keys_.end();) { - // Advance `it` if the next element is an alternate key (with a later timestamp) from the - // same generation. When we finish this loop, `it` is the last element of this generation - // and `it2` is the first element of the next generation. - auto it2 = std::next(it); - while (it2 != keys_.end() && it2->generation == it->generation) - it = it2++; - if (it2 == keys_.end()) - break; +void Keys::remove_expired() { + if (keys_.size() >= 2) { + // When we're done, this will point at the first element we want to keep (i.e. we want to + // remove everything in `[ begin(), lapsed_end )`). + auto lapsed_end = keys_.begin(); + + for (auto it = keys_.begin(); it != keys_.end();) { + // Advance `it` if the next element is an alternate key (with a later timestamp) from + // the same generation. When we finish this little loop, `it` is the last element of + // this generation and `it2` is the first element of the next generation. + auto it2 = std::next(it); + while (it2 != keys_.end() && it2->generation == it->generation) + it = it2++; + if (it2 == keys_.end()) + break; + + // it2 points at the lowest-timestamp value of the next-largest generation: if there is + // something more than 30 days newer than it2, then that tells us that `it`'s generation + // is no longer needed since a newer generation passed it more than 30 days ago. (We + // actually use 60 days for paranoid safety, but the logic is the same). + // + // NB: We don't trust the local system clock here (and the `timestamp` values are + // swarm-provided), because devices are notoriously imprecise, which means that since we + // only invalidate keys when new keys come in, we can hold onto one obsolete generation + // indefinitely (but this is a tiny overhead and not worth trying to build a + // system-clock-is-broken workaround to avoid). + if (it2->timestamp + KEY_EXPIRY < keys_.back().timestamp) + lapsed_end = it2; + else + break; + it = it2; + } - // it2 points at the lowest-timestamp value of the next-largest generation: if there is - // something more than 30 days newer than it2, then that tells us that `it`'s generation is - // no longer needed since a newer generation passed it more than 30 days ago. (We actually - // use 60 days for paranoid safety, but the logic is the same). - // - // NB: We don't trust the local system clock here (and the `timestamp` values are - // swarm-provided), because devices are notoriously imprecise, which means that since we - // only invalidate keys when new keys come in, we can hold onto one obsolete generation - // indefinitely (but this is a tiny overhead and not worth trying to build a - // system-clock-is-broken workaround to avoid). - if (it2->timestamp + KEY_EXPIRY < keys_.back().timestamp) - lapsed_end = it2; - else - break; - it = it2; + if (lapsed_end != keys_.begin()) + keys_.erase(keys_.begin(), lapsed_end); } - if (lapsed_end != keys_.begin()) - keys_.erase(keys_.begin(), lapsed_end); + // Drop any active message hashes for generations we are no longer keeping around + if (!keys_.empty()) + active_msgs_.erase( + active_msgs_.begin(), active_msgs_.lower_bound(keys_.front().generation)); + else + // Keys is empty, which means we aren't keep *any* keys around (or they are all invalid or + // something) and so it isn't really up to us to keep them alive, since that's a history of + // the group we apparently don't have access to. + active_msgs_.clear(); } bool Keys::needs_rekey() const { @@ -1299,6 +1362,7 @@ LIBSESSION_C_API bool groups_keys_pending_config( 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, @@ -1307,6 +1371,7 @@ LIBSESSION_C_API bool groups_keys_load_message( assert(data && info && members); try { unbox(conf).load_key_message( + msg_hash, ustring_view{data, datalen}, timestamp_ms, *unbox(info), @@ -1318,6 +1383,10 @@ LIBSESSION_C_API bool groups_keys_load_message( 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(); } diff --git a/src/config/internal.hpp b/src/config/internal.hpp index e78ba640..8872ea4c 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -5,7 +5,9 @@ #include #include #include +#include +#include "session/config/base.h" #include "session/config/base.hpp" #include "session/config/error.h" #include "session/types.hpp" @@ -80,6 +82,50 @@ 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 +// 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 + // 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} + // {pointer1}{pointer2}... + // {string data 1\0}{string data 2\0}... + // + // where config_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*); + // 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)); + ret->len = vals.size(); + + static_assert(alignof(config_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. + ret->value = reinterpret_cast(ret + 1); + char** next_ptr = ret->value; + char* next_str = reinterpret_cast(next_ptr + ret->len); + + for (const auto& v : vals) { + *(next_ptr++) = next_str; + std::memcpy(next_str, v.c_str(), v.size() + 1); + next_str += v.size() + 1; + } + + return ret; +} + // Throws std::invalid_argument if session_id doesn't look valid. Can optionally be passed a prefix // byte for id's that aren't starting with 0x05 (e.g. 0x03 for non-legacy group ids). void check_session_id(std::string_view session_id, std::string_view prefix = "05"); diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index e389df53..050f91da 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -674,9 +674,10 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { CHECK(hashes->value[0] == "fakehash1"sv); free(hashes); - config_string_list* keys = config_groups_keys(conf); + size_t key_len; + unsigned char* keys = config_get_keys(conf, &key_len); REQUIRE(keys); - REQUIRE(keys->len == 1); + REQUIRE(key_len == 1); session::config::UserGroups c2{ustring_view{seed}, std::nullopt}; diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index d5a867c9..48e30240 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -68,18 +67,21 @@ struct pseudo_client { ustring_view seed, bool admin, const unsigned char* gpk, - std::optional gsk) : + std::optional gsk, + std::optional info_dump = std::nullopt, + std::optional members_dump = std::nullopt, + std::optional keys_dump = std::nullopt) : secret_key{sk_from_seed(seed)}, info{ustring_view{gpk, 32}, admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt}, + info_dump}, members{ustring_view{gpk, 32}, admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt}, + members_dump}, keys{to_usv(secret_key), ustring_view{gpk, 32}, admin ? std::make_optional({*gsk, 64}) : std::nullopt, - std::nullopt, + keys_dump, info, members} {} }; @@ -169,20 +171,24 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { message, which they can decrypt with the group secret key. */ for (auto& a : admins) { - a.keys.load_key_message(new_keys_config1, get_timestamp(), a.info, a.members); + a.keys.load_key_message( + "keyhash1", new_keys_config1, get_timestamp_ms(), a.info, a.members); CHECK(a.info.merge(info_configs) == 1); CHECK(a.members.merge(mem_configs) == 1); CHECK(a.members.size() == 1); + CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s}}); } /* All attempts to merge non-admin members will throw, as none of the non admin members will be able to decrypt the new info/member configs using the updated keys */ for (auto& m : members) { - m.keys.load_key_message(new_keys_config1, get_timestamp(), m.info, m.members); + m.keys.load_key_message( + "keyhash1", new_keys_config1, get_timestamp_ms(), m.info, m.members); CHECK_THROWS(m.info.merge(info_configs)); CHECK_THROWS(m.members.merge(mem_configs)); CHECK(m.members.size() == 0); + CHECK(m.keys.current_hashes().empty()); } info_configs.clear(); @@ -210,17 +216,21 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { mem_configs.emplace_back("fakehash2", new_mem_config2); for (auto& a : admins) { - a.keys.load_key_message(new_keys_config2, get_timestamp(), a.info, a.members); + a.keys.load_key_message( + "keyhash2", new_keys_config2, get_timestamp_ms(), a.info, a.members); CHECK(a.info.merge(info_configs) == 1); CHECK(a.members.merge(mem_configs) == 1); CHECK(a.members.size() == 5); + CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s}}); } for (auto& m : members) { - m.keys.load_key_message(new_keys_config2, get_timestamp(), m.info, m.members); + m.keys.load_key_message( + "keyhash2", new_keys_config2, get_timestamp_ms(), m.info, m.members); CHECK(m.info.merge(info_configs) == 1); CHECK(m.members.merge(mem_configs) == 1); CHECK(m.members.size() == 5); + CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s}}); } info_configs.clear(); @@ -243,17 +253,22 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { mem_configs.emplace_back("fakehash3", new_mem_config3); for (auto& a : admins) { - a.keys.load_key_message(new_keys_config3, get_timestamp(), a.info, a.members); + a.keys.load_key_message( + "keyhash3", new_keys_config3, get_timestamp_ms(), a.info, a.members); CHECK(a.info.merge(info_configs) == 1); CHECK(a.members.merge(mem_configs) == 1); CHECK(a.info.get_name() == "tomatosauce"s); + CHECK(a.keys.current_hashes() == + std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s}}); } for (auto& m : members) { - m.keys.load_key_message(new_keys_config3, get_timestamp(), m.info, m.members); + m.keys.load_key_message( + "keyhash3", new_keys_config3, get_timestamp_ms(), m.info, m.members); CHECK(m.info.merge(info_configs) == 1); CHECK(m.members.merge(mem_configs) == 1); CHECK(m.info.get_name() == "tomatosauce"s); + CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}}); } info_configs.clear(); @@ -282,17 +297,22 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { mem_configs.emplace_back("fakehash4", new_mem_config4); for (auto& a : admins) { - CHECK(a.keys.load_key_message(new_keys_config4, get_timestamp(), a.info, a.members)); + CHECK(a.keys.load_key_message( + "keyhash4", new_keys_config4, get_timestamp_ms(), a.info, a.members)); CHECK(a.info.merge(info_configs) == 1); CHECK(a.members.merge(mem_configs) == 1); CHECK(a.members.size() == 3); + CHECK(a.keys.current_hashes() == + std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s, "keyhash4"s}}); } for (int i = 0; i < members.size(); i++) { auto& m = members[i]; - bool found_key = - m.keys.load_key_message(new_keys_config2, get_timestamp(), m.info, m.members); + bool found_key = m.keys.load_key_message( + "keyhash4", new_keys_config2, get_timestamp_ms(), m.info, m.members); + CHECK(m.keys.current_hashes() == + std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s}}); if (i < 2) { // We should still be in the group CHECK(found_key); CHECK(m.info.merge(info_configs) == 1); @@ -330,6 +350,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { auto memb = admin1.members.get_or_construct(m.session_id); memb.set_invited(); + memb.name = i == 0 ? "fred" : "JOHN"; admin1.members.set(memb); CHECK_FALSE(m.keys.admin()); @@ -355,30 +376,131 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { admin1.members.confirm_pushed(mseq5, "fakehash5"); info_configs.emplace_back("fakehash4", new_info_config4); + for (auto& a : admins) { + CHECK_FALSE( + a.keys.load_key_message("keyhash5", supp, get_timestamp_ms(), a.info, a.members)); + } + for (size_t i = 0; i < members.size(); i++) { - DYNAMIC_SECTION("supp key load " << i) { - auto& m = members[i]; - bool found_key = m.keys.load_key_message(supp, get_timestamp(), m.info, m.members); - - if (i < 1) { - // This supp key wasn't for us - CHECK_FALSE(found_key); - CHECK(m.keys.size() == 3); - CHECK(m.keys.group_keys().size() == 3); - } else { - CHECK(found_key); - // new_keys_config1 never went to the initial members, but did go out in the - // supplement, which is why we have the extra key here. - CHECK(m.keys.size() == 4); - CHECK(m.keys.group_keys().size() == 4); - } + auto& m = members[i]; + bool found_key = + m.keys.load_key_message("keyhash5", supp, get_timestamp_ms(), m.info, m.members); - CHECK(m.info.merge(info_configs) == 1); - CHECK(m.members.merge(mem_configs) == 1); - REQUIRE(m.info.get_name()); - CHECK(*m.info.get_name() == "tomatosauce"sv); - CHECK(m.members.size() == 5); + if (i < 1) { + // This supp key wasn't for us + CHECK_FALSE(found_key); + CHECK(m.keys.size() == 3); + CHECK(m.keys.group_keys().size() == 3); + } else { + CHECK(found_key); + // new_keys_config1 never went to the initial members, but did go out in the + // supplement, which is why we have the extra key here. + CHECK(m.keys.size() == 4); + CHECK(m.keys.group_keys().size() == 4); } + + CHECK(m.info.merge(info_configs) == 1); + CHECK(m.members.merge(mem_configs) == 1); + REQUIRE(m.info.get_name()); + CHECK(*m.info.get_name() == "tomatosauce"sv); + CHECK(m.members.size() == 5); + + if (i < 2) + CHECK(m.keys.current_hashes() == + std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}}); + else + CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash5"s}}); + } + + // Duplicate members[1] from dumps + auto& m1b = members.emplace_back( + member_seeds[1], + false, + group_pk.data(), + std::nullopt, + members[1].info.dump(), + members[1].members.dump(), + members[1].keys.dump()); + CHECK(m1b.keys.size() == 4); + CHECK(m1b.keys.group_keys().size() == 4); + CHECK(m1b.keys.current_hashes() == + std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}}); + CHECK(m1b.members.size() == 5); + auto m1b_m2 = m1b.members.get(members[2].session_id); + REQUIRE(m1b_m2); + CHECK(m1b_m2->invite_pending()); + CHECK(m1b_m2->name == "fred"); + + // Rekey after 10d, then again after 71d (10+61) and everything except those two new gens should + // get dropped as stale. + info_configs.clear(); + mem_configs.clear(); + ustring new_keys_config6{admin1.keys.rekey(admin1.info, admin1.members)}; + auto [iseq6, ipush6, iobs6] = admin1.info.push(); + info_configs.emplace_back("ifakehash6", ipush6); + admin1.info.confirm_pushed(iseq6, "ifakehash6"); + auto [mseq6, mpush6, mobs6] = admin1.members.push(); + mem_configs.emplace_back("mfakehash6", mpush6); + admin1.members.confirm_pushed(mseq6, "mfakehash6"); + + for (auto& a : admins) { + CHECK(a.keys.load_key_message( + "keyhash6", + new_keys_config6, + get_timestamp_ms() + 10LL * 86400 * 1000, + a.info, + a.members)); + CHECK(a.info.merge(info_configs) == 1); + CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.members.size() == 5); + CHECK(a.keys.current_hashes() == std::unordered_set{ + {"keyhash1"s, + "keyhash2"s, + "keyhash3"s, + "keyhash4"s, + "keyhash5"s, + "keyhash6"s}}); + } + + ustring new_keys_config7{admin1.keys.rekey(admin1.info, admin1.members)}; + auto [iseq7, ipush7, iobs7] = admin1.info.push(); + info_configs.emplace_back("ifakehash7", ipush7); + admin1.info.confirm_pushed(iseq7, "ifakehash7"); + auto [mseq7, mpush7, mobs7] = admin1.members.push(); + mem_configs.emplace_back("mfakehash7", mpush7); + admin1.members.confirm_pushed(mseq7, "mfakehash7"); + + for (auto& a : admins) { + CHECK(a.keys.load_key_message( + "keyhash7", + new_keys_config7, + get_timestamp_ms() + 71LL * 86400 * 1000, + a.info, + a.members)); + CHECK(a.info.merge(info_configs) == 2); + CHECK(a.members.merge(mem_configs) == 2); + CHECK(a.members.size() == 5); + CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); + } + + for (int i = 0; i < members.size(); i++) { + auto& m = members[i]; + CHECK(m.keys.load_key_message( + "keyhash6", + new_keys_config6, + get_timestamp_ms() + 10LL * 86400 * 1000, + m.info, + m.members)); + CHECK(m.keys.load_key_message( + "keyhash7", + new_keys_config7, + get_timestamp_ms() + 71LL * 86400 * 1000, + m.info, + m.members)); + CHECK(m.info.merge(info_configs) == 2); + CHECK(m.members.merge(mem_configs) == 2); + CHECK(m.members.size() == 5); + CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } } @@ -514,7 +636,13 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { */ for (auto& a : admins) { REQUIRE(groups_keys_load_message( - a.keys, new_keys_config_1, key_len1, get_timestamp(), a.info, a.members)); + a.keys, + "fakekeyshash1", + new_keys_config_1, + key_len1, + get_timestamp_ms(), + a.info, + a.members)); REQUIRE(config_merge(a.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); config_confirm_pushed(a.info, new_info_config1->seqno, "fakehash1"); @@ -531,7 +659,13 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { // this will return true if the message was parsed successfully, NOT if the keys were // decrypted REQUIRE(groups_keys_load_message( - m.keys, new_keys_config_1, key_len1, get_timestamp(), m.info, m.members)); + m.keys, + "fakekeyshash1", + new_keys_config_1, + key_len1, + get_timestamp_ms(), + m.info, + m.members)); REQUIRE_THROWS(config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); REQUIRE_THROWS(config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); @@ -577,7 +711,13 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { for (auto& a : admins) { REQUIRE(groups_keys_load_message( - a.keys, new_keys_config_2, key_len2, get_timestamp(), a.info, a.members)); + a.keys, + "fakekeyshash2", + new_keys_config_2, + key_len2, + get_timestamp_ms(), + a.info, + a.members)); REQUIRE(config_merge(a.info, merge_hash2, &merge_data2[0], &merge_size2[0], 1)); config_confirm_pushed(a.info, new_info_config2->seqno, "fakehash2"); diff --git a/tests/utils.hpp b/tests/utils.hpp index 7d45d3d4..76b145c1 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -34,8 +34,10 @@ inline constexpr auto operator""_kiB(unsigned long long kiB) { return kiB * 1024; } -inline int64_t get_timestamp() { - return std::chrono::steady_clock::now().time_since_epoch().count(); +inline int64_t get_timestamp_ms() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); } inline std::string_view to_sv(ustring_view x) { From c272e06f53b08d7b6eb92d39a7ac83c44c62eb41 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 5 Sep 2023 14:23:09 -0300 Subject: [PATCH 053/572] Doc fix: `push()` should be `pending_config()` --- include/session/config/groups/keys.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index cab57089..4424bde4 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -287,8 +287,8 @@ class Keys final : public ConfigSig { /// /// Outputs: /// - `ustring_view` containing the data that needs to be pushed to the config keys namespace - /// for the group. (This can be re-obtained from `push()` if needed until it has been - /// confirmed or superceded). This data must be consumed or copied from the returned + /// for the group. (This can be re-obtained from `pending_config()` if needed until it has + /// been confirmed or superceded). This data must be consumed or copied from the returned /// string_view immediately: it will not be valid past other calls on the Keys config object. ustring_view rekey(Info& info, Members& members); From 2adb20c0a7edcf77883ab6e8a4872381192f241b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 6 Sep 2023 19:04:01 -0300 Subject: [PATCH 054/572] Add invited and name to groups; add kicked methods This adds an `.invited` flag for all group types (legacy, new, and communities) that can be used to track a invited-but-not-yet-joined room by session clients. Also adds `.name` to new groups data so that the name from an invitation can be stored before accepting (after accepting the name will come from the shared group config message). Add `setKicked()` and `kicked()` methods which clear both auth_data and secretkey and report whether both are empty, respectively. (For the C API these are `ugroups_group_set_kicked` and `ugroups_group_is_kicked` and take a pointer to the `ugroups_group_info`). --- include/session/config/user_groups.h | 30 +++++++++++++ include/session/config/user_groups.hpp | 61 ++++++++++++++++---------- src/config/user_groups.cpp | 55 +++++++++++++++++++---- tests/test_config_user_groups.cpp | 16 +++++++ 4 files changed, 131 insertions(+), 31 deletions(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 65529fe0..b17bf2f7 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -35,6 +35,8 @@ typedef struct ugroups_legacy_group_info { int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) + bool invited; // True if this is in the invite-but-not-accepted state. + // For members use the ugroups_legacy_group_members and associated calls. void* _internal; // Internal storage, do not touch. @@ -45,6 +47,9 @@ typedef struct ugroups_legacy_group_info { typedef struct ugroups_group_info { char id[67]; // in hex; 66 hex chars + null terminator + char name[101]; // Null-terminated C string (human-readable). Max length is 100 (plus 1 for + // null). Will always be set (even if an empty string). + bool have_secretkey; // Will be true if the `secretkey` is populated unsigned char secretkey[64]; // If `have_secretkey` is set then this is the libsodium-style // "secret key" for the group (i.e. 32 byte seed + 32 byte pubkey) @@ -59,6 +64,9 @@ typedef struct ugroups_group_info { CONVO_NOTIFY_MODE notifications; // When the user wants notifications int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) + + bool invited; // True if this is in the invite-but-not-accepted state. + } ugroups_group_info; typedef struct ugroups_community_info { @@ -75,6 +83,9 @@ typedef struct ugroups_community_info { CONVO_NOTIFY_MODE notifications; // When the user wants notifications int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) + + bool invited; // True if this is in the invite-but-not-accepted state. + } ugroups_community_info; /// API: user_groups/user_groups_init @@ -426,6 +437,25 @@ LIBSESSION_EXPORT bool user_groups_erase_group(config_object* conf, const char* /// - `bool` -- Returns True if conversation was found and removed LIBSESSION_EXPORT bool user_groups_erase_legacy_group(config_object* conf, const char* group_id); +/// API: user_groups/ugroups_group_set_kicked +/// +/// Call when we have been kicked from a group; this clears group's secret key and auth key from the +/// group config setting. +/// +/// Inputs: +/// - `group` -- [in] pointer to the group info which we should set to kicked +/// +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). +/// +/// Inputs: +/// - `group` -- [in] pointer to the group info to query +/// +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_members_begin diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 4a3d7d7c..5187af1a 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -21,15 +21,8 @@ namespace session::config { /// keys used in this config, either currently or in the past (so that we don't reuse): /// -/// g - dict of groups (AKA closed groups) for new-style closed groups (i.e. not legacy closed -/// groups; see below for those). Each key is the group's public key (without 0x03 prefix). +/// *Within* the group dicts (i.e. not at the top level), we use these common values: /// -/// K - group seed, if known (i.e. an admin). This is just the seed, which is just the first -/// half (32 bytes) of the 64-byte libsodium-style Ed25519 secret key value (i.e. it omits -/// the cached public key in the second half). This field is always set, but will be empty -/// if the seed is not known. -/// s - authentication signature; this is used by non-admins to authenticate. Omitted when K is -/// non-empty. /// @ - notification setting (int). Omitted = use default setting; 1 = all, 2 = disabled, 3 = /// mentions-only. /// ! - mute timestamp: if set then don't show notifications for this contact's messages until @@ -39,7 +32,25 @@ namespace session::config { /// Integer. Omitted means not pinned; -1 means hidden, and a positive value is a pinned /// message for which higher priority values means the conversation is meant to appear /// earlier in the pinned conversation list. +/// i - 1 if this is a pending invite (i.e. we have a request but haven't yet joined it), +/// deleted once joined. /// j - joined at unix timestamp. Omitted if 0. +/// n - the room/group/etc. friendly name. See details for each group type below. +/// +/// Top-level keys: +/// +/// g - dict of groups (AKA closed groups) for new-style closed groups (i.e. not legacy closed +/// groups; see below for those). Each key is the group's public key (without 0x03 prefix). +/// +/// K - group seed, if known (i.e. an admin). This is just the seed, which is just the first +/// half (32 bytes) of the 64-byte libsodium-style Ed25519 secret key value (i.e. it omits +/// the cached public key in the second half). This field is always set, but will be empty +/// if the seed is not known. +/// s - authentication signature; this is used by non-admins to authenticate. Omitted when K is +/// non-empty. +/// n - the room name, from a the group invitation; this is intended to be removed once the +/// invitation has been accepted, as the name contained in the group info supercedes this). +/// @, !, +, i, j -- see common values, above. /// /// o - dict of communities (AKA open groups); within this dict (which deliberately has the same /// layout as convo_info_volatile) each key is the SOGS base URL (in canonical form), and value @@ -51,45 +62,43 @@ namespace session::config { /// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in /// the outer key, with the capitalization variation in use ("SudokuSolvers") in this /// key. This key is *always* present (to keep the room dict non-empty). -/// @ - notification setting (same values as groups, above). -/// ! - mute timestamp (see above). -/// + - the conversation priority, for pinning/hiding this community room. See above. -/// j - joined at unix timestamp. Omitted if 0. +/// @, !, +, i, j - see common values, above. /// /// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and /// value is a dict containing keys: /// -/// n - name (string). Always set, even if empty. +/// n - name (string). Always set, even if empty (to make sure there is always something set to +/// keep the entry alive). /// k - encryption public key (32 bytes). Optional. /// K - encryption secret key (32 bytes). Optional. /// m - set of member session ids (each 33 bytes). /// a - set of admin session ids (each 33 bytes). /// E - disappearing messages duration, in seconds, > 0. Omitted if disappearing messages is /// disabled. (Note that legacy groups only support expire after-read) -/// @ - notification setting (int). Same as above. -/// ! - mute timestamp (see above). -/// + - the conversation priority, for pinned/hidden conversations. See above. -/// j - joined at unix timestamp. Omitted if 0. +/// @, !, +, i, j - see common values, above. /// Common base type with fields shared by all the groups struct base_group_info { + static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded + int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means // pinned higher (i.e. higher priority conversations come first). int64_t joined_at = 0; // unix timestamp (seconds) when the group was joined (or re-joined) notify_mode notifications = notify_mode::defaulted; // When the user wants notifications int64_t mute_until = 0; // unix timestamp (seconds) until which notifications are disabled + std::string name; // human-readable; always set for a legacy closed group, only used before + // joining a new closed group (after joining the group info provide the name) + + bool invited = false; // True if this is currently in the invite-but-not-accepted state. + protected: void load(const dict& info_dict); }; /// Struct containing legacy group info (aka "closed groups"). struct legacy_group_info : base_group_info { - static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded - - std::string session_id; // The legacy group "session id" (33 bytes). - std::string name; // human-readable; this should normally always be set, but in theory could be - // set to an empty string. + std::string session_id; // The legacy group "session id" (33 bytes). ustring enc_pubkey; // bytes (32 or empty) ustring enc_seckey; // bytes (32 or empty) std::chrono::seconds disappearing_timer{0}; // 0 == disabled. @@ -196,6 +205,14 @@ struct group_info : base_group_info { group_info(const struct ugroups_group_info& c); // From c struct void into(struct ugroups_group_info& c) const; // Into c struct + /// Shortcut for clearing both secretkey and auth_data, which indicates that we were kicked from + /// the group. + void setKicked(); + + /// Returns true if we don't have room access, i.e. we were kicked and both secretkey and + /// auth_data are empty. + bool kicked() const; + private: friend class UserGroups; diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 957afc10..4119d322 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -39,6 +39,7 @@ static void base_into(const base_group_info& self, T& c) { c.joined_at = self.joined_at; c.notifications = static_cast(self.notifications); c.mute_until = self.mute_until; + c.invited = self.invited; } template @@ -47,6 +48,7 @@ static void base_from(base_group_info& self, const T& c) { self.joined_at = c.joined_at; self.notifications = static_cast(c.notifications); self.mute_until = c.mute_until; + self.invited = c.invited; } group_info::group_info(std::string sid) : id{std::move(sid)} { @@ -71,10 +73,12 @@ void community_info::into(ugroups_community_info& c) const { std::memcpy(c.pubkey, pubkey().data(), 32); } -static_assert(sizeof(ugroups_legacy_group_info::name) == legacy_group_info::NAME_MAX_LENGTH + 1); +static_assert(sizeof(ugroups_legacy_group_info::name) == base_group_info::NAME_MAX_LENGTH + 1); +static_assert(sizeof(ugroups_group_info::name) == base_group_info::NAME_MAX_LENGTH + 1); legacy_group_info::legacy_group_info(const ugroups_legacy_group_info& c, impl_t) : - session_id{c.session_id, 66}, name{c.name}, disappearing_timer{c.disappearing_timer} { + session_id{c.session_id, 66}, disappearing_timer{c.disappearing_timer} { + name = c.name; assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up base_from(*this, c); if (c.have_enc_keys) { @@ -134,14 +138,17 @@ void base_group_info::load(const dict& info_dict) { notifications = notify_mode::defaulted; mute_until = maybe_int(info_dict, "!").value_or(0); + + invited = maybe_int(info_dict, "i").value_or(0); } void legacy_group_info::load(const dict& info_dict) { base_group_info::load(info_dict); if (auto n = maybe_string(info_dict, "n")) - name = *n; - // otherwise leave the current `name` alone at whatever the object was constructed with + name = std::move(*n); + else + name.clear(); auto enc_pub = maybe_ustring(info_dict, "k"); auto enc_sec = maybe_ustring(info_dict, "K"); @@ -197,6 +204,10 @@ bool legacy_group_info::erase(const std::string& session_id) { group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { base_from(*this, c); + + name = c.name; + assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up + if (c.have_secretkey) secretkey.assign(c.secretkey, 64); if (c.have_auth_data) @@ -207,6 +218,7 @@ void group_info::into(ugroups_group_info& c) const { assert(id.size() == 66); base_into(*this, c); copy_c_str(c.id, id); + copy_c_str(c.name, name); if ((c.have_secretkey = secretkey.size() == 64)) std::memcpy(c.secretkey, secretkey.data(), 64); if ((c.have_auth_data = auth_data.size() == 100)) @@ -216,6 +228,11 @@ void group_info::into(ugroups_group_info& c) const { void group_info::load(const dict& info_dict) { base_group_info::load(info_dict); + if (auto n = maybe_string(info_dict, "n")) + name = std::move(*n); + else + name.clear(); + if (auto seed = maybe_ustring(info_dict, "K"); seed && seed->size() == 32) { std::array pk; pk[0] = 0x03; @@ -228,11 +245,20 @@ void group_info::load(const dict& info_dict) { auth_data = std::move(*sig); } +void group_info::setKicked() { + secretkey.clear(); + auth_data.clear(); +} + +bool group_info::kicked() const { + return secretkey.empty() && auth_data.empty(); +} + void community_info::load(const dict& info_dict) { base_group_info::load(info_dict); if (auto n = maybe_string(info_dict, "n")) - set_room(*n); + set_room(std::move(*n)); } UserGroups::UserGroups(ustring_view ed25519_secretkey, std::optional dumped) : @@ -361,15 +387,14 @@ void UserGroups::set_base(const base_group_info& bg, DictFieldProxy& info) const set_positive_int(info["j"], bg.joined_at); set_positive_int(info["@"], static_cast(bg.notifications)); set_positive_int(info["!"], bg.mute_until); + set_flag(info["i"], bg.invited); + // We don't set n here because it's subtly different in the three group types } void UserGroups::set(const legacy_group_info& g) { auto info = data["C"][session_id_to_bytes(g.session_id)]; set_base(g, info); - if (g.name.size() > legacy_group_info::NAME_MAX_LENGTH) - info["n"] = g.name.substr(0, legacy_group_info::NAME_MAX_LENGTH); - else - info["n"] = g.name; + info["n"] = std::string_view{g.name}.substr(0, legacy_group_info::NAME_MAX_LENGTH); set_pair_if( g.enc_pubkey.size() == 32 && g.enc_seckey.size() == 32, @@ -393,6 +418,9 @@ void UserGroups::set(const group_info& g) { auto info = data["g"][pk_bytes]; set_base(g, info); + set_nonempty_str( + info["n"], std::string_view{g.name}.substr(0, legacy_group_info::NAME_MAX_LENGTH)); + if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: ustring_view{g.secretkey.data() + 32, 32} == @@ -726,6 +754,15 @@ LIBSESSION_C_API bool user_groups_erase_legacy_group(config_object* conf, const } } +LIBSESSION_C_API void ugroups_group_set_kicked(ugroups_group_info* group) { + assert(group); + group->have_auth_data = false; + group->have_secretkey = false; +} +LIBSESSION_C_API bool ugroups_group_is_kicked(const ugroups_group_info* group) { + return !(group->have_auth_data || group->have_secretkey); +} + struct ugroups_legacy_members_iterator { using map_t = std::map; map_t& members; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 050f91da..5e433baa 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -490,11 +490,15 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c2->joined_at == 0); CHECK(c2->notifications == session::config::notify_mode::defaulted); CHECK(c2->mute_until == 0); + CHECK_FALSE(c2->invited); + CHECK(c2->name == ""); c2->priority = 123; c2->joined_at = 1234567890; c2->notifications = session::config::notify_mode::mentions_only; c2->mute_until = 456789012; + c2->invited = true; + c2->name = "Magic Special Room"; g2.set(*c2); @@ -525,6 +529,8 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c3->joined_at == 1234567890); CHECK(c3->notifications == session::config::notify_mode::mentions_only); CHECK(c3->mute_until == 456789012); + CHECK(c3->invited); + CHECK(c3->name == "Magic Special Room"); groups.erase(*c3); @@ -532,6 +538,16 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { REQUIRE(c3b); CHECK(c3b->auth_data.empty()); CHECK(to_hex(c3b->secretkey) == to_hex(seed) + oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK_FALSE(c3b->kicked()); + c3b->auth_data.resize(100); + CHECK_FALSE(c3b->kicked()); + c3b->setKicked(); + CHECK(c3b->kicked()); + CHECK(c3b->secretkey.empty()); + CHECK(c3b->auth_data.empty()); + c3b->auth_data.resize(100); + CHECK_FALSE(c3b->kicked()); + c3b->auth_data.clear(); auto gg = groups.get_or_construct_group( "030303030303030303030303030303030303030303030303030303030303030303"); From f61bc4acd6d670452bf350e10d4e3fb0f4563909 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 6 Sep 2023 19:46:48 -0300 Subject: [PATCH 055/572] Fix info/members key lists when loading from dump The key lists weren't being set properly when loading a Keys object from a dump; these sets the key lists properly. --- src/config/groups/keys.cpp | 3 +++ tests/test_group_keys.cpp | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 66776473..1f99909a 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -54,6 +54,9 @@ Keys::Keys( if (dumped) { load_dump(*dumped); + auto key_list = group_keys(); + members.replace_keys(key_list, /*dirty=*/false); + info.replace_keys(key_list, /*dirty=*/false); } else if (admin()) { rekey(info, members); } diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 48e30240..111307f9 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -502,6 +502,22 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.members.size() == 5); CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } + + // Make sure keys propagate on dump restore to info/members: + pseudo_client admin1b{ + admin1_seed, + true, + group_pk.data(), + group_sk.data(), + admin1.info.dump(), + admin1.members.dump(), + admin1.keys.dump()}; + admin1b.info.set_name("Test New Name"); + CHECK_NOTHROW(admin1b.info.push()); + admin1b.members.set( + admin1b.members.get_or_construct("05124076571076017981235497801235098712093870981273590" + "8746387172343")); + CHECK_NOTHROW(admin1b.members.push()); } TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { From fc5938503895c487a53f2714058061a44bf379a8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 6 Sep 2023 21:01:24 -0300 Subject: [PATCH 056/572] Add missing group C wrappers for volatile convos --- src/config/convo_info_volatile.cpp | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 3ce3dae1..fcef4675 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -555,6 +555,35 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( } } +LIBSESSION_C_API bool convo_info_volatile_get_group( + config_object* conf, convo_info_volatile_group* convo, const char* id) { + try { + conf->last_error = nullptr; + if (auto c = unbox(conf)->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; + } + return false; +} + +LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( + config_object* conf, convo_info_volatile_group* convo, const char* id) { + try { + conf->last_error = nullptr; + unbox(conf)->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; + } +} + + LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { try { @@ -591,6 +620,10 @@ 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 convo_info_volatile_set_group( + config_object* conf, const convo_info_volatile_group* convo) { + unbox(conf)->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}); @@ -611,6 +644,14 @@ LIBSESSION_C_API bool convo_info_volatile_erase_community( return false; } } +LIBSESSION_C_API bool convo_info_volatile_erase_group( + config_object* conf, const char* group_id) { + try { + return unbox(conf)->erase_group(group_id); + } catch (...) { + return false; + } +} LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( config_object* conf, const char* group_id) { try { @@ -629,6 +670,9 @@ LIBSESSION_C_API size_t convo_info_volatile_size_1to1(const config_object* conf) LIBSESSION_C_API size_t convo_info_volatile_size_communities(const config_object* conf) { return unbox(conf)->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 convo_info_volatile_size_legacy_groups(const config_object* conf) { return unbox(conf)->size_legacy_groups(); } @@ -653,6 +697,13 @@ LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_ new ConvoInfoVolatile::iterator{unbox(conf)->begin_communities()}; return it; } +LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( + const config_object* conf) { + auto* it = new convo_info_volatile_iterator{}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(conf)->begin_groups()}; + return it; +} LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( const config_object* conf) { auto* it = new convo_info_volatile_iterator{}; @@ -697,6 +748,11 @@ LIBSESSION_C_API bool convo_info_volatile_it_is_community( return convo_info_volatile_it_is_impl(it, c); } +LIBSESSION_C_API bool convo_info_volatile_it_is_group( + convo_info_volatile_iterator* it, convo_info_volatile_group* c) { + return convo_info_volatile_it_is_impl(it, c); +} + LIBSESSION_C_API bool convo_info_volatile_it_is_legacy_group( convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c) { return convo_info_volatile_it_is_impl(it, c); From e21302b598b5dde44fd72566d8b89d4d41cbb9ce Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 6 Sep 2023 22:27:36 -0300 Subject: [PATCH 057/572] Formatting --- src/config/convo_info_volatile.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index fcef4675..742549c5 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -583,7 +583,6 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( } } - LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { try { @@ -644,8 +643,7 @@ LIBSESSION_C_API bool convo_info_volatile_erase_community( return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_group( - config_object* conf, const char* group_id) { +LIBSESSION_C_API bool convo_info_volatile_erase_group(config_object* conf, const char* group_id) { try { return unbox(conf)->erase_group(group_id); } catch (...) { From 794864dec064aed3885428960b39ecc485a294fa Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 7 Sep 2023 21:03:40 -0300 Subject: [PATCH 058/572] Fix plaintext size calculation ciphertext has already been reduced by 1 + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES at this point, but that wasn't being accounted for in the size calculations. (Thanks to @ftrget for finding this one!) --- src/config/groups/keys.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 1f99909a..dd4cffe9 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1212,7 +1212,9 @@ std::optional Keys::decrypt_message(ustring_view ciphertext) const { case 'x': { auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); ciphertext.remove_prefix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - plain.resize(ciphertext.size() - OVERHEAD); + plain.resize( + ciphertext.size() - + (OVERHEAD - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES)); for (auto& k : keys_) { if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( plain.data(), From 19868bf5dfde44884830acfe58971d03429f3e08 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 8 Sep 2023 12:18:44 -0300 Subject: [PATCH 059/572] Add decrypt_message round-trip encryption test We were encrypting, but not testing that we can actually decrypt that message (and thus not catching the problem from the previous commit). --- tests/test_group_keys.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 111307f9..a57666a3 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -412,6 +412,18 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash5"s}}); } + auto decrypted1 = members.back().keys.decrypt_message(compressed); + REQUIRE(decrypted1); + CHECK(to_sv(*decrypted1) == msg); + + auto decrypted2 = members.back().keys.decrypt_message(uncompressed); + REQUIRE(decrypted2); + CHECK(to_sv(*decrypted2) == msg); + + auto bad_compressed = compressed; + bad_compressed.back() ^= 0b100; + CHECK_FALSE(members.back().keys.decrypt_message(bad_compressed)); + // Duplicate members[1] from dumps auto& m1b = members.emplace_back( member_seeds[1], From 360f59683b71a89f56b4503a6453eb4230f9656d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 14 Sep 2023 01:26:00 -0300 Subject: [PATCH 060/572] Fix issue with decryption of a still-pending key If you call rekey() and then encrypt_message() before the rekey is confirmed, decryption would fail because the pending key wouldn't be tried yet. This fixes it (including a verified-to-have-failed test case). Thanks to Harris for the help tracking down/debugging this one. --- src/config/groups/keys.cpp | 99 +++++++++++++++++++++----------------- tests/test_group_keys.cpp | 4 ++ 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index dd4cffe9..2874822e 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -863,6 +863,46 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { needs_dump_ = true; } +// Attempts xchacha20 decryption. +// +// Preconditions: +// - `ciphertext` must be at least 16 [crypto_aead_xchacha20poly1305_ietf_ABYTES] +// - `out` must have enough space (ciphertext.size() - 16 +// [crypto_aead_xchacha20poly1305_ietf_ABYTES]) +// - `nonce` must be 24 bytes [crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] +// - `key` must be 32 bytes [crypto_aead_xchacha20poly1305_ietf_KEYBYTES] +// +// The latter two are asserted in a debug build, but not otherwise checked. +// +// Returns true (after writing to `out`) if decryption succeeds, false if it fails. +namespace { + bool try_decrypting( + unsigned char* out, ustring_view encrypted, ustring_view nonce, ustring_view key) { + assert(encrypted.size() >= crypto_aead_xchacha20poly1305_ietf_ABYTES); + assert(nonce.size() == crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + assert(key.size() == crypto_aead_xchacha20poly1305_ietf_KEYBYTES); + + return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + out, + nullptr, + nullptr, + encrypted.data(), + encrypted.size(), + nullptr, + 0, + nonce.data(), + key.data()); + } + bool try_decrypting( + unsigned char* out, + ustring_view encrypted, + ustring_view nonce, + + const std::array& key) { + return try_decrypting(out, encrypted, nonce, ustring_view{key.data(), key.size()}); + } +} // namespace + bool Keys::load_key_message( std::string_view hash, ustring_view data, @@ -938,16 +978,7 @@ bool Keys::load_key_message( ustring plaintext; plaintext.resize(encrypted.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - plaintext.data(), - nullptr, - nullptr, - encrypted.data(), - encrypted.size(), - nullptr, - 0, - nonce.data(), - member_dec_key.data())) { + if (try_decrypting(plaintext.data(), encrypted, nonce, member_dec_key)) { // Decryption success, we found our key list! oxenc::bt_list_consumer key_infos{from_unsigned_sv(plaintext)}; @@ -1004,16 +1035,7 @@ bool Keys::load_key_message( if (admin()) { auto k = seed_hash(enc_key_admin_hash_key); - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - new_key.key.data(), - nullptr, - nullptr, - admin_key.data(), - admin_key.size(), - nullptr, - 0, - nonce.data(), - k.data())) + if (!try_decrypting(new_key.key.data(), admin_key, nonce, k)) throw config_value_error{"Failed to decrypt admin key from key message"}; found_key = true; @@ -1036,16 +1058,7 @@ bool Keys::load_key_message( if (found_key) continue; - if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - new_key.key.data(), - nullptr, - nullptr, - member_key.data(), - member_key.size(), - nullptr, - 0, - nonce.data(), - member_dec_key.data())) { + if (try_decrypting(new_key.key.data(), member_key, nonce, member_dec_key)) { // Decryption success, we found our key! found_key = true; } @@ -1215,19 +1228,19 @@ std::optional Keys::decrypt_message(ustring_view ciphertext) const { plain.resize( ciphertext.size() - (OVERHEAD - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES)); - for (auto& k : keys_) { - if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - plain.data(), - nullptr, - nullptr, - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, - nonce.data(), - k.key.data())) { - success = true; - break; + + if (auto pending = pending_key(); + pending && try_decrypting(plain.data(), ciphertext, nonce, *pending)) { + + success = true; + + } else { + + for (auto& k : keys_) { + if (try_decrypting(plain.data(), ciphertext, nonce, k.key)) { + success = true; + break; + } } } break; diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index a57666a3..3188c699 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -475,6 +475,10 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { } ustring new_keys_config7{admin1.keys.rekey(admin1.info, admin1.members)}; + + // Make sure we can encrypt & decrypt even if the rekey is still pending: + CHECK(admin1.keys.decrypt_message(admin1.keys.encrypt_message(to_usv("abc")))); + auto [iseq7, ipush7, iobs7] = admin1.info.push(); info_configs.emplace_back("ifakehash7", ipush7); admin1.info.confirm_pushed(iseq7, "ifakehash7"); From 31984034e76b5a8b2800a4af5f102215be087ff6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 18 Sep 2023 13:38:20 +1000 Subject: [PATCH 061/572] feat: add debug_dump() to ConfigBase and Keys classes --- include/session/config/base.hpp | 13 ++++++++++++- include/session/config/groups/keys.hpp | 13 ++++++++++++- src/config/base.cpp | 8 +++++++- src/config/groups/keys.cpp | 8 +++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 73516a4d..9d9fa3d9 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1020,7 +1020,7 @@ class ConfigBase : public ConfigSig { /// Returns a dump of the current state for storage in the database; this value would get passed /// into the constructor to reconstitute the object (including the push/not pushed status). This /// method is *not* virtual: if subclasses need to store extra data they should set it in the - /// `subclass_data` field. + /// `subclass_data` field. Updates the internal needs_dump flag to false. /// /// Inputs: None /// @@ -1028,6 +1028,17 @@ class ConfigBase : public ConfigSig { /// - `ustring` -- Returns binary data of the state dump ustring dump(); + /// API: base/ConfigBase::debug_dump + /// + /// Returns a dump of the current state for debugging. Does *not* update the + /// internal needs_dump flag. + /// + /// Inputs: None + /// + /// Outputs: + /// - `ustring` -- Returns binary data of the state dump + ustring debug_dump(); + /// API: base/ConfigBase::needs_dump /// /// Returns true if something has changed since the last call to `dump()` that requires calling diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 4424bde4..d0f88a4a 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -580,7 +580,7 @@ class Keys final : public ConfigSig { /// API: groups/Keys::dump /// /// Returns a dump of the current state of this keys config that allows the Keys object to be - /// reinstantiated from scratch. + /// reinstantiated from scratch. Updates the internal needs_dump flag to false. /// /// Although this can be called at any time, it is recommended to only do so when /// `needs_dump()` returns true. @@ -592,6 +592,17 @@ class Keys final : public ConfigSig { /// to the `Keys` constructor to reinitialize a Keys object with the current state. ustring dump(); + /// API: groups/Keys::debug_dump + /// + /// Returns a dump of the current state for debugging. Does *not* update the + /// internal needs_dump flag. + + /// Inputs: None + /// + /// Outputs: + /// - `ustring` -- Returns binary data of the state dump + ustring debug_dump(); + /// API: groups/Keys::encrypt_message /// /// Encrypts group message content; this is passed a binary value to encrypt and diff --git a/src/config/base.cpp b/src/config/base.cpp index d22c3b14..1cad5458 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -279,6 +279,13 @@ void ConfigBase::confirm_pushed(seqno_t seqno, std::string msg_hash) { } ustring ConfigBase::dump() { + auto d = this->debug_dump(); + + _needs_dump = false; + return d; +} + +ustring ConfigBase::debug_dump() { auto data = _config->serialize(false /* disable signing for local storage */); auto data_sv = from_unsigned_sv(data); oxenc::bt_list old_hashes; @@ -295,7 +302,6 @@ ustring ConfigBase::dump() { if (auto extra = extra_data(); !extra.empty()) d.append_bt("+", std::move(extra)); - _needs_dump = false; return ustring{to_unsigned_sv(d.view())}; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 2874822e..2e866b1d 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -67,6 +67,13 @@ bool Keys::needs_dump() const { } ustring Keys::dump() { + auto dumped = this->debug_dump(); + + needs_dump_ = false; + return dumped; +} + +ustring Keys::debug_dump() { oxenc::bt_dict_producer d; { auto active = d.append_list("active"); @@ -101,7 +108,6 @@ ustring Keys::dump() { pending.append("k", from_unsigned_sv(pending_key_)); } - needs_dump_ = false; return ustring{to_unsigned_sv(d.view())}; } From 849d215f24cb38a08361aa7ad21723018bd5837b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 18 Sep 2023 12:48:34 -0300 Subject: [PATCH 062/572] Rename & tweak - rename `debug_dump` to `make_dump`, since it is also used internally (for non-debugging). - make it `const` to ensure we aren't mutating state. (And we were, with clearing old hashes: that is now done only by `dump()` but not `make_dump()`). - Update API documentation wording --- include/session/config/base.hpp | 11 ++++++----- include/session/config/groups/keys.hpp | 11 ++++++----- src/config/base.cpp | 8 ++++---- src/config/groups/keys.cpp | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 9d9fa3d9..9e0803a8 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1020,7 +1020,7 @@ class ConfigBase : public ConfigSig { /// Returns a dump of the current state for storage in the database; this value would get passed /// into the constructor to reconstitute the object (including the push/not pushed status). This /// method is *not* virtual: if subclasses need to store extra data they should set it in the - /// `subclass_data` field. Updates the internal needs_dump flag to false. + /// `subclass_data` field. Resets the `needs_dump()` flag to false. /// /// Inputs: None /// @@ -1028,16 +1028,17 @@ class ConfigBase : public ConfigSig { /// - `ustring` -- Returns binary data of the state dump ustring dump(); - /// API: base/ConfigBase::debug_dump + /// API: base/ConfigBase::make_dump /// - /// Returns a dump of the current state for debugging. Does *not* update the - /// internal needs_dump flag. + /// Returns a dump of the current state; unlike `dump()` this does *not* update the internal + /// needs_dump flag; it is mostly used internally (by `dump()`), but can also be called + /// externally for debugging purposes. /// /// Inputs: None /// /// Outputs: /// - `ustring` -- Returns binary data of the state dump - ustring debug_dump(); + ustring make_dump() const; /// API: base/ConfigBase::needs_dump /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index d0f88a4a..e518163b 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -592,16 +592,17 @@ class Keys final : public ConfigSig { /// to the `Keys` constructor to reinitialize a Keys object with the current state. ustring dump(); - /// API: groups/Keys::debug_dump + /// API: groups/Keys::make_dump + /// + /// Returns a dump of the current state; unlike `dump()` this does *not* update the internal + /// needs_dump flag; it is mostly used internally (by `dump()`), but can also be called + /// externally for debugging purposes. /// - /// Returns a dump of the current state for debugging. Does *not* update the - /// internal needs_dump flag. - /// Inputs: None /// /// Outputs: /// - `ustring` -- Returns binary data of the state dump - ustring debug_dump(); + ustring make_dump() const; /// API: groups/Keys::encrypt_message /// diff --git a/src/config/base.cpp b/src/config/base.cpp index 1cad5458..43d435b9 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -279,13 +279,15 @@ void ConfigBase::confirm_pushed(seqno_t seqno, std::string msg_hash) { } ustring ConfigBase::dump() { - auto d = this->debug_dump(); + if (is_readonly()) + _old_hashes.clear(); + auto d = make_dump(); _needs_dump = false; return d; } -ustring ConfigBase::debug_dump() { +ustring ConfigBase::make_dump() const { auto data = _config->serialize(false /* disable signing for local storage */); auto data_sv = from_unsigned_sv(data); oxenc::bt_list old_hashes; @@ -295,8 +297,6 @@ ustring ConfigBase::debug_dump() { d.append("$", data_sv); d.append("(", _curr_hash); - if (is_readonly()) - _old_hashes.clear(); d.append_list(")").append(_old_hashes.begin(), _old_hashes.end()); if (auto extra = extra_data(); !extra.empty()) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 2e866b1d..5d25b4ac 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -67,13 +67,13 @@ bool Keys::needs_dump() const { } ustring Keys::dump() { - auto dumped = this->debug_dump(); + auto dumped = make_dump(); needs_dump_ = false; return dumped; } -ustring Keys::debug_dump() { +ustring Keys::make_dump() const { oxenc::bt_dict_producer d; { auto active = d.append_list("active"); From 40400eeeeec8f9d9fafc0d3b9b840af18ce4ea3f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 21 Sep 2023 01:11:25 -0300 Subject: [PATCH 063/572] Add signing and verification to encrypt_message/decrypt_message This redoes how we encode the result of encrypt_message to add sender identify and verification (entirely contained within libsession). These functions now produce/consume: encrypted( padded( bt-encoded( possibly-compressed-data, sender-ed25519-pubkey, ed25519-signature ) ) ) with signature verification on decryption. The decryption function thus now returns a pair instead of an optional value: the session id, and the original value, assuming that value was signed by the session id. Upon failure for any reason, an exception is thrown (rather than, previously, returning a nullopt without any way to get an informative reason for the failure for diagnostics). --- include/session/config/groups/keys.h | 29 +++- include/session/config/groups/keys.hpp | 87 ++++++---- src/config/groups/keys.cpp | 224 +++++++++++++++++-------- tests/test_group_keys.cpp | 22 ++- 4 files changed, 247 insertions(+), 115 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 25592c26..bbe79761 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -499,10 +499,11 @@ LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( /// API: groups/groups_keys_encrypt_message /// -/// Encrypts a message using the most recent group encryption key of this object. The message will -/// be compressed (if that reduces the size) before being encrypted. Decryption (and decompression, -/// if compression was applied) is performed by passing such a message into -/// groups_keys_decrypt_message. +/// Encrypts a message using the most recent group encryption key of this object. +/// +/// The message will be compressed (if that reduces the size), padded, authored, and signed before +/// being encrypted. Decryption and verification (and decompression, if compression was applied) is +/// performed by passing such a message into groups_keys_decrypt_message. /// /// Note: this method can fail if there are no encryption keys at all, or if the incoming message /// decompresses to a huge value (more than 1MB). If it fails then `ciphertext_out` is set to NULL @@ -526,25 +527,37 @@ LIBSESSION_EXPORT void groups_keys_encrypt_message( /// API: groups/groups_keys_decrypt_message /// /// Attempts to decrypt a message using all of the known active encryption keys of this object. The -/// message will be decompressed after decryption, if required. +/// message will be de-padded, decompressed (if compressed), and have its signature verified after +/// decryption. +/// +/// Upon failure this returns false and sets `conf.last_error` to a string containing a diagnostic +/// reason the decryption failed (intended for logging, not for end-user display). /// /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `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` +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. /// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// decrypted/decompressed 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! +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// 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. /// /// Outputs: /// - `bool` -- True if the message was successfully decrypted, false if decryption (or parsing or -/// decompression) failed with all of our known keys. +/// 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( - const config_group_keys* conf, + config_group_keys* conf, const unsigned char* cipherext_in, size_t cipherext_len, + char* session_id_out, unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index e518163b..edf0e220 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -237,7 +237,8 @@ class Keys final : public ConfigSig { /// if there are no encryption keys at all. (This is essentially the same as `group_keys()[0]`, /// except for the throwing and avoiding needing to constructor a vector). /// - /// You normally don't need to call this; you can just use encrypt_message() instead. + /// You normally don't need to call this; the key is used automatically by methods such as + /// encrypt_message() that need it. /// /// Inputs: none. /// @@ -606,58 +607,88 @@ class Keys final : public ConfigSig { /// API: groups/Keys::encrypt_message /// - /// Encrypts group message content; this is passed a binary value to encrypt and - /// encodes/encrypts it for the group using the latest encryption key this object knows about. - /// Such encrypted messages are intended to be passed to `decrypt_message` to decrypt them. - /// - /// The current implementation uses XChaCha20-Poly1305 and returns an encoded value where the - /// first byte indicates the encryption type ('x', or 'X' currently for uncompressed or - /// compressed XChaCha20), the next 24 bytes are the encryption nonce, and the remainder is the - /// ciphertext. The returned value will be 41 bytes larger than the plaintext, at most - /// (potentially less if compression is permitted). + /// Compresses, signs, and encrypts group message content. + /// + /// This method is passed a binary value containing a group message (typically a serialized + /// protobuf, but this method doesn't care about the specific data). That data will be, in + /// order: + /// - compressed (but only if this actually reduces the data size) + /// - signed by the user's underlying session Ed25519 pubkey + /// - tagged with the user's underlying session Ed25519 pubkey (from which the session id can be + /// computed). + /// - all of the above encoded into a bt-encoded dict + /// - suffix-padded with null bytes so that the final output value will be a multiple of 256 + /// bytes + /// - encrypted with the most-current group encryption key + /// + /// Since compression and padding is applied as part of this method, it is not required that the + /// given message include its own padding (and in fact, such padding will typically be + /// compressed down to nothing (if non-random)). + /// + /// This final encrypted value is then returned to be pushed to the swarm as-is (i.e. not + /// further wrapped). For users downloading the message, all of the above is processed in + /// reverse by passing the returned message into `decrypt_message()`. + /// + /// The current implementation uses XChaCha20-Poly1305 for encryption and zstd for compression; + /// the bt-encoded value is a dict consisting of keys: + /// - "": the version of this encoding, currently set to 1. This *MUST* be bumped if this is + /// changed in such a way that older clients will not be able to properly decrypt such a + /// message. + /// - "a": the *Ed25519* pubkey (32 bytes) of the author of the message. (This will be + /// converted to a x25519 pubkey to extract the sender's session id when decrypting). + /// - "s": signature by "a" of whichever of "d" or "z" are included in the data. + /// Exacly one of: + /// - "d": the uncompressed data (which must be non-empty if present) + /// - "z": the zstd-compressed data (which must be non-empty if present) /// /// When compression is enabled (by omitting the `compress` argument or specifying it as true) /// then ZSTD compression will be *attempted* on the plaintext message and will be used if the /// compressed data is smaller than the uncompressed data. If disabled, or if compression does - /// not reduce the size (i.e. because it is not compressible), then the message will not be - /// compressed. - /// - /// Future versions may change this to support other encryption algorithms. + /// not reduce the size, then the message will not be compressed. /// /// This method will throw on failure, which can happen in two cases: /// - if there no encryption keys are available at all (which should not occur in normal use). /// - if given a plaintext buffer larger than 1MB (even if the compressed version would be much - /// smaller). It is recommended that clients impose their own limits much smaller than this; - /// this limited is here to match the `decrypt_message` limit which is merely intended to - /// guard against decompression memory exhaustion attacks. + /// smaller). It is recommended that clients impose their own limits much smaller than this + /// on data passed into encrypt_message; this limitation is in *this* function to match the + /// `decrypt_message` limit which is merely intended to guard against decompression memory + /// exhaustion attacks. /// /// Inputs: /// - `plaintext` -- the binary message to encrypt. /// - `compress` -- can be specified as `false` to forcibly disable compression. Normally /// omitted, to use compression if and only if it reduces the size. + /// - `padding` -- the padding multiple: padding will be added as needed to attain a multiple of + /// this value for the final result. 0 or 1 disables padding entirely. Normally omitted to + /// use the default of next-multiple-of-256. /// /// Outputs: - /// - `ciphertext` -- the encrypted ciphertext of the message - ustring encrypt_message(ustring_view plaintext, bool compress = true) const; + /// - `ciphertext` -- the encrypted, etc. value to send to the swarm + ustring encrypt_message( + ustring_view plaintext, bool compress = true, size_t padding = 256) const; /// API: groups/Keys::decrypt_message /// - /// Decrypts group message content that was presumably encrypted with `encrypt_message`. This - /// will attempt decryption using *all* of the known group encryption keys and, if necessary, - /// decompressing the message. + /// Decrypts group message content that was presumably encrypted with `encrypt_message`, + /// verifies the sender signature, decompresses the message (if necessary) and then returns the + /// author pubkey and the plaintext data. /// /// To prevent against memory exhaustion attacks, this method will fail if the value is /// a compressed value that would decompress to a value larger than 1MB. /// /// Inputs: - /// - `ciphertext` -- a encoded, encrypted, (possibly) compressed message as produced by - /// `encrypt_message()`. + /// - `ciphertext` -- an encrypted, encoded, signed, (possibly) compressed message as produced + /// by `encrypt_message()`. /// /// Outputs: - /// - `std::optional` -- the decrypted, decompressed plaintext message if encryption - /// and decompression succeeds; otherwise returns `std::nullopt` if parsing, decryption, or - /// decompression fails. - std::optional decrypt_message(ustring_view ciphertext) const; + /// - `std::pair` -- the session ID (in hex) and the plaintext binary + /// data that was encrypted. + /// + /// On failure this throws a std::exception-derived exception with a `.what()` string containing + /// some diagnostic info on what part failed. Typically a production session client would catch + /// (and possibly log) but otherwise ignore such exceptions and just not process the message if + /// it throws. + std::pair decrypt_message(ustring_view ciphertext) const; }; } // namespace session::config::groups diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 5d25b4ac..36f1267d 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -295,7 +296,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { unsigned char, crypto_aead_xchacha20poly1305_ietf_KEYBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES> encrypted; - std::string_view enc_sv{reinterpret_cast(encrypted.data()), encrypted.size()}; + std::string_view enc_sv = from_unsigned_sv(encrypted); // Shared key for admins auto member_k = seed_hash(enc_key_admin_hash_key); @@ -362,8 +363,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); - std::string_view junk_view{ - reinterpret_cast(junk_data.data()), junk_data.size()}; + std::string_view junk_view = from_unsigned_sv(junk_data); while (!junk_view.empty()) { member_keys.append(junk_view.substr(0, encrypted.size())); junk_view.remove_prefix(encrypted.size()); @@ -730,16 +730,13 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( hseed.size(), user_ed25519_sk.data(), 32, - reinterpret_cast(seed_hash_key.data()), + to_unsigned(seed_hash_key.data()), seed_hash_key.size()); std::array tmp; crypto_generichash_blake2b_state st; crypto_generichash_blake2b_init( - &st, - reinterpret_cast(r_hash_key.data()), - r_hash_key.size(), - tmp.size()); + &st, to_unsigned(r_hash_key.data()), r_hash_key.size(), tmp.size()); crypto_generichash_blake2b_update(&st, hseed.data(), hseed.size()); crypto_generichash_blake2b_update(&st, kT.data(), kT.size()); crypto_generichash_blake2b_update(&st, msg.data(), msg.size()); @@ -1178,11 +1175,10 @@ std::optional Keys::pending_key() const { return std::nullopt; } -static constexpr size_t OVERHEAD = 1 // encryption type indicator - + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + - crypto_aead_xchacha20poly1305_ietf_ABYTES; +static constexpr size_t ENCRYPT_OVERHEAD = + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; -ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { +ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padding) const { if (plaintext.size() > MAX_PLAINTEXT_MESSAGE_SIZE) throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; ustring _compressed; @@ -1196,16 +1192,44 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { } } + oxenc::bt_dict_producer dict{}; + dict.append( + "", 1); // encoded data version (bump this if something changes in an incompatible way) + dict.append("a", std::string_view{from_unsigned(user_ed25519_sk.data()) + 32, 32}); + + std::array signature; + crypto_sign_ed25519_detached( + signature.data(), nullptr, plaintext.data(), plaintext.size(), user_ed25519_sk.data()); + + if (!compress) + dict.append("d", from_unsigned_sv(plaintext)); + + dict.append("s", from_unsigned_sv(signature)); + + if (compress) + dict.append("z", from_unsigned_sv(plaintext)); + + auto encoded = std::move(dict).str(); + + // suppose size == 250, padding = 256 + // so size + overhead(40) == 290 + // need padding of (256 - (290 % 256)) = 256 - 34 = 222 + // thus 290 + 222 = 512 + size_t final_len = ENCRYPT_OVERHEAD + encoded.size(); + if (padding > 1 && final_len % padding != 0) { + size_t to_append = padding - (final_len % padding); + encoded.resize(encoded.size() + to_append); + } + ustring ciphertext; - ciphertext.resize(OVERHEAD + plaintext.size()); - ciphertext[0] = compress ? 'X' : 'x'; - randombytes_buf(ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - ustring_view nonce{ciphertext.data() + 1, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + ciphertext.resize(ENCRYPT_OVERHEAD + encoded.size()); + randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ustring_view nonce{ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data() + 1 + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, nullptr, - plaintext.data(), - plaintext.size(), + to_unsigned(encoded.data()), + encoded.size(), nullptr, 0, nullptr, @@ -1216,59 +1240,115 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress) const { return ciphertext; } -std::optional Keys::decrypt_message(ustring_view ciphertext) const { - if (ciphertext.size() < OVERHEAD) - return std::nullopt; +std::pair Keys::decrypt_message(ustring_view ciphertext) const { + if (ciphertext.size() < ENCRYPT_OVERHEAD) + throw std::runtime_error{"ciphertext is too small to be encrypted data"}; ustring plain; - bool success = false; - bool compressed = false; - char type = static_cast(ciphertext[0]); - ciphertext.remove_prefix(1); - switch (type) { - case 'X': compressed = true; [[fallthrough]]; - case 'x': { - auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - ciphertext.remove_prefix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - plain.resize( - ciphertext.size() - - (OVERHEAD - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES)); - - if (auto pending = pending_key(); - pending && try_decrypting(plain.data(), ciphertext, nonce, *pending)) { - - success = true; - - } else { - - for (auto& k : keys_) { - if (try_decrypting(plain.data(), ciphertext, nonce, k.key)) { - success = true; - break; - } - } + auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext.remove_prefix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + plain.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + // + // Decrypt, using all the possible keys, starting with a pending one (if we have one) + // + bool decrypt_success = false; + if (auto pending = pending_key(); + pending && try_decrypting(plain.data(), ciphertext, nonce, *pending)) { + decrypt_success = true; + } else { + for (auto& k : keys_) { + if (try_decrypting(plain.data(), ciphertext, nonce, k.key)) { + decrypt_success = true; + break; } - break; } + } + + if (!decrypt_success) // none of the keys worked + throw std::runtime_error{"unable to decrypt ciphertext with any current group keys"}; + + // + // Removing any null padding bytes from the end + // + if (auto pos = plain.find_last_not_of('\0'); pos != std::string::npos) + plain.resize(pos + 1); + + // + // Now what we have less should be a bt_dict + // + if (plain.empty() || plain.front() != 'd' || plain.back() != 'e') + throw std::runtime_error{"decrypted data is not a bencoded dict"}; - default: - // Don't know how to handle this type (or it's garbage) - return std::nullopt; + oxenc::bt_dict_consumer dict{from_unsigned_sv(plain)}; + + if (!dict.skip_until("")) + throw std::runtime_error{"group message version tag (\"\") is missing"}; + if (auto v = dict.consume_integer(); v != 1) + throw std::runtime_error{ + "group message version tag (" + std::to_string(v) + + ") is not compatible (we support v1)"}; + + if (!dict.skip_until("a")) + throw std::runtime_error{"missing message author pubkey"}; + auto ed_pk = to_unsigned_sv(dict.consume_string_view()); + if (ed_pk.size() != 32) + throw std::runtime_error{ + "message author pubkey size (" + std::to_string(ed_pk.size()) + ") is invalid"}; + + std::array x_pk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pk.data(), ed_pk.data())) + throw std::runtime_error{ + "author ed25519 pubkey is invalid (unable to convert it to a session id)"}; + + std::pair result; + auto& [session_id, data] = result; + session_id.reserve(66); + session_id += "05"; + oxenc::to_hex(x_pk.begin(), x_pk.end(), std::back_inserter(session_id)); + + ustring_view raw_data; + if (dict.skip_until("d")) { + raw_data = to_unsigned_sv(dict.consume_string_view()); + if (raw_data.empty()) + throw std::runtime_error{"uncompressed message data (\"d\") cannot be empty"}; } - if (!success) // none of the keys worked - return std::nullopt; + if (!dict.skip_until("s")) + throw std::runtime_error{"message signature is missing"}; + auto ed_sig = to_unsigned_sv(dict.consume_string_view()); + if (ed_sig.size() != 64) + throw std::runtime_error{ + "message signature size (" + std::to_string(ed_sig.size()) + ") is invalid"}; + + bool compressed = false; + if (dict.skip_until("z")) { + if (!raw_data.empty()) + throw std::runtime_error{ + "message signature cannot contain both compressed (z) and uncompressed (d) " + "data"}; + raw_data = to_unsigned_sv(dict.consume_string_view()); + if (raw_data.empty()) + throw std::runtime_error{"compressed message data (\"z\") cannot be empty"}; + + compressed = true; + } else if (raw_data.empty()) + throw std::runtime_error{"message must contain compressed (z) or uncompressed (d) data"}; + + if (0 != crypto_sign_ed25519_verify_detached( + ed_sig.data(), raw_data.data(), raw_data.size(), ed_pk.data())) + throw std::runtime_error{"message signature failed validation"}; if (compressed) { - if (auto decomp = zstd_decompress(plain, MAX_PLAINTEXT_MESSAGE_SIZE)) - plain = std::move(*decomp); - else - // Decompression failed - return std::nullopt; - } + if (auto decomp = zstd_decompress(raw_data, MAX_PLAINTEXT_MESSAGE_SIZE)) { + data = std::move(*decomp); + } else + throw std::runtime_error{"message decompression failed"}; + } else + data = raw_data; - return std::move(plain); + return result; } } // namespace session::config::groups @@ -1449,21 +1529,25 @@ LIBSESSION_C_API void groups_keys_encrypt_message( } LIBSESSION_C_API bool groups_keys_decrypt_message( - const config_group_keys* conf, + 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); - auto plaintext = unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); - if (!plaintext) - return false; - - *plaintext_out = static_cast(std::malloc(plaintext->size())); - std::memcpy(*plaintext_out, plaintext->data(), plaintext->size()); - *plaintext_len = plaintext->size(); - return true; + try { + auto [session_id, plaintext] = + unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); + *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) { + set_error(conf, e.what()); + } + return false; } LIBSESSION_C_API bool groups_keys_key_supplement( diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 3188c699..7d7f42c5 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -339,10 +339,11 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { msg += msg; auto compressed = admin1.keys.encrypt_message(to_usv(msg)); + CHECK(compressed.size() == 256); auto uncompressed = admin1.keys.encrypt_message(to_usv(msg), false); + CHECK(uncompressed.size() == 2048); CHECK(compressed.size() < msg.size()); - CHECK(compressed.size() < uncompressed.size()); // Add two new members and send them supplemental keys for (int i = 0; i < 2; ++i) { @@ -412,17 +413,20 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash5"s}}); } - auto decrypted1 = members.back().keys.decrypt_message(compressed); - REQUIRE(decrypted1); - CHECK(to_sv(*decrypted1) == msg); + std::pair decrypted1, decrypted2; + REQUIRE_NOTHROW(decrypted1 = members.back().keys.decrypt_message(compressed)); + CHECK(decrypted1.first == admin1.session_id); + CHECK(to_sv(decrypted1.second) == msg); - auto decrypted2 = members.back().keys.decrypt_message(uncompressed); - REQUIRE(decrypted2); - CHECK(to_sv(*decrypted2) == msg); + REQUIRE_NOTHROW(decrypted2 = members.back().keys.decrypt_message(uncompressed)); + CHECK(decrypted2.first == admin1.session_id); + CHECK(to_sv(decrypted2.second) == msg); auto bad_compressed = compressed; bad_compressed.back() ^= 0b100; - CHECK_FALSE(members.back().keys.decrypt_message(bad_compressed)); + CHECK_THROWS_WITH( + members.back().keys.decrypt_message(bad_compressed), + "unable to decrypt ciphertext with any current group keys"); // Duplicate members[1] from dumps auto& m1b = members.emplace_back( @@ -477,7 +481,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { ustring new_keys_config7{admin1.keys.rekey(admin1.info, admin1.members)}; // Make sure we can encrypt & decrypt even if the rekey is still pending: - CHECK(admin1.keys.decrypt_message(admin1.keys.encrypt_message(to_usv("abc")))); + CHECK_NOTHROW(admin1.keys.decrypt_message(admin1.keys.encrypt_message(to_usv("abc")))); auto [iseq7, ipush7, iobs7] = admin1.info.push(); info_configs.emplace_back("ifakehash7", ipush7); From 9e19fe47c4477525716ccd0fb5239fbc2798f8eb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 26 Sep 2023 09:52:32 -0300 Subject: [PATCH 064/572] Fix C decrypt_message not setting session id --- src/config/groups/keys.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 36f1267d..d92e4af6 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1538,8 +1538,9 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( assert(ciphertext_in && plaintext_out && plaintext_len); try { - auto [session_id, plaintext] = + 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(); From 81bc4de369d88f3086c7067334c848cfce5fc865 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 22 Sep 2023 17:12:17 -0300 Subject: [PATCH 065/572] cmake static bundle cleanups Applies some cmake functions to make dealing with libraries for the static bundle easier. --- CMakeLists.txt | 8 ++----- cmake/AddStaticBundleLib.cmake | 14 ++++++++++++ cmake/combine_archives.cmake | 2 +- external/CMakeLists.txt | 2 ++ src/CMakeLists.txt | 41 +++++++++++++++++++++------------- 5 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 cmake/AddStaticBundleLib.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dd7edac..5e3a5700 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ if(STATIC_LIBSTD) endif() endif() +include(AddStaticBundleLib) # Always build PIC set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -97,12 +98,7 @@ add_subdirectory(src) if(STATIC_BUNDLE) include(combine_archives) - combine_archives(session-util - crypto - config - version - libsodium::sodium-internal - libzstd_static) + combine_archives(session-util libsession-static-bundle "${LIBSESSION_STATIC_BUNDLE_LIBS}") set(lib_folder lib) if(IOS) set(lib_folder "${lib_folder}-${ARCH}") diff --git a/cmake/AddStaticBundleLib.cmake b/cmake/AddStaticBundleLib.cmake new file mode 100644 index 00000000..8c482c47 --- /dev/null +++ b/cmake/AddStaticBundleLib.cmake @@ -0,0 +1,14 @@ + +set(LIBSESSION_STATIC_BUNDLE_LIBS "" CACHE INTERNAL "list of libs to go into the static bundle lib") + +# Call as: +# +# libsession_static_bundle(target [target2 ...]) +# +# to append the given target(s) to the list of libraries that will be combined to make the static +# bundled libsession-util.a. +function(libsession_static_bundle) + list(APPEND LIBSESSION_STATIC_BUNDLE_LIBS "${ARGN}") + list(REMOVE_DUPLICATES LIBSESSION_STATIC_BUNDLE_LIBS) + set(LIBSESSION_STATIC_BUNDLE_LIBS "${LIBSESSION_STATIC_BUNDLE_LIBS}" CACHE INTERNAL "") +endfunction() diff --git a/cmake/combine_archives.cmake b/cmake/combine_archives.cmake index 53064a91..0807b0c6 100644 --- a/cmake/combine_archives.cmake +++ b/cmake/combine_archives.cmake @@ -1,4 +1,4 @@ -function(combine_archives output_archive) +function(combine_archives output_archive dep_target) set(FULL_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/lib${output_archive}.a) if(NOT APPLE) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 9802ac83..0d7425de 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -64,6 +64,7 @@ endif() add_subdirectory(libsodium-internal) +libsession_static_bundle(libsodium::sodium-internal) set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "") @@ -84,3 +85,4 @@ export( NAMESPACE libsession:: FILE libsessionZstd.cmake ) +libsession_static_bundle(libzstd_static) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 041afbc8..5c2675a7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,14 +6,26 @@ if(WARNINGS_AS_ERRORS) target_compile_options(common INTERFACE -Werror) endif() -add_library(crypto + +set(export_targets) +macro(add_libsession_util_library name) + add_library(${name} ${ARGN}) + + set_target_properties( + ${name} + PROPERTIES OUTPUT_NAME session-util-${name}) + + libsession_static_bundle(${name}) + + list(APPEND export_targets ${name}) +endmacro() + + +add_libsession_util_library(crypto xed25519.cpp ) -set_target_properties( - crypto - PROPERTIES OUTPUT_NAME session-util-crypto) -add_library(config +add_libsession_util_library(config bt_merge.cpp config.cpp config/base.cpp @@ -31,20 +43,18 @@ add_library(config fields.cpp util.cpp ) -set_target_properties( - config - PROPERTIES OUTPUT_NAME session-util-config) target_link_libraries(crypto PUBLIC libsodium::sodium-internal - common) + common +) target_link_libraries(config PUBLIC crypto oxenc::oxenc libzstd::static - common) +) if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") @@ -80,17 +90,16 @@ else() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.c.in" "${CMAKE_CURRENT_BINARY_DIR}/version.c") endif() endif() -add_library(version version.c) -set_target_properties(version PROPERTIES OUTPUT_NAME session-util-version) +add_libsession_util_library(version version.c) target_include_directories(version PRIVATE ../include) target_link_libraries(common INTERFACE version) - -add_library(libsession::config ALIAS config) -add_library(libsession::crypto ALIAS crypto) +foreach(tgt ${export_targets}) + add_library("libsession::${tgt}" ALIAS "${tgt}") +endforeach() export( - TARGETS config crypto common version + TARGETS ${export_targets} common NAMESPACE libsession:: FILE libsessionTargets.cmake ) From 83be419ba9b544a6f0596dd32a940c13f238e182 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 20:35:58 -0300 Subject: [PATCH 066/572] Add StaticBuild support --- CMakeLists.txt | 24 +++- cmake/StaticBuild.cmake | 244 ++++++++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 8 +- tests/CMakeLists.txt | 2 +- 4 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 cmake/StaticBuild.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e3a5700..ee77e13b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,18 +45,27 @@ if(WIN32) set(default_static_libstd ON) endif() -if(MINGW OR ANDROID OR IOS OR STATIC_BUNDLE) - set(use_lto_default OFF) +option(BUILD_SHARED_LIBS "Build as shared library" OFF) + +if(BUILD_SHARED_LIBS) + set(static_default OFF) else() - set(use_lto_default ON) + set(static_default ON) endif() +option(BUILD_STATIC_DEPS "Build all dependencies statically rather than trying to link to them on the system" ${static_default}) +option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${static_default}) +if(MINGW OR ANDROID OR IOS) # OR STATIC_BUNDLE) + set(use_lto_default OFF) +else() + set(use_lto_default ON) +endif() option(WARNINGS_AS_ERRORS "Treat all compiler warnings as errors" OFF) + option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd}) -option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" OFF) -option(BUILD_SHARED_LIBS "Build as shared library" OFF) + option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) if(USE_LTO) @@ -95,7 +104,12 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(external) add_subdirectory(src) +if (BUILD_STATIC_DEPS) + include(StaticBuild) +endif() + if(STATIC_BUNDLE) + include(combine_archives) combine_archives(session-util libsession-static-bundle "${LIBSESSION_STATIC_BUNDLE_LIBS}") diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake new file mode 100644 index 00000000..25685b96 --- /dev/null +++ b/cmake/StaticBuild.cmake @@ -0,0 +1,244 @@ +# cmake bits to do a full static build, downloading and building all dependencies. + +# Most of these are CACHE STRINGs so that you can override them using -DWHATEVER during cmake +# invocation to override. + +set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") + +set(LIBUNISTRING_VERSION 1.1 CACHE STRING "libunistring version") +set(LIBUNISTRING_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libunistring + CACHE STRING "libunistring mirror(s)") +set(LIBUNISTRING_SOURCE libunistring-${LIBUNISTRING_VERSION}.tar.xz) +set(LIBUNISTRING_HASH SHA512=01a4267bbd301ea5c389b17ee918ae5b7d645da8b2c6c6f0f004ff2dead9f8e50cda2c6047358890a5fceadc8820ffc5154879193b9bb8970f3fb1fea1f411d6 + CACHE STRING "libunistring source hash") + +set(LIBIDN2_VERSION 2.3.4 CACHE STRING "libidn2 version") +set(LIBIDN2_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libidn + CACHE STRING "libidn2 mirror(s)") +set(LIBIDN2_SOURCE libidn2-${LIBIDN2_VERSION}.tar.gz) +set(LIBIDN2_HASH SHA512=a6e90ccef56cfd0b37e3333ab3594bb3cec7ca42a138ca8c4f4ce142da208fa792f6c78ca00c01001c2bc02831abcbaf1cf9bcc346a5290fd7b30708f5a462f3 + CACHE STRING "libidn2 source hash") + +set(GMP_VERSION 6.2.1 CACHE STRING "gmp version") +set(GMP_MIRROR ${LOCAL_MIRROR} https://gmplib.org/download/gmp + CACHE STRING "gmp mirror(s)") +set(GMP_SOURCE gmp-${GMP_VERSION}.tar.xz) +set(GMP_HASH SHA512=c99be0950a1d05a0297d65641dd35b75b74466f7bf03c9e8a99895a3b2f9a0856cd17887738fa51cf7499781b65c049769271cbcb77d057d2e9f1ec52e07dd84 + CACHE STRING "gmp source hash") + +set(NETTLE_VERSION 3.9.1 CACHE STRING "nettle version") +set(NETTLE_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/nettle + CACHE STRING "nettle mirror(s)") +set(NETTLE_SOURCE nettle-${NETTLE_VERSION}.tar.gz) +set(NETTLE_HASH SHA512=5939c4b43cf9ff6c6272245b85f123c81f8f4e37089fa4f39a00a570016d837f6e706a33226e4bbfc531b02a55b2756ff312461225ed88de338a73069e031ced + CACHE STRING "nettle source hash") + +set(LIBTASN1_VERSION 4.19.0 CACHE STRING "libtasn1 version") +set(LIBTASN1_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libtasn1 + CACHE STRING "libtasn1 mirror(s)") +set(LIBTASN1_SOURCE libtasn1-${LIBTASN1_VERSION}.tar.gz) +set(LIBTASN1_HASH SHA512=287f5eddfb5e21762d9f14d11997e56b953b980b2b03a97ed4cd6d37909bda1ed7d2cdff9da5d270a21d863ab7e54be6b85c05f1075ac5d8f0198997cf335ef4 + CACHE STRING "libtasn1 source hash") + + +include(ExternalProject) + +set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) +set(DEPS_SOURCEDIR ${CMAKE_BINARY_DIR}/static-deps-sources) + +include_directories(BEFORE SYSTEM ${DEPS_DESTDIR}/include) + +file(MAKE_DIRECTORY ${DEPS_DESTDIR}/include) + +set(deps_cc "${CMAKE_C_COMPILER}") +set(deps_cxx "${CMAKE_CXX_COMPILER}") +if(CMAKE_C_COMPILER_LAUNCHER) + set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") +endif() +if(CMAKE_CXX_COMPILER_LAUNCHER) + set(deps_cxx "${CMAKE_CXX_COMPILER_LAUNCHER} ${deps_cxx}") +endif() + + +function(expand_urls output source_file) + set(expanded) + foreach(mirror ${ARGN}) + list(APPEND expanded "${mirror}/${source_file}") + endforeach() + set(${output} "${expanded}" PARENT_SCOPE) +endfunction() + +function(add_static_target target ext_target libname) + add_library(${target} STATIC IMPORTED GLOBAL) + add_dependencies(${target} ${ext_target}) + set_target_properties(${target} PROPERTIES + IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname} + ) + if(ARGN) + target_link_libraries(${target} INTERFACE ${ARGN}) + endif() + libsession_static_bundle(${target}) +endfunction() + + + +set(cross_host "") +set(cross_rc "") +if(CMAKE_CROSSCOMPILING) + set(cross_host "--host=${ARCH_TRIPLET}") + if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) + set(cross_rc "WINDRES=${CMAKE_RC_COMPILER}") + endif() +endif() +if(ANDROID) + set(android_toolchain_suffix linux-android) + set(android_compiler_suffix linux-android23) + if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) + set(android_machine x86_64) + set(cross_host "--host=x86_64-linux-android") + set(android_compiler_prefix x86_64) + set(android_compiler_suffix linux-android23) + set(android_toolchain_prefix x86_64) + set(android_toolchain_suffix linux-android) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86) + set(android_machine x86) + set(cross_host "--host=i686-linux-android") + set(android_compiler_prefix i686) + set(android_compiler_suffix linux-android23) + set(android_toolchain_prefix i686) + set(android_toolchain_suffix linux-android) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a) + set(android_machine arm) + set(cross_host "--host=armv7a-linux-androideabi") + set(android_compiler_prefix armv7a) + set(android_compiler_suffix linux-androideabi23) + set(android_toolchain_prefix arm) + set(android_toolchain_suffix linux-androideabi) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a) + set(android_machine arm64) + set(cross_host "--host=aarch64-linux-android") + set(android_compiler_prefix aarch64) + set(android_compiler_suffix linux-android23) + set(android_toolchain_prefix aarch64) + set(android_toolchain_suffix linux-android) + else() + message(FATAL_ERROR "unknown android arch: ${CMAKE_ANDROID_ARCH_ABI}") + endif() + set(deps_cc "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang") + set(deps_cxx "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang++") + set(deps_ld "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_toolchain_suffix}-ld") + set(deps_ranlib "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ranlib") + set(deps_ar "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ar") +endif() + +set(deps_CFLAGS "-O2") +set(deps_CXXFLAGS "-O2") + +if(WITH_LTO) + set(deps_CFLAGS "${deps_CFLAGS} -flto") +endif() + +if(APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET) + set(deps_CFLAGS "${deps_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(deps_CXXFLAGS "${deps_CXXFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") +endif() + +if(_winver) + set(deps_CFLAGS "${deps_CFLAGS} -D_WIN32_WINNT=${_winver}") + set(deps_CXXFLAGS "${deps_CXXFLAGS} -D_WIN32_WINNT=${_winver}") +endif() + + +if("${CMAKE_GENERATOR}" STREQUAL "Unix Makefiles") + set(_make $(MAKE)) +else() + set(_make make) +endif() + + +# Builds a target; takes the target name (e.g. "readline") and builds it in an external project with +# target name suffixed with `_external`. Its upper-case value is used to get the download details +# (from the variables set above). The following options are supported and passed through to +# ExternalProject_Add if specified. If omitted, these defaults are used: +set(build_def_DEPENDS "") +set(build_def_PATCH_COMMAND "") +set(build_def_CONFIGURE_COMMAND ./configure ${cross_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic + "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}" "CXXFLAGS=${deps_CXXFLAGS}" ${cross_rc}) +set(build_def_CONFIGURE_EXTRA "") +set(build_def_BUILD_COMMAND ${_make}) +set(build_def_INSTALL_COMMAND ${_make} install) +set(build_def_BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/lib___TARGET___.a ${DEPS_DESTDIR}/include/___TARGET___.h) + +function(build_external target) + set(options DEPENDS PATCH_COMMAND CONFIGURE_COMMAND CONFIGURE_EXTRA BUILD_COMMAND INSTALL_COMMAND BUILD_BYPRODUCTS) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "${options}") + foreach(o ${options}) + if(NOT DEFINED arg_${o}) + set(arg_${o} ${build_def_${o}}) + endif() + endforeach() + string(REPLACE ___TARGET___ ${target} arg_BUILD_BYPRODUCTS "${arg_BUILD_BYPRODUCTS}") + + string(TOUPPER "${target}" prefix) + expand_urls(urls ${${prefix}_SOURCE} ${${prefix}_MIRROR}) + set(extract_ts) + if(NOT CMAKE_VERSION VERSION_LESS 3.24) + set(extract_ts DOWNLOAD_EXTRACT_TIMESTAMP ON) + endif() + ExternalProject_Add("${target}_external" + DEPENDS ${arg_DEPENDS} + BUILD_IN_SOURCE ON + PREFIX ${DEPS_SOURCEDIR} + URL ${urls} + URL_HASH ${${prefix}_HASH} + DOWNLOAD_NO_PROGRESS ON + PATCH_COMMAND ${arg_PATCH_COMMAND} + CONFIGURE_COMMAND ${arg_CONFIGURE_COMMAND} ${arg_CONFIGURE_EXTRA} + BUILD_COMMAND ${arg_BUILD_COMMAND} + INSTALL_COMMAND ${arg_INSTALL_COMMAND} + BUILD_BYPRODUCTS ${arg_BUILD_BYPRODUCTS} + ${extract_ts} + ) +endfunction() + + +build_external(libtasn1 + CONFIGURE_EXTRA --disable-doc + BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libtasn1.a ${DEPS_DESTDIR}/include/libtasn1.h) +add_static_target(libtasn1 libtasn1_external libtasn1.a) + +build_external(libunistring + BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libunistring.a ${DEPS_DESTDIR}/include/unistr.h) +add_static_target(libunistring libunistring_external libunistring.a) + +build_external(libidn2 + CONFIGURE_EXTRA --disable-doc + DEPENDS libunistring_external + BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libidn2.a ${DEPS_DESTDIR}/include/idn2.h) +add_static_target(libidn2 libidn2_external libidn2.a libunistring) + +build_external(gmp + CONFIGURE_EXTRA CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp + DEPENDS libidn2_external libtasn1_external) +add_static_target(gmp gmp_external libgmp.a libidn2 libtasn1) + +build_external(nettle + CONFIGURE_EXTRA --disable-openssl --libdir=${DEPS_DESTDIR}/lib + "CPPFLAGS=-I${DEPS_DESTDIR}/include" "LDFLAGS=-L${DEPS_DESTDIR}/lib" + + DEPENDS gmp_external + BUILD_BYPRODUCTS + ${DEPS_DESTDIR}/lib/libnettle.a + ${DEPS_DESTDIR}/lib/libhogweed.a + ${DEPS_DESTDIR}/include/nettle/version.h +) +add_static_target(nettle nettle_external libnettle.a gmp) +add_static_target(hogweed nettle_external libhogweed.a nettle) + +link_libraries(-static-libstdc++) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + link_libraries(-static-libgcc) +endif() +if(MINGW) + link_libraries(-Wl,-Bstatic -lpthread) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c2675a7..35be9fdd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,13 @@ add_library(common INTERFACE) +target_link_libraries(common INTERFACE + oxenc::oxenc) target_include_directories(common INTERFACE ../include) if(WARNINGS_AS_ERRORS) target_compile_options(common INTERFACE -Werror) endif() - set(export_targets) macro(add_libsession_util_library name) add_library(${name} ${ARGN}) @@ -46,13 +47,16 @@ add_libsession_util_library(config target_link_libraries(crypto PUBLIC + common + PRIVATE libsodium::sodium-internal common ) target_link_libraries(config PUBLIC crypto - oxenc::oxenc + common + PRIVATE libzstd::static ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3bf47602..7712f4e7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,7 +17,7 @@ add_executable(testAll ) target_link_libraries(testAll PRIVATE - config + libsession::config Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) From bc09e2eff47f5e1db2778f972a031e8011243eb2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 15:44:50 -0300 Subject: [PATCH 067/572] cmake improvements - add .so library version to aid with system packaging - make sure libsodium-internal is always built statically - always build the version library statically - fix test suite libsodium linkage when building shared libs --- CMakeLists.txt | 4 ++++ external/CMakeLists.txt | 6 +++++- src/CMakeLists.txt | 6 ++++-- tests/CMakeLists.txt | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ee77e13b..f570754f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,10 @@ project(libsession-util DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) +message(STATUS "${PROJECT_NAME} v${PROJECT_VERSION}") + +set(LIBSESSION_LIBVERSION ${PROJECT_VERSION}) + list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 0d7425de..a287e7bd 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -63,7 +63,11 @@ if(APPLE) endif() -add_subdirectory(libsodium-internal) +function(libsodium_internal_subdir) + set(BUILD_SHARED_LIBS OFF) + add_subdirectory(libsodium-internal) +endfunction() +libsodium_internal_subdir() libsession_static_bundle(libsodium::sodium-internal) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 35be9fdd..a629c3ca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,7 +14,9 @@ macro(add_libsession_util_library name) set_target_properties( ${name} - PROPERTIES OUTPUT_NAME session-util-${name}) + PROPERTIES + OUTPUT_NAME session-util-${name} + SOVERSION ${LIBSESSION_LIBVERSION}) libsession_static_bundle(${name}) @@ -94,7 +96,7 @@ else() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.c.in" "${CMAKE_CURRENT_BINARY_DIR}/version.c") endif() endif() -add_libsession_util_library(version version.c) +add_libsession_util_library(version STATIC version.c) target_include_directories(version PRIVATE ../include) target_link_libraries(common INTERFACE version) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7712f4e7..f2abf265 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(testAll target_link_libraries(testAll PRIVATE libsession::config + libsodium::sodium-internal Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) From 3789da858a5dfc6603198252df61274a5215d683 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 17:00:52 -0300 Subject: [PATCH 068/572] Fix sodium linkage config needs it as well --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a629c3ca..e3fdc7bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,13 +52,13 @@ target_link_libraries(crypto common PRIVATE libsodium::sodium-internal - common ) target_link_libraries(config PUBLIC crypto common PRIVATE + libsodium::sodium-internal libzstd::static ) From a78cbb3275adb51da3070e1c176854915c9f5cc5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 16:53:28 -0300 Subject: [PATCH 069/572] Make package installable --- CMakeLists.txt | 32 ++++++++++++++++++++++++++++++++ external/CMakeLists.txt | 2 +- libsession-util.pc.in | 14 ++++++++++++++ src/CMakeLists.txt | 9 ++++++--- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 libsession-util.pc.in diff --git a/CMakeLists.txt b/CMakeLists.txt index f570754f..d46a2740 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ message(STATUS "${PROJECT_NAME} v${PROJECT_VERSION}") set(LIBSESSION_LIBVERSION ${PROJECT_VERSION}) +include(GNUInstallDirs) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") @@ -60,6 +61,14 @@ endif() option(BUILD_STATIC_DEPS "Build all dependencies statically rather than trying to link to them on the system" ${static_default}) option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${static_default}) +if(BUILD_SHARED_LIBS OR libsession_IS_TOPLEVEL_PROJECT) + set(install_default ON) +else() + set(install_default OFF) +endif() + +option(LIBSESSION_INSTALL "Install libsession-util libraries and headers to cmake install target; defaults to ON if BUILD_SHARED_LIBS is enabled or when building the top-level project" ${install_default}) + if(MINGW OR ANDROID OR IOS) # OR STATIC_BUNDLE) set(use_lto_default OFF) else() @@ -131,3 +140,26 @@ option(WITH_TESTS "Enable unit tests" ${libsession_IS_TOPLEVEL_PROJECT}) if(WITH_TESTS) add_subdirectory(tests) endif() + + +if(LIBSESSION_INSTALL) + + install( + TARGETS ${libsession_export_targets} + EXPORT libsessionConfig + DESTINATION ${CMAKE_INSTALL_LIBDIR} + ) + + install(DIRECTORY include/session DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + PATTERN *.h PATTERN *.hpp) + + set(libsession_target_links) + foreach(tgt ${libsession_export_targets}) + set(libsession_target_links "${libsession_target_links} -lsession-${tgt}") + endforeach() + configure_file(libsession-util.pc.in libsession-util.pc @ONLY) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libsession-util.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + ) + +endif() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a287e7bd..f3ca466b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -77,7 +77,7 @@ set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "") set(ZSTD_BUILD_SHARED OFF CACHE BOOL "") set(ZSTD_BUILD_STATIC ON CACHE BOOL "") set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "") -add_subdirectory(zstd/build/cmake) +add_subdirectory(zstd/build/cmake EXCLUDE_FROM_ALL) # zstd's cmake doesn't properly set up include paths on its targets, so we have to wrap it in an # interface target that does: add_library(libzstd_static_fixed_includes INTERFACE) diff --git a/libsession-util.pc.in b/libsession-util.pc.in new file mode 100644 index 00000000..c59b05c5 --- /dev/null +++ b/libsession-util.pc.in @@ -0,0 +1,14 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=@CMAKE_INSTALL_FULL_LIBDIR@ +includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@ + +Name: libsession-util +Description: Session utility libraries +Version: @PROJECT_VERSION@ + +Libs: -L${libdir} @libsession_target_links@ +Libs.private: -lprotobuf-lite -lnettle +Requires: liboxenc +Requires.private: nettle protobuf-lite +Cflags: -I${includedir} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3fdc7bb..eac4d6cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,7 +15,7 @@ macro(add_libsession_util_library name) set_target_properties( ${name} PROPERTIES - OUTPUT_NAME session-util-${name} + OUTPUT_NAME session-${name} SOVERSION ${LIBSESSION_LIBVERSION}) libsession_static_bundle(${name}) @@ -96,7 +96,8 @@ else() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.c.in" "${CMAKE_CURRENT_BINARY_DIR}/version.c") endif() endif() -add_libsession_util_library(version STATIC version.c) +add_library(version STATIC version.c) +libsession_static_bundle(version) target_include_directories(version PRIVATE ../include) target_link_libraries(common INTERFACE version) @@ -105,7 +106,9 @@ foreach(tgt ${export_targets}) add_library("libsession::${tgt}" ALIAS "${tgt}") endforeach() export( - TARGETS ${export_targets} common + TARGETS ${export_targets} common version NAMESPACE libsession:: FILE libsessionTargets.cmake ) + +set(libsession_export_targets "${export_targets}" PARENT_SCOPE) From a2ac563fc1e8223e344c4870345f99fe0f5779aa Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 20:57:05 -0300 Subject: [PATCH 070/572] build shared libs for various ci jobs --- .drone.jsonnet | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index e33f6d4a..2f0088ec 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -94,6 +94,7 @@ local debian_build(name, 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE=' + build_type + ' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + + '-DBUILD_SHARED_LIBS=ON ' + '-DUSE_LTO=' + (if lto then 'ON ' else 'OFF ') + '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') + cmake_extra, @@ -215,6 +216,7 @@ local mac_builder(name, 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fcolor-diagnostics -DCMAKE_BUILD_TYPE=' + build_type + ' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + + '-DBUILD_SHARED_LIBS=ON ' + '-DUSE_LTO=' + (if lto then 'ON ' else 'OFF ') + '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') + cmake_extra, From 232f057961e1a847d7b2d782e699a75e77bbc176 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Sep 2023 21:00:49 -0300 Subject: [PATCH 071/572] Exclude static build packages we don't actually use The file added here includes some future use packages that we don't use yet; this updates the script to not build them if we don't depend on them. --- cmake/StaticBuild.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 25685b96..7cdee8a5 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -197,6 +197,7 @@ function(build_external target) BUILD_COMMAND ${arg_BUILD_COMMAND} INSTALL_COMMAND ${arg_INSTALL_COMMAND} BUILD_BYPRODUCTS ${arg_BUILD_BYPRODUCTS} + EXCLUDE_FROM_ALL ON ${extract_ts} ) endfunction() From e34e49597561b1ed1bd8814dd113a2a7bf1d6115 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 29 Sep 2023 12:43:28 -0300 Subject: [PATCH 072/572] Various StaticBuild fixes - Add fixes for cross-compiling gmp and nettle on macos. - Add fixes for GMP guessing its built type (sometimes wrongly) based on the host CPU (instead we force cmake's library architecture onto it now) - Various fixes/hacks for building on apple, as usual. - libunistring libidn2 and libtasn1 appear unneeded for libsession-util, so drop them. --- cmake/StaticBuild.cmake | 103 ++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 7cdee8a5..ab994c5e 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,25 +5,11 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") -set(LIBUNISTRING_VERSION 1.1 CACHE STRING "libunistring version") -set(LIBUNISTRING_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libunistring - CACHE STRING "libunistring mirror(s)") -set(LIBUNISTRING_SOURCE libunistring-${LIBUNISTRING_VERSION}.tar.xz) -set(LIBUNISTRING_HASH SHA512=01a4267bbd301ea5c389b17ee918ae5b7d645da8b2c6c6f0f004ff2dead9f8e50cda2c6047358890a5fceadc8820ffc5154879193b9bb8970f3fb1fea1f411d6 - CACHE STRING "libunistring source hash") - -set(LIBIDN2_VERSION 2.3.4 CACHE STRING "libidn2 version") -set(LIBIDN2_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libidn - CACHE STRING "libidn2 mirror(s)") -set(LIBIDN2_SOURCE libidn2-${LIBIDN2_VERSION}.tar.gz) -set(LIBIDN2_HASH SHA512=a6e90ccef56cfd0b37e3333ab3594bb3cec7ca42a138ca8c4f4ce142da208fa792f6c78ca00c01001c2bc02831abcbaf1cf9bcc346a5290fd7b30708f5a462f3 - CACHE STRING "libidn2 source hash") - -set(GMP_VERSION 6.2.1 CACHE STRING "gmp version") +set(GMP_VERSION 6.3.0 CACHE STRING "gmp version") set(GMP_MIRROR ${LOCAL_MIRROR} https://gmplib.org/download/gmp CACHE STRING "gmp mirror(s)") set(GMP_SOURCE gmp-${GMP_VERSION}.tar.xz) -set(GMP_HASH SHA512=c99be0950a1d05a0297d65641dd35b75b74466f7bf03c9e8a99895a3b2f9a0856cd17887738fa51cf7499781b65c049769271cbcb77d057d2e9f1ec52e07dd84 +set(GMP_HASH SHA512=e85a0dab5195889948a3462189f0e0598d331d3457612e2d3350799dba2e244316d256f8161df5219538eb003e4b5343f989aaa00f96321559063ed8c8f29fd2 CACHE STRING "gmp source hash") set(NETTLE_VERSION 3.9.1 CACHE STRING "nettle version") @@ -33,13 +19,6 @@ set(NETTLE_SOURCE nettle-${NETTLE_VERSION}.tar.gz) set(NETTLE_HASH SHA512=5939c4b43cf9ff6c6272245b85f123c81f8f4e37089fa4f39a00a570016d837f6e706a33226e4bbfc531b02a55b2756ff312461225ed88de338a73069e031ced CACHE STRING "nettle source hash") -set(LIBTASN1_VERSION 4.19.0 CACHE STRING "libtasn1 version") -set(LIBTASN1_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/libtasn1 - CACHE STRING "libtasn1 mirror(s)") -set(LIBTASN1_SOURCE libtasn1-${LIBTASN1_VERSION}.tar.gz) -set(LIBTASN1_HASH SHA512=287f5eddfb5e21762d9f14d11997e56b953b980b2b03a97ed4cd6d37909bda1ed7d2cdff9da5d270a21d863ab7e54be6b85c05f1075ac5d8f0198997cf335ef4 - CACHE STRING "libtasn1 source hash") - include(ExternalProject) @@ -85,6 +64,9 @@ endfunction() set(cross_host "") set(cross_rc "") if(CMAKE_CROSSCOMPILING) + if(APPLE AND NOT ARCH_TRIPLET AND APPLE_TARGET_TRIPLE) + set(ARCH_TRIPLET "${APPLE_TARGET_TRIPLE}") + endif() set(cross_host "--host=${ARCH_TRIPLET}") if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) set(cross_rc "WINDRES=${CMAKE_RC_COMPILER}") @@ -139,8 +121,13 @@ if(WITH_LTO) endif() if(APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET) - set(deps_CFLAGS "${deps_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - set(deps_CXXFLAGS "${deps_CXXFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + if(SDK_NAME) + set(deps_CFLAGS "${deps_CFLAGS} -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(deps_CXXFLAGS "${deps_CXXFLAGS} -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + set(deps_CFLAGS "${deps_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(deps_CXXFLAGS "${deps_CXXFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() endif() if(_winver) @@ -203,29 +190,61 @@ function(build_external target) endfunction() -build_external(libtasn1 - CONFIGURE_EXTRA --disable-doc - BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libtasn1.a ${DEPS_DESTDIR}/include/libtasn1.h) -add_static_target(libtasn1 libtasn1_external libtasn1.a) +set(apple_cflags_arch) +set(apple_cxxflags_arch) +set(apple_ldflags_arch) +set(gmp_build_host "${cross_host}") +if(APPLE AND CMAKE_CROSSCOMPILING) + if(gmp_build_host MATCHES "^(.*-.*-)ios([0-9]+)(-.*)?$") + set(gmp_build_host "${CMAKE_MATCH_1}darwin${CMAKE_MATCH_2}${CMAKE_MATCH_3}") + endif() + if(gmp_build_host MATCHES "^(.*-.*-.*)-simulator$") + set(gmp_build_host "${CMAKE_MATCH_1}") + endif() + + set(apple_arch) + if(ARCH_TRIPLET MATCHES "^(arm|aarch)64.*") + set(apple_arch "arm64") + elseif(ARCH_TRIPLET MATCHES "^x86_64.*") + set(apple_arch "x86_64") + else() + message(FATAL_ERROR "Don't know how to specify -arch for GMP for ${ARCH_TRIPLET} (${APPLE_TARGET_TRIPLE})") + endif() -build_external(libunistring - BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libunistring.a ${DEPS_DESTDIR}/include/unistr.h) -add_static_target(libunistring libunistring_external libunistring.a) + set(apple_cflags_arch " -arch ${apple_arch}") + set(apple_cxxflags_arch " -arch ${apple_arch}") + if(CMAKE_OSX_DEPLOYMENT_TARGET) + if (SDK_NAME) + set(apple_ldflags_arch " -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + elseif(CMAKE_OSX_DEPLOYMENT_TARGET) + set(apple_ldflags_arch " -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + endif() + set(apple_ldflags_arch "${apple_ldflags_arch} -arch ${apple_arch}") -build_external(libidn2 - CONFIGURE_EXTRA --disable-doc - DEPENDS libunistring_external - BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/libidn2.a ${DEPS_DESTDIR}/include/idn2.h) -add_static_target(libidn2 libidn2_external libidn2.a libunistring) + if(CMAKE_OSX_SYSROOT) + foreach(f c cxx ld) + set(apple_${f}flags_arch "${apple_${f}flags_arch} -isysroot ${CMAKE_OSX_SYSROOT}") + endforeach() + endif() +elseif(gmp_build_host STREQUAL "") + set(gmp_build_host "--build=${CMAKE_LIBRARY_ARCHITECTURE}") +endif() build_external(gmp - CONFIGURE_EXTRA CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp - DEPENDS libidn2_external libtasn1_external) -add_static_target(gmp gmp_external libgmp.a libidn2 libtasn1) + CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic + "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" + "LDFLAGS=${apple_ldflags_arch}" ${cross_rc} CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp +) +add_static_target(gmp gmp_external libgmp.a) build_external(nettle - CONFIGURE_EXTRA --disable-openssl --libdir=${DEPS_DESTDIR}/lib - "CPPFLAGS=-I${DEPS_DESTDIR}/include" "LDFLAGS=-L${DEPS_DESTDIR}/lib" + CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --libdir=${DEPS_DESTDIR}/lib + --with-pic --disable-openssl + "CC=${deps_cc}" "CXX=${deps_cxx}" + "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" + "CPPFLAGS=-I${DEPS_DESTDIR}/include" + "LDFLAGS=-L${DEPS_DESTDIR}/lib${apple_ldflags_arch}" DEPENDS gmp_external BUILD_BYPRODUCTS From 57c15c338cdc7d76d8becda3cc707f1014f1772a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 29 Sep 2023 17:11:32 -0300 Subject: [PATCH 073/572] Add submodule check & system_or_submodule --- external/CMakeLists.txt | 63 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index f3ca466b..1ef10941 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,3 +1,64 @@ +option(SUBMODULE_CHECK "Enables checking that vendored library submodules are up to date" ON) +if(SUBMODULE_CHECK) + find_package(Git) + if(GIT_FOUND) + function(check_submodule relative_path) + execute_process(COMMAND git rev-parse "HEAD" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE localHead) + execute_process(COMMAND git rev-parse "HEAD:external/${relative_path}" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE checkedHead) + string(COMPARE EQUAL "${localHead}" "${checkedHead}" upToDate) + if (upToDate) + message(STATUS "Submodule 'external/${relative_path}' is up-to-date") + else() + message(FATAL_ERROR "Submodule 'external/${relative_path}' is not up-to-date. Please update with\ngit submodule update --init --recursive\nor run cmake with -DSUBMODULE_CHECK=OFF") + endif() + + # Extra arguments check nested submodules + foreach(submod ${ARGN}) + execute_process(COMMAND git rev-parse "HEAD" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path}/${submod} OUTPUT_VARIABLE localHead) + execute_process(COMMAND git rev-parse "HEAD:${submod}" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE checkedHead) + string(COMPARE EQUAL "${localHead}" "${checkedHead}" upToDate) + if (NOT upToDate) + message(FATAL_ERROR "Nested submodule '${relative_path}/${submod}' is not up-to-date. Please update with\ngit submodule update --init --recursive\nor run cmake with -DSUBMODULE_CHECK=OFF") + endif() + endforeach() + endfunction () + + message(STATUS "Checking submodules") + check_submodule(ios-cmake) + check_submodule(libsodium-internal) + check_submodule(oxen-encoding) + check_submodule(zstd) + endif() +endif() + + +if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) + find_package(PkgConfig REQUIRED) +endif() + +macro(system_or_submodule BIGNAME smallname pkgconf subdir) + option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) + if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES) + pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET GLOBAL) + endif() + if(${BIGNAME}_FOUND) + add_library(${smallname} INTERFACE IMPORTED GLOBAL) + if(NOT TARGET PkgConfig::${BIGNAME} AND CMAKE_VERSION VERSION_LESS "3.21") + # Work around cmake bug 22180 (PkgConfig::THING not set if no flags needed) + else() + target_link_libraries(${smallname} INTERFACE PkgConfig::${BIGNAME}) + endif() + message(STATUS "Found system ${smallname} ${${BIGNAME}_VERSION}") + else() + message(STATUS "using ${smallname} submodule") + add_subdirectory(${subdir}) + endif() + if(NOT TARGET ${smallname}::${smallname}) + add_library(${smallname}::${smallname} ALIAS ${smallname}) + endif() +endmacro() + + set(deps_cc "${CMAKE_C_COMPILER}") set(cross_host "") set(cross_rc "") @@ -41,7 +102,7 @@ if(CMAKE_CROSSCOMPILING) endif() -add_subdirectory(oxen-encoding) +system_or_submodule(OXENC oxenc liboxenc>=1.0.8 oxen-encoding) if(CMAKE_C_COMPILER_LAUNCHER) From c4aa5b7f71d9a4885c1a3824f753a2aec670a976 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 8 Sep 2023 19:43:55 -0300 Subject: [PATCH 074/572] Add onion req parsing/encryption support This adds support for onion request encryption/decryption and onion request final request parsing and replying. This code is moving over from libonionrequest, though with some significant rewriting: - the request parser object structure is significantly slimmed down - the AES256-GCM implementation reimplemented in nettle rather than depending on OpenSSL for it - AES256-CBC is dropped entirely; we technically still support it but have never actually used it, because it sucks. This implementation supports just the AES256-GCM and XChaCha20-Poly1305 encryptions. The nettle dependency is also required by libquic for gnutls, which will likely eventually become a dependency of this code, so this move away from OpenSSL aligns with that change. --- .../session/onionreq/channel_encryption.hpp | 62 +++++ include/session/onionreq/key_types.hpp | 119 +++++++++ include/session/onionreq/parser.hpp | 46 ++++ src/CMakeLists.txt | 30 +++ src/onionreq/channel_encryption.cpp | 227 ++++++++++++++++++ src/onionreq/key_types.cpp | 85 +++++++ src/onionreq/parser.cpp | 46 ++++ tests/CMakeLists.txt | 2 + tests/test_onionreq.cpp | 87 +++++++ 9 files changed, 704 insertions(+) create mode 100644 include/session/onionreq/channel_encryption.hpp create mode 100644 include/session/onionreq/key_types.hpp create mode 100644 include/session/onionreq/parser.hpp create mode 100644 src/onionreq/channel_encryption.cpp create mode 100644 src/onionreq/key_types.cpp create mode 100644 src/onionreq/parser.cpp create mode 100644 tests/test_onionreq.cpp diff --git a/include/session/onionreq/channel_encryption.hpp b/include/session/onionreq/channel_encryption.hpp new file mode 100644 index 00000000..db37271c --- /dev/null +++ b/include/session/onionreq/channel_encryption.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "key_types.hpp" + +namespace session::onionreq { + +enum class EncryptType { + aes_gcm, + xchacha20, +}; + +// Takes the encryption type as a string, returns the EncryptType value (or throws if invalid). +// Supported values: aes-gcm and xchacha20. gcm is accepted as an aliases for aes-gcm. +EncryptType parse_enc_type(std::string_view enc_type); + +inline constexpr std::string_view to_string(EncryptType type) { + switch (type) { + case EncryptType::xchacha20: return "xchacha20"sv; + case EncryptType::aes_gcm: return "aes-gcm"sv; + } + return ""sv; +} + +// Encryption/decription class for encryption/decrypting outgoing/incoming messages. +class ChannelEncryption { + public: + ChannelEncryption(x25519_seckey private_key, x25519_pubkey public_key, bool server = true) : + private_key_{std::move(private_key)}, + public_key_{std::move(public_key)}, + server_{server} {} + + // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. + // `reply` should be false for a client-to-snode message, and true on a returning + // snode-to-client message. + ustring encrypt(EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const; + ustring decrypt(EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const; + + // AES-GCM encryption. + ustring encrypt_aesgcm(ustring_view plainText, const x25519_pubkey& pubKey) const; + ustring decrypt_aesgcm(ustring_view cipherText, const x25519_pubkey& pubKey) const; + + // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a + // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of + // H(aB || A || B), which Bob can compute when receiving as H(bA || A || B). The returned value + // always has the crypto_aead_xchacha20poly1305_ietf_NPUBBYTES nonce prepended to the beginning. + // + // When Bob (the server) encrypts a method for Alice (the client), he uses shared key + // H(bA || A || B) (note that this is *different* that what would result if Bob was a client + // sending to Alice the client). + ustring encrypt_xchacha20(ustring_view plaintext, const x25519_pubkey& pubKey) const; + ustring decrypt_xchacha20(ustring_view ciphertext, const x25519_pubkey& pubKey) const; + + private: + const x25519_seckey private_key_; + const x25519_pubkey public_key_; + bool server_; // True if we are the server (i.e. the snode). +}; + +} // namespace session::onionreq diff --git a/include/session/onionreq/key_types.hpp b/include/session/onionreq/key_types.hpp new file mode 100644 index 00000000..0f776da2 --- /dev/null +++ b/include/session/onionreq/key_types.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../types.hpp" +#include "../util.hpp" + +namespace session::onionreq { + +using namespace std::literals; + +namespace detail { + template + inline constexpr std::array null_bytes = {0}; + + void load_from_hex(void* buffer, size_t length, std::string_view hex); + void load_from_bytes(void* buffer, size_t length, std::string_view bytes); + +} // namespace detail + +template +struct alignas(size_t) key_base : std::array { + std::string_view view() const { + return {reinterpret_cast(this->data()), KeyLength}; + } + std::string hex() const { return oxenc::to_hex(view()); } + explicit operator bool() const { return *this != detail::null_bytes; } + + // Loads the key from a hex string; throws if the hex is the wrong size or not hex. + static Derived from_hex(std::string_view hex) { + Derived d; + detail::load_from_hex(d.data(), d.size(), hex); + return d; + } + // Same as above, but returns nullopt if invalid instead of throwing + static std::optional maybe_from_hex(std::string_view hex) { + try { + return from_hex(hex); + } catch (...) { + } + return std::nullopt; + } + // Loads the key from a byte string; throws if the wrong size. + static Derived from_bytes(std::string_view bytes) { + Derived d; + detail::load_from_bytes(d.data(), d.size(), bytes); + return d; + } + static Derived from_bytes(ustring_view bytes) { return from_bytes(from_unsigned_sv(bytes)); } +}; + +template +struct pubkey_base : key_base { + using PubKeyBase = pubkey_base; +}; + +struct legacy_pubkey : pubkey_base {}; +struct x25519_pubkey : pubkey_base {}; +struct ed25519_pubkey : pubkey_base { + // Returns the {base32z}.snode representation of this pubkey + std::string snode_address() const; +}; + +template +struct seckey_base : key_base {}; + +struct legacy_seckey : seckey_base { + legacy_pubkey pubkey() const; +}; +struct ed25519_seckey : seckey_base { + ed25519_pubkey pubkey() const; +}; +struct x25519_seckey : seckey_base { + x25519_pubkey pubkey() const; +}; + +using legacy_keypair = std::pair; +using ed25519_keypair = std::pair; +using x25519_keypair = std::pair; + +/// Parse a pubkey string value encoded in any of base32z, b64, hex, or raw bytes, based on the +/// length of the value. Returns a null pk (i.e. operator bool() returns false) and warns on +/// invalid input (i.e. wrong length or invalid encoding). +legacy_pubkey parse_legacy_pubkey(std::string_view pubkey_in); +ed25519_pubkey parse_ed25519_pubkey(std::string_view pubkey_in); +x25519_pubkey parse_x25519_pubkey(std::string_view pubkey_in); + +} // namespace session::onionreq + +namespace std { +template +struct hash> { + size_t operator()(const session::onionreq::pubkey_base& pk) const { + // pubkeys are already random enough to use the first bytes directly as a good (and fast) + // hash value + static_assert(alignof(decltype(pk)) >= alignof(size_t)); + return *reinterpret_cast(pk.data()); + } +}; + +template <> +struct hash : hash { +}; +template <> +struct hash : hash { +}; +template <> +struct hash + : hash {}; + +} // namespace std diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp new file mode 100644 index 00000000..70a938ec --- /dev/null +++ b/include/session/onionreq/parser.hpp @@ -0,0 +1,46 @@ +#include + +#include "session/onionreq/channel_encryption.hpp" +#include "session/types.hpp" + +namespace session::onionreq { + +/// The default maximum size of an onion request accepted by the OnionReqParser constructor. +constexpr size_t DEFAULT_MAX_SIZE = 10'485'760; // 10 MiB + +class OnionReqParser { + private: + x25519_keypair keys; + ChannelEncryption enc; + EncryptType enc_type = EncryptType::aes_gcm; + x25519_pubkey remote_pk; + ustring payload_; + + public: + /// Constructs a parser, parsing the given request sent to us. Throws if parsing or decryption + /// fails. + OnionReqParser( + ustring_view x25519_pubkey, + ustring_view x25519_privkey, + ustring_view req, + size_t max_size = DEFAULT_MAX_SIZE); + + /// plaintext payload, decrypted from the incoming request during construction. + ustring_view payload() const { return payload_; } + + /// Extracts payload from this object (via a std::move); after the call the object's payload + /// will be empty. + ustring move_payload() { + ustring ret{std::move(payload_)}; + payload_.clear(); // Guarantee empty, even if SSO active + return ret; + } + + ustring_view remote_pubkey() const { return to_unsigned_sv(remote_pk.view()); } + + /// Encrypts a reply using the appropriate encryption as determined when parsing the + /// request. + ustring encrypt_reply(ustring_view reply) const; +}; + +} // namespace session::onionreq diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eac4d6cd..64ac6849 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,19 @@ macro(add_libsession_util_library name) endmacro() +find_package(nlohmann_json REQUIRED) + +if(NOT BUILD_STATIC_DEPS) + find_package(PkgConfig REQUIRED) + + if(NOT target nettle) + pkg_check_modules(NETTLE nettle IMPORTED_TARGET) + add_library(nettle INTERFACE) + target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) + endif() +endif() + + add_libsession_util_library(crypto xed25519.cpp ) @@ -47,6 +60,13 @@ add_libsession_util_library(config util.cpp ) +add_libsession_util_library(onionreq + onionreq/channel_encryption.cpp + onionreq/key_types.cpp + onionreq/parser.cpp +) + + target_link_libraries(crypto PUBLIC common @@ -62,6 +82,16 @@ target_link_libraries(config libzstd::static ) +target_link_libraries(onionreq + PUBLIC + crypto + common + PRIVATE + nlohmann_json::nlohmann_json + libsodium::sodium-internal + nettle +) + if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") # GCC 11 has an overzealous (and false) stringop-overread warning, but only when LTO is off. diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/channel_encryption.cpp new file mode 100644 index 00000000..afcc6b56 --- /dev/null +++ b/src/onionreq/channel_encryption.cpp @@ -0,0 +1,227 @@ +#include "session/onionreq/channel_encryption.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace session::onionreq { + +namespace { + + // Derive shared secret from our (ephemeral) `seckey` and the other party's `pubkey` + std::array calculate_shared_secret( + const x25519_seckey& seckey, const x25519_pubkey& pubkey) { + std::array secret; + if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) + throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); + return secret; + } + + constexpr std::string_view salt{"LOKI"}; + + std::array derive_symmetric_key( + const x25519_seckey& seckey, const x25519_pubkey& pubkey) { + auto key = calculate_shared_secret(seckey, pubkey); + + auto usalt = to_unsigned_sv(salt); + + crypto_auth_hmacsha256_state state; + + crypto_auth_hmacsha256_init(&state, usalt.data(), usalt.size()); + crypto_auth_hmacsha256_update(&state, key.data(), key.size()); + crypto_auth_hmacsha256_final(&state, key.data()); + + return key; + } + + // More robust shared secret calculation, used when using xchacha20-poly1305 encryption. (This + // could be used for AES-GCM as well, but would break backwards compatibility with existing + // Session clients). + std::array xchacha20_shared_key( + const x25519_pubkey& local_pub, + const x25519_seckey& local_sec, + const x25519_pubkey& remote_pub, + bool local_first) { + std::array key; + static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES >= crypto_scalarmult_BYTES); + if (0 != crypto_scalarmult( + key.data(), + local_sec.data(), + remote_pub.data())) // Use key as tmp storage for aB + throw std::runtime_error{"Failed to compute shared key for xchacha20"}; + crypto_generichash_state h; + crypto_generichash_init(&h, nullptr, 0, key.size()); + crypto_generichash_update(&h, key.data(), crypto_scalarmult_BYTES); + crypto_generichash_update( + &h, (local_first ? local_pub : remote_pub).data(), local_pub.size()); + crypto_generichash_update( + &h, (local_first ? remote_pub : local_pub).data(), local_pub.size()); + crypto_generichash_final(&h, key.data(), key.size()); + return key; + } + +} // namespace + +EncryptType parse_enc_type(std::string_view enc_type) { + if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") + return EncryptType::xchacha20; + if (enc_type == "aes-gcm" || enc_type == "gcm") + return EncryptType::aes_gcm; + throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; +} + +ustring ChannelEncryption::encrypt( + EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const { + switch (type) { + case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); + case EncryptType::aes_gcm: return encrypt_aesgcm(plaintext, pubkey); + } + throw std::runtime_error{"Invalid encryption type"}; +} + +ustring ChannelEncryption::decrypt( + EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const { + switch (type) { + case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); + case EncryptType::aes_gcm: return decrypt_aesgcm(ciphertext, pubkey); + } + throw std::runtime_error{"Invalid decryption type"}; +} + +ustring ChannelEncryption::encrypt_aesgcm( + ustring_view plaintext, const x25519_pubkey& pubKey) const { + + auto key = derive_symmetric_key(private_key_, pubKey); + + // Initialise cipher context with the key + struct gcm_aes256_ctx ctx; + static_assert(key.size() == AES256_KEY_SIZE); + gcm_aes256_set_key(&ctx, key.data()); + + ustring output; + output.resize(GCM_IV_SIZE + plaintext.size() + GCM_DIGEST_SIZE); + + // Start the output with the random IV, and load it into ctx + auto* o = output.data(); + randombytes_buf(o, GCM_IV_SIZE); + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, o); + o += GCM_IV_SIZE; + + // Append encrypted data + gcm_aes256_encrypt(&ctx, plaintext.size(), o, plaintext.data()); + o += plaintext.size(); + + // Append digest + gcm_aes256_digest(&ctx, GCM_DIGEST_SIZE, o); + o += GCM_DIGEST_SIZE; + + assert(o == output.data() + output.size()); + + return output; +} + +ustring ChannelEncryption::decrypt_aesgcm( + ustring_view ciphertext, const x25519_pubkey& pubKey) const { + + if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) + throw std::runtime_error{"ciphertext data is too short"}; + + auto key = derive_symmetric_key(private_key_, pubKey); + + // Initialise cipher context with the key + struct gcm_aes256_ctx ctx; + static_assert(key.size() == AES256_KEY_SIZE); + gcm_aes256_set_key(&ctx, key.data()); + + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, ciphertext.data()); + + ciphertext.remove_prefix(GCM_IV_SIZE); + auto digest_in = ciphertext.substr(ciphertext.size() - GCM_DIGEST_SIZE); + ciphertext.remove_suffix(GCM_DIGEST_SIZE); + + ustring plaintext; + plaintext.resize(ciphertext.size()); + + gcm_aes256_decrypt(&ctx, ciphertext.size(), plaintext.data(), ciphertext.data()); + + std::array digest_out; + gcm_aes256_digest(&ctx, digest_out.size(), digest_out.data()); + + if (sodium_memcmp(digest_out.data(), digest_in.data(), GCM_DIGEST_SIZE) != 0) + throw std::runtime_error{"Decryption failed (AES256-GCM)"}; + + return plaintext; +} + +ustring ChannelEncryption::encrypt_xchacha20( + ustring_view plaintext, const x25519_pubkey& pubKey) const { + + ustring ciphertext; + ciphertext.resize( + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); + + // Generate random nonce, and stash it at the beginning of ciphertext: + randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + auto* c = ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + unsigned long long clen; + + crypto_aead_xchacha20poly1305_ietf_encrypt( + c, + &clen, + plaintext.data(), + plaintext.size(), + nullptr, + 0, // additional data + nullptr, // nsec (always unused) + ciphertext.data(), + key.data()); + assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); + ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); + return ciphertext; +} + +ustring ChannelEncryption::decrypt_xchacha20( + ustring_view ciphertext, const x25519_pubkey& pubKey) const { + + // Extract nonce from the beginning of the ciphertext: + auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext.remove_prefix(nonce.size()); + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::runtime_error{"Invalid ciphertext: too short"}; + + const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); + + ustring plaintext; + plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + auto* m = plaintext.data(); + unsigned long long mlen; + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + m, + &mlen, + nullptr, // nsec (always unused) + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, // additional data + nonce.data(), + key.data())) + throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; + assert(mlen <= plaintext.size()); + plaintext.resize(mlen); + return plaintext; +} + +} // namespace session::onionreq diff --git a/src/onionreq/key_types.cpp b/src/onionreq/key_types.cpp new file mode 100644 index 00000000..f234f16e --- /dev/null +++ b/src/onionreq/key_types.cpp @@ -0,0 +1,85 @@ +#include "session/onionreq/key_types.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace session::onionreq { + +namespace detail { + + void load_from_hex(void* buffer, size_t length, std::string_view hex) { + if (!oxenc::is_hex(hex)) + throw std::runtime_error{"Hex key data is invalid: data is not hex"}; + if (hex.size() != 2 * length) + throw std::runtime_error{ + "Hex key data is invalid: expected " + std::to_string(length) + + " hex digits, received " + std::to_string(hex.size())}; + oxenc::from_hex(hex.begin(), hex.end(), reinterpret_cast(buffer)); + } + + void load_from_bytes(void* buffer, size_t length, std::string_view bytes) { + if (bytes.size() != length) + throw std::runtime_error{ + "Key data is invalid: expected " + std::to_string(length) + + " bytes, received " + std::to_string(bytes.size())}; + std::memmove(buffer, bytes.data(), length); + } + +} // namespace detail + +std::string ed25519_pubkey::snode_address() const { + auto addr = oxenc::to_base32z(begin(), end()); + addr += ".snode"; + return addr; +} + +legacy_pubkey legacy_seckey::pubkey() const { + legacy_pubkey pk; + crypto_scalarmult_ed25519_base_noclamp(pk.data(), data()); + return pk; +}; +ed25519_pubkey ed25519_seckey::pubkey() const { + ed25519_pubkey pk; + crypto_sign_ed25519_sk_to_pk(pk.data(), data()); + return pk; +}; +x25519_pubkey x25519_seckey::pubkey() const { + x25519_pubkey pk; + crypto_scalarmult_curve25519_base(pk.data(), data()); + return pk; +}; + +template +static T parse_pubkey(std::string_view pubkey_in) { + T pk{}; + static_assert(pk.size() == 32); + if (pubkey_in.size() == 32) + detail::load_from_bytes(pk.data(), 32, pubkey_in); + else if (pubkey_in.size() == 64 && oxenc::is_hex(pubkey_in)) + oxenc::from_hex(pubkey_in.begin(), pubkey_in.end(), pk.begin()); + else if ( + (pubkey_in.size() == 43 || (pubkey_in.size() == 44 && pubkey_in.back() == '=')) && + oxenc::is_base64(pubkey_in)) + oxenc::from_base64(pubkey_in.begin(), pubkey_in.end(), pk.begin()); + else if (pubkey_in.size() == 52 && oxenc::is_base32z(pubkey_in)) + oxenc::from_base32z(pubkey_in.begin(), pubkey_in.end(), pk.begin()); + + return pk; +} + +legacy_pubkey parse_legacy_pubkey(std::string_view pubkey_in) { + return parse_pubkey(pubkey_in); +} +ed25519_pubkey parse_ed25519_pubkey(std::string_view pubkey_in) { + return parse_pubkey(pubkey_in); +} +x25519_pubkey parse_x25519_pubkey(std::string_view pubkey_in) { + return parse_pubkey(pubkey_in); +} + +} // namespace session::onionreq diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp new file mode 100644 index 00000000..a1167a34 --- /dev/null +++ b/src/onionreq/parser.cpp @@ -0,0 +1,46 @@ +#include "session/onionreq/parser.hpp" + +#include +#include + +#include +#include + +namespace session::onionreq { + +OnionReqParser::OnionReqParser( + ustring_view x25519_pk, ustring_view x25519_sk, ustring_view req, size_t max_size) : + keys{x25519_pubkey::from_bytes(x25519_pk), x25519_seckey::from_bytes(x25519_sk)}, + enc{keys.second, keys.first} { + if (sodium_init() == -1) + throw std::runtime_error{"Failed to initialize libsodium!"}; + if (req.size() < sizeof(uint32_t)) + throw std::invalid_argument{"onion request data too small"}; + if (req.size() > max_size) + throw std::invalid_argument{"onion request data too big"}; + auto size = oxenc::load_little_to_host(req.data()); + req.remove_prefix(sizeof(size)); + + if (req.size() < size) + throw std::invalid_argument{"encrypted onion request data segment too small"}; + auto ciphertext = req.substr(0, size); + req.remove_prefix(size); + auto metadata = nlohmann::json::parse(req); + + if (auto encit = metadata.find("enc_type"); encit != metadata.end()) + enc_type = parse_enc_type(encit->get()); + // else leave it at the backwards-compat AES-GCM default + + if (auto itr = metadata.find("ephemeral_key"); itr != metadata.end()) + remote_pk = parse_x25519_pubkey(itr->get()); + else + throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; + + payload_ = enc.decrypt(enc_type, ciphertext, remote_pk); +} + +ustring OnionReqParser::encrypt_reply(ustring_view reply) const { + return enc.encrypt(enc_type, reply, remote_pk); +} + +} // namespace session::onionreq diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f2abf265..9bef2f19 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,12 +13,14 @@ add_executable(testAll test_group_keys.cpp test_group_info.cpp test_group_members.cpp + test_onionreq.cpp test_xed25519.cpp ) target_link_libraries(testAll PRIVATE libsession::config libsodium::sodium-internal + libsession::onionreq Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp new file mode 100644 index 00000000..93465674 --- /dev/null +++ b/tests/test_onionreq.cpp @@ -0,0 +1,87 @@ +#include +#include +#include + +#include "utils.hpp" + +using namespace session; +using namespace session::onionreq; + +TEST_CASE("Onion request encryption", "[encryption][onionreq]") { + + auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytes; + auto a = "ccc335912da8939e2b44816728a5a4773063efa82bf7ae2d42a0abfa2caa452d"_hexbytes; + auto B = "caea52c5b0c316d85ffb53ea536826618b13dee40685f166f632653114526a78"_hexbytes; + auto b = "8fcd8ad3a15c76f76f1c56dff0c529999f8c59b4acda79e05666e54d5727dca1"_hexbytes; + + auto enc_gcm = + "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" + ""_hexbytes; + auto enc_gcm_broken1 = + "1eb6ae1cd72f60999486365759bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" + ""_hexbytes; + auto enc_gcm_broken2 = + "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b69" + ""_hexbytes; + auto enc_xchacha20 = + "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" + "7d1638d765db75de02b032"_hexbytes; + auto enc_xchacha20_broken1 = + "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" + "7d1638d765db75de02b033"_hexbytes; + auto enc_xchacha20_broken2 = + "9e1a3abe60eff3ea5c23556ccfe225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" + "7d1638d765db75de02b032"_hexbytes; + + ChannelEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; + + CHECK(from_unsigned_sv(e.decrypt_aesgcm(enc_gcm, x25519_pubkey::from_bytes(A))) == + "Hello world"); + CHECK(from_unsigned_sv(e.decrypt_xchacha20(enc_xchacha20, x25519_pubkey::from_bytes(A))) == + "Hello world"); + CHECK_THROWS(e.decrypt_aesgcm(enc_xchacha20_broken1, x25519_pubkey::from_bytes(A))); + CHECK_THROWS(e.decrypt_aesgcm(enc_xchacha20_broken2, x25519_pubkey::from_bytes(A))); + CHECK_THROWS(e.decrypt_xchacha20(enc_xchacha20_broken1, x25519_pubkey::from_bytes(A))); + CHECK_THROWS(e.decrypt_xchacha20(enc_xchacha20_broken2, x25519_pubkey::from_bytes(A))); +} + +TEST_CASE("Onion request parser", "[onionreq][parser]") { + + auto A = "8167e97672005c669a48858c69895f395ca235219ac3f7a4210022b1f910e652"_hexbytes; + auto a = "d2ee09e1a557a077d385fcb69a11ffb6909ecdcc8348def3e0e4172c8a1431c1"_hexbytes; + auto B = "8388de69bc0d4b6196133233ad9a46ba0473474bc67718aad96a3a33c257f726"_hexbytes; + auto b = "2f4d1c0d28e137777ec0a316e9f4f763e3e66662a6c51994c6315c9ef34b6deb"_hexbytes; + + auto enc_gcm = + "270000009525d587d188c92a966eef0e7162bef99a6171a124575b998072a8ee7eb265e0b6f0930ed96504" + "7b22656e635f74797065223a20226165732d67636d222c2022657068656d6572616c5f6b6579223a202238" + "31363765393736373230303563363639613438383538633639383935663339356361323335323139616333" + "6637613432313030323262316639313065363532227d"_hexbytes; + auto enc_gcm_broken1 = ""_hexbytes; + auto enc_gcm_broken2 = ""_hexbytes; + auto enc_xchacha20 = + "33000000e440bc244ddcafd947b86fc5a964aa58de54a6d75cc0f0f3840db14b6c1176a8e2e0a04d5fbdf9" + "8f23adee1edc8362ab99b10b7b22656e635f74797065223a2022786368616368613230222c202265706865" + "6d6572616c5f6b6579223a2022383136376539373637323030356336363961343838353863363938393566" + "33393563613233353231396163336637613432313030323262316639313065363532227d"_hexbytes; + auto enc_xchacha20_broken1 = ""_hexbytes; + auto enc_xchacha20_broken2 = ""_hexbytes; + + OnionReqParser parser_gcm{B, b, enc_gcm}; + CHECK(from_unsigned_sv(parser_gcm.payload()) == "Hello world"); + CHECK(parser_gcm.remote_pubkey() == A); + auto aes_reply = parser_gcm.encrypt_reply(to_unsigned_sv("Goodbye world")); + CHECK(aes_reply.size() == 12 + 13 + 16); + + ChannelEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; + CHECK(from_unsigned_sv(e.decrypt_aesgcm(aes_reply, x25519_pubkey::from_bytes(B))) == + "Goodbye world"); + + OnionReqParser parser_xchacha20{B, b, enc_xchacha20}; + CHECK(from_unsigned_sv(parser_xchacha20.payload()) == "Hello world"); + CHECK(parser_xchacha20.remote_pubkey() == A); + auto xcha_reply = parser_xchacha20.encrypt_reply(to_unsigned_sv("Goodbye world")); + CHECK(xcha_reply.size() == 16 + 13 + 24); + CHECK(from_unsigned_sv(e.decrypt_xchacha20(xcha_reply, x25519_pubkey::from_bytes(B))) == + "Goodbye world"); +} From bdbe541a529e2e419571570b7fbe6a711c08d55b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 13 Sep 2023 13:47:20 -0300 Subject: [PATCH 075/572] Add submodule for nlohmann_json Use the submodule if we are building static deps, or if we can't find a system install. --- .drone.jsonnet | 2 +- .gitmodules | 3 +++ external/CMakeLists.txt | 6 ++++++ external/nlohmann-json | 1 + src/CMakeLists.txt | 2 -- 5 files changed, 11 insertions(+), 3 deletions(-) create mode 160000 external/nlohmann-json diff --git a/.drone.jsonnet b/.drone.jsonnet index 2f0088ec..385af530 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -12,7 +12,7 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local default_deps_nocxx = []; +local default_deps_nocxx = ['nlohmann-json3-dev']; local default_deps = ['g++'] + default_deps_nocxx; local docker_base = 'registry.oxen.rocks/lokinet-ci-'; diff --git a/.gitmodules b/.gitmodules index 98d3ec63..e8d2e5df 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "external/zstd"] path = external/zstd url = https://github.com/facebook/zstd.git +[submodule "external/nlohmann-json"] + path = external/nlohmann-json + url = https://github.com/nlohmann/json.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 1ef10941..9379d4bd 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -27,6 +27,7 @@ if(SUBMODULE_CHECK) check_submodule(ios-cmake) check_submodule(libsodium-internal) check_submodule(oxen-encoding) + check_submodule(nlohmann-json) check_submodule(zstd) endif() endif() @@ -151,3 +152,8 @@ export( FILE libsessionZstd.cmake ) libsession_static_bundle(libzstd_static) + + +set(JSON_BuildTests OFF CACHE INTERNAL "") +set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use +system_or_submodule(NLOHMANN nlohmann_json nlohmann_json>=3.7.0 nlohmann-json) diff --git a/external/nlohmann-json b/external/nlohmann-json new file mode 160000 index 00000000..bc889afb --- /dev/null +++ b/external/nlohmann-json @@ -0,0 +1 @@ +Subproject commit bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64ac6849..95777f8e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,8 +24,6 @@ macro(add_libsession_util_library name) endmacro() -find_package(nlohmann_json REQUIRED) - if(NOT BUILD_STATIC_DEPS) find_package(PkgConfig REQUIRED) From b631c095407e7cef9da205b60876f372a2cab37d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 14 Sep 2023 01:22:12 -0300 Subject: [PATCH 076/572] Tweak nettle inclusion --- src/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 95777f8e..27294815 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,9 +27,9 @@ endmacro() if(NOT BUILD_STATIC_DEPS) find_package(PkgConfig REQUIRED) - if(NOT target nettle) - pkg_check_modules(NETTLE nettle IMPORTED_TARGET) - add_library(nettle INTERFACE) + if(NOT TARGET nettle) + pkg_check_modules(NETTLE nettle IMPORTED_TARGET REQUIRED) + add_library(nettle INTERFACE IMPORTED) target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) endif() endif() From cd3024d634f5a9acc671d4200c3bf61d0e1c2958 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 29 Sep 2023 17:41:13 -0300 Subject: [PATCH 077/572] Fix include dirs for static libs --- cmake/StaticBuild.cmake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index ab994c5e..a5a288f0 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -25,10 +25,11 @@ include(ExternalProject) set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) set(DEPS_SOURCEDIR ${CMAKE_BINARY_DIR}/static-deps-sources) -include_directories(BEFORE SYSTEM ${DEPS_DESTDIR}/include) - file(MAKE_DIRECTORY ${DEPS_DESTDIR}/include) +add_library(libsession-external-libs INTERFACE IMPORTED GLOBAL) +target_include_directories(libsession-external-libs SYSTEM BEFORE INTERFACE ${DEPS_DESTDIR}/include) + set(deps_cc "${CMAKE_C_COMPILER}") set(deps_cxx "${CMAKE_CXX_COMPILER}") if(CMAKE_C_COMPILER_LAUNCHER) @@ -50,6 +51,7 @@ endfunction() function(add_static_target target ext_target libname) add_library(${target} STATIC IMPORTED GLOBAL) add_dependencies(${target} ${ext_target}) + target_link_libraries(${target} INTERFACE libsession-external-libs) set_target_properties(${target} PROPERTIES IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname} ) From c78f65af62f57220279ced0ea931087ec54d5b84 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 25 Sep 2023 21:17:42 -0300 Subject: [PATCH 078/572] New communities 25xxx blinding implementation --- include/session/blinding.hpp | 74 +++++++++++++++++ include/session/xed25519.hpp | 2 +- src/CMakeLists.txt | 1 + src/blinding.cpp | 154 +++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_blinding.cpp | 145 +++++++++++++++++++++++++++++++++ 6 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 include/session/blinding.hpp create mode 100644 src/blinding.cpp create mode 100644 tests/test_blinding.cpp diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp new file mode 100644 index 00000000..b56d5893 --- /dev/null +++ b/include/session/blinding.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include "util.hpp" + +namespace session { + +/// Helper functions for community 25xxx blinded ID from a Session pubkey and the server pubkey. +/// This is a verifiable Ed25519 pubkey (prefixed with 25) and is fully deterministic. +/// +/// Details: we first compute a blinding factor: +/// +/// k = H(SESSION_ID || SERVER_PK) mod L, where H = 64-byte BLAKE2b +/// +/// Next we convert the Session ID (an X25519 pubkey) to Ed25519, and also clear the sign bit +/// (i.e. take the positive). Thus we give up one bit of the pubkey (because the initial X25519 +/// -> Ed25519 conversion lost it). +/// +/// X = session id x25519 pubkey +/// A = |Ed(X)| -- i.e. + of the two possible Ed25519 pubkey alternatives for X +/// +/// then the blinded pubkey is prefix "25" followed by `kA`. +/// +/// To create such a signature, starting from the underlying Ed25519 keypair from seed `s`, with +/// private scalar `z` (NOT the seed) and public key point `S`, we use the same blinding factor +/// to compute private scalar `a` associated with `A`: +/// +/// a = z if S is positive (i.e. sign bit is 0) +/// a = -z if S is negative (sign bit is 1) +/// +/// which yields our blinded private scalar `ka`. +/// +/// From here we generate a signature very similarly to EdDSA, but adapted to this different +/// private signing mechanism. For a message `M`: +/// +/// r = H64(H32(seed, key="SessCommBlind25_seed") || kA || M, key="SessCommBlind25_sig") mod L +/// +/// analagously to Ed25519's +/// +/// r = SHA512(SHA512(seed)[32:64] || M) mod L +/// +/// but using BLAKE2b 64-byte and 32-byte keyed hashes instead of SHA512. (We also include the +/// `A` in the hash so that the same message with different server_pks will result in different +/// `r` values). +/// +/// From there we follow the standard EdDSA construction: +/// +/// R = rG +/// S = r + H(R || kA || M) ka (mod L) +/// +/// (using the standard Ed25519 SHA-512 here for H, so that this is verifiable as a standard +/// Ed25519 signature). +/// +/// This (R, S) signature is then Ed25519-verifiable using pubkey kA. + +/// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is +/// exposed for debugging/testing. Takes session id and pk in bytes, not hex. session id can +/// be 05-prefixed (33 bytes) or unprefixed (32 bytes). +std::array blind25_factor(ustring_view session_id, ustring_view server_pk); + +/// Computes the 25-blinded id from a session id and server pubkey. Values accepted and +/// returned are hex-encoded. +std::string blind25_id(std::string_view session_id, std::string_view server_pk); + +/// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would +/// be returned from blind25_id(). +/// +/// Takes the Ed25519 secret key (64 bytes) and the server pubkey (in hex (64 digits) or bytes +/// (32 bytes)). Returns the 64-byte signature. +ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); + +} // namespace session diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index 2c55679f..e389e9d0 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -29,7 +29,7 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string /// Given a curve25519 pubkey, this returns the associated XEd25519-derived Ed25519 pubkey. Note, /// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519 /// pubkey: this always returns the positive value. You can get the other possibility (the -/// negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`. +/// negative) by setting the sign bit, i.e. `returned_pubkey[31] |= 0x80`. std::array pubkey(ustring_view curve25519_pubkey); /// "Softer" version that takes/returns strings of regular chars diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 27294815..bf1a6b76 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,6 +36,7 @@ endif() add_libsession_util_library(crypto + blinding.cpp xed25519.cpp ) diff --git a/src/blinding.cpp b/src/blinding.cpp new file mode 100644 index 00000000..1048ec1e --- /dev/null +++ b/src/blinding.cpp @@ -0,0 +1,154 @@ +#include "session/blinding.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "session/xed25519.hpp" + +namespace session { + +using namespace std::literals; + +using uc32 = std::array; +using uc33 = std::array; +using uc64 = std::array; + +std::array blind25_factor(ustring_view session_id, ustring_view server_pk) { + assert(session_id.size() == 32 || session_id.size() == 33); + assert(server_pk.size() == 32); + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, nullptr, 0, 64); + if (session_id.size() == 32) { + constexpr unsigned char prefix = 0x05; + crypto_generichash_blake2b_update(&st, &prefix, 1); + } + crypto_generichash_blake2b_update(&st, session_id.data(), session_id.size()); + crypto_generichash_blake2b_update(&st, server_pk.data(), server_pk.size()); + uc64 blind_hash; + crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + + uc32 k; + crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); + return k; +} + +std::string blind25_id(std::string_view session_id, std::string_view server_pk) { + if (session_id.size() != 66 || !oxenc::is_hex(session_id)) + throw std::invalid_argument{"blind25_id: session_id must be hex (66 digits)"}; + if (session_id[0] != '0' || session_id[1] != '5') + throw std::invalid_argument{"blind25_id: session_id must start with 05"}; + if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) + throw std::invalid_argument{"blind25_id: server_pk must be hex (64 digits)"}; + + uc33 raw_sid; + oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); + ustring_view sid_bytes{raw_sid.data(), raw_sid.size()}; + auto k = blind25_factor(sid_bytes, to_unsigned_sv(oxenc::from_hex(server_pk))); + + auto ed_pk = xed25519::pubkey(sid_bytes.substr(1)); + uc32 blinded_pk; + if (0 != crypto_scalarmult_ed25519_noclamp(blinded_pk.data(), k.data(), ed_pk.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + + std::string result; + result.resize(66); + result[0] = '2'; + result[1] = '5'; + oxenc::to_hex(blinded_pk.begin(), blinded_pk.end(), result.begin() + 2); + return result; +} + +static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); +static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); + +ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind25_sign: Invalid ed25519_sk is not the expected 64 bytes"}; + uc32 server_pk; + if (server_pk_in.size() == 32) + std::memcpy(server_pk.data(), server_pk_in.data(), 32); + else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) + oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); + else + throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + + ustring_view S{ed25519_sk.data() + 32, 32}; + + uc32 z; + crypto_sign_ed25519_sk_to_curve25519(z.data(), ed25519_sk.data()); + + uc33 session_id; + session_id[0] = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) + throw std::runtime_error{ + "blind25_sign: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; + + ustring_view X{session_id.data() + 1, 32}; + + auto k = blind25_factor(X, {server_pk.data(), server_pk.size()}); + + uc32 a; + uc32 A; + std::memcpy(A.data(), S.data(), 32); + if (S[31] & 0x80) { + // Ed25519 pubkey is negative, so we need to negate `z` to make things come out right + crypto_core_ed25519_scalar_negate(a.data(), z.data()); + A[31] &= 0x7f; + } else + std::memcpy(a.data(), z.data(), 32); + + // Turn a, A into their blinded versions + crypto_core_ed25519_scalar_mul(a.data(), k.data(), a.data()); + crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + + uc32 seedhash; + crypto_generichash_blake2b( + seedhash.data(), + seedhash.size(), + ed25519_sk.data(), + 32, + hash_key_seed.data(), + hash_key_seed.size()); + + uc64 r_hash; + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, hash_key_sig.data(), hash_key_sig.size(), r_hash.size()); + crypto_generichash_blake2b_update(&st, seedhash.data(), seedhash.size()); + crypto_generichash_blake2b_update(&st, A.data(), A.size()); + crypto_generichash_blake2b_update(&st, message.data(), message.size()); + crypto_generichash_blake2b_final(&st, r_hash.data(), r_hash.size()); + + uc32 r; + crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); + + ustring result; + result.resize(64); + auto* sig_R = result.data(); + auto* sig_S = result.data() + 32; + crypto_scalarmult_ed25519_base_noclamp(sig_R, r.data()); + + crypto_hash_sha512_state st2; + crypto_hash_sha512_init(&st2); + crypto_hash_sha512_update(&st2, sig_R, 32); + crypto_hash_sha512_update(&st2, A.data(), A.size()); + crypto_hash_sha512_update(&st2, message.data(), message.size()); + uc64 hram; + crypto_hash_sha512_final(&st2, hram.data()); + + crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) + + crypto_core_ed25519_scalar_mul(sig_S, sig_S, a.data()); // S = H(R||A||M) a + crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a + + return result; +} + +} // namespace session diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9bef2f19..ce3f0b59 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(Catch2) add_executable(testAll + test_blinding.cpp test_bt_merge.cpp test_bugs.cpp test_compression.cpp diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp new file mode 100644 index 00000000..7aeaea80 --- /dev/null +++ b/tests/test_blinding.cpp @@ -0,0 +1,145 @@ + +#include +#include +#include + +#include + +#include "session/blinding.hpp" +#include "session/util.hpp" + +using namespace std::literals; +using namespace session; + +constexpr std::array seed1{ + 0xfe, 0xcd, 0x9a, 0x60, 0x34, 0xbc, 0x9a, 0xba, 0x27, 0x39, 0x25, 0xde, 0xe7, + 0x06, 0x2b, 0x12, 0x33, 0x34, 0x58, 0x7c, 0x3c, 0x62, 0x57, 0x34, 0x1a, 0xfa, + 0xe2, 0xd7, 0xfe, 0x85, 0xe1, 0x22, 0xf4, 0xef, 0x87, 0x39, 0x08, 0xf6, 0xa5, + 0x37, 0x7b, 0xa3, 0x85, 0x3f, 0x0e, 0x2f, 0xa3, 0x26, 0xee, 0xd9, 0xe7, 0x41, + 0xed, 0xf9, 0xf7, 0xd0, 0x31, 0x1a, 0x3e, 0xcc, 0x66, 0xa5, 0x7b, 0x32}; +constexpr std::array seed2{ + 0x86, 0x59, 0xef, 0xdc, 0xbe, 0x09, 0x49, 0xe0, 0xf8, 0x11, 0x41, 0xe6, 0xd3, + 0x97, 0xe8, 0xbe, 0x75, 0xf4, 0x5d, 0x09, 0x26, 0x2f, 0x20, 0x9d, 0x59, 0x50, + 0xe9, 0x79, 0x89, 0xeb, 0x43, 0xc7, 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, + 0x45, 0x44, 0xc1, 0xc5, 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, + 0xe8, 0xaa, 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x81}; + +constexpr ustring_view pub1{seed1.data() + 32, 32}; +constexpr ustring_view pub2{seed2.data() + 32, 32}; + +constexpr std::array xpub1{ + 0xfe, 0x94, 0xb7, 0xad, 0x4b, 0x7f, 0x1c, 0xc1, 0xbb, 0x92, 0x67, + 0x1f, 0x1f, 0x0d, 0x24, 0x3f, 0x22, 0x6e, 0x11, 0x5b, 0x33, 0x77, + 0x04, 0x65, 0xe8, 0x2b, 0x50, 0x3f, 0xc3, 0xe9, 0x6e, 0x1f, +}; +constexpr std::array xpub2{ + 0x05, 0xc9, 0xa9, 0xbf, 0x17, 0x8f, 0xa6, 0x44, 0xd4, 0x4b, 0xeb, + 0xf6, 0x28, 0x71, 0x6d, 0xc7, 0xf2, 0xdf, 0x3d, 0x08, 0x42, 0xe9, + 0x78, 0x81, 0x96, 0x2c, 0x72, 0x36, 0x99, 0x15, 0x20, 0x73, +}; + +constexpr std::array pub2_abs{ + 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, 0x45, 0x44, 0xc1, 0xc5, + 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, 0xe8, 0xaa, + 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x01, +}; + +const std::string session_id1 = "05" + oxenc::to_hex(xpub1.begin(), xpub1.end()); +const std::string session_id2 = "05" + oxenc::to_hex(xpub2.begin(), xpub2.end()); + +TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") { + REQUIRE(sodium_init() >= 0); + + CHECK(blind25_id( + session_id1, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == + "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); + CHECK(blind25_id( + session_id1, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == + "2598589c7885b56cbeae6ab7b4224f202815520a54995872cb1833b44db6401c8d"); + CHECK(blind25_id( + session_id2, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == + "25a69cc6884530bf8498d22892e563716c4742f2845a7eb608de2aecbe7b6b5996"); +} + +template +ustring_view to_usv(const std::array& val) { + return {val.data(), N}; +} + +TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { + + std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; + auto b25_1 = blind25_id(session_id1, server_pks[0]); + auto b25_2 = blind25_id(session_id1, server_pks[1]); + auto b25_3 = blind25_id(session_id2, server_pks[2]); + auto b25_4 = blind25_id(session_id2, server_pks[3]); + auto b25_5 = blind25_id(session_id2, server_pks[4]); + auto b25_6 = blind25_id(session_id1, server_pks[5]); + + auto sig1 = blind25_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); + CHECK(oxenc::to_hex(sig1) == + "e6c57de4ac0cd278abbeef815bd88b163a037085deae789ecaaf4805884c4c3d3db25f3afa856241366cb341" + "a3a4c9bbaa2cda81d028079c956fab16a7fe6206"); + CHECK(0 == crypto_sign_verify_detached( + sig1.data(), + to_unsigned("hello"), + 5, + to_unsigned(oxenc::from_hex(b25_1).data()) + 1)); + + auto sig2 = blind25_sign(to_usv(seed1), server_pks[1], to_unsigned_sv("world")); + CHECK(oxenc::to_hex(sig2) == + "4460b606e9f55a7cba0bbe24207fe2859c3422783373788b6b070b2fa62ceba4f2a50749a6cee68e095747a3" + "69927f9f4afa86edaf055cad68110e35e8b06607"); + CHECK(0 == crypto_sign_verify_detached( + sig2.data(), + to_unsigned("world"), + 5, + to_unsigned(oxenc::from_hex(b25_2).data()) + 1)); + + auto sig3 = blind25_sign(to_usv(seed2), server_pks[2], to_unsigned_sv("this")); + CHECK(oxenc::to_hex(sig3) == + "57bb2f80c88ce2f677902ee58e02cbd83e4e1ec9e06e1c72a34b4ab76d0f5219cfd141ac5ce7016c73c8382d" + "b99df9f317f2bc0af6ca68edac2a9a7670938902"); + CHECK(0 == crypto_sign_verify_detached( + sig3.data(), + to_unsigned("this"), + 4, + to_unsigned(oxenc::from_hex(b25_3).data()) + 1)); + + auto sig4 = blind25_sign(to_usv(seed2), server_pks[3], to_unsigned_sv("is")); + CHECK(oxenc::to_hex(sig4) == + "ecce032b27b09d2d3d6df4ebab8cae86656c64fd1e3e70d6f020cd7e1a8058c57e3df7b6b01e90ccd592ac4a" + "845dde7a2fdceb1a328a6690686851583133ea0c"); + CHECK(0 == crypto_sign_verify_detached( + sig4.data(), + to_unsigned("is"), + 2, + to_unsigned(oxenc::from_hex(b25_4).data()) + 1)); + + auto sig5 = blind25_sign(to_usv(seed2), server_pks[4], to_unsigned_sv("")); + CHECK(oxenc::to_hex(sig5) == + "bf2fb9a511adbf5827e2e3bcf09f0a1cff80f85556fb76d8001aa8483b5f22e14539b170eaa0dbfa1489d1b8" + "618ce8b48d7512cb5602c7eb8a05ce330a68350b"); + CHECK(0 == + crypto_sign_verify_detached( + sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b25_5).data()) + 1)); + + auto sig6 = blind25_sign(to_usv(seed1), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6) == + "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" + "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); + CHECK(0 == crypto_sign_verify_detached( + sig6.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); +} From 02a14d0c50733870fc4ac46437b6627576542d95 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 29 Sep 2023 17:52:24 -0300 Subject: [PATCH 079/572] Bump clang ci jobs to v16 --- .drone.jsonnet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 385af530..bd647b76 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -300,8 +300,8 @@ local static_build(name, // Various debian builds debian_build('Debian sid (amd64)', docker_base + 'debian-sid'), debian_build('Debian sid/Debug (amd64)', docker_base + 'debian-sid', build_type='Debug'), - clang(14), - full_llvm(14), + clang(16), + full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), debian_build('Debian buster (amd64)', docker_base + 'debian-buster'), debian_build('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'), From 2d2011e4bd4c2149fb426c381fd048a79249b80a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 29 Sep 2023 17:52:42 -0300 Subject: [PATCH 080/572] Fix bionic build (bionic doesn't have nlohmann-json3-dev) --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index bd647b76..b89162f0 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -308,7 +308,7 @@ local static_build(name, debian_build('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic (amd64)', docker_base + 'ubuntu-bionic', - deps=['g++-8'] + default_deps_nocxx, + deps=['g++-8'], kitware_repo='bionic', cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), From 83f73563299bfb4a1d733a65642f34e73459a0fe Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 2 Oct 2023 12:14:34 -0300 Subject: [PATCH 081/572] Allow blind25_sign to take just the seed Sometimes this is more convenient from calling code. --- include/session/blinding.hpp | 7 +++++-- src/blinding.cpp | 8 +++++++- tests/test_blinding.cpp | 11 +++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index b56d5893..aa04e5cd 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -67,8 +67,11 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk); /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). /// -/// Takes the Ed25519 secret key (64 bytes) and the server pubkey (in hex (64 digits) or bytes -/// (32 bytes)). Returns the 64-byte signature. +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); } // namespace session diff --git a/src/blinding.cpp b/src/blinding.cpp index 1048ec1e..c9529370 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -69,9 +69,15 @@ static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } if (ed25519_sk.size() != 64) throw std::invalid_argument{ - "blind25_sign: Invalid ed25519_sk is not the expected 64 bytes"}; + "blind25_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; uc32 server_pk; if (server_pk_in.size() == 32) std::memcpy(server_pk.data(), server_pk_in.data(), 32); diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 7aeaea80..3abd96c2 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -142,4 +142,15 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { to_unsigned("omg!"), 4, to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); + + // Test that it works when given just the seed instead of the whole sk: + auto sig6b = blind25_sign(to_usv(seed1).substr(0, 32), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6b) == + "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" + "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); + CHECK(0 == crypto_sign_verify_detached( + sig6b.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); } From 1b928507455e7737e68c53a530c65b15809d4f05 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 2 Oct 2023 16:12:21 -0300 Subject: [PATCH 082/572] Overload blind25_id with byte-accepting version Avoids unnecessary conversions to/from hex when we start with bytes and want bytes (i.e. `bytes->bytes` instead of `bytes->hex->bytes->bytes->hex->bytes`). --- include/session/blinding.hpp | 5 ++++ include/session/util.hpp | 4 +++ src/blinding.cpp | 50 ++++++++++++++++++++++++++---------- tests/test_blinding.cpp | 16 +++++++++--- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index aa04e5cd..164621d5 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -64,6 +64,11 @@ std::array blind25_factor(ustring_view session_id, ustring_vi /// returned are hex-encoded. std::string blind25_id(std::string_view session_id, std::string_view server_pk); +/// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a +/// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may +/// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). +ustring blind25_id(ustring_view session_id, ustring_view server_pk); + /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). /// diff --git a/include/session/util.hpp b/include/session/util.hpp index 322756c9..49163bc2 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -38,6 +38,10 @@ template inline std::string_view from_unsigned_sv(const std::vector& v) { return {from_unsigned(v.data()), v.size()}; } +template +inline std::basic_string_view to_sv(const std::array& v) { + return {v.data(), N}; +} /// 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) { diff --git a/src/blinding.cpp b/src/blinding.cpp index c9529370..b9783895 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -39,6 +39,36 @@ std::array blind25_factor(ustring_view session_id, ustring_vi return k; } +namespace { + + void blind25_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { + auto k = blind25_factor(session_id, server_pk); + if (session_id.size() == 33) + session_id.remove_prefix(1); + auto ed_pk = xed25519::pubkey(session_id); + if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + out[0] = 0x25; + } + +} // namespace + +ustring blind25_id(ustring_view session_id, ustring_view server_pk) { + if (session_id.size() == 33) { + if (session_id[0] != 0x05) + throw std::invalid_argument{"blind25_id: session_id must start with 0x05"}; + } else if (session_id.size() != 32) { + throw std::invalid_argument{"blind25_id: session_id must be 32 or 33 bytes"}; + } + if (server_pk.size() != 32) + throw std::invalid_argument{"blind25_id: server_pk must be 32 bytes"}; + + ustring result; + result.resize(33); + blind25_id_impl(session_id, server_pk, result.data()); + return result; +} + std::string blind25_id(std::string_view session_id, std::string_view server_pk) { if (session_id.size() != 66 || !oxenc::is_hex(session_id)) throw std::invalid_argument{"blind25_id: session_id must be hex (66 digits)"}; @@ -49,20 +79,12 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) uc33 raw_sid; oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); - ustring_view sid_bytes{raw_sid.data(), raw_sid.size()}; - auto k = blind25_factor(sid_bytes, to_unsigned_sv(oxenc::from_hex(server_pk))); - - auto ed_pk = xed25519::pubkey(sid_bytes.substr(1)); - uc32 blinded_pk; - if (0 != crypto_scalarmult_ed25519_noclamp(blinded_pk.data(), k.data(), ed_pk.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - - std::string result; - result.resize(66); - result[0] = '2'; - result[1] = '5'; - oxenc::to_hex(blinded_pk.begin(), blinded_pk.end(), result.begin() + 2); - return result; + uc32 raw_server_pk; + oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); + + uc33 blinded; + blind25_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); + return oxenc::to_hex(blinded.begin(), blinded.end()); } static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 3abd96c2..b8d8bd51 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -4,9 +4,11 @@ #include #include +#include #include "session/blinding.hpp" #include "session/util.hpp" +#include "utils.hpp" using namespace std::literals; using namespace session; @@ -62,11 +64,17 @@ TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") session_id2, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == "25a69cc6884530bf8498d22892e563716c4742f2845a7eb608de2aecbe7b6b5996"); -} -template -ustring_view to_usv(const std::array& val) { - return {val.data(), N}; + ustring session_id1_raw; + oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); + CHECK(oxenc::to_hex(blind25_id( + session_id1_raw, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); + CHECK(oxenc::to_hex(blind25_id( + session_id1_raw.substr(1), + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); } TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { From cda4d32e2672e9f8dd4ca1822b0cc3e0c5005497 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 2 Oct 2023 21:42:23 -0300 Subject: [PATCH 083/572] Add (and test) missing member::set_name function --- src/config/groups/members.cpp | 6 ++++++ tests/test_group_members.cpp | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 9d330d7f..18b4ece1 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -157,6 +157,12 @@ void member::into(config_group_member& m) const { m.promoted = promotion_status; } +void member::set_name(std::string n) { + if (n.size() > MAX_NAME_LENGTH) + throw std::invalid_argument{"Invalid member name: exceeds maximum length"}; + name = std::move(n); +} + } // namespace session::config::groups using namespace session; diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index e78d6723..d990ab61 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -81,7 +81,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // 10 members: for (int i = 10; i < 20; i++) { auto m = gmem1.get_or_construct(sids[i]); - m.name = "Member " + std::to_string(i); + m.set_name("Member " + std::to_string(i)); m.profile_picture.url = "http://example.com/" + std::to_string(i); m.profile_picture.key = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; @@ -93,6 +93,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.set(m); } + REQUIRE_THROWS(gmem1.get(sids[14])->set_name(std::string(200, 'c'))); + CHECK(gmem1.needs_push()); auto [s1, p1, o1] = gmem1.push(); CHECK(p1.size() == 768); From 2173e4d7e9631ac5a4a1f0bca47afba5a36f0cf1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 19 Sep 2023 22:17:41 -0300 Subject: [PATCH 084/572] Add wine to CI deps We use wine to test the windows CI builds --- .drone.jsonnet | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index b89162f0..3d3dff0f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -39,7 +39,7 @@ local debian_pipeline(name, image: image, pull: 'always', [if allow_fail then 'failure']: 'ignore', - environment: { SSH_KEY: { from_secret: 'SSH_KEY' } }, + environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, WINEDEBUG: '-all' }, commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', 'echo "man-db man-db/auto-update boolean false" | debconf-set-selections', @@ -131,6 +131,7 @@ local windows_cross_pipeline(name, allow_fail=allow_fail, deps=[ 'g++-mingw-w64-' + winarch + '-posix', + 'wine', ], build=[ 'mkdir build', From 08f33be2669a1dead548fa3e66459c8bf25a602e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 7 Sep 2023 17:12:08 -0300 Subject: [PATCH 085/572] Turn on test timing reporting for ci test suite --- .drone.jsonnet | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 3d3dff0f..8e45544d 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -108,7 +108,7 @@ local debian_build(name, [if allow_fail then 'failure']: 'ignore', commands: [ 'cd build', - './tests/testAll --colour-mode ansi', + './tests/testAll --colour-mode ansi -d yes', ], }] else []) ); @@ -154,7 +154,7 @@ local windows_cross_pipeline(name, commands: [ apt_get_quiet + ' install -y --no-install-recommends wine64', 'cd build', - 'wine-stable ./tests/testAll.exe --colour-mode ansi', + 'wine-stable ./tests/testAll.exe --colour-mode ansi -d yes', ], }] else []) ); @@ -229,7 +229,7 @@ local mac_builder(name, [if allow_fail then 'failure']: 'ignore', commands: [ 'cd build', - './tests/testAll --colour-mode ansi', + './tests/testAll --colour-mode ansi -d yes', ], }] else [])); From f2832cfbb75a1162fa01852e8aa9513c7be60de0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 7 Sep 2023 17:04:28 -0300 Subject: [PATCH 086/572] Disable -Werror for stringop-overflow It's stupidly difficult to avoid false positives (and protobuf triggers it). --- src/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bf1a6b76..ab8aa784 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,15 @@ target_link_libraries(common INTERFACE target_include_directories(common INTERFACE ../include) if(WARNINGS_AS_ERRORS) target_compile_options(common INTERFACE -Werror) + message(STATUS "Compiling with fatal warnings (-Werror)") + if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND + CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 11 AND + CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14) + # -Wstringop-overflow triggers (falsely) in protobuf in GCC 11/12/13 so disable it there + # (fingers crossed for 14). + message(STATUS "Disabling -Werror for buggy GCC stringop-overflow") + target_compile_options(common INTERFACE -Wno-error=stringop-overflow) + endif() endif() set(export_targets) From 43384d7f92870e888bbfe15d0e2fbbaadfcbee21 Mon Sep 17 00:00:00 2001 From: dr7ana Date: Fri, 1 Sep 2023 12:55:20 -0700 Subject: [PATCH 087/572] Protobuf integration & config wrapping/unwrapping This adds support for automatic wrapping and unwrapping of config messages that may be wrapped in protobuf; this was unintentional, but is out in the wild in current Session releases. The long-term plan is to stop doing this eventually, but supporting it here allows us to change from the current mode (always wrap/unwrap affected message types) to a mode where we only unwrap, and then finally we can drop unwrapping as well. For now: we wrap/unwrap the existing released message types (user profile, conversation info, and so on) but not the new groups configs (which should always be un-wrapped). Various notes: - protobuf added as external dependency. - protobuf 21.12 is used, which is an older release, but is what is still being used by lots of projects and Linux distributions because Google applied huge amounts of extra bloat in v22 (including pulling a massive pile of crap in abseil), broke the build system in various ways, and made everything suck to the point that many Linux distributions have just stuck with the latest 21.12 because of all the broken crap Google imposed on everyone in v22 and newer. - SessionProtos.pro and WebSocketResources.proto looped in from session-ios codebase, with one minor change to let us use it in "lite" mode (which is still very bloated, but nothing like non-lite mode). - Added pregenerated header/cpp files; we experimented with building them as part of the build, but that ended up neigh impossible for our various cross-compiled builds. (These also get excluded from format.sh because they break if formatted. Hurray.) - Add protobuf::libprotobuf-lite to the static build list. - Adds a dummy test script that does nothing, but tests that we can probably link against the combined archive (when building with `-DSTATIC_BUILD=ON`). --- .drone.jsonnet | 3 +- CMakeLists.txt | 3 +- include/session/config/base.hpp | 68 +- include/session/config/contacts.hpp | 2 + .../session/config/convo_info_volatile.hpp | 2 + include/session/config/user_groups.hpp | 2 + include/session/config/user_profile.hpp | 2 + include/session/protos.hpp | 16 + include/session/util.hpp | 6 + proto/CMakeLists.txt | 41 + proto/SessionProtos.pb.cc | 10111 +++++++++++++ proto/SessionProtos.pb.h | 12309 ++++++++++++++++ proto/SessionProtos.proto | 292 + proto/WebSocketResources.pb.cc | 1223 ++ proto/WebSocketResources.pb.h | 1567 ++ proto/WebSocketResources.proto | 51 + src/CMakeLists.txt | 18 + src/config/base.cpp | 45 +- src/config/contacts.cpp | 1 + src/protos.cpp | 105 + tests/CMakeLists.txt | 11 +- tests/static_bundle.cpp | 12 + tests/test_bugs.cpp | 3 +- tests/test_config_contacts.cpp | 4 +- tests/test_config_userprofile.cpp | 17 +- tests/test_proto.cpp | 69 + utils/format.sh | 2 +- 27 files changed, 25941 insertions(+), 44 deletions(-) create mode 100644 include/session/protos.hpp create mode 100644 proto/CMakeLists.txt create mode 100644 proto/SessionProtos.pb.cc create mode 100644 proto/SessionProtos.pb.h create mode 100644 proto/SessionProtos.proto create mode 100644 proto/WebSocketResources.pb.cc create mode 100644 proto/WebSocketResources.pb.h create mode 100644 proto/WebSocketResources.proto create mode 100644 src/protos.cpp create mode 100644 tests/static_bundle.cpp create mode 100644 tests/test_proto.cpp diff --git a/.drone.jsonnet b/.drone.jsonnet index 8e45544d..5b574798 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -61,7 +61,7 @@ local debian_pipeline(name, ] else [] ) + [ 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y cmake make git ccache ' + std.join(' ', deps), + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y cmake make git ccache ca-certificates ' + std.join(' ', deps), ] + build, }, ] + extra_steps, @@ -304,7 +304,6 @@ local static_build(name, clang(16), full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), - debian_build('Debian buster (amd64)', docker_base + 'debian-buster'), debian_build('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic (amd64)', diff --git a/CMakeLists.txt b/CMakeLists.txt index d46a2740..17286af9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.13...3.23) +cmake_minimum_required(VERSION 3.14...3.23) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -116,6 +116,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(external) add_subdirectory(src) +add_subdirectory(proto) if (BUILD_STATIC_DEPS) include(StaticBuild) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 9e0803a8..e1775dc4 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -200,6 +200,12 @@ class ConfigBase : public ConfigSig { void set_verifier(ConfigMessage::verify_callable v) override; void set_signer(ConfigMessage::sign_callable s) override; + // Virtual method to be overloaded by derived classes. Protobuf wrapped messages are used + // for legacy types, so we need different logic depending on the class in question. All new + // types will reject the protobuf and directly handle the binary data. Old types will try + // protobuf parsing on incoming messages and handle all outgoing messages directly in binary + virtual bool accepts_protobuf() const { return false; } + public: // class for proxying subfield access; this class should never be stored but only used // ephemerally (most of its methods are rvalue-qualified). This lets constructs such as @@ -745,6 +751,36 @@ class ConfigBase : public ConfigSig { }; protected: + /// API: base/ConfigBase::_merge + /// + /// Internal implementation of 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. + /// + /// 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 + /// int _merge(const std::vector>& configs); + /// int _merge(const std::vector>& configs); + /// ``` + /// + /// Inputs: + /// - `configs` -- vector of pairs containing the message hash and the raw message body + /// + /// Outputs: + /// - `int` -- Returns how many config messages that were successfully parsed + int _merge(const std::vector>& configs); + /// API: base/ConfigBase::extra_data /// /// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute @@ -849,37 +885,23 @@ class ConfigBase : public ConfigSig { /// 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. - /// - /// 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 - /// int merge(const std::vector>& configs); - /// int merge(const std::vector>& configs); - /// ``` + /// Wrapper around ConfigBase::_merge to handle protobuf parsing. Currently, in the transition + /// from legacy to new config groups, legacy groups using protobuf must be compatible with + /// new config groups handling raw binary I/O. As a result, this method checks the + /// accepts_protobuf() override for the given class, and either passes the parsed protobuf or + /// the original binary data to _merge /// /// Inputs: /// - `configs` -- vector of pairs containing the message hash and the raw message body /// /// Outputs: /// - `int` -- Returns how many config messages that were successfully parsed - virtual int merge(const std::vector>& configs); - - // Same as merge (above )but takes the values as ustring's as sometimes that is more convenient. int merge(const std::vector>& configs); + // Same as above, but passes the ustring_views to the overload of protos::handle_incoming that + // takes ustring_views + int merge(const std::vector>& configs); + /// API: base/ConfigBase::is_dirty /// /// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 1cad126a..7ac4b237 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -323,6 +323,8 @@ class Contacts : public ConfigBase { /// - `bool` - Returns true if the contact list is empty bool empty() const { return size() == 0; } + bool accepts_protobuf() const override { return true; } + struct iterator; /// API: contacts/contacts::begin /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 28b3e822..24c3605c 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -529,6 +529,8 @@ class ConvoInfoVolatile : public ConfigBase { /// - `bool` -- Returns true if the convesation list is empty bool empty() const { return size() == 0; } + bool accepts_protobuf() const override { return true; } + struct iterator; /// API: convo_info_volatile/ConvoInfoVolatile::begin /// diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 5187af1a..d35855dd 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -561,6 +561,8 @@ class UserGroups : public ConfigBase { /// - `bool` - Returns true if the contact list is empty bool empty() const { return size() == 0; } + bool accepts_protobuf() const override { return true; } + struct iterator; /// API: user_groups/UserGroups::begin /// diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index e077b40a..d99f19e9 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -187,6 +187,8 @@ class UserProfile final : public ConfigBase { /// not, and `std::nullopt` to drop the setting from the config (and thus use the client's /// default). void set_blinded_msgreqs(std::optional enabled); + + bool accepts_protobuf() const override { return true; } }; } // namespace session::config diff --git a/include/session/protos.hpp b/include/session/protos.hpp new file mode 100644 index 00000000..6d5f219f --- /dev/null +++ b/include/session/protos.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "session/config/namespaces.hpp" +#include "session/util.hpp" + +namespace session::protos { + +ustring handle_incoming(ustring_view data); + +ustring handle_incoming(ustring data); + +ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t); + +ustring handle_outgoing(ustring data, int64_t seqno, config::Namespace t); + +} // namespace session::protos diff --git a/include/session/util.hpp b/include/session/util.hpp index 49163bc2..5e906a6a 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,5 +1,7 @@ #pragma once + #include +#include #include #include #include @@ -43,6 +45,10 @@ inline std::basic_string_view to_sv(const std::array& v) { return {v.data(), N}; } +inline uint64_t get_timestamp() { + return std::chrono::steady_clock::now().time_since_epoch().count(); +} + /// 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/proto/CMakeLists.txt b/proto/CMakeLists.txt new file mode 100644 index 00000000..25630a72 --- /dev/null +++ b/proto/CMakeLists.txt @@ -0,0 +1,41 @@ + +function(check_target target) + if (NOT TARGET ${target}) + message(FATAL_ERROR "Project failed to compile required target: ${target}") + endif() +endfunction() + +include(FetchContent) + +FetchContent_Declare( + protobuf + GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git + GIT_TAG v3.21.12 # apparently this must be a tag (not hash) for git_shallow to work? + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) + +set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) +set(protobuf_INSTALL ON CACHE BOOL "" FORCE) +set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) +set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") +set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) + +message(STATUS "Pulling protobuf repository...") + +FetchContent_MakeAvailable(protobuf) + +check_target(protobuf::libprotobuf-lite) + +libsession_static_bundle(protobuf::libprotobuf-lite) + + +add_custom_target(regen-protobuf + protoc --cpp_out=. SessionProtos.proto WebSocketResources.proto + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" +) diff --git a/proto/SessionProtos.pb.cc b/proto/SessionProtos.pb.cc new file mode 100644 index 00000000..cd353a0f --- /dev/null +++ b/proto/SessionProtos.pb.cc @@ -0,0 +1,10111 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: SessionProtos.proto + +#include "SessionProtos.pb.h" + +#include + +#include +#include +#include +#include +// @@protoc_insertion_point(includes) +#include + +PROTOBUF_PRAGMA_INIT_SEG + +namespace _pb = ::PROTOBUF_NAMESPACE_ID; +namespace _pbi = _pb::internal; + +namespace SessionProtos { +PROTOBUF_CONSTEXPR Envelope::Envelope( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.source_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.content_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u} + , /*decltype(_impl_.servertimestamp_)*/uint64_t{0u} + , /*decltype(_impl_.sourcedevice_)*/0u + , /*decltype(_impl_.type_)*/6} {} +struct EnvelopeDefaultTypeInternal { + PROTOBUF_CONSTEXPR EnvelopeDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~EnvelopeDefaultTypeInternal() {} + union { + Envelope _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 EnvelopeDefaultTypeInternal _Envelope_default_instance_; +PROTOBUF_CONSTEXPR TypingMessage::TypingMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u} + , /*decltype(_impl_.action_)*/0} {} +struct TypingMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR TypingMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~TypingMessageDefaultTypeInternal() {} + union { + TypingMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 TypingMessageDefaultTypeInternal _TypingMessage_default_instance_; +PROTOBUF_CONSTEXPR UnsendRequest::UnsendRequest( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.author_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u}} {} +struct UnsendRequestDefaultTypeInternal { + PROTOBUF_CONSTEXPR UnsendRequestDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~UnsendRequestDefaultTypeInternal() {} + union { + UnsendRequest _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 UnsendRequestDefaultTypeInternal _UnsendRequest_default_instance_; +PROTOBUF_CONSTEXPR MessageRequestResponse::MessageRequestResponse( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.profilekey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profile_)*/nullptr + , /*decltype(_impl_.isapproved_)*/false} {} +struct MessageRequestResponseDefaultTypeInternal { + PROTOBUF_CONSTEXPR MessageRequestResponseDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~MessageRequestResponseDefaultTypeInternal() {} + union { + MessageRequestResponse _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 MessageRequestResponseDefaultTypeInternal _MessageRequestResponse_default_instance_; +PROTOBUF_CONSTEXPR Content::Content( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.datamessage_)*/nullptr + , /*decltype(_impl_.callmessage_)*/nullptr + , /*decltype(_impl_.receiptmessage_)*/nullptr + , /*decltype(_impl_.typingmessage_)*/nullptr + , /*decltype(_impl_.configurationmessage_)*/nullptr + , /*decltype(_impl_.dataextractionnotification_)*/nullptr + , /*decltype(_impl_.unsendrequest_)*/nullptr + , /*decltype(_impl_.messagerequestresponse_)*/nullptr + , /*decltype(_impl_.sharedconfigmessage_)*/nullptr} {} +struct ContentDefaultTypeInternal { + PROTOBUF_CONSTEXPR ContentDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~ContentDefaultTypeInternal() {} + union { + Content _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 ContentDefaultTypeInternal _Content_default_instance_; +PROTOBUF_CONSTEXPR CallMessage::CallMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.sdps_)*/{} + , /*decltype(_impl_.sdpmlineindexes_)*/{} + , /*decltype(_impl_.sdpmids_)*/{} + , /*decltype(_impl_.uuid_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.type_)*/6} {} +struct CallMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR CallMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~CallMessageDefaultTypeInternal() {} + union { + CallMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 CallMessageDefaultTypeInternal _CallMessage_default_instance_; +PROTOBUF_CONSTEXPR KeyPair::KeyPair( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.publickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.privatekey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}} {} +struct KeyPairDefaultTypeInternal { + PROTOBUF_CONSTEXPR KeyPairDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~KeyPairDefaultTypeInternal() {} + union { + KeyPair _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 KeyPairDefaultTypeInternal _KeyPair_default_instance_; +PROTOBUF_CONSTEXPR DataExtractionNotification::DataExtractionNotification( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u} + , /*decltype(_impl_.type_)*/1} {} +struct DataExtractionNotificationDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataExtractionNotificationDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataExtractionNotificationDefaultTypeInternal() {} + union { + DataExtractionNotification _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataExtractionNotificationDefaultTypeInternal _DataExtractionNotification_default_instance_; +PROTOBUF_CONSTEXPR LokiProfile::LokiProfile( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.displayname_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilepicture_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}} {} +struct LokiProfileDefaultTypeInternal { + PROTOBUF_CONSTEXPR LokiProfileDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~LokiProfileDefaultTypeInternal() {} + union { + LokiProfile _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 LokiProfileDefaultTypeInternal _LokiProfile_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_Quote_QuotedAttachment::DataMessage_Quote_QuotedAttachment( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.contenttype_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.filename_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.thumbnail_)*/nullptr + , /*decltype(_impl_.flags_)*/0u} {} +struct DataMessage_Quote_QuotedAttachmentDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_Quote_QuotedAttachmentDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_Quote_QuotedAttachmentDefaultTypeInternal() {} + union { + DataMessage_Quote_QuotedAttachment _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_Quote_QuotedAttachmentDefaultTypeInternal _DataMessage_Quote_QuotedAttachment_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_Quote::DataMessage_Quote( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.attachments_)*/{} + , /*decltype(_impl_.author_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.text_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.id_)*/uint64_t{0u}} {} +struct DataMessage_QuoteDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_QuoteDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_QuoteDefaultTypeInternal() {} + union { + DataMessage_Quote _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_QuoteDefaultTypeInternal _DataMessage_Quote_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_Preview::DataMessage_Preview( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.url_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.title_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.image_)*/nullptr} {} +struct DataMessage_PreviewDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_PreviewDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_PreviewDefaultTypeInternal() {} + union { + DataMessage_Preview _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_PreviewDefaultTypeInternal _DataMessage_Preview_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_Reaction::DataMessage_Reaction( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.author_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.emoji_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.id_)*/uint64_t{0u} + , /*decltype(_impl_.action_)*/0} {} +struct DataMessage_ReactionDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_ReactionDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_ReactionDefaultTypeInternal() {} + union { + DataMessage_Reaction _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_ReactionDefaultTypeInternal _DataMessage_Reaction_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_OpenGroupInvitation::DataMessage_OpenGroupInvitation( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.url_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.name_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}} {} +struct DataMessage_OpenGroupInvitationDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_OpenGroupInvitationDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_OpenGroupInvitationDefaultTypeInternal() {} + union { + DataMessage_OpenGroupInvitation _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_OpenGroupInvitationDefaultTypeInternal _DataMessage_OpenGroupInvitation_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessage_KeyPairWrapper::DataMessage_ClosedGroupControlMessage_KeyPairWrapper( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.publickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.encryptedkeypair_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}} {} +struct DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal() {} + union { + DataMessage_ClosedGroupControlMessage_KeyPairWrapper _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal _DataMessage_ClosedGroupControlMessage_KeyPairWrapper_default_instance_; +PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessage::DataMessage_ClosedGroupControlMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.members_)*/{} + , /*decltype(_impl_.admins_)*/{} + , /*decltype(_impl_.wrappers_)*/{} + , /*decltype(_impl_.publickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.name_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.encryptionkeypair_)*/nullptr + , /*decltype(_impl_.expirationtimer_)*/0u + , /*decltype(_impl_.type_)*/1} {} +struct DataMessage_ClosedGroupControlMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessage_ClosedGroupControlMessageDefaultTypeInternal() {} + union { + DataMessage_ClosedGroupControlMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessage_ClosedGroupControlMessageDefaultTypeInternal _DataMessage_ClosedGroupControlMessage_default_instance_; +PROTOBUF_CONSTEXPR DataMessage::DataMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.attachments_)*/{} + , /*decltype(_impl_.preview_)*/{} + , /*decltype(_impl_.body_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilekey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.synctarget_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.quote_)*/nullptr + , /*decltype(_impl_.reaction_)*/nullptr + , /*decltype(_impl_.profile_)*/nullptr + , /*decltype(_impl_.opengroupinvitation_)*/nullptr + , /*decltype(_impl_.closedgroupcontrolmessage_)*/nullptr + , /*decltype(_impl_.flags_)*/0u + , /*decltype(_impl_.expiretimer_)*/0u + , /*decltype(_impl_.timestamp_)*/uint64_t{0u} + , /*decltype(_impl_.blockscommunitymessagerequests_)*/false} {} +struct DataMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR DataMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~DataMessageDefaultTypeInternal() {} + union { + DataMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DataMessageDefaultTypeInternal _DataMessage_default_instance_; +PROTOBUF_CONSTEXPR ConfigurationMessage_ClosedGroup::ConfigurationMessage_ClosedGroup( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.members_)*/{} + , /*decltype(_impl_.admins_)*/{} + , /*decltype(_impl_.publickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.name_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.encryptionkeypair_)*/nullptr + , /*decltype(_impl_.expirationtimer_)*/0u} {} +struct ConfigurationMessage_ClosedGroupDefaultTypeInternal { + PROTOBUF_CONSTEXPR ConfigurationMessage_ClosedGroupDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~ConfigurationMessage_ClosedGroupDefaultTypeInternal() {} + union { + ConfigurationMessage_ClosedGroup _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 ConfigurationMessage_ClosedGroupDefaultTypeInternal _ConfigurationMessage_ClosedGroup_default_instance_; +PROTOBUF_CONSTEXPR ConfigurationMessage_Contact::ConfigurationMessage_Contact( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.publickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.name_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilepicture_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilekey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.isapproved_)*/false + , /*decltype(_impl_.isblocked_)*/false + , /*decltype(_impl_.didapproveme_)*/false} {} +struct ConfigurationMessage_ContactDefaultTypeInternal { + PROTOBUF_CONSTEXPR ConfigurationMessage_ContactDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~ConfigurationMessage_ContactDefaultTypeInternal() {} + union { + ConfigurationMessage_Contact _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 ConfigurationMessage_ContactDefaultTypeInternal _ConfigurationMessage_Contact_default_instance_; +PROTOBUF_CONSTEXPR ConfigurationMessage::ConfigurationMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.closedgroups_)*/{} + , /*decltype(_impl_.opengroups_)*/{} + , /*decltype(_impl_.contacts_)*/{} + , /*decltype(_impl_.displayname_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilepicture_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.profilekey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}} {} +struct ConfigurationMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR ConfigurationMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~ConfigurationMessageDefaultTypeInternal() {} + union { + ConfigurationMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 ConfigurationMessageDefaultTypeInternal _ConfigurationMessage_default_instance_; +PROTOBUF_CONSTEXPR ReceiptMessage::ReceiptMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.timestamp_)*/{} + , /*decltype(_impl_.type_)*/0} {} +struct ReceiptMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR ReceiptMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~ReceiptMessageDefaultTypeInternal() {} + union { + ReceiptMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 ReceiptMessageDefaultTypeInternal _ReceiptMessage_default_instance_; +PROTOBUF_CONSTEXPR AttachmentPointer::AttachmentPointer( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.contenttype_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.key_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.thumbnail_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.digest_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.filename_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.caption_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.url_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.id_)*/uint64_t{0u} + , /*decltype(_impl_.size_)*/0u + , /*decltype(_impl_.flags_)*/0u + , /*decltype(_impl_.width_)*/0u + , /*decltype(_impl_.height_)*/0u} {} +struct AttachmentPointerDefaultTypeInternal { + PROTOBUF_CONSTEXPR AttachmentPointerDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~AttachmentPointerDefaultTypeInternal() {} + union { + AttachmentPointer _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 AttachmentPointerDefaultTypeInternal _AttachmentPointer_default_instance_; +PROTOBUF_CONSTEXPR SharedConfigMessage::SharedConfigMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.data_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.seqno_)*/int64_t{0} + , /*decltype(_impl_.kind_)*/1} {} +struct SharedConfigMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR SharedConfigMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~SharedConfigMessageDefaultTypeInternal() {} + union { + SharedConfigMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 SharedConfigMessageDefaultTypeInternal _SharedConfigMessage_default_instance_; +} // namespace SessionProtos +namespace SessionProtos { +bool Envelope_Type_IsValid(int value) { + switch (value) { + case 6: + case 7: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed Envelope_Type_strings[2] = {}; + +static const char Envelope_Type_names[] = + "CLOSED_GROUP_MESSAGE" + "SESSION_MESSAGE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry Envelope_Type_entries[] = { + { {Envelope_Type_names + 0, 20}, 7 }, + { {Envelope_Type_names + 20, 15}, 6 }, +}; + +static const int Envelope_Type_entries_by_number[] = { + 1, // 6 -> SESSION_MESSAGE + 0, // 7 -> CLOSED_GROUP_MESSAGE +}; + +const std::string& Envelope_Type_Name( + Envelope_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + Envelope_Type_entries, + Envelope_Type_entries_by_number, + 2, Envelope_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + Envelope_Type_entries, + Envelope_Type_entries_by_number, + 2, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + Envelope_Type_strings[idx].get(); +} +bool Envelope_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, Envelope_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + Envelope_Type_entries, 2, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr Envelope_Type Envelope::SESSION_MESSAGE; +constexpr Envelope_Type Envelope::CLOSED_GROUP_MESSAGE; +constexpr Envelope_Type Envelope::Type_MIN; +constexpr Envelope_Type Envelope::Type_MAX; +constexpr int Envelope::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool TypingMessage_Action_IsValid(int value) { + switch (value) { + case 0: + case 1: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed TypingMessage_Action_strings[2] = {}; + +static const char TypingMessage_Action_names[] = + "STARTED" + "STOPPED"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry TypingMessage_Action_entries[] = { + { {TypingMessage_Action_names + 0, 7}, 0 }, + { {TypingMessage_Action_names + 7, 7}, 1 }, +}; + +static const int TypingMessage_Action_entries_by_number[] = { + 0, // 0 -> STARTED + 1, // 1 -> STOPPED +}; + +const std::string& TypingMessage_Action_Name( + TypingMessage_Action value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + TypingMessage_Action_entries, + TypingMessage_Action_entries_by_number, + 2, TypingMessage_Action_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + TypingMessage_Action_entries, + TypingMessage_Action_entries_by_number, + 2, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + TypingMessage_Action_strings[idx].get(); +} +bool TypingMessage_Action_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, TypingMessage_Action* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + TypingMessage_Action_entries, 2, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr TypingMessage_Action TypingMessage::STARTED; +constexpr TypingMessage_Action TypingMessage::STOPPED; +constexpr TypingMessage_Action TypingMessage::Action_MIN; +constexpr TypingMessage_Action TypingMessage::Action_MAX; +constexpr int TypingMessage::Action_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool CallMessage_Type_IsValid(int value) { + switch (value) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed CallMessage_Type_strings[6] = {}; + +static const char CallMessage_Type_names[] = + "ANSWER" + "END_CALL" + "ICE_CANDIDATES" + "OFFER" + "PRE_OFFER" + "PROVISIONAL_ANSWER"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry CallMessage_Type_entries[] = { + { {CallMessage_Type_names + 0, 6}, 2 }, + { {CallMessage_Type_names + 6, 8}, 5 }, + { {CallMessage_Type_names + 14, 14}, 4 }, + { {CallMessage_Type_names + 28, 5}, 1 }, + { {CallMessage_Type_names + 33, 9}, 6 }, + { {CallMessage_Type_names + 42, 18}, 3 }, +}; + +static const int CallMessage_Type_entries_by_number[] = { + 3, // 1 -> OFFER + 0, // 2 -> ANSWER + 5, // 3 -> PROVISIONAL_ANSWER + 2, // 4 -> ICE_CANDIDATES + 1, // 5 -> END_CALL + 4, // 6 -> PRE_OFFER +}; + +const std::string& CallMessage_Type_Name( + CallMessage_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + CallMessage_Type_entries, + CallMessage_Type_entries_by_number, + 6, CallMessage_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + CallMessage_Type_entries, + CallMessage_Type_entries_by_number, + 6, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + CallMessage_Type_strings[idx].get(); +} +bool CallMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, CallMessage_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + CallMessage_Type_entries, 6, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr CallMessage_Type CallMessage::PRE_OFFER; +constexpr CallMessage_Type CallMessage::OFFER; +constexpr CallMessage_Type CallMessage::ANSWER; +constexpr CallMessage_Type CallMessage::PROVISIONAL_ANSWER; +constexpr CallMessage_Type CallMessage::ICE_CANDIDATES; +constexpr CallMessage_Type CallMessage::END_CALL; +constexpr CallMessage_Type CallMessage::Type_MIN; +constexpr CallMessage_Type CallMessage::Type_MAX; +constexpr int CallMessage::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool DataExtractionNotification_Type_IsValid(int value) { + switch (value) { + case 1: + case 2: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed DataExtractionNotification_Type_strings[2] = {}; + +static const char DataExtractionNotification_Type_names[] = + "MEDIA_SAVED" + "SCREENSHOT"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry DataExtractionNotification_Type_entries[] = { + { {DataExtractionNotification_Type_names + 0, 11}, 2 }, + { {DataExtractionNotification_Type_names + 11, 10}, 1 }, +}; + +static const int DataExtractionNotification_Type_entries_by_number[] = { + 1, // 1 -> SCREENSHOT + 0, // 2 -> MEDIA_SAVED +}; + +const std::string& DataExtractionNotification_Type_Name( + DataExtractionNotification_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + DataExtractionNotification_Type_entries, + DataExtractionNotification_Type_entries_by_number, + 2, DataExtractionNotification_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + DataExtractionNotification_Type_entries, + DataExtractionNotification_Type_entries_by_number, + 2, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + DataExtractionNotification_Type_strings[idx].get(); +} +bool DataExtractionNotification_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataExtractionNotification_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + DataExtractionNotification_Type_entries, 2, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr DataExtractionNotification_Type DataExtractionNotification::SCREENSHOT; +constexpr DataExtractionNotification_Type DataExtractionNotification::MEDIA_SAVED; +constexpr DataExtractionNotification_Type DataExtractionNotification::Type_MIN; +constexpr DataExtractionNotification_Type DataExtractionNotification::Type_MAX; +constexpr int DataExtractionNotification::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool DataMessage_Quote_QuotedAttachment_Flags_IsValid(int value) { + switch (value) { + case 1: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed DataMessage_Quote_QuotedAttachment_Flags_strings[1] = {}; + +static const char DataMessage_Quote_QuotedAttachment_Flags_names[] = + "VOICE_MESSAGE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry DataMessage_Quote_QuotedAttachment_Flags_entries[] = { + { {DataMessage_Quote_QuotedAttachment_Flags_names + 0, 13}, 1 }, +}; + +static const int DataMessage_Quote_QuotedAttachment_Flags_entries_by_number[] = { + 0, // 1 -> VOICE_MESSAGE +}; + +const std::string& DataMessage_Quote_QuotedAttachment_Flags_Name( + DataMessage_Quote_QuotedAttachment_Flags value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + DataMessage_Quote_QuotedAttachment_Flags_entries, + DataMessage_Quote_QuotedAttachment_Flags_entries_by_number, + 1, DataMessage_Quote_QuotedAttachment_Flags_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + DataMessage_Quote_QuotedAttachment_Flags_entries, + DataMessage_Quote_QuotedAttachment_Flags_entries_by_number, + 1, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + DataMessage_Quote_QuotedAttachment_Flags_strings[idx].get(); +} +bool DataMessage_Quote_QuotedAttachment_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Quote_QuotedAttachment_Flags* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + DataMessage_Quote_QuotedAttachment_Flags_entries, 1, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr DataMessage_Quote_QuotedAttachment_Flags DataMessage_Quote_QuotedAttachment::VOICE_MESSAGE; +constexpr DataMessage_Quote_QuotedAttachment_Flags DataMessage_Quote_QuotedAttachment::Flags_MIN; +constexpr DataMessage_Quote_QuotedAttachment_Flags DataMessage_Quote_QuotedAttachment::Flags_MAX; +constexpr int DataMessage_Quote_QuotedAttachment::Flags_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool DataMessage_Reaction_Action_IsValid(int value) { + switch (value) { + case 0: + case 1: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed DataMessage_Reaction_Action_strings[2] = {}; + +static const char DataMessage_Reaction_Action_names[] = + "REACT" + "REMOVE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry DataMessage_Reaction_Action_entries[] = { + { {DataMessage_Reaction_Action_names + 0, 5}, 0 }, + { {DataMessage_Reaction_Action_names + 5, 6}, 1 }, +}; + +static const int DataMessage_Reaction_Action_entries_by_number[] = { + 0, // 0 -> REACT + 1, // 1 -> REMOVE +}; + +const std::string& DataMessage_Reaction_Action_Name( + DataMessage_Reaction_Action value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + DataMessage_Reaction_Action_entries, + DataMessage_Reaction_Action_entries_by_number, + 2, DataMessage_Reaction_Action_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + DataMessage_Reaction_Action_entries, + DataMessage_Reaction_Action_entries_by_number, + 2, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + DataMessage_Reaction_Action_strings[idx].get(); +} +bool DataMessage_Reaction_Action_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Reaction_Action* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + DataMessage_Reaction_Action_entries, 2, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr DataMessage_Reaction_Action DataMessage_Reaction::REACT; +constexpr DataMessage_Reaction_Action DataMessage_Reaction::REMOVE; +constexpr DataMessage_Reaction_Action DataMessage_Reaction::Action_MIN; +constexpr DataMessage_Reaction_Action DataMessage_Reaction::Action_MAX; +constexpr int DataMessage_Reaction::Action_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool DataMessage_ClosedGroupControlMessage_Type_IsValid(int value) { + switch (value) { + case 1: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed DataMessage_ClosedGroupControlMessage_Type_strings[7] = {}; + +static const char DataMessage_ClosedGroupControlMessage_Type_names[] = + "ENCRYPTION_KEY_PAIR" + "ENCRYPTION_KEY_PAIR_REQUEST" + "MEMBERS_ADDED" + "MEMBERS_REMOVED" + "MEMBER_LEFT" + "NAME_CHANGE" + "NEW"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry DataMessage_ClosedGroupControlMessage_Type_entries[] = { + { {DataMessage_ClosedGroupControlMessage_Type_names + 0, 19}, 3 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 19, 27}, 8 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 46, 13}, 5 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 59, 15}, 6 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 74, 11}, 7 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 85, 11}, 4 }, + { {DataMessage_ClosedGroupControlMessage_Type_names + 96, 3}, 1 }, +}; + +static const int DataMessage_ClosedGroupControlMessage_Type_entries_by_number[] = { + 6, // 1 -> NEW + 0, // 3 -> ENCRYPTION_KEY_PAIR + 5, // 4 -> NAME_CHANGE + 2, // 5 -> MEMBERS_ADDED + 3, // 6 -> MEMBERS_REMOVED + 4, // 7 -> MEMBER_LEFT + 1, // 8 -> ENCRYPTION_KEY_PAIR_REQUEST +}; + +const std::string& DataMessage_ClosedGroupControlMessage_Type_Name( + DataMessage_ClosedGroupControlMessage_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + DataMessage_ClosedGroupControlMessage_Type_entries, + DataMessage_ClosedGroupControlMessage_Type_entries_by_number, + 7, DataMessage_ClosedGroupControlMessage_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + DataMessage_ClosedGroupControlMessage_Type_entries, + DataMessage_ClosedGroupControlMessage_Type_entries_by_number, + 7, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + DataMessage_ClosedGroupControlMessage_Type_strings[idx].get(); +} +bool DataMessage_ClosedGroupControlMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_ClosedGroupControlMessage_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + DataMessage_ClosedGroupControlMessage_Type_entries, 7, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::NEW; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::ENCRYPTION_KEY_PAIR; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::NAME_CHANGE; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::MEMBERS_ADDED; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::MEMBERS_REMOVED; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::MEMBER_LEFT; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::ENCRYPTION_KEY_PAIR_REQUEST; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::Type_MIN; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::Type_MAX; +constexpr int DataMessage_ClosedGroupControlMessage::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool DataMessage_Flags_IsValid(int value) { + switch (value) { + case 2: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed DataMessage_Flags_strings[1] = {}; + +static const char DataMessage_Flags_names[] = + "EXPIRATION_TIMER_UPDATE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry DataMessage_Flags_entries[] = { + { {DataMessage_Flags_names + 0, 23}, 2 }, +}; + +static const int DataMessage_Flags_entries_by_number[] = { + 0, // 2 -> EXPIRATION_TIMER_UPDATE +}; + +const std::string& DataMessage_Flags_Name( + DataMessage_Flags value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + DataMessage_Flags_entries, + DataMessage_Flags_entries_by_number, + 1, DataMessage_Flags_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + DataMessage_Flags_entries, + DataMessage_Flags_entries_by_number, + 1, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + DataMessage_Flags_strings[idx].get(); +} +bool DataMessage_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Flags* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + DataMessage_Flags_entries, 1, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr DataMessage_Flags DataMessage::EXPIRATION_TIMER_UPDATE; +constexpr DataMessage_Flags DataMessage::Flags_MIN; +constexpr DataMessage_Flags DataMessage::Flags_MAX; +constexpr int DataMessage::Flags_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool ReceiptMessage_Type_IsValid(int value) { + switch (value) { + case 0: + case 1: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed ReceiptMessage_Type_strings[2] = {}; + +static const char ReceiptMessage_Type_names[] = + "DELIVERY" + "READ"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry ReceiptMessage_Type_entries[] = { + { {ReceiptMessage_Type_names + 0, 8}, 0 }, + { {ReceiptMessage_Type_names + 8, 4}, 1 }, +}; + +static const int ReceiptMessage_Type_entries_by_number[] = { + 0, // 0 -> DELIVERY + 1, // 1 -> READ +}; + +const std::string& ReceiptMessage_Type_Name( + ReceiptMessage_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + ReceiptMessage_Type_entries, + ReceiptMessage_Type_entries_by_number, + 2, ReceiptMessage_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + ReceiptMessage_Type_entries, + ReceiptMessage_Type_entries_by_number, + 2, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + ReceiptMessage_Type_strings[idx].get(); +} +bool ReceiptMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, ReceiptMessage_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + ReceiptMessage_Type_entries, 2, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr ReceiptMessage_Type ReceiptMessage::DELIVERY; +constexpr ReceiptMessage_Type ReceiptMessage::READ; +constexpr ReceiptMessage_Type ReceiptMessage::Type_MIN; +constexpr ReceiptMessage_Type ReceiptMessage::Type_MAX; +constexpr int ReceiptMessage::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool AttachmentPointer_Flags_IsValid(int value) { + switch (value) { + case 1: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed AttachmentPointer_Flags_strings[1] = {}; + +static const char AttachmentPointer_Flags_names[] = + "VOICE_MESSAGE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry AttachmentPointer_Flags_entries[] = { + { {AttachmentPointer_Flags_names + 0, 13}, 1 }, +}; + +static const int AttachmentPointer_Flags_entries_by_number[] = { + 0, // 1 -> VOICE_MESSAGE +}; + +const std::string& AttachmentPointer_Flags_Name( + AttachmentPointer_Flags value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + AttachmentPointer_Flags_entries, + AttachmentPointer_Flags_entries_by_number, + 1, AttachmentPointer_Flags_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + AttachmentPointer_Flags_entries, + AttachmentPointer_Flags_entries_by_number, + 1, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + AttachmentPointer_Flags_strings[idx].get(); +} +bool AttachmentPointer_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, AttachmentPointer_Flags* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + AttachmentPointer_Flags_entries, 1, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr AttachmentPointer_Flags AttachmentPointer::VOICE_MESSAGE; +constexpr AttachmentPointer_Flags AttachmentPointer::Flags_MIN; +constexpr AttachmentPointer_Flags AttachmentPointer::Flags_MAX; +constexpr int AttachmentPointer::Flags_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool SharedConfigMessage_Kind_IsValid(int value) { + switch (value) { + case 1: + case 2: + case 3: + case 4: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed SharedConfigMessage_Kind_strings[4] = {}; + +static const char SharedConfigMessage_Kind_names[] = + "CONTACTS" + "CONVO_INFO_VOLATILE" + "USER_GROUPS" + "USER_PROFILE"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry SharedConfigMessage_Kind_entries[] = { + { {SharedConfigMessage_Kind_names + 0, 8}, 2 }, + { {SharedConfigMessage_Kind_names + 8, 19}, 3 }, + { {SharedConfigMessage_Kind_names + 27, 11}, 4 }, + { {SharedConfigMessage_Kind_names + 38, 12}, 1 }, +}; + +static const int SharedConfigMessage_Kind_entries_by_number[] = { + 3, // 1 -> USER_PROFILE + 0, // 2 -> CONTACTS + 1, // 3 -> CONVO_INFO_VOLATILE + 2, // 4 -> USER_GROUPS +}; + +const std::string& SharedConfigMessage_Kind_Name( + SharedConfigMessage_Kind value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + SharedConfigMessage_Kind_entries, + SharedConfigMessage_Kind_entries_by_number, + 4, SharedConfigMessage_Kind_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + SharedConfigMessage_Kind_entries, + SharedConfigMessage_Kind_entries_by_number, + 4, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + SharedConfigMessage_Kind_strings[idx].get(); +} +bool SharedConfigMessage_Kind_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, SharedConfigMessage_Kind* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + SharedConfigMessage_Kind_entries, 4, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr SharedConfigMessage_Kind SharedConfigMessage::USER_PROFILE; +constexpr SharedConfigMessage_Kind SharedConfigMessage::CONTACTS; +constexpr SharedConfigMessage_Kind SharedConfigMessage::CONVO_INFO_VOLATILE; +constexpr SharedConfigMessage_Kind SharedConfigMessage::USER_GROUPS; +constexpr SharedConfigMessage_Kind SharedConfigMessage::Kind_MIN; +constexpr SharedConfigMessage_Kind SharedConfigMessage::Kind_MAX; +constexpr int SharedConfigMessage::Kind_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) + +// =================================================================== + +class Envelope::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } + static void set_has_source(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_sourcedevice(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_content(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_servertimestamp(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000024) ^ 0x00000024) != 0; + } +}; + +Envelope::Envelope(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.Envelope) +} +Envelope::Envelope(const Envelope& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + Envelope* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.source_){} + , decltype(_impl_.content_){} + , decltype(_impl_.timestamp_){} + , decltype(_impl_.servertimestamp_){} + , decltype(_impl_.sourcedevice_){} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.source_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.source_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_source()) { + _this->_impl_.source_.Set(from._internal_source(), + _this->GetArenaForAllocation()); + } + _impl_.content_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.content_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_content()) { + _this->_impl_.content_.Set(from._internal_content(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.timestamp_, &from._impl_.timestamp_, + static_cast(reinterpret_cast(&_impl_.type_) - + reinterpret_cast(&_impl_.timestamp_)) + sizeof(_impl_.type_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.Envelope) +} + +inline void Envelope::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.source_){} + , decltype(_impl_.content_){} + , decltype(_impl_.timestamp_){uint64_t{0u}} + , decltype(_impl_.servertimestamp_){uint64_t{0u}} + , decltype(_impl_.sourcedevice_){0u} + , decltype(_impl_.type_){6} + }; + _impl_.source_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.source_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.content_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.content_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +Envelope::~Envelope() { + // @@protoc_insertion_point(destructor:SessionProtos.Envelope) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void Envelope::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.source_.Destroy(); + _impl_.content_.Destroy(); +} + +void Envelope::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void Envelope::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.Envelope) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.source_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.content_.ClearNonDefaultToEmpty(); + } + } + if (cached_has_bits & 0x0000003cu) { + ::memset(&_impl_.timestamp_, 0, static_cast( + reinterpret_cast(&_impl_.sourcedevice_) - + reinterpret_cast(&_impl_.timestamp_)) + sizeof(_impl_.sourcedevice_)); + _impl_.type_ = 6; + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* Envelope::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.Envelope.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::Envelope_Type_IsValid(val))) { + _internal_set_type(static_cast<::SessionProtos::Envelope_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // optional string source = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_source(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required uint64 timestamp = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 40)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 sourceDevice = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 56)) { + _Internal::set_has_sourcedevice(&has_bits); + _impl_.sourcedevice_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes content = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 66)) { + auto str = _internal_mutable_content(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint64 serverTimestamp = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 80)) { + _Internal::set_has_servertimestamp(&has_bits); + _impl_.servertimestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* Envelope::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.Envelope) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.Envelope.Type type = 1; + if (cached_has_bits & 0x00000020u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // optional string source = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_source(), target); + } + + // required uint64 timestamp = 5; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(5, this->_internal_timestamp(), target); + } + + // optional uint32 sourceDevice = 7; + if (cached_has_bits & 0x00000010u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(7, this->_internal_sourcedevice(), target); + } + + // optional bytes content = 8; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 8, this->_internal_content(), target); + } + + // optional uint64 serverTimestamp = 10; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(10, this->_internal_servertimestamp(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.Envelope) + return target; +} + +size_t Envelope::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.Envelope) + size_t total_size = 0; + + if (_internal_has_timestamp()) { + // required uint64 timestamp = 5; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + if (_internal_has_type()) { + // required .SessionProtos.Envelope.Type type = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + + return total_size; +} +size_t Envelope::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.Envelope) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000024) ^ 0x00000024) == 0) { // All required fields are present. + // required uint64 timestamp = 5; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + + // required .SessionProtos.Envelope.Type type = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + // optional string source = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_source()); + } + + // optional bytes content = 8; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_content()); + } + + } + if (cached_has_bits & 0x00000018u) { + // optional uint64 serverTimestamp = 10; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_servertimestamp()); + } + + // optional uint32 sourceDevice = 7; + if (cached_has_bits & 0x00000010u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_sourcedevice()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void Envelope::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void Envelope::MergeFrom(const Envelope& from) { + Envelope* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.Envelope) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000003fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_source(from._internal_source()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_content(from._internal_content()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.timestamp_ = from._impl_.timestamp_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.servertimestamp_ = from._impl_.servertimestamp_; + } + if (cached_has_bits & 0x00000010u) { + _this->_impl_.sourcedevice_ = from._impl_.sourcedevice_; + } + if (cached_has_bits & 0x00000020u) { + _this->_impl_.type_ = from._impl_.type_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void Envelope::CopyFrom(const Envelope& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.Envelope) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Envelope::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void Envelope::InternalSwap(Envelope* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.source_, lhs_arena, + &other->_impl_.source_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.content_, lhs_arena, + &other->_impl_.content_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(Envelope, _impl_.sourcedevice_) + + sizeof(Envelope::_impl_.sourcedevice_) + - PROTOBUF_FIELD_OFFSET(Envelope, _impl_.timestamp_)>( + reinterpret_cast(&_impl_.timestamp_), + reinterpret_cast(&other->_impl_.timestamp_)); + swap(_impl_.type_, other->_impl_.type_); +} + +std::string Envelope::GetTypeName() const { + return "SessionProtos.Envelope"; +} + + +// =================================================================== + +class TypingMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_action(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +TypingMessage::TypingMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.TypingMessage) +} +TypingMessage::TypingMessage(const TypingMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + TypingMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){} + , decltype(_impl_.action_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + ::memcpy(&_impl_.timestamp_, &from._impl_.timestamp_, + static_cast(reinterpret_cast(&_impl_.action_) - + reinterpret_cast(&_impl_.timestamp_)) + sizeof(_impl_.action_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.TypingMessage) +} + +inline void TypingMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){uint64_t{0u}} + , decltype(_impl_.action_){0} + }; +} + +TypingMessage::~TypingMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.TypingMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void TypingMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); +} + +void TypingMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void TypingMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.TypingMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + ::memset(&_impl_.timestamp_, 0, static_cast( + reinterpret_cast(&_impl_.action_) - + reinterpret_cast(&_impl_.timestamp_)) + sizeof(_impl_.action_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* TypingMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint64 timestamp = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required .SessionProtos.TypingMessage.Action action = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 16)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::TypingMessage_Action_IsValid(val))) { + _internal_set_action(static_cast<::SessionProtos::TypingMessage_Action>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(2, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* TypingMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.TypingMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint64 timestamp = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_timestamp(), target); + } + + // required .SessionProtos.TypingMessage.Action action = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 2, this->_internal_action(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.TypingMessage) + return target; +} + +size_t TypingMessage::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.TypingMessage) + size_t total_size = 0; + + if (_internal_has_timestamp()) { + // required uint64 timestamp = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + if (_internal_has_action()) { + // required .SessionProtos.TypingMessage.Action action = 2; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_action()); + } + + return total_size; +} +size_t TypingMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.TypingMessage) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required uint64 timestamp = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + + // required .SessionProtos.TypingMessage.Action action = 2; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_action()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void TypingMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void TypingMessage::MergeFrom(const TypingMessage& from) { + TypingMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.TypingMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_impl_.timestamp_ = from._impl_.timestamp_; + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.action_ = from._impl_.action_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void TypingMessage::CopyFrom(const TypingMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.TypingMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool TypingMessage::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void TypingMessage::InternalSwap(TypingMessage* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(TypingMessage, _impl_.action_) + + sizeof(TypingMessage::_impl_.action_) + - PROTOBUF_FIELD_OFFSET(TypingMessage, _impl_.timestamp_)>( + reinterpret_cast(&_impl_.timestamp_), + reinterpret_cast(&other->_impl_.timestamp_)); +} + +std::string TypingMessage::GetTypeName() const { + return "SessionProtos.TypingMessage"; +} + + +// =================================================================== + +class UnsendRequest::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_author(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +UnsendRequest::UnsendRequest(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.UnsendRequest) +} +UnsendRequest::UnsendRequest(const UnsendRequest& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + UnsendRequest* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.author_){} + , decltype(_impl_.timestamp_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_author()) { + _this->_impl_.author_.Set(from._internal_author(), + _this->GetArenaForAllocation()); + } + _this->_impl_.timestamp_ = from._impl_.timestamp_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.UnsendRequest) +} + +inline void UnsendRequest::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.author_){} + , decltype(_impl_.timestamp_){uint64_t{0u}} + }; + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +UnsendRequest::~UnsendRequest() { + // @@protoc_insertion_point(destructor:SessionProtos.UnsendRequest) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void UnsendRequest::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.author_.Destroy(); +} + +void UnsendRequest::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void UnsendRequest::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.UnsendRequest) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + _impl_.author_.ClearNonDefaultToEmpty(); + } + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* UnsendRequest::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint64 timestamp = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string author = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_author(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* UnsendRequest::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.UnsendRequest) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint64 timestamp = 1; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_timestamp(), target); + } + + // required string author = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_author(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.UnsendRequest) + return target; +} + +size_t UnsendRequest::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.UnsendRequest) + size_t total_size = 0; + + if (_internal_has_author()) { + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + } + + if (_internal_has_timestamp()) { + // required uint64 timestamp = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + return total_size; +} +size_t UnsendRequest::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.UnsendRequest) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + + // required uint64 timestamp = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void UnsendRequest::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void UnsendRequest::MergeFrom(const UnsendRequest& from) { + UnsendRequest* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.UnsendRequest) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_author(from._internal_author()); + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.timestamp_ = from._impl_.timestamp_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void UnsendRequest::CopyFrom(const UnsendRequest& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.UnsendRequest) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool UnsendRequest::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void UnsendRequest::InternalSwap(UnsendRequest* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.author_, lhs_arena, + &other->_impl_.author_, rhs_arena + ); + swap(_impl_.timestamp_, other->_impl_.timestamp_); +} + +std::string UnsendRequest::GetTypeName() const { + return "SessionProtos.UnsendRequest"; +} + + +// =================================================================== + +class MessageRequestResponse::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_isapproved(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_profilekey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static const ::SessionProtos::LokiProfile& profile(const MessageRequestResponse* msg); + static void set_has_profile(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000004) ^ 0x00000004) != 0; + } +}; + +const ::SessionProtos::LokiProfile& +MessageRequestResponse::_Internal::profile(const MessageRequestResponse* msg) { + return *msg->_impl_.profile_; +} +MessageRequestResponse::MessageRequestResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.MessageRequestResponse) +} +MessageRequestResponse::MessageRequestResponse(const MessageRequestResponse& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + MessageRequestResponse* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.profile_){nullptr} + , decltype(_impl_.isapproved_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilekey()) { + _this->_impl_.profilekey_.Set(from._internal_profilekey(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_profile()) { + _this->_impl_.profile_ = new ::SessionProtos::LokiProfile(*from._impl_.profile_); + } + _this->_impl_.isapproved_ = from._impl_.isapproved_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.MessageRequestResponse) +} + +inline void MessageRequestResponse::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.profile_){nullptr} + , decltype(_impl_.isapproved_){false} + }; + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +MessageRequestResponse::~MessageRequestResponse() { + // @@protoc_insertion_point(destructor:SessionProtos.MessageRequestResponse) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void MessageRequestResponse::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.profilekey_.Destroy(); + if (this != internal_default_instance()) delete _impl_.profile_; +} + +void MessageRequestResponse::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void MessageRequestResponse::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.MessageRequestResponse) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.profilekey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + GOOGLE_DCHECK(_impl_.profile_ != nullptr); + _impl_.profile_->Clear(); + } + } + _impl_.isapproved_ = false; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* MessageRequestResponse::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required bool isApproved = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_isapproved(&has_bits); + _impl_.isapproved_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes profileKey = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_profilekey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.LokiProfile profile = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_profile(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* MessageRequestResponse::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.MessageRequestResponse) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required bool isApproved = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(1, this->_internal_isapproved(), target); + } + + // optional bytes profileKey = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_profilekey(), target); + } + + // optional .SessionProtos.LokiProfile profile = 3; + if (cached_has_bits & 0x00000002u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::profile(this), + _Internal::profile(this).GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.MessageRequestResponse) + return target; +} + +size_t MessageRequestResponse::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.MessageRequestResponse) + size_t total_size = 0; + + // required bool isApproved = 1; + if (_internal_has_isapproved()) { + total_size += 1 + 1; + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + // optional bytes profileKey = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_profilekey()); + } + + // optional .SessionProtos.LokiProfile profile = 3; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.profile_); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void MessageRequestResponse::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void MessageRequestResponse::MergeFrom(const MessageRequestResponse& from) { + MessageRequestResponse* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.MessageRequestResponse) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_profilekey(from._internal_profilekey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_mutable_profile()->::SessionProtos::LokiProfile::MergeFrom( + from._internal_profile()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.isapproved_ = from._impl_.isapproved_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void MessageRequestResponse::CopyFrom(const MessageRequestResponse& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.MessageRequestResponse) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool MessageRequestResponse::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void MessageRequestResponse::InternalSwap(MessageRequestResponse* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilekey_, lhs_arena, + &other->_impl_.profilekey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(MessageRequestResponse, _impl_.isapproved_) + + sizeof(MessageRequestResponse::_impl_.isapproved_) + - PROTOBUF_FIELD_OFFSET(MessageRequestResponse, _impl_.profile_)>( + reinterpret_cast(&_impl_.profile_), + reinterpret_cast(&other->_impl_.profile_)); +} + +std::string MessageRequestResponse::GetTypeName() const { + return "SessionProtos.MessageRequestResponse"; +} + + +// =================================================================== + +class Content::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static const ::SessionProtos::DataMessage& datamessage(const Content* msg); + static void set_has_datamessage(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static const ::SessionProtos::CallMessage& callmessage(const Content* msg); + static void set_has_callmessage(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::SessionProtos::ReceiptMessage& receiptmessage(const Content* msg); + static void set_has_receiptmessage(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static const ::SessionProtos::TypingMessage& typingmessage(const Content* msg); + static void set_has_typingmessage(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static const ::SessionProtos::ConfigurationMessage& configurationmessage(const Content* msg); + static void set_has_configurationmessage(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static const ::SessionProtos::DataExtractionNotification& dataextractionnotification(const Content* msg); + static void set_has_dataextractionnotification(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } + static const ::SessionProtos::UnsendRequest& unsendrequest(const Content* msg); + static void set_has_unsendrequest(HasBits* has_bits) { + (*has_bits)[0] |= 64u; + } + static const ::SessionProtos::MessageRequestResponse& messagerequestresponse(const Content* msg); + static void set_has_messagerequestresponse(HasBits* has_bits) { + (*has_bits)[0] |= 128u; + } + static const ::SessionProtos::SharedConfigMessage& sharedconfigmessage(const Content* msg); + static void set_has_sharedconfigmessage(HasBits* has_bits) { + (*has_bits)[0] |= 256u; + } +}; + +const ::SessionProtos::DataMessage& +Content::_Internal::datamessage(const Content* msg) { + return *msg->_impl_.datamessage_; +} +const ::SessionProtos::CallMessage& +Content::_Internal::callmessage(const Content* msg) { + return *msg->_impl_.callmessage_; +} +const ::SessionProtos::ReceiptMessage& +Content::_Internal::receiptmessage(const Content* msg) { + return *msg->_impl_.receiptmessage_; +} +const ::SessionProtos::TypingMessage& +Content::_Internal::typingmessage(const Content* msg) { + return *msg->_impl_.typingmessage_; +} +const ::SessionProtos::ConfigurationMessage& +Content::_Internal::configurationmessage(const Content* msg) { + return *msg->_impl_.configurationmessage_; +} +const ::SessionProtos::DataExtractionNotification& +Content::_Internal::dataextractionnotification(const Content* msg) { + return *msg->_impl_.dataextractionnotification_; +} +const ::SessionProtos::UnsendRequest& +Content::_Internal::unsendrequest(const Content* msg) { + return *msg->_impl_.unsendrequest_; +} +const ::SessionProtos::MessageRequestResponse& +Content::_Internal::messagerequestresponse(const Content* msg) { + return *msg->_impl_.messagerequestresponse_; +} +const ::SessionProtos::SharedConfigMessage& +Content::_Internal::sharedconfigmessage(const Content* msg) { + return *msg->_impl_.sharedconfigmessage_; +} +Content::Content(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.Content) +} +Content::Content(const Content& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + Content* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.datamessage_){nullptr} + , decltype(_impl_.callmessage_){nullptr} + , decltype(_impl_.receiptmessage_){nullptr} + , decltype(_impl_.typingmessage_){nullptr} + , decltype(_impl_.configurationmessage_){nullptr} + , decltype(_impl_.dataextractionnotification_){nullptr} + , decltype(_impl_.unsendrequest_){nullptr} + , decltype(_impl_.messagerequestresponse_){nullptr} + , decltype(_impl_.sharedconfigmessage_){nullptr}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + if (from._internal_has_datamessage()) { + _this->_impl_.datamessage_ = new ::SessionProtos::DataMessage(*from._impl_.datamessage_); + } + if (from._internal_has_callmessage()) { + _this->_impl_.callmessage_ = new ::SessionProtos::CallMessage(*from._impl_.callmessage_); + } + if (from._internal_has_receiptmessage()) { + _this->_impl_.receiptmessage_ = new ::SessionProtos::ReceiptMessage(*from._impl_.receiptmessage_); + } + if (from._internal_has_typingmessage()) { + _this->_impl_.typingmessage_ = new ::SessionProtos::TypingMessage(*from._impl_.typingmessage_); + } + if (from._internal_has_configurationmessage()) { + _this->_impl_.configurationmessage_ = new ::SessionProtos::ConfigurationMessage(*from._impl_.configurationmessage_); + } + if (from._internal_has_dataextractionnotification()) { + _this->_impl_.dataextractionnotification_ = new ::SessionProtos::DataExtractionNotification(*from._impl_.dataextractionnotification_); + } + if (from._internal_has_unsendrequest()) { + _this->_impl_.unsendrequest_ = new ::SessionProtos::UnsendRequest(*from._impl_.unsendrequest_); + } + if (from._internal_has_messagerequestresponse()) { + _this->_impl_.messagerequestresponse_ = new ::SessionProtos::MessageRequestResponse(*from._impl_.messagerequestresponse_); + } + if (from._internal_has_sharedconfigmessage()) { + _this->_impl_.sharedconfigmessage_ = new ::SessionProtos::SharedConfigMessage(*from._impl_.sharedconfigmessage_); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.Content) +} + +inline void Content::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.datamessage_){nullptr} + , decltype(_impl_.callmessage_){nullptr} + , decltype(_impl_.receiptmessage_){nullptr} + , decltype(_impl_.typingmessage_){nullptr} + , decltype(_impl_.configurationmessage_){nullptr} + , decltype(_impl_.dataextractionnotification_){nullptr} + , decltype(_impl_.unsendrequest_){nullptr} + , decltype(_impl_.messagerequestresponse_){nullptr} + , decltype(_impl_.sharedconfigmessage_){nullptr} + }; +} + +Content::~Content() { + // @@protoc_insertion_point(destructor:SessionProtos.Content) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void Content::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (this != internal_default_instance()) delete _impl_.datamessage_; + if (this != internal_default_instance()) delete _impl_.callmessage_; + if (this != internal_default_instance()) delete _impl_.receiptmessage_; + if (this != internal_default_instance()) delete _impl_.typingmessage_; + if (this != internal_default_instance()) delete _impl_.configurationmessage_; + if (this != internal_default_instance()) delete _impl_.dataextractionnotification_; + if (this != internal_default_instance()) delete _impl_.unsendrequest_; + if (this != internal_default_instance()) delete _impl_.messagerequestresponse_; + if (this != internal_default_instance()) delete _impl_.sharedconfigmessage_; +} + +void Content::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void Content::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.Content) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + if (cached_has_bits & 0x00000001u) { + GOOGLE_DCHECK(_impl_.datamessage_ != nullptr); + _impl_.datamessage_->Clear(); + } + if (cached_has_bits & 0x00000002u) { + GOOGLE_DCHECK(_impl_.callmessage_ != nullptr); + _impl_.callmessage_->Clear(); + } + if (cached_has_bits & 0x00000004u) { + GOOGLE_DCHECK(_impl_.receiptmessage_ != nullptr); + _impl_.receiptmessage_->Clear(); + } + if (cached_has_bits & 0x00000008u) { + GOOGLE_DCHECK(_impl_.typingmessage_ != nullptr); + _impl_.typingmessage_->Clear(); + } + if (cached_has_bits & 0x00000010u) { + GOOGLE_DCHECK(_impl_.configurationmessage_ != nullptr); + _impl_.configurationmessage_->Clear(); + } + if (cached_has_bits & 0x00000020u) { + GOOGLE_DCHECK(_impl_.dataextractionnotification_ != nullptr); + _impl_.dataextractionnotification_->Clear(); + } + if (cached_has_bits & 0x00000040u) { + GOOGLE_DCHECK(_impl_.unsendrequest_ != nullptr); + _impl_.unsendrequest_->Clear(); + } + if (cached_has_bits & 0x00000080u) { + GOOGLE_DCHECK(_impl_.messagerequestresponse_ != nullptr); + _impl_.messagerequestresponse_->Clear(); + } + } + if (cached_has_bits & 0x00000100u) { + GOOGLE_DCHECK(_impl_.sharedconfigmessage_ != nullptr); + _impl_.sharedconfigmessage_->Clear(); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* Content::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional .SessionProtos.DataMessage dataMessage = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + ptr = ctx->ParseMessage(_internal_mutable_datamessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.CallMessage callMessage = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_callmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.ReceiptMessage receiptMessage = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr = ctx->ParseMessage(_internal_mutable_receiptmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.TypingMessage typingMessage = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + ptr = ctx->ParseMessage(_internal_mutable_typingmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.ConfigurationMessage configurationMessage = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 58)) { + ptr = ctx->ParseMessage(_internal_mutable_configurationmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.DataExtractionNotification dataExtractionNotification = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 66)) { + ptr = ctx->ParseMessage(_internal_mutable_dataextractionnotification(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.UnsendRequest unsendRequest = 9; + case 9: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 74)) { + ptr = ctx->ParseMessage(_internal_mutable_unsendrequest(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.MessageRequestResponse messageRequestResponse = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 82)) { + ptr = ctx->ParseMessage(_internal_mutable_messagerequestresponse(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.SharedConfigMessage sharedConfigMessage = 11; + case 11: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 90)) { + ptr = ctx->ParseMessage(_internal_mutable_sharedconfigmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* Content::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.Content) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional .SessionProtos.DataMessage dataMessage = 1; + if (cached_has_bits & 0x00000001u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(1, _Internal::datamessage(this), + _Internal::datamessage(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.CallMessage callMessage = 3; + if (cached_has_bits & 0x00000002u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::callmessage(this), + _Internal::callmessage(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.ReceiptMessage receiptMessage = 5; + if (cached_has_bits & 0x00000004u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(5, _Internal::receiptmessage(this), + _Internal::receiptmessage(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.TypingMessage typingMessage = 6; + if (cached_has_bits & 0x00000008u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(6, _Internal::typingmessage(this), + _Internal::typingmessage(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.ConfigurationMessage configurationMessage = 7; + if (cached_has_bits & 0x00000010u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(7, _Internal::configurationmessage(this), + _Internal::configurationmessage(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.DataExtractionNotification dataExtractionNotification = 8; + if (cached_has_bits & 0x00000020u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(8, _Internal::dataextractionnotification(this), + _Internal::dataextractionnotification(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.UnsendRequest unsendRequest = 9; + if (cached_has_bits & 0x00000040u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(9, _Internal::unsendrequest(this), + _Internal::unsendrequest(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.MessageRequestResponse messageRequestResponse = 10; + if (cached_has_bits & 0x00000080u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(10, _Internal::messagerequestresponse(this), + _Internal::messagerequestresponse(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.SharedConfigMessage sharedConfigMessage = 11; + if (cached_has_bits & 0x00000100u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(11, _Internal::sharedconfigmessage(this), + _Internal::sharedconfigmessage(this).GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.Content) + return target; +} + +size_t Content::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.Content) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + // optional .SessionProtos.DataMessage dataMessage = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.datamessage_); + } + + // optional .SessionProtos.CallMessage callMessage = 3; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.callmessage_); + } + + // optional .SessionProtos.ReceiptMessage receiptMessage = 5; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.receiptmessage_); + } + + // optional .SessionProtos.TypingMessage typingMessage = 6; + if (cached_has_bits & 0x00000008u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.typingmessage_); + } + + // optional .SessionProtos.ConfigurationMessage configurationMessage = 7; + if (cached_has_bits & 0x00000010u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.configurationmessage_); + } + + // optional .SessionProtos.DataExtractionNotification dataExtractionNotification = 8; + if (cached_has_bits & 0x00000020u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.dataextractionnotification_); + } + + // optional .SessionProtos.UnsendRequest unsendRequest = 9; + if (cached_has_bits & 0x00000040u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.unsendrequest_); + } + + // optional .SessionProtos.MessageRequestResponse messageRequestResponse = 10; + if (cached_has_bits & 0x00000080u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.messagerequestresponse_); + } + + } + // optional .SessionProtos.SharedConfigMessage sharedConfigMessage = 11; + if (cached_has_bits & 0x00000100u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.sharedconfigmessage_); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void Content::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void Content::MergeFrom(const Content& from) { + Content* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.Content) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_mutable_datamessage()->::SessionProtos::DataMessage::MergeFrom( + from._internal_datamessage()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_mutable_callmessage()->::SessionProtos::CallMessage::MergeFrom( + from._internal_callmessage()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_mutable_receiptmessage()->::SessionProtos::ReceiptMessage::MergeFrom( + from._internal_receiptmessage()); + } + if (cached_has_bits & 0x00000008u) { + _this->_internal_mutable_typingmessage()->::SessionProtos::TypingMessage::MergeFrom( + from._internal_typingmessage()); + } + if (cached_has_bits & 0x00000010u) { + _this->_internal_mutable_configurationmessage()->::SessionProtos::ConfigurationMessage::MergeFrom( + from._internal_configurationmessage()); + } + if (cached_has_bits & 0x00000020u) { + _this->_internal_mutable_dataextractionnotification()->::SessionProtos::DataExtractionNotification::MergeFrom( + from._internal_dataextractionnotification()); + } + if (cached_has_bits & 0x00000040u) { + _this->_internal_mutable_unsendrequest()->::SessionProtos::UnsendRequest::MergeFrom( + from._internal_unsendrequest()); + } + if (cached_has_bits & 0x00000080u) { + _this->_internal_mutable_messagerequestresponse()->::SessionProtos::MessageRequestResponse::MergeFrom( + from._internal_messagerequestresponse()); + } + } + if (cached_has_bits & 0x00000100u) { + _this->_internal_mutable_sharedconfigmessage()->::SessionProtos::SharedConfigMessage::MergeFrom( + from._internal_sharedconfigmessage()); + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void Content::CopyFrom(const Content& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.Content) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Content::IsInitialized() const { + if (_internal_has_datamessage()) { + if (!_impl_.datamessage_->IsInitialized()) return false; + } + if (_internal_has_callmessage()) { + if (!_impl_.callmessage_->IsInitialized()) return false; + } + if (_internal_has_receiptmessage()) { + if (!_impl_.receiptmessage_->IsInitialized()) return false; + } + if (_internal_has_typingmessage()) { + if (!_impl_.typingmessage_->IsInitialized()) return false; + } + if (_internal_has_configurationmessage()) { + if (!_impl_.configurationmessage_->IsInitialized()) return false; + } + if (_internal_has_dataextractionnotification()) { + if (!_impl_.dataextractionnotification_->IsInitialized()) return false; + } + if (_internal_has_unsendrequest()) { + if (!_impl_.unsendrequest_->IsInitialized()) return false; + } + if (_internal_has_messagerequestresponse()) { + if (!_impl_.messagerequestresponse_->IsInitialized()) return false; + } + if (_internal_has_sharedconfigmessage()) { + if (!_impl_.sharedconfigmessage_->IsInitialized()) return false; + } + return true; +} + +void Content::InternalSwap(Content* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(Content, _impl_.sharedconfigmessage_) + + sizeof(Content::_impl_.sharedconfigmessage_) + - PROTOBUF_FIELD_OFFSET(Content, _impl_.datamessage_)>( + reinterpret_cast(&_impl_.datamessage_), + reinterpret_cast(&other->_impl_.datamessage_)); +} + +std::string Content::GetTypeName() const { + return "SessionProtos.Content"; +} + + +// =================================================================== + +class CallMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_uuid(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +CallMessage::CallMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.CallMessage) +} +CallMessage::CallMessage(const CallMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + CallMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.sdps_){from._impl_.sdps_} + , decltype(_impl_.sdpmlineindexes_){from._impl_.sdpmlineindexes_} + , decltype(_impl_.sdpmids_){from._impl_.sdpmids_} + , decltype(_impl_.uuid_){} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.uuid_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.uuid_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_uuid()) { + _this->_impl_.uuid_.Set(from._internal_uuid(), + _this->GetArenaForAllocation()); + } + _this->_impl_.type_ = from._impl_.type_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.CallMessage) +} + +inline void CallMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.sdps_){arena} + , decltype(_impl_.sdpmlineindexes_){arena} + , decltype(_impl_.sdpmids_){arena} + , decltype(_impl_.uuid_){} + , decltype(_impl_.type_){6} + }; + _impl_.uuid_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.uuid_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +CallMessage::~CallMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.CallMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void CallMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.sdps_.~RepeatedPtrField(); + _impl_.sdpmlineindexes_.~RepeatedField(); + _impl_.sdpmids_.~RepeatedPtrField(); + _impl_.uuid_.Destroy(); +} + +void CallMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void CallMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.CallMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.sdps_.Clear(); + _impl_.sdpmlineindexes_.Clear(); + _impl_.sdpmids_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.uuid_.ClearNonDefaultToEmpty(); + } + _impl_.type_ = 6; + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* CallMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.CallMessage.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::CallMessage_Type_IsValid(val))) { + _internal_set_type(static_cast<::SessionProtos::CallMessage_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // repeated string sdps = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_sdps(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<18>(ptr)); + } else + goto handle_unusual; + continue; + // repeated uint32 sdpMLineIndexes = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 24)) { + ptr -= 1; + do { + ptr += 1; + _internal_add_sdpmlineindexes(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr)); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<24>(ptr)); + } else if (static_cast(tag) == 26) { + ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedUInt32Parser(_internal_mutable_sdpmlineindexes(), ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated string sdpMids = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_sdpmids(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<34>(ptr)); + } else + goto handle_unusual; + continue; + // required string uuid = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + auto str = _internal_mutable_uuid(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* CallMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.CallMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.CallMessage.Type type = 1; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // repeated string sdps = 2; + for (int i = 0, n = this->_internal_sdps_size(); i < n; i++) { + const auto& s = this->_internal_sdps(i); + target = stream->WriteString(2, s, target); + } + + // repeated uint32 sdpMLineIndexes = 3; + for (int i = 0, n = this->_internal_sdpmlineindexes_size(); i < n; i++) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(3, this->_internal_sdpmlineindexes(i), target); + } + + // repeated string sdpMids = 4; + for (int i = 0, n = this->_internal_sdpmids_size(); i < n; i++) { + const auto& s = this->_internal_sdpmids(i); + target = stream->WriteString(4, s, target); + } + + // required string uuid = 5; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 5, this->_internal_uuid(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.CallMessage) + return target; +} + +size_t CallMessage::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.CallMessage) + size_t total_size = 0; + + if (_internal_has_uuid()) { + // required string uuid = 5; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_uuid()); + } + + if (_internal_has_type()) { + // required .SessionProtos.CallMessage.Type type = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + + return total_size; +} +size_t CallMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.CallMessage) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required string uuid = 5; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_uuid()); + + // required .SessionProtos.CallMessage.Type type = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated string sdps = 2; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.sdps_.size()); + for (int i = 0, n = _impl_.sdps_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + _impl_.sdps_.Get(i)); + } + + // repeated uint32 sdpMLineIndexes = 3; + { + size_t data_size = ::_pbi::WireFormatLite:: + UInt32Size(this->_impl_.sdpmlineindexes_); + total_size += 1 * + ::_pbi::FromIntSize(this->_internal_sdpmlineindexes_size()); + total_size += data_size; + } + + // repeated string sdpMids = 4; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.sdpmids_.size()); + for (int i = 0, n = _impl_.sdpmids_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + _impl_.sdpmids_.Get(i)); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void CallMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void CallMessage::MergeFrom(const CallMessage& from) { + CallMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.CallMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.sdps_.MergeFrom(from._impl_.sdps_); + _this->_impl_.sdpmlineindexes_.MergeFrom(from._impl_.sdpmlineindexes_); + _this->_impl_.sdpmids_.MergeFrom(from._impl_.sdpmids_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_uuid(from._internal_uuid()); + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.type_ = from._impl_.type_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void CallMessage::CopyFrom(const CallMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.CallMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool CallMessage::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void CallMessage::InternalSwap(CallMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.sdps_.InternalSwap(&other->_impl_.sdps_); + _impl_.sdpmlineindexes_.InternalSwap(&other->_impl_.sdpmlineindexes_); + _impl_.sdpmids_.InternalSwap(&other->_impl_.sdpmids_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.uuid_, lhs_arena, + &other->_impl_.uuid_, rhs_arena + ); + swap(_impl_.type_, other->_impl_.type_); +} + +std::string CallMessage::GetTypeName() const { + return "SessionProtos.CallMessage"; +} + + +// =================================================================== + +class KeyPair::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_publickey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_privatekey(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +KeyPair::KeyPair(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.KeyPair) +} +KeyPair::KeyPair(const KeyPair& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + KeyPair* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.privatekey_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_publickey()) { + _this->_impl_.publickey_.Set(from._internal_publickey(), + _this->GetArenaForAllocation()); + } + _impl_.privatekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.privatekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_privatekey()) { + _this->_impl_.privatekey_.Set(from._internal_privatekey(), + _this->GetArenaForAllocation()); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.KeyPair) +} + +inline void KeyPair::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.privatekey_){} + }; + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.privatekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.privatekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +KeyPair::~KeyPair() { + // @@protoc_insertion_point(destructor:SessionProtos.KeyPair) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void KeyPair::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.publickey_.Destroy(); + _impl_.privatekey_.Destroy(); +} + +void KeyPair::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void KeyPair::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.KeyPair) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.publickey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.privatekey_.ClearNonDefaultToEmpty(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* KeyPair::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required bytes publicKey = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_publickey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required bytes privateKey = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_privatekey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* KeyPair::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.KeyPair) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required bytes publicKey = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 1, this->_internal_publickey(), target); + } + + // required bytes privateKey = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_privatekey(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.KeyPair) + return target; +} + +size_t KeyPair::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.KeyPair) + size_t total_size = 0; + + if (_internal_has_publickey()) { + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + } + + if (_internal_has_privatekey()) { + // required bytes privateKey = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_privatekey()); + } + + return total_size; +} +size_t KeyPair::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.KeyPair) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + + // required bytes privateKey = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_privatekey()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void KeyPair::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void KeyPair::MergeFrom(const KeyPair& from) { + KeyPair* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.KeyPair) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_publickey(from._internal_publickey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_privatekey(from._internal_privatekey()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void KeyPair::CopyFrom(const KeyPair& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.KeyPair) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool KeyPair::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void KeyPair::InternalSwap(KeyPair* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.publickey_, lhs_arena, + &other->_impl_.publickey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.privatekey_, lhs_arena, + &other->_impl_.privatekey_, rhs_arena + ); +} + +std::string KeyPair::GetTypeName() const { + return "SessionProtos.KeyPair"; +} + + +// =================================================================== + +class DataExtractionNotification::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000002) ^ 0x00000002) != 0; + } +}; + +DataExtractionNotification::DataExtractionNotification(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataExtractionNotification) +} +DataExtractionNotification::DataExtractionNotification(const DataExtractionNotification& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataExtractionNotification* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + ::memcpy(&_impl_.timestamp_, &from._impl_.timestamp_, + static_cast(reinterpret_cast(&_impl_.type_) - + reinterpret_cast(&_impl_.timestamp_)) + sizeof(_impl_.type_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataExtractionNotification) +} + +inline void DataExtractionNotification::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){uint64_t{0u}} + , decltype(_impl_.type_){1} + }; +} + +DataExtractionNotification::~DataExtractionNotification() { + // @@protoc_insertion_point(destructor:SessionProtos.DataExtractionNotification) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataExtractionNotification::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); +} + +void DataExtractionNotification::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataExtractionNotification::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataExtractionNotification) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + _impl_.timestamp_ = uint64_t{0u}; + _impl_.type_ = 1; + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataExtractionNotification::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.DataExtractionNotification.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::DataExtractionNotification_Type_IsValid(val))) { + _internal_set_type(static_cast<::SessionProtos::DataExtractionNotification_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // optional uint64 timestamp = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 16)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataExtractionNotification::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataExtractionNotification) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.DataExtractionNotification.Type type = 1; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // optional uint64 timestamp = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(2, this->_internal_timestamp(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataExtractionNotification) + return target; +} + +size_t DataExtractionNotification::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataExtractionNotification) + size_t total_size = 0; + + // required .SessionProtos.DataExtractionNotification.Type type = 1; + if (_internal_has_type()) { + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // optional uint64 timestamp = 2; + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataExtractionNotification::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataExtractionNotification::MergeFrom(const DataExtractionNotification& from) { + DataExtractionNotification* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataExtractionNotification) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_impl_.timestamp_ = from._impl_.timestamp_; + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.type_ = from._impl_.type_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataExtractionNotification::CopyFrom(const DataExtractionNotification& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataExtractionNotification) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataExtractionNotification::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void DataExtractionNotification::InternalSwap(DataExtractionNotification* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + swap(_impl_.timestamp_, other->_impl_.timestamp_); + swap(_impl_.type_, other->_impl_.type_); +} + +std::string DataExtractionNotification::GetTypeName() const { + return "SessionProtos.DataExtractionNotification"; +} + + +// =================================================================== + +class LokiProfile::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_displayname(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_profilepicture(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } +}; + +LokiProfile::LokiProfile(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.LokiProfile) +} +LokiProfile::LokiProfile(const LokiProfile& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + LokiProfile* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.displayname_){} + , decltype(_impl_.profilepicture_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.displayname_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.displayname_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_displayname()) { + _this->_impl_.displayname_.Set(from._internal_displayname(), + _this->GetArenaForAllocation()); + } + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilepicture()) { + _this->_impl_.profilepicture_.Set(from._internal_profilepicture(), + _this->GetArenaForAllocation()); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.LokiProfile) +} + +inline void LokiProfile::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.displayname_){} + , decltype(_impl_.profilepicture_){} + }; + _impl_.displayname_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.displayname_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +LokiProfile::~LokiProfile() { + // @@protoc_insertion_point(destructor:SessionProtos.LokiProfile) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void LokiProfile::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.displayname_.Destroy(); + _impl_.profilepicture_.Destroy(); +} + +void LokiProfile::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void LokiProfile::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.LokiProfile) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.displayname_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.profilepicture_.ClearNonDefaultToEmpty(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* LokiProfile::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional string displayName = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_displayname(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string profilePicture = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_profilepicture(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* LokiProfile::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.LokiProfile) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional string displayName = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_displayname(), target); + } + + // optional string profilePicture = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_profilepicture(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.LokiProfile) + return target; +} + +size_t LokiProfile::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.LokiProfile) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + // optional string displayName = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_displayname()); + } + + // optional string profilePicture = 2; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_profilepicture()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void LokiProfile::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void LokiProfile::MergeFrom(const LokiProfile& from) { + LokiProfile* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.LokiProfile) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_displayname(from._internal_displayname()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_profilepicture(from._internal_profilepicture()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void LokiProfile::CopyFrom(const LokiProfile& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.LokiProfile) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool LokiProfile::IsInitialized() const { + return true; +} + +void LokiProfile::InternalSwap(LokiProfile* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.displayname_, lhs_arena, + &other->_impl_.displayname_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilepicture_, lhs_arena, + &other->_impl_.profilepicture_, rhs_arena + ); +} + +std::string LokiProfile::GetTypeName() const { + return "SessionProtos.LokiProfile"; +} + + +// =================================================================== + +class DataMessage_Quote_QuotedAttachment::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_contenttype(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_filename(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::SessionProtos::AttachmentPointer& thumbnail(const DataMessage_Quote_QuotedAttachment* msg); + static void set_has_thumbnail(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_flags(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } +}; + +const ::SessionProtos::AttachmentPointer& +DataMessage_Quote_QuotedAttachment::_Internal::thumbnail(const DataMessage_Quote_QuotedAttachment* msg) { + return *msg->_impl_.thumbnail_; +} +DataMessage_Quote_QuotedAttachment::DataMessage_Quote_QuotedAttachment(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.Quote.QuotedAttachment) +} +DataMessage_Quote_QuotedAttachment::DataMessage_Quote_QuotedAttachment(const DataMessage_Quote_QuotedAttachment& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_Quote_QuotedAttachment* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.contenttype_){} + , decltype(_impl_.filename_){} + , decltype(_impl_.thumbnail_){nullptr} + , decltype(_impl_.flags_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.contenttype_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.contenttype_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_contenttype()) { + _this->_impl_.contenttype_.Set(from._internal_contenttype(), + _this->GetArenaForAllocation()); + } + _impl_.filename_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_filename()) { + _this->_impl_.filename_.Set(from._internal_filename(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_thumbnail()) { + _this->_impl_.thumbnail_ = new ::SessionProtos::AttachmentPointer(*from._impl_.thumbnail_); + } + _this->_impl_.flags_ = from._impl_.flags_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.Quote.QuotedAttachment) +} + +inline void DataMessage_Quote_QuotedAttachment::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.contenttype_){} + , decltype(_impl_.filename_){} + , decltype(_impl_.thumbnail_){nullptr} + , decltype(_impl_.flags_){0u} + }; + _impl_.contenttype_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.contenttype_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_Quote_QuotedAttachment::~DataMessage_Quote_QuotedAttachment() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.Quote.QuotedAttachment) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_Quote_QuotedAttachment::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.contenttype_.Destroy(); + _impl_.filename_.Destroy(); + if (this != internal_default_instance()) delete _impl_.thumbnail_; +} + +void DataMessage_Quote_QuotedAttachment::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_Quote_QuotedAttachment::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.Quote.QuotedAttachment) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.contenttype_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.filename_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + GOOGLE_DCHECK(_impl_.thumbnail_ != nullptr); + _impl_.thumbnail_->Clear(); + } + } + _impl_.flags_ = 0u; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_Quote_QuotedAttachment::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional string contentType = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_contenttype(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string fileName = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_filename(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.AttachmentPointer thumbnail = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_thumbnail(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 flags = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 32)) { + _Internal::set_has_flags(&has_bits); + _impl_.flags_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_Quote_QuotedAttachment::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.Quote.QuotedAttachment) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional string contentType = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_contenttype(), target); + } + + // optional string fileName = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_filename(), target); + } + + // optional .SessionProtos.AttachmentPointer thumbnail = 3; + if (cached_has_bits & 0x00000004u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::thumbnail(this), + _Internal::thumbnail(this).GetCachedSize(), target, stream); + } + + // optional uint32 flags = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_flags(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.Quote.QuotedAttachment) + return target; +} + +size_t DataMessage_Quote_QuotedAttachment::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.Quote.QuotedAttachment) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional string contentType = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_contenttype()); + } + + // optional string fileName = 2; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_filename()); + } + + // optional .SessionProtos.AttachmentPointer thumbnail = 3; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.thumbnail_); + } + + // optional uint32 flags = 4; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_flags()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_Quote_QuotedAttachment::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_Quote_QuotedAttachment::MergeFrom(const DataMessage_Quote_QuotedAttachment& from) { + DataMessage_Quote_QuotedAttachment* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.Quote.QuotedAttachment) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_contenttype(from._internal_contenttype()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_filename(from._internal_filename()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_mutable_thumbnail()->::SessionProtos::AttachmentPointer::MergeFrom( + from._internal_thumbnail()); + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.flags_ = from._impl_.flags_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_Quote_QuotedAttachment::CopyFrom(const DataMessage_Quote_QuotedAttachment& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.Quote.QuotedAttachment) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_Quote_QuotedAttachment::IsInitialized() const { + if (_internal_has_thumbnail()) { + if (!_impl_.thumbnail_->IsInitialized()) return false; + } + return true; +} + +void DataMessage_Quote_QuotedAttachment::InternalSwap(DataMessage_Quote_QuotedAttachment* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.contenttype_, lhs_arena, + &other->_impl_.contenttype_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.filename_, lhs_arena, + &other->_impl_.filename_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(DataMessage_Quote_QuotedAttachment, _impl_.flags_) + + sizeof(DataMessage_Quote_QuotedAttachment::_impl_.flags_) + - PROTOBUF_FIELD_OFFSET(DataMessage_Quote_QuotedAttachment, _impl_.thumbnail_)>( + reinterpret_cast(&_impl_.thumbnail_), + reinterpret_cast(&other->_impl_.thumbnail_)); +} + +std::string DataMessage_Quote_QuotedAttachment::GetTypeName() const { + return "SessionProtos.DataMessage.Quote.QuotedAttachment"; +} + + +// =================================================================== + +class DataMessage_Quote::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_id(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_author(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_text(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000005) ^ 0x00000005) != 0; + } +}; + +DataMessage_Quote::DataMessage_Quote(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.Quote) +} +DataMessage_Quote::DataMessage_Quote(const DataMessage_Quote& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_Quote* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.attachments_){from._impl_.attachments_} + , decltype(_impl_.author_){} + , decltype(_impl_.text_){} + , decltype(_impl_.id_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_author()) { + _this->_impl_.author_.Set(from._internal_author(), + _this->GetArenaForAllocation()); + } + _impl_.text_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.text_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_text()) { + _this->_impl_.text_.Set(from._internal_text(), + _this->GetArenaForAllocation()); + } + _this->_impl_.id_ = from._impl_.id_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.Quote) +} + +inline void DataMessage_Quote::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.attachments_){arena} + , decltype(_impl_.author_){} + , decltype(_impl_.text_){} + , decltype(_impl_.id_){uint64_t{0u}} + }; + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.text_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.text_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_Quote::~DataMessage_Quote() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.Quote) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_Quote::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.attachments_.~RepeatedPtrField(); + _impl_.author_.Destroy(); + _impl_.text_.Destroy(); +} + +void DataMessage_Quote::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_Quote::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.Quote) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.attachments_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.author_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.text_.ClearNonDefaultToEmpty(); + } + } + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_Quote::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint64 id = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_id(&has_bits); + _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string author = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_author(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string text = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_text(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated .SessionProtos.DataMessage.Quote.QuotedAttachment attachments = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_attachments(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<34>(ptr)); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_Quote::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.Quote) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint64 id = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target); + } + + // required string author = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_author(), target); + } + + // optional string text = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_text(), target); + } + + // repeated .SessionProtos.DataMessage.Quote.QuotedAttachment attachments = 4; + for (unsigned i = 0, + n = static_cast(this->_internal_attachments_size()); i < n; i++) { + const auto& repfield = this->_internal_attachments(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(4, repfield, repfield.GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.Quote) + return target; +} + +size_t DataMessage_Quote::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.DataMessage.Quote) + size_t total_size = 0; + + if (_internal_has_author()) { + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + } + + if (_internal_has_id()) { + // required uint64 id = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + } + + return total_size; +} +size_t DataMessage_Quote::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.Quote) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000005) ^ 0x00000005) == 0) { // All required fields are present. + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + + // required uint64 id = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated .SessionProtos.DataMessage.Quote.QuotedAttachment attachments = 4; + total_size += 1UL * this->_internal_attachments_size(); + for (const auto& msg : this->_impl_.attachments_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + // optional string text = 3; + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_text()); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_Quote::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_Quote::MergeFrom(const DataMessage_Quote& from) { + DataMessage_Quote* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.Quote) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.attachments_.MergeFrom(from._impl_.attachments_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_author(from._internal_author()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_text(from._internal_text()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.id_ = from._impl_.id_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_Quote::CopyFrom(const DataMessage_Quote& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.Quote) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_Quote::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.attachments_)) + return false; + return true; +} + +void DataMessage_Quote::InternalSwap(DataMessage_Quote* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.attachments_.InternalSwap(&other->_impl_.attachments_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.author_, lhs_arena, + &other->_impl_.author_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.text_, lhs_arena, + &other->_impl_.text_, rhs_arena + ); + swap(_impl_.id_, other->_impl_.id_); +} + +std::string DataMessage_Quote::GetTypeName() const { + return "SessionProtos.DataMessage.Quote"; +} + + +// =================================================================== + +class DataMessage_Preview::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_url(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_title(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::SessionProtos::AttachmentPointer& image(const DataMessage_Preview* msg); + static void set_has_image(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000001) ^ 0x00000001) != 0; + } +}; + +const ::SessionProtos::AttachmentPointer& +DataMessage_Preview::_Internal::image(const DataMessage_Preview* msg) { + return *msg->_impl_.image_; +} +DataMessage_Preview::DataMessage_Preview(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.Preview) +} +DataMessage_Preview::DataMessage_Preview(const DataMessage_Preview& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_Preview* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.url_){} + , decltype(_impl_.title_){} + , decltype(_impl_.image_){nullptr}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_url()) { + _this->_impl_.url_.Set(from._internal_url(), + _this->GetArenaForAllocation()); + } + _impl_.title_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.title_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_title()) { + _this->_impl_.title_.Set(from._internal_title(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_image()) { + _this->_impl_.image_ = new ::SessionProtos::AttachmentPointer(*from._impl_.image_); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.Preview) +} + +inline void DataMessage_Preview::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.url_){} + , decltype(_impl_.title_){} + , decltype(_impl_.image_){nullptr} + }; + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.title_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.title_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_Preview::~DataMessage_Preview() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.Preview) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_Preview::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.url_.Destroy(); + _impl_.title_.Destroy(); + if (this != internal_default_instance()) delete _impl_.image_; +} + +void DataMessage_Preview::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_Preview::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.Preview) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.url_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.title_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + GOOGLE_DCHECK(_impl_.image_ != nullptr); + _impl_.image_->Clear(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_Preview::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required string url = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_url(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string title = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_title(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.AttachmentPointer image = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_image(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_Preview::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.Preview) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required string url = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_url(), target); + } + + // optional string title = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_title(), target); + } + + // optional .SessionProtos.AttachmentPointer image = 3; + if (cached_has_bits & 0x00000004u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::image(this), + _Internal::image(this).GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.Preview) + return target; +} + +size_t DataMessage_Preview::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.Preview) + size_t total_size = 0; + + // required string url = 1; + if (_internal_has_url()) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_url()); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000006u) { + // optional string title = 2; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_title()); + } + + // optional .SessionProtos.AttachmentPointer image = 3; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.image_); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_Preview::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_Preview::MergeFrom(const DataMessage_Preview& from) { + DataMessage_Preview* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.Preview) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_url(from._internal_url()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_title(from._internal_title()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_mutable_image()->::SessionProtos::AttachmentPointer::MergeFrom( + from._internal_image()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_Preview::CopyFrom(const DataMessage_Preview& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.Preview) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_Preview::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + if (_internal_has_image()) { + if (!_impl_.image_->IsInitialized()) return false; + } + return true; +} + +void DataMessage_Preview::InternalSwap(DataMessage_Preview* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.url_, lhs_arena, + &other->_impl_.url_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.title_, lhs_arena, + &other->_impl_.title_, rhs_arena + ); + swap(_impl_.image_, other->_impl_.image_); +} + +std::string DataMessage_Preview::GetTypeName() const { + return "SessionProtos.DataMessage.Preview"; +} + + +// =================================================================== + +class DataMessage_Reaction::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_id(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_author(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_emoji(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_action(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x0000000d) ^ 0x0000000d) != 0; + } +}; + +DataMessage_Reaction::DataMessage_Reaction(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.Reaction) +} +DataMessage_Reaction::DataMessage_Reaction(const DataMessage_Reaction& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_Reaction* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.author_){} + , decltype(_impl_.emoji_){} + , decltype(_impl_.id_){} + , decltype(_impl_.action_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_author()) { + _this->_impl_.author_.Set(from._internal_author(), + _this->GetArenaForAllocation()); + } + _impl_.emoji_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.emoji_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_emoji()) { + _this->_impl_.emoji_.Set(from._internal_emoji(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.id_, &from._impl_.id_, + static_cast(reinterpret_cast(&_impl_.action_) - + reinterpret_cast(&_impl_.id_)) + sizeof(_impl_.action_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.Reaction) +} + +inline void DataMessage_Reaction::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.author_){} + , decltype(_impl_.emoji_){} + , decltype(_impl_.id_){uint64_t{0u}} + , decltype(_impl_.action_){0} + }; + _impl_.author_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.author_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.emoji_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.emoji_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_Reaction::~DataMessage_Reaction() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.Reaction) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_Reaction::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.author_.Destroy(); + _impl_.emoji_.Destroy(); +} + +void DataMessage_Reaction::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_Reaction::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.Reaction) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.author_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.emoji_.ClearNonDefaultToEmpty(); + } + } + if (cached_has_bits & 0x0000000cu) { + ::memset(&_impl_.id_, 0, static_cast( + reinterpret_cast(&_impl_.action_) - + reinterpret_cast(&_impl_.id_)) + sizeof(_impl_.action_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_Reaction::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint64 id = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_id(&has_bits); + _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string author = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_author(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string emoji = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_emoji(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required .SessionProtos.DataMessage.Reaction.Action action = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 32)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::DataMessage_Reaction_Action_IsValid(val))) { + _internal_set_action(static_cast<::SessionProtos::DataMessage_Reaction_Action>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(4, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_Reaction::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.Reaction) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint64 id = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target); + } + + // required string author = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_author(), target); + } + + // optional string emoji = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_emoji(), target); + } + + // required .SessionProtos.DataMessage.Reaction.Action action = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 4, this->_internal_action(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.Reaction) + return target; +} + +size_t DataMessage_Reaction::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.DataMessage.Reaction) + size_t total_size = 0; + + if (_internal_has_author()) { + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + } + + if (_internal_has_id()) { + // required uint64 id = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + } + + if (_internal_has_action()) { + // required .SessionProtos.DataMessage.Reaction.Action action = 4; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_action()); + } + + return total_size; +} +size_t DataMessage_Reaction::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.Reaction) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x0000000d) ^ 0x0000000d) == 0) { // All required fields are present. + // required string author = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_author()); + + // required uint64 id = 1; + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + + // required .SessionProtos.DataMessage.Reaction.Action action = 4; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_action()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // optional string emoji = 3; + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_emoji()); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_Reaction::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_Reaction::MergeFrom(const DataMessage_Reaction& from) { + DataMessage_Reaction* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.Reaction) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_author(from._internal_author()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_emoji(from._internal_emoji()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.id_ = from._impl_.id_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.action_ = from._impl_.action_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_Reaction::CopyFrom(const DataMessage_Reaction& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.Reaction) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_Reaction::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void DataMessage_Reaction::InternalSwap(DataMessage_Reaction* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.author_, lhs_arena, + &other->_impl_.author_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.emoji_, lhs_arena, + &other->_impl_.emoji_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(DataMessage_Reaction, _impl_.action_) + + sizeof(DataMessage_Reaction::_impl_.action_) + - PROTOBUF_FIELD_OFFSET(DataMessage_Reaction, _impl_.id_)>( + reinterpret_cast(&_impl_.id_), + reinterpret_cast(&other->_impl_.id_)); +} + +std::string DataMessage_Reaction::GetTypeName() const { + return "SessionProtos.DataMessage.Reaction"; +} + + +// =================================================================== + +class DataMessage_OpenGroupInvitation::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_url(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_name(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +DataMessage_OpenGroupInvitation::DataMessage_OpenGroupInvitation(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.OpenGroupInvitation) +} +DataMessage_OpenGroupInvitation::DataMessage_OpenGroupInvitation(const DataMessage_OpenGroupInvitation& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_OpenGroupInvitation* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.url_){} + , decltype(_impl_.name_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_url()) { + _this->_impl_.url_.Set(from._internal_url(), + _this->GetArenaForAllocation()); + } + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_name()) { + _this->_impl_.name_.Set(from._internal_name(), + _this->GetArenaForAllocation()); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.OpenGroupInvitation) +} + +inline void DataMessage_OpenGroupInvitation::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.url_){} + , decltype(_impl_.name_){} + }; + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_OpenGroupInvitation::~DataMessage_OpenGroupInvitation() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.OpenGroupInvitation) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_OpenGroupInvitation::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.url_.Destroy(); + _impl_.name_.Destroy(); +} + +void DataMessage_OpenGroupInvitation::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_OpenGroupInvitation::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.OpenGroupInvitation) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.url_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.name_.ClearNonDefaultToEmpty(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_OpenGroupInvitation::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required string url = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_url(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string name = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_name(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_OpenGroupInvitation::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.OpenGroupInvitation) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required string url = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_url(), target); + } + + // required string name = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_name(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.OpenGroupInvitation) + return target; +} + +size_t DataMessage_OpenGroupInvitation::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.DataMessage.OpenGroupInvitation) + size_t total_size = 0; + + if (_internal_has_url()) { + // required string url = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_url()); + } + + if (_internal_has_name()) { + // required string name = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + } + + return total_size; +} +size_t DataMessage_OpenGroupInvitation::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.OpenGroupInvitation) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required string url = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_url()); + + // required string name = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_OpenGroupInvitation::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_OpenGroupInvitation::MergeFrom(const DataMessage_OpenGroupInvitation& from) { + DataMessage_OpenGroupInvitation* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.OpenGroupInvitation) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_url(from._internal_url()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_name(from._internal_name()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_OpenGroupInvitation::CopyFrom(const DataMessage_OpenGroupInvitation& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.OpenGroupInvitation) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_OpenGroupInvitation::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void DataMessage_OpenGroupInvitation::InternalSwap(DataMessage_OpenGroupInvitation* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.url_, lhs_arena, + &other->_impl_.url_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.name_, lhs_arena, + &other->_impl_.name_, rhs_arena + ); +} + +std::string DataMessage_OpenGroupInvitation::GetTypeName() const { + return "SessionProtos.DataMessage.OpenGroupInvitation"; +} + + +// =================================================================== + +class DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_publickey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_encryptedkeypair(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +DataMessage_ClosedGroupControlMessage_KeyPairWrapper::DataMessage_ClosedGroupControlMessage_KeyPairWrapper(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) +} +DataMessage_ClosedGroupControlMessage_KeyPairWrapper::DataMessage_ClosedGroupControlMessage_KeyPairWrapper(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_ClosedGroupControlMessage_KeyPairWrapper* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.encryptedkeypair_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_publickey()) { + _this->_impl_.publickey_.Set(from._internal_publickey(), + _this->GetArenaForAllocation()); + } + _impl_.encryptedkeypair_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.encryptedkeypair_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_encryptedkeypair()) { + _this->_impl_.encryptedkeypair_.Set(from._internal_encryptedkeypair(), + _this->GetArenaForAllocation()); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) +} + +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.encryptedkeypair_){} + }; + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.encryptedkeypair_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.encryptedkeypair_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_ClosedGroupControlMessage_KeyPairWrapper::~DataMessage_ClosedGroupControlMessage_KeyPairWrapper() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.publickey_.Destroy(); + _impl_.encryptedkeypair_.Destroy(); +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.publickey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.encryptedkeypair_.ClearNonDefaultToEmpty(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required bytes publicKey = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_publickey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required bytes encryptedKeyPair = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_encryptedkeypair(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required bytes publicKey = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 1, this->_internal_publickey(), target); + } + + // required bytes encryptedKeyPair = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_encryptedkeypair(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + return target; +} + +size_t DataMessage_ClosedGroupControlMessage_KeyPairWrapper::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + size_t total_size = 0; + + if (_internal_has_publickey()) { + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + } + + if (_internal_has_encryptedkeypair()) { + // required bytes encryptedKeyPair = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_encryptedkeypair()); + } + + return total_size; +} +size_t DataMessage_ClosedGroupControlMessage_KeyPairWrapper::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + + // required bytes encryptedKeyPair = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_encryptedkeypair()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::MergeFrom(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from) { + DataMessage_ClosedGroupControlMessage_KeyPairWrapper* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_publickey(from._internal_publickey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_encryptedkeypair(from._internal_encryptedkeypair()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::CopyFrom(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_ClosedGroupControlMessage_KeyPairWrapper::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::InternalSwap(DataMessage_ClosedGroupControlMessage_KeyPairWrapper* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.publickey_, lhs_arena, + &other->_impl_.publickey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.encryptedkeypair_, lhs_arena, + &other->_impl_.encryptedkeypair_, rhs_arena + ); +} + +std::string DataMessage_ClosedGroupControlMessage_KeyPairWrapper::GetTypeName() const { + return "SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper"; +} + + +// =================================================================== + +class DataMessage_ClosedGroupControlMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static void set_has_publickey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_name(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::SessionProtos::KeyPair& encryptionkeypair(const DataMessage_ClosedGroupControlMessage* msg); + static void set_has_encryptionkeypair(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_expirationtimer(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000010) ^ 0x00000010) != 0; + } +}; + +const ::SessionProtos::KeyPair& +DataMessage_ClosedGroupControlMessage::_Internal::encryptionkeypair(const DataMessage_ClosedGroupControlMessage* msg) { + return *msg->_impl_.encryptionkeypair_; +} +DataMessage_ClosedGroupControlMessage::DataMessage_ClosedGroupControlMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage.ClosedGroupControlMessage) +} +DataMessage_ClosedGroupControlMessage::DataMessage_ClosedGroupControlMessage(const DataMessage_ClosedGroupControlMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage_ClosedGroupControlMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.members_){from._impl_.members_} + , decltype(_impl_.admins_){from._impl_.admins_} + , decltype(_impl_.wrappers_){from._impl_.wrappers_} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.encryptionkeypair_){nullptr} + , decltype(_impl_.expirationtimer_){} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_publickey()) { + _this->_impl_.publickey_.Set(from._internal_publickey(), + _this->GetArenaForAllocation()); + } + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_name()) { + _this->_impl_.name_.Set(from._internal_name(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_encryptionkeypair()) { + _this->_impl_.encryptionkeypair_ = new ::SessionProtos::KeyPair(*from._impl_.encryptionkeypair_); + } + ::memcpy(&_impl_.expirationtimer_, &from._impl_.expirationtimer_, + static_cast(reinterpret_cast(&_impl_.type_) - + reinterpret_cast(&_impl_.expirationtimer_)) + sizeof(_impl_.type_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage.ClosedGroupControlMessage) +} + +inline void DataMessage_ClosedGroupControlMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.members_){arena} + , decltype(_impl_.admins_){arena} + , decltype(_impl_.wrappers_){arena} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.encryptionkeypair_){nullptr} + , decltype(_impl_.expirationtimer_){0u} + , decltype(_impl_.type_){1} + }; + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage_ClosedGroupControlMessage::~DataMessage_ClosedGroupControlMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage.ClosedGroupControlMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage_ClosedGroupControlMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.members_.~RepeatedPtrField(); + _impl_.admins_.~RepeatedPtrField(); + _impl_.wrappers_.~RepeatedPtrField(); + _impl_.publickey_.Destroy(); + _impl_.name_.Destroy(); + if (this != internal_default_instance()) delete _impl_.encryptionkeypair_; +} + +void DataMessage_ClosedGroupControlMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage_ClosedGroupControlMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage.ClosedGroupControlMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.members_.Clear(); + _impl_.admins_.Clear(); + _impl_.wrappers_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.publickey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.name_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + GOOGLE_DCHECK(_impl_.encryptionkeypair_ != nullptr); + _impl_.encryptionkeypair_->Clear(); + } + } + if (cached_has_bits & 0x00000018u) { + _impl_.expirationtimer_ = 0u; + _impl_.type_ = 1; + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage_ClosedGroupControlMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.DataMessage.ClosedGroupControlMessage.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type_IsValid(val))) { + _internal_set_type(static_cast<::SessionProtos::DataMessage_ClosedGroupControlMessage_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // optional bytes publicKey = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_publickey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string name = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_name(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.KeyPair encryptionKeyPair = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + ptr = ctx->ParseMessage(_internal_mutable_encryptionkeypair(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated bytes members = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_members(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr)); + } else + goto handle_unusual; + continue; + // repeated bytes admins = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_admins(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<50>(ptr)); + } else + goto handle_unusual; + continue; + // repeated .SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper wrappers = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 58)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_wrappers(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<58>(ptr)); + } else + goto handle_unusual; + continue; + // optional uint32 expirationTimer = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 64)) { + _Internal::set_has_expirationtimer(&has_bits); + _impl_.expirationtimer_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage_ClosedGroupControlMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage.ClosedGroupControlMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.DataMessage.ClosedGroupControlMessage.Type type = 1; + if (cached_has_bits & 0x00000010u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // optional bytes publicKey = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_publickey(), target); + } + + // optional string name = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_name(), target); + } + + // optional .SessionProtos.KeyPair encryptionKeyPair = 4; + if (cached_has_bits & 0x00000004u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(4, _Internal::encryptionkeypair(this), + _Internal::encryptionkeypair(this).GetCachedSize(), target, stream); + } + + // repeated bytes members = 5; + for (int i = 0, n = this->_internal_members_size(); i < n; i++) { + const auto& s = this->_internal_members(i); + target = stream->WriteBytes(5, s, target); + } + + // repeated bytes admins = 6; + for (int i = 0, n = this->_internal_admins_size(); i < n; i++) { + const auto& s = this->_internal_admins(i); + target = stream->WriteBytes(6, s, target); + } + + // repeated .SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper wrappers = 7; + for (unsigned i = 0, + n = static_cast(this->_internal_wrappers_size()); i < n; i++) { + const auto& repfield = this->_internal_wrappers(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(7, repfield, repfield.GetCachedSize(), target, stream); + } + + // optional uint32 expirationTimer = 8; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(8, this->_internal_expirationtimer(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage.ClosedGroupControlMessage) + return target; +} + +size_t DataMessage_ClosedGroupControlMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage.ClosedGroupControlMessage) + size_t total_size = 0; + + // required .SessionProtos.DataMessage.ClosedGroupControlMessage.Type type = 1; + if (_internal_has_type()) { + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated bytes members = 5; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.members_.size()); + for (int i = 0, n = _impl_.members_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + _impl_.members_.Get(i)); + } + + // repeated bytes admins = 6; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.admins_.size()); + for (int i = 0, n = _impl_.admins_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + _impl_.admins_.Get(i)); + } + + // repeated .SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper wrappers = 7; + total_size += 1UL * this->_internal_wrappers_size(); + for (const auto& msg : this->_impl_.wrappers_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional bytes publicKey = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + } + + // optional string name = 3; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + } + + // optional .SessionProtos.KeyPair encryptionKeyPair = 4; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.encryptionkeypair_); + } + + // optional uint32 expirationTimer = 8; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_expirationtimer()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage_ClosedGroupControlMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage_ClosedGroupControlMessage::MergeFrom(const DataMessage_ClosedGroupControlMessage& from) { + DataMessage_ClosedGroupControlMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage.ClosedGroupControlMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.members_.MergeFrom(from._impl_.members_); + _this->_impl_.admins_.MergeFrom(from._impl_.admins_); + _this->_impl_.wrappers_.MergeFrom(from._impl_.wrappers_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000001fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_publickey(from._internal_publickey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_name(from._internal_name()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_mutable_encryptionkeypair()->::SessionProtos::KeyPair::MergeFrom( + from._internal_encryptionkeypair()); + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.expirationtimer_ = from._impl_.expirationtimer_; + } + if (cached_has_bits & 0x00000010u) { + _this->_impl_.type_ = from._impl_.type_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage_ClosedGroupControlMessage::CopyFrom(const DataMessage_ClosedGroupControlMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage.ClosedGroupControlMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage_ClosedGroupControlMessage::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.wrappers_)) + return false; + if (_internal_has_encryptionkeypair()) { + if (!_impl_.encryptionkeypair_->IsInitialized()) return false; + } + return true; +} + +void DataMessage_ClosedGroupControlMessage::InternalSwap(DataMessage_ClosedGroupControlMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.members_.InternalSwap(&other->_impl_.members_); + _impl_.admins_.InternalSwap(&other->_impl_.admins_); + _impl_.wrappers_.InternalSwap(&other->_impl_.wrappers_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.publickey_, lhs_arena, + &other->_impl_.publickey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.name_, lhs_arena, + &other->_impl_.name_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(DataMessage_ClosedGroupControlMessage, _impl_.expirationtimer_) + + sizeof(DataMessage_ClosedGroupControlMessage::_impl_.expirationtimer_) + - PROTOBUF_FIELD_OFFSET(DataMessage_ClosedGroupControlMessage, _impl_.encryptionkeypair_)>( + reinterpret_cast(&_impl_.encryptionkeypair_), + reinterpret_cast(&other->_impl_.encryptionkeypair_)); + swap(_impl_.type_, other->_impl_.type_); +} + +std::string DataMessage_ClosedGroupControlMessage::GetTypeName() const { + return "SessionProtos.DataMessage.ClosedGroupControlMessage"; +} + + +// =================================================================== + +class DataMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_body(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_flags(HasBits* has_bits) { + (*has_bits)[0] |= 256u; + } + static void set_has_expiretimer(HasBits* has_bits) { + (*has_bits)[0] |= 512u; + } + static void set_has_profilekey(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 1024u; + } + static const ::SessionProtos::DataMessage_Quote& quote(const DataMessage* msg); + static void set_has_quote(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static const ::SessionProtos::DataMessage_Reaction& reaction(const DataMessage* msg); + static void set_has_reaction(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static const ::SessionProtos::LokiProfile& profile(const DataMessage* msg); + static void set_has_profile(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } + static const ::SessionProtos::DataMessage_OpenGroupInvitation& opengroupinvitation(const DataMessage* msg); + static void set_has_opengroupinvitation(HasBits* has_bits) { + (*has_bits)[0] |= 64u; + } + static const ::SessionProtos::DataMessage_ClosedGroupControlMessage& closedgroupcontrolmessage(const DataMessage* msg); + static void set_has_closedgroupcontrolmessage(HasBits* has_bits) { + (*has_bits)[0] |= 128u; + } + static void set_has_synctarget(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_blockscommunitymessagerequests(HasBits* has_bits) { + (*has_bits)[0] |= 2048u; + } +}; + +const ::SessionProtos::DataMessage_Quote& +DataMessage::_Internal::quote(const DataMessage* msg) { + return *msg->_impl_.quote_; +} +const ::SessionProtos::DataMessage_Reaction& +DataMessage::_Internal::reaction(const DataMessage* msg) { + return *msg->_impl_.reaction_; +} +const ::SessionProtos::LokiProfile& +DataMessage::_Internal::profile(const DataMessage* msg) { + return *msg->_impl_.profile_; +} +const ::SessionProtos::DataMessage_OpenGroupInvitation& +DataMessage::_Internal::opengroupinvitation(const DataMessage* msg) { + return *msg->_impl_.opengroupinvitation_; +} +const ::SessionProtos::DataMessage_ClosedGroupControlMessage& +DataMessage::_Internal::closedgroupcontrolmessage(const DataMessage* msg) { + return *msg->_impl_.closedgroupcontrolmessage_; +} +DataMessage::DataMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.DataMessage) +} +DataMessage::DataMessage(const DataMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + DataMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.attachments_){from._impl_.attachments_} + , decltype(_impl_.preview_){from._impl_.preview_} + , decltype(_impl_.body_){} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.synctarget_){} + , decltype(_impl_.quote_){nullptr} + , decltype(_impl_.reaction_){nullptr} + , decltype(_impl_.profile_){nullptr} + , decltype(_impl_.opengroupinvitation_){nullptr} + , decltype(_impl_.closedgroupcontrolmessage_){nullptr} + , decltype(_impl_.flags_){} + , decltype(_impl_.expiretimer_){} + , decltype(_impl_.timestamp_){} + , decltype(_impl_.blockscommunitymessagerequests_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_body()) { + _this->_impl_.body_.Set(from._internal_body(), + _this->GetArenaForAllocation()); + } + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilekey()) { + _this->_impl_.profilekey_.Set(from._internal_profilekey(), + _this->GetArenaForAllocation()); + } + _impl_.synctarget_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.synctarget_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_synctarget()) { + _this->_impl_.synctarget_.Set(from._internal_synctarget(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_quote()) { + _this->_impl_.quote_ = new ::SessionProtos::DataMessage_Quote(*from._impl_.quote_); + } + if (from._internal_has_reaction()) { + _this->_impl_.reaction_ = new ::SessionProtos::DataMessage_Reaction(*from._impl_.reaction_); + } + if (from._internal_has_profile()) { + _this->_impl_.profile_ = new ::SessionProtos::LokiProfile(*from._impl_.profile_); + } + if (from._internal_has_opengroupinvitation()) { + _this->_impl_.opengroupinvitation_ = new ::SessionProtos::DataMessage_OpenGroupInvitation(*from._impl_.opengroupinvitation_); + } + if (from._internal_has_closedgroupcontrolmessage()) { + _this->_impl_.closedgroupcontrolmessage_ = new ::SessionProtos::DataMessage_ClosedGroupControlMessage(*from._impl_.closedgroupcontrolmessage_); + } + ::memcpy(&_impl_.flags_, &from._impl_.flags_, + static_cast(reinterpret_cast(&_impl_.blockscommunitymessagerequests_) - + reinterpret_cast(&_impl_.flags_)) + sizeof(_impl_.blockscommunitymessagerequests_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.DataMessage) +} + +inline void DataMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.attachments_){arena} + , decltype(_impl_.preview_){arena} + , decltype(_impl_.body_){} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.synctarget_){} + , decltype(_impl_.quote_){nullptr} + , decltype(_impl_.reaction_){nullptr} + , decltype(_impl_.profile_){nullptr} + , decltype(_impl_.opengroupinvitation_){nullptr} + , decltype(_impl_.closedgroupcontrolmessage_){nullptr} + , decltype(_impl_.flags_){0u} + , decltype(_impl_.expiretimer_){0u} + , decltype(_impl_.timestamp_){uint64_t{0u}} + , decltype(_impl_.blockscommunitymessagerequests_){false} + }; + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.synctarget_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.synctarget_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +DataMessage::~DataMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.DataMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void DataMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.attachments_.~RepeatedPtrField(); + _impl_.preview_.~RepeatedPtrField(); + _impl_.body_.Destroy(); + _impl_.profilekey_.Destroy(); + _impl_.synctarget_.Destroy(); + if (this != internal_default_instance()) delete _impl_.quote_; + if (this != internal_default_instance()) delete _impl_.reaction_; + if (this != internal_default_instance()) delete _impl_.profile_; + if (this != internal_default_instance()) delete _impl_.opengroupinvitation_; + if (this != internal_default_instance()) delete _impl_.closedgroupcontrolmessage_; +} + +void DataMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void DataMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.DataMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.attachments_.Clear(); + _impl_.preview_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + if (cached_has_bits & 0x00000001u) { + _impl_.body_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.profilekey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + _impl_.synctarget_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000008u) { + GOOGLE_DCHECK(_impl_.quote_ != nullptr); + _impl_.quote_->Clear(); + } + if (cached_has_bits & 0x00000010u) { + GOOGLE_DCHECK(_impl_.reaction_ != nullptr); + _impl_.reaction_->Clear(); + } + if (cached_has_bits & 0x00000020u) { + GOOGLE_DCHECK(_impl_.profile_ != nullptr); + _impl_.profile_->Clear(); + } + if (cached_has_bits & 0x00000040u) { + GOOGLE_DCHECK(_impl_.opengroupinvitation_ != nullptr); + _impl_.opengroupinvitation_->Clear(); + } + if (cached_has_bits & 0x00000080u) { + GOOGLE_DCHECK(_impl_.closedgroupcontrolmessage_ != nullptr); + _impl_.closedgroupcontrolmessage_->Clear(); + } + } + if (cached_has_bits & 0x00000f00u) { + ::memset(&_impl_.flags_, 0, static_cast( + reinterpret_cast(&_impl_.blockscommunitymessagerequests_) - + reinterpret_cast(&_impl_.flags_)) + sizeof(_impl_.blockscommunitymessagerequests_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* DataMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional string body = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_body(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated .SessionProtos.AttachmentPointer attachments = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_attachments(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<18>(ptr)); + } else + goto handle_unusual; + continue; + // optional uint32 flags = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 32)) { + _Internal::set_has_flags(&has_bits); + _impl_.flags_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 expireTimer = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 40)) { + _Internal::set_has_expiretimer(&has_bits); + _impl_.expiretimer_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes profileKey = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + auto str = _internal_mutable_profilekey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint64 timestamp = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 56)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.DataMessage.Quote quote = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 66)) { + ptr = ctx->ParseMessage(_internal_mutable_quote(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated .SessionProtos.DataMessage.Preview preview = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 82)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_preview(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<82>(ptr)); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.DataMessage.Reaction reaction = 11; + case 11: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 90)) { + ptr = ctx->ParseMessage(_internal_mutable_reaction(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.LokiProfile profile = 101; + case 101: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr = ctx->ParseMessage(_internal_mutable_profile(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.DataMessage.OpenGroupInvitation openGroupInvitation = 102; + case 102: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + ptr = ctx->ParseMessage(_internal_mutable_opengroupinvitation(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; + case 104: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 66)) { + ptr = ctx->ParseMessage(_internal_mutable_closedgroupcontrolmessage(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string syncTarget = 105; + case 105: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 74)) { + auto str = _internal_mutable_synctarget(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool blocksCommunityMessageRequests = 106; + case 106: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 80)) { + _Internal::set_has_blockscommunitymessagerequests(&has_bits); + _impl_.blockscommunitymessagerequests_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* DataMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.DataMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional string body = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_body(), target); + } + + // repeated .SessionProtos.AttachmentPointer attachments = 2; + for (unsigned i = 0, + n = static_cast(this->_internal_attachments_size()); i < n; i++) { + const auto& repfield = this->_internal_attachments(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(2, repfield, repfield.GetCachedSize(), target, stream); + } + + // optional uint32 flags = 4; + if (cached_has_bits & 0x00000100u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_flags(), target); + } + + // optional uint32 expireTimer = 5; + if (cached_has_bits & 0x00000200u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(5, this->_internal_expiretimer(), target); + } + + // optional bytes profileKey = 6; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 6, this->_internal_profilekey(), target); + } + + // optional uint64 timestamp = 7; + if (cached_has_bits & 0x00000400u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(7, this->_internal_timestamp(), target); + } + + // optional .SessionProtos.DataMessage.Quote quote = 8; + if (cached_has_bits & 0x00000008u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(8, _Internal::quote(this), + _Internal::quote(this).GetCachedSize(), target, stream); + } + + // repeated .SessionProtos.DataMessage.Preview preview = 10; + for (unsigned i = 0, + n = static_cast(this->_internal_preview_size()); i < n; i++) { + const auto& repfield = this->_internal_preview(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(10, repfield, repfield.GetCachedSize(), target, stream); + } + + // optional .SessionProtos.DataMessage.Reaction reaction = 11; + if (cached_has_bits & 0x00000010u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(11, _Internal::reaction(this), + _Internal::reaction(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.LokiProfile profile = 101; + if (cached_has_bits & 0x00000020u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(101, _Internal::profile(this), + _Internal::profile(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.DataMessage.OpenGroupInvitation openGroupInvitation = 102; + if (cached_has_bits & 0x00000040u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(102, _Internal::opengroupinvitation(this), + _Internal::opengroupinvitation(this).GetCachedSize(), target, stream); + } + + // optional .SessionProtos.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; + if (cached_has_bits & 0x00000080u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(104, _Internal::closedgroupcontrolmessage(this), + _Internal::closedgroupcontrolmessage(this).GetCachedSize(), target, stream); + } + + // optional string syncTarget = 105; + if (cached_has_bits & 0x00000004u) { + target = stream->WriteStringMaybeAliased( + 105, this->_internal_synctarget(), target); + } + + // optional bool blocksCommunityMessageRequests = 106; + if (cached_has_bits & 0x00000800u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(106, this->_internal_blockscommunitymessagerequests(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.DataMessage) + return target; +} + +size_t DataMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.DataMessage) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated .SessionProtos.AttachmentPointer attachments = 2; + total_size += 1UL * this->_internal_attachments_size(); + for (const auto& msg : this->_impl_.attachments_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + // repeated .SessionProtos.DataMessage.Preview preview = 10; + total_size += 1UL * this->_internal_preview_size(); + for (const auto& msg : this->_impl_.preview_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + // optional string body = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_body()); + } + + // optional bytes profileKey = 6; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_profilekey()); + } + + // optional string syncTarget = 105; + if (cached_has_bits & 0x00000004u) { + total_size += 2 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_synctarget()); + } + + // optional .SessionProtos.DataMessage.Quote quote = 8; + if (cached_has_bits & 0x00000008u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.quote_); + } + + // optional .SessionProtos.DataMessage.Reaction reaction = 11; + if (cached_has_bits & 0x00000010u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.reaction_); + } + + // optional .SessionProtos.LokiProfile profile = 101; + if (cached_has_bits & 0x00000020u) { + total_size += 2 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.profile_); + } + + // optional .SessionProtos.DataMessage.OpenGroupInvitation openGroupInvitation = 102; + if (cached_has_bits & 0x00000040u) { + total_size += 2 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.opengroupinvitation_); + } + + // optional .SessionProtos.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; + if (cached_has_bits & 0x00000080u) { + total_size += 2 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.closedgroupcontrolmessage_); + } + + } + if (cached_has_bits & 0x00000f00u) { + // optional uint32 flags = 4; + if (cached_has_bits & 0x00000100u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_flags()); + } + + // optional uint32 expireTimer = 5; + if (cached_has_bits & 0x00000200u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_expiretimer()); + } + + // optional uint64 timestamp = 7; + if (cached_has_bits & 0x00000400u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + // optional bool blocksCommunityMessageRequests = 106; + if (cached_has_bits & 0x00000800u) { + total_size += 2 + 1; + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void DataMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void DataMessage::MergeFrom(const DataMessage& from) { + DataMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.DataMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.attachments_.MergeFrom(from._impl_.attachments_); + _this->_impl_.preview_.MergeFrom(from._impl_.preview_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_body(from._internal_body()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_profilekey(from._internal_profilekey()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_set_synctarget(from._internal_synctarget()); + } + if (cached_has_bits & 0x00000008u) { + _this->_internal_mutable_quote()->::SessionProtos::DataMessage_Quote::MergeFrom( + from._internal_quote()); + } + if (cached_has_bits & 0x00000010u) { + _this->_internal_mutable_reaction()->::SessionProtos::DataMessage_Reaction::MergeFrom( + from._internal_reaction()); + } + if (cached_has_bits & 0x00000020u) { + _this->_internal_mutable_profile()->::SessionProtos::LokiProfile::MergeFrom( + from._internal_profile()); + } + if (cached_has_bits & 0x00000040u) { + _this->_internal_mutable_opengroupinvitation()->::SessionProtos::DataMessage_OpenGroupInvitation::MergeFrom( + from._internal_opengroupinvitation()); + } + if (cached_has_bits & 0x00000080u) { + _this->_internal_mutable_closedgroupcontrolmessage()->::SessionProtos::DataMessage_ClosedGroupControlMessage::MergeFrom( + from._internal_closedgroupcontrolmessage()); + } + } + if (cached_has_bits & 0x00000f00u) { + if (cached_has_bits & 0x00000100u) { + _this->_impl_.flags_ = from._impl_.flags_; + } + if (cached_has_bits & 0x00000200u) { + _this->_impl_.expiretimer_ = from._impl_.expiretimer_; + } + if (cached_has_bits & 0x00000400u) { + _this->_impl_.timestamp_ = from._impl_.timestamp_; + } + if (cached_has_bits & 0x00000800u) { + _this->_impl_.blockscommunitymessagerequests_ = from._impl_.blockscommunitymessagerequests_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void DataMessage::CopyFrom(const DataMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.DataMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool DataMessage::IsInitialized() const { + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.attachments_)) + return false; + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.preview_)) + return false; + if (_internal_has_quote()) { + if (!_impl_.quote_->IsInitialized()) return false; + } + if (_internal_has_reaction()) { + if (!_impl_.reaction_->IsInitialized()) return false; + } + if (_internal_has_opengroupinvitation()) { + if (!_impl_.opengroupinvitation_->IsInitialized()) return false; + } + if (_internal_has_closedgroupcontrolmessage()) { + if (!_impl_.closedgroupcontrolmessage_->IsInitialized()) return false; + } + return true; +} + +void DataMessage::InternalSwap(DataMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.attachments_.InternalSwap(&other->_impl_.attachments_); + _impl_.preview_.InternalSwap(&other->_impl_.preview_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.body_, lhs_arena, + &other->_impl_.body_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilekey_, lhs_arena, + &other->_impl_.profilekey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.synctarget_, lhs_arena, + &other->_impl_.synctarget_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(DataMessage, _impl_.blockscommunitymessagerequests_) + + sizeof(DataMessage::_impl_.blockscommunitymessagerequests_) + - PROTOBUF_FIELD_OFFSET(DataMessage, _impl_.quote_)>( + reinterpret_cast(&_impl_.quote_), + reinterpret_cast(&other->_impl_.quote_)); +} + +std::string DataMessage::GetTypeName() const { + return "SessionProtos.DataMessage"; +} + + +// =================================================================== + +class ConfigurationMessage_ClosedGroup::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_publickey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_name(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::SessionProtos::KeyPair& encryptionkeypair(const ConfigurationMessage_ClosedGroup* msg); + static void set_has_encryptionkeypair(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_expirationtimer(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } +}; + +const ::SessionProtos::KeyPair& +ConfigurationMessage_ClosedGroup::_Internal::encryptionkeypair(const ConfigurationMessage_ClosedGroup* msg) { + return *msg->_impl_.encryptionkeypair_; +} +ConfigurationMessage_ClosedGroup::ConfigurationMessage_ClosedGroup(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.ConfigurationMessage.ClosedGroup) +} +ConfigurationMessage_ClosedGroup::ConfigurationMessage_ClosedGroup(const ConfigurationMessage_ClosedGroup& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + ConfigurationMessage_ClosedGroup* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.members_){from._impl_.members_} + , decltype(_impl_.admins_){from._impl_.admins_} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.encryptionkeypair_){nullptr} + , decltype(_impl_.expirationtimer_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_publickey()) { + _this->_impl_.publickey_.Set(from._internal_publickey(), + _this->GetArenaForAllocation()); + } + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_name()) { + _this->_impl_.name_.Set(from._internal_name(), + _this->GetArenaForAllocation()); + } + if (from._internal_has_encryptionkeypair()) { + _this->_impl_.encryptionkeypair_ = new ::SessionProtos::KeyPair(*from._impl_.encryptionkeypair_); + } + _this->_impl_.expirationtimer_ = from._impl_.expirationtimer_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.ConfigurationMessage.ClosedGroup) +} + +inline void ConfigurationMessage_ClosedGroup::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.members_){arena} + , decltype(_impl_.admins_){arena} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.encryptionkeypair_){nullptr} + , decltype(_impl_.expirationtimer_){0u} + }; + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +ConfigurationMessage_ClosedGroup::~ConfigurationMessage_ClosedGroup() { + // @@protoc_insertion_point(destructor:SessionProtos.ConfigurationMessage.ClosedGroup) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void ConfigurationMessage_ClosedGroup::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.members_.~RepeatedPtrField(); + _impl_.admins_.~RepeatedPtrField(); + _impl_.publickey_.Destroy(); + _impl_.name_.Destroy(); + if (this != internal_default_instance()) delete _impl_.encryptionkeypair_; +} + +void ConfigurationMessage_ClosedGroup::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void ConfigurationMessage_ClosedGroup::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.ConfigurationMessage.ClosedGroup) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.members_.Clear(); + _impl_.admins_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.publickey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.name_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + GOOGLE_DCHECK(_impl_.encryptionkeypair_ != nullptr); + _impl_.encryptionkeypair_->Clear(); + } + } + _impl_.expirationtimer_ = 0u; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* ConfigurationMessage_ClosedGroup::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional bytes publicKey = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_publickey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string name = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_name(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .SessionProtos.KeyPair encryptionKeyPair = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_encryptionkeypair(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated bytes members = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_members(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<34>(ptr)); + } else + goto handle_unusual; + continue; + // repeated bytes admins = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_admins(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr)); + } else + goto handle_unusual; + continue; + // optional uint32 expirationTimer = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 48)) { + _Internal::set_has_expirationtimer(&has_bits); + _impl_.expirationtimer_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* ConfigurationMessage_ClosedGroup::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.ConfigurationMessage.ClosedGroup) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional bytes publicKey = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 1, this->_internal_publickey(), target); + } + + // optional string name = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_name(), target); + } + + // optional .SessionProtos.KeyPair encryptionKeyPair = 3; + if (cached_has_bits & 0x00000004u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::encryptionkeypair(this), + _Internal::encryptionkeypair(this).GetCachedSize(), target, stream); + } + + // repeated bytes members = 4; + for (int i = 0, n = this->_internal_members_size(); i < n; i++) { + const auto& s = this->_internal_members(i); + target = stream->WriteBytes(4, s, target); + } + + // repeated bytes admins = 5; + for (int i = 0, n = this->_internal_admins_size(); i < n; i++) { + const auto& s = this->_internal_admins(i); + target = stream->WriteBytes(5, s, target); + } + + // optional uint32 expirationTimer = 6; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(6, this->_internal_expirationtimer(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.ConfigurationMessage.ClosedGroup) + return target; +} + +size_t ConfigurationMessage_ClosedGroup::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.ConfigurationMessage.ClosedGroup) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated bytes members = 4; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.members_.size()); + for (int i = 0, n = _impl_.members_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + _impl_.members_.Get(i)); + } + + // repeated bytes admins = 5; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.admins_.size()); + for (int i = 0, n = _impl_.admins_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + _impl_.admins_.Get(i)); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional bytes publicKey = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + } + + // optional string name = 2; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + } + + // optional .SessionProtos.KeyPair encryptionKeyPair = 3; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.encryptionkeypair_); + } + + // optional uint32 expirationTimer = 6; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_expirationtimer()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void ConfigurationMessage_ClosedGroup::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void ConfigurationMessage_ClosedGroup::MergeFrom(const ConfigurationMessage_ClosedGroup& from) { + ConfigurationMessage_ClosedGroup* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.ConfigurationMessage.ClosedGroup) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.members_.MergeFrom(from._impl_.members_); + _this->_impl_.admins_.MergeFrom(from._impl_.admins_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_publickey(from._internal_publickey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_name(from._internal_name()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_mutable_encryptionkeypair()->::SessionProtos::KeyPair::MergeFrom( + from._internal_encryptionkeypair()); + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.expirationtimer_ = from._impl_.expirationtimer_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void ConfigurationMessage_ClosedGroup::CopyFrom(const ConfigurationMessage_ClosedGroup& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.ConfigurationMessage.ClosedGroup) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool ConfigurationMessage_ClosedGroup::IsInitialized() const { + if (_internal_has_encryptionkeypair()) { + if (!_impl_.encryptionkeypair_->IsInitialized()) return false; + } + return true; +} + +void ConfigurationMessage_ClosedGroup::InternalSwap(ConfigurationMessage_ClosedGroup* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.members_.InternalSwap(&other->_impl_.members_); + _impl_.admins_.InternalSwap(&other->_impl_.admins_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.publickey_, lhs_arena, + &other->_impl_.publickey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.name_, lhs_arena, + &other->_impl_.name_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(ConfigurationMessage_ClosedGroup, _impl_.expirationtimer_) + + sizeof(ConfigurationMessage_ClosedGroup::_impl_.expirationtimer_) + - PROTOBUF_FIELD_OFFSET(ConfigurationMessage_ClosedGroup, _impl_.encryptionkeypair_)>( + reinterpret_cast(&_impl_.encryptionkeypair_), + reinterpret_cast(&other->_impl_.encryptionkeypair_)); +} + +std::string ConfigurationMessage_ClosedGroup::GetTypeName() const { + return "SessionProtos.ConfigurationMessage.ClosedGroup"; +} + + +// =================================================================== + +class ConfigurationMessage_Contact::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_publickey(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_name(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_profilepicture(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_profilekey(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static void set_has_isapproved(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static void set_has_isblocked(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } + static void set_has_didapproveme(HasBits* has_bits) { + (*has_bits)[0] |= 64u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000003) ^ 0x00000003) != 0; + } +}; + +ConfigurationMessage_Contact::ConfigurationMessage_Contact(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.ConfigurationMessage.Contact) +} +ConfigurationMessage_Contact::ConfigurationMessage_Contact(const ConfigurationMessage_Contact& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + ConfigurationMessage_Contact* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.profilepicture_){} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.isapproved_){} + , decltype(_impl_.isblocked_){} + , decltype(_impl_.didapproveme_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_publickey()) { + _this->_impl_.publickey_.Set(from._internal_publickey(), + _this->GetArenaForAllocation()); + } + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_name()) { + _this->_impl_.name_.Set(from._internal_name(), + _this->GetArenaForAllocation()); + } + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilepicture()) { + _this->_impl_.profilepicture_.Set(from._internal_profilepicture(), + _this->GetArenaForAllocation()); + } + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilekey()) { + _this->_impl_.profilekey_.Set(from._internal_profilekey(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.isapproved_, &from._impl_.isapproved_, + static_cast(reinterpret_cast(&_impl_.didapproveme_) - + reinterpret_cast(&_impl_.isapproved_)) + sizeof(_impl_.didapproveme_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.ConfigurationMessage.Contact) +} + +inline void ConfigurationMessage_Contact::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.publickey_){} + , decltype(_impl_.name_){} + , decltype(_impl_.profilepicture_){} + , decltype(_impl_.profilekey_){} + , decltype(_impl_.isapproved_){false} + , decltype(_impl_.isblocked_){false} + , decltype(_impl_.didapproveme_){false} + }; + _impl_.publickey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.publickey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.name_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +ConfigurationMessage_Contact::~ConfigurationMessage_Contact() { + // @@protoc_insertion_point(destructor:SessionProtos.ConfigurationMessage.Contact) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void ConfigurationMessage_Contact::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.publickey_.Destroy(); + _impl_.name_.Destroy(); + _impl_.profilepicture_.Destroy(); + _impl_.profilekey_.Destroy(); +} + +void ConfigurationMessage_Contact::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void ConfigurationMessage_Contact::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.ConfigurationMessage.Contact) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _impl_.publickey_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.name_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + _impl_.profilepicture_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000008u) { + _impl_.profilekey_.ClearNonDefaultToEmpty(); + } + } + ::memset(&_impl_.isapproved_, 0, static_cast( + reinterpret_cast(&_impl_.didapproveme_) - + reinterpret_cast(&_impl_.isapproved_)) + sizeof(_impl_.didapproveme_)); + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* ConfigurationMessage_Contact::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required bytes publicKey = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_publickey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string name = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_name(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string profilePicture = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_profilepicture(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes profileKey = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + auto str = _internal_mutable_profilekey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool isApproved = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 40)) { + _Internal::set_has_isapproved(&has_bits); + _impl_.isapproved_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool isBlocked = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 48)) { + _Internal::set_has_isblocked(&has_bits); + _impl_.isblocked_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool didApproveMe = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 56)) { + _Internal::set_has_didapproveme(&has_bits); + _impl_.didapproveme_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* ConfigurationMessage_Contact::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.ConfigurationMessage.Contact) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required bytes publicKey = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 1, this->_internal_publickey(), target); + } + + // required string name = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_name(), target); + } + + // optional string profilePicture = 3; + if (cached_has_bits & 0x00000004u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_profilepicture(), target); + } + + // optional bytes profileKey = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->WriteBytesMaybeAliased( + 4, this->_internal_profilekey(), target); + } + + // optional bool isApproved = 5; + if (cached_has_bits & 0x00000010u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(5, this->_internal_isapproved(), target); + } + + // optional bool isBlocked = 6; + if (cached_has_bits & 0x00000020u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(6, this->_internal_isblocked(), target); + } + + // optional bool didApproveMe = 7; + if (cached_has_bits & 0x00000040u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(7, this->_internal_didapproveme(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.ConfigurationMessage.Contact) + return target; +} + +size_t ConfigurationMessage_Contact::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.ConfigurationMessage.Contact) + size_t total_size = 0; + + if (_internal_has_publickey()) { + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + } + + if (_internal_has_name()) { + // required string name = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + } + + return total_size; +} +size_t ConfigurationMessage_Contact::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.ConfigurationMessage.Contact) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000003) ^ 0x00000003) == 0) { // All required fields are present. + // required bytes publicKey = 1; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_publickey()); + + // required string name = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_name()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000007cu) { + // optional string profilePicture = 3; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_profilepicture()); + } + + // optional bytes profileKey = 4; + if (cached_has_bits & 0x00000008u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_profilekey()); + } + + // optional bool isApproved = 5; + if (cached_has_bits & 0x00000010u) { + total_size += 1 + 1; + } + + // optional bool isBlocked = 6; + if (cached_has_bits & 0x00000020u) { + total_size += 1 + 1; + } + + // optional bool didApproveMe = 7; + if (cached_has_bits & 0x00000040u) { + total_size += 1 + 1; + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void ConfigurationMessage_Contact::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void ConfigurationMessage_Contact::MergeFrom(const ConfigurationMessage_Contact& from) { + ConfigurationMessage_Contact* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.ConfigurationMessage.Contact) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000007fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_publickey(from._internal_publickey()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_name(from._internal_name()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_set_profilepicture(from._internal_profilepicture()); + } + if (cached_has_bits & 0x00000008u) { + _this->_internal_set_profilekey(from._internal_profilekey()); + } + if (cached_has_bits & 0x00000010u) { + _this->_impl_.isapproved_ = from._impl_.isapproved_; + } + if (cached_has_bits & 0x00000020u) { + _this->_impl_.isblocked_ = from._impl_.isblocked_; + } + if (cached_has_bits & 0x00000040u) { + _this->_impl_.didapproveme_ = from._impl_.didapproveme_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void ConfigurationMessage_Contact::CopyFrom(const ConfigurationMessage_Contact& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.ConfigurationMessage.Contact) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool ConfigurationMessage_Contact::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void ConfigurationMessage_Contact::InternalSwap(ConfigurationMessage_Contact* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.publickey_, lhs_arena, + &other->_impl_.publickey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.name_, lhs_arena, + &other->_impl_.name_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilepicture_, lhs_arena, + &other->_impl_.profilepicture_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilekey_, lhs_arena, + &other->_impl_.profilekey_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(ConfigurationMessage_Contact, _impl_.didapproveme_) + + sizeof(ConfigurationMessage_Contact::_impl_.didapproveme_) + - PROTOBUF_FIELD_OFFSET(ConfigurationMessage_Contact, _impl_.isapproved_)>( + reinterpret_cast(&_impl_.isapproved_), + reinterpret_cast(&other->_impl_.isapproved_)); +} + +std::string ConfigurationMessage_Contact::GetTypeName() const { + return "SessionProtos.ConfigurationMessage.Contact"; +} + + +// =================================================================== + +class ConfigurationMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_displayname(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_profilepicture(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_profilekey(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } +}; + +ConfigurationMessage::ConfigurationMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.ConfigurationMessage) +} +ConfigurationMessage::ConfigurationMessage(const ConfigurationMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + ConfigurationMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.closedgroups_){from._impl_.closedgroups_} + , decltype(_impl_.opengroups_){from._impl_.opengroups_} + , decltype(_impl_.contacts_){from._impl_.contacts_} + , decltype(_impl_.displayname_){} + , decltype(_impl_.profilepicture_){} + , decltype(_impl_.profilekey_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.displayname_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.displayname_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_displayname()) { + _this->_impl_.displayname_.Set(from._internal_displayname(), + _this->GetArenaForAllocation()); + } + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilepicture()) { + _this->_impl_.profilepicture_.Set(from._internal_profilepicture(), + _this->GetArenaForAllocation()); + } + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_profilekey()) { + _this->_impl_.profilekey_.Set(from._internal_profilekey(), + _this->GetArenaForAllocation()); + } + // @@protoc_insertion_point(copy_constructor:SessionProtos.ConfigurationMessage) +} + +inline void ConfigurationMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.closedgroups_){arena} + , decltype(_impl_.opengroups_){arena} + , decltype(_impl_.contacts_){arena} + , decltype(_impl_.displayname_){} + , decltype(_impl_.profilepicture_){} + , decltype(_impl_.profilekey_){} + }; + _impl_.displayname_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.displayname_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.profilekey_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +ConfigurationMessage::~ConfigurationMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.ConfigurationMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void ConfigurationMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.closedgroups_.~RepeatedPtrField(); + _impl_.opengroups_.~RepeatedPtrField(); + _impl_.contacts_.~RepeatedPtrField(); + _impl_.displayname_.Destroy(); + _impl_.profilepicture_.Destroy(); + _impl_.profilekey_.Destroy(); +} + +void ConfigurationMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void ConfigurationMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.ConfigurationMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.closedgroups_.Clear(); + _impl_.opengroups_.Clear(); + _impl_.contacts_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.displayname_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.profilepicture_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + _impl_.profilekey_.ClearNonDefaultToEmpty(); + } + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* ConfigurationMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // repeated .SessionProtos.ConfigurationMessage.ClosedGroup closedGroups = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_closedgroups(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<10>(ptr)); + } else + goto handle_unusual; + continue; + // repeated string openGroups = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_opengroups(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<18>(ptr)); + } else + goto handle_unusual; + continue; + // optional string displayName = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_displayname(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string profilePicture = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + auto str = _internal_mutable_profilepicture(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes profileKey = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + auto str = _internal_mutable_profilekey(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated .SessionProtos.ConfigurationMessage.Contact contacts = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_contacts(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<50>(ptr)); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* ConfigurationMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.ConfigurationMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + // repeated .SessionProtos.ConfigurationMessage.ClosedGroup closedGroups = 1; + for (unsigned i = 0, + n = static_cast(this->_internal_closedgroups_size()); i < n; i++) { + const auto& repfield = this->_internal_closedgroups(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(1, repfield, repfield.GetCachedSize(), target, stream); + } + + // repeated string openGroups = 2; + for (int i = 0, n = this->_internal_opengroups_size(); i < n; i++) { + const auto& s = this->_internal_opengroups(i); + target = stream->WriteString(2, s, target); + } + + cached_has_bits = _impl_._has_bits_[0]; + // optional string displayName = 3; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_displayname(), target); + } + + // optional string profilePicture = 4; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 4, this->_internal_profilepicture(), target); + } + + // optional bytes profileKey = 5; + if (cached_has_bits & 0x00000004u) { + target = stream->WriteBytesMaybeAliased( + 5, this->_internal_profilekey(), target); + } + + // repeated .SessionProtos.ConfigurationMessage.Contact contacts = 6; + for (unsigned i = 0, + n = static_cast(this->_internal_contacts_size()); i < n; i++) { + const auto& repfield = this->_internal_contacts(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(6, repfield, repfield.GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.ConfigurationMessage) + return target; +} + +size_t ConfigurationMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.ConfigurationMessage) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated .SessionProtos.ConfigurationMessage.ClosedGroup closedGroups = 1; + total_size += 1UL * this->_internal_closedgroups_size(); + for (const auto& msg : this->_impl_.closedgroups_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + // repeated string openGroups = 2; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.opengroups_.size()); + for (int i = 0, n = _impl_.opengroups_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + _impl_.opengroups_.Get(i)); + } + + // repeated .SessionProtos.ConfigurationMessage.Contact contacts = 6; + total_size += 1UL * this->_internal_contacts_size(); + for (const auto& msg : this->_impl_.contacts_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + // optional string displayName = 3; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_displayname()); + } + + // optional string profilePicture = 4; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_profilepicture()); + } + + // optional bytes profileKey = 5; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_profilekey()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void ConfigurationMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void ConfigurationMessage::MergeFrom(const ConfigurationMessage& from) { + ConfigurationMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.ConfigurationMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.closedgroups_.MergeFrom(from._impl_.closedgroups_); + _this->_impl_.opengroups_.MergeFrom(from._impl_.opengroups_); + _this->_impl_.contacts_.MergeFrom(from._impl_.contacts_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_displayname(from._internal_displayname()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_profilepicture(from._internal_profilepicture()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_set_profilekey(from._internal_profilekey()); + } + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void ConfigurationMessage::CopyFrom(const ConfigurationMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.ConfigurationMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool ConfigurationMessage::IsInitialized() const { + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.closedgroups_)) + return false; + if (!::PROTOBUF_NAMESPACE_ID::internal::AllAreInitialized(_impl_.contacts_)) + return false; + return true; +} + +void ConfigurationMessage::InternalSwap(ConfigurationMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.closedgroups_.InternalSwap(&other->_impl_.closedgroups_); + _impl_.opengroups_.InternalSwap(&other->_impl_.opengroups_); + _impl_.contacts_.InternalSwap(&other->_impl_.contacts_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.displayname_, lhs_arena, + &other->_impl_.displayname_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilepicture_, lhs_arena, + &other->_impl_.profilepicture_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.profilekey_, lhs_arena, + &other->_impl_.profilekey_, rhs_arena + ); +} + +std::string ConfigurationMessage::GetTypeName() const { + return "SessionProtos.ConfigurationMessage"; +} + + +// =================================================================== + +class ReceiptMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000001) ^ 0x00000001) != 0; + } +}; + +ReceiptMessage::ReceiptMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.ReceiptMessage) +} +ReceiptMessage::ReceiptMessage(const ReceiptMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + ReceiptMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){from._impl_.timestamp_} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _this->_impl_.type_ = from._impl_.type_; + // @@protoc_insertion_point(copy_constructor:SessionProtos.ReceiptMessage) +} + +inline void ReceiptMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){arena} + , decltype(_impl_.type_){0} + }; +} + +ReceiptMessage::~ReceiptMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.ReceiptMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void ReceiptMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.timestamp_.~RepeatedField(); +} + +void ReceiptMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void ReceiptMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.ReceiptMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.timestamp_.Clear(); + _impl_.type_ = 0; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* ReceiptMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.ReceiptMessage.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::ReceiptMessage_Type_IsValid(val))) { + _internal_set_type(static_cast<::SessionProtos::ReceiptMessage_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // repeated uint64 timestamp = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 16)) { + ptr -= 1; + do { + ptr += 1; + _internal_add_timestamp(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<16>(ptr)); + } else if (static_cast(tag) == 18) { + ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedUInt64Parser(_internal_mutable_timestamp(), ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* ReceiptMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.ReceiptMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.ReceiptMessage.Type type = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // repeated uint64 timestamp = 2; + for (int i = 0, n = this->_internal_timestamp_size(); i < n; i++) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(2, this->_internal_timestamp(i), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.ReceiptMessage) + return target; +} + +size_t ReceiptMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.ReceiptMessage) + size_t total_size = 0; + + // required .SessionProtos.ReceiptMessage.Type type = 1; + if (_internal_has_type()) { + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated uint64 timestamp = 2; + { + size_t data_size = ::_pbi::WireFormatLite:: + UInt64Size(this->_impl_.timestamp_); + total_size += 1 * + ::_pbi::FromIntSize(this->_internal_timestamp_size()); + total_size += data_size; + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void ReceiptMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void ReceiptMessage::MergeFrom(const ReceiptMessage& from) { + ReceiptMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.ReceiptMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.timestamp_.MergeFrom(from._impl_.timestamp_); + if (from._internal_has_type()) { + _this->_internal_set_type(from._internal_type()); + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void ReceiptMessage::CopyFrom(const ReceiptMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.ReceiptMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool ReceiptMessage::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void ReceiptMessage::InternalSwap(ReceiptMessage* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.timestamp_.InternalSwap(&other->_impl_.timestamp_); + swap(_impl_.type_, other->_impl_.type_); +} + +std::string ReceiptMessage::GetTypeName() const { + return "SessionProtos.ReceiptMessage"; +} + + +// =================================================================== + +class AttachmentPointer::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_id(HasBits* has_bits) { + (*has_bits)[0] |= 128u; + } + static void set_has_contenttype(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_key(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_size(HasBits* has_bits) { + (*has_bits)[0] |= 256u; + } + static void set_has_thumbnail(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_digest(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static void set_has_filename(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static void set_has_flags(HasBits* has_bits) { + (*has_bits)[0] |= 512u; + } + static void set_has_width(HasBits* has_bits) { + (*has_bits)[0] |= 1024u; + } + static void set_has_height(HasBits* has_bits) { + (*has_bits)[0] |= 2048u; + } + static void set_has_caption(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } + static void set_has_url(HasBits* has_bits) { + (*has_bits)[0] |= 64u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000080) ^ 0x00000080) != 0; + } +}; + +AttachmentPointer::AttachmentPointer(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.AttachmentPointer) +} +AttachmentPointer::AttachmentPointer(const AttachmentPointer& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + AttachmentPointer* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.contenttype_){} + , decltype(_impl_.key_){} + , decltype(_impl_.thumbnail_){} + , decltype(_impl_.digest_){} + , decltype(_impl_.filename_){} + , decltype(_impl_.caption_){} + , decltype(_impl_.url_){} + , decltype(_impl_.id_){} + , decltype(_impl_.size_){} + , decltype(_impl_.flags_){} + , decltype(_impl_.width_){} + , decltype(_impl_.height_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.contenttype_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.contenttype_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_contenttype()) { + _this->_impl_.contenttype_.Set(from._internal_contenttype(), + _this->GetArenaForAllocation()); + } + _impl_.key_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.key_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_key()) { + _this->_impl_.key_.Set(from._internal_key(), + _this->GetArenaForAllocation()); + } + _impl_.thumbnail_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.thumbnail_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_thumbnail()) { + _this->_impl_.thumbnail_.Set(from._internal_thumbnail(), + _this->GetArenaForAllocation()); + } + _impl_.digest_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.digest_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_digest()) { + _this->_impl_.digest_.Set(from._internal_digest(), + _this->GetArenaForAllocation()); + } + _impl_.filename_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_filename()) { + _this->_impl_.filename_.Set(from._internal_filename(), + _this->GetArenaForAllocation()); + } + _impl_.caption_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.caption_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_caption()) { + _this->_impl_.caption_.Set(from._internal_caption(), + _this->GetArenaForAllocation()); + } + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_url()) { + _this->_impl_.url_.Set(from._internal_url(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.id_, &from._impl_.id_, + static_cast(reinterpret_cast(&_impl_.height_) - + reinterpret_cast(&_impl_.id_)) + sizeof(_impl_.height_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.AttachmentPointer) +} + +inline void AttachmentPointer::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.contenttype_){} + , decltype(_impl_.key_){} + , decltype(_impl_.thumbnail_){} + , decltype(_impl_.digest_){} + , decltype(_impl_.filename_){} + , decltype(_impl_.caption_){} + , decltype(_impl_.url_){} + , decltype(_impl_.id_){uint64_t{0u}} + , decltype(_impl_.size_){0u} + , decltype(_impl_.flags_){0u} + , decltype(_impl_.width_){0u} + , decltype(_impl_.height_){0u} + }; + _impl_.contenttype_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.contenttype_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.key_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.key_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.thumbnail_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.thumbnail_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.digest_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.digest_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.filename_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.caption_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.caption_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.url_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +AttachmentPointer::~AttachmentPointer() { + // @@protoc_insertion_point(destructor:SessionProtos.AttachmentPointer) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void AttachmentPointer::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.contenttype_.Destroy(); + _impl_.key_.Destroy(); + _impl_.thumbnail_.Destroy(); + _impl_.digest_.Destroy(); + _impl_.filename_.Destroy(); + _impl_.caption_.Destroy(); + _impl_.url_.Destroy(); +} + +void AttachmentPointer::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void AttachmentPointer::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.AttachmentPointer) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000007fu) { + if (cached_has_bits & 0x00000001u) { + _impl_.contenttype_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.key_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + _impl_.thumbnail_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000008u) { + _impl_.digest_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000010u) { + _impl_.filename_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000020u) { + _impl_.caption_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000040u) { + _impl_.url_.ClearNonDefaultToEmpty(); + } + } + _impl_.id_ = uint64_t{0u}; + if (cached_has_bits & 0x00000f00u) { + ::memset(&_impl_.size_, 0, static_cast( + reinterpret_cast(&_impl_.height_) - + reinterpret_cast(&_impl_.size_)) + sizeof(_impl_.height_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* AttachmentPointer::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required fixed64 id = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 9)) { + _Internal::set_has_id(&has_bits); + _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::UnalignedLoad(ptr); + ptr += sizeof(uint64_t); + } else + goto handle_unusual; + continue; + // optional string contentType = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_contenttype(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes key = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_key(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 size = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 32)) { + _Internal::set_has_size(&has_bits); + _impl_.size_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes thumbnail = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + auto str = _internal_mutable_thumbnail(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes digest = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 50)) { + auto str = _internal_mutable_digest(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string fileName = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 58)) { + auto str = _internal_mutable_filename(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 flags = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 64)) { + _Internal::set_has_flags(&has_bits); + _impl_.flags_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 width = 9; + case 9: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 72)) { + _Internal::set_has_width(&has_bits); + _impl_.width_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 height = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 80)) { + _Internal::set_has_height(&has_bits); + _impl_.height_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string caption = 11; + case 11: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 90)) { + auto str = _internal_mutable_caption(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string url = 101; + case 101: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + auto str = _internal_mutable_url(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* AttachmentPointer::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.AttachmentPointer) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required fixed64 id = 1; + if (cached_has_bits & 0x00000080u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteFixed64ToArray(1, this->_internal_id(), target); + } + + // optional string contentType = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_contenttype(), target); + } + + // optional bytes key = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 3, this->_internal_key(), target); + } + + // optional uint32 size = 4; + if (cached_has_bits & 0x00000100u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_size(), target); + } + + // optional bytes thumbnail = 5; + if (cached_has_bits & 0x00000004u) { + target = stream->WriteBytesMaybeAliased( + 5, this->_internal_thumbnail(), target); + } + + // optional bytes digest = 6; + if (cached_has_bits & 0x00000008u) { + target = stream->WriteBytesMaybeAliased( + 6, this->_internal_digest(), target); + } + + // optional string fileName = 7; + if (cached_has_bits & 0x00000010u) { + target = stream->WriteStringMaybeAliased( + 7, this->_internal_filename(), target); + } + + // optional uint32 flags = 8; + if (cached_has_bits & 0x00000200u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(8, this->_internal_flags(), target); + } + + // optional uint32 width = 9; + if (cached_has_bits & 0x00000400u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(9, this->_internal_width(), target); + } + + // optional uint32 height = 10; + if (cached_has_bits & 0x00000800u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(10, this->_internal_height(), target); + } + + // optional string caption = 11; + if (cached_has_bits & 0x00000020u) { + target = stream->WriteStringMaybeAliased( + 11, this->_internal_caption(), target); + } + + // optional string url = 101; + if (cached_has_bits & 0x00000040u) { + target = stream->WriteStringMaybeAliased( + 101, this->_internal_url(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.AttachmentPointer) + return target; +} + +size_t AttachmentPointer::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.AttachmentPointer) + size_t total_size = 0; + + // required fixed64 id = 1; + if (_internal_has_id()) { + total_size += 1 + 8; + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000007fu) { + // optional string contentType = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_contenttype()); + } + + // optional bytes key = 3; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_key()); + } + + // optional bytes thumbnail = 5; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_thumbnail()); + } + + // optional bytes digest = 6; + if (cached_has_bits & 0x00000008u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_digest()); + } + + // optional string fileName = 7; + if (cached_has_bits & 0x00000010u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_filename()); + } + + // optional string caption = 11; + if (cached_has_bits & 0x00000020u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_caption()); + } + + // optional string url = 101; + if (cached_has_bits & 0x00000040u) { + total_size += 2 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_url()); + } + + } + if (cached_has_bits & 0x00000f00u) { + // optional uint32 size = 4; + if (cached_has_bits & 0x00000100u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_size()); + } + + // optional uint32 flags = 8; + if (cached_has_bits & 0x00000200u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_flags()); + } + + // optional uint32 width = 9; + if (cached_has_bits & 0x00000400u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_width()); + } + + // optional uint32 height = 10; + if (cached_has_bits & 0x00000800u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_height()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void AttachmentPointer::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void AttachmentPointer::MergeFrom(const AttachmentPointer& from) { + AttachmentPointer* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.AttachmentPointer) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x000000ffu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_contenttype(from._internal_contenttype()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_key(from._internal_key()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_set_thumbnail(from._internal_thumbnail()); + } + if (cached_has_bits & 0x00000008u) { + _this->_internal_set_digest(from._internal_digest()); + } + if (cached_has_bits & 0x00000010u) { + _this->_internal_set_filename(from._internal_filename()); + } + if (cached_has_bits & 0x00000020u) { + _this->_internal_set_caption(from._internal_caption()); + } + if (cached_has_bits & 0x00000040u) { + _this->_internal_set_url(from._internal_url()); + } + if (cached_has_bits & 0x00000080u) { + _this->_impl_.id_ = from._impl_.id_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + if (cached_has_bits & 0x00000f00u) { + if (cached_has_bits & 0x00000100u) { + _this->_impl_.size_ = from._impl_.size_; + } + if (cached_has_bits & 0x00000200u) { + _this->_impl_.flags_ = from._impl_.flags_; + } + if (cached_has_bits & 0x00000400u) { + _this->_impl_.width_ = from._impl_.width_; + } + if (cached_has_bits & 0x00000800u) { + _this->_impl_.height_ = from._impl_.height_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void AttachmentPointer::CopyFrom(const AttachmentPointer& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.AttachmentPointer) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool AttachmentPointer::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void AttachmentPointer::InternalSwap(AttachmentPointer* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.contenttype_, lhs_arena, + &other->_impl_.contenttype_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.key_, lhs_arena, + &other->_impl_.key_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.thumbnail_, lhs_arena, + &other->_impl_.thumbnail_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.digest_, lhs_arena, + &other->_impl_.digest_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.filename_, lhs_arena, + &other->_impl_.filename_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.caption_, lhs_arena, + &other->_impl_.caption_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.url_, lhs_arena, + &other->_impl_.url_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(AttachmentPointer, _impl_.height_) + + sizeof(AttachmentPointer::_impl_.height_) + - PROTOBUF_FIELD_OFFSET(AttachmentPointer, _impl_.id_)>( + reinterpret_cast(&_impl_.id_), + reinterpret_cast(&other->_impl_.id_)); +} + +std::string AttachmentPointer::GetTypeName() const { + return "SessionProtos.AttachmentPointer"; +} + + +// =================================================================== + +class SharedConfigMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_kind(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_seqno(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_data(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000007) ^ 0x00000007) != 0; + } +}; + +SharedConfigMessage::SharedConfigMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:SessionProtos.SharedConfigMessage) +} +SharedConfigMessage::SharedConfigMessage(const SharedConfigMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + SharedConfigMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.data_){} + , decltype(_impl_.seqno_){} + , decltype(_impl_.kind_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.data_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.data_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_data()) { + _this->_impl_.data_.Set(from._internal_data(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.seqno_, &from._impl_.seqno_, + static_cast(reinterpret_cast(&_impl_.kind_) - + reinterpret_cast(&_impl_.seqno_)) + sizeof(_impl_.kind_)); + // @@protoc_insertion_point(copy_constructor:SessionProtos.SharedConfigMessage) +} + +inline void SharedConfigMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.data_){} + , decltype(_impl_.seqno_){int64_t{0}} + , decltype(_impl_.kind_){1} + }; + _impl_.data_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.data_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +SharedConfigMessage::~SharedConfigMessage() { + // @@protoc_insertion_point(destructor:SessionProtos.SharedConfigMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void SharedConfigMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.data_.Destroy(); +} + +void SharedConfigMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void SharedConfigMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:SessionProtos.SharedConfigMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + _impl_.data_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000006u) { + _impl_.seqno_ = int64_t{0}; + _impl_.kind_ = 1; + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* SharedConfigMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required .SessionProtos.SharedConfigMessage.Kind kind = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::SessionProtos::SharedConfigMessage_Kind_IsValid(val))) { + _internal_set_kind(static_cast<::SessionProtos::SharedConfigMessage_Kind>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // required int64 seqno = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 16)) { + _Internal::set_has_seqno(&has_bits); + _impl_.seqno_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required bytes data = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_data(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* SharedConfigMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:SessionProtos.SharedConfigMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required .SessionProtos.SharedConfigMessage.Kind kind = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_kind(), target); + } + + // required int64 seqno = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteInt64ToArray(2, this->_internal_seqno(), target); + } + + // required bytes data = 3; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteBytesMaybeAliased( + 3, this->_internal_data(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:SessionProtos.SharedConfigMessage) + return target; +} + +size_t SharedConfigMessage::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:SessionProtos.SharedConfigMessage) + size_t total_size = 0; + + if (_internal_has_data()) { + // required bytes data = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_data()); + } + + if (_internal_has_seqno()) { + // required int64 seqno = 2; + total_size += ::_pbi::WireFormatLite::Int64SizePlusOne(this->_internal_seqno()); + } + + if (_internal_has_kind()) { + // required .SessionProtos.SharedConfigMessage.Kind kind = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_kind()); + } + + return total_size; +} +size_t SharedConfigMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:SessionProtos.SharedConfigMessage) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000007) ^ 0x00000007) == 0) { // All required fields are present. + // required bytes data = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_data()); + + // required int64 seqno = 2; + total_size += ::_pbi::WireFormatLite::Int64SizePlusOne(this->_internal_seqno()); + + // required .SessionProtos.SharedConfigMessage.Kind kind = 1; + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_kind()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void SharedConfigMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void SharedConfigMessage::MergeFrom(const SharedConfigMessage& from) { + SharedConfigMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:SessionProtos.SharedConfigMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_data(from._internal_data()); + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.seqno_ = from._impl_.seqno_; + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.kind_ = from._impl_.kind_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void SharedConfigMessage::CopyFrom(const SharedConfigMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:SessionProtos.SharedConfigMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool SharedConfigMessage::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void SharedConfigMessage::InternalSwap(SharedConfigMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.data_, lhs_arena, + &other->_impl_.data_, rhs_arena + ); + swap(_impl_.seqno_, other->_impl_.seqno_); + swap(_impl_.kind_, other->_impl_.kind_); +} + +std::string SharedConfigMessage::GetTypeName() const { + return "SessionProtos.SharedConfigMessage"; +} + + +// @@protoc_insertion_point(namespace_scope) +} // namespace SessionProtos +PROTOBUF_NAMESPACE_OPEN +template<> PROTOBUF_NOINLINE ::SessionProtos::Envelope* +Arena::CreateMaybeMessage< ::SessionProtos::Envelope >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::Envelope >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::TypingMessage* +Arena::CreateMaybeMessage< ::SessionProtos::TypingMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::TypingMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::UnsendRequest* +Arena::CreateMaybeMessage< ::SessionProtos::UnsendRequest >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::UnsendRequest >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::MessageRequestResponse* +Arena::CreateMaybeMessage< ::SessionProtos::MessageRequestResponse >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::MessageRequestResponse >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::Content* +Arena::CreateMaybeMessage< ::SessionProtos::Content >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::Content >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::CallMessage* +Arena::CreateMaybeMessage< ::SessionProtos::CallMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::CallMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::KeyPair* +Arena::CreateMaybeMessage< ::SessionProtos::KeyPair >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::KeyPair >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataExtractionNotification* +Arena::CreateMaybeMessage< ::SessionProtos::DataExtractionNotification >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataExtractionNotification >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::LokiProfile* +Arena::CreateMaybeMessage< ::SessionProtos::LokiProfile >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::LokiProfile >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_Quote_QuotedAttachment* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_Quote_QuotedAttachment >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_Quote_QuotedAttachment >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_Quote* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_Quote >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_Quote >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_Preview* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_Preview >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_Preview >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_Reaction* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_Reaction >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_Reaction >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_OpenGroupInvitation* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_OpenGroupInvitation >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_OpenGroupInvitation >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage_ClosedGroupControlMessage* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage_ClosedGroupControlMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage_ClosedGroupControlMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::DataMessage* +Arena::CreateMaybeMessage< ::SessionProtos::DataMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::DataMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::ConfigurationMessage_ClosedGroup* +Arena::CreateMaybeMessage< ::SessionProtos::ConfigurationMessage_ClosedGroup >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::ConfigurationMessage_ClosedGroup >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::ConfigurationMessage_Contact* +Arena::CreateMaybeMessage< ::SessionProtos::ConfigurationMessage_Contact >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::ConfigurationMessage_Contact >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::ConfigurationMessage* +Arena::CreateMaybeMessage< ::SessionProtos::ConfigurationMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::ConfigurationMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::ReceiptMessage* +Arena::CreateMaybeMessage< ::SessionProtos::ReceiptMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::ReceiptMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::AttachmentPointer* +Arena::CreateMaybeMessage< ::SessionProtos::AttachmentPointer >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::AttachmentPointer >(arena); +} +template<> PROTOBUF_NOINLINE ::SessionProtos::SharedConfigMessage* +Arena::CreateMaybeMessage< ::SessionProtos::SharedConfigMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::SessionProtos::SharedConfigMessage >(arena); +} +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) +#include diff --git a/proto/SessionProtos.pb.h b/proto/SessionProtos.pb.h new file mode 100644 index 00000000..e75a5652 --- /dev/null +++ b/proto/SessionProtos.pb.h @@ -0,0 +1,12309 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: SessionProtos.proto + +#ifndef GOOGLE_PROTOBUF_INCLUDED_SessionProtos_2eproto +#define GOOGLE_PROTOBUF_INCLUDED_SessionProtos_2eproto + +#include +#include + +#include +#if PROTOBUF_VERSION < 3021000 +#error This file was generated by a newer version of protoc which is +#error incompatible with your Protocol Buffer headers. Please update +#error your headers. +#endif +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION +#error This file was generated by an older version of protoc which is +#error incompatible with your Protocol Buffer headers. Please +#error regenerate this file with a newer version of protoc. +#endif + +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: export +#include // IWYU pragma: export +#include +// @@protoc_insertion_point(includes) +#include +#define PROTOBUF_INTERNAL_EXPORT_SessionProtos_2eproto +PROTOBUF_NAMESPACE_OPEN +namespace internal { +class AnyMetadata; +} // namespace internal +PROTOBUF_NAMESPACE_CLOSE + +// Internal implementation detail -- do not use these members. +struct TableStruct_SessionProtos_2eproto { + static const uint32_t offsets[]; +}; +namespace SessionProtos { +class AttachmentPointer; +struct AttachmentPointerDefaultTypeInternal; +extern AttachmentPointerDefaultTypeInternal _AttachmentPointer_default_instance_; +class CallMessage; +struct CallMessageDefaultTypeInternal; +extern CallMessageDefaultTypeInternal _CallMessage_default_instance_; +class ConfigurationMessage; +struct ConfigurationMessageDefaultTypeInternal; +extern ConfigurationMessageDefaultTypeInternal _ConfigurationMessage_default_instance_; +class ConfigurationMessage_ClosedGroup; +struct ConfigurationMessage_ClosedGroupDefaultTypeInternal; +extern ConfigurationMessage_ClosedGroupDefaultTypeInternal _ConfigurationMessage_ClosedGroup_default_instance_; +class ConfigurationMessage_Contact; +struct ConfigurationMessage_ContactDefaultTypeInternal; +extern ConfigurationMessage_ContactDefaultTypeInternal _ConfigurationMessage_Contact_default_instance_; +class Content; +struct ContentDefaultTypeInternal; +extern ContentDefaultTypeInternal _Content_default_instance_; +class DataExtractionNotification; +struct DataExtractionNotificationDefaultTypeInternal; +extern DataExtractionNotificationDefaultTypeInternal _DataExtractionNotification_default_instance_; +class DataMessage; +struct DataMessageDefaultTypeInternal; +extern DataMessageDefaultTypeInternal _DataMessage_default_instance_; +class DataMessage_ClosedGroupControlMessage; +struct DataMessage_ClosedGroupControlMessageDefaultTypeInternal; +extern DataMessage_ClosedGroupControlMessageDefaultTypeInternal _DataMessage_ClosedGroupControlMessage_default_instance_; +class DataMessage_ClosedGroupControlMessage_KeyPairWrapper; +struct DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal; +extern DataMessage_ClosedGroupControlMessage_KeyPairWrapperDefaultTypeInternal _DataMessage_ClosedGroupControlMessage_KeyPairWrapper_default_instance_; +class DataMessage_OpenGroupInvitation; +struct DataMessage_OpenGroupInvitationDefaultTypeInternal; +extern DataMessage_OpenGroupInvitationDefaultTypeInternal _DataMessage_OpenGroupInvitation_default_instance_; +class DataMessage_Preview; +struct DataMessage_PreviewDefaultTypeInternal; +extern DataMessage_PreviewDefaultTypeInternal _DataMessage_Preview_default_instance_; +class DataMessage_Quote; +struct DataMessage_QuoteDefaultTypeInternal; +extern DataMessage_QuoteDefaultTypeInternal _DataMessage_Quote_default_instance_; +class DataMessage_Quote_QuotedAttachment; +struct DataMessage_Quote_QuotedAttachmentDefaultTypeInternal; +extern DataMessage_Quote_QuotedAttachmentDefaultTypeInternal _DataMessage_Quote_QuotedAttachment_default_instance_; +class DataMessage_Reaction; +struct DataMessage_ReactionDefaultTypeInternal; +extern DataMessage_ReactionDefaultTypeInternal _DataMessage_Reaction_default_instance_; +class Envelope; +struct EnvelopeDefaultTypeInternal; +extern EnvelopeDefaultTypeInternal _Envelope_default_instance_; +class KeyPair; +struct KeyPairDefaultTypeInternal; +extern KeyPairDefaultTypeInternal _KeyPair_default_instance_; +class LokiProfile; +struct LokiProfileDefaultTypeInternal; +extern LokiProfileDefaultTypeInternal _LokiProfile_default_instance_; +class MessageRequestResponse; +struct MessageRequestResponseDefaultTypeInternal; +extern MessageRequestResponseDefaultTypeInternal _MessageRequestResponse_default_instance_; +class ReceiptMessage; +struct ReceiptMessageDefaultTypeInternal; +extern ReceiptMessageDefaultTypeInternal _ReceiptMessage_default_instance_; +class SharedConfigMessage; +struct SharedConfigMessageDefaultTypeInternal; +extern SharedConfigMessageDefaultTypeInternal _SharedConfigMessage_default_instance_; +class TypingMessage; +struct TypingMessageDefaultTypeInternal; +extern TypingMessageDefaultTypeInternal _TypingMessage_default_instance_; +class UnsendRequest; +struct UnsendRequestDefaultTypeInternal; +extern UnsendRequestDefaultTypeInternal _UnsendRequest_default_instance_; +} // namespace SessionProtos +PROTOBUF_NAMESPACE_OPEN +template<> ::SessionProtos::AttachmentPointer* Arena::CreateMaybeMessage<::SessionProtos::AttachmentPointer>(Arena*); +template<> ::SessionProtos::CallMessage* Arena::CreateMaybeMessage<::SessionProtos::CallMessage>(Arena*); +template<> ::SessionProtos::ConfigurationMessage* Arena::CreateMaybeMessage<::SessionProtos::ConfigurationMessage>(Arena*); +template<> ::SessionProtos::ConfigurationMessage_ClosedGroup* Arena::CreateMaybeMessage<::SessionProtos::ConfigurationMessage_ClosedGroup>(Arena*); +template<> ::SessionProtos::ConfigurationMessage_Contact* Arena::CreateMaybeMessage<::SessionProtos::ConfigurationMessage_Contact>(Arena*); +template<> ::SessionProtos::Content* Arena::CreateMaybeMessage<::SessionProtos::Content>(Arena*); +template<> ::SessionProtos::DataExtractionNotification* Arena::CreateMaybeMessage<::SessionProtos::DataExtractionNotification>(Arena*); +template<> ::SessionProtos::DataMessage* Arena::CreateMaybeMessage<::SessionProtos::DataMessage>(Arena*); +template<> ::SessionProtos::DataMessage_ClosedGroupControlMessage* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_ClosedGroupControlMessage>(Arena*); +template<> ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper>(Arena*); +template<> ::SessionProtos::DataMessage_OpenGroupInvitation* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_OpenGroupInvitation>(Arena*); +template<> ::SessionProtos::DataMessage_Preview* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_Preview>(Arena*); +template<> ::SessionProtos::DataMessage_Quote* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_Quote>(Arena*); +template<> ::SessionProtos::DataMessage_Quote_QuotedAttachment* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_Quote_QuotedAttachment>(Arena*); +template<> ::SessionProtos::DataMessage_Reaction* Arena::CreateMaybeMessage<::SessionProtos::DataMessage_Reaction>(Arena*); +template<> ::SessionProtos::Envelope* Arena::CreateMaybeMessage<::SessionProtos::Envelope>(Arena*); +template<> ::SessionProtos::KeyPair* Arena::CreateMaybeMessage<::SessionProtos::KeyPair>(Arena*); +template<> ::SessionProtos::LokiProfile* Arena::CreateMaybeMessage<::SessionProtos::LokiProfile>(Arena*); +template<> ::SessionProtos::MessageRequestResponse* Arena::CreateMaybeMessage<::SessionProtos::MessageRequestResponse>(Arena*); +template<> ::SessionProtos::ReceiptMessage* Arena::CreateMaybeMessage<::SessionProtos::ReceiptMessage>(Arena*); +template<> ::SessionProtos::SharedConfigMessage* Arena::CreateMaybeMessage<::SessionProtos::SharedConfigMessage>(Arena*); +template<> ::SessionProtos::TypingMessage* Arena::CreateMaybeMessage<::SessionProtos::TypingMessage>(Arena*); +template<> ::SessionProtos::UnsendRequest* Arena::CreateMaybeMessage<::SessionProtos::UnsendRequest>(Arena*); +PROTOBUF_NAMESPACE_CLOSE +namespace SessionProtos { + +enum Envelope_Type : int { + Envelope_Type_SESSION_MESSAGE = 6, + Envelope_Type_CLOSED_GROUP_MESSAGE = 7 +}; +bool Envelope_Type_IsValid(int value); +constexpr Envelope_Type Envelope_Type_Type_MIN = Envelope_Type_SESSION_MESSAGE; +constexpr Envelope_Type Envelope_Type_Type_MAX = Envelope_Type_CLOSED_GROUP_MESSAGE; +constexpr int Envelope_Type_Type_ARRAYSIZE = Envelope_Type_Type_MAX + 1; + +const std::string& Envelope_Type_Name(Envelope_Type value); +template +inline const std::string& Envelope_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Envelope_Type_Name."); + return Envelope_Type_Name(static_cast(enum_t_value)); +} +bool Envelope_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, Envelope_Type* value); +enum TypingMessage_Action : int { + TypingMessage_Action_STARTED = 0, + TypingMessage_Action_STOPPED = 1 +}; +bool TypingMessage_Action_IsValid(int value); +constexpr TypingMessage_Action TypingMessage_Action_Action_MIN = TypingMessage_Action_STARTED; +constexpr TypingMessage_Action TypingMessage_Action_Action_MAX = TypingMessage_Action_STOPPED; +constexpr int TypingMessage_Action_Action_ARRAYSIZE = TypingMessage_Action_Action_MAX + 1; + +const std::string& TypingMessage_Action_Name(TypingMessage_Action value); +template +inline const std::string& TypingMessage_Action_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function TypingMessage_Action_Name."); + return TypingMessage_Action_Name(static_cast(enum_t_value)); +} +bool TypingMessage_Action_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, TypingMessage_Action* value); +enum CallMessage_Type : int { + CallMessage_Type_PRE_OFFER = 6, + CallMessage_Type_OFFER = 1, + CallMessage_Type_ANSWER = 2, + CallMessage_Type_PROVISIONAL_ANSWER = 3, + CallMessage_Type_ICE_CANDIDATES = 4, + CallMessage_Type_END_CALL = 5 +}; +bool CallMessage_Type_IsValid(int value); +constexpr CallMessage_Type CallMessage_Type_Type_MIN = CallMessage_Type_OFFER; +constexpr CallMessage_Type CallMessage_Type_Type_MAX = CallMessage_Type_PRE_OFFER; +constexpr int CallMessage_Type_Type_ARRAYSIZE = CallMessage_Type_Type_MAX + 1; + +const std::string& CallMessage_Type_Name(CallMessage_Type value); +template +inline const std::string& CallMessage_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function CallMessage_Type_Name."); + return CallMessage_Type_Name(static_cast(enum_t_value)); +} +bool CallMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, CallMessage_Type* value); +enum DataExtractionNotification_Type : int { + DataExtractionNotification_Type_SCREENSHOT = 1, + DataExtractionNotification_Type_MEDIA_SAVED = 2 +}; +bool DataExtractionNotification_Type_IsValid(int value); +constexpr DataExtractionNotification_Type DataExtractionNotification_Type_Type_MIN = DataExtractionNotification_Type_SCREENSHOT; +constexpr DataExtractionNotification_Type DataExtractionNotification_Type_Type_MAX = DataExtractionNotification_Type_MEDIA_SAVED; +constexpr int DataExtractionNotification_Type_Type_ARRAYSIZE = DataExtractionNotification_Type_Type_MAX + 1; + +const std::string& DataExtractionNotification_Type_Name(DataExtractionNotification_Type value); +template +inline const std::string& DataExtractionNotification_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function DataExtractionNotification_Type_Name."); + return DataExtractionNotification_Type_Name(static_cast(enum_t_value)); +} +bool DataExtractionNotification_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataExtractionNotification_Type* value); +enum DataMessage_Quote_QuotedAttachment_Flags : int { + DataMessage_Quote_QuotedAttachment_Flags_VOICE_MESSAGE = 1 +}; +bool DataMessage_Quote_QuotedAttachment_Flags_IsValid(int value); +constexpr DataMessage_Quote_QuotedAttachment_Flags DataMessage_Quote_QuotedAttachment_Flags_Flags_MIN = DataMessage_Quote_QuotedAttachment_Flags_VOICE_MESSAGE; +constexpr DataMessage_Quote_QuotedAttachment_Flags DataMessage_Quote_QuotedAttachment_Flags_Flags_MAX = DataMessage_Quote_QuotedAttachment_Flags_VOICE_MESSAGE; +constexpr int DataMessage_Quote_QuotedAttachment_Flags_Flags_ARRAYSIZE = DataMessage_Quote_QuotedAttachment_Flags_Flags_MAX + 1; + +const std::string& DataMessage_Quote_QuotedAttachment_Flags_Name(DataMessage_Quote_QuotedAttachment_Flags value); +template +inline const std::string& DataMessage_Quote_QuotedAttachment_Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function DataMessage_Quote_QuotedAttachment_Flags_Name."); + return DataMessage_Quote_QuotedAttachment_Flags_Name(static_cast(enum_t_value)); +} +bool DataMessage_Quote_QuotedAttachment_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Quote_QuotedAttachment_Flags* value); +enum DataMessage_Reaction_Action : int { + DataMessage_Reaction_Action_REACT = 0, + DataMessage_Reaction_Action_REMOVE = 1 +}; +bool DataMessage_Reaction_Action_IsValid(int value); +constexpr DataMessage_Reaction_Action DataMessage_Reaction_Action_Action_MIN = DataMessage_Reaction_Action_REACT; +constexpr DataMessage_Reaction_Action DataMessage_Reaction_Action_Action_MAX = DataMessage_Reaction_Action_REMOVE; +constexpr int DataMessage_Reaction_Action_Action_ARRAYSIZE = DataMessage_Reaction_Action_Action_MAX + 1; + +const std::string& DataMessage_Reaction_Action_Name(DataMessage_Reaction_Action value); +template +inline const std::string& DataMessage_Reaction_Action_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function DataMessage_Reaction_Action_Name."); + return DataMessage_Reaction_Action_Name(static_cast(enum_t_value)); +} +bool DataMessage_Reaction_Action_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Reaction_Action* value); +enum DataMessage_ClosedGroupControlMessage_Type : int { + DataMessage_ClosedGroupControlMessage_Type_NEW = 1, + DataMessage_ClosedGroupControlMessage_Type_ENCRYPTION_KEY_PAIR = 3, + DataMessage_ClosedGroupControlMessage_Type_NAME_CHANGE = 4, + DataMessage_ClosedGroupControlMessage_Type_MEMBERS_ADDED = 5, + DataMessage_ClosedGroupControlMessage_Type_MEMBERS_REMOVED = 6, + DataMessage_ClosedGroupControlMessage_Type_MEMBER_LEFT = 7, + DataMessage_ClosedGroupControlMessage_Type_ENCRYPTION_KEY_PAIR_REQUEST = 8 +}; +bool DataMessage_ClosedGroupControlMessage_Type_IsValid(int value); +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage_Type_Type_MIN = DataMessage_ClosedGroupControlMessage_Type_NEW; +constexpr DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage_Type_Type_MAX = DataMessage_ClosedGroupControlMessage_Type_ENCRYPTION_KEY_PAIR_REQUEST; +constexpr int DataMessage_ClosedGroupControlMessage_Type_Type_ARRAYSIZE = DataMessage_ClosedGroupControlMessage_Type_Type_MAX + 1; + +const std::string& DataMessage_ClosedGroupControlMessage_Type_Name(DataMessage_ClosedGroupControlMessage_Type value); +template +inline const std::string& DataMessage_ClosedGroupControlMessage_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function DataMessage_ClosedGroupControlMessage_Type_Name."); + return DataMessage_ClosedGroupControlMessage_Type_Name(static_cast(enum_t_value)); +} +bool DataMessage_ClosedGroupControlMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_ClosedGroupControlMessage_Type* value); +enum DataMessage_Flags : int { + DataMessage_Flags_EXPIRATION_TIMER_UPDATE = 2 +}; +bool DataMessage_Flags_IsValid(int value); +constexpr DataMessage_Flags DataMessage_Flags_Flags_MIN = DataMessage_Flags_EXPIRATION_TIMER_UPDATE; +constexpr DataMessage_Flags DataMessage_Flags_Flags_MAX = DataMessage_Flags_EXPIRATION_TIMER_UPDATE; +constexpr int DataMessage_Flags_Flags_ARRAYSIZE = DataMessage_Flags_Flags_MAX + 1; + +const std::string& DataMessage_Flags_Name(DataMessage_Flags value); +template +inline const std::string& DataMessage_Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function DataMessage_Flags_Name."); + return DataMessage_Flags_Name(static_cast(enum_t_value)); +} +bool DataMessage_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, DataMessage_Flags* value); +enum ReceiptMessage_Type : int { + ReceiptMessage_Type_DELIVERY = 0, + ReceiptMessage_Type_READ = 1 +}; +bool ReceiptMessage_Type_IsValid(int value); +constexpr ReceiptMessage_Type ReceiptMessage_Type_Type_MIN = ReceiptMessage_Type_DELIVERY; +constexpr ReceiptMessage_Type ReceiptMessage_Type_Type_MAX = ReceiptMessage_Type_READ; +constexpr int ReceiptMessage_Type_Type_ARRAYSIZE = ReceiptMessage_Type_Type_MAX + 1; + +const std::string& ReceiptMessage_Type_Name(ReceiptMessage_Type value); +template +inline const std::string& ReceiptMessage_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function ReceiptMessage_Type_Name."); + return ReceiptMessage_Type_Name(static_cast(enum_t_value)); +} +bool ReceiptMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, ReceiptMessage_Type* value); +enum AttachmentPointer_Flags : int { + AttachmentPointer_Flags_VOICE_MESSAGE = 1 +}; +bool AttachmentPointer_Flags_IsValid(int value); +constexpr AttachmentPointer_Flags AttachmentPointer_Flags_Flags_MIN = AttachmentPointer_Flags_VOICE_MESSAGE; +constexpr AttachmentPointer_Flags AttachmentPointer_Flags_Flags_MAX = AttachmentPointer_Flags_VOICE_MESSAGE; +constexpr int AttachmentPointer_Flags_Flags_ARRAYSIZE = AttachmentPointer_Flags_Flags_MAX + 1; + +const std::string& AttachmentPointer_Flags_Name(AttachmentPointer_Flags value); +template +inline const std::string& AttachmentPointer_Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function AttachmentPointer_Flags_Name."); + return AttachmentPointer_Flags_Name(static_cast(enum_t_value)); +} +bool AttachmentPointer_Flags_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, AttachmentPointer_Flags* value); +enum SharedConfigMessage_Kind : int { + SharedConfigMessage_Kind_USER_PROFILE = 1, + SharedConfigMessage_Kind_CONTACTS = 2, + SharedConfigMessage_Kind_CONVO_INFO_VOLATILE = 3, + SharedConfigMessage_Kind_USER_GROUPS = 4 +}; +bool SharedConfigMessage_Kind_IsValid(int value); +constexpr SharedConfigMessage_Kind SharedConfigMessage_Kind_Kind_MIN = SharedConfigMessage_Kind_USER_PROFILE; +constexpr SharedConfigMessage_Kind SharedConfigMessage_Kind_Kind_MAX = SharedConfigMessage_Kind_USER_GROUPS; +constexpr int SharedConfigMessage_Kind_Kind_ARRAYSIZE = SharedConfigMessage_Kind_Kind_MAX + 1; + +const std::string& SharedConfigMessage_Kind_Name(SharedConfigMessage_Kind value); +template +inline const std::string& SharedConfigMessage_Kind_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function SharedConfigMessage_Kind_Name."); + return SharedConfigMessage_Kind_Name(static_cast(enum_t_value)); +} +bool SharedConfigMessage_Kind_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, SharedConfigMessage_Kind* value); +// =================================================================== + +class Envelope final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.Envelope) */ { + public: + inline Envelope() : Envelope(nullptr) {} + ~Envelope() override; + explicit PROTOBUF_CONSTEXPR Envelope(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + Envelope(const Envelope& from); + Envelope(Envelope&& from) noexcept + : Envelope() { + *this = ::std::move(from); + } + + inline Envelope& operator=(const Envelope& from) { + CopyFrom(from); + return *this; + } + inline Envelope& operator=(Envelope&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const Envelope& default_instance() { + return *internal_default_instance(); + } + static inline const Envelope* internal_default_instance() { + return reinterpret_cast( + &_Envelope_default_instance_); + } + static constexpr int kIndexInFileMessages = + 0; + + friend void swap(Envelope& a, Envelope& b) { + a.Swap(&b); + } + inline void Swap(Envelope* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(Envelope* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + Envelope* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const Envelope& from); + void MergeFrom(const Envelope& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(Envelope* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.Envelope"; + } + protected: + explicit Envelope(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef Envelope_Type Type; + static constexpr Type SESSION_MESSAGE = + Envelope_Type_SESSION_MESSAGE; + static constexpr Type CLOSED_GROUP_MESSAGE = + Envelope_Type_CLOSED_GROUP_MESSAGE; + static inline bool Type_IsValid(int value) { + return Envelope_Type_IsValid(value); + } + static constexpr Type Type_MIN = + Envelope_Type_Type_MIN; + static constexpr Type Type_MAX = + Envelope_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + Envelope_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return Envelope_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return Envelope_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kSourceFieldNumber = 2, + kContentFieldNumber = 8, + kTimestampFieldNumber = 5, + kServerTimestampFieldNumber = 10, + kSourceDeviceFieldNumber = 7, + kTypeFieldNumber = 1, + }; + // optional string source = 2; + bool has_source() const; + private: + bool _internal_has_source() const; + public: + void clear_source(); + const std::string& source() const; + template + void set_source(ArgT0&& arg0, ArgT... args); + std::string* mutable_source(); + PROTOBUF_NODISCARD std::string* release_source(); + void set_allocated_source(std::string* source); + private: + const std::string& _internal_source() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_source(const std::string& value); + std::string* _internal_mutable_source(); + public: + + // optional bytes content = 8; + bool has_content() const; + private: + bool _internal_has_content() const; + public: + void clear_content(); + const std::string& content() const; + template + void set_content(ArgT0&& arg0, ArgT... args); + std::string* mutable_content(); + PROTOBUF_NODISCARD std::string* release_content(); + void set_allocated_content(std::string* content); + private: + const std::string& _internal_content() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_content(const std::string& value); + std::string* _internal_mutable_content(); + public: + + // required uint64 timestamp = 5; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // optional uint64 serverTimestamp = 10; + bool has_servertimestamp() const; + private: + bool _internal_has_servertimestamp() const; + public: + void clear_servertimestamp(); + uint64_t servertimestamp() const; + void set_servertimestamp(uint64_t value); + private: + uint64_t _internal_servertimestamp() const; + void _internal_set_servertimestamp(uint64_t value); + public: + + // optional uint32 sourceDevice = 7; + bool has_sourcedevice() const; + private: + bool _internal_has_sourcedevice() const; + public: + void clear_sourcedevice(); + uint32_t sourcedevice() const; + void set_sourcedevice(uint32_t value); + private: + uint32_t _internal_sourcedevice() const; + void _internal_set_sourcedevice(uint32_t value); + public: + + // required .SessionProtos.Envelope.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::SessionProtos::Envelope_Type type() const; + void set_type(::SessionProtos::Envelope_Type value); + private: + ::SessionProtos::Envelope_Type _internal_type() const; + void _internal_set_type(::SessionProtos::Envelope_Type value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.Envelope) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr source_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr content_; + uint64_t timestamp_; + uint64_t servertimestamp_; + uint32_t sourcedevice_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class TypingMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.TypingMessage) */ { + public: + inline TypingMessage() : TypingMessage(nullptr) {} + ~TypingMessage() override; + explicit PROTOBUF_CONSTEXPR TypingMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + TypingMessage(const TypingMessage& from); + TypingMessage(TypingMessage&& from) noexcept + : TypingMessage() { + *this = ::std::move(from); + } + + inline TypingMessage& operator=(const TypingMessage& from) { + CopyFrom(from); + return *this; + } + inline TypingMessage& operator=(TypingMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const TypingMessage& default_instance() { + return *internal_default_instance(); + } + static inline const TypingMessage* internal_default_instance() { + return reinterpret_cast( + &_TypingMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 1; + + friend void swap(TypingMessage& a, TypingMessage& b) { + a.Swap(&b); + } + inline void Swap(TypingMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(TypingMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + TypingMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const TypingMessage& from); + void MergeFrom(const TypingMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(TypingMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.TypingMessage"; + } + protected: + explicit TypingMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef TypingMessage_Action Action; + static constexpr Action STARTED = + TypingMessage_Action_STARTED; + static constexpr Action STOPPED = + TypingMessage_Action_STOPPED; + static inline bool Action_IsValid(int value) { + return TypingMessage_Action_IsValid(value); + } + static constexpr Action Action_MIN = + TypingMessage_Action_Action_MIN; + static constexpr Action Action_MAX = + TypingMessage_Action_Action_MAX; + static constexpr int Action_ARRAYSIZE = + TypingMessage_Action_Action_ARRAYSIZE; + template + static inline const std::string& Action_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Action_Name."); + return TypingMessage_Action_Name(enum_t_value); + } + static inline bool Action_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Action* value) { + return TypingMessage_Action_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kTimestampFieldNumber = 1, + kActionFieldNumber = 2, + }; + // required uint64 timestamp = 1; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // required .SessionProtos.TypingMessage.Action action = 2; + bool has_action() const; + private: + bool _internal_has_action() const; + public: + void clear_action(); + ::SessionProtos::TypingMessage_Action action() const; + void set_action(::SessionProtos::TypingMessage_Action value); + private: + ::SessionProtos::TypingMessage_Action _internal_action() const; + void _internal_set_action(::SessionProtos::TypingMessage_Action value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.TypingMessage) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint64_t timestamp_; + int action_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class UnsendRequest final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.UnsendRequest) */ { + public: + inline UnsendRequest() : UnsendRequest(nullptr) {} + ~UnsendRequest() override; + explicit PROTOBUF_CONSTEXPR UnsendRequest(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + UnsendRequest(const UnsendRequest& from); + UnsendRequest(UnsendRequest&& from) noexcept + : UnsendRequest() { + *this = ::std::move(from); + } + + inline UnsendRequest& operator=(const UnsendRequest& from) { + CopyFrom(from); + return *this; + } + inline UnsendRequest& operator=(UnsendRequest&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const UnsendRequest& default_instance() { + return *internal_default_instance(); + } + static inline const UnsendRequest* internal_default_instance() { + return reinterpret_cast( + &_UnsendRequest_default_instance_); + } + static constexpr int kIndexInFileMessages = + 2; + + friend void swap(UnsendRequest& a, UnsendRequest& b) { + a.Swap(&b); + } + inline void Swap(UnsendRequest* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(UnsendRequest* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + UnsendRequest* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const UnsendRequest& from); + void MergeFrom(const UnsendRequest& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(UnsendRequest* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.UnsendRequest"; + } + protected: + explicit UnsendRequest(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kAuthorFieldNumber = 2, + kTimestampFieldNumber = 1, + }; + // required string author = 2; + bool has_author() const; + private: + bool _internal_has_author() const; + public: + void clear_author(); + const std::string& author() const; + template + void set_author(ArgT0&& arg0, ArgT... args); + std::string* mutable_author(); + PROTOBUF_NODISCARD std::string* release_author(); + void set_allocated_author(std::string* author); + private: + const std::string& _internal_author() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_author(const std::string& value); + std::string* _internal_mutable_author(); + public: + + // required uint64 timestamp = 1; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.UnsendRequest) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr author_; + uint64_t timestamp_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class MessageRequestResponse final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.MessageRequestResponse) */ { + public: + inline MessageRequestResponse() : MessageRequestResponse(nullptr) {} + ~MessageRequestResponse() override; + explicit PROTOBUF_CONSTEXPR MessageRequestResponse(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + MessageRequestResponse(const MessageRequestResponse& from); + MessageRequestResponse(MessageRequestResponse&& from) noexcept + : MessageRequestResponse() { + *this = ::std::move(from); + } + + inline MessageRequestResponse& operator=(const MessageRequestResponse& from) { + CopyFrom(from); + return *this; + } + inline MessageRequestResponse& operator=(MessageRequestResponse&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const MessageRequestResponse& default_instance() { + return *internal_default_instance(); + } + static inline const MessageRequestResponse* internal_default_instance() { + return reinterpret_cast( + &_MessageRequestResponse_default_instance_); + } + static constexpr int kIndexInFileMessages = + 3; + + friend void swap(MessageRequestResponse& a, MessageRequestResponse& b) { + a.Swap(&b); + } + inline void Swap(MessageRequestResponse* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(MessageRequestResponse* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + MessageRequestResponse* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const MessageRequestResponse& from); + void MergeFrom(const MessageRequestResponse& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(MessageRequestResponse* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.MessageRequestResponse"; + } + protected: + explicit MessageRequestResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kProfileKeyFieldNumber = 2, + kProfileFieldNumber = 3, + kIsApprovedFieldNumber = 1, + }; + // optional bytes profileKey = 2; + bool has_profilekey() const; + private: + bool _internal_has_profilekey() const; + public: + void clear_profilekey(); + const std::string& profilekey() const; + template + void set_profilekey(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilekey(); + PROTOBUF_NODISCARD std::string* release_profilekey(); + void set_allocated_profilekey(std::string* profilekey); + private: + const std::string& _internal_profilekey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilekey(const std::string& value); + std::string* _internal_mutable_profilekey(); + public: + + // optional .SessionProtos.LokiProfile profile = 3; + bool has_profile() const; + private: + bool _internal_has_profile() const; + public: + void clear_profile(); + const ::SessionProtos::LokiProfile& profile() const; + PROTOBUF_NODISCARD ::SessionProtos::LokiProfile* release_profile(); + ::SessionProtos::LokiProfile* mutable_profile(); + void set_allocated_profile(::SessionProtos::LokiProfile* profile); + private: + const ::SessionProtos::LokiProfile& _internal_profile() const; + ::SessionProtos::LokiProfile* _internal_mutable_profile(); + public: + void unsafe_arena_set_allocated_profile( + ::SessionProtos::LokiProfile* profile); + ::SessionProtos::LokiProfile* unsafe_arena_release_profile(); + + // required bool isApproved = 1; + bool has_isapproved() const; + private: + bool _internal_has_isapproved() const; + public: + void clear_isapproved(); + bool isapproved() const; + void set_isapproved(bool value); + private: + bool _internal_isapproved() const; + void _internal_set_isapproved(bool value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.MessageRequestResponse) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilekey_; + ::SessionProtos::LokiProfile* profile_; + bool isapproved_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class Content final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.Content) */ { + public: + inline Content() : Content(nullptr) {} + ~Content() override; + explicit PROTOBUF_CONSTEXPR Content(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + Content(const Content& from); + Content(Content&& from) noexcept + : Content() { + *this = ::std::move(from); + } + + inline Content& operator=(const Content& from) { + CopyFrom(from); + return *this; + } + inline Content& operator=(Content&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const Content& default_instance() { + return *internal_default_instance(); + } + static inline const Content* internal_default_instance() { + return reinterpret_cast( + &_Content_default_instance_); + } + static constexpr int kIndexInFileMessages = + 4; + + friend void swap(Content& a, Content& b) { + a.Swap(&b); + } + inline void Swap(Content* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(Content* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + Content* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const Content& from); + void MergeFrom(const Content& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(Content* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.Content"; + } + protected: + explicit Content(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kDataMessageFieldNumber = 1, + kCallMessageFieldNumber = 3, + kReceiptMessageFieldNumber = 5, + kTypingMessageFieldNumber = 6, + kConfigurationMessageFieldNumber = 7, + kDataExtractionNotificationFieldNumber = 8, + kUnsendRequestFieldNumber = 9, + kMessageRequestResponseFieldNumber = 10, + kSharedConfigMessageFieldNumber = 11, + }; + // optional .SessionProtos.DataMessage dataMessage = 1; + bool has_datamessage() const; + private: + bool _internal_has_datamessage() const; + public: + void clear_datamessage(); + const ::SessionProtos::DataMessage& datamessage() const; + PROTOBUF_NODISCARD ::SessionProtos::DataMessage* release_datamessage(); + ::SessionProtos::DataMessage* mutable_datamessage(); + void set_allocated_datamessage(::SessionProtos::DataMessage* datamessage); + private: + const ::SessionProtos::DataMessage& _internal_datamessage() const; + ::SessionProtos::DataMessage* _internal_mutable_datamessage(); + public: + void unsafe_arena_set_allocated_datamessage( + ::SessionProtos::DataMessage* datamessage); + ::SessionProtos::DataMessage* unsafe_arena_release_datamessage(); + + // optional .SessionProtos.CallMessage callMessage = 3; + bool has_callmessage() const; + private: + bool _internal_has_callmessage() const; + public: + void clear_callmessage(); + const ::SessionProtos::CallMessage& callmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::CallMessage* release_callmessage(); + ::SessionProtos::CallMessage* mutable_callmessage(); + void set_allocated_callmessage(::SessionProtos::CallMessage* callmessage); + private: + const ::SessionProtos::CallMessage& _internal_callmessage() const; + ::SessionProtos::CallMessage* _internal_mutable_callmessage(); + public: + void unsafe_arena_set_allocated_callmessage( + ::SessionProtos::CallMessage* callmessage); + ::SessionProtos::CallMessage* unsafe_arena_release_callmessage(); + + // optional .SessionProtos.ReceiptMessage receiptMessage = 5; + bool has_receiptmessage() const; + private: + bool _internal_has_receiptmessage() const; + public: + void clear_receiptmessage(); + const ::SessionProtos::ReceiptMessage& receiptmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::ReceiptMessage* release_receiptmessage(); + ::SessionProtos::ReceiptMessage* mutable_receiptmessage(); + void set_allocated_receiptmessage(::SessionProtos::ReceiptMessage* receiptmessage); + private: + const ::SessionProtos::ReceiptMessage& _internal_receiptmessage() const; + ::SessionProtos::ReceiptMessage* _internal_mutable_receiptmessage(); + public: + void unsafe_arena_set_allocated_receiptmessage( + ::SessionProtos::ReceiptMessage* receiptmessage); + ::SessionProtos::ReceiptMessage* unsafe_arena_release_receiptmessage(); + + // optional .SessionProtos.TypingMessage typingMessage = 6; + bool has_typingmessage() const; + private: + bool _internal_has_typingmessage() const; + public: + void clear_typingmessage(); + const ::SessionProtos::TypingMessage& typingmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::TypingMessage* release_typingmessage(); + ::SessionProtos::TypingMessage* mutable_typingmessage(); + void set_allocated_typingmessage(::SessionProtos::TypingMessage* typingmessage); + private: + const ::SessionProtos::TypingMessage& _internal_typingmessage() const; + ::SessionProtos::TypingMessage* _internal_mutable_typingmessage(); + public: + void unsafe_arena_set_allocated_typingmessage( + ::SessionProtos::TypingMessage* typingmessage); + ::SessionProtos::TypingMessage* unsafe_arena_release_typingmessage(); + + // optional .SessionProtos.ConfigurationMessage configurationMessage = 7; + bool has_configurationmessage() const; + private: + bool _internal_has_configurationmessage() const; + public: + void clear_configurationmessage(); + const ::SessionProtos::ConfigurationMessage& configurationmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::ConfigurationMessage* release_configurationmessage(); + ::SessionProtos::ConfigurationMessage* mutable_configurationmessage(); + void set_allocated_configurationmessage(::SessionProtos::ConfigurationMessage* configurationmessage); + private: + const ::SessionProtos::ConfigurationMessage& _internal_configurationmessage() const; + ::SessionProtos::ConfigurationMessage* _internal_mutable_configurationmessage(); + public: + void unsafe_arena_set_allocated_configurationmessage( + ::SessionProtos::ConfigurationMessage* configurationmessage); + ::SessionProtos::ConfigurationMessage* unsafe_arena_release_configurationmessage(); + + // optional .SessionProtos.DataExtractionNotification dataExtractionNotification = 8; + bool has_dataextractionnotification() const; + private: + bool _internal_has_dataextractionnotification() const; + public: + void clear_dataextractionnotification(); + const ::SessionProtos::DataExtractionNotification& dataextractionnotification() const; + PROTOBUF_NODISCARD ::SessionProtos::DataExtractionNotification* release_dataextractionnotification(); + ::SessionProtos::DataExtractionNotification* mutable_dataextractionnotification(); + void set_allocated_dataextractionnotification(::SessionProtos::DataExtractionNotification* dataextractionnotification); + private: + const ::SessionProtos::DataExtractionNotification& _internal_dataextractionnotification() const; + ::SessionProtos::DataExtractionNotification* _internal_mutable_dataextractionnotification(); + public: + void unsafe_arena_set_allocated_dataextractionnotification( + ::SessionProtos::DataExtractionNotification* dataextractionnotification); + ::SessionProtos::DataExtractionNotification* unsafe_arena_release_dataextractionnotification(); + + // optional .SessionProtos.UnsendRequest unsendRequest = 9; + bool has_unsendrequest() const; + private: + bool _internal_has_unsendrequest() const; + public: + void clear_unsendrequest(); + const ::SessionProtos::UnsendRequest& unsendrequest() const; + PROTOBUF_NODISCARD ::SessionProtos::UnsendRequest* release_unsendrequest(); + ::SessionProtos::UnsendRequest* mutable_unsendrequest(); + void set_allocated_unsendrequest(::SessionProtos::UnsendRequest* unsendrequest); + private: + const ::SessionProtos::UnsendRequest& _internal_unsendrequest() const; + ::SessionProtos::UnsendRequest* _internal_mutable_unsendrequest(); + public: + void unsafe_arena_set_allocated_unsendrequest( + ::SessionProtos::UnsendRequest* unsendrequest); + ::SessionProtos::UnsendRequest* unsafe_arena_release_unsendrequest(); + + // optional .SessionProtos.MessageRequestResponse messageRequestResponse = 10; + bool has_messagerequestresponse() const; + private: + bool _internal_has_messagerequestresponse() const; + public: + void clear_messagerequestresponse(); + const ::SessionProtos::MessageRequestResponse& messagerequestresponse() const; + PROTOBUF_NODISCARD ::SessionProtos::MessageRequestResponse* release_messagerequestresponse(); + ::SessionProtos::MessageRequestResponse* mutable_messagerequestresponse(); + void set_allocated_messagerequestresponse(::SessionProtos::MessageRequestResponse* messagerequestresponse); + private: + const ::SessionProtos::MessageRequestResponse& _internal_messagerequestresponse() const; + ::SessionProtos::MessageRequestResponse* _internal_mutable_messagerequestresponse(); + public: + void unsafe_arena_set_allocated_messagerequestresponse( + ::SessionProtos::MessageRequestResponse* messagerequestresponse); + ::SessionProtos::MessageRequestResponse* unsafe_arena_release_messagerequestresponse(); + + // optional .SessionProtos.SharedConfigMessage sharedConfigMessage = 11; + bool has_sharedconfigmessage() const; + private: + bool _internal_has_sharedconfigmessage() const; + public: + void clear_sharedconfigmessage(); + const ::SessionProtos::SharedConfigMessage& sharedconfigmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::SharedConfigMessage* release_sharedconfigmessage(); + ::SessionProtos::SharedConfigMessage* mutable_sharedconfigmessage(); + void set_allocated_sharedconfigmessage(::SessionProtos::SharedConfigMessage* sharedconfigmessage); + private: + const ::SessionProtos::SharedConfigMessage& _internal_sharedconfigmessage() const; + ::SessionProtos::SharedConfigMessage* _internal_mutable_sharedconfigmessage(); + public: + void unsafe_arena_set_allocated_sharedconfigmessage( + ::SessionProtos::SharedConfigMessage* sharedconfigmessage); + ::SessionProtos::SharedConfigMessage* unsafe_arena_release_sharedconfigmessage(); + + // @@protoc_insertion_point(class_scope:SessionProtos.Content) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::SessionProtos::DataMessage* datamessage_; + ::SessionProtos::CallMessage* callmessage_; + ::SessionProtos::ReceiptMessage* receiptmessage_; + ::SessionProtos::TypingMessage* typingmessage_; + ::SessionProtos::ConfigurationMessage* configurationmessage_; + ::SessionProtos::DataExtractionNotification* dataextractionnotification_; + ::SessionProtos::UnsendRequest* unsendrequest_; + ::SessionProtos::MessageRequestResponse* messagerequestresponse_; + ::SessionProtos::SharedConfigMessage* sharedconfigmessage_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class CallMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.CallMessage) */ { + public: + inline CallMessage() : CallMessage(nullptr) {} + ~CallMessage() override; + explicit PROTOBUF_CONSTEXPR CallMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + CallMessage(const CallMessage& from); + CallMessage(CallMessage&& from) noexcept + : CallMessage() { + *this = ::std::move(from); + } + + inline CallMessage& operator=(const CallMessage& from) { + CopyFrom(from); + return *this; + } + inline CallMessage& operator=(CallMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const CallMessage& default_instance() { + return *internal_default_instance(); + } + static inline const CallMessage* internal_default_instance() { + return reinterpret_cast( + &_CallMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 5; + + friend void swap(CallMessage& a, CallMessage& b) { + a.Swap(&b); + } + inline void Swap(CallMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(CallMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + CallMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const CallMessage& from); + void MergeFrom(const CallMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(CallMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.CallMessage"; + } + protected: + explicit CallMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef CallMessage_Type Type; + static constexpr Type PRE_OFFER = + CallMessage_Type_PRE_OFFER; + static constexpr Type OFFER = + CallMessage_Type_OFFER; + static constexpr Type ANSWER = + CallMessage_Type_ANSWER; + static constexpr Type PROVISIONAL_ANSWER = + CallMessage_Type_PROVISIONAL_ANSWER; + static constexpr Type ICE_CANDIDATES = + CallMessage_Type_ICE_CANDIDATES; + static constexpr Type END_CALL = + CallMessage_Type_END_CALL; + static inline bool Type_IsValid(int value) { + return CallMessage_Type_IsValid(value); + } + static constexpr Type Type_MIN = + CallMessage_Type_Type_MIN; + static constexpr Type Type_MAX = + CallMessage_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + CallMessage_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return CallMessage_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return CallMessage_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kSdpsFieldNumber = 2, + kSdpMLineIndexesFieldNumber = 3, + kSdpMidsFieldNumber = 4, + kUuidFieldNumber = 5, + kTypeFieldNumber = 1, + }; + // repeated string sdps = 2; + int sdps_size() const; + private: + int _internal_sdps_size() const; + public: + void clear_sdps(); + const std::string& sdps(int index) const; + std::string* mutable_sdps(int index); + void set_sdps(int index, const std::string& value); + void set_sdps(int index, std::string&& value); + void set_sdps(int index, const char* value); + void set_sdps(int index, const char* value, size_t size); + std::string* add_sdps(); + void add_sdps(const std::string& value); + void add_sdps(std::string&& value); + void add_sdps(const char* value); + void add_sdps(const char* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& sdps() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_sdps(); + private: + const std::string& _internal_sdps(int index) const; + std::string* _internal_add_sdps(); + public: + + // repeated uint32 sdpMLineIndexes = 3; + int sdpmlineindexes_size() const; + private: + int _internal_sdpmlineindexes_size() const; + public: + void clear_sdpmlineindexes(); + private: + uint32_t _internal_sdpmlineindexes(int index) const; + const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >& + _internal_sdpmlineindexes() const; + void _internal_add_sdpmlineindexes(uint32_t value); + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >* + _internal_mutable_sdpmlineindexes(); + public: + uint32_t sdpmlineindexes(int index) const; + void set_sdpmlineindexes(int index, uint32_t value); + void add_sdpmlineindexes(uint32_t value); + const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >& + sdpmlineindexes() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >* + mutable_sdpmlineindexes(); + + // repeated string sdpMids = 4; + int sdpmids_size() const; + private: + int _internal_sdpmids_size() const; + public: + void clear_sdpmids(); + const std::string& sdpmids(int index) const; + std::string* mutable_sdpmids(int index); + void set_sdpmids(int index, const std::string& value); + void set_sdpmids(int index, std::string&& value); + void set_sdpmids(int index, const char* value); + void set_sdpmids(int index, const char* value, size_t size); + std::string* add_sdpmids(); + void add_sdpmids(const std::string& value); + void add_sdpmids(std::string&& value); + void add_sdpmids(const char* value); + void add_sdpmids(const char* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& sdpmids() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_sdpmids(); + private: + const std::string& _internal_sdpmids(int index) const; + std::string* _internal_add_sdpmids(); + public: + + // required string uuid = 5; + bool has_uuid() const; + private: + bool _internal_has_uuid() const; + public: + void clear_uuid(); + const std::string& uuid() const; + template + void set_uuid(ArgT0&& arg0, ArgT... args); + std::string* mutable_uuid(); + PROTOBUF_NODISCARD std::string* release_uuid(); + void set_allocated_uuid(std::string* uuid); + private: + const std::string& _internal_uuid() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_uuid(const std::string& value); + std::string* _internal_mutable_uuid(); + public: + + // required .SessionProtos.CallMessage.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::SessionProtos::CallMessage_Type type() const; + void set_type(::SessionProtos::CallMessage_Type value); + private: + ::SessionProtos::CallMessage_Type _internal_type() const; + void _internal_set_type(::SessionProtos::CallMessage_Type value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.CallMessage) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField sdps_; + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t > sdpmlineindexes_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField sdpmids_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr uuid_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class KeyPair final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.KeyPair) */ { + public: + inline KeyPair() : KeyPair(nullptr) {} + ~KeyPair() override; + explicit PROTOBUF_CONSTEXPR KeyPair(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + KeyPair(const KeyPair& from); + KeyPair(KeyPair&& from) noexcept + : KeyPair() { + *this = ::std::move(from); + } + + inline KeyPair& operator=(const KeyPair& from) { + CopyFrom(from); + return *this; + } + inline KeyPair& operator=(KeyPair&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const KeyPair& default_instance() { + return *internal_default_instance(); + } + static inline const KeyPair* internal_default_instance() { + return reinterpret_cast( + &_KeyPair_default_instance_); + } + static constexpr int kIndexInFileMessages = + 6; + + friend void swap(KeyPair& a, KeyPair& b) { + a.Swap(&b); + } + inline void Swap(KeyPair* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(KeyPair* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + KeyPair* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const KeyPair& from); + void MergeFrom(const KeyPair& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(KeyPair* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.KeyPair"; + } + protected: + explicit KeyPair(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kPublicKeyFieldNumber = 1, + kPrivateKeyFieldNumber = 2, + }; + // required bytes publicKey = 1; + bool has_publickey() const; + private: + bool _internal_has_publickey() const; + public: + void clear_publickey(); + const std::string& publickey() const; + template + void set_publickey(ArgT0&& arg0, ArgT... args); + std::string* mutable_publickey(); + PROTOBUF_NODISCARD std::string* release_publickey(); + void set_allocated_publickey(std::string* publickey); + private: + const std::string& _internal_publickey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_publickey(const std::string& value); + std::string* _internal_mutable_publickey(); + public: + + // required bytes privateKey = 2; + bool has_privatekey() const; + private: + bool _internal_has_privatekey() const; + public: + void clear_privatekey(); + const std::string& privatekey() const; + template + void set_privatekey(ArgT0&& arg0, ArgT... args); + std::string* mutable_privatekey(); + PROTOBUF_NODISCARD std::string* release_privatekey(); + void set_allocated_privatekey(std::string* privatekey); + private: + const std::string& _internal_privatekey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_privatekey(const std::string& value); + std::string* _internal_mutable_privatekey(); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.KeyPair) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr publickey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr privatekey_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataExtractionNotification final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataExtractionNotification) */ { + public: + inline DataExtractionNotification() : DataExtractionNotification(nullptr) {} + ~DataExtractionNotification() override; + explicit PROTOBUF_CONSTEXPR DataExtractionNotification(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataExtractionNotification(const DataExtractionNotification& from); + DataExtractionNotification(DataExtractionNotification&& from) noexcept + : DataExtractionNotification() { + *this = ::std::move(from); + } + + inline DataExtractionNotification& operator=(const DataExtractionNotification& from) { + CopyFrom(from); + return *this; + } + inline DataExtractionNotification& operator=(DataExtractionNotification&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataExtractionNotification& default_instance() { + return *internal_default_instance(); + } + static inline const DataExtractionNotification* internal_default_instance() { + return reinterpret_cast( + &_DataExtractionNotification_default_instance_); + } + static constexpr int kIndexInFileMessages = + 7; + + friend void swap(DataExtractionNotification& a, DataExtractionNotification& b) { + a.Swap(&b); + } + inline void Swap(DataExtractionNotification* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataExtractionNotification* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataExtractionNotification* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataExtractionNotification& from); + void MergeFrom(const DataExtractionNotification& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataExtractionNotification* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataExtractionNotification"; + } + protected: + explicit DataExtractionNotification(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataExtractionNotification_Type Type; + static constexpr Type SCREENSHOT = + DataExtractionNotification_Type_SCREENSHOT; + static constexpr Type MEDIA_SAVED = + DataExtractionNotification_Type_MEDIA_SAVED; + static inline bool Type_IsValid(int value) { + return DataExtractionNotification_Type_IsValid(value); + } + static constexpr Type Type_MIN = + DataExtractionNotification_Type_Type_MIN; + static constexpr Type Type_MAX = + DataExtractionNotification_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + DataExtractionNotification_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return DataExtractionNotification_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return DataExtractionNotification_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kTimestampFieldNumber = 2, + kTypeFieldNumber = 1, + }; + // optional uint64 timestamp = 2; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // required .SessionProtos.DataExtractionNotification.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::SessionProtos::DataExtractionNotification_Type type() const; + void set_type(::SessionProtos::DataExtractionNotification_Type value); + private: + ::SessionProtos::DataExtractionNotification_Type _internal_type() const; + void _internal_set_type(::SessionProtos::DataExtractionNotification_Type value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataExtractionNotification) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint64_t timestamp_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class LokiProfile final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.LokiProfile) */ { + public: + inline LokiProfile() : LokiProfile(nullptr) {} + ~LokiProfile() override; + explicit PROTOBUF_CONSTEXPR LokiProfile(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + LokiProfile(const LokiProfile& from); + LokiProfile(LokiProfile&& from) noexcept + : LokiProfile() { + *this = ::std::move(from); + } + + inline LokiProfile& operator=(const LokiProfile& from) { + CopyFrom(from); + return *this; + } + inline LokiProfile& operator=(LokiProfile&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const LokiProfile& default_instance() { + return *internal_default_instance(); + } + static inline const LokiProfile* internal_default_instance() { + return reinterpret_cast( + &_LokiProfile_default_instance_); + } + static constexpr int kIndexInFileMessages = + 8; + + friend void swap(LokiProfile& a, LokiProfile& b) { + a.Swap(&b); + } + inline void Swap(LokiProfile* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(LokiProfile* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + LokiProfile* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const LokiProfile& from); + void MergeFrom(const LokiProfile& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(LokiProfile* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.LokiProfile"; + } + protected: + explicit LokiProfile(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kDisplayNameFieldNumber = 1, + kProfilePictureFieldNumber = 2, + }; + // optional string displayName = 1; + bool has_displayname() const; + private: + bool _internal_has_displayname() const; + public: + void clear_displayname(); + const std::string& displayname() const; + template + void set_displayname(ArgT0&& arg0, ArgT... args); + std::string* mutable_displayname(); + PROTOBUF_NODISCARD std::string* release_displayname(); + void set_allocated_displayname(std::string* displayname); + private: + const std::string& _internal_displayname() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_displayname(const std::string& value); + std::string* _internal_mutable_displayname(); + public: + + // optional string profilePicture = 2; + bool has_profilepicture() const; + private: + bool _internal_has_profilepicture() const; + public: + void clear_profilepicture(); + const std::string& profilepicture() const; + template + void set_profilepicture(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilepicture(); + PROTOBUF_NODISCARD std::string* release_profilepicture(); + void set_allocated_profilepicture(std::string* profilepicture); + private: + const std::string& _internal_profilepicture() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilepicture(const std::string& value); + std::string* _internal_mutable_profilepicture(); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.LokiProfile) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr displayname_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilepicture_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_Quote_QuotedAttachment final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.Quote.QuotedAttachment) */ { + public: + inline DataMessage_Quote_QuotedAttachment() : DataMessage_Quote_QuotedAttachment(nullptr) {} + ~DataMessage_Quote_QuotedAttachment() override; + explicit PROTOBUF_CONSTEXPR DataMessage_Quote_QuotedAttachment(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_Quote_QuotedAttachment(const DataMessage_Quote_QuotedAttachment& from); + DataMessage_Quote_QuotedAttachment(DataMessage_Quote_QuotedAttachment&& from) noexcept + : DataMessage_Quote_QuotedAttachment() { + *this = ::std::move(from); + } + + inline DataMessage_Quote_QuotedAttachment& operator=(const DataMessage_Quote_QuotedAttachment& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_Quote_QuotedAttachment& operator=(DataMessage_Quote_QuotedAttachment&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_Quote_QuotedAttachment& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_Quote_QuotedAttachment* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_Quote_QuotedAttachment_default_instance_); + } + static constexpr int kIndexInFileMessages = + 9; + + friend void swap(DataMessage_Quote_QuotedAttachment& a, DataMessage_Quote_QuotedAttachment& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_Quote_QuotedAttachment* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_Quote_QuotedAttachment* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_Quote_QuotedAttachment* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_Quote_QuotedAttachment& from); + void MergeFrom(const DataMessage_Quote_QuotedAttachment& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_Quote_QuotedAttachment* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.Quote.QuotedAttachment"; + } + protected: + explicit DataMessage_Quote_QuotedAttachment(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataMessage_Quote_QuotedAttachment_Flags Flags; + static constexpr Flags VOICE_MESSAGE = + DataMessage_Quote_QuotedAttachment_Flags_VOICE_MESSAGE; + static inline bool Flags_IsValid(int value) { + return DataMessage_Quote_QuotedAttachment_Flags_IsValid(value); + } + static constexpr Flags Flags_MIN = + DataMessage_Quote_QuotedAttachment_Flags_Flags_MIN; + static constexpr Flags Flags_MAX = + DataMessage_Quote_QuotedAttachment_Flags_Flags_MAX; + static constexpr int Flags_ARRAYSIZE = + DataMessage_Quote_QuotedAttachment_Flags_Flags_ARRAYSIZE; + template + static inline const std::string& Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Flags_Name."); + return DataMessage_Quote_QuotedAttachment_Flags_Name(enum_t_value); + } + static inline bool Flags_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Flags* value) { + return DataMessage_Quote_QuotedAttachment_Flags_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kContentTypeFieldNumber = 1, + kFileNameFieldNumber = 2, + kThumbnailFieldNumber = 3, + kFlagsFieldNumber = 4, + }; + // optional string contentType = 1; + bool has_contenttype() const; + private: + bool _internal_has_contenttype() const; + public: + void clear_contenttype(); + const std::string& contenttype() const; + template + void set_contenttype(ArgT0&& arg0, ArgT... args); + std::string* mutable_contenttype(); + PROTOBUF_NODISCARD std::string* release_contenttype(); + void set_allocated_contenttype(std::string* contenttype); + private: + const std::string& _internal_contenttype() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_contenttype(const std::string& value); + std::string* _internal_mutable_contenttype(); + public: + + // optional string fileName = 2; + bool has_filename() const; + private: + bool _internal_has_filename() const; + public: + void clear_filename(); + const std::string& filename() const; + template + void set_filename(ArgT0&& arg0, ArgT... args); + std::string* mutable_filename(); + PROTOBUF_NODISCARD std::string* release_filename(); + void set_allocated_filename(std::string* filename); + private: + const std::string& _internal_filename() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_filename(const std::string& value); + std::string* _internal_mutable_filename(); + public: + + // optional .SessionProtos.AttachmentPointer thumbnail = 3; + bool has_thumbnail() const; + private: + bool _internal_has_thumbnail() const; + public: + void clear_thumbnail(); + const ::SessionProtos::AttachmentPointer& thumbnail() const; + PROTOBUF_NODISCARD ::SessionProtos::AttachmentPointer* release_thumbnail(); + ::SessionProtos::AttachmentPointer* mutable_thumbnail(); + void set_allocated_thumbnail(::SessionProtos::AttachmentPointer* thumbnail); + private: + const ::SessionProtos::AttachmentPointer& _internal_thumbnail() const; + ::SessionProtos::AttachmentPointer* _internal_mutable_thumbnail(); + public: + void unsafe_arena_set_allocated_thumbnail( + ::SessionProtos::AttachmentPointer* thumbnail); + ::SessionProtos::AttachmentPointer* unsafe_arena_release_thumbnail(); + + // optional uint32 flags = 4; + bool has_flags() const; + private: + bool _internal_has_flags() const; + public: + void clear_flags(); + uint32_t flags() const; + void set_flags(uint32_t value); + private: + uint32_t _internal_flags() const; + void _internal_set_flags(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.Quote.QuotedAttachment) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr contenttype_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr filename_; + ::SessionProtos::AttachmentPointer* thumbnail_; + uint32_t flags_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_Quote final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.Quote) */ { + public: + inline DataMessage_Quote() : DataMessage_Quote(nullptr) {} + ~DataMessage_Quote() override; + explicit PROTOBUF_CONSTEXPR DataMessage_Quote(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_Quote(const DataMessage_Quote& from); + DataMessage_Quote(DataMessage_Quote&& from) noexcept + : DataMessage_Quote() { + *this = ::std::move(from); + } + + inline DataMessage_Quote& operator=(const DataMessage_Quote& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_Quote& operator=(DataMessage_Quote&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_Quote& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_Quote* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_Quote_default_instance_); + } + static constexpr int kIndexInFileMessages = + 10; + + friend void swap(DataMessage_Quote& a, DataMessage_Quote& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_Quote* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_Quote* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_Quote* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_Quote& from); + void MergeFrom(const DataMessage_Quote& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_Quote* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.Quote"; + } + protected: + explicit DataMessage_Quote(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataMessage_Quote_QuotedAttachment QuotedAttachment; + + // accessors ------------------------------------------------------- + + enum : int { + kAttachmentsFieldNumber = 4, + kAuthorFieldNumber = 2, + kTextFieldNumber = 3, + kIdFieldNumber = 1, + }; + // repeated .SessionProtos.DataMessage.Quote.QuotedAttachment attachments = 4; + int attachments_size() const; + private: + int _internal_attachments_size() const; + public: + void clear_attachments(); + ::SessionProtos::DataMessage_Quote_QuotedAttachment* mutable_attachments(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Quote_QuotedAttachment >* + mutable_attachments(); + private: + const ::SessionProtos::DataMessage_Quote_QuotedAttachment& _internal_attachments(int index) const; + ::SessionProtos::DataMessage_Quote_QuotedAttachment* _internal_add_attachments(); + public: + const ::SessionProtos::DataMessage_Quote_QuotedAttachment& attachments(int index) const; + ::SessionProtos::DataMessage_Quote_QuotedAttachment* add_attachments(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Quote_QuotedAttachment >& + attachments() const; + + // required string author = 2; + bool has_author() const; + private: + bool _internal_has_author() const; + public: + void clear_author(); + const std::string& author() const; + template + void set_author(ArgT0&& arg0, ArgT... args); + std::string* mutable_author(); + PROTOBUF_NODISCARD std::string* release_author(); + void set_allocated_author(std::string* author); + private: + const std::string& _internal_author() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_author(const std::string& value); + std::string* _internal_mutable_author(); + public: + + // optional string text = 3; + bool has_text() const; + private: + bool _internal_has_text() const; + public: + void clear_text(); + const std::string& text() const; + template + void set_text(ArgT0&& arg0, ArgT... args); + std::string* mutable_text(); + PROTOBUF_NODISCARD std::string* release_text(); + void set_allocated_text(std::string* text); + private: + const std::string& _internal_text() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_text(const std::string& value); + std::string* _internal_mutable_text(); + public: + + // required uint64 id = 1; + bool has_id() const; + private: + bool _internal_has_id() const; + public: + void clear_id(); + uint64_t id() const; + void set_id(uint64_t value); + private: + uint64_t _internal_id() const; + void _internal_set_id(uint64_t value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.Quote) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Quote_QuotedAttachment > attachments_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr author_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr text_; + uint64_t id_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_Preview final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.Preview) */ { + public: + inline DataMessage_Preview() : DataMessage_Preview(nullptr) {} + ~DataMessage_Preview() override; + explicit PROTOBUF_CONSTEXPR DataMessage_Preview(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_Preview(const DataMessage_Preview& from); + DataMessage_Preview(DataMessage_Preview&& from) noexcept + : DataMessage_Preview() { + *this = ::std::move(from); + } + + inline DataMessage_Preview& operator=(const DataMessage_Preview& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_Preview& operator=(DataMessage_Preview&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_Preview& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_Preview* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_Preview_default_instance_); + } + static constexpr int kIndexInFileMessages = + 11; + + friend void swap(DataMessage_Preview& a, DataMessage_Preview& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_Preview* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_Preview* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_Preview* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_Preview& from); + void MergeFrom(const DataMessage_Preview& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_Preview* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.Preview"; + } + protected: + explicit DataMessage_Preview(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kUrlFieldNumber = 1, + kTitleFieldNumber = 2, + kImageFieldNumber = 3, + }; + // required string url = 1; + bool has_url() const; + private: + bool _internal_has_url() const; + public: + void clear_url(); + const std::string& url() const; + template + void set_url(ArgT0&& arg0, ArgT... args); + std::string* mutable_url(); + PROTOBUF_NODISCARD std::string* release_url(); + void set_allocated_url(std::string* url); + private: + const std::string& _internal_url() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_url(const std::string& value); + std::string* _internal_mutable_url(); + public: + + // optional string title = 2; + bool has_title() const; + private: + bool _internal_has_title() const; + public: + void clear_title(); + const std::string& title() const; + template + void set_title(ArgT0&& arg0, ArgT... args); + std::string* mutable_title(); + PROTOBUF_NODISCARD std::string* release_title(); + void set_allocated_title(std::string* title); + private: + const std::string& _internal_title() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_title(const std::string& value); + std::string* _internal_mutable_title(); + public: + + // optional .SessionProtos.AttachmentPointer image = 3; + bool has_image() const; + private: + bool _internal_has_image() const; + public: + void clear_image(); + const ::SessionProtos::AttachmentPointer& image() const; + PROTOBUF_NODISCARD ::SessionProtos::AttachmentPointer* release_image(); + ::SessionProtos::AttachmentPointer* mutable_image(); + void set_allocated_image(::SessionProtos::AttachmentPointer* image); + private: + const ::SessionProtos::AttachmentPointer& _internal_image() const; + ::SessionProtos::AttachmentPointer* _internal_mutable_image(); + public: + void unsafe_arena_set_allocated_image( + ::SessionProtos::AttachmentPointer* image); + ::SessionProtos::AttachmentPointer* unsafe_arena_release_image(); + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.Preview) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr url_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr title_; + ::SessionProtos::AttachmentPointer* image_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_Reaction final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.Reaction) */ { + public: + inline DataMessage_Reaction() : DataMessage_Reaction(nullptr) {} + ~DataMessage_Reaction() override; + explicit PROTOBUF_CONSTEXPR DataMessage_Reaction(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_Reaction(const DataMessage_Reaction& from); + DataMessage_Reaction(DataMessage_Reaction&& from) noexcept + : DataMessage_Reaction() { + *this = ::std::move(from); + } + + inline DataMessage_Reaction& operator=(const DataMessage_Reaction& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_Reaction& operator=(DataMessage_Reaction&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_Reaction& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_Reaction* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_Reaction_default_instance_); + } + static constexpr int kIndexInFileMessages = + 12; + + friend void swap(DataMessage_Reaction& a, DataMessage_Reaction& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_Reaction* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_Reaction* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_Reaction* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_Reaction& from); + void MergeFrom(const DataMessage_Reaction& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_Reaction* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.Reaction"; + } + protected: + explicit DataMessage_Reaction(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataMessage_Reaction_Action Action; + static constexpr Action REACT = + DataMessage_Reaction_Action_REACT; + static constexpr Action REMOVE = + DataMessage_Reaction_Action_REMOVE; + static inline bool Action_IsValid(int value) { + return DataMessage_Reaction_Action_IsValid(value); + } + static constexpr Action Action_MIN = + DataMessage_Reaction_Action_Action_MIN; + static constexpr Action Action_MAX = + DataMessage_Reaction_Action_Action_MAX; + static constexpr int Action_ARRAYSIZE = + DataMessage_Reaction_Action_Action_ARRAYSIZE; + template + static inline const std::string& Action_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Action_Name."); + return DataMessage_Reaction_Action_Name(enum_t_value); + } + static inline bool Action_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Action* value) { + return DataMessage_Reaction_Action_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kAuthorFieldNumber = 2, + kEmojiFieldNumber = 3, + kIdFieldNumber = 1, + kActionFieldNumber = 4, + }; + // required string author = 2; + bool has_author() const; + private: + bool _internal_has_author() const; + public: + void clear_author(); + const std::string& author() const; + template + void set_author(ArgT0&& arg0, ArgT... args); + std::string* mutable_author(); + PROTOBUF_NODISCARD std::string* release_author(); + void set_allocated_author(std::string* author); + private: + const std::string& _internal_author() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_author(const std::string& value); + std::string* _internal_mutable_author(); + public: + + // optional string emoji = 3; + bool has_emoji() const; + private: + bool _internal_has_emoji() const; + public: + void clear_emoji(); + const std::string& emoji() const; + template + void set_emoji(ArgT0&& arg0, ArgT... args); + std::string* mutable_emoji(); + PROTOBUF_NODISCARD std::string* release_emoji(); + void set_allocated_emoji(std::string* emoji); + private: + const std::string& _internal_emoji() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_emoji(const std::string& value); + std::string* _internal_mutable_emoji(); + public: + + // required uint64 id = 1; + bool has_id() const; + private: + bool _internal_has_id() const; + public: + void clear_id(); + uint64_t id() const; + void set_id(uint64_t value); + private: + uint64_t _internal_id() const; + void _internal_set_id(uint64_t value); + public: + + // required .SessionProtos.DataMessage.Reaction.Action action = 4; + bool has_action() const; + private: + bool _internal_has_action() const; + public: + void clear_action(); + ::SessionProtos::DataMessage_Reaction_Action action() const; + void set_action(::SessionProtos::DataMessage_Reaction_Action value); + private: + ::SessionProtos::DataMessage_Reaction_Action _internal_action() const; + void _internal_set_action(::SessionProtos::DataMessage_Reaction_Action value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.Reaction) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr author_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr emoji_; + uint64_t id_; + int action_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_OpenGroupInvitation final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.OpenGroupInvitation) */ { + public: + inline DataMessage_OpenGroupInvitation() : DataMessage_OpenGroupInvitation(nullptr) {} + ~DataMessage_OpenGroupInvitation() override; + explicit PROTOBUF_CONSTEXPR DataMessage_OpenGroupInvitation(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_OpenGroupInvitation(const DataMessage_OpenGroupInvitation& from); + DataMessage_OpenGroupInvitation(DataMessage_OpenGroupInvitation&& from) noexcept + : DataMessage_OpenGroupInvitation() { + *this = ::std::move(from); + } + + inline DataMessage_OpenGroupInvitation& operator=(const DataMessage_OpenGroupInvitation& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_OpenGroupInvitation& operator=(DataMessage_OpenGroupInvitation&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_OpenGroupInvitation& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_OpenGroupInvitation* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_OpenGroupInvitation_default_instance_); + } + static constexpr int kIndexInFileMessages = + 13; + + friend void swap(DataMessage_OpenGroupInvitation& a, DataMessage_OpenGroupInvitation& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_OpenGroupInvitation* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_OpenGroupInvitation* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_OpenGroupInvitation* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_OpenGroupInvitation& from); + void MergeFrom(const DataMessage_OpenGroupInvitation& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_OpenGroupInvitation* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.OpenGroupInvitation"; + } + protected: + explicit DataMessage_OpenGroupInvitation(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kUrlFieldNumber = 1, + kNameFieldNumber = 3, + }; + // required string url = 1; + bool has_url() const; + private: + bool _internal_has_url() const; + public: + void clear_url(); + const std::string& url() const; + template + void set_url(ArgT0&& arg0, ArgT... args); + std::string* mutable_url(); + PROTOBUF_NODISCARD std::string* release_url(); + void set_allocated_url(std::string* url); + private: + const std::string& _internal_url() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_url(const std::string& value); + std::string* _internal_mutable_url(); + public: + + // required string name = 3; + bool has_name() const; + private: + bool _internal_has_name() const; + public: + void clear_name(); + const std::string& name() const; + template + void set_name(ArgT0&& arg0, ArgT... args); + std::string* mutable_name(); + PROTOBUF_NODISCARD std::string* release_name(); + void set_allocated_name(std::string* name); + private: + const std::string& _internal_name() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value); + std::string* _internal_mutable_name(); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.OpenGroupInvitation) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr url_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_ClosedGroupControlMessage_KeyPairWrapper final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) */ { + public: + inline DataMessage_ClosedGroupControlMessage_KeyPairWrapper() : DataMessage_ClosedGroupControlMessage_KeyPairWrapper(nullptr) {} + ~DataMessage_ClosedGroupControlMessage_KeyPairWrapper() override; + explicit PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessage_KeyPairWrapper(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_ClosedGroupControlMessage_KeyPairWrapper(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from); + DataMessage_ClosedGroupControlMessage_KeyPairWrapper(DataMessage_ClosedGroupControlMessage_KeyPairWrapper&& from) noexcept + : DataMessage_ClosedGroupControlMessage_KeyPairWrapper() { + *this = ::std::move(from); + } + + inline DataMessage_ClosedGroupControlMessage_KeyPairWrapper& operator=(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_ClosedGroupControlMessage_KeyPairWrapper& operator=(DataMessage_ClosedGroupControlMessage_KeyPairWrapper&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_ClosedGroupControlMessage_KeyPairWrapper* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_ClosedGroupControlMessage_KeyPairWrapper_default_instance_); + } + static constexpr int kIndexInFileMessages = + 14; + + friend void swap(DataMessage_ClosedGroupControlMessage_KeyPairWrapper& a, DataMessage_ClosedGroupControlMessage_KeyPairWrapper& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_ClosedGroupControlMessage_KeyPairWrapper* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_ClosedGroupControlMessage_KeyPairWrapper* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_ClosedGroupControlMessage_KeyPairWrapper* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from); + void MergeFrom(const DataMessage_ClosedGroupControlMessage_KeyPairWrapper& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_ClosedGroupControlMessage_KeyPairWrapper* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper"; + } + protected: + explicit DataMessage_ClosedGroupControlMessage_KeyPairWrapper(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kPublicKeyFieldNumber = 1, + kEncryptedKeyPairFieldNumber = 2, + }; + // required bytes publicKey = 1; + bool has_publickey() const; + private: + bool _internal_has_publickey() const; + public: + void clear_publickey(); + const std::string& publickey() const; + template + void set_publickey(ArgT0&& arg0, ArgT... args); + std::string* mutable_publickey(); + PROTOBUF_NODISCARD std::string* release_publickey(); + void set_allocated_publickey(std::string* publickey); + private: + const std::string& _internal_publickey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_publickey(const std::string& value); + std::string* _internal_mutable_publickey(); + public: + + // required bytes encryptedKeyPair = 2; + bool has_encryptedkeypair() const; + private: + bool _internal_has_encryptedkeypair() const; + public: + void clear_encryptedkeypair(); + const std::string& encryptedkeypair() const; + template + void set_encryptedkeypair(ArgT0&& arg0, ArgT... args); + std::string* mutable_encryptedkeypair(); + PROTOBUF_NODISCARD std::string* release_encryptedkeypair(); + void set_allocated_encryptedkeypair(std::string* encryptedkeypair); + private: + const std::string& _internal_encryptedkeypair() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_encryptedkeypair(const std::string& value); + std::string* _internal_mutable_encryptedkeypair(); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr publickey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr encryptedkeypair_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage_ClosedGroupControlMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage.ClosedGroupControlMessage) */ { + public: + inline DataMessage_ClosedGroupControlMessage() : DataMessage_ClosedGroupControlMessage(nullptr) {} + ~DataMessage_ClosedGroupControlMessage() override; + explicit PROTOBUF_CONSTEXPR DataMessage_ClosedGroupControlMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage_ClosedGroupControlMessage(const DataMessage_ClosedGroupControlMessage& from); + DataMessage_ClosedGroupControlMessage(DataMessage_ClosedGroupControlMessage&& from) noexcept + : DataMessage_ClosedGroupControlMessage() { + *this = ::std::move(from); + } + + inline DataMessage_ClosedGroupControlMessage& operator=(const DataMessage_ClosedGroupControlMessage& from) { + CopyFrom(from); + return *this; + } + inline DataMessage_ClosedGroupControlMessage& operator=(DataMessage_ClosedGroupControlMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage_ClosedGroupControlMessage& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage_ClosedGroupControlMessage* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_ClosedGroupControlMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 15; + + friend void swap(DataMessage_ClosedGroupControlMessage& a, DataMessage_ClosedGroupControlMessage& b) { + a.Swap(&b); + } + inline void Swap(DataMessage_ClosedGroupControlMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage_ClosedGroupControlMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage_ClosedGroupControlMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage_ClosedGroupControlMessage& from); + void MergeFrom(const DataMessage_ClosedGroupControlMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage_ClosedGroupControlMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage.ClosedGroupControlMessage"; + } + protected: + explicit DataMessage_ClosedGroupControlMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataMessage_ClosedGroupControlMessage_KeyPairWrapper KeyPairWrapper; + + typedef DataMessage_ClosedGroupControlMessage_Type Type; + static constexpr Type NEW = + DataMessage_ClosedGroupControlMessage_Type_NEW; + static constexpr Type ENCRYPTION_KEY_PAIR = + DataMessage_ClosedGroupControlMessage_Type_ENCRYPTION_KEY_PAIR; + static constexpr Type NAME_CHANGE = + DataMessage_ClosedGroupControlMessage_Type_NAME_CHANGE; + static constexpr Type MEMBERS_ADDED = + DataMessage_ClosedGroupControlMessage_Type_MEMBERS_ADDED; + static constexpr Type MEMBERS_REMOVED = + DataMessage_ClosedGroupControlMessage_Type_MEMBERS_REMOVED; + static constexpr Type MEMBER_LEFT = + DataMessage_ClosedGroupControlMessage_Type_MEMBER_LEFT; + static constexpr Type ENCRYPTION_KEY_PAIR_REQUEST = + DataMessage_ClosedGroupControlMessage_Type_ENCRYPTION_KEY_PAIR_REQUEST; + static inline bool Type_IsValid(int value) { + return DataMessage_ClosedGroupControlMessage_Type_IsValid(value); + } + static constexpr Type Type_MIN = + DataMessage_ClosedGroupControlMessage_Type_Type_MIN; + static constexpr Type Type_MAX = + DataMessage_ClosedGroupControlMessage_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + DataMessage_ClosedGroupControlMessage_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return DataMessage_ClosedGroupControlMessage_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return DataMessage_ClosedGroupControlMessage_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kMembersFieldNumber = 5, + kAdminsFieldNumber = 6, + kWrappersFieldNumber = 7, + kPublicKeyFieldNumber = 2, + kNameFieldNumber = 3, + kEncryptionKeyPairFieldNumber = 4, + kExpirationTimerFieldNumber = 8, + kTypeFieldNumber = 1, + }; + // repeated bytes members = 5; + int members_size() const; + private: + int _internal_members_size() const; + public: + void clear_members(); + const std::string& members(int index) const; + std::string* mutable_members(int index); + void set_members(int index, const std::string& value); + void set_members(int index, std::string&& value); + void set_members(int index, const char* value); + void set_members(int index, const void* value, size_t size); + std::string* add_members(); + void add_members(const std::string& value); + void add_members(std::string&& value); + void add_members(const char* value); + void add_members(const void* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& members() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_members(); + private: + const std::string& _internal_members(int index) const; + std::string* _internal_add_members(); + public: + + // repeated bytes admins = 6; + int admins_size() const; + private: + int _internal_admins_size() const; + public: + void clear_admins(); + const std::string& admins(int index) const; + std::string* mutable_admins(int index); + void set_admins(int index, const std::string& value); + void set_admins(int index, std::string&& value); + void set_admins(int index, const char* value); + void set_admins(int index, const void* value, size_t size); + std::string* add_admins(); + void add_admins(const std::string& value); + void add_admins(std::string&& value); + void add_admins(const char* value); + void add_admins(const void* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& admins() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_admins(); + private: + const std::string& _internal_admins(int index) const; + std::string* _internal_add_admins(); + public: + + // repeated .SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper wrappers = 7; + int wrappers_size() const; + private: + int _internal_wrappers_size() const; + public: + void clear_wrappers(); + ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* mutable_wrappers(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >* + mutable_wrappers(); + private: + const ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper& _internal_wrappers(int index) const; + ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* _internal_add_wrappers(); + public: + const ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper& wrappers(int index) const; + ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* add_wrappers(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >& + wrappers() const; + + // optional bytes publicKey = 2; + bool has_publickey() const; + private: + bool _internal_has_publickey() const; + public: + void clear_publickey(); + const std::string& publickey() const; + template + void set_publickey(ArgT0&& arg0, ArgT... args); + std::string* mutable_publickey(); + PROTOBUF_NODISCARD std::string* release_publickey(); + void set_allocated_publickey(std::string* publickey); + private: + const std::string& _internal_publickey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_publickey(const std::string& value); + std::string* _internal_mutable_publickey(); + public: + + // optional string name = 3; + bool has_name() const; + private: + bool _internal_has_name() const; + public: + void clear_name(); + const std::string& name() const; + template + void set_name(ArgT0&& arg0, ArgT... args); + std::string* mutable_name(); + PROTOBUF_NODISCARD std::string* release_name(); + void set_allocated_name(std::string* name); + private: + const std::string& _internal_name() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value); + std::string* _internal_mutable_name(); + public: + + // optional .SessionProtos.KeyPair encryptionKeyPair = 4; + bool has_encryptionkeypair() const; + private: + bool _internal_has_encryptionkeypair() const; + public: + void clear_encryptionkeypair(); + const ::SessionProtos::KeyPair& encryptionkeypair() const; + PROTOBUF_NODISCARD ::SessionProtos::KeyPair* release_encryptionkeypair(); + ::SessionProtos::KeyPair* mutable_encryptionkeypair(); + void set_allocated_encryptionkeypair(::SessionProtos::KeyPair* encryptionkeypair); + private: + const ::SessionProtos::KeyPair& _internal_encryptionkeypair() const; + ::SessionProtos::KeyPair* _internal_mutable_encryptionkeypair(); + public: + void unsafe_arena_set_allocated_encryptionkeypair( + ::SessionProtos::KeyPair* encryptionkeypair); + ::SessionProtos::KeyPair* unsafe_arena_release_encryptionkeypair(); + + // optional uint32 expirationTimer = 8; + bool has_expirationtimer() const; + private: + bool _internal_has_expirationtimer() const; + public: + void clear_expirationtimer(); + uint32_t expirationtimer() const; + void set_expirationtimer(uint32_t value); + private: + uint32_t _internal_expirationtimer() const; + void _internal_set_expirationtimer(uint32_t value); + public: + + // required .SessionProtos.DataMessage.ClosedGroupControlMessage.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type type() const; + void set_type(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type value); + private: + ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type _internal_type() const; + void _internal_set_type(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage.ClosedGroupControlMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField members_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField admins_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper > wrappers_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr publickey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_; + ::SessionProtos::KeyPair* encryptionkeypair_; + uint32_t expirationtimer_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class DataMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.DataMessage) */ { + public: + inline DataMessage() : DataMessage(nullptr) {} + ~DataMessage() override; + explicit PROTOBUF_CONSTEXPR DataMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + DataMessage(const DataMessage& from); + DataMessage(DataMessage&& from) noexcept + : DataMessage() { + *this = ::std::move(from); + } + + inline DataMessage& operator=(const DataMessage& from) { + CopyFrom(from); + return *this; + } + inline DataMessage& operator=(DataMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const DataMessage& default_instance() { + return *internal_default_instance(); + } + static inline const DataMessage* internal_default_instance() { + return reinterpret_cast( + &_DataMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 16; + + friend void swap(DataMessage& a, DataMessage& b) { + a.Swap(&b); + } + inline void Swap(DataMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(DataMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + DataMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const DataMessage& from); + void MergeFrom(const DataMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(DataMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.DataMessage"; + } + protected: + explicit DataMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef DataMessage_Quote Quote; + typedef DataMessage_Preview Preview; + typedef DataMessage_Reaction Reaction; + typedef DataMessage_OpenGroupInvitation OpenGroupInvitation; + typedef DataMessage_ClosedGroupControlMessage ClosedGroupControlMessage; + + typedef DataMessage_Flags Flags; + static constexpr Flags EXPIRATION_TIMER_UPDATE = + DataMessage_Flags_EXPIRATION_TIMER_UPDATE; + static inline bool Flags_IsValid(int value) { + return DataMessage_Flags_IsValid(value); + } + static constexpr Flags Flags_MIN = + DataMessage_Flags_Flags_MIN; + static constexpr Flags Flags_MAX = + DataMessage_Flags_Flags_MAX; + static constexpr int Flags_ARRAYSIZE = + DataMessage_Flags_Flags_ARRAYSIZE; + template + static inline const std::string& Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Flags_Name."); + return DataMessage_Flags_Name(enum_t_value); + } + static inline bool Flags_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Flags* value) { + return DataMessage_Flags_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kAttachmentsFieldNumber = 2, + kPreviewFieldNumber = 10, + kBodyFieldNumber = 1, + kProfileKeyFieldNumber = 6, + kSyncTargetFieldNumber = 105, + kQuoteFieldNumber = 8, + kReactionFieldNumber = 11, + kProfileFieldNumber = 101, + kOpenGroupInvitationFieldNumber = 102, + kClosedGroupControlMessageFieldNumber = 104, + kFlagsFieldNumber = 4, + kExpireTimerFieldNumber = 5, + kTimestampFieldNumber = 7, + kBlocksCommunityMessageRequestsFieldNumber = 106, + }; + // repeated .SessionProtos.AttachmentPointer attachments = 2; + int attachments_size() const; + private: + int _internal_attachments_size() const; + public: + void clear_attachments(); + ::SessionProtos::AttachmentPointer* mutable_attachments(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::AttachmentPointer >* + mutable_attachments(); + private: + const ::SessionProtos::AttachmentPointer& _internal_attachments(int index) const; + ::SessionProtos::AttachmentPointer* _internal_add_attachments(); + public: + const ::SessionProtos::AttachmentPointer& attachments(int index) const; + ::SessionProtos::AttachmentPointer* add_attachments(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::AttachmentPointer >& + attachments() const; + + // repeated .SessionProtos.DataMessage.Preview preview = 10; + int preview_size() const; + private: + int _internal_preview_size() const; + public: + void clear_preview(); + ::SessionProtos::DataMessage_Preview* mutable_preview(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Preview >* + mutable_preview(); + private: + const ::SessionProtos::DataMessage_Preview& _internal_preview(int index) const; + ::SessionProtos::DataMessage_Preview* _internal_add_preview(); + public: + const ::SessionProtos::DataMessage_Preview& preview(int index) const; + ::SessionProtos::DataMessage_Preview* add_preview(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Preview >& + preview() const; + + // optional string body = 1; + bool has_body() const; + private: + bool _internal_has_body() const; + public: + void clear_body(); + const std::string& body() const; + template + void set_body(ArgT0&& arg0, ArgT... args); + std::string* mutable_body(); + PROTOBUF_NODISCARD std::string* release_body(); + void set_allocated_body(std::string* body); + private: + const std::string& _internal_body() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_body(const std::string& value); + std::string* _internal_mutable_body(); + public: + + // optional bytes profileKey = 6; + bool has_profilekey() const; + private: + bool _internal_has_profilekey() const; + public: + void clear_profilekey(); + const std::string& profilekey() const; + template + void set_profilekey(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilekey(); + PROTOBUF_NODISCARD std::string* release_profilekey(); + void set_allocated_profilekey(std::string* profilekey); + private: + const std::string& _internal_profilekey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilekey(const std::string& value); + std::string* _internal_mutable_profilekey(); + public: + + // optional string syncTarget = 105; + bool has_synctarget() const; + private: + bool _internal_has_synctarget() const; + public: + void clear_synctarget(); + const std::string& synctarget() const; + template + void set_synctarget(ArgT0&& arg0, ArgT... args); + std::string* mutable_synctarget(); + PROTOBUF_NODISCARD std::string* release_synctarget(); + void set_allocated_synctarget(std::string* synctarget); + private: + const std::string& _internal_synctarget() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_synctarget(const std::string& value); + std::string* _internal_mutable_synctarget(); + public: + + // optional .SessionProtos.DataMessage.Quote quote = 8; + bool has_quote() const; + private: + bool _internal_has_quote() const; + public: + void clear_quote(); + const ::SessionProtos::DataMessage_Quote& quote() const; + PROTOBUF_NODISCARD ::SessionProtos::DataMessage_Quote* release_quote(); + ::SessionProtos::DataMessage_Quote* mutable_quote(); + void set_allocated_quote(::SessionProtos::DataMessage_Quote* quote); + private: + const ::SessionProtos::DataMessage_Quote& _internal_quote() const; + ::SessionProtos::DataMessage_Quote* _internal_mutable_quote(); + public: + void unsafe_arena_set_allocated_quote( + ::SessionProtos::DataMessage_Quote* quote); + ::SessionProtos::DataMessage_Quote* unsafe_arena_release_quote(); + + // optional .SessionProtos.DataMessage.Reaction reaction = 11; + bool has_reaction() const; + private: + bool _internal_has_reaction() const; + public: + void clear_reaction(); + const ::SessionProtos::DataMessage_Reaction& reaction() const; + PROTOBUF_NODISCARD ::SessionProtos::DataMessage_Reaction* release_reaction(); + ::SessionProtos::DataMessage_Reaction* mutable_reaction(); + void set_allocated_reaction(::SessionProtos::DataMessage_Reaction* reaction); + private: + const ::SessionProtos::DataMessage_Reaction& _internal_reaction() const; + ::SessionProtos::DataMessage_Reaction* _internal_mutable_reaction(); + public: + void unsafe_arena_set_allocated_reaction( + ::SessionProtos::DataMessage_Reaction* reaction); + ::SessionProtos::DataMessage_Reaction* unsafe_arena_release_reaction(); + + // optional .SessionProtos.LokiProfile profile = 101; + bool has_profile() const; + private: + bool _internal_has_profile() const; + public: + void clear_profile(); + const ::SessionProtos::LokiProfile& profile() const; + PROTOBUF_NODISCARD ::SessionProtos::LokiProfile* release_profile(); + ::SessionProtos::LokiProfile* mutable_profile(); + void set_allocated_profile(::SessionProtos::LokiProfile* profile); + private: + const ::SessionProtos::LokiProfile& _internal_profile() const; + ::SessionProtos::LokiProfile* _internal_mutable_profile(); + public: + void unsafe_arena_set_allocated_profile( + ::SessionProtos::LokiProfile* profile); + ::SessionProtos::LokiProfile* unsafe_arena_release_profile(); + + // optional .SessionProtos.DataMessage.OpenGroupInvitation openGroupInvitation = 102; + bool has_opengroupinvitation() const; + private: + bool _internal_has_opengroupinvitation() const; + public: + void clear_opengroupinvitation(); + const ::SessionProtos::DataMessage_OpenGroupInvitation& opengroupinvitation() const; + PROTOBUF_NODISCARD ::SessionProtos::DataMessage_OpenGroupInvitation* release_opengroupinvitation(); + ::SessionProtos::DataMessage_OpenGroupInvitation* mutable_opengroupinvitation(); + void set_allocated_opengroupinvitation(::SessionProtos::DataMessage_OpenGroupInvitation* opengroupinvitation); + private: + const ::SessionProtos::DataMessage_OpenGroupInvitation& _internal_opengroupinvitation() const; + ::SessionProtos::DataMessage_OpenGroupInvitation* _internal_mutable_opengroupinvitation(); + public: + void unsafe_arena_set_allocated_opengroupinvitation( + ::SessionProtos::DataMessage_OpenGroupInvitation* opengroupinvitation); + ::SessionProtos::DataMessage_OpenGroupInvitation* unsafe_arena_release_opengroupinvitation(); + + // optional .SessionProtos.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; + bool has_closedgroupcontrolmessage() const; + private: + bool _internal_has_closedgroupcontrolmessage() const; + public: + void clear_closedgroupcontrolmessage(); + const ::SessionProtos::DataMessage_ClosedGroupControlMessage& closedgroupcontrolmessage() const; + PROTOBUF_NODISCARD ::SessionProtos::DataMessage_ClosedGroupControlMessage* release_closedgroupcontrolmessage(); + ::SessionProtos::DataMessage_ClosedGroupControlMessage* mutable_closedgroupcontrolmessage(); + void set_allocated_closedgroupcontrolmessage(::SessionProtos::DataMessage_ClosedGroupControlMessage* closedgroupcontrolmessage); + private: + const ::SessionProtos::DataMessage_ClosedGroupControlMessage& _internal_closedgroupcontrolmessage() const; + ::SessionProtos::DataMessage_ClosedGroupControlMessage* _internal_mutable_closedgroupcontrolmessage(); + public: + void unsafe_arena_set_allocated_closedgroupcontrolmessage( + ::SessionProtos::DataMessage_ClosedGroupControlMessage* closedgroupcontrolmessage); + ::SessionProtos::DataMessage_ClosedGroupControlMessage* unsafe_arena_release_closedgroupcontrolmessage(); + + // optional uint32 flags = 4; + bool has_flags() const; + private: + bool _internal_has_flags() const; + public: + void clear_flags(); + uint32_t flags() const; + void set_flags(uint32_t value); + private: + uint32_t _internal_flags() const; + void _internal_set_flags(uint32_t value); + public: + + // optional uint32 expireTimer = 5; + bool has_expiretimer() const; + private: + bool _internal_has_expiretimer() const; + public: + void clear_expiretimer(); + uint32_t expiretimer() const; + void set_expiretimer(uint32_t value); + private: + uint32_t _internal_expiretimer() const; + void _internal_set_expiretimer(uint32_t value); + public: + + // optional uint64 timestamp = 7; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // optional bool blocksCommunityMessageRequests = 106; + bool has_blockscommunitymessagerequests() const; + private: + bool _internal_has_blockscommunitymessagerequests() const; + public: + void clear_blockscommunitymessagerequests(); + bool blockscommunitymessagerequests() const; + void set_blockscommunitymessagerequests(bool value); + private: + bool _internal_blockscommunitymessagerequests() const; + void _internal_set_blockscommunitymessagerequests(bool value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.DataMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::AttachmentPointer > attachments_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Preview > preview_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr body_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilekey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr synctarget_; + ::SessionProtos::DataMessage_Quote* quote_; + ::SessionProtos::DataMessage_Reaction* reaction_; + ::SessionProtos::LokiProfile* profile_; + ::SessionProtos::DataMessage_OpenGroupInvitation* opengroupinvitation_; + ::SessionProtos::DataMessage_ClosedGroupControlMessage* closedgroupcontrolmessage_; + uint32_t flags_; + uint32_t expiretimer_; + uint64_t timestamp_; + bool blockscommunitymessagerequests_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class ConfigurationMessage_ClosedGroup final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.ConfigurationMessage.ClosedGroup) */ { + public: + inline ConfigurationMessage_ClosedGroup() : ConfigurationMessage_ClosedGroup(nullptr) {} + ~ConfigurationMessage_ClosedGroup() override; + explicit PROTOBUF_CONSTEXPR ConfigurationMessage_ClosedGroup(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + ConfigurationMessage_ClosedGroup(const ConfigurationMessage_ClosedGroup& from); + ConfigurationMessage_ClosedGroup(ConfigurationMessage_ClosedGroup&& from) noexcept + : ConfigurationMessage_ClosedGroup() { + *this = ::std::move(from); + } + + inline ConfigurationMessage_ClosedGroup& operator=(const ConfigurationMessage_ClosedGroup& from) { + CopyFrom(from); + return *this; + } + inline ConfigurationMessage_ClosedGroup& operator=(ConfigurationMessage_ClosedGroup&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const ConfigurationMessage_ClosedGroup& default_instance() { + return *internal_default_instance(); + } + static inline const ConfigurationMessage_ClosedGroup* internal_default_instance() { + return reinterpret_cast( + &_ConfigurationMessage_ClosedGroup_default_instance_); + } + static constexpr int kIndexInFileMessages = + 17; + + friend void swap(ConfigurationMessage_ClosedGroup& a, ConfigurationMessage_ClosedGroup& b) { + a.Swap(&b); + } + inline void Swap(ConfigurationMessage_ClosedGroup* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(ConfigurationMessage_ClosedGroup* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + ConfigurationMessage_ClosedGroup* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const ConfigurationMessage_ClosedGroup& from); + void MergeFrom(const ConfigurationMessage_ClosedGroup& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(ConfigurationMessage_ClosedGroup* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.ConfigurationMessage.ClosedGroup"; + } + protected: + explicit ConfigurationMessage_ClosedGroup(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kMembersFieldNumber = 4, + kAdminsFieldNumber = 5, + kPublicKeyFieldNumber = 1, + kNameFieldNumber = 2, + kEncryptionKeyPairFieldNumber = 3, + kExpirationTimerFieldNumber = 6, + }; + // repeated bytes members = 4; + int members_size() const; + private: + int _internal_members_size() const; + public: + void clear_members(); + const std::string& members(int index) const; + std::string* mutable_members(int index); + void set_members(int index, const std::string& value); + void set_members(int index, std::string&& value); + void set_members(int index, const char* value); + void set_members(int index, const void* value, size_t size); + std::string* add_members(); + void add_members(const std::string& value); + void add_members(std::string&& value); + void add_members(const char* value); + void add_members(const void* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& members() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_members(); + private: + const std::string& _internal_members(int index) const; + std::string* _internal_add_members(); + public: + + // repeated bytes admins = 5; + int admins_size() const; + private: + int _internal_admins_size() const; + public: + void clear_admins(); + const std::string& admins(int index) const; + std::string* mutable_admins(int index); + void set_admins(int index, const std::string& value); + void set_admins(int index, std::string&& value); + void set_admins(int index, const char* value); + void set_admins(int index, const void* value, size_t size); + std::string* add_admins(); + void add_admins(const std::string& value); + void add_admins(std::string&& value); + void add_admins(const char* value); + void add_admins(const void* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& admins() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_admins(); + private: + const std::string& _internal_admins(int index) const; + std::string* _internal_add_admins(); + public: + + // optional bytes publicKey = 1; + bool has_publickey() const; + private: + bool _internal_has_publickey() const; + public: + void clear_publickey(); + const std::string& publickey() const; + template + void set_publickey(ArgT0&& arg0, ArgT... args); + std::string* mutable_publickey(); + PROTOBUF_NODISCARD std::string* release_publickey(); + void set_allocated_publickey(std::string* publickey); + private: + const std::string& _internal_publickey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_publickey(const std::string& value); + std::string* _internal_mutable_publickey(); + public: + + // optional string name = 2; + bool has_name() const; + private: + bool _internal_has_name() const; + public: + void clear_name(); + const std::string& name() const; + template + void set_name(ArgT0&& arg0, ArgT... args); + std::string* mutable_name(); + PROTOBUF_NODISCARD std::string* release_name(); + void set_allocated_name(std::string* name); + private: + const std::string& _internal_name() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value); + std::string* _internal_mutable_name(); + public: + + // optional .SessionProtos.KeyPair encryptionKeyPair = 3; + bool has_encryptionkeypair() const; + private: + bool _internal_has_encryptionkeypair() const; + public: + void clear_encryptionkeypair(); + const ::SessionProtos::KeyPair& encryptionkeypair() const; + PROTOBUF_NODISCARD ::SessionProtos::KeyPair* release_encryptionkeypair(); + ::SessionProtos::KeyPair* mutable_encryptionkeypair(); + void set_allocated_encryptionkeypair(::SessionProtos::KeyPair* encryptionkeypair); + private: + const ::SessionProtos::KeyPair& _internal_encryptionkeypair() const; + ::SessionProtos::KeyPair* _internal_mutable_encryptionkeypair(); + public: + void unsafe_arena_set_allocated_encryptionkeypair( + ::SessionProtos::KeyPair* encryptionkeypair); + ::SessionProtos::KeyPair* unsafe_arena_release_encryptionkeypair(); + + // optional uint32 expirationTimer = 6; + bool has_expirationtimer() const; + private: + bool _internal_has_expirationtimer() const; + public: + void clear_expirationtimer(); + uint32_t expirationtimer() const; + void set_expirationtimer(uint32_t value); + private: + uint32_t _internal_expirationtimer() const; + void _internal_set_expirationtimer(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.ConfigurationMessage.ClosedGroup) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField members_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField admins_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr publickey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_; + ::SessionProtos::KeyPair* encryptionkeypair_; + uint32_t expirationtimer_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class ConfigurationMessage_Contact final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.ConfigurationMessage.Contact) */ { + public: + inline ConfigurationMessage_Contact() : ConfigurationMessage_Contact(nullptr) {} + ~ConfigurationMessage_Contact() override; + explicit PROTOBUF_CONSTEXPR ConfigurationMessage_Contact(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + ConfigurationMessage_Contact(const ConfigurationMessage_Contact& from); + ConfigurationMessage_Contact(ConfigurationMessage_Contact&& from) noexcept + : ConfigurationMessage_Contact() { + *this = ::std::move(from); + } + + inline ConfigurationMessage_Contact& operator=(const ConfigurationMessage_Contact& from) { + CopyFrom(from); + return *this; + } + inline ConfigurationMessage_Contact& operator=(ConfigurationMessage_Contact&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const ConfigurationMessage_Contact& default_instance() { + return *internal_default_instance(); + } + static inline const ConfigurationMessage_Contact* internal_default_instance() { + return reinterpret_cast( + &_ConfigurationMessage_Contact_default_instance_); + } + static constexpr int kIndexInFileMessages = + 18; + + friend void swap(ConfigurationMessage_Contact& a, ConfigurationMessage_Contact& b) { + a.Swap(&b); + } + inline void Swap(ConfigurationMessage_Contact* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(ConfigurationMessage_Contact* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + ConfigurationMessage_Contact* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const ConfigurationMessage_Contact& from); + void MergeFrom(const ConfigurationMessage_Contact& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(ConfigurationMessage_Contact* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.ConfigurationMessage.Contact"; + } + protected: + explicit ConfigurationMessage_Contact(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kPublicKeyFieldNumber = 1, + kNameFieldNumber = 2, + kProfilePictureFieldNumber = 3, + kProfileKeyFieldNumber = 4, + kIsApprovedFieldNumber = 5, + kIsBlockedFieldNumber = 6, + kDidApproveMeFieldNumber = 7, + }; + // required bytes publicKey = 1; + bool has_publickey() const; + private: + bool _internal_has_publickey() const; + public: + void clear_publickey(); + const std::string& publickey() const; + template + void set_publickey(ArgT0&& arg0, ArgT... args); + std::string* mutable_publickey(); + PROTOBUF_NODISCARD std::string* release_publickey(); + void set_allocated_publickey(std::string* publickey); + private: + const std::string& _internal_publickey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_publickey(const std::string& value); + std::string* _internal_mutable_publickey(); + public: + + // required string name = 2; + bool has_name() const; + private: + bool _internal_has_name() const; + public: + void clear_name(); + const std::string& name() const; + template + void set_name(ArgT0&& arg0, ArgT... args); + std::string* mutable_name(); + PROTOBUF_NODISCARD std::string* release_name(); + void set_allocated_name(std::string* name); + private: + const std::string& _internal_name() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value); + std::string* _internal_mutable_name(); + public: + + // optional string profilePicture = 3; + bool has_profilepicture() const; + private: + bool _internal_has_profilepicture() const; + public: + void clear_profilepicture(); + const std::string& profilepicture() const; + template + void set_profilepicture(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilepicture(); + PROTOBUF_NODISCARD std::string* release_profilepicture(); + void set_allocated_profilepicture(std::string* profilepicture); + private: + const std::string& _internal_profilepicture() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilepicture(const std::string& value); + std::string* _internal_mutable_profilepicture(); + public: + + // optional bytes profileKey = 4; + bool has_profilekey() const; + private: + bool _internal_has_profilekey() const; + public: + void clear_profilekey(); + const std::string& profilekey() const; + template + void set_profilekey(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilekey(); + PROTOBUF_NODISCARD std::string* release_profilekey(); + void set_allocated_profilekey(std::string* profilekey); + private: + const std::string& _internal_profilekey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilekey(const std::string& value); + std::string* _internal_mutable_profilekey(); + public: + + // optional bool isApproved = 5; + bool has_isapproved() const; + private: + bool _internal_has_isapproved() const; + public: + void clear_isapproved(); + bool isapproved() const; + void set_isapproved(bool value); + private: + bool _internal_isapproved() const; + void _internal_set_isapproved(bool value); + public: + + // optional bool isBlocked = 6; + bool has_isblocked() const; + private: + bool _internal_has_isblocked() const; + public: + void clear_isblocked(); + bool isblocked() const; + void set_isblocked(bool value); + private: + bool _internal_isblocked() const; + void _internal_set_isblocked(bool value); + public: + + // optional bool didApproveMe = 7; + bool has_didapproveme() const; + private: + bool _internal_has_didapproveme() const; + public: + void clear_didapproveme(); + bool didapproveme() const; + void set_didapproveme(bool value); + private: + bool _internal_didapproveme() const; + void _internal_set_didapproveme(bool value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.ConfigurationMessage.Contact) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr publickey_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilepicture_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilekey_; + bool isapproved_; + bool isblocked_; + bool didapproveme_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class ConfigurationMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.ConfigurationMessage) */ { + public: + inline ConfigurationMessage() : ConfigurationMessage(nullptr) {} + ~ConfigurationMessage() override; + explicit PROTOBUF_CONSTEXPR ConfigurationMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + ConfigurationMessage(const ConfigurationMessage& from); + ConfigurationMessage(ConfigurationMessage&& from) noexcept + : ConfigurationMessage() { + *this = ::std::move(from); + } + + inline ConfigurationMessage& operator=(const ConfigurationMessage& from) { + CopyFrom(from); + return *this; + } + inline ConfigurationMessage& operator=(ConfigurationMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const ConfigurationMessage& default_instance() { + return *internal_default_instance(); + } + static inline const ConfigurationMessage* internal_default_instance() { + return reinterpret_cast( + &_ConfigurationMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 19; + + friend void swap(ConfigurationMessage& a, ConfigurationMessage& b) { + a.Swap(&b); + } + inline void Swap(ConfigurationMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(ConfigurationMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + ConfigurationMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const ConfigurationMessage& from); + void MergeFrom(const ConfigurationMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(ConfigurationMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.ConfigurationMessage"; + } + protected: + explicit ConfigurationMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef ConfigurationMessage_ClosedGroup ClosedGroup; + typedef ConfigurationMessage_Contact Contact; + + // accessors ------------------------------------------------------- + + enum : int { + kClosedGroupsFieldNumber = 1, + kOpenGroupsFieldNumber = 2, + kContactsFieldNumber = 6, + kDisplayNameFieldNumber = 3, + kProfilePictureFieldNumber = 4, + kProfileKeyFieldNumber = 5, + }; + // repeated .SessionProtos.ConfigurationMessage.ClosedGroup closedGroups = 1; + int closedgroups_size() const; + private: + int _internal_closedgroups_size() const; + public: + void clear_closedgroups(); + ::SessionProtos::ConfigurationMessage_ClosedGroup* mutable_closedgroups(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_ClosedGroup >* + mutable_closedgroups(); + private: + const ::SessionProtos::ConfigurationMessage_ClosedGroup& _internal_closedgroups(int index) const; + ::SessionProtos::ConfigurationMessage_ClosedGroup* _internal_add_closedgroups(); + public: + const ::SessionProtos::ConfigurationMessage_ClosedGroup& closedgroups(int index) const; + ::SessionProtos::ConfigurationMessage_ClosedGroup* add_closedgroups(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_ClosedGroup >& + closedgroups() const; + + // repeated string openGroups = 2; + int opengroups_size() const; + private: + int _internal_opengroups_size() const; + public: + void clear_opengroups(); + const std::string& opengroups(int index) const; + std::string* mutable_opengroups(int index); + void set_opengroups(int index, const std::string& value); + void set_opengroups(int index, std::string&& value); + void set_opengroups(int index, const char* value); + void set_opengroups(int index, const char* value, size_t size); + std::string* add_opengroups(); + void add_opengroups(const std::string& value); + void add_opengroups(std::string&& value); + void add_opengroups(const char* value); + void add_opengroups(const char* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& opengroups() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_opengroups(); + private: + const std::string& _internal_opengroups(int index) const; + std::string* _internal_add_opengroups(); + public: + + // repeated .SessionProtos.ConfigurationMessage.Contact contacts = 6; + int contacts_size() const; + private: + int _internal_contacts_size() const; + public: + void clear_contacts(); + ::SessionProtos::ConfigurationMessage_Contact* mutable_contacts(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_Contact >* + mutable_contacts(); + private: + const ::SessionProtos::ConfigurationMessage_Contact& _internal_contacts(int index) const; + ::SessionProtos::ConfigurationMessage_Contact* _internal_add_contacts(); + public: + const ::SessionProtos::ConfigurationMessage_Contact& contacts(int index) const; + ::SessionProtos::ConfigurationMessage_Contact* add_contacts(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_Contact >& + contacts() const; + + // optional string displayName = 3; + bool has_displayname() const; + private: + bool _internal_has_displayname() const; + public: + void clear_displayname(); + const std::string& displayname() const; + template + void set_displayname(ArgT0&& arg0, ArgT... args); + std::string* mutable_displayname(); + PROTOBUF_NODISCARD std::string* release_displayname(); + void set_allocated_displayname(std::string* displayname); + private: + const std::string& _internal_displayname() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_displayname(const std::string& value); + std::string* _internal_mutable_displayname(); + public: + + // optional string profilePicture = 4; + bool has_profilepicture() const; + private: + bool _internal_has_profilepicture() const; + public: + void clear_profilepicture(); + const std::string& profilepicture() const; + template + void set_profilepicture(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilepicture(); + PROTOBUF_NODISCARD std::string* release_profilepicture(); + void set_allocated_profilepicture(std::string* profilepicture); + private: + const std::string& _internal_profilepicture() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilepicture(const std::string& value); + std::string* _internal_mutable_profilepicture(); + public: + + // optional bytes profileKey = 5; + bool has_profilekey() const; + private: + bool _internal_has_profilekey() const; + public: + void clear_profilekey(); + const std::string& profilekey() const; + template + void set_profilekey(ArgT0&& arg0, ArgT... args); + std::string* mutable_profilekey(); + PROTOBUF_NODISCARD std::string* release_profilekey(); + void set_allocated_profilekey(std::string* profilekey); + private: + const std::string& _internal_profilekey() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_profilekey(const std::string& value); + std::string* _internal_mutable_profilekey(); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.ConfigurationMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_ClosedGroup > closedgroups_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField opengroups_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_Contact > contacts_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr displayname_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilepicture_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr profilekey_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class ReceiptMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.ReceiptMessage) */ { + public: + inline ReceiptMessage() : ReceiptMessage(nullptr) {} + ~ReceiptMessage() override; + explicit PROTOBUF_CONSTEXPR ReceiptMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + ReceiptMessage(const ReceiptMessage& from); + ReceiptMessage(ReceiptMessage&& from) noexcept + : ReceiptMessage() { + *this = ::std::move(from); + } + + inline ReceiptMessage& operator=(const ReceiptMessage& from) { + CopyFrom(from); + return *this; + } + inline ReceiptMessage& operator=(ReceiptMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const ReceiptMessage& default_instance() { + return *internal_default_instance(); + } + static inline const ReceiptMessage* internal_default_instance() { + return reinterpret_cast( + &_ReceiptMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 20; + + friend void swap(ReceiptMessage& a, ReceiptMessage& b) { + a.Swap(&b); + } + inline void Swap(ReceiptMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(ReceiptMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + ReceiptMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const ReceiptMessage& from); + void MergeFrom(const ReceiptMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(ReceiptMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.ReceiptMessage"; + } + protected: + explicit ReceiptMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef ReceiptMessage_Type Type; + static constexpr Type DELIVERY = + ReceiptMessage_Type_DELIVERY; + static constexpr Type READ = + ReceiptMessage_Type_READ; + static inline bool Type_IsValid(int value) { + return ReceiptMessage_Type_IsValid(value); + } + static constexpr Type Type_MIN = + ReceiptMessage_Type_Type_MIN; + static constexpr Type Type_MAX = + ReceiptMessage_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + ReceiptMessage_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return ReceiptMessage_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return ReceiptMessage_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kTimestampFieldNumber = 2, + kTypeFieldNumber = 1, + }; + // repeated uint64 timestamp = 2; + int timestamp_size() const; + private: + int _internal_timestamp_size() const; + public: + void clear_timestamp(); + private: + uint64_t _internal_timestamp(int index) const; + const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >& + _internal_timestamp() const; + void _internal_add_timestamp(uint64_t value); + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >* + _internal_mutable_timestamp(); + public: + uint64_t timestamp(int index) const; + void set_timestamp(int index, uint64_t value); + void add_timestamp(uint64_t value); + const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >& + timestamp() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >* + mutable_timestamp(); + + // required .SessionProtos.ReceiptMessage.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::SessionProtos::ReceiptMessage_Type type() const; + void set_type(::SessionProtos::ReceiptMessage_Type value); + private: + ::SessionProtos::ReceiptMessage_Type _internal_type() const; + void _internal_set_type(::SessionProtos::ReceiptMessage_Type value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.ReceiptMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t > timestamp_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class AttachmentPointer final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.AttachmentPointer) */ { + public: + inline AttachmentPointer() : AttachmentPointer(nullptr) {} + ~AttachmentPointer() override; + explicit PROTOBUF_CONSTEXPR AttachmentPointer(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + AttachmentPointer(const AttachmentPointer& from); + AttachmentPointer(AttachmentPointer&& from) noexcept + : AttachmentPointer() { + *this = ::std::move(from); + } + + inline AttachmentPointer& operator=(const AttachmentPointer& from) { + CopyFrom(from); + return *this; + } + inline AttachmentPointer& operator=(AttachmentPointer&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const AttachmentPointer& default_instance() { + return *internal_default_instance(); + } + static inline const AttachmentPointer* internal_default_instance() { + return reinterpret_cast( + &_AttachmentPointer_default_instance_); + } + static constexpr int kIndexInFileMessages = + 21; + + friend void swap(AttachmentPointer& a, AttachmentPointer& b) { + a.Swap(&b); + } + inline void Swap(AttachmentPointer* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(AttachmentPointer* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + AttachmentPointer* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const AttachmentPointer& from); + void MergeFrom(const AttachmentPointer& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(AttachmentPointer* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.AttachmentPointer"; + } + protected: + explicit AttachmentPointer(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef AttachmentPointer_Flags Flags; + static constexpr Flags VOICE_MESSAGE = + AttachmentPointer_Flags_VOICE_MESSAGE; + static inline bool Flags_IsValid(int value) { + return AttachmentPointer_Flags_IsValid(value); + } + static constexpr Flags Flags_MIN = + AttachmentPointer_Flags_Flags_MIN; + static constexpr Flags Flags_MAX = + AttachmentPointer_Flags_Flags_MAX; + static constexpr int Flags_ARRAYSIZE = + AttachmentPointer_Flags_Flags_ARRAYSIZE; + template + static inline const std::string& Flags_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Flags_Name."); + return AttachmentPointer_Flags_Name(enum_t_value); + } + static inline bool Flags_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Flags* value) { + return AttachmentPointer_Flags_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kContentTypeFieldNumber = 2, + kKeyFieldNumber = 3, + kThumbnailFieldNumber = 5, + kDigestFieldNumber = 6, + kFileNameFieldNumber = 7, + kCaptionFieldNumber = 11, + kUrlFieldNumber = 101, + kIdFieldNumber = 1, + kSizeFieldNumber = 4, + kFlagsFieldNumber = 8, + kWidthFieldNumber = 9, + kHeightFieldNumber = 10, + }; + // optional string contentType = 2; + bool has_contenttype() const; + private: + bool _internal_has_contenttype() const; + public: + void clear_contenttype(); + const std::string& contenttype() const; + template + void set_contenttype(ArgT0&& arg0, ArgT... args); + std::string* mutable_contenttype(); + PROTOBUF_NODISCARD std::string* release_contenttype(); + void set_allocated_contenttype(std::string* contenttype); + private: + const std::string& _internal_contenttype() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_contenttype(const std::string& value); + std::string* _internal_mutable_contenttype(); + public: + + // optional bytes key = 3; + bool has_key() const; + private: + bool _internal_has_key() const; + public: + void clear_key(); + const std::string& key() const; + template + void set_key(ArgT0&& arg0, ArgT... args); + std::string* mutable_key(); + PROTOBUF_NODISCARD std::string* release_key(); + void set_allocated_key(std::string* key); + private: + const std::string& _internal_key() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_key(const std::string& value); + std::string* _internal_mutable_key(); + public: + + // optional bytes thumbnail = 5; + bool has_thumbnail() const; + private: + bool _internal_has_thumbnail() const; + public: + void clear_thumbnail(); + const std::string& thumbnail() const; + template + void set_thumbnail(ArgT0&& arg0, ArgT... args); + std::string* mutable_thumbnail(); + PROTOBUF_NODISCARD std::string* release_thumbnail(); + void set_allocated_thumbnail(std::string* thumbnail); + private: + const std::string& _internal_thumbnail() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_thumbnail(const std::string& value); + std::string* _internal_mutable_thumbnail(); + public: + + // optional bytes digest = 6; + bool has_digest() const; + private: + bool _internal_has_digest() const; + public: + void clear_digest(); + const std::string& digest() const; + template + void set_digest(ArgT0&& arg0, ArgT... args); + std::string* mutable_digest(); + PROTOBUF_NODISCARD std::string* release_digest(); + void set_allocated_digest(std::string* digest); + private: + const std::string& _internal_digest() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_digest(const std::string& value); + std::string* _internal_mutable_digest(); + public: + + // optional string fileName = 7; + bool has_filename() const; + private: + bool _internal_has_filename() const; + public: + void clear_filename(); + const std::string& filename() const; + template + void set_filename(ArgT0&& arg0, ArgT... args); + std::string* mutable_filename(); + PROTOBUF_NODISCARD std::string* release_filename(); + void set_allocated_filename(std::string* filename); + private: + const std::string& _internal_filename() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_filename(const std::string& value); + std::string* _internal_mutable_filename(); + public: + + // optional string caption = 11; + bool has_caption() const; + private: + bool _internal_has_caption() const; + public: + void clear_caption(); + const std::string& caption() const; + template + void set_caption(ArgT0&& arg0, ArgT... args); + std::string* mutable_caption(); + PROTOBUF_NODISCARD std::string* release_caption(); + void set_allocated_caption(std::string* caption); + private: + const std::string& _internal_caption() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_caption(const std::string& value); + std::string* _internal_mutable_caption(); + public: + + // optional string url = 101; + bool has_url() const; + private: + bool _internal_has_url() const; + public: + void clear_url(); + const std::string& url() const; + template + void set_url(ArgT0&& arg0, ArgT... args); + std::string* mutable_url(); + PROTOBUF_NODISCARD std::string* release_url(); + void set_allocated_url(std::string* url); + private: + const std::string& _internal_url() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_url(const std::string& value); + std::string* _internal_mutable_url(); + public: + + // required fixed64 id = 1; + bool has_id() const; + private: + bool _internal_has_id() const; + public: + void clear_id(); + uint64_t id() const; + void set_id(uint64_t value); + private: + uint64_t _internal_id() const; + void _internal_set_id(uint64_t value); + public: + + // optional uint32 size = 4; + bool has_size() const; + private: + bool _internal_has_size() const; + public: + void clear_size(); + uint32_t size() const; + void set_size(uint32_t value); + private: + uint32_t _internal_size() const; + void _internal_set_size(uint32_t value); + public: + + // optional uint32 flags = 8; + bool has_flags() const; + private: + bool _internal_has_flags() const; + public: + void clear_flags(); + uint32_t flags() const; + void set_flags(uint32_t value); + private: + uint32_t _internal_flags() const; + void _internal_set_flags(uint32_t value); + public: + + // optional uint32 width = 9; + bool has_width() const; + private: + bool _internal_has_width() const; + public: + void clear_width(); + uint32_t width() const; + void set_width(uint32_t value); + private: + uint32_t _internal_width() const; + void _internal_set_width(uint32_t value); + public: + + // optional uint32 height = 10; + bool has_height() const; + private: + bool _internal_has_height() const; + public: + void clear_height(); + uint32_t height() const; + void set_height(uint32_t value); + private: + uint32_t _internal_height() const; + void _internal_set_height(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.AttachmentPointer) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr contenttype_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr key_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr thumbnail_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr digest_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr filename_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr caption_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr url_; + uint64_t id_; + uint32_t size_; + uint32_t flags_; + uint32_t width_; + uint32_t height_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// ------------------------------------------------------------------- + +class SharedConfigMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:SessionProtos.SharedConfigMessage) */ { + public: + inline SharedConfigMessage() : SharedConfigMessage(nullptr) {} + ~SharedConfigMessage() override; + explicit PROTOBUF_CONSTEXPR SharedConfigMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + SharedConfigMessage(const SharedConfigMessage& from); + SharedConfigMessage(SharedConfigMessage&& from) noexcept + : SharedConfigMessage() { + *this = ::std::move(from); + } + + inline SharedConfigMessage& operator=(const SharedConfigMessage& from) { + CopyFrom(from); + return *this; + } + inline SharedConfigMessage& operator=(SharedConfigMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const SharedConfigMessage& default_instance() { + return *internal_default_instance(); + } + static inline const SharedConfigMessage* internal_default_instance() { + return reinterpret_cast( + &_SharedConfigMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 22; + + friend void swap(SharedConfigMessage& a, SharedConfigMessage& b) { + a.Swap(&b); + } + inline void Swap(SharedConfigMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(SharedConfigMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + SharedConfigMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const SharedConfigMessage& from); + void MergeFrom(const SharedConfigMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(SharedConfigMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "SessionProtos.SharedConfigMessage"; + } + protected: + explicit SharedConfigMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef SharedConfigMessage_Kind Kind; + static constexpr Kind USER_PROFILE = + SharedConfigMessage_Kind_USER_PROFILE; + static constexpr Kind CONTACTS = + SharedConfigMessage_Kind_CONTACTS; + static constexpr Kind CONVO_INFO_VOLATILE = + SharedConfigMessage_Kind_CONVO_INFO_VOLATILE; + static constexpr Kind USER_GROUPS = + SharedConfigMessage_Kind_USER_GROUPS; + static inline bool Kind_IsValid(int value) { + return SharedConfigMessage_Kind_IsValid(value); + } + static constexpr Kind Kind_MIN = + SharedConfigMessage_Kind_Kind_MIN; + static constexpr Kind Kind_MAX = + SharedConfigMessage_Kind_Kind_MAX; + static constexpr int Kind_ARRAYSIZE = + SharedConfigMessage_Kind_Kind_ARRAYSIZE; + template + static inline const std::string& Kind_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Kind_Name."); + return SharedConfigMessage_Kind_Name(enum_t_value); + } + static inline bool Kind_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Kind* value) { + return SharedConfigMessage_Kind_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kDataFieldNumber = 3, + kSeqnoFieldNumber = 2, + kKindFieldNumber = 1, + }; + // required bytes data = 3; + bool has_data() const; + private: + bool _internal_has_data() const; + public: + void clear_data(); + const std::string& data() const; + template + void set_data(ArgT0&& arg0, ArgT... args); + std::string* mutable_data(); + PROTOBUF_NODISCARD std::string* release_data(); + void set_allocated_data(std::string* data); + private: + const std::string& _internal_data() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_data(const std::string& value); + std::string* _internal_mutable_data(); + public: + + // required int64 seqno = 2; + bool has_seqno() const; + private: + bool _internal_has_seqno() const; + public: + void clear_seqno(); + int64_t seqno() const; + void set_seqno(int64_t value); + private: + int64_t _internal_seqno() const; + void _internal_set_seqno(int64_t value); + public: + + // required .SessionProtos.SharedConfigMessage.Kind kind = 1; + bool has_kind() const; + private: + bool _internal_has_kind() const; + public: + void clear_kind(); + ::SessionProtos::SharedConfigMessage_Kind kind() const; + void set_kind(::SessionProtos::SharedConfigMessage_Kind value); + private: + ::SessionProtos::SharedConfigMessage_Kind _internal_kind() const; + void _internal_set_kind(::SessionProtos::SharedConfigMessage_Kind value); + public: + + // @@protoc_insertion_point(class_scope:SessionProtos.SharedConfigMessage) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr data_; + int64_t seqno_; + int kind_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_SessionProtos_2eproto; +}; +// =================================================================== + + +// =================================================================== + +#ifdef __GNUC__ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif // __GNUC__ +// Envelope + +// required .SessionProtos.Envelope.Type type = 1; +inline bool Envelope::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + return value; +} +inline bool Envelope::has_type() const { + return _internal_has_type(); +} +inline void Envelope::clear_type() { + _impl_.type_ = 6; + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline ::SessionProtos::Envelope_Type Envelope::_internal_type() const { + return static_cast< ::SessionProtos::Envelope_Type >(_impl_.type_); +} +inline ::SessionProtos::Envelope_Type Envelope::type() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.type) + return _internal_type(); +} +inline void Envelope::_internal_set_type(::SessionProtos::Envelope_Type value) { + assert(::SessionProtos::Envelope_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.type_ = value; +} +inline void Envelope::set_type(::SessionProtos::Envelope_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.type) +} + +// optional string source = 2; +inline bool Envelope::_internal_has_source() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool Envelope::has_source() const { + return _internal_has_source(); +} +inline void Envelope::clear_source() { + _impl_.source_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& Envelope::source() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.source) + return _internal_source(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void Envelope::set_source(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.source_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.source) +} +inline std::string* Envelope::mutable_source() { + std::string* _s = _internal_mutable_source(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Envelope.source) + return _s; +} +inline const std::string& Envelope::_internal_source() const { + return _impl_.source_.Get(); +} +inline void Envelope::_internal_set_source(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.source_.Set(value, GetArenaForAllocation()); +} +inline std::string* Envelope::_internal_mutable_source() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.source_.Mutable(GetArenaForAllocation()); +} +inline std::string* Envelope::release_source() { + // @@protoc_insertion_point(field_release:SessionProtos.Envelope.source) + if (!_internal_has_source()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.source_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.source_.IsDefault()) { + _impl_.source_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void Envelope::set_allocated_source(std::string* source) { + if (source != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.source_.SetAllocated(source, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.source_.IsDefault()) { + _impl_.source_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Envelope.source) +} + +// optional uint32 sourceDevice = 7; +inline bool Envelope::_internal_has_sourcedevice() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + return value; +} +inline bool Envelope::has_sourcedevice() const { + return _internal_has_sourcedevice(); +} +inline void Envelope::clear_sourcedevice() { + _impl_.sourcedevice_ = 0u; + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline uint32_t Envelope::_internal_sourcedevice() const { + return _impl_.sourcedevice_; +} +inline uint32_t Envelope::sourcedevice() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.sourceDevice) + return _internal_sourcedevice(); +} +inline void Envelope::_internal_set_sourcedevice(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.sourcedevice_ = value; +} +inline void Envelope::set_sourcedevice(uint32_t value) { + _internal_set_sourcedevice(value); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.sourceDevice) +} + +// required uint64 timestamp = 5; +inline bool Envelope::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool Envelope::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void Envelope::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint64_t Envelope::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t Envelope::timestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.timestamp) + return _internal_timestamp(); +} +inline void Envelope::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.timestamp_ = value; +} +inline void Envelope::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.timestamp) +} + +// optional bytes content = 8; +inline bool Envelope::_internal_has_content() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool Envelope::has_content() const { + return _internal_has_content(); +} +inline void Envelope::clear_content() { + _impl_.content_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& Envelope::content() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.content) + return _internal_content(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void Envelope::set_content(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.content_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.content) +} +inline std::string* Envelope::mutable_content() { + std::string* _s = _internal_mutable_content(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Envelope.content) + return _s; +} +inline const std::string& Envelope::_internal_content() const { + return _impl_.content_.Get(); +} +inline void Envelope::_internal_set_content(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.content_.Set(value, GetArenaForAllocation()); +} +inline std::string* Envelope::_internal_mutable_content() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.content_.Mutable(GetArenaForAllocation()); +} +inline std::string* Envelope::release_content() { + // @@protoc_insertion_point(field_release:SessionProtos.Envelope.content) + if (!_internal_has_content()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.content_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.content_.IsDefault()) { + _impl_.content_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void Envelope::set_allocated_content(std::string* content) { + if (content != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.content_.SetAllocated(content, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.content_.IsDefault()) { + _impl_.content_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Envelope.content) +} + +// optional uint64 serverTimestamp = 10; +inline bool Envelope::_internal_has_servertimestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool Envelope::has_servertimestamp() const { + return _internal_has_servertimestamp(); +} +inline void Envelope::clear_servertimestamp() { + _impl_.servertimestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint64_t Envelope::_internal_servertimestamp() const { + return _impl_.servertimestamp_; +} +inline uint64_t Envelope::servertimestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.Envelope.serverTimestamp) + return _internal_servertimestamp(); +} +inline void Envelope::_internal_set_servertimestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.servertimestamp_ = value; +} +inline void Envelope::set_servertimestamp(uint64_t value) { + _internal_set_servertimestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.Envelope.serverTimestamp) +} + +// ------------------------------------------------------------------- + +// TypingMessage + +// required uint64 timestamp = 1; +inline bool TypingMessage::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool TypingMessage::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void TypingMessage::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline uint64_t TypingMessage::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t TypingMessage::timestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.TypingMessage.timestamp) + return _internal_timestamp(); +} +inline void TypingMessage::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.timestamp_ = value; +} +inline void TypingMessage::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.TypingMessage.timestamp) +} + +// required .SessionProtos.TypingMessage.Action action = 2; +inline bool TypingMessage::_internal_has_action() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool TypingMessage::has_action() const { + return _internal_has_action(); +} +inline void TypingMessage::clear_action() { + _impl_.action_ = 0; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline ::SessionProtos::TypingMessage_Action TypingMessage::_internal_action() const { + return static_cast< ::SessionProtos::TypingMessage_Action >(_impl_.action_); +} +inline ::SessionProtos::TypingMessage_Action TypingMessage::action() const { + // @@protoc_insertion_point(field_get:SessionProtos.TypingMessage.action) + return _internal_action(); +} +inline void TypingMessage::_internal_set_action(::SessionProtos::TypingMessage_Action value) { + assert(::SessionProtos::TypingMessage_Action_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.action_ = value; +} +inline void TypingMessage::set_action(::SessionProtos::TypingMessage_Action value) { + _internal_set_action(value); + // @@protoc_insertion_point(field_set:SessionProtos.TypingMessage.action) +} + +// ------------------------------------------------------------------- + +// UnsendRequest + +// required uint64 timestamp = 1; +inline bool UnsendRequest::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool UnsendRequest::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void UnsendRequest::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline uint64_t UnsendRequest::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t UnsendRequest::timestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.UnsendRequest.timestamp) + return _internal_timestamp(); +} +inline void UnsendRequest::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.timestamp_ = value; +} +inline void UnsendRequest::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.UnsendRequest.timestamp) +} + +// required string author = 2; +inline bool UnsendRequest::_internal_has_author() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool UnsendRequest::has_author() const { + return _internal_has_author(); +} +inline void UnsendRequest::clear_author() { + _impl_.author_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& UnsendRequest::author() const { + // @@protoc_insertion_point(field_get:SessionProtos.UnsendRequest.author) + return _internal_author(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void UnsendRequest::set_author(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.UnsendRequest.author) +} +inline std::string* UnsendRequest::mutable_author() { + std::string* _s = _internal_mutable_author(); + // @@protoc_insertion_point(field_mutable:SessionProtos.UnsendRequest.author) + return _s; +} +inline const std::string& UnsendRequest::_internal_author() const { + return _impl_.author_.Get(); +} +inline void UnsendRequest::_internal_set_author(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(value, GetArenaForAllocation()); +} +inline std::string* UnsendRequest::_internal_mutable_author() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.author_.Mutable(GetArenaForAllocation()); +} +inline std::string* UnsendRequest::release_author() { + // @@protoc_insertion_point(field_release:SessionProtos.UnsendRequest.author) + if (!_internal_has_author()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.author_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void UnsendRequest::set_allocated_author(std::string* author) { + if (author != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.author_.SetAllocated(author, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.UnsendRequest.author) +} + +// ------------------------------------------------------------------- + +// MessageRequestResponse + +// required bool isApproved = 1; +inline bool MessageRequestResponse::_internal_has_isapproved() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool MessageRequestResponse::has_isapproved() const { + return _internal_has_isapproved(); +} +inline void MessageRequestResponse::clear_isapproved() { + _impl_.isapproved_ = false; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline bool MessageRequestResponse::_internal_isapproved() const { + return _impl_.isapproved_; +} +inline bool MessageRequestResponse::isapproved() const { + // @@protoc_insertion_point(field_get:SessionProtos.MessageRequestResponse.isApproved) + return _internal_isapproved(); +} +inline void MessageRequestResponse::_internal_set_isapproved(bool value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.isapproved_ = value; +} +inline void MessageRequestResponse::set_isapproved(bool value) { + _internal_set_isapproved(value); + // @@protoc_insertion_point(field_set:SessionProtos.MessageRequestResponse.isApproved) +} + +// optional bytes profileKey = 2; +inline bool MessageRequestResponse::_internal_has_profilekey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool MessageRequestResponse::has_profilekey() const { + return _internal_has_profilekey(); +} +inline void MessageRequestResponse::clear_profilekey() { + _impl_.profilekey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& MessageRequestResponse::profilekey() const { + // @@protoc_insertion_point(field_get:SessionProtos.MessageRequestResponse.profileKey) + return _internal_profilekey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void MessageRequestResponse::set_profilekey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.profilekey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.MessageRequestResponse.profileKey) +} +inline std::string* MessageRequestResponse::mutable_profilekey() { + std::string* _s = _internal_mutable_profilekey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.MessageRequestResponse.profileKey) + return _s; +} +inline const std::string& MessageRequestResponse::_internal_profilekey() const { + return _impl_.profilekey_.Get(); +} +inline void MessageRequestResponse::_internal_set_profilekey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.profilekey_.Set(value, GetArenaForAllocation()); +} +inline std::string* MessageRequestResponse::_internal_mutable_profilekey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.profilekey_.Mutable(GetArenaForAllocation()); +} +inline std::string* MessageRequestResponse::release_profilekey() { + // @@protoc_insertion_point(field_release:SessionProtos.MessageRequestResponse.profileKey) + if (!_internal_has_profilekey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.profilekey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void MessageRequestResponse::set_allocated_profilekey(std::string* profilekey) { + if (profilekey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.profilekey_.SetAllocated(profilekey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.MessageRequestResponse.profileKey) +} + +// optional .SessionProtos.LokiProfile profile = 3; +inline bool MessageRequestResponse::_internal_has_profile() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + PROTOBUF_ASSUME(!value || _impl_.profile_ != nullptr); + return value; +} +inline bool MessageRequestResponse::has_profile() const { + return _internal_has_profile(); +} +inline void MessageRequestResponse::clear_profile() { + if (_impl_.profile_ != nullptr) _impl_.profile_->Clear(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const ::SessionProtos::LokiProfile& MessageRequestResponse::_internal_profile() const { + const ::SessionProtos::LokiProfile* p = _impl_.profile_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_LokiProfile_default_instance_); +} +inline const ::SessionProtos::LokiProfile& MessageRequestResponse::profile() const { + // @@protoc_insertion_point(field_get:SessionProtos.MessageRequestResponse.profile) + return _internal_profile(); +} +inline void MessageRequestResponse::unsafe_arena_set_allocated_profile( + ::SessionProtos::LokiProfile* profile) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.profile_); + } + _impl_.profile_ = profile; + if (profile) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.MessageRequestResponse.profile) +} +inline ::SessionProtos::LokiProfile* MessageRequestResponse::release_profile() { + _impl_._has_bits_[0] &= ~0x00000002u; + ::SessionProtos::LokiProfile* temp = _impl_.profile_; + _impl_.profile_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::LokiProfile* MessageRequestResponse::unsafe_arena_release_profile() { + // @@protoc_insertion_point(field_release:SessionProtos.MessageRequestResponse.profile) + _impl_._has_bits_[0] &= ~0x00000002u; + ::SessionProtos::LokiProfile* temp = _impl_.profile_; + _impl_.profile_ = nullptr; + return temp; +} +inline ::SessionProtos::LokiProfile* MessageRequestResponse::_internal_mutable_profile() { + _impl_._has_bits_[0] |= 0x00000002u; + if (_impl_.profile_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::LokiProfile>(GetArenaForAllocation()); + _impl_.profile_ = p; + } + return _impl_.profile_; +} +inline ::SessionProtos::LokiProfile* MessageRequestResponse::mutable_profile() { + ::SessionProtos::LokiProfile* _msg = _internal_mutable_profile(); + // @@protoc_insertion_point(field_mutable:SessionProtos.MessageRequestResponse.profile) + return _msg; +} +inline void MessageRequestResponse::set_allocated_profile(::SessionProtos::LokiProfile* profile) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.profile_; + } + if (profile) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(profile); + if (message_arena != submessage_arena) { + profile = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, profile, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.profile_ = profile; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.MessageRequestResponse.profile) +} + +// ------------------------------------------------------------------- + +// Content + +// optional .SessionProtos.DataMessage dataMessage = 1; +inline bool Content::_internal_has_datamessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + PROTOBUF_ASSUME(!value || _impl_.datamessage_ != nullptr); + return value; +} +inline bool Content::has_datamessage() const { + return _internal_has_datamessage(); +} +inline void Content::clear_datamessage() { + if (_impl_.datamessage_ != nullptr) _impl_.datamessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const ::SessionProtos::DataMessage& Content::_internal_datamessage() const { + const ::SessionProtos::DataMessage* p = _impl_.datamessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataMessage_default_instance_); +} +inline const ::SessionProtos::DataMessage& Content::datamessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.dataMessage) + return _internal_datamessage(); +} +inline void Content::unsafe_arena_set_allocated_datamessage( + ::SessionProtos::DataMessage* datamessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.datamessage_); + } + _impl_.datamessage_ = datamessage; + if (datamessage) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.dataMessage) +} +inline ::SessionProtos::DataMessage* Content::release_datamessage() { + _impl_._has_bits_[0] &= ~0x00000001u; + ::SessionProtos::DataMessage* temp = _impl_.datamessage_; + _impl_.datamessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataMessage* Content::unsafe_arena_release_datamessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.dataMessage) + _impl_._has_bits_[0] &= ~0x00000001u; + ::SessionProtos::DataMessage* temp = _impl_.datamessage_; + _impl_.datamessage_ = nullptr; + return temp; +} +inline ::SessionProtos::DataMessage* Content::_internal_mutable_datamessage() { + _impl_._has_bits_[0] |= 0x00000001u; + if (_impl_.datamessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataMessage>(GetArenaForAllocation()); + _impl_.datamessage_ = p; + } + return _impl_.datamessage_; +} +inline ::SessionProtos::DataMessage* Content::mutable_datamessage() { + ::SessionProtos::DataMessage* _msg = _internal_mutable_datamessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.dataMessage) + return _msg; +} +inline void Content::set_allocated_datamessage(::SessionProtos::DataMessage* datamessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.datamessage_; + } + if (datamessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(datamessage); + if (message_arena != submessage_arena) { + datamessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, datamessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.datamessage_ = datamessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.dataMessage) +} + +// optional .SessionProtos.CallMessage callMessage = 3; +inline bool Content::_internal_has_callmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + PROTOBUF_ASSUME(!value || _impl_.callmessage_ != nullptr); + return value; +} +inline bool Content::has_callmessage() const { + return _internal_has_callmessage(); +} +inline void Content::clear_callmessage() { + if (_impl_.callmessage_ != nullptr) _impl_.callmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const ::SessionProtos::CallMessage& Content::_internal_callmessage() const { + const ::SessionProtos::CallMessage* p = _impl_.callmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_CallMessage_default_instance_); +} +inline const ::SessionProtos::CallMessage& Content::callmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.callMessage) + return _internal_callmessage(); +} +inline void Content::unsafe_arena_set_allocated_callmessage( + ::SessionProtos::CallMessage* callmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.callmessage_); + } + _impl_.callmessage_ = callmessage; + if (callmessage) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.callMessage) +} +inline ::SessionProtos::CallMessage* Content::release_callmessage() { + _impl_._has_bits_[0] &= ~0x00000002u; + ::SessionProtos::CallMessage* temp = _impl_.callmessage_; + _impl_.callmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::CallMessage* Content::unsafe_arena_release_callmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.callMessage) + _impl_._has_bits_[0] &= ~0x00000002u; + ::SessionProtos::CallMessage* temp = _impl_.callmessage_; + _impl_.callmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::CallMessage* Content::_internal_mutable_callmessage() { + _impl_._has_bits_[0] |= 0x00000002u; + if (_impl_.callmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::CallMessage>(GetArenaForAllocation()); + _impl_.callmessage_ = p; + } + return _impl_.callmessage_; +} +inline ::SessionProtos::CallMessage* Content::mutable_callmessage() { + ::SessionProtos::CallMessage* _msg = _internal_mutable_callmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.callMessage) + return _msg; +} +inline void Content::set_allocated_callmessage(::SessionProtos::CallMessage* callmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.callmessage_; + } + if (callmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(callmessage); + if (message_arena != submessage_arena) { + callmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, callmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.callmessage_ = callmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.callMessage) +} + +// optional .SessionProtos.ReceiptMessage receiptMessage = 5; +inline bool Content::_internal_has_receiptmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + PROTOBUF_ASSUME(!value || _impl_.receiptmessage_ != nullptr); + return value; +} +inline bool Content::has_receiptmessage() const { + return _internal_has_receiptmessage(); +} +inline void Content::clear_receiptmessage() { + if (_impl_.receiptmessage_ != nullptr) _impl_.receiptmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const ::SessionProtos::ReceiptMessage& Content::_internal_receiptmessage() const { + const ::SessionProtos::ReceiptMessage* p = _impl_.receiptmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_ReceiptMessage_default_instance_); +} +inline const ::SessionProtos::ReceiptMessage& Content::receiptmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.receiptMessage) + return _internal_receiptmessage(); +} +inline void Content::unsafe_arena_set_allocated_receiptmessage( + ::SessionProtos::ReceiptMessage* receiptmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.receiptmessage_); + } + _impl_.receiptmessage_ = receiptmessage; + if (receiptmessage) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.receiptMessage) +} +inline ::SessionProtos::ReceiptMessage* Content::release_receiptmessage() { + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::ReceiptMessage* temp = _impl_.receiptmessage_; + _impl_.receiptmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::ReceiptMessage* Content::unsafe_arena_release_receiptmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.receiptMessage) + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::ReceiptMessage* temp = _impl_.receiptmessage_; + _impl_.receiptmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::ReceiptMessage* Content::_internal_mutable_receiptmessage() { + _impl_._has_bits_[0] |= 0x00000004u; + if (_impl_.receiptmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::ReceiptMessage>(GetArenaForAllocation()); + _impl_.receiptmessage_ = p; + } + return _impl_.receiptmessage_; +} +inline ::SessionProtos::ReceiptMessage* Content::mutable_receiptmessage() { + ::SessionProtos::ReceiptMessage* _msg = _internal_mutable_receiptmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.receiptMessage) + return _msg; +} +inline void Content::set_allocated_receiptmessage(::SessionProtos::ReceiptMessage* receiptmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.receiptmessage_; + } + if (receiptmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(receiptmessage); + if (message_arena != submessage_arena) { + receiptmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, receiptmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.receiptmessage_ = receiptmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.receiptMessage) +} + +// optional .SessionProtos.TypingMessage typingMessage = 6; +inline bool Content::_internal_has_typingmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + PROTOBUF_ASSUME(!value || _impl_.typingmessage_ != nullptr); + return value; +} +inline bool Content::has_typingmessage() const { + return _internal_has_typingmessage(); +} +inline void Content::clear_typingmessage() { + if (_impl_.typingmessage_ != nullptr) _impl_.typingmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline const ::SessionProtos::TypingMessage& Content::_internal_typingmessage() const { + const ::SessionProtos::TypingMessage* p = _impl_.typingmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_TypingMessage_default_instance_); +} +inline const ::SessionProtos::TypingMessage& Content::typingmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.typingMessage) + return _internal_typingmessage(); +} +inline void Content::unsafe_arena_set_allocated_typingmessage( + ::SessionProtos::TypingMessage* typingmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.typingmessage_); + } + _impl_.typingmessage_ = typingmessage; + if (typingmessage) { + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.typingMessage) +} +inline ::SessionProtos::TypingMessage* Content::release_typingmessage() { + _impl_._has_bits_[0] &= ~0x00000008u; + ::SessionProtos::TypingMessage* temp = _impl_.typingmessage_; + _impl_.typingmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::TypingMessage* Content::unsafe_arena_release_typingmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.typingMessage) + _impl_._has_bits_[0] &= ~0x00000008u; + ::SessionProtos::TypingMessage* temp = _impl_.typingmessage_; + _impl_.typingmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::TypingMessage* Content::_internal_mutable_typingmessage() { + _impl_._has_bits_[0] |= 0x00000008u; + if (_impl_.typingmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::TypingMessage>(GetArenaForAllocation()); + _impl_.typingmessage_ = p; + } + return _impl_.typingmessage_; +} +inline ::SessionProtos::TypingMessage* Content::mutable_typingmessage() { + ::SessionProtos::TypingMessage* _msg = _internal_mutable_typingmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.typingMessage) + return _msg; +} +inline void Content::set_allocated_typingmessage(::SessionProtos::TypingMessage* typingmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.typingmessage_; + } + if (typingmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(typingmessage); + if (message_arena != submessage_arena) { + typingmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, typingmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + _impl_.typingmessage_ = typingmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.typingMessage) +} + +// optional .SessionProtos.ConfigurationMessage configurationMessage = 7; +inline bool Content::_internal_has_configurationmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + PROTOBUF_ASSUME(!value || _impl_.configurationmessage_ != nullptr); + return value; +} +inline bool Content::has_configurationmessage() const { + return _internal_has_configurationmessage(); +} +inline void Content::clear_configurationmessage() { + if (_impl_.configurationmessage_ != nullptr) _impl_.configurationmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline const ::SessionProtos::ConfigurationMessage& Content::_internal_configurationmessage() const { + const ::SessionProtos::ConfigurationMessage* p = _impl_.configurationmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_ConfigurationMessage_default_instance_); +} +inline const ::SessionProtos::ConfigurationMessage& Content::configurationmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.configurationMessage) + return _internal_configurationmessage(); +} +inline void Content::unsafe_arena_set_allocated_configurationmessage( + ::SessionProtos::ConfigurationMessage* configurationmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.configurationmessage_); + } + _impl_.configurationmessage_ = configurationmessage; + if (configurationmessage) { + _impl_._has_bits_[0] |= 0x00000010u; + } else { + _impl_._has_bits_[0] &= ~0x00000010u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.configurationMessage) +} +inline ::SessionProtos::ConfigurationMessage* Content::release_configurationmessage() { + _impl_._has_bits_[0] &= ~0x00000010u; + ::SessionProtos::ConfigurationMessage* temp = _impl_.configurationmessage_; + _impl_.configurationmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::ConfigurationMessage* Content::unsafe_arena_release_configurationmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.configurationMessage) + _impl_._has_bits_[0] &= ~0x00000010u; + ::SessionProtos::ConfigurationMessage* temp = _impl_.configurationmessage_; + _impl_.configurationmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::ConfigurationMessage* Content::_internal_mutable_configurationmessage() { + _impl_._has_bits_[0] |= 0x00000010u; + if (_impl_.configurationmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::ConfigurationMessage>(GetArenaForAllocation()); + _impl_.configurationmessage_ = p; + } + return _impl_.configurationmessage_; +} +inline ::SessionProtos::ConfigurationMessage* Content::mutable_configurationmessage() { + ::SessionProtos::ConfigurationMessage* _msg = _internal_mutable_configurationmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.configurationMessage) + return _msg; +} +inline void Content::set_allocated_configurationmessage(::SessionProtos::ConfigurationMessage* configurationmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.configurationmessage_; + } + if (configurationmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(configurationmessage); + if (message_arena != submessage_arena) { + configurationmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, configurationmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000010u; + } else { + _impl_._has_bits_[0] &= ~0x00000010u; + } + _impl_.configurationmessage_ = configurationmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.configurationMessage) +} + +// optional .SessionProtos.DataExtractionNotification dataExtractionNotification = 8; +inline bool Content::_internal_has_dataextractionnotification() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + PROTOBUF_ASSUME(!value || _impl_.dataextractionnotification_ != nullptr); + return value; +} +inline bool Content::has_dataextractionnotification() const { + return _internal_has_dataextractionnotification(); +} +inline void Content::clear_dataextractionnotification() { + if (_impl_.dataextractionnotification_ != nullptr) _impl_.dataextractionnotification_->Clear(); + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline const ::SessionProtos::DataExtractionNotification& Content::_internal_dataextractionnotification() const { + const ::SessionProtos::DataExtractionNotification* p = _impl_.dataextractionnotification_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataExtractionNotification_default_instance_); +} +inline const ::SessionProtos::DataExtractionNotification& Content::dataextractionnotification() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.dataExtractionNotification) + return _internal_dataextractionnotification(); +} +inline void Content::unsafe_arena_set_allocated_dataextractionnotification( + ::SessionProtos::DataExtractionNotification* dataextractionnotification) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.dataextractionnotification_); + } + _impl_.dataextractionnotification_ = dataextractionnotification; + if (dataextractionnotification) { + _impl_._has_bits_[0] |= 0x00000020u; + } else { + _impl_._has_bits_[0] &= ~0x00000020u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.dataExtractionNotification) +} +inline ::SessionProtos::DataExtractionNotification* Content::release_dataextractionnotification() { + _impl_._has_bits_[0] &= ~0x00000020u; + ::SessionProtos::DataExtractionNotification* temp = _impl_.dataextractionnotification_; + _impl_.dataextractionnotification_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataExtractionNotification* Content::unsafe_arena_release_dataextractionnotification() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.dataExtractionNotification) + _impl_._has_bits_[0] &= ~0x00000020u; + ::SessionProtos::DataExtractionNotification* temp = _impl_.dataextractionnotification_; + _impl_.dataextractionnotification_ = nullptr; + return temp; +} +inline ::SessionProtos::DataExtractionNotification* Content::_internal_mutable_dataextractionnotification() { + _impl_._has_bits_[0] |= 0x00000020u; + if (_impl_.dataextractionnotification_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataExtractionNotification>(GetArenaForAllocation()); + _impl_.dataextractionnotification_ = p; + } + return _impl_.dataextractionnotification_; +} +inline ::SessionProtos::DataExtractionNotification* Content::mutable_dataextractionnotification() { + ::SessionProtos::DataExtractionNotification* _msg = _internal_mutable_dataextractionnotification(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.dataExtractionNotification) + return _msg; +} +inline void Content::set_allocated_dataextractionnotification(::SessionProtos::DataExtractionNotification* dataextractionnotification) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.dataextractionnotification_; + } + if (dataextractionnotification) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(dataextractionnotification); + if (message_arena != submessage_arena) { + dataextractionnotification = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, dataextractionnotification, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000020u; + } else { + _impl_._has_bits_[0] &= ~0x00000020u; + } + _impl_.dataextractionnotification_ = dataextractionnotification; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.dataExtractionNotification) +} + +// optional .SessionProtos.UnsendRequest unsendRequest = 9; +inline bool Content::_internal_has_unsendrequest() const { + bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; + PROTOBUF_ASSUME(!value || _impl_.unsendrequest_ != nullptr); + return value; +} +inline bool Content::has_unsendrequest() const { + return _internal_has_unsendrequest(); +} +inline void Content::clear_unsendrequest() { + if (_impl_.unsendrequest_ != nullptr) _impl_.unsendrequest_->Clear(); + _impl_._has_bits_[0] &= ~0x00000040u; +} +inline const ::SessionProtos::UnsendRequest& Content::_internal_unsendrequest() const { + const ::SessionProtos::UnsendRequest* p = _impl_.unsendrequest_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_UnsendRequest_default_instance_); +} +inline const ::SessionProtos::UnsendRequest& Content::unsendrequest() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.unsendRequest) + return _internal_unsendrequest(); +} +inline void Content::unsafe_arena_set_allocated_unsendrequest( + ::SessionProtos::UnsendRequest* unsendrequest) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.unsendrequest_); + } + _impl_.unsendrequest_ = unsendrequest; + if (unsendrequest) { + _impl_._has_bits_[0] |= 0x00000040u; + } else { + _impl_._has_bits_[0] &= ~0x00000040u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.unsendRequest) +} +inline ::SessionProtos::UnsendRequest* Content::release_unsendrequest() { + _impl_._has_bits_[0] &= ~0x00000040u; + ::SessionProtos::UnsendRequest* temp = _impl_.unsendrequest_; + _impl_.unsendrequest_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::UnsendRequest* Content::unsafe_arena_release_unsendrequest() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.unsendRequest) + _impl_._has_bits_[0] &= ~0x00000040u; + ::SessionProtos::UnsendRequest* temp = _impl_.unsendrequest_; + _impl_.unsendrequest_ = nullptr; + return temp; +} +inline ::SessionProtos::UnsendRequest* Content::_internal_mutable_unsendrequest() { + _impl_._has_bits_[0] |= 0x00000040u; + if (_impl_.unsendrequest_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::UnsendRequest>(GetArenaForAllocation()); + _impl_.unsendrequest_ = p; + } + return _impl_.unsendrequest_; +} +inline ::SessionProtos::UnsendRequest* Content::mutable_unsendrequest() { + ::SessionProtos::UnsendRequest* _msg = _internal_mutable_unsendrequest(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.unsendRequest) + return _msg; +} +inline void Content::set_allocated_unsendrequest(::SessionProtos::UnsendRequest* unsendrequest) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.unsendrequest_; + } + if (unsendrequest) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(unsendrequest); + if (message_arena != submessage_arena) { + unsendrequest = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, unsendrequest, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000040u; + } else { + _impl_._has_bits_[0] &= ~0x00000040u; + } + _impl_.unsendrequest_ = unsendrequest; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.unsendRequest) +} + +// optional .SessionProtos.MessageRequestResponse messageRequestResponse = 10; +inline bool Content::_internal_has_messagerequestresponse() const { + bool value = (_impl_._has_bits_[0] & 0x00000080u) != 0; + PROTOBUF_ASSUME(!value || _impl_.messagerequestresponse_ != nullptr); + return value; +} +inline bool Content::has_messagerequestresponse() const { + return _internal_has_messagerequestresponse(); +} +inline void Content::clear_messagerequestresponse() { + if (_impl_.messagerequestresponse_ != nullptr) _impl_.messagerequestresponse_->Clear(); + _impl_._has_bits_[0] &= ~0x00000080u; +} +inline const ::SessionProtos::MessageRequestResponse& Content::_internal_messagerequestresponse() const { + const ::SessionProtos::MessageRequestResponse* p = _impl_.messagerequestresponse_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_MessageRequestResponse_default_instance_); +} +inline const ::SessionProtos::MessageRequestResponse& Content::messagerequestresponse() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.messageRequestResponse) + return _internal_messagerequestresponse(); +} +inline void Content::unsafe_arena_set_allocated_messagerequestresponse( + ::SessionProtos::MessageRequestResponse* messagerequestresponse) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.messagerequestresponse_); + } + _impl_.messagerequestresponse_ = messagerequestresponse; + if (messagerequestresponse) { + _impl_._has_bits_[0] |= 0x00000080u; + } else { + _impl_._has_bits_[0] &= ~0x00000080u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.messageRequestResponse) +} +inline ::SessionProtos::MessageRequestResponse* Content::release_messagerequestresponse() { + _impl_._has_bits_[0] &= ~0x00000080u; + ::SessionProtos::MessageRequestResponse* temp = _impl_.messagerequestresponse_; + _impl_.messagerequestresponse_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::MessageRequestResponse* Content::unsafe_arena_release_messagerequestresponse() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.messageRequestResponse) + _impl_._has_bits_[0] &= ~0x00000080u; + ::SessionProtos::MessageRequestResponse* temp = _impl_.messagerequestresponse_; + _impl_.messagerequestresponse_ = nullptr; + return temp; +} +inline ::SessionProtos::MessageRequestResponse* Content::_internal_mutable_messagerequestresponse() { + _impl_._has_bits_[0] |= 0x00000080u; + if (_impl_.messagerequestresponse_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::MessageRequestResponse>(GetArenaForAllocation()); + _impl_.messagerequestresponse_ = p; + } + return _impl_.messagerequestresponse_; +} +inline ::SessionProtos::MessageRequestResponse* Content::mutable_messagerequestresponse() { + ::SessionProtos::MessageRequestResponse* _msg = _internal_mutable_messagerequestresponse(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.messageRequestResponse) + return _msg; +} +inline void Content::set_allocated_messagerequestresponse(::SessionProtos::MessageRequestResponse* messagerequestresponse) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.messagerequestresponse_; + } + if (messagerequestresponse) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(messagerequestresponse); + if (message_arena != submessage_arena) { + messagerequestresponse = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, messagerequestresponse, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000080u; + } else { + _impl_._has_bits_[0] &= ~0x00000080u; + } + _impl_.messagerequestresponse_ = messagerequestresponse; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.messageRequestResponse) +} + +// optional .SessionProtos.SharedConfigMessage sharedConfigMessage = 11; +inline bool Content::_internal_has_sharedconfigmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000100u) != 0; + PROTOBUF_ASSUME(!value || _impl_.sharedconfigmessage_ != nullptr); + return value; +} +inline bool Content::has_sharedconfigmessage() const { + return _internal_has_sharedconfigmessage(); +} +inline void Content::clear_sharedconfigmessage() { + if (_impl_.sharedconfigmessage_ != nullptr) _impl_.sharedconfigmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000100u; +} +inline const ::SessionProtos::SharedConfigMessage& Content::_internal_sharedconfigmessage() const { + const ::SessionProtos::SharedConfigMessage* p = _impl_.sharedconfigmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_SharedConfigMessage_default_instance_); +} +inline const ::SessionProtos::SharedConfigMessage& Content::sharedconfigmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.Content.sharedConfigMessage) + return _internal_sharedconfigmessage(); +} +inline void Content::unsafe_arena_set_allocated_sharedconfigmessage( + ::SessionProtos::SharedConfigMessage* sharedconfigmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.sharedconfigmessage_); + } + _impl_.sharedconfigmessage_ = sharedconfigmessage; + if (sharedconfigmessage) { + _impl_._has_bits_[0] |= 0x00000100u; + } else { + _impl_._has_bits_[0] &= ~0x00000100u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.Content.sharedConfigMessage) +} +inline ::SessionProtos::SharedConfigMessage* Content::release_sharedconfigmessage() { + _impl_._has_bits_[0] &= ~0x00000100u; + ::SessionProtos::SharedConfigMessage* temp = _impl_.sharedconfigmessage_; + _impl_.sharedconfigmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::SharedConfigMessage* Content::unsafe_arena_release_sharedconfigmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.Content.sharedConfigMessage) + _impl_._has_bits_[0] &= ~0x00000100u; + ::SessionProtos::SharedConfigMessage* temp = _impl_.sharedconfigmessage_; + _impl_.sharedconfigmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::SharedConfigMessage* Content::_internal_mutable_sharedconfigmessage() { + _impl_._has_bits_[0] |= 0x00000100u; + if (_impl_.sharedconfigmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::SharedConfigMessage>(GetArenaForAllocation()); + _impl_.sharedconfigmessage_ = p; + } + return _impl_.sharedconfigmessage_; +} +inline ::SessionProtos::SharedConfigMessage* Content::mutable_sharedconfigmessage() { + ::SessionProtos::SharedConfigMessage* _msg = _internal_mutable_sharedconfigmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.Content.sharedConfigMessage) + return _msg; +} +inline void Content::set_allocated_sharedconfigmessage(::SessionProtos::SharedConfigMessage* sharedconfigmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.sharedconfigmessage_; + } + if (sharedconfigmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(sharedconfigmessage); + if (message_arena != submessage_arena) { + sharedconfigmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, sharedconfigmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000100u; + } else { + _impl_._has_bits_[0] &= ~0x00000100u; + } + _impl_.sharedconfigmessage_ = sharedconfigmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.Content.sharedConfigMessage) +} + +// ------------------------------------------------------------------- + +// CallMessage + +// required .SessionProtos.CallMessage.Type type = 1; +inline bool CallMessage::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool CallMessage::has_type() const { + return _internal_has_type(); +} +inline void CallMessage::clear_type() { + _impl_.type_ = 6; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline ::SessionProtos::CallMessage_Type CallMessage::_internal_type() const { + return static_cast< ::SessionProtos::CallMessage_Type >(_impl_.type_); +} +inline ::SessionProtos::CallMessage_Type CallMessage::type() const { + // @@protoc_insertion_point(field_get:SessionProtos.CallMessage.type) + return _internal_type(); +} +inline void CallMessage::_internal_set_type(::SessionProtos::CallMessage_Type value) { + assert(::SessionProtos::CallMessage_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.type_ = value; +} +inline void CallMessage::set_type(::SessionProtos::CallMessage_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.type) +} + +// repeated string sdps = 2; +inline int CallMessage::_internal_sdps_size() const { + return _impl_.sdps_.size(); +} +inline int CallMessage::sdps_size() const { + return _internal_sdps_size(); +} +inline void CallMessage::clear_sdps() { + _impl_.sdps_.Clear(); +} +inline std::string* CallMessage::add_sdps() { + std::string* _s = _internal_add_sdps(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.CallMessage.sdps) + return _s; +} +inline const std::string& CallMessage::_internal_sdps(int index) const { + return _impl_.sdps_.Get(index); +} +inline const std::string& CallMessage::sdps(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.CallMessage.sdps) + return _internal_sdps(index); +} +inline std::string* CallMessage::mutable_sdps(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.CallMessage.sdps) + return _impl_.sdps_.Mutable(index); +} +inline void CallMessage::set_sdps(int index, const std::string& value) { + _impl_.sdps_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::set_sdps(int index, std::string&& value) { + _impl_.sdps_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::set_sdps(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.sdps_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::set_sdps(int index, const char* value, size_t size) { + _impl_.sdps_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.CallMessage.sdps) +} +inline std::string* CallMessage::_internal_add_sdps() { + return _impl_.sdps_.Add(); +} +inline void CallMessage::add_sdps(const std::string& value) { + _impl_.sdps_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::add_sdps(std::string&& value) { + _impl_.sdps_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::add_sdps(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.sdps_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.CallMessage.sdps) +} +inline void CallMessage::add_sdps(const char* value, size_t size) { + _impl_.sdps_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.CallMessage.sdps) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +CallMessage::sdps() const { + // @@protoc_insertion_point(field_list:SessionProtos.CallMessage.sdps) + return _impl_.sdps_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +CallMessage::mutable_sdps() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.CallMessage.sdps) + return &_impl_.sdps_; +} + +// repeated uint32 sdpMLineIndexes = 3; +inline int CallMessage::_internal_sdpmlineindexes_size() const { + return _impl_.sdpmlineindexes_.size(); +} +inline int CallMessage::sdpmlineindexes_size() const { + return _internal_sdpmlineindexes_size(); +} +inline void CallMessage::clear_sdpmlineindexes() { + _impl_.sdpmlineindexes_.Clear(); +} +inline uint32_t CallMessage::_internal_sdpmlineindexes(int index) const { + return _impl_.sdpmlineindexes_.Get(index); +} +inline uint32_t CallMessage::sdpmlineindexes(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.CallMessage.sdpMLineIndexes) + return _internal_sdpmlineindexes(index); +} +inline void CallMessage::set_sdpmlineindexes(int index, uint32_t value) { + _impl_.sdpmlineindexes_.Set(index, value); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.sdpMLineIndexes) +} +inline void CallMessage::_internal_add_sdpmlineindexes(uint32_t value) { + _impl_.sdpmlineindexes_.Add(value); +} +inline void CallMessage::add_sdpmlineindexes(uint32_t value) { + _internal_add_sdpmlineindexes(value); + // @@protoc_insertion_point(field_add:SessionProtos.CallMessage.sdpMLineIndexes) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >& +CallMessage::_internal_sdpmlineindexes() const { + return _impl_.sdpmlineindexes_; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >& +CallMessage::sdpmlineindexes() const { + // @@protoc_insertion_point(field_list:SessionProtos.CallMessage.sdpMLineIndexes) + return _internal_sdpmlineindexes(); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >* +CallMessage::_internal_mutable_sdpmlineindexes() { + return &_impl_.sdpmlineindexes_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint32_t >* +CallMessage::mutable_sdpmlineindexes() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.CallMessage.sdpMLineIndexes) + return _internal_mutable_sdpmlineindexes(); +} + +// repeated string sdpMids = 4; +inline int CallMessage::_internal_sdpmids_size() const { + return _impl_.sdpmids_.size(); +} +inline int CallMessage::sdpmids_size() const { + return _internal_sdpmids_size(); +} +inline void CallMessage::clear_sdpmids() { + _impl_.sdpmids_.Clear(); +} +inline std::string* CallMessage::add_sdpmids() { + std::string* _s = _internal_add_sdpmids(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.CallMessage.sdpMids) + return _s; +} +inline const std::string& CallMessage::_internal_sdpmids(int index) const { + return _impl_.sdpmids_.Get(index); +} +inline const std::string& CallMessage::sdpmids(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.CallMessage.sdpMids) + return _internal_sdpmids(index); +} +inline std::string* CallMessage::mutable_sdpmids(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.CallMessage.sdpMids) + return _impl_.sdpmids_.Mutable(index); +} +inline void CallMessage::set_sdpmids(int index, const std::string& value) { + _impl_.sdpmids_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::set_sdpmids(int index, std::string&& value) { + _impl_.sdpmids_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::set_sdpmids(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.sdpmids_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::set_sdpmids(int index, const char* value, size_t size) { + _impl_.sdpmids_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.CallMessage.sdpMids) +} +inline std::string* CallMessage::_internal_add_sdpmids() { + return _impl_.sdpmids_.Add(); +} +inline void CallMessage::add_sdpmids(const std::string& value) { + _impl_.sdpmids_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::add_sdpmids(std::string&& value) { + _impl_.sdpmids_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::add_sdpmids(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.sdpmids_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.CallMessage.sdpMids) +} +inline void CallMessage::add_sdpmids(const char* value, size_t size) { + _impl_.sdpmids_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.CallMessage.sdpMids) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +CallMessage::sdpmids() const { + // @@protoc_insertion_point(field_list:SessionProtos.CallMessage.sdpMids) + return _impl_.sdpmids_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +CallMessage::mutable_sdpmids() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.CallMessage.sdpMids) + return &_impl_.sdpmids_; +} + +// required string uuid = 5; +inline bool CallMessage::_internal_has_uuid() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool CallMessage::has_uuid() const { + return _internal_has_uuid(); +} +inline void CallMessage::clear_uuid() { + _impl_.uuid_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& CallMessage::uuid() const { + // @@protoc_insertion_point(field_get:SessionProtos.CallMessage.uuid) + return _internal_uuid(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void CallMessage::set_uuid(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.uuid_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.CallMessage.uuid) +} +inline std::string* CallMessage::mutable_uuid() { + std::string* _s = _internal_mutable_uuid(); + // @@protoc_insertion_point(field_mutable:SessionProtos.CallMessage.uuid) + return _s; +} +inline const std::string& CallMessage::_internal_uuid() const { + return _impl_.uuid_.Get(); +} +inline void CallMessage::_internal_set_uuid(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.uuid_.Set(value, GetArenaForAllocation()); +} +inline std::string* CallMessage::_internal_mutable_uuid() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.uuid_.Mutable(GetArenaForAllocation()); +} +inline std::string* CallMessage::release_uuid() { + // @@protoc_insertion_point(field_release:SessionProtos.CallMessage.uuid) + if (!_internal_has_uuid()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.uuid_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.uuid_.IsDefault()) { + _impl_.uuid_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void CallMessage::set_allocated_uuid(std::string* uuid) { + if (uuid != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.uuid_.SetAllocated(uuid, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.uuid_.IsDefault()) { + _impl_.uuid_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.CallMessage.uuid) +} + +// ------------------------------------------------------------------- + +// KeyPair + +// required bytes publicKey = 1; +inline bool KeyPair::_internal_has_publickey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool KeyPair::has_publickey() const { + return _internal_has_publickey(); +} +inline void KeyPair::clear_publickey() { + _impl_.publickey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& KeyPair::publickey() const { + // @@protoc_insertion_point(field_get:SessionProtos.KeyPair.publicKey) + return _internal_publickey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void KeyPair::set_publickey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.KeyPair.publicKey) +} +inline std::string* KeyPair::mutable_publickey() { + std::string* _s = _internal_mutable_publickey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.KeyPair.publicKey) + return _s; +} +inline const std::string& KeyPair::_internal_publickey() const { + return _impl_.publickey_.Get(); +} +inline void KeyPair::_internal_set_publickey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.Set(value, GetArenaForAllocation()); +} +inline std::string* KeyPair::_internal_mutable_publickey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.publickey_.Mutable(GetArenaForAllocation()); +} +inline std::string* KeyPair::release_publickey() { + // @@protoc_insertion_point(field_release:SessionProtos.KeyPair.publicKey) + if (!_internal_has_publickey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.publickey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void KeyPair::set_allocated_publickey(std::string* publickey) { + if (publickey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.publickey_.SetAllocated(publickey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.KeyPair.publicKey) +} + +// required bytes privateKey = 2; +inline bool KeyPair::_internal_has_privatekey() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool KeyPair::has_privatekey() const { + return _internal_has_privatekey(); +} +inline void KeyPair::clear_privatekey() { + _impl_.privatekey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& KeyPair::privatekey() const { + // @@protoc_insertion_point(field_get:SessionProtos.KeyPair.privateKey) + return _internal_privatekey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void KeyPair::set_privatekey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.privatekey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.KeyPair.privateKey) +} +inline std::string* KeyPair::mutable_privatekey() { + std::string* _s = _internal_mutable_privatekey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.KeyPair.privateKey) + return _s; +} +inline const std::string& KeyPair::_internal_privatekey() const { + return _impl_.privatekey_.Get(); +} +inline void KeyPair::_internal_set_privatekey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.privatekey_.Set(value, GetArenaForAllocation()); +} +inline std::string* KeyPair::_internal_mutable_privatekey() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.privatekey_.Mutable(GetArenaForAllocation()); +} +inline std::string* KeyPair::release_privatekey() { + // @@protoc_insertion_point(field_release:SessionProtos.KeyPair.privateKey) + if (!_internal_has_privatekey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.privatekey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.privatekey_.IsDefault()) { + _impl_.privatekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void KeyPair::set_allocated_privatekey(std::string* privatekey) { + if (privatekey != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.privatekey_.SetAllocated(privatekey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.privatekey_.IsDefault()) { + _impl_.privatekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.KeyPair.privateKey) +} + +// ------------------------------------------------------------------- + +// DataExtractionNotification + +// required .SessionProtos.DataExtractionNotification.Type type = 1; +inline bool DataExtractionNotification::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataExtractionNotification::has_type() const { + return _internal_has_type(); +} +inline void DataExtractionNotification::clear_type() { + _impl_.type_ = 1; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline ::SessionProtos::DataExtractionNotification_Type DataExtractionNotification::_internal_type() const { + return static_cast< ::SessionProtos::DataExtractionNotification_Type >(_impl_.type_); +} +inline ::SessionProtos::DataExtractionNotification_Type DataExtractionNotification::type() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataExtractionNotification.type) + return _internal_type(); +} +inline void DataExtractionNotification::_internal_set_type(::SessionProtos::DataExtractionNotification_Type value) { + assert(::SessionProtos::DataExtractionNotification_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.type_ = value; +} +inline void DataExtractionNotification::set_type(::SessionProtos::DataExtractionNotification_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataExtractionNotification.type) +} + +// optional uint64 timestamp = 2; +inline bool DataExtractionNotification::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataExtractionNotification::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void DataExtractionNotification::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline uint64_t DataExtractionNotification::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t DataExtractionNotification::timestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataExtractionNotification.timestamp) + return _internal_timestamp(); +} +inline void DataExtractionNotification::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.timestamp_ = value; +} +inline void DataExtractionNotification::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataExtractionNotification.timestamp) +} + +// ------------------------------------------------------------------- + +// LokiProfile + +// optional string displayName = 1; +inline bool LokiProfile::_internal_has_displayname() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool LokiProfile::has_displayname() const { + return _internal_has_displayname(); +} +inline void LokiProfile::clear_displayname() { + _impl_.displayname_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& LokiProfile::displayname() const { + // @@protoc_insertion_point(field_get:SessionProtos.LokiProfile.displayName) + return _internal_displayname(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void LokiProfile::set_displayname(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.displayname_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.LokiProfile.displayName) +} +inline std::string* LokiProfile::mutable_displayname() { + std::string* _s = _internal_mutable_displayname(); + // @@protoc_insertion_point(field_mutable:SessionProtos.LokiProfile.displayName) + return _s; +} +inline const std::string& LokiProfile::_internal_displayname() const { + return _impl_.displayname_.Get(); +} +inline void LokiProfile::_internal_set_displayname(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.displayname_.Set(value, GetArenaForAllocation()); +} +inline std::string* LokiProfile::_internal_mutable_displayname() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.displayname_.Mutable(GetArenaForAllocation()); +} +inline std::string* LokiProfile::release_displayname() { + // @@protoc_insertion_point(field_release:SessionProtos.LokiProfile.displayName) + if (!_internal_has_displayname()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.displayname_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.displayname_.IsDefault()) { + _impl_.displayname_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void LokiProfile::set_allocated_displayname(std::string* displayname) { + if (displayname != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.displayname_.SetAllocated(displayname, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.displayname_.IsDefault()) { + _impl_.displayname_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.LokiProfile.displayName) +} + +// optional string profilePicture = 2; +inline bool LokiProfile::_internal_has_profilepicture() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool LokiProfile::has_profilepicture() const { + return _internal_has_profilepicture(); +} +inline void LokiProfile::clear_profilepicture() { + _impl_.profilepicture_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& LokiProfile::profilepicture() const { + // @@protoc_insertion_point(field_get:SessionProtos.LokiProfile.profilePicture) + return _internal_profilepicture(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void LokiProfile::set_profilepicture(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilepicture_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.LokiProfile.profilePicture) +} +inline std::string* LokiProfile::mutable_profilepicture() { + std::string* _s = _internal_mutable_profilepicture(); + // @@protoc_insertion_point(field_mutable:SessionProtos.LokiProfile.profilePicture) + return _s; +} +inline const std::string& LokiProfile::_internal_profilepicture() const { + return _impl_.profilepicture_.Get(); +} +inline void LokiProfile::_internal_set_profilepicture(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilepicture_.Set(value, GetArenaForAllocation()); +} +inline std::string* LokiProfile::_internal_mutable_profilepicture() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.profilepicture_.Mutable(GetArenaForAllocation()); +} +inline std::string* LokiProfile::release_profilepicture() { + // @@protoc_insertion_point(field_release:SessionProtos.LokiProfile.profilePicture) + if (!_internal_has_profilepicture()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.profilepicture_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void LokiProfile::set_allocated_profilepicture(std::string* profilepicture) { + if (profilepicture != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.profilepicture_.SetAllocated(profilepicture, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.LokiProfile.profilePicture) +} + +// ------------------------------------------------------------------- + +// DataMessage_Quote_QuotedAttachment + +// optional string contentType = 1; +inline bool DataMessage_Quote_QuotedAttachment::_internal_has_contenttype() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_Quote_QuotedAttachment::has_contenttype() const { + return _internal_has_contenttype(); +} +inline void DataMessage_Quote_QuotedAttachment::clear_contenttype() { + _impl_.contenttype_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_Quote_QuotedAttachment::contenttype() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.QuotedAttachment.contentType) + return _internal_contenttype(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Quote_QuotedAttachment::set_contenttype(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.contenttype_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.QuotedAttachment.contentType) +} +inline std::string* DataMessage_Quote_QuotedAttachment::mutable_contenttype() { + std::string* _s = _internal_mutable_contenttype(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.QuotedAttachment.contentType) + return _s; +} +inline const std::string& DataMessage_Quote_QuotedAttachment::_internal_contenttype() const { + return _impl_.contenttype_.Get(); +} +inline void DataMessage_Quote_QuotedAttachment::_internal_set_contenttype(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.contenttype_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote_QuotedAttachment::_internal_mutable_contenttype() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.contenttype_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote_QuotedAttachment::release_contenttype() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Quote.QuotedAttachment.contentType) + if (!_internal_has_contenttype()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.contenttype_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.contenttype_.IsDefault()) { + _impl_.contenttype_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Quote_QuotedAttachment::set_allocated_contenttype(std::string* contenttype) { + if (contenttype != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.contenttype_.SetAllocated(contenttype, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.contenttype_.IsDefault()) { + _impl_.contenttype_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Quote.QuotedAttachment.contentType) +} + +// optional string fileName = 2; +inline bool DataMessage_Quote_QuotedAttachment::_internal_has_filename() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_Quote_QuotedAttachment::has_filename() const { + return _internal_has_filename(); +} +inline void DataMessage_Quote_QuotedAttachment::clear_filename() { + _impl_.filename_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_Quote_QuotedAttachment::filename() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.QuotedAttachment.fileName) + return _internal_filename(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Quote_QuotedAttachment::set_filename(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.filename_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.QuotedAttachment.fileName) +} +inline std::string* DataMessage_Quote_QuotedAttachment::mutable_filename() { + std::string* _s = _internal_mutable_filename(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.QuotedAttachment.fileName) + return _s; +} +inline const std::string& DataMessage_Quote_QuotedAttachment::_internal_filename() const { + return _impl_.filename_.Get(); +} +inline void DataMessage_Quote_QuotedAttachment::_internal_set_filename(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.filename_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote_QuotedAttachment::_internal_mutable_filename() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.filename_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote_QuotedAttachment::release_filename() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Quote.QuotedAttachment.fileName) + if (!_internal_has_filename()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.filename_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.filename_.IsDefault()) { + _impl_.filename_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Quote_QuotedAttachment::set_allocated_filename(std::string* filename) { + if (filename != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.filename_.SetAllocated(filename, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.filename_.IsDefault()) { + _impl_.filename_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Quote.QuotedAttachment.fileName) +} + +// optional .SessionProtos.AttachmentPointer thumbnail = 3; +inline bool DataMessage_Quote_QuotedAttachment::_internal_has_thumbnail() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + PROTOBUF_ASSUME(!value || _impl_.thumbnail_ != nullptr); + return value; +} +inline bool DataMessage_Quote_QuotedAttachment::has_thumbnail() const { + return _internal_has_thumbnail(); +} +inline void DataMessage_Quote_QuotedAttachment::clear_thumbnail() { + if (_impl_.thumbnail_ != nullptr) _impl_.thumbnail_->Clear(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const ::SessionProtos::AttachmentPointer& DataMessage_Quote_QuotedAttachment::_internal_thumbnail() const { + const ::SessionProtos::AttachmentPointer* p = _impl_.thumbnail_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_AttachmentPointer_default_instance_); +} +inline const ::SessionProtos::AttachmentPointer& DataMessage_Quote_QuotedAttachment::thumbnail() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.QuotedAttachment.thumbnail) + return _internal_thumbnail(); +} +inline void DataMessage_Quote_QuotedAttachment::unsafe_arena_set_allocated_thumbnail( + ::SessionProtos::AttachmentPointer* thumbnail) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.thumbnail_); + } + _impl_.thumbnail_ = thumbnail; + if (thumbnail) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.Quote.QuotedAttachment.thumbnail) +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Quote_QuotedAttachment::release_thumbnail() { + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::AttachmentPointer* temp = _impl_.thumbnail_; + _impl_.thumbnail_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Quote_QuotedAttachment::unsafe_arena_release_thumbnail() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Quote.QuotedAttachment.thumbnail) + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::AttachmentPointer* temp = _impl_.thumbnail_; + _impl_.thumbnail_ = nullptr; + return temp; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Quote_QuotedAttachment::_internal_mutable_thumbnail() { + _impl_._has_bits_[0] |= 0x00000004u; + if (_impl_.thumbnail_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::AttachmentPointer>(GetArenaForAllocation()); + _impl_.thumbnail_ = p; + } + return _impl_.thumbnail_; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Quote_QuotedAttachment::mutable_thumbnail() { + ::SessionProtos::AttachmentPointer* _msg = _internal_mutable_thumbnail(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.QuotedAttachment.thumbnail) + return _msg; +} +inline void DataMessage_Quote_QuotedAttachment::set_allocated_thumbnail(::SessionProtos::AttachmentPointer* thumbnail) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.thumbnail_; + } + if (thumbnail) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(thumbnail); + if (message_arena != submessage_arena) { + thumbnail = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, thumbnail, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.thumbnail_ = thumbnail; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Quote.QuotedAttachment.thumbnail) +} + +// optional uint32 flags = 4; +inline bool DataMessage_Quote_QuotedAttachment::_internal_has_flags() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool DataMessage_Quote_QuotedAttachment::has_flags() const { + return _internal_has_flags(); +} +inline void DataMessage_Quote_QuotedAttachment::clear_flags() { + _impl_.flags_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t DataMessage_Quote_QuotedAttachment::_internal_flags() const { + return _impl_.flags_; +} +inline uint32_t DataMessage_Quote_QuotedAttachment::flags() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.QuotedAttachment.flags) + return _internal_flags(); +} +inline void DataMessage_Quote_QuotedAttachment::_internal_set_flags(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.flags_ = value; +} +inline void DataMessage_Quote_QuotedAttachment::set_flags(uint32_t value) { + _internal_set_flags(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.QuotedAttachment.flags) +} + +// ------------------------------------------------------------------- + +// DataMessage_Quote + +// required uint64 id = 1; +inline bool DataMessage_Quote::_internal_has_id() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool DataMessage_Quote::has_id() const { + return _internal_has_id(); +} +inline void DataMessage_Quote::clear_id() { + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint64_t DataMessage_Quote::_internal_id() const { + return _impl_.id_; +} +inline uint64_t DataMessage_Quote::id() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.id) + return _internal_id(); +} +inline void DataMessage_Quote::_internal_set_id(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.id_ = value; +} +inline void DataMessage_Quote::set_id(uint64_t value) { + _internal_set_id(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.id) +} + +// required string author = 2; +inline bool DataMessage_Quote::_internal_has_author() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_Quote::has_author() const { + return _internal_has_author(); +} +inline void DataMessage_Quote::clear_author() { + _impl_.author_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_Quote::author() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.author) + return _internal_author(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Quote::set_author(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.author) +} +inline std::string* DataMessage_Quote::mutable_author() { + std::string* _s = _internal_mutable_author(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.author) + return _s; +} +inline const std::string& DataMessage_Quote::_internal_author() const { + return _impl_.author_.Get(); +} +inline void DataMessage_Quote::_internal_set_author(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote::_internal_mutable_author() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.author_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote::release_author() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Quote.author) + if (!_internal_has_author()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.author_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Quote::set_allocated_author(std::string* author) { + if (author != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.author_.SetAllocated(author, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Quote.author) +} + +// optional string text = 3; +inline bool DataMessage_Quote::_internal_has_text() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_Quote::has_text() const { + return _internal_has_text(); +} +inline void DataMessage_Quote::clear_text() { + _impl_.text_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_Quote::text() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.text) + return _internal_text(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Quote::set_text(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.text_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Quote.text) +} +inline std::string* DataMessage_Quote::mutable_text() { + std::string* _s = _internal_mutable_text(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.text) + return _s; +} +inline const std::string& DataMessage_Quote::_internal_text() const { + return _impl_.text_.Get(); +} +inline void DataMessage_Quote::_internal_set_text(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.text_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote::_internal_mutable_text() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.text_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Quote::release_text() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Quote.text) + if (!_internal_has_text()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.text_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.text_.IsDefault()) { + _impl_.text_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Quote::set_allocated_text(std::string* text) { + if (text != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.text_.SetAllocated(text, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.text_.IsDefault()) { + _impl_.text_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Quote.text) +} + +// repeated .SessionProtos.DataMessage.Quote.QuotedAttachment attachments = 4; +inline int DataMessage_Quote::_internal_attachments_size() const { + return _impl_.attachments_.size(); +} +inline int DataMessage_Quote::attachments_size() const { + return _internal_attachments_size(); +} +inline void DataMessage_Quote::clear_attachments() { + _impl_.attachments_.Clear(); +} +inline ::SessionProtos::DataMessage_Quote_QuotedAttachment* DataMessage_Quote::mutable_attachments(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Quote.attachments) + return _impl_.attachments_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Quote_QuotedAttachment >* +DataMessage_Quote::mutable_attachments() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.Quote.attachments) + return &_impl_.attachments_; +} +inline const ::SessionProtos::DataMessage_Quote_QuotedAttachment& DataMessage_Quote::_internal_attachments(int index) const { + return _impl_.attachments_.Get(index); +} +inline const ::SessionProtos::DataMessage_Quote_QuotedAttachment& DataMessage_Quote::attachments(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Quote.attachments) + return _internal_attachments(index); +} +inline ::SessionProtos::DataMessage_Quote_QuotedAttachment* DataMessage_Quote::_internal_add_attachments() { + return _impl_.attachments_.Add(); +} +inline ::SessionProtos::DataMessage_Quote_QuotedAttachment* DataMessage_Quote::add_attachments() { + ::SessionProtos::DataMessage_Quote_QuotedAttachment* _add = _internal_add_attachments(); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.Quote.attachments) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Quote_QuotedAttachment >& +DataMessage_Quote::attachments() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.Quote.attachments) + return _impl_.attachments_; +} + +// ------------------------------------------------------------------- + +// DataMessage_Preview + +// required string url = 1; +inline bool DataMessage_Preview::_internal_has_url() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_Preview::has_url() const { + return _internal_has_url(); +} +inline void DataMessage_Preview::clear_url() { + _impl_.url_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_Preview::url() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Preview.url) + return _internal_url(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Preview::set_url(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.url_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Preview.url) +} +inline std::string* DataMessage_Preview::mutable_url() { + std::string* _s = _internal_mutable_url(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Preview.url) + return _s; +} +inline const std::string& DataMessage_Preview::_internal_url() const { + return _impl_.url_.Get(); +} +inline void DataMessage_Preview::_internal_set_url(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.url_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Preview::_internal_mutable_url() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.url_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Preview::release_url() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Preview.url) + if (!_internal_has_url()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.url_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Preview::set_allocated_url(std::string* url) { + if (url != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.url_.SetAllocated(url, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Preview.url) +} + +// optional string title = 2; +inline bool DataMessage_Preview::_internal_has_title() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_Preview::has_title() const { + return _internal_has_title(); +} +inline void DataMessage_Preview::clear_title() { + _impl_.title_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_Preview::title() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Preview.title) + return _internal_title(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Preview::set_title(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.title_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Preview.title) +} +inline std::string* DataMessage_Preview::mutable_title() { + std::string* _s = _internal_mutable_title(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Preview.title) + return _s; +} +inline const std::string& DataMessage_Preview::_internal_title() const { + return _impl_.title_.Get(); +} +inline void DataMessage_Preview::_internal_set_title(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.title_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Preview::_internal_mutable_title() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.title_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Preview::release_title() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Preview.title) + if (!_internal_has_title()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.title_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.title_.IsDefault()) { + _impl_.title_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Preview::set_allocated_title(std::string* title) { + if (title != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.title_.SetAllocated(title, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.title_.IsDefault()) { + _impl_.title_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Preview.title) +} + +// optional .SessionProtos.AttachmentPointer image = 3; +inline bool DataMessage_Preview::_internal_has_image() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + PROTOBUF_ASSUME(!value || _impl_.image_ != nullptr); + return value; +} +inline bool DataMessage_Preview::has_image() const { + return _internal_has_image(); +} +inline void DataMessage_Preview::clear_image() { + if (_impl_.image_ != nullptr) _impl_.image_->Clear(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const ::SessionProtos::AttachmentPointer& DataMessage_Preview::_internal_image() const { + const ::SessionProtos::AttachmentPointer* p = _impl_.image_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_AttachmentPointer_default_instance_); +} +inline const ::SessionProtos::AttachmentPointer& DataMessage_Preview::image() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Preview.image) + return _internal_image(); +} +inline void DataMessage_Preview::unsafe_arena_set_allocated_image( + ::SessionProtos::AttachmentPointer* image) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.image_); + } + _impl_.image_ = image; + if (image) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.Preview.image) +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Preview::release_image() { + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::AttachmentPointer* temp = _impl_.image_; + _impl_.image_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Preview::unsafe_arena_release_image() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Preview.image) + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::AttachmentPointer* temp = _impl_.image_; + _impl_.image_ = nullptr; + return temp; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Preview::_internal_mutable_image() { + _impl_._has_bits_[0] |= 0x00000004u; + if (_impl_.image_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::AttachmentPointer>(GetArenaForAllocation()); + _impl_.image_ = p; + } + return _impl_.image_; +} +inline ::SessionProtos::AttachmentPointer* DataMessage_Preview::mutable_image() { + ::SessionProtos::AttachmentPointer* _msg = _internal_mutable_image(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Preview.image) + return _msg; +} +inline void DataMessage_Preview::set_allocated_image(::SessionProtos::AttachmentPointer* image) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.image_; + } + if (image) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(image); + if (message_arena != submessage_arena) { + image = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, image, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.image_ = image; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Preview.image) +} + +// ------------------------------------------------------------------- + +// DataMessage_Reaction + +// required uint64 id = 1; +inline bool DataMessage_Reaction::_internal_has_id() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool DataMessage_Reaction::has_id() const { + return _internal_has_id(); +} +inline void DataMessage_Reaction::clear_id() { + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint64_t DataMessage_Reaction::_internal_id() const { + return _impl_.id_; +} +inline uint64_t DataMessage_Reaction::id() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Reaction.id) + return _internal_id(); +} +inline void DataMessage_Reaction::_internal_set_id(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.id_ = value; +} +inline void DataMessage_Reaction::set_id(uint64_t value) { + _internal_set_id(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Reaction.id) +} + +// required string author = 2; +inline bool DataMessage_Reaction::_internal_has_author() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_Reaction::has_author() const { + return _internal_has_author(); +} +inline void DataMessage_Reaction::clear_author() { + _impl_.author_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_Reaction::author() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Reaction.author) + return _internal_author(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Reaction::set_author(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Reaction.author) +} +inline std::string* DataMessage_Reaction::mutable_author() { + std::string* _s = _internal_mutable_author(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Reaction.author) + return _s; +} +inline const std::string& DataMessage_Reaction::_internal_author() const { + return _impl_.author_.Get(); +} +inline void DataMessage_Reaction::_internal_set_author(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.author_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Reaction::_internal_mutable_author() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.author_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Reaction::release_author() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Reaction.author) + if (!_internal_has_author()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.author_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Reaction::set_allocated_author(std::string* author) { + if (author != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.author_.SetAllocated(author, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.author_.IsDefault()) { + _impl_.author_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Reaction.author) +} + +// optional string emoji = 3; +inline bool DataMessage_Reaction::_internal_has_emoji() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_Reaction::has_emoji() const { + return _internal_has_emoji(); +} +inline void DataMessage_Reaction::clear_emoji() { + _impl_.emoji_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_Reaction::emoji() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Reaction.emoji) + return _internal_emoji(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_Reaction::set_emoji(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.emoji_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Reaction.emoji) +} +inline std::string* DataMessage_Reaction::mutable_emoji() { + std::string* _s = _internal_mutable_emoji(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.Reaction.emoji) + return _s; +} +inline const std::string& DataMessage_Reaction::_internal_emoji() const { + return _impl_.emoji_.Get(); +} +inline void DataMessage_Reaction::_internal_set_emoji(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.emoji_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_Reaction::_internal_mutable_emoji() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.emoji_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_Reaction::release_emoji() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.Reaction.emoji) + if (!_internal_has_emoji()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.emoji_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.emoji_.IsDefault()) { + _impl_.emoji_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_Reaction::set_allocated_emoji(std::string* emoji) { + if (emoji != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.emoji_.SetAllocated(emoji, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.emoji_.IsDefault()) { + _impl_.emoji_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.Reaction.emoji) +} + +// required .SessionProtos.DataMessage.Reaction.Action action = 4; +inline bool DataMessage_Reaction::_internal_has_action() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool DataMessage_Reaction::has_action() const { + return _internal_has_action(); +} +inline void DataMessage_Reaction::clear_action() { + _impl_.action_ = 0; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline ::SessionProtos::DataMessage_Reaction_Action DataMessage_Reaction::_internal_action() const { + return static_cast< ::SessionProtos::DataMessage_Reaction_Action >(_impl_.action_); +} +inline ::SessionProtos::DataMessage_Reaction_Action DataMessage_Reaction::action() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.Reaction.action) + return _internal_action(); +} +inline void DataMessage_Reaction::_internal_set_action(::SessionProtos::DataMessage_Reaction_Action value) { + assert(::SessionProtos::DataMessage_Reaction_Action_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.action_ = value; +} +inline void DataMessage_Reaction::set_action(::SessionProtos::DataMessage_Reaction_Action value) { + _internal_set_action(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.Reaction.action) +} + +// ------------------------------------------------------------------- + +// DataMessage_OpenGroupInvitation + +// required string url = 1; +inline bool DataMessage_OpenGroupInvitation::_internal_has_url() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_OpenGroupInvitation::has_url() const { + return _internal_has_url(); +} +inline void DataMessage_OpenGroupInvitation::clear_url() { + _impl_.url_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_OpenGroupInvitation::url() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.OpenGroupInvitation.url) + return _internal_url(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_OpenGroupInvitation::set_url(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.url_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.OpenGroupInvitation.url) +} +inline std::string* DataMessage_OpenGroupInvitation::mutable_url() { + std::string* _s = _internal_mutable_url(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.OpenGroupInvitation.url) + return _s; +} +inline const std::string& DataMessage_OpenGroupInvitation::_internal_url() const { + return _impl_.url_.Get(); +} +inline void DataMessage_OpenGroupInvitation::_internal_set_url(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.url_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_OpenGroupInvitation::_internal_mutable_url() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.url_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_OpenGroupInvitation::release_url() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.OpenGroupInvitation.url) + if (!_internal_has_url()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.url_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_OpenGroupInvitation::set_allocated_url(std::string* url) { + if (url != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.url_.SetAllocated(url, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.OpenGroupInvitation.url) +} + +// required string name = 3; +inline bool DataMessage_OpenGroupInvitation::_internal_has_name() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_OpenGroupInvitation::has_name() const { + return _internal_has_name(); +} +inline void DataMessage_OpenGroupInvitation::clear_name() { + _impl_.name_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_OpenGroupInvitation::name() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.OpenGroupInvitation.name) + return _internal_name(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_OpenGroupInvitation::set_name(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.OpenGroupInvitation.name) +} +inline std::string* DataMessage_OpenGroupInvitation::mutable_name() { + std::string* _s = _internal_mutable_name(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.OpenGroupInvitation.name) + return _s; +} +inline const std::string& DataMessage_OpenGroupInvitation::_internal_name() const { + return _impl_.name_.Get(); +} +inline void DataMessage_OpenGroupInvitation::_internal_set_name(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_OpenGroupInvitation::_internal_mutable_name() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.name_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_OpenGroupInvitation::release_name() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.OpenGroupInvitation.name) + if (!_internal_has_name()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.name_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_OpenGroupInvitation::set_allocated_name(std::string* name) { + if (name != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.name_.SetAllocated(name, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.OpenGroupInvitation.name) +} + +// ------------------------------------------------------------------- + +// DataMessage_ClosedGroupControlMessage_KeyPairWrapper + +// required bytes publicKey = 1; +inline bool DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_has_publickey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage_KeyPairWrapper::has_publickey() const { + return _internal_has_publickey(); +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::clear_publickey() { + _impl_.publickey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_ClosedGroupControlMessage_KeyPairWrapper::publickey() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.publicKey) + return _internal_publickey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::set_publickey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.publicKey) +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::mutable_publickey() { + std::string* _s = _internal_mutable_publickey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.publicKey) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_publickey() const { + return _impl_.publickey_.Get(); +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_set_publickey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_mutable_publickey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.publickey_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::release_publickey() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.publicKey) + if (!_internal_has_publickey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.publickey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::set_allocated_publickey(std::string* publickey) { + if (publickey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.publickey_.SetAllocated(publickey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.publicKey) +} + +// required bytes encryptedKeyPair = 2; +inline bool DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_has_encryptedkeypair() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage_KeyPairWrapper::has_encryptedkeypair() const { + return _internal_has_encryptedkeypair(); +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::clear_encryptedkeypair() { + _impl_.encryptedkeypair_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_ClosedGroupControlMessage_KeyPairWrapper::encryptedkeypair() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.encryptedKeyPair) + return _internal_encryptedkeypair(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::set_encryptedkeypair(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.encryptedkeypair_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.encryptedKeyPair) +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::mutable_encryptedkeypair() { + std::string* _s = _internal_mutable_encryptedkeypair(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.encryptedKeyPair) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_encryptedkeypair() const { + return _impl_.encryptedkeypair_.Get(); +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_set_encryptedkeypair(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.encryptedkeypair_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::_internal_mutable_encryptedkeypair() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.encryptedkeypair_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage_KeyPairWrapper::release_encryptedkeypair() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.encryptedKeyPair) + if (!_internal_has_encryptedkeypair()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.encryptedkeypair_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.encryptedkeypair_.IsDefault()) { + _impl_.encryptedkeypair_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_ClosedGroupControlMessage_KeyPairWrapper::set_allocated_encryptedkeypair(std::string* encryptedkeypair) { + if (encryptedkeypair != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.encryptedkeypair_.SetAllocated(encryptedkeypair, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.encryptedkeypair_.IsDefault()) { + _impl_.encryptedkeypair_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper.encryptedKeyPair) +} + +// ------------------------------------------------------------------- + +// DataMessage_ClosedGroupControlMessage + +// required .SessionProtos.DataMessage.ClosedGroupControlMessage.Type type = 1; +inline bool DataMessage_ClosedGroupControlMessage::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage::has_type() const { + return _internal_has_type(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_type() { + _impl_.type_ = 1; + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::_internal_type() const { + return static_cast< ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type >(_impl_.type_); +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type DataMessage_ClosedGroupControlMessage::type() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.type) + return _internal_type(); +} +inline void DataMessage_ClosedGroupControlMessage::_internal_set_type(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type value) { + assert(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.type_ = value; +} +inline void DataMessage_ClosedGroupControlMessage::set_type(::SessionProtos::DataMessage_ClosedGroupControlMessage_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.type) +} + +// optional bytes publicKey = 2; +inline bool DataMessage_ClosedGroupControlMessage::_internal_has_publickey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage::has_publickey() const { + return _internal_has_publickey(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_publickey() { + _impl_.publickey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::publickey() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.publicKey) + return _internal_publickey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_ClosedGroupControlMessage::set_publickey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.publicKey) +} +inline std::string* DataMessage_ClosedGroupControlMessage::mutable_publickey() { + std::string* _s = _internal_mutable_publickey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.publicKey) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::_internal_publickey() const { + return _impl_.publickey_.Get(); +} +inline void DataMessage_ClosedGroupControlMessage::_internal_set_publickey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage::_internal_mutable_publickey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.publickey_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage::release_publickey() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.ClosedGroupControlMessage.publicKey) + if (!_internal_has_publickey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.publickey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_ClosedGroupControlMessage::set_allocated_publickey(std::string* publickey) { + if (publickey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.publickey_.SetAllocated(publickey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.publicKey) +} + +// optional string name = 3; +inline bool DataMessage_ClosedGroupControlMessage::_internal_has_name() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage::has_name() const { + return _internal_has_name(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_name() { + _impl_.name_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::name() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.name) + return _internal_name(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage_ClosedGroupControlMessage::set_name(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.name) +} +inline std::string* DataMessage_ClosedGroupControlMessage::mutable_name() { + std::string* _s = _internal_mutable_name(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.name) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::_internal_name() const { + return _impl_.name_.Get(); +} +inline void DataMessage_ClosedGroupControlMessage::_internal_set_name(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage::_internal_mutable_name() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.name_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage_ClosedGroupControlMessage::release_name() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.ClosedGroupControlMessage.name) + if (!_internal_has_name()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.name_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage_ClosedGroupControlMessage::set_allocated_name(std::string* name) { + if (name != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.name_.SetAllocated(name, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.name) +} + +// optional .SessionProtos.KeyPair encryptionKeyPair = 4; +inline bool DataMessage_ClosedGroupControlMessage::_internal_has_encryptionkeypair() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + PROTOBUF_ASSUME(!value || _impl_.encryptionkeypair_ != nullptr); + return value; +} +inline bool DataMessage_ClosedGroupControlMessage::has_encryptionkeypair() const { + return _internal_has_encryptionkeypair(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_encryptionkeypair() { + if (_impl_.encryptionkeypair_ != nullptr) _impl_.encryptionkeypair_->Clear(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const ::SessionProtos::KeyPair& DataMessage_ClosedGroupControlMessage::_internal_encryptionkeypair() const { + const ::SessionProtos::KeyPair* p = _impl_.encryptionkeypair_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_KeyPair_default_instance_); +} +inline const ::SessionProtos::KeyPair& DataMessage_ClosedGroupControlMessage::encryptionkeypair() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.encryptionKeyPair) + return _internal_encryptionkeypair(); +} +inline void DataMessage_ClosedGroupControlMessage::unsafe_arena_set_allocated_encryptionkeypair( + ::SessionProtos::KeyPair* encryptionkeypair) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.encryptionkeypair_); + } + _impl_.encryptionkeypair_ = encryptionkeypair; + if (encryptionkeypair) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.encryptionKeyPair) +} +inline ::SessionProtos::KeyPair* DataMessage_ClosedGroupControlMessage::release_encryptionkeypair() { + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::KeyPair* temp = _impl_.encryptionkeypair_; + _impl_.encryptionkeypair_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::KeyPair* DataMessage_ClosedGroupControlMessage::unsafe_arena_release_encryptionkeypair() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.ClosedGroupControlMessage.encryptionKeyPair) + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::KeyPair* temp = _impl_.encryptionkeypair_; + _impl_.encryptionkeypair_ = nullptr; + return temp; +} +inline ::SessionProtos::KeyPair* DataMessage_ClosedGroupControlMessage::_internal_mutable_encryptionkeypair() { + _impl_._has_bits_[0] |= 0x00000004u; + if (_impl_.encryptionkeypair_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::KeyPair>(GetArenaForAllocation()); + _impl_.encryptionkeypair_ = p; + } + return _impl_.encryptionkeypair_; +} +inline ::SessionProtos::KeyPair* DataMessage_ClosedGroupControlMessage::mutable_encryptionkeypair() { + ::SessionProtos::KeyPair* _msg = _internal_mutable_encryptionkeypair(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.encryptionKeyPair) + return _msg; +} +inline void DataMessage_ClosedGroupControlMessage::set_allocated_encryptionkeypair(::SessionProtos::KeyPair* encryptionkeypair) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.encryptionkeypair_; + } + if (encryptionkeypair) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(encryptionkeypair); + if (message_arena != submessage_arena) { + encryptionkeypair = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, encryptionkeypair, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.encryptionkeypair_ = encryptionkeypair; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.ClosedGroupControlMessage.encryptionKeyPair) +} + +// repeated bytes members = 5; +inline int DataMessage_ClosedGroupControlMessage::_internal_members_size() const { + return _impl_.members_.size(); +} +inline int DataMessage_ClosedGroupControlMessage::members_size() const { + return _internal_members_size(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_members() { + _impl_.members_.Clear(); +} +inline std::string* DataMessage_ClosedGroupControlMessage::add_members() { + std::string* _s = _internal_add_members(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.members) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::_internal_members(int index) const { + return _impl_.members_.Get(index); +} +inline const std::string& DataMessage_ClosedGroupControlMessage::members(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.members) + return _internal_members(index); +} +inline std::string* DataMessage_ClosedGroupControlMessage::mutable_members(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.members) + return _impl_.members_.Mutable(index); +} +inline void DataMessage_ClosedGroupControlMessage::set_members(int index, const std::string& value) { + _impl_.members_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::set_members(int index, std::string&& value) { + _impl_.members_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::set_members(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.members_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::set_members(int index, const void* value, size_t size) { + _impl_.members_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline std::string* DataMessage_ClosedGroupControlMessage::_internal_add_members() { + return _impl_.members_.Add(); +} +inline void DataMessage_ClosedGroupControlMessage::add_members(const std::string& value) { + _impl_.members_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::add_members(std::string&& value) { + _impl_.members_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::add_members(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.members_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline void DataMessage_ClosedGroupControlMessage::add_members(const void* value, size_t size) { + _impl_.members_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.DataMessage.ClosedGroupControlMessage.members) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +DataMessage_ClosedGroupControlMessage::members() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.ClosedGroupControlMessage.members) + return _impl_.members_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +DataMessage_ClosedGroupControlMessage::mutable_members() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.ClosedGroupControlMessage.members) + return &_impl_.members_; +} + +// repeated bytes admins = 6; +inline int DataMessage_ClosedGroupControlMessage::_internal_admins_size() const { + return _impl_.admins_.size(); +} +inline int DataMessage_ClosedGroupControlMessage::admins_size() const { + return _internal_admins_size(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_admins() { + _impl_.admins_.Clear(); +} +inline std::string* DataMessage_ClosedGroupControlMessage::add_admins() { + std::string* _s = _internal_add_admins(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) + return _s; +} +inline const std::string& DataMessage_ClosedGroupControlMessage::_internal_admins(int index) const { + return _impl_.admins_.Get(index); +} +inline const std::string& DataMessage_ClosedGroupControlMessage::admins(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) + return _internal_admins(index); +} +inline std::string* DataMessage_ClosedGroupControlMessage::mutable_admins(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) + return _impl_.admins_.Mutable(index); +} +inline void DataMessage_ClosedGroupControlMessage::set_admins(int index, const std::string& value) { + _impl_.admins_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::set_admins(int index, std::string&& value) { + _impl_.admins_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::set_admins(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.admins_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::set_admins(int index, const void* value, size_t size) { + _impl_.admins_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline std::string* DataMessage_ClosedGroupControlMessage::_internal_add_admins() { + return _impl_.admins_.Add(); +} +inline void DataMessage_ClosedGroupControlMessage::add_admins(const std::string& value) { + _impl_.admins_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::add_admins(std::string&& value) { + _impl_.admins_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::add_admins(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.admins_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline void DataMessage_ClosedGroupControlMessage::add_admins(const void* value, size_t size) { + _impl_.admins_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +DataMessage_ClosedGroupControlMessage::admins() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) + return _impl_.admins_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +DataMessage_ClosedGroupControlMessage::mutable_admins() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.ClosedGroupControlMessage.admins) + return &_impl_.admins_; +} + +// repeated .SessionProtos.DataMessage.ClosedGroupControlMessage.KeyPairWrapper wrappers = 7; +inline int DataMessage_ClosedGroupControlMessage::_internal_wrappers_size() const { + return _impl_.wrappers_.size(); +} +inline int DataMessage_ClosedGroupControlMessage::wrappers_size() const { + return _internal_wrappers_size(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_wrappers() { + _impl_.wrappers_.Clear(); +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* DataMessage_ClosedGroupControlMessage::mutable_wrappers(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.ClosedGroupControlMessage.wrappers) + return _impl_.wrappers_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >* +DataMessage_ClosedGroupControlMessage::mutable_wrappers() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.ClosedGroupControlMessage.wrappers) + return &_impl_.wrappers_; +} +inline const ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper& DataMessage_ClosedGroupControlMessage::_internal_wrappers(int index) const { + return _impl_.wrappers_.Get(index); +} +inline const ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper& DataMessage_ClosedGroupControlMessage::wrappers(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.wrappers) + return _internal_wrappers(index); +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* DataMessage_ClosedGroupControlMessage::_internal_add_wrappers() { + return _impl_.wrappers_.Add(); +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* DataMessage_ClosedGroupControlMessage::add_wrappers() { + ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper* _add = _internal_add_wrappers(); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.ClosedGroupControlMessage.wrappers) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_ClosedGroupControlMessage_KeyPairWrapper >& +DataMessage_ClosedGroupControlMessage::wrappers() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.ClosedGroupControlMessage.wrappers) + return _impl_.wrappers_; +} + +// optional uint32 expirationTimer = 8; +inline bool DataMessage_ClosedGroupControlMessage::_internal_has_expirationtimer() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool DataMessage_ClosedGroupControlMessage::has_expirationtimer() const { + return _internal_has_expirationtimer(); +} +inline void DataMessage_ClosedGroupControlMessage::clear_expirationtimer() { + _impl_.expirationtimer_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t DataMessage_ClosedGroupControlMessage::_internal_expirationtimer() const { + return _impl_.expirationtimer_; +} +inline uint32_t DataMessage_ClosedGroupControlMessage::expirationtimer() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.ClosedGroupControlMessage.expirationTimer) + return _internal_expirationtimer(); +} +inline void DataMessage_ClosedGroupControlMessage::_internal_set_expirationtimer(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.expirationtimer_ = value; +} +inline void DataMessage_ClosedGroupControlMessage::set_expirationtimer(uint32_t value) { + _internal_set_expirationtimer(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.ClosedGroupControlMessage.expirationTimer) +} + +// ------------------------------------------------------------------- + +// DataMessage + +// optional string body = 1; +inline bool DataMessage::_internal_has_body() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool DataMessage::has_body() const { + return _internal_has_body(); +} +inline void DataMessage::clear_body() { + _impl_.body_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& DataMessage::body() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.body) + return _internal_body(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage::set_body(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.body_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.body) +} +inline std::string* DataMessage::mutable_body() { + std::string* _s = _internal_mutable_body(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.body) + return _s; +} +inline const std::string& DataMessage::_internal_body() const { + return _impl_.body_.Get(); +} +inline void DataMessage::_internal_set_body(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.body_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage::_internal_mutable_body() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.body_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage::release_body() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.body) + if (!_internal_has_body()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.body_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage::set_allocated_body(std::string* body) { + if (body != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.body_.SetAllocated(body, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.body) +} + +// repeated .SessionProtos.AttachmentPointer attachments = 2; +inline int DataMessage::_internal_attachments_size() const { + return _impl_.attachments_.size(); +} +inline int DataMessage::attachments_size() const { + return _internal_attachments_size(); +} +inline void DataMessage::clear_attachments() { + _impl_.attachments_.Clear(); +} +inline ::SessionProtos::AttachmentPointer* DataMessage::mutable_attachments(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.attachments) + return _impl_.attachments_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::AttachmentPointer >* +DataMessage::mutable_attachments() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.attachments) + return &_impl_.attachments_; +} +inline const ::SessionProtos::AttachmentPointer& DataMessage::_internal_attachments(int index) const { + return _impl_.attachments_.Get(index); +} +inline const ::SessionProtos::AttachmentPointer& DataMessage::attachments(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.attachments) + return _internal_attachments(index); +} +inline ::SessionProtos::AttachmentPointer* DataMessage::_internal_add_attachments() { + return _impl_.attachments_.Add(); +} +inline ::SessionProtos::AttachmentPointer* DataMessage::add_attachments() { + ::SessionProtos::AttachmentPointer* _add = _internal_add_attachments(); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.attachments) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::AttachmentPointer >& +DataMessage::attachments() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.attachments) + return _impl_.attachments_; +} + +// optional uint32 flags = 4; +inline bool DataMessage::_internal_has_flags() const { + bool value = (_impl_._has_bits_[0] & 0x00000100u) != 0; + return value; +} +inline bool DataMessage::has_flags() const { + return _internal_has_flags(); +} +inline void DataMessage::clear_flags() { + _impl_.flags_ = 0u; + _impl_._has_bits_[0] &= ~0x00000100u; +} +inline uint32_t DataMessage::_internal_flags() const { + return _impl_.flags_; +} +inline uint32_t DataMessage::flags() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.flags) + return _internal_flags(); +} +inline void DataMessage::_internal_set_flags(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000100u; + _impl_.flags_ = value; +} +inline void DataMessage::set_flags(uint32_t value) { + _internal_set_flags(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.flags) +} + +// optional uint32 expireTimer = 5; +inline bool DataMessage::_internal_has_expiretimer() const { + bool value = (_impl_._has_bits_[0] & 0x00000200u) != 0; + return value; +} +inline bool DataMessage::has_expiretimer() const { + return _internal_has_expiretimer(); +} +inline void DataMessage::clear_expiretimer() { + _impl_.expiretimer_ = 0u; + _impl_._has_bits_[0] &= ~0x00000200u; +} +inline uint32_t DataMessage::_internal_expiretimer() const { + return _impl_.expiretimer_; +} +inline uint32_t DataMessage::expiretimer() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.expireTimer) + return _internal_expiretimer(); +} +inline void DataMessage::_internal_set_expiretimer(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000200u; + _impl_.expiretimer_ = value; +} +inline void DataMessage::set_expiretimer(uint32_t value) { + _internal_set_expiretimer(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.expireTimer) +} + +// optional bytes profileKey = 6; +inline bool DataMessage::_internal_has_profilekey() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool DataMessage::has_profilekey() const { + return _internal_has_profilekey(); +} +inline void DataMessage::clear_profilekey() { + _impl_.profilekey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& DataMessage::profilekey() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.profileKey) + return _internal_profilekey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage::set_profilekey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilekey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.profileKey) +} +inline std::string* DataMessage::mutable_profilekey() { + std::string* _s = _internal_mutable_profilekey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.profileKey) + return _s; +} +inline const std::string& DataMessage::_internal_profilekey() const { + return _impl_.profilekey_.Get(); +} +inline void DataMessage::_internal_set_profilekey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilekey_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage::_internal_mutable_profilekey() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.profilekey_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage::release_profilekey() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.profileKey) + if (!_internal_has_profilekey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.profilekey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage::set_allocated_profilekey(std::string* profilekey) { + if (profilekey != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.profilekey_.SetAllocated(profilekey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.profileKey) +} + +// optional uint64 timestamp = 7; +inline bool DataMessage::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000400u) != 0; + return value; +} +inline bool DataMessage::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void DataMessage::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000400u; +} +inline uint64_t DataMessage::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t DataMessage::timestamp() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.timestamp) + return _internal_timestamp(); +} +inline void DataMessage::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000400u; + _impl_.timestamp_ = value; +} +inline void DataMessage::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.timestamp) +} + +// optional .SessionProtos.DataMessage.Quote quote = 8; +inline bool DataMessage::_internal_has_quote() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + PROTOBUF_ASSUME(!value || _impl_.quote_ != nullptr); + return value; +} +inline bool DataMessage::has_quote() const { + return _internal_has_quote(); +} +inline void DataMessage::clear_quote() { + if (_impl_.quote_ != nullptr) _impl_.quote_->Clear(); + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline const ::SessionProtos::DataMessage_Quote& DataMessage::_internal_quote() const { + const ::SessionProtos::DataMessage_Quote* p = _impl_.quote_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataMessage_Quote_default_instance_); +} +inline const ::SessionProtos::DataMessage_Quote& DataMessage::quote() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.quote) + return _internal_quote(); +} +inline void DataMessage::unsafe_arena_set_allocated_quote( + ::SessionProtos::DataMessage_Quote* quote) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.quote_); + } + _impl_.quote_ = quote; + if (quote) { + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.quote) +} +inline ::SessionProtos::DataMessage_Quote* DataMessage::release_quote() { + _impl_._has_bits_[0] &= ~0x00000008u; + ::SessionProtos::DataMessage_Quote* temp = _impl_.quote_; + _impl_.quote_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataMessage_Quote* DataMessage::unsafe_arena_release_quote() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.quote) + _impl_._has_bits_[0] &= ~0x00000008u; + ::SessionProtos::DataMessage_Quote* temp = _impl_.quote_; + _impl_.quote_ = nullptr; + return temp; +} +inline ::SessionProtos::DataMessage_Quote* DataMessage::_internal_mutable_quote() { + _impl_._has_bits_[0] |= 0x00000008u; + if (_impl_.quote_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataMessage_Quote>(GetArenaForAllocation()); + _impl_.quote_ = p; + } + return _impl_.quote_; +} +inline ::SessionProtos::DataMessage_Quote* DataMessage::mutable_quote() { + ::SessionProtos::DataMessage_Quote* _msg = _internal_mutable_quote(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.quote) + return _msg; +} +inline void DataMessage::set_allocated_quote(::SessionProtos::DataMessage_Quote* quote) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.quote_; + } + if (quote) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(quote); + if (message_arena != submessage_arena) { + quote = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, quote, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + _impl_.quote_ = quote; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.quote) +} + +// repeated .SessionProtos.DataMessage.Preview preview = 10; +inline int DataMessage::_internal_preview_size() const { + return _impl_.preview_.size(); +} +inline int DataMessage::preview_size() const { + return _internal_preview_size(); +} +inline void DataMessage::clear_preview() { + _impl_.preview_.Clear(); +} +inline ::SessionProtos::DataMessage_Preview* DataMessage::mutable_preview(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.preview) + return _impl_.preview_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Preview >* +DataMessage::mutable_preview() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.DataMessage.preview) + return &_impl_.preview_; +} +inline const ::SessionProtos::DataMessage_Preview& DataMessage::_internal_preview(int index) const { + return _impl_.preview_.Get(index); +} +inline const ::SessionProtos::DataMessage_Preview& DataMessage::preview(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.preview) + return _internal_preview(index); +} +inline ::SessionProtos::DataMessage_Preview* DataMessage::_internal_add_preview() { + return _impl_.preview_.Add(); +} +inline ::SessionProtos::DataMessage_Preview* DataMessage::add_preview() { + ::SessionProtos::DataMessage_Preview* _add = _internal_add_preview(); + // @@protoc_insertion_point(field_add:SessionProtos.DataMessage.preview) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::DataMessage_Preview >& +DataMessage::preview() const { + // @@protoc_insertion_point(field_list:SessionProtos.DataMessage.preview) + return _impl_.preview_; +} + +// optional .SessionProtos.DataMessage.Reaction reaction = 11; +inline bool DataMessage::_internal_has_reaction() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + PROTOBUF_ASSUME(!value || _impl_.reaction_ != nullptr); + return value; +} +inline bool DataMessage::has_reaction() const { + return _internal_has_reaction(); +} +inline void DataMessage::clear_reaction() { + if (_impl_.reaction_ != nullptr) _impl_.reaction_->Clear(); + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline const ::SessionProtos::DataMessage_Reaction& DataMessage::_internal_reaction() const { + const ::SessionProtos::DataMessage_Reaction* p = _impl_.reaction_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataMessage_Reaction_default_instance_); +} +inline const ::SessionProtos::DataMessage_Reaction& DataMessage::reaction() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.reaction) + return _internal_reaction(); +} +inline void DataMessage::unsafe_arena_set_allocated_reaction( + ::SessionProtos::DataMessage_Reaction* reaction) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.reaction_); + } + _impl_.reaction_ = reaction; + if (reaction) { + _impl_._has_bits_[0] |= 0x00000010u; + } else { + _impl_._has_bits_[0] &= ~0x00000010u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.reaction) +} +inline ::SessionProtos::DataMessage_Reaction* DataMessage::release_reaction() { + _impl_._has_bits_[0] &= ~0x00000010u; + ::SessionProtos::DataMessage_Reaction* temp = _impl_.reaction_; + _impl_.reaction_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataMessage_Reaction* DataMessage::unsafe_arena_release_reaction() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.reaction) + _impl_._has_bits_[0] &= ~0x00000010u; + ::SessionProtos::DataMessage_Reaction* temp = _impl_.reaction_; + _impl_.reaction_ = nullptr; + return temp; +} +inline ::SessionProtos::DataMessage_Reaction* DataMessage::_internal_mutable_reaction() { + _impl_._has_bits_[0] |= 0x00000010u; + if (_impl_.reaction_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataMessage_Reaction>(GetArenaForAllocation()); + _impl_.reaction_ = p; + } + return _impl_.reaction_; +} +inline ::SessionProtos::DataMessage_Reaction* DataMessage::mutable_reaction() { + ::SessionProtos::DataMessage_Reaction* _msg = _internal_mutable_reaction(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.reaction) + return _msg; +} +inline void DataMessage::set_allocated_reaction(::SessionProtos::DataMessage_Reaction* reaction) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.reaction_; + } + if (reaction) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(reaction); + if (message_arena != submessage_arena) { + reaction = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, reaction, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000010u; + } else { + _impl_._has_bits_[0] &= ~0x00000010u; + } + _impl_.reaction_ = reaction; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.reaction) +} + +// optional .SessionProtos.LokiProfile profile = 101; +inline bool DataMessage::_internal_has_profile() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + PROTOBUF_ASSUME(!value || _impl_.profile_ != nullptr); + return value; +} +inline bool DataMessage::has_profile() const { + return _internal_has_profile(); +} +inline void DataMessage::clear_profile() { + if (_impl_.profile_ != nullptr) _impl_.profile_->Clear(); + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline const ::SessionProtos::LokiProfile& DataMessage::_internal_profile() const { + const ::SessionProtos::LokiProfile* p = _impl_.profile_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_LokiProfile_default_instance_); +} +inline const ::SessionProtos::LokiProfile& DataMessage::profile() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.profile) + return _internal_profile(); +} +inline void DataMessage::unsafe_arena_set_allocated_profile( + ::SessionProtos::LokiProfile* profile) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.profile_); + } + _impl_.profile_ = profile; + if (profile) { + _impl_._has_bits_[0] |= 0x00000020u; + } else { + _impl_._has_bits_[0] &= ~0x00000020u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.profile) +} +inline ::SessionProtos::LokiProfile* DataMessage::release_profile() { + _impl_._has_bits_[0] &= ~0x00000020u; + ::SessionProtos::LokiProfile* temp = _impl_.profile_; + _impl_.profile_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::LokiProfile* DataMessage::unsafe_arena_release_profile() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.profile) + _impl_._has_bits_[0] &= ~0x00000020u; + ::SessionProtos::LokiProfile* temp = _impl_.profile_; + _impl_.profile_ = nullptr; + return temp; +} +inline ::SessionProtos::LokiProfile* DataMessage::_internal_mutable_profile() { + _impl_._has_bits_[0] |= 0x00000020u; + if (_impl_.profile_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::LokiProfile>(GetArenaForAllocation()); + _impl_.profile_ = p; + } + return _impl_.profile_; +} +inline ::SessionProtos::LokiProfile* DataMessage::mutable_profile() { + ::SessionProtos::LokiProfile* _msg = _internal_mutable_profile(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.profile) + return _msg; +} +inline void DataMessage::set_allocated_profile(::SessionProtos::LokiProfile* profile) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.profile_; + } + if (profile) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(profile); + if (message_arena != submessage_arena) { + profile = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, profile, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000020u; + } else { + _impl_._has_bits_[0] &= ~0x00000020u; + } + _impl_.profile_ = profile; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.profile) +} + +// optional .SessionProtos.DataMessage.OpenGroupInvitation openGroupInvitation = 102; +inline bool DataMessage::_internal_has_opengroupinvitation() const { + bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; + PROTOBUF_ASSUME(!value || _impl_.opengroupinvitation_ != nullptr); + return value; +} +inline bool DataMessage::has_opengroupinvitation() const { + return _internal_has_opengroupinvitation(); +} +inline void DataMessage::clear_opengroupinvitation() { + if (_impl_.opengroupinvitation_ != nullptr) _impl_.opengroupinvitation_->Clear(); + _impl_._has_bits_[0] &= ~0x00000040u; +} +inline const ::SessionProtos::DataMessage_OpenGroupInvitation& DataMessage::_internal_opengroupinvitation() const { + const ::SessionProtos::DataMessage_OpenGroupInvitation* p = _impl_.opengroupinvitation_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataMessage_OpenGroupInvitation_default_instance_); +} +inline const ::SessionProtos::DataMessage_OpenGroupInvitation& DataMessage::opengroupinvitation() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.openGroupInvitation) + return _internal_opengroupinvitation(); +} +inline void DataMessage::unsafe_arena_set_allocated_opengroupinvitation( + ::SessionProtos::DataMessage_OpenGroupInvitation* opengroupinvitation) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.opengroupinvitation_); + } + _impl_.opengroupinvitation_ = opengroupinvitation; + if (opengroupinvitation) { + _impl_._has_bits_[0] |= 0x00000040u; + } else { + _impl_._has_bits_[0] &= ~0x00000040u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.openGroupInvitation) +} +inline ::SessionProtos::DataMessage_OpenGroupInvitation* DataMessage::release_opengroupinvitation() { + _impl_._has_bits_[0] &= ~0x00000040u; + ::SessionProtos::DataMessage_OpenGroupInvitation* temp = _impl_.opengroupinvitation_; + _impl_.opengroupinvitation_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataMessage_OpenGroupInvitation* DataMessage::unsafe_arena_release_opengroupinvitation() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.openGroupInvitation) + _impl_._has_bits_[0] &= ~0x00000040u; + ::SessionProtos::DataMessage_OpenGroupInvitation* temp = _impl_.opengroupinvitation_; + _impl_.opengroupinvitation_ = nullptr; + return temp; +} +inline ::SessionProtos::DataMessage_OpenGroupInvitation* DataMessage::_internal_mutable_opengroupinvitation() { + _impl_._has_bits_[0] |= 0x00000040u; + if (_impl_.opengroupinvitation_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataMessage_OpenGroupInvitation>(GetArenaForAllocation()); + _impl_.opengroupinvitation_ = p; + } + return _impl_.opengroupinvitation_; +} +inline ::SessionProtos::DataMessage_OpenGroupInvitation* DataMessage::mutable_opengroupinvitation() { + ::SessionProtos::DataMessage_OpenGroupInvitation* _msg = _internal_mutable_opengroupinvitation(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.openGroupInvitation) + return _msg; +} +inline void DataMessage::set_allocated_opengroupinvitation(::SessionProtos::DataMessage_OpenGroupInvitation* opengroupinvitation) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.opengroupinvitation_; + } + if (opengroupinvitation) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(opengroupinvitation); + if (message_arena != submessage_arena) { + opengroupinvitation = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, opengroupinvitation, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000040u; + } else { + _impl_._has_bits_[0] &= ~0x00000040u; + } + _impl_.opengroupinvitation_ = opengroupinvitation; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.openGroupInvitation) +} + +// optional .SessionProtos.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; +inline bool DataMessage::_internal_has_closedgroupcontrolmessage() const { + bool value = (_impl_._has_bits_[0] & 0x00000080u) != 0; + PROTOBUF_ASSUME(!value || _impl_.closedgroupcontrolmessage_ != nullptr); + return value; +} +inline bool DataMessage::has_closedgroupcontrolmessage() const { + return _internal_has_closedgroupcontrolmessage(); +} +inline void DataMessage::clear_closedgroupcontrolmessage() { + if (_impl_.closedgroupcontrolmessage_ != nullptr) _impl_.closedgroupcontrolmessage_->Clear(); + _impl_._has_bits_[0] &= ~0x00000080u; +} +inline const ::SessionProtos::DataMessage_ClosedGroupControlMessage& DataMessage::_internal_closedgroupcontrolmessage() const { + const ::SessionProtos::DataMessage_ClosedGroupControlMessage* p = _impl_.closedgroupcontrolmessage_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_DataMessage_ClosedGroupControlMessage_default_instance_); +} +inline const ::SessionProtos::DataMessage_ClosedGroupControlMessage& DataMessage::closedgroupcontrolmessage() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.closedGroupControlMessage) + return _internal_closedgroupcontrolmessage(); +} +inline void DataMessage::unsafe_arena_set_allocated_closedgroupcontrolmessage( + ::SessionProtos::DataMessage_ClosedGroupControlMessage* closedgroupcontrolmessage) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.closedgroupcontrolmessage_); + } + _impl_.closedgroupcontrolmessage_ = closedgroupcontrolmessage; + if (closedgroupcontrolmessage) { + _impl_._has_bits_[0] |= 0x00000080u; + } else { + _impl_._has_bits_[0] &= ~0x00000080u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.DataMessage.closedGroupControlMessage) +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage* DataMessage::release_closedgroupcontrolmessage() { + _impl_._has_bits_[0] &= ~0x00000080u; + ::SessionProtos::DataMessage_ClosedGroupControlMessage* temp = _impl_.closedgroupcontrolmessage_; + _impl_.closedgroupcontrolmessage_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage* DataMessage::unsafe_arena_release_closedgroupcontrolmessage() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.closedGroupControlMessage) + _impl_._has_bits_[0] &= ~0x00000080u; + ::SessionProtos::DataMessage_ClosedGroupControlMessage* temp = _impl_.closedgroupcontrolmessage_; + _impl_.closedgroupcontrolmessage_ = nullptr; + return temp; +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage* DataMessage::_internal_mutable_closedgroupcontrolmessage() { + _impl_._has_bits_[0] |= 0x00000080u; + if (_impl_.closedgroupcontrolmessage_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::DataMessage_ClosedGroupControlMessage>(GetArenaForAllocation()); + _impl_.closedgroupcontrolmessage_ = p; + } + return _impl_.closedgroupcontrolmessage_; +} +inline ::SessionProtos::DataMessage_ClosedGroupControlMessage* DataMessage::mutable_closedgroupcontrolmessage() { + ::SessionProtos::DataMessage_ClosedGroupControlMessage* _msg = _internal_mutable_closedgroupcontrolmessage(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.closedGroupControlMessage) + return _msg; +} +inline void DataMessage::set_allocated_closedgroupcontrolmessage(::SessionProtos::DataMessage_ClosedGroupControlMessage* closedgroupcontrolmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.closedgroupcontrolmessage_; + } + if (closedgroupcontrolmessage) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(closedgroupcontrolmessage); + if (message_arena != submessage_arena) { + closedgroupcontrolmessage = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, closedgroupcontrolmessage, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000080u; + } else { + _impl_._has_bits_[0] &= ~0x00000080u; + } + _impl_.closedgroupcontrolmessage_ = closedgroupcontrolmessage; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.closedGroupControlMessage) +} + +// optional string syncTarget = 105; +inline bool DataMessage::_internal_has_synctarget() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool DataMessage::has_synctarget() const { + return _internal_has_synctarget(); +} +inline void DataMessage::clear_synctarget() { + _impl_.synctarget_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const std::string& DataMessage::synctarget() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.syncTarget) + return _internal_synctarget(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void DataMessage::set_synctarget(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.synctarget_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.syncTarget) +} +inline std::string* DataMessage::mutable_synctarget() { + std::string* _s = _internal_mutable_synctarget(); + // @@protoc_insertion_point(field_mutable:SessionProtos.DataMessage.syncTarget) + return _s; +} +inline const std::string& DataMessage::_internal_synctarget() const { + return _impl_.synctarget_.Get(); +} +inline void DataMessage::_internal_set_synctarget(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.synctarget_.Set(value, GetArenaForAllocation()); +} +inline std::string* DataMessage::_internal_mutable_synctarget() { + _impl_._has_bits_[0] |= 0x00000004u; + return _impl_.synctarget_.Mutable(GetArenaForAllocation()); +} +inline std::string* DataMessage::release_synctarget() { + // @@protoc_insertion_point(field_release:SessionProtos.DataMessage.syncTarget) + if (!_internal_has_synctarget()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000004u; + auto* p = _impl_.synctarget_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.synctarget_.IsDefault()) { + _impl_.synctarget_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void DataMessage::set_allocated_synctarget(std::string* synctarget) { + if (synctarget != nullptr) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.synctarget_.SetAllocated(synctarget, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.synctarget_.IsDefault()) { + _impl_.synctarget_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.DataMessage.syncTarget) +} + +// optional bool blocksCommunityMessageRequests = 106; +inline bool DataMessage::_internal_has_blockscommunitymessagerequests() const { + bool value = (_impl_._has_bits_[0] & 0x00000800u) != 0; + return value; +} +inline bool DataMessage::has_blockscommunitymessagerequests() const { + return _internal_has_blockscommunitymessagerequests(); +} +inline void DataMessage::clear_blockscommunitymessagerequests() { + _impl_.blockscommunitymessagerequests_ = false; + _impl_._has_bits_[0] &= ~0x00000800u; +} +inline bool DataMessage::_internal_blockscommunitymessagerequests() const { + return _impl_.blockscommunitymessagerequests_; +} +inline bool DataMessage::blockscommunitymessagerequests() const { + // @@protoc_insertion_point(field_get:SessionProtos.DataMessage.blocksCommunityMessageRequests) + return _internal_blockscommunitymessagerequests(); +} +inline void DataMessage::_internal_set_blockscommunitymessagerequests(bool value) { + _impl_._has_bits_[0] |= 0x00000800u; + _impl_.blockscommunitymessagerequests_ = value; +} +inline void DataMessage::set_blockscommunitymessagerequests(bool value) { + _internal_set_blockscommunitymessagerequests(value); + // @@protoc_insertion_point(field_set:SessionProtos.DataMessage.blocksCommunityMessageRequests) +} + +// ------------------------------------------------------------------- + +// ConfigurationMessage_ClosedGroup + +// optional bytes publicKey = 1; +inline bool ConfigurationMessage_ClosedGroup::_internal_has_publickey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool ConfigurationMessage_ClosedGroup::has_publickey() const { + return _internal_has_publickey(); +} +inline void ConfigurationMessage_ClosedGroup::clear_publickey() { + _impl_.publickey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& ConfigurationMessage_ClosedGroup::publickey() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.publicKey) + return _internal_publickey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_ClosedGroup::set_publickey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.publicKey) +} +inline std::string* ConfigurationMessage_ClosedGroup::mutable_publickey() { + std::string* _s = _internal_mutable_publickey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.publicKey) + return _s; +} +inline const std::string& ConfigurationMessage_ClosedGroup::_internal_publickey() const { + return _impl_.publickey_.Get(); +} +inline void ConfigurationMessage_ClosedGroup::_internal_set_publickey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_ClosedGroup::_internal_mutable_publickey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.publickey_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_ClosedGroup::release_publickey() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.ClosedGroup.publicKey) + if (!_internal_has_publickey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.publickey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_ClosedGroup::set_allocated_publickey(std::string* publickey) { + if (publickey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.publickey_.SetAllocated(publickey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.ClosedGroup.publicKey) +} + +// optional string name = 2; +inline bool ConfigurationMessage_ClosedGroup::_internal_has_name() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool ConfigurationMessage_ClosedGroup::has_name() const { + return _internal_has_name(); +} +inline void ConfigurationMessage_ClosedGroup::clear_name() { + _impl_.name_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& ConfigurationMessage_ClosedGroup::name() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.name) + return _internal_name(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_ClosedGroup::set_name(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.name) +} +inline std::string* ConfigurationMessage_ClosedGroup::mutable_name() { + std::string* _s = _internal_mutable_name(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.name) + return _s; +} +inline const std::string& ConfigurationMessage_ClosedGroup::_internal_name() const { + return _impl_.name_.Get(); +} +inline void ConfigurationMessage_ClosedGroup::_internal_set_name(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_ClosedGroup::_internal_mutable_name() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.name_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_ClosedGroup::release_name() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.ClosedGroup.name) + if (!_internal_has_name()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.name_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_ClosedGroup::set_allocated_name(std::string* name) { + if (name != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.name_.SetAllocated(name, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.ClosedGroup.name) +} + +// optional .SessionProtos.KeyPair encryptionKeyPair = 3; +inline bool ConfigurationMessage_ClosedGroup::_internal_has_encryptionkeypair() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + PROTOBUF_ASSUME(!value || _impl_.encryptionkeypair_ != nullptr); + return value; +} +inline bool ConfigurationMessage_ClosedGroup::has_encryptionkeypair() const { + return _internal_has_encryptionkeypair(); +} +inline void ConfigurationMessage_ClosedGroup::clear_encryptionkeypair() { + if (_impl_.encryptionkeypair_ != nullptr) _impl_.encryptionkeypair_->Clear(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const ::SessionProtos::KeyPair& ConfigurationMessage_ClosedGroup::_internal_encryptionkeypair() const { + const ::SessionProtos::KeyPair* p = _impl_.encryptionkeypair_; + return p != nullptr ? *p : reinterpret_cast( + ::SessionProtos::_KeyPair_default_instance_); +} +inline const ::SessionProtos::KeyPair& ConfigurationMessage_ClosedGroup::encryptionkeypair() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.encryptionKeyPair) + return _internal_encryptionkeypair(); +} +inline void ConfigurationMessage_ClosedGroup::unsafe_arena_set_allocated_encryptionkeypair( + ::SessionProtos::KeyPair* encryptionkeypair) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.encryptionkeypair_); + } + _impl_.encryptionkeypair_ = encryptionkeypair; + if (encryptionkeypair) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:SessionProtos.ConfigurationMessage.ClosedGroup.encryptionKeyPair) +} +inline ::SessionProtos::KeyPair* ConfigurationMessage_ClosedGroup::release_encryptionkeypair() { + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::KeyPair* temp = _impl_.encryptionkeypair_; + _impl_.encryptionkeypair_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::SessionProtos::KeyPair* ConfigurationMessage_ClosedGroup::unsafe_arena_release_encryptionkeypair() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.ClosedGroup.encryptionKeyPair) + _impl_._has_bits_[0] &= ~0x00000004u; + ::SessionProtos::KeyPair* temp = _impl_.encryptionkeypair_; + _impl_.encryptionkeypair_ = nullptr; + return temp; +} +inline ::SessionProtos::KeyPair* ConfigurationMessage_ClosedGroup::_internal_mutable_encryptionkeypair() { + _impl_._has_bits_[0] |= 0x00000004u; + if (_impl_.encryptionkeypair_ == nullptr) { + auto* p = CreateMaybeMessage<::SessionProtos::KeyPair>(GetArenaForAllocation()); + _impl_.encryptionkeypair_ = p; + } + return _impl_.encryptionkeypair_; +} +inline ::SessionProtos::KeyPair* ConfigurationMessage_ClosedGroup::mutable_encryptionkeypair() { + ::SessionProtos::KeyPair* _msg = _internal_mutable_encryptionkeypair(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.encryptionKeyPair) + return _msg; +} +inline void ConfigurationMessage_ClosedGroup::set_allocated_encryptionkeypair(::SessionProtos::KeyPair* encryptionkeypair) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.encryptionkeypair_; + } + if (encryptionkeypair) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(encryptionkeypair); + if (message_arena != submessage_arena) { + encryptionkeypair = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, encryptionkeypair, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.encryptionkeypair_ = encryptionkeypair; + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.ClosedGroup.encryptionKeyPair) +} + +// repeated bytes members = 4; +inline int ConfigurationMessage_ClosedGroup::_internal_members_size() const { + return _impl_.members_.size(); +} +inline int ConfigurationMessage_ClosedGroup::members_size() const { + return _internal_members_size(); +} +inline void ConfigurationMessage_ClosedGroup::clear_members() { + _impl_.members_.Clear(); +} +inline std::string* ConfigurationMessage_ClosedGroup::add_members() { + std::string* _s = _internal_add_members(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.members) + return _s; +} +inline const std::string& ConfigurationMessage_ClosedGroup::_internal_members(int index) const { + return _impl_.members_.Get(index); +} +inline const std::string& ConfigurationMessage_ClosedGroup::members(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.members) + return _internal_members(index); +} +inline std::string* ConfigurationMessage_ClosedGroup::mutable_members(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.members) + return _impl_.members_.Mutable(index); +} +inline void ConfigurationMessage_ClosedGroup::set_members(int index, const std::string& value) { + _impl_.members_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::set_members(int index, std::string&& value) { + _impl_.members_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::set_members(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.members_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::set_members(int index, const void* value, size_t size) { + _impl_.members_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline std::string* ConfigurationMessage_ClosedGroup::_internal_add_members() { + return _impl_.members_.Add(); +} +inline void ConfigurationMessage_ClosedGroup::add_members(const std::string& value) { + _impl_.members_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::add_members(std::string&& value) { + _impl_.members_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::add_members(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.members_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline void ConfigurationMessage_ClosedGroup::add_members(const void* value, size_t size) { + _impl_.members_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.ConfigurationMessage.ClosedGroup.members) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +ConfigurationMessage_ClosedGroup::members() const { + // @@protoc_insertion_point(field_list:SessionProtos.ConfigurationMessage.ClosedGroup.members) + return _impl_.members_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +ConfigurationMessage_ClosedGroup::mutable_members() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ConfigurationMessage.ClosedGroup.members) + return &_impl_.members_; +} + +// repeated bytes admins = 5; +inline int ConfigurationMessage_ClosedGroup::_internal_admins_size() const { + return _impl_.admins_.size(); +} +inline int ConfigurationMessage_ClosedGroup::admins_size() const { + return _internal_admins_size(); +} +inline void ConfigurationMessage_ClosedGroup::clear_admins() { + _impl_.admins_.Clear(); +} +inline std::string* ConfigurationMessage_ClosedGroup::add_admins() { + std::string* _s = _internal_add_admins(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.admins) + return _s; +} +inline const std::string& ConfigurationMessage_ClosedGroup::_internal_admins(int index) const { + return _impl_.admins_.Get(index); +} +inline const std::string& ConfigurationMessage_ClosedGroup::admins(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.admins) + return _internal_admins(index); +} +inline std::string* ConfigurationMessage_ClosedGroup::mutable_admins(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.ClosedGroup.admins) + return _impl_.admins_.Mutable(index); +} +inline void ConfigurationMessage_ClosedGroup::set_admins(int index, const std::string& value) { + _impl_.admins_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::set_admins(int index, std::string&& value) { + _impl_.admins_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::set_admins(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.admins_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::set_admins(int index, const void* value, size_t size) { + _impl_.admins_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline std::string* ConfigurationMessage_ClosedGroup::_internal_add_admins() { + return _impl_.admins_.Add(); +} +inline void ConfigurationMessage_ClosedGroup::add_admins(const std::string& value) { + _impl_.admins_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::add_admins(std::string&& value) { + _impl_.admins_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::add_admins(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.admins_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline void ConfigurationMessage_ClosedGroup::add_admins(const void* value, size_t size) { + _impl_.admins_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.ConfigurationMessage.ClosedGroup.admins) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +ConfigurationMessage_ClosedGroup::admins() const { + // @@protoc_insertion_point(field_list:SessionProtos.ConfigurationMessage.ClosedGroup.admins) + return _impl_.admins_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +ConfigurationMessage_ClosedGroup::mutable_admins() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ConfigurationMessage.ClosedGroup.admins) + return &_impl_.admins_; +} + +// optional uint32 expirationTimer = 6; +inline bool ConfigurationMessage_ClosedGroup::_internal_has_expirationtimer() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool ConfigurationMessage_ClosedGroup::has_expirationtimer() const { + return _internal_has_expirationtimer(); +} +inline void ConfigurationMessage_ClosedGroup::clear_expirationtimer() { + _impl_.expirationtimer_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t ConfigurationMessage_ClosedGroup::_internal_expirationtimer() const { + return _impl_.expirationtimer_; +} +inline uint32_t ConfigurationMessage_ClosedGroup::expirationtimer() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.ClosedGroup.expirationTimer) + return _internal_expirationtimer(); +} +inline void ConfigurationMessage_ClosedGroup::_internal_set_expirationtimer(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.expirationtimer_ = value; +} +inline void ConfigurationMessage_ClosedGroup::set_expirationtimer(uint32_t value) { + _internal_set_expirationtimer(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.ClosedGroup.expirationTimer) +} + +// ------------------------------------------------------------------- + +// ConfigurationMessage_Contact + +// required bytes publicKey = 1; +inline bool ConfigurationMessage_Contact::_internal_has_publickey() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_publickey() const { + return _internal_has_publickey(); +} +inline void ConfigurationMessage_Contact::clear_publickey() { + _impl_.publickey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& ConfigurationMessage_Contact::publickey() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.publicKey) + return _internal_publickey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_Contact::set_publickey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.publicKey) +} +inline std::string* ConfigurationMessage_Contact::mutable_publickey() { + std::string* _s = _internal_mutable_publickey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.Contact.publicKey) + return _s; +} +inline const std::string& ConfigurationMessage_Contact::_internal_publickey() const { + return _impl_.publickey_.Get(); +} +inline void ConfigurationMessage_Contact::_internal_set_publickey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.publickey_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::_internal_mutable_publickey() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.publickey_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::release_publickey() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.Contact.publicKey) + if (!_internal_has_publickey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.publickey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_Contact::set_allocated_publickey(std::string* publickey) { + if (publickey != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.publickey_.SetAllocated(publickey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.publickey_.IsDefault()) { + _impl_.publickey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.Contact.publicKey) +} + +// required string name = 2; +inline bool ConfigurationMessage_Contact::_internal_has_name() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_name() const { + return _internal_has_name(); +} +inline void ConfigurationMessage_Contact::clear_name() { + _impl_.name_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& ConfigurationMessage_Contact::name() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.name) + return _internal_name(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_Contact::set_name(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.name) +} +inline std::string* ConfigurationMessage_Contact::mutable_name() { + std::string* _s = _internal_mutable_name(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.Contact.name) + return _s; +} +inline const std::string& ConfigurationMessage_Contact::_internal_name() const { + return _impl_.name_.Get(); +} +inline void ConfigurationMessage_Contact::_internal_set_name(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.name_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::_internal_mutable_name() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.name_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::release_name() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.Contact.name) + if (!_internal_has_name()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.name_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_Contact::set_allocated_name(std::string* name) { + if (name != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.name_.SetAllocated(name, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.name_.IsDefault()) { + _impl_.name_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.Contact.name) +} + +// optional string profilePicture = 3; +inline bool ConfigurationMessage_Contact::_internal_has_profilepicture() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_profilepicture() const { + return _internal_has_profilepicture(); +} +inline void ConfigurationMessage_Contact::clear_profilepicture() { + _impl_.profilepicture_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const std::string& ConfigurationMessage_Contact::profilepicture() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.profilePicture) + return _internal_profilepicture(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_Contact::set_profilepicture(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.profilepicture_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.profilePicture) +} +inline std::string* ConfigurationMessage_Contact::mutable_profilepicture() { + std::string* _s = _internal_mutable_profilepicture(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.Contact.profilePicture) + return _s; +} +inline const std::string& ConfigurationMessage_Contact::_internal_profilepicture() const { + return _impl_.profilepicture_.Get(); +} +inline void ConfigurationMessage_Contact::_internal_set_profilepicture(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.profilepicture_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::_internal_mutable_profilepicture() { + _impl_._has_bits_[0] |= 0x00000004u; + return _impl_.profilepicture_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::release_profilepicture() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.Contact.profilePicture) + if (!_internal_has_profilepicture()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000004u; + auto* p = _impl_.profilepicture_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_Contact::set_allocated_profilepicture(std::string* profilepicture) { + if (profilepicture != nullptr) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.profilepicture_.SetAllocated(profilepicture, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.Contact.profilePicture) +} + +// optional bytes profileKey = 4; +inline bool ConfigurationMessage_Contact::_internal_has_profilekey() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_profilekey() const { + return _internal_has_profilekey(); +} +inline void ConfigurationMessage_Contact::clear_profilekey() { + _impl_.profilekey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline const std::string& ConfigurationMessage_Contact::profilekey() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.profileKey) + return _internal_profilekey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage_Contact::set_profilekey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.profilekey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.profileKey) +} +inline std::string* ConfigurationMessage_Contact::mutable_profilekey() { + std::string* _s = _internal_mutable_profilekey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.Contact.profileKey) + return _s; +} +inline const std::string& ConfigurationMessage_Contact::_internal_profilekey() const { + return _impl_.profilekey_.Get(); +} +inline void ConfigurationMessage_Contact::_internal_set_profilekey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.profilekey_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::_internal_mutable_profilekey() { + _impl_._has_bits_[0] |= 0x00000008u; + return _impl_.profilekey_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage_Contact::release_profilekey() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.Contact.profileKey) + if (!_internal_has_profilekey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000008u; + auto* p = _impl_.profilekey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage_Contact::set_allocated_profilekey(std::string* profilekey) { + if (profilekey != nullptr) { + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + _impl_.profilekey_.SetAllocated(profilekey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.Contact.profileKey) +} + +// optional bool isApproved = 5; +inline bool ConfigurationMessage_Contact::_internal_has_isapproved() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_isapproved() const { + return _internal_has_isapproved(); +} +inline void ConfigurationMessage_Contact::clear_isapproved() { + _impl_.isapproved_ = false; + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline bool ConfigurationMessage_Contact::_internal_isapproved() const { + return _impl_.isapproved_; +} +inline bool ConfigurationMessage_Contact::isapproved() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.isApproved) + return _internal_isapproved(); +} +inline void ConfigurationMessage_Contact::_internal_set_isapproved(bool value) { + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.isapproved_ = value; +} +inline void ConfigurationMessage_Contact::set_isapproved(bool value) { + _internal_set_isapproved(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.isApproved) +} + +// optional bool isBlocked = 6; +inline bool ConfigurationMessage_Contact::_internal_has_isblocked() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_isblocked() const { + return _internal_has_isblocked(); +} +inline void ConfigurationMessage_Contact::clear_isblocked() { + _impl_.isblocked_ = false; + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline bool ConfigurationMessage_Contact::_internal_isblocked() const { + return _impl_.isblocked_; +} +inline bool ConfigurationMessage_Contact::isblocked() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.isBlocked) + return _internal_isblocked(); +} +inline void ConfigurationMessage_Contact::_internal_set_isblocked(bool value) { + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.isblocked_ = value; +} +inline void ConfigurationMessage_Contact::set_isblocked(bool value) { + _internal_set_isblocked(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.isBlocked) +} + +// optional bool didApproveMe = 7; +inline bool ConfigurationMessage_Contact::_internal_has_didapproveme() const { + bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; + return value; +} +inline bool ConfigurationMessage_Contact::has_didapproveme() const { + return _internal_has_didapproveme(); +} +inline void ConfigurationMessage_Contact::clear_didapproveme() { + _impl_.didapproveme_ = false; + _impl_._has_bits_[0] &= ~0x00000040u; +} +inline bool ConfigurationMessage_Contact::_internal_didapproveme() const { + return _impl_.didapproveme_; +} +inline bool ConfigurationMessage_Contact::didapproveme() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.Contact.didApproveMe) + return _internal_didapproveme(); +} +inline void ConfigurationMessage_Contact::_internal_set_didapproveme(bool value) { + _impl_._has_bits_[0] |= 0x00000040u; + _impl_.didapproveme_ = value; +} +inline void ConfigurationMessage_Contact::set_didapproveme(bool value) { + _internal_set_didapproveme(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.Contact.didApproveMe) +} + +// ------------------------------------------------------------------- + +// ConfigurationMessage + +// repeated .SessionProtos.ConfigurationMessage.ClosedGroup closedGroups = 1; +inline int ConfigurationMessage::_internal_closedgroups_size() const { + return _impl_.closedgroups_.size(); +} +inline int ConfigurationMessage::closedgroups_size() const { + return _internal_closedgroups_size(); +} +inline void ConfigurationMessage::clear_closedgroups() { + _impl_.closedgroups_.Clear(); +} +inline ::SessionProtos::ConfigurationMessage_ClosedGroup* ConfigurationMessage::mutable_closedgroups(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.closedGroups) + return _impl_.closedgroups_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_ClosedGroup >* +ConfigurationMessage::mutable_closedgroups() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ConfigurationMessage.closedGroups) + return &_impl_.closedgroups_; +} +inline const ::SessionProtos::ConfigurationMessage_ClosedGroup& ConfigurationMessage::_internal_closedgroups(int index) const { + return _impl_.closedgroups_.Get(index); +} +inline const ::SessionProtos::ConfigurationMessage_ClosedGroup& ConfigurationMessage::closedgroups(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.closedGroups) + return _internal_closedgroups(index); +} +inline ::SessionProtos::ConfigurationMessage_ClosedGroup* ConfigurationMessage::_internal_add_closedgroups() { + return _impl_.closedgroups_.Add(); +} +inline ::SessionProtos::ConfigurationMessage_ClosedGroup* ConfigurationMessage::add_closedgroups() { + ::SessionProtos::ConfigurationMessage_ClosedGroup* _add = _internal_add_closedgroups(); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.closedGroups) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_ClosedGroup >& +ConfigurationMessage::closedgroups() const { + // @@protoc_insertion_point(field_list:SessionProtos.ConfigurationMessage.closedGroups) + return _impl_.closedgroups_; +} + +// repeated string openGroups = 2; +inline int ConfigurationMessage::_internal_opengroups_size() const { + return _impl_.opengroups_.size(); +} +inline int ConfigurationMessage::opengroups_size() const { + return _internal_opengroups_size(); +} +inline void ConfigurationMessage::clear_opengroups() { + _impl_.opengroups_.Clear(); +} +inline std::string* ConfigurationMessage::add_opengroups() { + std::string* _s = _internal_add_opengroups(); + // @@protoc_insertion_point(field_add_mutable:SessionProtos.ConfigurationMessage.openGroups) + return _s; +} +inline const std::string& ConfigurationMessage::_internal_opengroups(int index) const { + return _impl_.opengroups_.Get(index); +} +inline const std::string& ConfigurationMessage::opengroups(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.openGroups) + return _internal_opengroups(index); +} +inline std::string* ConfigurationMessage::mutable_opengroups(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.openGroups) + return _impl_.opengroups_.Mutable(index); +} +inline void ConfigurationMessage::set_opengroups(int index, const std::string& value) { + _impl_.opengroups_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::set_opengroups(int index, std::string&& value) { + _impl_.opengroups_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::set_opengroups(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.opengroups_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::set_opengroups(int index, const char* value, size_t size) { + _impl_.opengroups_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:SessionProtos.ConfigurationMessage.openGroups) +} +inline std::string* ConfigurationMessage::_internal_add_opengroups() { + return _impl_.opengroups_.Add(); +} +inline void ConfigurationMessage::add_opengroups(const std::string& value) { + _impl_.opengroups_.Add()->assign(value); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::add_opengroups(std::string&& value) { + _impl_.opengroups_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::add_opengroups(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.opengroups_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:SessionProtos.ConfigurationMessage.openGroups) +} +inline void ConfigurationMessage::add_opengroups(const char* value, size_t size) { + _impl_.opengroups_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:SessionProtos.ConfigurationMessage.openGroups) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +ConfigurationMessage::opengroups() const { + // @@protoc_insertion_point(field_list:SessionProtos.ConfigurationMessage.openGroups) + return _impl_.opengroups_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +ConfigurationMessage::mutable_opengroups() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ConfigurationMessage.openGroups) + return &_impl_.opengroups_; +} + +// optional string displayName = 3; +inline bool ConfigurationMessage::_internal_has_displayname() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool ConfigurationMessage::has_displayname() const { + return _internal_has_displayname(); +} +inline void ConfigurationMessage::clear_displayname() { + _impl_.displayname_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& ConfigurationMessage::displayname() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.displayName) + return _internal_displayname(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage::set_displayname(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.displayname_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.displayName) +} +inline std::string* ConfigurationMessage::mutable_displayname() { + std::string* _s = _internal_mutable_displayname(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.displayName) + return _s; +} +inline const std::string& ConfigurationMessage::_internal_displayname() const { + return _impl_.displayname_.Get(); +} +inline void ConfigurationMessage::_internal_set_displayname(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.displayname_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::_internal_mutable_displayname() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.displayname_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::release_displayname() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.displayName) + if (!_internal_has_displayname()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.displayname_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.displayname_.IsDefault()) { + _impl_.displayname_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage::set_allocated_displayname(std::string* displayname) { + if (displayname != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.displayname_.SetAllocated(displayname, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.displayname_.IsDefault()) { + _impl_.displayname_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.displayName) +} + +// optional string profilePicture = 4; +inline bool ConfigurationMessage::_internal_has_profilepicture() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool ConfigurationMessage::has_profilepicture() const { + return _internal_has_profilepicture(); +} +inline void ConfigurationMessage::clear_profilepicture() { + _impl_.profilepicture_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& ConfigurationMessage::profilepicture() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.profilePicture) + return _internal_profilepicture(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage::set_profilepicture(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilepicture_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.profilePicture) +} +inline std::string* ConfigurationMessage::mutable_profilepicture() { + std::string* _s = _internal_mutable_profilepicture(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.profilePicture) + return _s; +} +inline const std::string& ConfigurationMessage::_internal_profilepicture() const { + return _impl_.profilepicture_.Get(); +} +inline void ConfigurationMessage::_internal_set_profilepicture(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.profilepicture_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::_internal_mutable_profilepicture() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.profilepicture_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::release_profilepicture() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.profilePicture) + if (!_internal_has_profilepicture()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.profilepicture_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage::set_allocated_profilepicture(std::string* profilepicture) { + if (profilepicture != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.profilepicture_.SetAllocated(profilepicture, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilepicture_.IsDefault()) { + _impl_.profilepicture_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.profilePicture) +} + +// optional bytes profileKey = 5; +inline bool ConfigurationMessage::_internal_has_profilekey() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool ConfigurationMessage::has_profilekey() const { + return _internal_has_profilekey(); +} +inline void ConfigurationMessage::clear_profilekey() { + _impl_.profilekey_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const std::string& ConfigurationMessage::profilekey() const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.profileKey) + return _internal_profilekey(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void ConfigurationMessage::set_profilekey(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.profilekey_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ConfigurationMessage.profileKey) +} +inline std::string* ConfigurationMessage::mutable_profilekey() { + std::string* _s = _internal_mutable_profilekey(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.profileKey) + return _s; +} +inline const std::string& ConfigurationMessage::_internal_profilekey() const { + return _impl_.profilekey_.Get(); +} +inline void ConfigurationMessage::_internal_set_profilekey(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.profilekey_.Set(value, GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::_internal_mutable_profilekey() { + _impl_._has_bits_[0] |= 0x00000004u; + return _impl_.profilekey_.Mutable(GetArenaForAllocation()); +} +inline std::string* ConfigurationMessage::release_profilekey() { + // @@protoc_insertion_point(field_release:SessionProtos.ConfigurationMessage.profileKey) + if (!_internal_has_profilekey()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000004u; + auto* p = _impl_.profilekey_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void ConfigurationMessage::set_allocated_profilekey(std::string* profilekey) { + if (profilekey != nullptr) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.profilekey_.SetAllocated(profilekey, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.profilekey_.IsDefault()) { + _impl_.profilekey_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ConfigurationMessage.profileKey) +} + +// repeated .SessionProtos.ConfigurationMessage.Contact contacts = 6; +inline int ConfigurationMessage::_internal_contacts_size() const { + return _impl_.contacts_.size(); +} +inline int ConfigurationMessage::contacts_size() const { + return _internal_contacts_size(); +} +inline void ConfigurationMessage::clear_contacts() { + _impl_.contacts_.Clear(); +} +inline ::SessionProtos::ConfigurationMessage_Contact* ConfigurationMessage::mutable_contacts(int index) { + // @@protoc_insertion_point(field_mutable:SessionProtos.ConfigurationMessage.contacts) + return _impl_.contacts_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_Contact >* +ConfigurationMessage::mutable_contacts() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ConfigurationMessage.contacts) + return &_impl_.contacts_; +} +inline const ::SessionProtos::ConfigurationMessage_Contact& ConfigurationMessage::_internal_contacts(int index) const { + return _impl_.contacts_.Get(index); +} +inline const ::SessionProtos::ConfigurationMessage_Contact& ConfigurationMessage::contacts(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ConfigurationMessage.contacts) + return _internal_contacts(index); +} +inline ::SessionProtos::ConfigurationMessage_Contact* ConfigurationMessage::_internal_add_contacts() { + return _impl_.contacts_.Add(); +} +inline ::SessionProtos::ConfigurationMessage_Contact* ConfigurationMessage::add_contacts() { + ::SessionProtos::ConfigurationMessage_Contact* _add = _internal_add_contacts(); + // @@protoc_insertion_point(field_add:SessionProtos.ConfigurationMessage.contacts) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::SessionProtos::ConfigurationMessage_Contact >& +ConfigurationMessage::contacts() const { + // @@protoc_insertion_point(field_list:SessionProtos.ConfigurationMessage.contacts) + return _impl_.contacts_; +} + +// ------------------------------------------------------------------- + +// ReceiptMessage + +// required .SessionProtos.ReceiptMessage.Type type = 1; +inline bool ReceiptMessage::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool ReceiptMessage::has_type() const { + return _internal_has_type(); +} +inline void ReceiptMessage::clear_type() { + _impl_.type_ = 0; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline ::SessionProtos::ReceiptMessage_Type ReceiptMessage::_internal_type() const { + return static_cast< ::SessionProtos::ReceiptMessage_Type >(_impl_.type_); +} +inline ::SessionProtos::ReceiptMessage_Type ReceiptMessage::type() const { + // @@protoc_insertion_point(field_get:SessionProtos.ReceiptMessage.type) + return _internal_type(); +} +inline void ReceiptMessage::_internal_set_type(::SessionProtos::ReceiptMessage_Type value) { + assert(::SessionProtos::ReceiptMessage_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.type_ = value; +} +inline void ReceiptMessage::set_type(::SessionProtos::ReceiptMessage_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:SessionProtos.ReceiptMessage.type) +} + +// repeated uint64 timestamp = 2; +inline int ReceiptMessage::_internal_timestamp_size() const { + return _impl_.timestamp_.size(); +} +inline int ReceiptMessage::timestamp_size() const { + return _internal_timestamp_size(); +} +inline void ReceiptMessage::clear_timestamp() { + _impl_.timestamp_.Clear(); +} +inline uint64_t ReceiptMessage::_internal_timestamp(int index) const { + return _impl_.timestamp_.Get(index); +} +inline uint64_t ReceiptMessage::timestamp(int index) const { + // @@protoc_insertion_point(field_get:SessionProtos.ReceiptMessage.timestamp) + return _internal_timestamp(index); +} +inline void ReceiptMessage::set_timestamp(int index, uint64_t value) { + _impl_.timestamp_.Set(index, value); + // @@protoc_insertion_point(field_set:SessionProtos.ReceiptMessage.timestamp) +} +inline void ReceiptMessage::_internal_add_timestamp(uint64_t value) { + _impl_.timestamp_.Add(value); +} +inline void ReceiptMessage::add_timestamp(uint64_t value) { + _internal_add_timestamp(value); + // @@protoc_insertion_point(field_add:SessionProtos.ReceiptMessage.timestamp) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >& +ReceiptMessage::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >& +ReceiptMessage::timestamp() const { + // @@protoc_insertion_point(field_list:SessionProtos.ReceiptMessage.timestamp) + return _internal_timestamp(); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >* +ReceiptMessage::_internal_mutable_timestamp() { + return &_impl_.timestamp_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedField< uint64_t >* +ReceiptMessage::mutable_timestamp() { + // @@protoc_insertion_point(field_mutable_list:SessionProtos.ReceiptMessage.timestamp) + return _internal_mutable_timestamp(); +} + +// ------------------------------------------------------------------- + +// AttachmentPointer + +// required fixed64 id = 1; +inline bool AttachmentPointer::_internal_has_id() const { + bool value = (_impl_._has_bits_[0] & 0x00000080u) != 0; + return value; +} +inline bool AttachmentPointer::has_id() const { + return _internal_has_id(); +} +inline void AttachmentPointer::clear_id() { + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000080u; +} +inline uint64_t AttachmentPointer::_internal_id() const { + return _impl_.id_; +} +inline uint64_t AttachmentPointer::id() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.id) + return _internal_id(); +} +inline void AttachmentPointer::_internal_set_id(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000080u; + _impl_.id_ = value; +} +inline void AttachmentPointer::set_id(uint64_t value) { + _internal_set_id(value); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.id) +} + +// optional string contentType = 2; +inline bool AttachmentPointer::_internal_has_contenttype() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool AttachmentPointer::has_contenttype() const { + return _internal_has_contenttype(); +} +inline void AttachmentPointer::clear_contenttype() { + _impl_.contenttype_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& AttachmentPointer::contenttype() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.contentType) + return _internal_contenttype(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_contenttype(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.contenttype_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.contentType) +} +inline std::string* AttachmentPointer::mutable_contenttype() { + std::string* _s = _internal_mutable_contenttype(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.contentType) + return _s; +} +inline const std::string& AttachmentPointer::_internal_contenttype() const { + return _impl_.contenttype_.Get(); +} +inline void AttachmentPointer::_internal_set_contenttype(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.contenttype_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_contenttype() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.contenttype_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_contenttype() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.contentType) + if (!_internal_has_contenttype()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.contenttype_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.contenttype_.IsDefault()) { + _impl_.contenttype_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_contenttype(std::string* contenttype) { + if (contenttype != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.contenttype_.SetAllocated(contenttype, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.contenttype_.IsDefault()) { + _impl_.contenttype_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.contentType) +} + +// optional bytes key = 3; +inline bool AttachmentPointer::_internal_has_key() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool AttachmentPointer::has_key() const { + return _internal_has_key(); +} +inline void AttachmentPointer::clear_key() { + _impl_.key_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& AttachmentPointer::key() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.key) + return _internal_key(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_key(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.key_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.key) +} +inline std::string* AttachmentPointer::mutable_key() { + std::string* _s = _internal_mutable_key(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.key) + return _s; +} +inline const std::string& AttachmentPointer::_internal_key() const { + return _impl_.key_.Get(); +} +inline void AttachmentPointer::_internal_set_key(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.key_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_key() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.key_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_key() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.key) + if (!_internal_has_key()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.key_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.key_.IsDefault()) { + _impl_.key_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_key(std::string* key) { + if (key != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.key_.SetAllocated(key, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.key_.IsDefault()) { + _impl_.key_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.key) +} + +// optional uint32 size = 4; +inline bool AttachmentPointer::_internal_has_size() const { + bool value = (_impl_._has_bits_[0] & 0x00000100u) != 0; + return value; +} +inline bool AttachmentPointer::has_size() const { + return _internal_has_size(); +} +inline void AttachmentPointer::clear_size() { + _impl_.size_ = 0u; + _impl_._has_bits_[0] &= ~0x00000100u; +} +inline uint32_t AttachmentPointer::_internal_size() const { + return _impl_.size_; +} +inline uint32_t AttachmentPointer::size() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.size) + return _internal_size(); +} +inline void AttachmentPointer::_internal_set_size(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000100u; + _impl_.size_ = value; +} +inline void AttachmentPointer::set_size(uint32_t value) { + _internal_set_size(value); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.size) +} + +// optional bytes thumbnail = 5; +inline bool AttachmentPointer::_internal_has_thumbnail() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool AttachmentPointer::has_thumbnail() const { + return _internal_has_thumbnail(); +} +inline void AttachmentPointer::clear_thumbnail() { + _impl_.thumbnail_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const std::string& AttachmentPointer::thumbnail() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.thumbnail) + return _internal_thumbnail(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_thumbnail(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.thumbnail_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.thumbnail) +} +inline std::string* AttachmentPointer::mutable_thumbnail() { + std::string* _s = _internal_mutable_thumbnail(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.thumbnail) + return _s; +} +inline const std::string& AttachmentPointer::_internal_thumbnail() const { + return _impl_.thumbnail_.Get(); +} +inline void AttachmentPointer::_internal_set_thumbnail(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.thumbnail_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_thumbnail() { + _impl_._has_bits_[0] |= 0x00000004u; + return _impl_.thumbnail_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_thumbnail() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.thumbnail) + if (!_internal_has_thumbnail()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000004u; + auto* p = _impl_.thumbnail_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.thumbnail_.IsDefault()) { + _impl_.thumbnail_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_thumbnail(std::string* thumbnail) { + if (thumbnail != nullptr) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.thumbnail_.SetAllocated(thumbnail, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.thumbnail_.IsDefault()) { + _impl_.thumbnail_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.thumbnail) +} + +// optional bytes digest = 6; +inline bool AttachmentPointer::_internal_has_digest() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool AttachmentPointer::has_digest() const { + return _internal_has_digest(); +} +inline void AttachmentPointer::clear_digest() { + _impl_.digest_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline const std::string& AttachmentPointer::digest() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.digest) + return _internal_digest(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_digest(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.digest_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.digest) +} +inline std::string* AttachmentPointer::mutable_digest() { + std::string* _s = _internal_mutable_digest(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.digest) + return _s; +} +inline const std::string& AttachmentPointer::_internal_digest() const { + return _impl_.digest_.Get(); +} +inline void AttachmentPointer::_internal_set_digest(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.digest_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_digest() { + _impl_._has_bits_[0] |= 0x00000008u; + return _impl_.digest_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_digest() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.digest) + if (!_internal_has_digest()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000008u; + auto* p = _impl_.digest_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.digest_.IsDefault()) { + _impl_.digest_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_digest(std::string* digest) { + if (digest != nullptr) { + _impl_._has_bits_[0] |= 0x00000008u; + } else { + _impl_._has_bits_[0] &= ~0x00000008u; + } + _impl_.digest_.SetAllocated(digest, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.digest_.IsDefault()) { + _impl_.digest_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.digest) +} + +// optional string fileName = 7; +inline bool AttachmentPointer::_internal_has_filename() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + return value; +} +inline bool AttachmentPointer::has_filename() const { + return _internal_has_filename(); +} +inline void AttachmentPointer::clear_filename() { + _impl_.filename_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline const std::string& AttachmentPointer::filename() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.fileName) + return _internal_filename(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_filename(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.filename_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.fileName) +} +inline std::string* AttachmentPointer::mutable_filename() { + std::string* _s = _internal_mutable_filename(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.fileName) + return _s; +} +inline const std::string& AttachmentPointer::_internal_filename() const { + return _impl_.filename_.Get(); +} +inline void AttachmentPointer::_internal_set_filename(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.filename_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_filename() { + _impl_._has_bits_[0] |= 0x00000010u; + return _impl_.filename_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_filename() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.fileName) + if (!_internal_has_filename()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000010u; + auto* p = _impl_.filename_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.filename_.IsDefault()) { + _impl_.filename_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_filename(std::string* filename) { + if (filename != nullptr) { + _impl_._has_bits_[0] |= 0x00000010u; + } else { + _impl_._has_bits_[0] &= ~0x00000010u; + } + _impl_.filename_.SetAllocated(filename, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.filename_.IsDefault()) { + _impl_.filename_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.fileName) +} + +// optional uint32 flags = 8; +inline bool AttachmentPointer::_internal_has_flags() const { + bool value = (_impl_._has_bits_[0] & 0x00000200u) != 0; + return value; +} +inline bool AttachmentPointer::has_flags() const { + return _internal_has_flags(); +} +inline void AttachmentPointer::clear_flags() { + _impl_.flags_ = 0u; + _impl_._has_bits_[0] &= ~0x00000200u; +} +inline uint32_t AttachmentPointer::_internal_flags() const { + return _impl_.flags_; +} +inline uint32_t AttachmentPointer::flags() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.flags) + return _internal_flags(); +} +inline void AttachmentPointer::_internal_set_flags(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000200u; + _impl_.flags_ = value; +} +inline void AttachmentPointer::set_flags(uint32_t value) { + _internal_set_flags(value); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.flags) +} + +// optional uint32 width = 9; +inline bool AttachmentPointer::_internal_has_width() const { + bool value = (_impl_._has_bits_[0] & 0x00000400u) != 0; + return value; +} +inline bool AttachmentPointer::has_width() const { + return _internal_has_width(); +} +inline void AttachmentPointer::clear_width() { + _impl_.width_ = 0u; + _impl_._has_bits_[0] &= ~0x00000400u; +} +inline uint32_t AttachmentPointer::_internal_width() const { + return _impl_.width_; +} +inline uint32_t AttachmentPointer::width() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.width) + return _internal_width(); +} +inline void AttachmentPointer::_internal_set_width(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000400u; + _impl_.width_ = value; +} +inline void AttachmentPointer::set_width(uint32_t value) { + _internal_set_width(value); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.width) +} + +// optional uint32 height = 10; +inline bool AttachmentPointer::_internal_has_height() const { + bool value = (_impl_._has_bits_[0] & 0x00000800u) != 0; + return value; +} +inline bool AttachmentPointer::has_height() const { + return _internal_has_height(); +} +inline void AttachmentPointer::clear_height() { + _impl_.height_ = 0u; + _impl_._has_bits_[0] &= ~0x00000800u; +} +inline uint32_t AttachmentPointer::_internal_height() const { + return _impl_.height_; +} +inline uint32_t AttachmentPointer::height() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.height) + return _internal_height(); +} +inline void AttachmentPointer::_internal_set_height(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000800u; + _impl_.height_ = value; +} +inline void AttachmentPointer::set_height(uint32_t value) { + _internal_set_height(value); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.height) +} + +// optional string caption = 11; +inline bool AttachmentPointer::_internal_has_caption() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + return value; +} +inline bool AttachmentPointer::has_caption() const { + return _internal_has_caption(); +} +inline void AttachmentPointer::clear_caption() { + _impl_.caption_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline const std::string& AttachmentPointer::caption() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.caption) + return _internal_caption(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_caption(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.caption_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.caption) +} +inline std::string* AttachmentPointer::mutable_caption() { + std::string* _s = _internal_mutable_caption(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.caption) + return _s; +} +inline const std::string& AttachmentPointer::_internal_caption() const { + return _impl_.caption_.Get(); +} +inline void AttachmentPointer::_internal_set_caption(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.caption_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_caption() { + _impl_._has_bits_[0] |= 0x00000020u; + return _impl_.caption_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_caption() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.caption) + if (!_internal_has_caption()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000020u; + auto* p = _impl_.caption_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.caption_.IsDefault()) { + _impl_.caption_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_caption(std::string* caption) { + if (caption != nullptr) { + _impl_._has_bits_[0] |= 0x00000020u; + } else { + _impl_._has_bits_[0] &= ~0x00000020u; + } + _impl_.caption_.SetAllocated(caption, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.caption_.IsDefault()) { + _impl_.caption_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.caption) +} + +// optional string url = 101; +inline bool AttachmentPointer::_internal_has_url() const { + bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; + return value; +} +inline bool AttachmentPointer::has_url() const { + return _internal_has_url(); +} +inline void AttachmentPointer::clear_url() { + _impl_.url_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000040u; +} +inline const std::string& AttachmentPointer::url() const { + // @@protoc_insertion_point(field_get:SessionProtos.AttachmentPointer.url) + return _internal_url(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void AttachmentPointer::set_url(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000040u; + _impl_.url_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.AttachmentPointer.url) +} +inline std::string* AttachmentPointer::mutable_url() { + std::string* _s = _internal_mutable_url(); + // @@protoc_insertion_point(field_mutable:SessionProtos.AttachmentPointer.url) + return _s; +} +inline const std::string& AttachmentPointer::_internal_url() const { + return _impl_.url_.Get(); +} +inline void AttachmentPointer::_internal_set_url(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000040u; + _impl_.url_.Set(value, GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::_internal_mutable_url() { + _impl_._has_bits_[0] |= 0x00000040u; + return _impl_.url_.Mutable(GetArenaForAllocation()); +} +inline std::string* AttachmentPointer::release_url() { + // @@protoc_insertion_point(field_release:SessionProtos.AttachmentPointer.url) + if (!_internal_has_url()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000040u; + auto* p = _impl_.url_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void AttachmentPointer::set_allocated_url(std::string* url) { + if (url != nullptr) { + _impl_._has_bits_[0] |= 0x00000040u; + } else { + _impl_._has_bits_[0] &= ~0x00000040u; + } + _impl_.url_.SetAllocated(url, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.url_.IsDefault()) { + _impl_.url_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.AttachmentPointer.url) +} + +// ------------------------------------------------------------------- + +// SharedConfigMessage + +// required .SessionProtos.SharedConfigMessage.Kind kind = 1; +inline bool SharedConfigMessage::_internal_has_kind() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool SharedConfigMessage::has_kind() const { + return _internal_has_kind(); +} +inline void SharedConfigMessage::clear_kind() { + _impl_.kind_ = 1; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline ::SessionProtos::SharedConfigMessage_Kind SharedConfigMessage::_internal_kind() const { + return static_cast< ::SessionProtos::SharedConfigMessage_Kind >(_impl_.kind_); +} +inline ::SessionProtos::SharedConfigMessage_Kind SharedConfigMessage::kind() const { + // @@protoc_insertion_point(field_get:SessionProtos.SharedConfigMessage.kind) + return _internal_kind(); +} +inline void SharedConfigMessage::_internal_set_kind(::SessionProtos::SharedConfigMessage_Kind value) { + assert(::SessionProtos::SharedConfigMessage_Kind_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.kind_ = value; +} +inline void SharedConfigMessage::set_kind(::SessionProtos::SharedConfigMessage_Kind value) { + _internal_set_kind(value); + // @@protoc_insertion_point(field_set:SessionProtos.SharedConfigMessage.kind) +} + +// required int64 seqno = 2; +inline bool SharedConfigMessage::_internal_has_seqno() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool SharedConfigMessage::has_seqno() const { + return _internal_has_seqno(); +} +inline void SharedConfigMessage::clear_seqno() { + _impl_.seqno_ = int64_t{0}; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline int64_t SharedConfigMessage::_internal_seqno() const { + return _impl_.seqno_; +} +inline int64_t SharedConfigMessage::seqno() const { + // @@protoc_insertion_point(field_get:SessionProtos.SharedConfigMessage.seqno) + return _internal_seqno(); +} +inline void SharedConfigMessage::_internal_set_seqno(int64_t value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.seqno_ = value; +} +inline void SharedConfigMessage::set_seqno(int64_t value) { + _internal_set_seqno(value); + // @@protoc_insertion_point(field_set:SessionProtos.SharedConfigMessage.seqno) +} + +// required bytes data = 3; +inline bool SharedConfigMessage::_internal_has_data() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool SharedConfigMessage::has_data() const { + return _internal_has_data(); +} +inline void SharedConfigMessage::clear_data() { + _impl_.data_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& SharedConfigMessage::data() const { + // @@protoc_insertion_point(field_get:SessionProtos.SharedConfigMessage.data) + return _internal_data(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void SharedConfigMessage::set_data(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.data_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.SharedConfigMessage.data) +} +inline std::string* SharedConfigMessage::mutable_data() { + std::string* _s = _internal_mutable_data(); + // @@protoc_insertion_point(field_mutable:SessionProtos.SharedConfigMessage.data) + return _s; +} +inline const std::string& SharedConfigMessage::_internal_data() const { + return _impl_.data_.Get(); +} +inline void SharedConfigMessage::_internal_set_data(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.data_.Set(value, GetArenaForAllocation()); +} +inline std::string* SharedConfigMessage::_internal_mutable_data() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.data_.Mutable(GetArenaForAllocation()); +} +inline std::string* SharedConfigMessage::release_data() { + // @@protoc_insertion_point(field_release:SessionProtos.SharedConfigMessage.data) + if (!_internal_has_data()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.data_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.data_.IsDefault()) { + _impl_.data_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void SharedConfigMessage::set_allocated_data(std::string* data) { + if (data != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.data_.SetAllocated(data, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.data_.IsDefault()) { + _impl_.data_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:SessionProtos.SharedConfigMessage.data) +} + +#ifdef __GNUC__ + #pragma GCC diagnostic pop +#endif // __GNUC__ +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + + +// @@protoc_insertion_point(namespace_scope) + +} // namespace SessionProtos + +PROTOBUF_NAMESPACE_OPEN + +template <> struct is_proto_enum< ::SessionProtos::Envelope_Type> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::TypingMessage_Action> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::CallMessage_Type> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::DataExtractionNotification_Type> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::DataMessage_Quote_QuotedAttachment_Flags> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::DataMessage_Reaction_Action> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::DataMessage_ClosedGroupControlMessage_Type> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::DataMessage_Flags> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::ReceiptMessage_Type> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::AttachmentPointer_Flags> : ::std::true_type {}; +template <> struct is_proto_enum< ::SessionProtos::SharedConfigMessage_Kind> : ::std::true_type {}; + +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) + +#include +#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_SessionProtos_2eproto diff --git a/proto/SessionProtos.proto b/proto/SessionProtos.proto new file mode 100644 index 00000000..d112322b --- /dev/null +++ b/proto/SessionProtos.proto @@ -0,0 +1,292 @@ +// iOS - since we use a modern proto-compiler, we must specify the legacy proto format. +syntax = "proto2"; + +// iOS - package name determines class prefix +package SessionProtos; + +option optimize_for = LITE_RUNTIME; + +message Envelope { + + enum Type { + SESSION_MESSAGE = 6; + CLOSED_GROUP_MESSAGE = 7; + } + + // @required + required Type type = 1; + optional string source = 2; + optional uint32 sourceDevice = 7; + // @required + required uint64 timestamp = 5; + optional bytes content = 8; + optional uint64 serverTimestamp = 10; +} + +message TypingMessage { + + enum Action { + STARTED = 0; + STOPPED = 1; + } + + // @required + required uint64 timestamp = 1; + // @required + required Action action = 2; +} + +message UnsendRequest { + // @required + required uint64 timestamp = 1; + // @required + required string author = 2; +} + +message MessageRequestResponse { + // @required + required bool isApproved = 1; // Whether the request was approved + optional bytes profileKey = 2; + optional LokiProfile profile = 3; +} + +message Content { + optional DataMessage dataMessage = 1; + optional CallMessage callMessage = 3; + optional ReceiptMessage receiptMessage = 5; + optional TypingMessage typingMessage = 6; + optional ConfigurationMessage configurationMessage = 7; + optional DataExtractionNotification dataExtractionNotification = 8; + optional UnsendRequest unsendRequest = 9; + optional MessageRequestResponse messageRequestResponse = 10; + optional SharedConfigMessage sharedConfigMessage = 11; +} + +message CallMessage { + + enum Type { + PRE_OFFER = 6; + OFFER = 1; + ANSWER = 2; + PROVISIONAL_ANSWER = 3; + ICE_CANDIDATES = 4; + END_CALL = 5; + } + + // Multiple ICE candidates may be batched together for performance + + // @required + required Type type = 1; + repeated string sdps = 2; + repeated uint32 sdpMLineIndexes = 3; + repeated string sdpMids = 4; + // @required + required string uuid = 5; +} + +message KeyPair { + // @required + required bytes publicKey = 1; + // @required + required bytes privateKey = 2; +} + +message DataExtractionNotification { + + enum Type { + SCREENSHOT = 1; + MEDIA_SAVED = 2; // timestamp + } + + // @required + required Type type = 1; + optional uint64 timestamp = 2; +} + +message LokiProfile { + optional string displayName = 1; + optional string profilePicture = 2; +} + +message DataMessage { + + enum Flags { + EXPIRATION_TIMER_UPDATE = 2; + } + + message Quote { + + message QuotedAttachment { + + enum Flags { + VOICE_MESSAGE = 1; + } + + optional string contentType = 1; + optional string fileName = 2; + optional AttachmentPointer thumbnail = 3; + optional uint32 flags = 4; + } + + // @required + required uint64 id = 1; + // @required + required string author = 2; + optional string text = 3; + repeated QuotedAttachment attachments = 4; + } + + message Preview { + // @required + required string url = 1; + optional string title = 2; + optional AttachmentPointer image = 3; + } + + message Reaction { + enum Action { + REACT = 0; + REMOVE = 1; + } + // @required + required uint64 id = 1; // Message timestamp + // @required + required string author = 2; + optional string emoji = 3; + // @required + required Action action = 4; + } + + message OpenGroupInvitation { + // @required + required string url = 1; + // @required + required string name = 3; + } + + message ClosedGroupControlMessage { + + enum Type { + NEW = 1; // publicKey, name, encryptionKeyPair, members, admins, expirationTimer + ENCRYPTION_KEY_PAIR = 3; // publicKey, wrappers + NAME_CHANGE = 4; // name + MEMBERS_ADDED = 5; // members + MEMBERS_REMOVED = 6; // members + MEMBER_LEFT = 7; + ENCRYPTION_KEY_PAIR_REQUEST = 8; + } + + message KeyPairWrapper { + // @required + required bytes publicKey = 1; // The public key of the user the key pair is meant for + // @required + required bytes encryptedKeyPair = 2; // The encrypted key pair + } + + // @required + required Type type = 1; + optional bytes publicKey = 2; + optional string name = 3; + optional KeyPair encryptionKeyPair = 4; + repeated bytes members = 5; + repeated bytes admins = 6; + repeated KeyPairWrapper wrappers = 7; + optional uint32 expirationTimer = 8; + } + + optional string body = 1; + repeated AttachmentPointer attachments = 2; + // optional GroupContext group = 3; // No longer used + optional uint32 flags = 4; + optional uint32 expireTimer = 5; + optional bytes profileKey = 6; + optional uint64 timestamp = 7; + optional Quote quote = 8; + repeated Preview preview = 10; + optional Reaction reaction = 11; + optional LokiProfile profile = 101; + optional OpenGroupInvitation openGroupInvitation = 102; + optional ClosedGroupControlMessage closedGroupControlMessage = 104; + optional string syncTarget = 105; + optional bool blocksCommunityMessageRequests = 106; +} + +message ConfigurationMessage { + + message ClosedGroup { + optional bytes publicKey = 1; + optional string name = 2; + optional KeyPair encryptionKeyPair = 3; + repeated bytes members = 4; + repeated bytes admins = 5; + optional uint32 expirationTimer = 6; + } + + message Contact { + // @required + required bytes publicKey = 1; + // @required + required string name = 2; + optional string profilePicture = 3; + optional bytes profileKey = 4; + optional bool isApproved = 5; // added for msg requests + optional bool isBlocked = 6; // added for msg requests + optional bool didApproveMe = 7; // added for msg requests + } + + repeated ClosedGroup closedGroups = 1; + repeated string openGroups = 2; + optional string displayName = 3; + optional string profilePicture = 4; + optional bytes profileKey = 5; + repeated Contact contacts = 6; +} + +message ReceiptMessage { + + enum Type { + DELIVERY = 0; + READ = 1; + } + + // @required + required Type type = 1; + repeated uint64 timestamp = 2; +} + +message AttachmentPointer { + + enum Flags { + VOICE_MESSAGE = 1; + } + + // @required + required fixed64 id = 1; + optional string contentType = 2; + optional bytes key = 3; + optional uint32 size = 4; + optional bytes thumbnail = 5; + optional bytes digest = 6; + optional string fileName = 7; + optional uint32 flags = 8; + optional uint32 width = 9; + optional uint32 height = 10; + optional string caption = 11; + optional string url = 101; +} + +message SharedConfigMessage { + enum Kind { + USER_PROFILE = 1; + CONTACTS = 2; + CONVO_INFO_VOLATILE = 3; + USER_GROUPS = 4; + } + + // @required + required Kind kind = 1; + // @required + required int64 seqno = 2; + // @required + required bytes data = 3; +} diff --git a/proto/WebSocketResources.pb.cc b/proto/WebSocketResources.pb.cc new file mode 100644 index 00000000..9f9fdb77 --- /dev/null +++ b/proto/WebSocketResources.pb.cc @@ -0,0 +1,1223 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: WebSocketResources.proto + +#include "WebSocketResources.pb.h" + +#include + +#include +#include +#include +#include +// @@protoc_insertion_point(includes) +#include + +PROTOBUF_PRAGMA_INIT_SEG + +namespace _pb = ::PROTOBUF_NAMESPACE_ID; +namespace _pbi = _pb::internal; + +namespace WebSocketProtos { +PROTOBUF_CONSTEXPR WebSocketRequestMessage::WebSocketRequestMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.headers_)*/{} + , /*decltype(_impl_.verb_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.path_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.body_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.requestid_)*/uint64_t{0u}} {} +struct WebSocketRequestMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR WebSocketRequestMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~WebSocketRequestMessageDefaultTypeInternal() {} + union { + WebSocketRequestMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 WebSocketRequestMessageDefaultTypeInternal _WebSocketRequestMessage_default_instance_; +PROTOBUF_CONSTEXPR WebSocketResponseMessage::WebSocketResponseMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.headers_)*/{} + , /*decltype(_impl_.message_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.body_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.requestid_)*/uint64_t{0u} + , /*decltype(_impl_.status_)*/0u} {} +struct WebSocketResponseMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR WebSocketResponseMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~WebSocketResponseMessageDefaultTypeInternal() {} + union { + WebSocketResponseMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 WebSocketResponseMessageDefaultTypeInternal _WebSocketResponseMessage_default_instance_; +PROTOBUF_CONSTEXPR WebSocketMessage::WebSocketMessage( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.request_)*/nullptr + , /*decltype(_impl_.response_)*/nullptr + , /*decltype(_impl_.type_)*/0} {} +struct WebSocketMessageDefaultTypeInternal { + PROTOBUF_CONSTEXPR WebSocketMessageDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~WebSocketMessageDefaultTypeInternal() {} + union { + WebSocketMessage _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 WebSocketMessageDefaultTypeInternal _WebSocketMessage_default_instance_; +} // namespace WebSocketProtos +namespace WebSocketProtos { +bool WebSocketMessage_Type_IsValid(int value) { + switch (value) { + case 0: + case 1: + case 2: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed WebSocketMessage_Type_strings[3] = {}; + +static const char WebSocketMessage_Type_names[] = + "REQUEST" + "RESPONSE" + "UNKNOWN"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry WebSocketMessage_Type_entries[] = { + { {WebSocketMessage_Type_names + 0, 7}, 1 }, + { {WebSocketMessage_Type_names + 7, 8}, 2 }, + { {WebSocketMessage_Type_names + 15, 7}, 0 }, +}; + +static const int WebSocketMessage_Type_entries_by_number[] = { + 2, // 0 -> UNKNOWN + 0, // 1 -> REQUEST + 1, // 2 -> RESPONSE +}; + +const std::string& WebSocketMessage_Type_Name( + WebSocketMessage_Type value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + WebSocketMessage_Type_entries, + WebSocketMessage_Type_entries_by_number, + 3, WebSocketMessage_Type_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + WebSocketMessage_Type_entries, + WebSocketMessage_Type_entries_by_number, + 3, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + WebSocketMessage_Type_strings[idx].get(); +} +bool WebSocketMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, WebSocketMessage_Type* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + WebSocketMessage_Type_entries, 3, name, &int_value); + if (success) { + *value = static_cast(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr WebSocketMessage_Type WebSocketMessage::UNKNOWN; +constexpr WebSocketMessage_Type WebSocketMessage::REQUEST; +constexpr WebSocketMessage_Type WebSocketMessage::RESPONSE; +constexpr WebSocketMessage_Type WebSocketMessage::Type_MIN; +constexpr WebSocketMessage_Type WebSocketMessage::Type_MAX; +constexpr int WebSocketMessage::Type_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) + +// =================================================================== + +class WebSocketRequestMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_verb(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_path(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_body(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_requestid(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } +}; + +WebSocketRequestMessage::WebSocketRequestMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:WebSocketProtos.WebSocketRequestMessage) +} +WebSocketRequestMessage::WebSocketRequestMessage(const WebSocketRequestMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + WebSocketRequestMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.headers_){from._impl_.headers_} + , decltype(_impl_.verb_){} + , decltype(_impl_.path_){} + , decltype(_impl_.body_){} + , decltype(_impl_.requestid_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.verb_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.verb_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_verb()) { + _this->_impl_.verb_.Set(from._internal_verb(), + _this->GetArenaForAllocation()); + } + _impl_.path_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.path_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_path()) { + _this->_impl_.path_.Set(from._internal_path(), + _this->GetArenaForAllocation()); + } + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_body()) { + _this->_impl_.body_.Set(from._internal_body(), + _this->GetArenaForAllocation()); + } + _this->_impl_.requestid_ = from._impl_.requestid_; + // @@protoc_insertion_point(copy_constructor:WebSocketProtos.WebSocketRequestMessage) +} + +inline void WebSocketRequestMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.headers_){arena} + , decltype(_impl_.verb_){} + , decltype(_impl_.path_){} + , decltype(_impl_.body_){} + , decltype(_impl_.requestid_){uint64_t{0u}} + }; + _impl_.verb_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.verb_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.path_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.path_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +WebSocketRequestMessage::~WebSocketRequestMessage() { + // @@protoc_insertion_point(destructor:WebSocketProtos.WebSocketRequestMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void WebSocketRequestMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.headers_.~RepeatedPtrField(); + _impl_.verb_.Destroy(); + _impl_.path_.Destroy(); + _impl_.body_.Destroy(); +} + +void WebSocketRequestMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void WebSocketRequestMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:WebSocketProtos.WebSocketRequestMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.headers_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _impl_.verb_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.path_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000004u) { + _impl_.body_.ClearNonDefaultToEmpty(); + } + } + _impl_.requestid_ = uint64_t{0u}; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* WebSocketRequestMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional string verb = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 10)) { + auto str = _internal_mutable_verb(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string path = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + auto str = _internal_mutable_path(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes body = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_body(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint64 requestId = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 32)) { + _Internal::set_has_requestid(&has_bits); + _impl_.requestid_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated string headers = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_headers(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr)); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* WebSocketRequestMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:WebSocketProtos.WebSocketRequestMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional string verb = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 1, this->_internal_verb(), target); + } + + // optional string path = 2; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_path(), target); + } + + // optional bytes body = 3; + if (cached_has_bits & 0x00000004u) { + target = stream->WriteBytesMaybeAliased( + 3, this->_internal_body(), target); + } + + // optional uint64 requestId = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(4, this->_internal_requestid(), target); + } + + // repeated string headers = 5; + for (int i = 0, n = this->_internal_headers_size(); i < n; i++) { + const auto& s = this->_internal_headers(i); + target = stream->WriteString(5, s, target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:WebSocketProtos.WebSocketRequestMessage) + return target; +} + +size_t WebSocketRequestMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:WebSocketProtos.WebSocketRequestMessage) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated string headers = 5; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.headers_.size()); + for (int i = 0, n = _impl_.headers_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + _impl_.headers_.Get(i)); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional string verb = 1; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_verb()); + } + + // optional string path = 2; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_path()); + } + + // optional bytes body = 3; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_body()); + } + + // optional uint64 requestId = 4; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_requestid()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void WebSocketRequestMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void WebSocketRequestMessage::MergeFrom(const WebSocketRequestMessage& from) { + WebSocketRequestMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:WebSocketProtos.WebSocketRequestMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.headers_.MergeFrom(from._impl_.headers_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_verb(from._internal_verb()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_path(from._internal_path()); + } + if (cached_has_bits & 0x00000004u) { + _this->_internal_set_body(from._internal_body()); + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.requestid_ = from._impl_.requestid_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void WebSocketRequestMessage::CopyFrom(const WebSocketRequestMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:WebSocketProtos.WebSocketRequestMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool WebSocketRequestMessage::IsInitialized() const { + return true; +} + +void WebSocketRequestMessage::InternalSwap(WebSocketRequestMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.headers_.InternalSwap(&other->_impl_.headers_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.verb_, lhs_arena, + &other->_impl_.verb_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.path_, lhs_arena, + &other->_impl_.path_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.body_, lhs_arena, + &other->_impl_.body_, rhs_arena + ); + swap(_impl_.requestid_, other->_impl_.requestid_); +} + +std::string WebSocketRequestMessage::GetTypeName() const { + return "WebSocketProtos.WebSocketRequestMessage"; +} + + +// =================================================================== + +class WebSocketResponseMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_requestid(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_status(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static void set_has_message(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_body(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } +}; + +WebSocketResponseMessage::WebSocketResponseMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:WebSocketProtos.WebSocketResponseMessage) +} +WebSocketResponseMessage::WebSocketResponseMessage(const WebSocketResponseMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + WebSocketResponseMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.headers_){from._impl_.headers_} + , decltype(_impl_.message_){} + , decltype(_impl_.body_){} + , decltype(_impl_.requestid_){} + , decltype(_impl_.status_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + _impl_.message_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.message_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_message()) { + _this->_impl_.message_.Set(from._internal_message(), + _this->GetArenaForAllocation()); + } + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_body()) { + _this->_impl_.body_.Set(from._internal_body(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.requestid_, &from._impl_.requestid_, + static_cast(reinterpret_cast(&_impl_.status_) - + reinterpret_cast(&_impl_.requestid_)) + sizeof(_impl_.status_)); + // @@protoc_insertion_point(copy_constructor:WebSocketProtos.WebSocketResponseMessage) +} + +inline void WebSocketResponseMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.headers_){arena} + , decltype(_impl_.message_){} + , decltype(_impl_.body_){} + , decltype(_impl_.requestid_){uint64_t{0u}} + , decltype(_impl_.status_){0u} + }; + _impl_.message_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.message_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.body_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +WebSocketResponseMessage::~WebSocketResponseMessage() { + // @@protoc_insertion_point(destructor:WebSocketProtos.WebSocketResponseMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void WebSocketResponseMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.headers_.~RepeatedPtrField(); + _impl_.message_.Destroy(); + _impl_.body_.Destroy(); +} + +void WebSocketResponseMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void WebSocketResponseMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:WebSocketProtos.WebSocketResponseMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.headers_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.message_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.body_.ClearNonDefaultToEmpty(); + } + } + if (cached_has_bits & 0x0000000cu) { + ::memset(&_impl_.requestid_, 0, static_cast( + reinterpret_cast(&_impl_.status_) - + reinterpret_cast(&_impl_.requestid_)) + sizeof(_impl_.status_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* WebSocketResponseMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional uint64 requestId = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + _Internal::set_has_requestid(&has_bits); + _impl_.requestid_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 status = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 16)) { + _Internal::set_has_status(&has_bits); + _impl_.status_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional string message = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + auto str = _internal_mutable_message(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bytes body = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 34)) { + auto str = _internal_mutable_body(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated string headers = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 42)) { + ptr -= 1; + do { + ptr += 1; + auto str = _internal_add_headers(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr)); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* WebSocketResponseMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:WebSocketProtos.WebSocketResponseMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional uint64 requestId = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_requestid(), target); + } + + // optional uint32 status = 2; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(2, this->_internal_status(), target); + } + + // optional string message = 3; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_message(), target); + } + + // optional bytes body = 4; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteBytesMaybeAliased( + 4, this->_internal_body(), target); + } + + // repeated string headers = 5; + for (int i = 0, n = this->_internal_headers_size(); i < n; i++) { + const auto& s = this->_internal_headers(i); + target = stream->WriteString(5, s, target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:WebSocketProtos.WebSocketResponseMessage) + return target; +} + +size_t WebSocketResponseMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:WebSocketProtos.WebSocketResponseMessage) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated string headers = 5; + total_size += 1 * + ::PROTOBUF_NAMESPACE_ID::internal::FromIntSize(_impl_.headers_.size()); + for (int i = 0, n = _impl_.headers_.size(); i < n; i++) { + total_size += ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + _impl_.headers_.Get(i)); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional string message = 3; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_message()); + } + + // optional bytes body = 4; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_body()); + } + + // optional uint64 requestId = 1; + if (cached_has_bits & 0x00000004u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_requestid()); + } + + // optional uint32 status = 2; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_status()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void WebSocketResponseMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void WebSocketResponseMessage::MergeFrom(const WebSocketResponseMessage& from) { + WebSocketResponseMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:WebSocketProtos.WebSocketResponseMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.headers_.MergeFrom(from._impl_.headers_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_message(from._internal_message()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_body(from._internal_body()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.requestid_ = from._impl_.requestid_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.status_ = from._impl_.status_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void WebSocketResponseMessage::CopyFrom(const WebSocketResponseMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:WebSocketProtos.WebSocketResponseMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool WebSocketResponseMessage::IsInitialized() const { + return true; +} + +void WebSocketResponseMessage::InternalSwap(WebSocketResponseMessage* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.headers_.InternalSwap(&other->_impl_.headers_); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.message_, lhs_arena, + &other->_impl_.message_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.body_, lhs_arena, + &other->_impl_.body_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(WebSocketResponseMessage, _impl_.status_) + + sizeof(WebSocketResponseMessage::_impl_.status_) + - PROTOBUF_FIELD_OFFSET(WebSocketResponseMessage, _impl_.requestid_)>( + reinterpret_cast(&_impl_.requestid_), + reinterpret_cast(&other->_impl_.requestid_)); +} + +std::string WebSocketResponseMessage::GetTypeName() const { + return "WebSocketProtos.WebSocketResponseMessage"; +} + + +// =================================================================== + +class WebSocketMessage::_Internal { + public: + using HasBits = decltype(std::declval()._impl_._has_bits_); + static void set_has_type(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static const ::WebSocketProtos::WebSocketRequestMessage& request(const WebSocketMessage* msg); + static void set_has_request(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static const ::WebSocketProtos::WebSocketResponseMessage& response(const WebSocketMessage* msg); + static void set_has_response(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } +}; + +const ::WebSocketProtos::WebSocketRequestMessage& +WebSocketMessage::_Internal::request(const WebSocketMessage* msg) { + return *msg->_impl_.request_; +} +const ::WebSocketProtos::WebSocketResponseMessage& +WebSocketMessage::_Internal::response(const WebSocketMessage* msg) { + return *msg->_impl_.response_; +} +WebSocketMessage::WebSocketMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:WebSocketProtos.WebSocketMessage) +} +WebSocketMessage::WebSocketMessage(const WebSocketMessage& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + WebSocketMessage* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.request_){nullptr} + , decltype(_impl_.response_){nullptr} + , decltype(_impl_.type_){}}; + + _internal_metadata_.MergeFrom(from._internal_metadata_); + if (from._internal_has_request()) { + _this->_impl_.request_ = new ::WebSocketProtos::WebSocketRequestMessage(*from._impl_.request_); + } + if (from._internal_has_response()) { + _this->_impl_.response_ = new ::WebSocketProtos::WebSocketResponseMessage(*from._impl_.response_); + } + _this->_impl_.type_ = from._impl_.type_; + // @@protoc_insertion_point(copy_constructor:WebSocketProtos.WebSocketMessage) +} + +inline void WebSocketMessage::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.request_){nullptr} + , decltype(_impl_.response_){nullptr} + , decltype(_impl_.type_){0} + }; +} + +WebSocketMessage::~WebSocketMessage() { + // @@protoc_insertion_point(destructor:WebSocketProtos.WebSocketMessage) + if (auto *arena = _internal_metadata_.DeleteReturnArena()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void WebSocketMessage::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (this != internal_default_instance()) delete _impl_.request_; + if (this != internal_default_instance()) delete _impl_.response_; +} + +void WebSocketMessage::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void WebSocketMessage::Clear() { +// @@protoc_insertion_point(message_clear_start:WebSocketProtos.WebSocketMessage) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + GOOGLE_DCHECK(_impl_.request_ != nullptr); + _impl_.request_->Clear(); + } + if (cached_has_bits & 0x00000002u) { + GOOGLE_DCHECK(_impl_.response_ != nullptr); + _impl_.response_->Clear(); + } + } + _impl_.type_ = 0; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear(); +} + +const char* WebSocketMessage::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional .WebSocketProtos.WebSocketMessage.Type type = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 8)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::WebSocketProtos::WebSocketMessage_Type_IsValid(val))) { + _internal_set_type(static_cast<::WebSocketProtos::WebSocketMessage_Type>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(1, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; + // optional .WebSocketProtos.WebSocketRequestMessage request = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { + ptr = ctx->ParseMessage(_internal_mutable_request(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .WebSocketProtos.WebSocketResponseMessage response = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_response(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* WebSocketMessage::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:WebSocketProtos.WebSocketMessage) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional .WebSocketProtos.WebSocketMessage.Type type = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 1, this->_internal_type(), target); + } + + // optional .WebSocketProtos.WebSocketRequestMessage request = 2; + if (cached_has_bits & 0x00000001u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(2, _Internal::request(this), + _Internal::request(this).GetCachedSize(), target, stream); + } + + // optional .WebSocketProtos.WebSocketResponseMessage response = 3; + if (cached_has_bits & 0x00000002u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::response(this), + _Internal::response(this).GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast(_internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:WebSocketProtos.WebSocketMessage) + return target; +} + +size_t WebSocketMessage::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:WebSocketProtos.WebSocketMessage) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + // optional .WebSocketProtos.WebSocketRequestMessage request = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.request_); + } + + // optional .WebSocketProtos.WebSocketResponseMessage response = 3; + if (cached_has_bits & 0x00000002u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.response_); + } + + // optional .WebSocketProtos.WebSocketMessage.Type type = 1; + if (cached_has_bits & 0x00000004u) { + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_type()); + } + + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void WebSocketMessage::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast( + &from)); +} + +void WebSocketMessage::MergeFrom(const WebSocketMessage& from) { + WebSocketMessage* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:WebSocketProtos.WebSocketMessage) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_mutable_request()->::WebSocketProtos::WebSocketRequestMessage::MergeFrom( + from._internal_request()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_mutable_response()->::WebSocketProtos::WebSocketResponseMessage::MergeFrom( + from._internal_response()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.type_ = from._impl_.type_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom(from._internal_metadata_); +} + +void WebSocketMessage::CopyFrom(const WebSocketMessage& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:WebSocketProtos.WebSocketMessage) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool WebSocketMessage::IsInitialized() const { + return true; +} + +void WebSocketMessage::InternalSwap(WebSocketMessage* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(WebSocketMessage, _impl_.type_) + + sizeof(WebSocketMessage::_impl_.type_) + - PROTOBUF_FIELD_OFFSET(WebSocketMessage, _impl_.request_)>( + reinterpret_cast(&_impl_.request_), + reinterpret_cast(&other->_impl_.request_)); +} + +std::string WebSocketMessage::GetTypeName() const { + return "WebSocketProtos.WebSocketMessage"; +} + + +// @@protoc_insertion_point(namespace_scope) +} // namespace WebSocketProtos +PROTOBUF_NAMESPACE_OPEN +template<> PROTOBUF_NOINLINE ::WebSocketProtos::WebSocketRequestMessage* +Arena::CreateMaybeMessage< ::WebSocketProtos::WebSocketRequestMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::WebSocketProtos::WebSocketRequestMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::WebSocketProtos::WebSocketResponseMessage* +Arena::CreateMaybeMessage< ::WebSocketProtos::WebSocketResponseMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::WebSocketProtos::WebSocketResponseMessage >(arena); +} +template<> PROTOBUF_NOINLINE ::WebSocketProtos::WebSocketMessage* +Arena::CreateMaybeMessage< ::WebSocketProtos::WebSocketMessage >(Arena* arena) { + return Arena::CreateMessageInternal< ::WebSocketProtos::WebSocketMessage >(arena); +} +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) +#include diff --git a/proto/WebSocketResources.pb.h b/proto/WebSocketResources.pb.h new file mode 100644 index 00000000..b4f41d86 --- /dev/null +++ b/proto/WebSocketResources.pb.h @@ -0,0 +1,1567 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: WebSocketResources.proto + +#ifndef GOOGLE_PROTOBUF_INCLUDED_WebSocketResources_2eproto +#define GOOGLE_PROTOBUF_INCLUDED_WebSocketResources_2eproto + +#include +#include + +#include +#if PROTOBUF_VERSION < 3021000 +#error This file was generated by a newer version of protoc which is +#error incompatible with your Protocol Buffer headers. Please update +#error your headers. +#endif +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION +#error This file was generated by an older version of protoc which is +#error incompatible with your Protocol Buffer headers. Please +#error regenerate this file with a newer version of protoc. +#endif + +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: export +#include // IWYU pragma: export +#include +// @@protoc_insertion_point(includes) +#include +#define PROTOBUF_INTERNAL_EXPORT_WebSocketResources_2eproto +PROTOBUF_NAMESPACE_OPEN +namespace internal { +class AnyMetadata; +} // namespace internal +PROTOBUF_NAMESPACE_CLOSE + +// Internal implementation detail -- do not use these members. +struct TableStruct_WebSocketResources_2eproto { + static const uint32_t offsets[]; +}; +namespace WebSocketProtos { +class WebSocketMessage; +struct WebSocketMessageDefaultTypeInternal; +extern WebSocketMessageDefaultTypeInternal _WebSocketMessage_default_instance_; +class WebSocketRequestMessage; +struct WebSocketRequestMessageDefaultTypeInternal; +extern WebSocketRequestMessageDefaultTypeInternal _WebSocketRequestMessage_default_instance_; +class WebSocketResponseMessage; +struct WebSocketResponseMessageDefaultTypeInternal; +extern WebSocketResponseMessageDefaultTypeInternal _WebSocketResponseMessage_default_instance_; +} // namespace WebSocketProtos +PROTOBUF_NAMESPACE_OPEN +template<> ::WebSocketProtos::WebSocketMessage* Arena::CreateMaybeMessage<::WebSocketProtos::WebSocketMessage>(Arena*); +template<> ::WebSocketProtos::WebSocketRequestMessage* Arena::CreateMaybeMessage<::WebSocketProtos::WebSocketRequestMessage>(Arena*); +template<> ::WebSocketProtos::WebSocketResponseMessage* Arena::CreateMaybeMessage<::WebSocketProtos::WebSocketResponseMessage>(Arena*); +PROTOBUF_NAMESPACE_CLOSE +namespace WebSocketProtos { + +enum WebSocketMessage_Type : int { + WebSocketMessage_Type_UNKNOWN = 0, + WebSocketMessage_Type_REQUEST = 1, + WebSocketMessage_Type_RESPONSE = 2 +}; +bool WebSocketMessage_Type_IsValid(int value); +constexpr WebSocketMessage_Type WebSocketMessage_Type_Type_MIN = WebSocketMessage_Type_UNKNOWN; +constexpr WebSocketMessage_Type WebSocketMessage_Type_Type_MAX = WebSocketMessage_Type_RESPONSE; +constexpr int WebSocketMessage_Type_Type_ARRAYSIZE = WebSocketMessage_Type_Type_MAX + 1; + +const std::string& WebSocketMessage_Type_Name(WebSocketMessage_Type value); +template +inline const std::string& WebSocketMessage_Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function WebSocketMessage_Type_Name."); + return WebSocketMessage_Type_Name(static_cast(enum_t_value)); +} +bool WebSocketMessage_Type_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, WebSocketMessage_Type* value); +// =================================================================== + +class WebSocketRequestMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:WebSocketProtos.WebSocketRequestMessage) */ { + public: + inline WebSocketRequestMessage() : WebSocketRequestMessage(nullptr) {} + ~WebSocketRequestMessage() override; + explicit PROTOBUF_CONSTEXPR WebSocketRequestMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + WebSocketRequestMessage(const WebSocketRequestMessage& from); + WebSocketRequestMessage(WebSocketRequestMessage&& from) noexcept + : WebSocketRequestMessage() { + *this = ::std::move(from); + } + + inline WebSocketRequestMessage& operator=(const WebSocketRequestMessage& from) { + CopyFrom(from); + return *this; + } + inline WebSocketRequestMessage& operator=(WebSocketRequestMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const WebSocketRequestMessage& default_instance() { + return *internal_default_instance(); + } + static inline const WebSocketRequestMessage* internal_default_instance() { + return reinterpret_cast( + &_WebSocketRequestMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 0; + + friend void swap(WebSocketRequestMessage& a, WebSocketRequestMessage& b) { + a.Swap(&b); + } + inline void Swap(WebSocketRequestMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(WebSocketRequestMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + WebSocketRequestMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const WebSocketRequestMessage& from); + void MergeFrom(const WebSocketRequestMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(WebSocketRequestMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "WebSocketProtos.WebSocketRequestMessage"; + } + protected: + explicit WebSocketRequestMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kHeadersFieldNumber = 5, + kVerbFieldNumber = 1, + kPathFieldNumber = 2, + kBodyFieldNumber = 3, + kRequestIdFieldNumber = 4, + }; + // repeated string headers = 5; + int headers_size() const; + private: + int _internal_headers_size() const; + public: + void clear_headers(); + const std::string& headers(int index) const; + std::string* mutable_headers(int index); + void set_headers(int index, const std::string& value); + void set_headers(int index, std::string&& value); + void set_headers(int index, const char* value); + void set_headers(int index, const char* value, size_t size); + std::string* add_headers(); + void add_headers(const std::string& value); + void add_headers(std::string&& value); + void add_headers(const char* value); + void add_headers(const char* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& headers() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_headers(); + private: + const std::string& _internal_headers(int index) const; + std::string* _internal_add_headers(); + public: + + // optional string verb = 1; + bool has_verb() const; + private: + bool _internal_has_verb() const; + public: + void clear_verb(); + const std::string& verb() const; + template + void set_verb(ArgT0&& arg0, ArgT... args); + std::string* mutable_verb(); + PROTOBUF_NODISCARD std::string* release_verb(); + void set_allocated_verb(std::string* verb); + private: + const std::string& _internal_verb() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_verb(const std::string& value); + std::string* _internal_mutable_verb(); + public: + + // optional string path = 2; + bool has_path() const; + private: + bool _internal_has_path() const; + public: + void clear_path(); + const std::string& path() const; + template + void set_path(ArgT0&& arg0, ArgT... args); + std::string* mutable_path(); + PROTOBUF_NODISCARD std::string* release_path(); + void set_allocated_path(std::string* path); + private: + const std::string& _internal_path() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_path(const std::string& value); + std::string* _internal_mutable_path(); + public: + + // optional bytes body = 3; + bool has_body() const; + private: + bool _internal_has_body() const; + public: + void clear_body(); + const std::string& body() const; + template + void set_body(ArgT0&& arg0, ArgT... args); + std::string* mutable_body(); + PROTOBUF_NODISCARD std::string* release_body(); + void set_allocated_body(std::string* body); + private: + const std::string& _internal_body() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_body(const std::string& value); + std::string* _internal_mutable_body(); + public: + + // optional uint64 requestId = 4; + bool has_requestid() const; + private: + bool _internal_has_requestid() const; + public: + void clear_requestid(); + uint64_t requestid() const; + void set_requestid(uint64_t value); + private: + uint64_t _internal_requestid() const; + void _internal_set_requestid(uint64_t value); + public: + + // @@protoc_insertion_point(class_scope:WebSocketProtos.WebSocketRequestMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField headers_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr verb_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr path_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr body_; + uint64_t requestid_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_WebSocketResources_2eproto; +}; +// ------------------------------------------------------------------- + +class WebSocketResponseMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:WebSocketProtos.WebSocketResponseMessage) */ { + public: + inline WebSocketResponseMessage() : WebSocketResponseMessage(nullptr) {} + ~WebSocketResponseMessage() override; + explicit PROTOBUF_CONSTEXPR WebSocketResponseMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + WebSocketResponseMessage(const WebSocketResponseMessage& from); + WebSocketResponseMessage(WebSocketResponseMessage&& from) noexcept + : WebSocketResponseMessage() { + *this = ::std::move(from); + } + + inline WebSocketResponseMessage& operator=(const WebSocketResponseMessage& from) { + CopyFrom(from); + return *this; + } + inline WebSocketResponseMessage& operator=(WebSocketResponseMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const WebSocketResponseMessage& default_instance() { + return *internal_default_instance(); + } + static inline const WebSocketResponseMessage* internal_default_instance() { + return reinterpret_cast( + &_WebSocketResponseMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 1; + + friend void swap(WebSocketResponseMessage& a, WebSocketResponseMessage& b) { + a.Swap(&b); + } + inline void Swap(WebSocketResponseMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(WebSocketResponseMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + WebSocketResponseMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const WebSocketResponseMessage& from); + void MergeFrom(const WebSocketResponseMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(WebSocketResponseMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "WebSocketProtos.WebSocketResponseMessage"; + } + protected: + explicit WebSocketResponseMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kHeadersFieldNumber = 5, + kMessageFieldNumber = 3, + kBodyFieldNumber = 4, + kRequestIdFieldNumber = 1, + kStatusFieldNumber = 2, + }; + // repeated string headers = 5; + int headers_size() const; + private: + int _internal_headers_size() const; + public: + void clear_headers(); + const std::string& headers(int index) const; + std::string* mutable_headers(int index); + void set_headers(int index, const std::string& value); + void set_headers(int index, std::string&& value); + void set_headers(int index, const char* value); + void set_headers(int index, const char* value, size_t size); + std::string* add_headers(); + void add_headers(const std::string& value); + void add_headers(std::string&& value); + void add_headers(const char* value); + void add_headers(const char* value, size_t size); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& headers() const; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* mutable_headers(); + private: + const std::string& _internal_headers(int index) const; + std::string* _internal_add_headers(); + public: + + // optional string message = 3; + bool has_message() const; + private: + bool _internal_has_message() const; + public: + void clear_message(); + const std::string& message() const; + template + void set_message(ArgT0&& arg0, ArgT... args); + std::string* mutable_message(); + PROTOBUF_NODISCARD std::string* release_message(); + void set_allocated_message(std::string* message); + private: + const std::string& _internal_message() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_message(const std::string& value); + std::string* _internal_mutable_message(); + public: + + // optional bytes body = 4; + bool has_body() const; + private: + bool _internal_has_body() const; + public: + void clear_body(); + const std::string& body() const; + template + void set_body(ArgT0&& arg0, ArgT... args); + std::string* mutable_body(); + PROTOBUF_NODISCARD std::string* release_body(); + void set_allocated_body(std::string* body); + private: + const std::string& _internal_body() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_body(const std::string& value); + std::string* _internal_mutable_body(); + public: + + // optional uint64 requestId = 1; + bool has_requestid() const; + private: + bool _internal_has_requestid() const; + public: + void clear_requestid(); + uint64_t requestid() const; + void set_requestid(uint64_t value); + private: + uint64_t _internal_requestid() const; + void _internal_set_requestid(uint64_t value); + public: + + // optional uint32 status = 2; + bool has_status() const; + private: + bool _internal_has_status() const; + public: + void clear_status(); + uint32_t status() const; + void set_status(uint32_t value); + private: + uint32_t _internal_status() const; + void _internal_set_status(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:WebSocketProtos.WebSocketResponseMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField headers_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr message_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr body_; + uint64_t requestid_; + uint32_t status_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_WebSocketResources_2eproto; +}; +// ------------------------------------------------------------------- + +class WebSocketMessage final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:WebSocketProtos.WebSocketMessage) */ { + public: + inline WebSocketMessage() : WebSocketMessage(nullptr) {} + ~WebSocketMessage() override; + explicit PROTOBUF_CONSTEXPR WebSocketMessage(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + WebSocketMessage(const WebSocketMessage& from); + WebSocketMessage(WebSocketMessage&& from) noexcept + : WebSocketMessage() { + *this = ::std::move(from); + } + + inline WebSocketMessage& operator=(const WebSocketMessage& from) { + CopyFrom(from); + return *this; + } + inline WebSocketMessage& operator=(WebSocketMessage&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields(); + } + + static const WebSocketMessage& default_instance() { + return *internal_default_instance(); + } + static inline const WebSocketMessage* internal_default_instance() { + return reinterpret_cast( + &_WebSocketMessage_default_instance_); + } + static constexpr int kIndexInFileMessages = + 2; + + friend void swap(WebSocketMessage& a, WebSocketMessage& b) { + a.Swap(&b); + } + inline void Swap(WebSocketMessage* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(WebSocketMessage* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + WebSocketMessage* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const WebSocketMessage& from); + void MergeFrom(const WebSocketMessage& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(WebSocketMessage* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "WebSocketProtos.WebSocketMessage"; + } + protected: + explicit WebSocketMessage(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef WebSocketMessage_Type Type; + static constexpr Type UNKNOWN = + WebSocketMessage_Type_UNKNOWN; + static constexpr Type REQUEST = + WebSocketMessage_Type_REQUEST; + static constexpr Type RESPONSE = + WebSocketMessage_Type_RESPONSE; + static inline bool Type_IsValid(int value) { + return WebSocketMessage_Type_IsValid(value); + } + static constexpr Type Type_MIN = + WebSocketMessage_Type_Type_MIN; + static constexpr Type Type_MAX = + WebSocketMessage_Type_Type_MAX; + static constexpr int Type_ARRAYSIZE = + WebSocketMessage_Type_Type_ARRAYSIZE; + template + static inline const std::string& Type_Name(T enum_t_value) { + static_assert(::std::is_same::value || + ::std::is_integral::value, + "Incorrect type passed to function Type_Name."); + return WebSocketMessage_Type_Name(enum_t_value); + } + static inline bool Type_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Type* value) { + return WebSocketMessage_Type_Parse(name, value); + } + + // accessors ------------------------------------------------------- + + enum : int { + kRequestFieldNumber = 2, + kResponseFieldNumber = 3, + kTypeFieldNumber = 1, + }; + // optional .WebSocketProtos.WebSocketRequestMessage request = 2; + bool has_request() const; + private: + bool _internal_has_request() const; + public: + void clear_request(); + const ::WebSocketProtos::WebSocketRequestMessage& request() const; + PROTOBUF_NODISCARD ::WebSocketProtos::WebSocketRequestMessage* release_request(); + ::WebSocketProtos::WebSocketRequestMessage* mutable_request(); + void set_allocated_request(::WebSocketProtos::WebSocketRequestMessage* request); + private: + const ::WebSocketProtos::WebSocketRequestMessage& _internal_request() const; + ::WebSocketProtos::WebSocketRequestMessage* _internal_mutable_request(); + public: + void unsafe_arena_set_allocated_request( + ::WebSocketProtos::WebSocketRequestMessage* request); + ::WebSocketProtos::WebSocketRequestMessage* unsafe_arena_release_request(); + + // optional .WebSocketProtos.WebSocketResponseMessage response = 3; + bool has_response() const; + private: + bool _internal_has_response() const; + public: + void clear_response(); + const ::WebSocketProtos::WebSocketResponseMessage& response() const; + PROTOBUF_NODISCARD ::WebSocketProtos::WebSocketResponseMessage* release_response(); + ::WebSocketProtos::WebSocketResponseMessage* mutable_response(); + void set_allocated_response(::WebSocketProtos::WebSocketResponseMessage* response); + private: + const ::WebSocketProtos::WebSocketResponseMessage& _internal_response() const; + ::WebSocketProtos::WebSocketResponseMessage* _internal_mutable_response(); + public: + void unsafe_arena_set_allocated_response( + ::WebSocketProtos::WebSocketResponseMessage* response); + ::WebSocketProtos::WebSocketResponseMessage* unsafe_arena_release_response(); + + // optional .WebSocketProtos.WebSocketMessage.Type type = 1; + bool has_type() const; + private: + bool _internal_has_type() const; + public: + void clear_type(); + ::WebSocketProtos::WebSocketMessage_Type type() const; + void set_type(::WebSocketProtos::WebSocketMessage_Type value); + private: + ::WebSocketProtos::WebSocketMessage_Type _internal_type() const; + void _internal_set_type(::WebSocketProtos::WebSocketMessage_Type value); + public: + + // @@protoc_insertion_point(class_scope:WebSocketProtos.WebSocketMessage) + private: + class _Internal; + + template friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::WebSocketProtos::WebSocketRequestMessage* request_; + ::WebSocketProtos::WebSocketResponseMessage* response_; + int type_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_WebSocketResources_2eproto; +}; +// =================================================================== + + +// =================================================================== + +#ifdef __GNUC__ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif // __GNUC__ +// WebSocketRequestMessage + +// optional string verb = 1; +inline bool WebSocketRequestMessage::_internal_has_verb() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool WebSocketRequestMessage::has_verb() const { + return _internal_has_verb(); +} +inline void WebSocketRequestMessage::clear_verb() { + _impl_.verb_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& WebSocketRequestMessage::verb() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketRequestMessage.verb) + return _internal_verb(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void WebSocketRequestMessage::set_verb(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.verb_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.verb) +} +inline std::string* WebSocketRequestMessage::mutable_verb() { + std::string* _s = _internal_mutable_verb(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketRequestMessage.verb) + return _s; +} +inline const std::string& WebSocketRequestMessage::_internal_verb() const { + return _impl_.verb_.Get(); +} +inline void WebSocketRequestMessage::_internal_set_verb(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.verb_.Set(value, GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::_internal_mutable_verb() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.verb_.Mutable(GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::release_verb() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketRequestMessage.verb) + if (!_internal_has_verb()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.verb_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.verb_.IsDefault()) { + _impl_.verb_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void WebSocketRequestMessage::set_allocated_verb(std::string* verb) { + if (verb != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.verb_.SetAllocated(verb, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.verb_.IsDefault()) { + _impl_.verb_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketRequestMessage.verb) +} + +// optional string path = 2; +inline bool WebSocketRequestMessage::_internal_has_path() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool WebSocketRequestMessage::has_path() const { + return _internal_has_path(); +} +inline void WebSocketRequestMessage::clear_path() { + _impl_.path_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& WebSocketRequestMessage::path() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketRequestMessage.path) + return _internal_path(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void WebSocketRequestMessage::set_path(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.path_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.path) +} +inline std::string* WebSocketRequestMessage::mutable_path() { + std::string* _s = _internal_mutable_path(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketRequestMessage.path) + return _s; +} +inline const std::string& WebSocketRequestMessage::_internal_path() const { + return _impl_.path_.Get(); +} +inline void WebSocketRequestMessage::_internal_set_path(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.path_.Set(value, GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::_internal_mutable_path() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.path_.Mutable(GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::release_path() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketRequestMessage.path) + if (!_internal_has_path()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.path_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.path_.IsDefault()) { + _impl_.path_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void WebSocketRequestMessage::set_allocated_path(std::string* path) { + if (path != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.path_.SetAllocated(path, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.path_.IsDefault()) { + _impl_.path_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketRequestMessage.path) +} + +// optional bytes body = 3; +inline bool WebSocketRequestMessage::_internal_has_body() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool WebSocketRequestMessage::has_body() const { + return _internal_has_body(); +} +inline void WebSocketRequestMessage::clear_body() { + _impl_.body_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline const std::string& WebSocketRequestMessage::body() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketRequestMessage.body) + return _internal_body(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void WebSocketRequestMessage::set_body(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.body_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.body) +} +inline std::string* WebSocketRequestMessage::mutable_body() { + std::string* _s = _internal_mutable_body(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketRequestMessage.body) + return _s; +} +inline const std::string& WebSocketRequestMessage::_internal_body() const { + return _impl_.body_.Get(); +} +inline void WebSocketRequestMessage::_internal_set_body(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.body_.Set(value, GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::_internal_mutable_body() { + _impl_._has_bits_[0] |= 0x00000004u; + return _impl_.body_.Mutable(GetArenaForAllocation()); +} +inline std::string* WebSocketRequestMessage::release_body() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketRequestMessage.body) + if (!_internal_has_body()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000004u; + auto* p = _impl_.body_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void WebSocketRequestMessage::set_allocated_body(std::string* body) { + if (body != nullptr) { + _impl_._has_bits_[0] |= 0x00000004u; + } else { + _impl_._has_bits_[0] &= ~0x00000004u; + } + _impl_.body_.SetAllocated(body, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketRequestMessage.body) +} + +// repeated string headers = 5; +inline int WebSocketRequestMessage::_internal_headers_size() const { + return _impl_.headers_.size(); +} +inline int WebSocketRequestMessage::headers_size() const { + return _internal_headers_size(); +} +inline void WebSocketRequestMessage::clear_headers() { + _impl_.headers_.Clear(); +} +inline std::string* WebSocketRequestMessage::add_headers() { + std::string* _s = _internal_add_headers(); + // @@protoc_insertion_point(field_add_mutable:WebSocketProtos.WebSocketRequestMessage.headers) + return _s; +} +inline const std::string& WebSocketRequestMessage::_internal_headers(int index) const { + return _impl_.headers_.Get(index); +} +inline const std::string& WebSocketRequestMessage::headers(int index) const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketRequestMessage.headers) + return _internal_headers(index); +} +inline std::string* WebSocketRequestMessage::mutable_headers(int index) { + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketRequestMessage.headers) + return _impl_.headers_.Mutable(index); +} +inline void WebSocketRequestMessage::set_headers(int index, const std::string& value) { + _impl_.headers_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::set_headers(int index, std::string&& value) { + _impl_.headers_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::set_headers(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.headers_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::set_headers(int index, const char* value, size_t size) { + _impl_.headers_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline std::string* WebSocketRequestMessage::_internal_add_headers() { + return _impl_.headers_.Add(); +} +inline void WebSocketRequestMessage::add_headers(const std::string& value) { + _impl_.headers_.Add()->assign(value); + // @@protoc_insertion_point(field_add:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::add_headers(std::string&& value) { + _impl_.headers_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::add_headers(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.headers_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline void WebSocketRequestMessage::add_headers(const char* value, size_t size) { + _impl_.headers_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:WebSocketProtos.WebSocketRequestMessage.headers) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +WebSocketRequestMessage::headers() const { + // @@protoc_insertion_point(field_list:WebSocketProtos.WebSocketRequestMessage.headers) + return _impl_.headers_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +WebSocketRequestMessage::mutable_headers() { + // @@protoc_insertion_point(field_mutable_list:WebSocketProtos.WebSocketRequestMessage.headers) + return &_impl_.headers_; +} + +// optional uint64 requestId = 4; +inline bool WebSocketRequestMessage::_internal_has_requestid() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool WebSocketRequestMessage::has_requestid() const { + return _internal_has_requestid(); +} +inline void WebSocketRequestMessage::clear_requestid() { + _impl_.requestid_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint64_t WebSocketRequestMessage::_internal_requestid() const { + return _impl_.requestid_; +} +inline uint64_t WebSocketRequestMessage::requestid() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketRequestMessage.requestId) + return _internal_requestid(); +} +inline void WebSocketRequestMessage::_internal_set_requestid(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.requestid_ = value; +} +inline void WebSocketRequestMessage::set_requestid(uint64_t value) { + _internal_set_requestid(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketRequestMessage.requestId) +} + +// ------------------------------------------------------------------- + +// WebSocketResponseMessage + +// optional uint64 requestId = 1; +inline bool WebSocketResponseMessage::_internal_has_requestid() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool WebSocketResponseMessage::has_requestid() const { + return _internal_has_requestid(); +} +inline void WebSocketResponseMessage::clear_requestid() { + _impl_.requestid_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint64_t WebSocketResponseMessage::_internal_requestid() const { + return _impl_.requestid_; +} +inline uint64_t WebSocketResponseMessage::requestid() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketResponseMessage.requestId) + return _internal_requestid(); +} +inline void WebSocketResponseMessage::_internal_set_requestid(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.requestid_ = value; +} +inline void WebSocketResponseMessage::set_requestid(uint64_t value) { + _internal_set_requestid(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.requestId) +} + +// optional uint32 status = 2; +inline bool WebSocketResponseMessage::_internal_has_status() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool WebSocketResponseMessage::has_status() const { + return _internal_has_status(); +} +inline void WebSocketResponseMessage::clear_status() { + _impl_.status_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t WebSocketResponseMessage::_internal_status() const { + return _impl_.status_; +} +inline uint32_t WebSocketResponseMessage::status() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketResponseMessage.status) + return _internal_status(); +} +inline void WebSocketResponseMessage::_internal_set_status(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.status_ = value; +} +inline void WebSocketResponseMessage::set_status(uint32_t value) { + _internal_set_status(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.status) +} + +// optional string message = 3; +inline bool WebSocketResponseMessage::_internal_has_message() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool WebSocketResponseMessage::has_message() const { + return _internal_has_message(); +} +inline void WebSocketResponseMessage::clear_message() { + _impl_.message_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& WebSocketResponseMessage::message() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketResponseMessage.message) + return _internal_message(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void WebSocketResponseMessage::set_message(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.message_.Set(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.message) +} +inline std::string* WebSocketResponseMessage::mutable_message() { + std::string* _s = _internal_mutable_message(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketResponseMessage.message) + return _s; +} +inline const std::string& WebSocketResponseMessage::_internal_message() const { + return _impl_.message_.Get(); +} +inline void WebSocketResponseMessage::_internal_set_message(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.message_.Set(value, GetArenaForAllocation()); +} +inline std::string* WebSocketResponseMessage::_internal_mutable_message() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.message_.Mutable(GetArenaForAllocation()); +} +inline std::string* WebSocketResponseMessage::release_message() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketResponseMessage.message) + if (!_internal_has_message()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.message_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.message_.IsDefault()) { + _impl_.message_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void WebSocketResponseMessage::set_allocated_message(std::string* message) { + if (message != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.message_.SetAllocated(message, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.message_.IsDefault()) { + _impl_.message_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketResponseMessage.message) +} + +// repeated string headers = 5; +inline int WebSocketResponseMessage::_internal_headers_size() const { + return _impl_.headers_.size(); +} +inline int WebSocketResponseMessage::headers_size() const { + return _internal_headers_size(); +} +inline void WebSocketResponseMessage::clear_headers() { + _impl_.headers_.Clear(); +} +inline std::string* WebSocketResponseMessage::add_headers() { + std::string* _s = _internal_add_headers(); + // @@protoc_insertion_point(field_add_mutable:WebSocketProtos.WebSocketResponseMessage.headers) + return _s; +} +inline const std::string& WebSocketResponseMessage::_internal_headers(int index) const { + return _impl_.headers_.Get(index); +} +inline const std::string& WebSocketResponseMessage::headers(int index) const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketResponseMessage.headers) + return _internal_headers(index); +} +inline std::string* WebSocketResponseMessage::mutable_headers(int index) { + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketResponseMessage.headers) + return _impl_.headers_.Mutable(index); +} +inline void WebSocketResponseMessage::set_headers(int index, const std::string& value) { + _impl_.headers_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::set_headers(int index, std::string&& value) { + _impl_.headers_.Mutable(index)->assign(std::move(value)); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::set_headers(int index, const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.headers_.Mutable(index)->assign(value); + // @@protoc_insertion_point(field_set_char:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::set_headers(int index, const char* value, size_t size) { + _impl_.headers_.Mutable(index)->assign( + reinterpret_cast(value), size); + // @@protoc_insertion_point(field_set_pointer:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline std::string* WebSocketResponseMessage::_internal_add_headers() { + return _impl_.headers_.Add(); +} +inline void WebSocketResponseMessage::add_headers(const std::string& value) { + _impl_.headers_.Add()->assign(value); + // @@protoc_insertion_point(field_add:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::add_headers(std::string&& value) { + _impl_.headers_.Add(std::move(value)); + // @@protoc_insertion_point(field_add:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::add_headers(const char* value) { + GOOGLE_DCHECK(value != nullptr); + _impl_.headers_.Add()->assign(value); + // @@protoc_insertion_point(field_add_char:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline void WebSocketResponseMessage::add_headers(const char* value, size_t size) { + _impl_.headers_.Add()->assign(reinterpret_cast(value), size); + // @@protoc_insertion_point(field_add_pointer:WebSocketProtos.WebSocketResponseMessage.headers) +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField& +WebSocketResponseMessage::headers() const { + // @@protoc_insertion_point(field_list:WebSocketProtos.WebSocketResponseMessage.headers) + return _impl_.headers_; +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField* +WebSocketResponseMessage::mutable_headers() { + // @@protoc_insertion_point(field_mutable_list:WebSocketProtos.WebSocketResponseMessage.headers) + return &_impl_.headers_; +} + +// optional bytes body = 4; +inline bool WebSocketResponseMessage::_internal_has_body() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool WebSocketResponseMessage::has_body() const { + return _internal_has_body(); +} +inline void WebSocketResponseMessage::clear_body() { + _impl_.body_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& WebSocketResponseMessage::body() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketResponseMessage.body) + return _internal_body(); +} +template +inline PROTOBUF_ALWAYS_INLINE +void WebSocketResponseMessage::set_body(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.body_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketResponseMessage.body) +} +inline std::string* WebSocketResponseMessage::mutable_body() { + std::string* _s = _internal_mutable_body(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketResponseMessage.body) + return _s; +} +inline const std::string& WebSocketResponseMessage::_internal_body() const { + return _impl_.body_.Get(); +} +inline void WebSocketResponseMessage::_internal_set_body(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.body_.Set(value, GetArenaForAllocation()); +} +inline std::string* WebSocketResponseMessage::_internal_mutable_body() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.body_.Mutable(GetArenaForAllocation()); +} +inline std::string* WebSocketResponseMessage::release_body() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketResponseMessage.body) + if (!_internal_has_body()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.body_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void WebSocketResponseMessage::set_allocated_body(std::string* body) { + if (body != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.body_.SetAllocated(body, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.body_.IsDefault()) { + _impl_.body_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketResponseMessage.body) +} + +// ------------------------------------------------------------------- + +// WebSocketMessage + +// optional .WebSocketProtos.WebSocketMessage.Type type = 1; +inline bool WebSocketMessage::_internal_has_type() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool WebSocketMessage::has_type() const { + return _internal_has_type(); +} +inline void WebSocketMessage::clear_type() { + _impl_.type_ = 0; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline ::WebSocketProtos::WebSocketMessage_Type WebSocketMessage::_internal_type() const { + return static_cast< ::WebSocketProtos::WebSocketMessage_Type >(_impl_.type_); +} +inline ::WebSocketProtos::WebSocketMessage_Type WebSocketMessage::type() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketMessage.type) + return _internal_type(); +} +inline void WebSocketMessage::_internal_set_type(::WebSocketProtos::WebSocketMessage_Type value) { + assert(::WebSocketProtos::WebSocketMessage_Type_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.type_ = value; +} +inline void WebSocketMessage::set_type(::WebSocketProtos::WebSocketMessage_Type value) { + _internal_set_type(value); + // @@protoc_insertion_point(field_set:WebSocketProtos.WebSocketMessage.type) +} + +// optional .WebSocketProtos.WebSocketRequestMessage request = 2; +inline bool WebSocketMessage::_internal_has_request() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + PROTOBUF_ASSUME(!value || _impl_.request_ != nullptr); + return value; +} +inline bool WebSocketMessage::has_request() const { + return _internal_has_request(); +} +inline void WebSocketMessage::clear_request() { + if (_impl_.request_ != nullptr) _impl_.request_->Clear(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const ::WebSocketProtos::WebSocketRequestMessage& WebSocketMessage::_internal_request() const { + const ::WebSocketProtos::WebSocketRequestMessage* p = _impl_.request_; + return p != nullptr ? *p : reinterpret_cast( + ::WebSocketProtos::_WebSocketRequestMessage_default_instance_); +} +inline const ::WebSocketProtos::WebSocketRequestMessage& WebSocketMessage::request() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketMessage.request) + return _internal_request(); +} +inline void WebSocketMessage::unsafe_arena_set_allocated_request( + ::WebSocketProtos::WebSocketRequestMessage* request) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.request_); + } + _impl_.request_ = request; + if (request) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:WebSocketProtos.WebSocketMessage.request) +} +inline ::WebSocketProtos::WebSocketRequestMessage* WebSocketMessage::release_request() { + _impl_._has_bits_[0] &= ~0x00000001u; + ::WebSocketProtos::WebSocketRequestMessage* temp = _impl_.request_; + _impl_.request_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::WebSocketProtos::WebSocketRequestMessage* WebSocketMessage::unsafe_arena_release_request() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketMessage.request) + _impl_._has_bits_[0] &= ~0x00000001u; + ::WebSocketProtos::WebSocketRequestMessage* temp = _impl_.request_; + _impl_.request_ = nullptr; + return temp; +} +inline ::WebSocketProtos::WebSocketRequestMessage* WebSocketMessage::_internal_mutable_request() { + _impl_._has_bits_[0] |= 0x00000001u; + if (_impl_.request_ == nullptr) { + auto* p = CreateMaybeMessage<::WebSocketProtos::WebSocketRequestMessage>(GetArenaForAllocation()); + _impl_.request_ = p; + } + return _impl_.request_; +} +inline ::WebSocketProtos::WebSocketRequestMessage* WebSocketMessage::mutable_request() { + ::WebSocketProtos::WebSocketRequestMessage* _msg = _internal_mutable_request(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketMessage.request) + return _msg; +} +inline void WebSocketMessage::set_allocated_request(::WebSocketProtos::WebSocketRequestMessage* request) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.request_; + } + if (request) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(request); + if (message_arena != submessage_arena) { + request = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, request, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.request_ = request; + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketMessage.request) +} + +// optional .WebSocketProtos.WebSocketResponseMessage response = 3; +inline bool WebSocketMessage::_internal_has_response() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + PROTOBUF_ASSUME(!value || _impl_.response_ != nullptr); + return value; +} +inline bool WebSocketMessage::has_response() const { + return _internal_has_response(); +} +inline void WebSocketMessage::clear_response() { + if (_impl_.response_ != nullptr) _impl_.response_->Clear(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const ::WebSocketProtos::WebSocketResponseMessage& WebSocketMessage::_internal_response() const { + const ::WebSocketProtos::WebSocketResponseMessage* p = _impl_.response_; + return p != nullptr ? *p : reinterpret_cast( + ::WebSocketProtos::_WebSocketResponseMessage_default_instance_); +} +inline const ::WebSocketProtos::WebSocketResponseMessage& WebSocketMessage::response() const { + // @@protoc_insertion_point(field_get:WebSocketProtos.WebSocketMessage.response) + return _internal_response(); +} +inline void WebSocketMessage::unsafe_arena_set_allocated_response( + ::WebSocketProtos::WebSocketResponseMessage* response) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.response_); + } + _impl_.response_ = response; + if (response) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:WebSocketProtos.WebSocketMessage.response) +} +inline ::WebSocketProtos::WebSocketResponseMessage* WebSocketMessage::release_response() { + _impl_._has_bits_[0] &= ~0x00000002u; + ::WebSocketProtos::WebSocketResponseMessage* temp = _impl_.response_; + _impl_.response_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::WebSocketProtos::WebSocketResponseMessage* WebSocketMessage::unsafe_arena_release_response() { + // @@protoc_insertion_point(field_release:WebSocketProtos.WebSocketMessage.response) + _impl_._has_bits_[0] &= ~0x00000002u; + ::WebSocketProtos::WebSocketResponseMessage* temp = _impl_.response_; + _impl_.response_ = nullptr; + return temp; +} +inline ::WebSocketProtos::WebSocketResponseMessage* WebSocketMessage::_internal_mutable_response() { + _impl_._has_bits_[0] |= 0x00000002u; + if (_impl_.response_ == nullptr) { + auto* p = CreateMaybeMessage<::WebSocketProtos::WebSocketResponseMessage>(GetArenaForAllocation()); + _impl_.response_ = p; + } + return _impl_.response_; +} +inline ::WebSocketProtos::WebSocketResponseMessage* WebSocketMessage::mutable_response() { + ::WebSocketProtos::WebSocketResponseMessage* _msg = _internal_mutable_response(); + // @@protoc_insertion_point(field_mutable:WebSocketProtos.WebSocketMessage.response) + return _msg; +} +inline void WebSocketMessage::set_allocated_response(::WebSocketProtos::WebSocketResponseMessage* response) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.response_; + } + if (response) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(response); + if (message_arena != submessage_arena) { + response = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, response, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.response_ = response; + // @@protoc_insertion_point(field_set_allocated:WebSocketProtos.WebSocketMessage.response) +} + +#ifdef __GNUC__ + #pragma GCC diagnostic pop +#endif // __GNUC__ +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + + +// @@protoc_insertion_point(namespace_scope) + +} // namespace WebSocketProtos + +PROTOBUF_NAMESPACE_OPEN + +template <> struct is_proto_enum< ::WebSocketProtos::WebSocketMessage_Type> : ::std::true_type {}; + +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) + +#include +#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_WebSocketResources_2eproto diff --git a/proto/WebSocketResources.proto b/proto/WebSocketResources.proto new file mode 100644 index 00000000..9107fc9a --- /dev/null +++ b/proto/WebSocketResources.proto @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ + +// iOS - since we use a modern proto-compiler, we must specify +// the legacy proto format. +syntax = "proto2"; + +// iOS - package name determines class prefix +package WebSocketProtos; + +option optimize_for = LITE_RUNTIME; + +option java_package = "org.whispersystems.signalservice.internal.websocket"; +option java_outer_classname = "WebSocketProtos"; + +message WebSocketRequestMessage { + // @required + optional string verb = 1; + // @required + optional string path = 2; + optional bytes body = 3; + repeated string headers = 5; + // @required + optional uint64 requestId = 4; +} + +message WebSocketResponseMessage { + // @required + optional uint64 requestId = 1; + // @required + optional uint32 status = 2; + optional string message = 3; + repeated string headers = 5; + optional bytes body = 4; +} + +message WebSocketMessage { + enum Type { + UNKNOWN = 0; + REQUEST = 1; + RESPONSE = 2; + } + + // @required + optional Type type = 1; + optional WebSocketRequestMessage request = 2; + optional WebSocketResponseMessage response = 3; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ab8aa784..e5c75be6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,7 @@ if(WARNINGS_AS_ERRORS) endif() endif() + set(export_targets) macro(add_libsession_util_library name) add_library(${name} ${ARGN}) @@ -74,13 +75,30 @@ add_libsession_util_library(onionreq onionreq/parser.cpp ) +add_libsession_util_library(protos + protos.cpp + ../proto/SessionProtos.pb.cc + ../proto/WebSocketResources.pb.cc +) + + + +target_link_libraries(protos + PUBLIC + common + PRIVATE + protobuf::libprotobuf-lite +) +target_include_directories(protos PUBLIC ../proto) target_link_libraries(crypto PUBLIC common PRIVATE + protos libsodium::sodium-internal ) + target_link_libraries(config PUBLIC crypto diff --git a/src/config/base.cpp b/src/config/base.cpp index 43d435b9..538b29cb 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -16,6 +16,7 @@ #include "session/config/base.h" #include "session/config/encrypt.hpp" #include "session/export.h" +#include "session/protos.hpp" #include "session/util.hpp" using namespace std::literals; @@ -45,6 +46,13 @@ MutableConfigMessage& ConfigBase::dirty() { throw std::runtime_error{"Internal error: unexpected dirty but non-mutable ConfigMessage"}; } +template +std::unique_ptr make_config_message(bool from_dirty, Args&&... args) { + if (from_dirty) + return std::make_unique(std::forward(args)...); + return std::make_unique(std::forward(args)...); +} + int ConfigBase::merge(const std::vector>& configs) { std::vector> config_views; config_views.reserve(configs.size()); @@ -53,14 +61,27 @@ int ConfigBase::merge(const std::vector>& config return merge(config_views); } -template -std::unique_ptr make_config_message(bool from_dirty, Args&&... args) { - if (from_dirty) - return std::make_unique(std::forward(args)...); - return std::make_unique(std::forward(args)...); +int ConfigBase::merge(const std::vector>& configs) { + if (accepts_protobuf()) { + std::list keep_alive; + std::vector> parsed; + parsed.reserve(configs.size()); + + for (auto& [h, c] : configs) { + try { + parsed.emplace_back(h, keep_alive.emplace_back(protos::handle_incoming(c))); + } catch (...) { + parsed.emplace_back(h, c); + } + } + + return _merge(parsed); + } + + return _merge(configs); } -int ConfigBase::merge(const std::vector>& configs) { +int ConfigBase::_merge(const std::vector>& configs) { if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; @@ -245,8 +266,9 @@ std::tuple> ConfigBase::push() { if (_keys.empty()) throw std::logic_error{"Cannot push data without an encryption key!"}; - std::tuple> ret{ - _config->seqno(), _config->serialize(), {}}; + auto s = _config->seqno(); + + std::tuple> ret{s, _config->serialize(), {}}; auto& [seqno, msg, obs] = ret; if (auto lvl = compression_level()) @@ -255,6 +277,13 @@ std::tuple> ConfigBase::push() { pad_message(msg); // Prefix pad with nulls encrypt_inplace(msg, key(), encryption_domain()); + if (accepts_protobuf()) { + try { + msg = protos::handle_outgoing(msg, s, storage_namespace()); + } catch (...) { + // do nothing + } + } if (msg.size() > MAX_MESSAGE_SIZE) throw std::length_error{"Config data is too large"}; diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 6c2ca1ce..fa61e1ed 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -14,6 +14,7 @@ using namespace std::literals; using namespace session::config; +using session::ustring; using session::ustring_view; LIBSESSION_C_API const size_t CONTACT_MAX_NAME_LENGTH = contact_info::MAX_NAME_LENGTH; diff --git a/src/protos.cpp b/src/protos.cpp new file mode 100644 index 00000000..9135a92f --- /dev/null +++ b/src/protos.cpp @@ -0,0 +1,105 @@ +#include "session/protos.hpp" + +#include "SessionProtos.pb.h" +#include "WebSocketResources.pb.h" + +namespace session::protos { + +SessionProtos::SharedConfigMessage_Kind encode_namespace(session::config::Namespace t) { + switch (t) { + case session::config::Namespace::UserProfile: + return SessionProtos::SharedConfigMessage_Kind_USER_PROFILE; + case session::config::Namespace::Contacts: + return SessionProtos::SharedConfigMessage_Kind_CONTACTS; + case session::config::Namespace::ConvoInfoVolatile: + return SessionProtos::SharedConfigMessage_Kind_CONVO_INFO_VOLATILE; + case session::config::Namespace::UserGroups: + return SessionProtos::SharedConfigMessage_Kind_USER_GROUPS; + default: + throw std::invalid_argument{"Error: cannot encode invalid SharedConfigMessage type"}; + } +} + +ustring parse_request(const WebSocketProtos::WebSocketRequestMessage& msg) { + const auto& data = msg.body(); + auto envelope = SessionProtos::Envelope(); + + if (auto b = envelope.ParseFromString( + {reinterpret_cast(data.data()), data.size()})) { + const auto& content = envelope.content(); + auto config = SessionProtos::SharedConfigMessage(); + + if (auto c = config.ParseFromString(content)) { + const auto& kind = config.kind(); + const auto& s = config.data(); + + switch (kind) { + case (SessionProtos::SharedConfigMessage_Kind_USER_PROFILE): + case (SessionProtos::SharedConfigMessage_Kind_CONTACTS): + case (SessionProtos::SharedConfigMessage_Kind_CONVO_INFO_VOLATILE): + case (SessionProtos::SharedConfigMessage_Kind_USER_GROUPS): + return {reinterpret_cast(s.data()), s.size()}; + default: throw std::invalid_argument{"Error: received invalid SharedConfigMessage"}; + } + } + } + + throw std::invalid_argument{"Error: received invalid WebSocketRequestMessage"}; +} + +ustring handle_incoming(ustring_view data) { + auto req = WebSocketProtos::WebSocketMessage(); + + if (auto b = req.ParseFromString({reinterpret_cast(data.data()), data.size()})) { + const auto& msg_type = req.type(); + + switch (msg_type) { + case (WebSocketProtos::WebSocketMessage_Type_REQUEST): + return parse_request(req.request()); + case (WebSocketProtos::WebSocketMessage_Type_UNKNOWN): + case (WebSocketProtos::WebSocketMessage_Type_RESPONSE): + throw std::invalid_argument{"Error: received invalid WebSocketRequest"}; + } + } + + // if ParseFromString fails, we have a raw (not protobuf encoded) message + return {data.data(), data.size()}; +} + +ustring handle_incoming(ustring data) { + return handle_incoming(ustring_view{data.data(), data.size()}); +} + +ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t) { + if (static_cast(t) > 5) + throw std::invalid_argument{"Error: received invalid outgoing SharedConfigMessage type"}; + + auto config = SessionProtos::SharedConfigMessage(); + config.set_kind(encode_namespace(t)); + config.set_seqno(seqno); + *config.mutable_data() = std::string{reinterpret_cast(data.data()), data.size()}; + + auto envelope = SessionProtos::Envelope(); + *envelope.mutable_content() = config.SerializeAsString(); + envelope.set_timestamp(0); + envelope.set_type(SessionProtos::Envelope_Type::Envelope_Type_SESSION_MESSAGE); + + auto webreq = WebSocketProtos::WebSocketRequestMessage(); + webreq.set_verb(""); + webreq.set_path(""); + webreq.set_requestid(0); + *webreq.mutable_body() = envelope.SerializeAsString(); + + auto msg = WebSocketProtos::WebSocketMessage(); + msg.set_type(WebSocketProtos::WebSocketMessage_Type_REQUEST); + *msg.mutable_request() = webreq; + + std::string output = msg.SerializeAsString(); + return {reinterpret_cast(output.data()), output.size()}; +} + +ustring handle_outgoing(ustring data, int64_t seqno, config::Namespace t) { + return handle_outgoing(ustring_view{data.data(), data.size()}, seqno, t); +} + +} // namespace session::protos diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ce3f0b59..b80bc963 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,16 +15,25 @@ add_executable(testAll test_group_info.cpp test_group_members.cpp test_onionreq.cpp + test_proto.cpp test_xed25519.cpp ) target_link_libraries(testAll PRIVATE libsession::config - libsodium::sodium-internal libsession::onionreq + libsession::protos + libsodium::sodium-internal Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) + +if(STATIC_BUNDLE) + add_executable(static-bundle-test static_bundle.cpp) + target_include_directories(static-bundle-test PUBLIC ../include) + target_link_libraries(static-bundle-test PRIVATE "${PROJECT_BINARY_DIR}/libsession-util.a" oxenc::oxenc) + add_dependencies(static-bundle-test session-util) +endif() diff --git a/tests/static_bundle.cpp b/tests/static_bundle.cpp new file mode 100644 index 00000000..beb158ad --- /dev/null +++ b/tests/static_bundle.cpp @@ -0,0 +1,12 @@ +// This file isn't designed to do anything useful, but just to test that we can compile and link +// against the combined static bundle (when using cmake ... -DSTATIC_BUILD=ON) + +#include +#include + +int main() { + if (std::mt19937_64{}() == 123) { + auto& k = *reinterpret_cast(12345); + k.encrypt_message(session::ustring_view{}); + } +} diff --git a/tests/test_bugs.cpp b/tests/test_bugs.cpp index b4ccb35b..86ee6ab4 100644 --- a/tests/test_bugs.cpp +++ b/tests/test_bugs.cpp @@ -50,8 +50,9 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { REQUIRE(seqno3 == 2); CHECK(obs2 == std::vector{"fakehash1"s}); - c1.merge(std::vector>{ + auto r = c1.merge(std::vector>{ {{"fakehash2", data2}, {"fakehash3", data3}}}); + CHECK(r == 2); CHECK(c1.needs_dump()); CHECK(c1.needs_push()); // because we have the merge conflict to push CHECK(c1.is_dirty()); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index db6b9510..93f8248f 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -199,7 +199,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(seqno == seqno2 + 1); std::tie(seqno2, to_push2, obs2) = contacts2.push(); CHECK(seqno == seqno2); - CHECK(to_push == to_push2); + CHECK(printable(to_push) == printable(to_push2)); CHECK(as_set(obs) == make_set("fakehash3a"s, "fakehash3b")); CHECK(as_set(obs2) == make_set("fakehash3a"s, "fakehash3b")); @@ -414,7 +414,7 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { auto [seqno, to_push, obs] = contacts.push(); CHECK(seqno == 1); - CHECK(to_push.size() == 46'080); + CHECK(to_push.size() == 46'112); // TODO: return to 46'080 once we remove protobuf wrapping auto dump = contacts.dump(); // With tons of duplicate info the push should have been nicely compressible: CHECK(dump.size() > 1'320'000); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 79e2941a..47c1dfc8 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -56,14 +56,21 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // We don't need to push since we haven't changed anything, so this call is mainly just for // testing: config_push_data* to_push = config_push(conf); + constexpr auto PROTOBUF_OVERHEAD = 28; // To be removed once we no longer protobuf wrap this + constexpr auto PROTOBUF_DATA_OFFSET = 26; REQUIRE(to_push); CHECK(to_push->seqno == 0); - CHECK(to_push->config_len == 256); + CHECK(to_push->config_len == 256 + PROTOBUF_OVERHEAD); const char* enc_domain = "UserProfile"; REQUIRE(config_encryption_domain(conf) == std::string_view{enc_domain}); size_t to_push_decr_size; + + // Get the de-protobufed pointer and length: + ustring_view inner{ + to_push->config + PROTOBUF_DATA_OFFSET, to_push->config_len - PROTOBUF_OVERHEAD}; + unsigned char* to_push_decrypted = config_decrypt( - to_push->config, to_push->config_len, ed_sk.data(), enc_domain, &to_push_decr_size); + inner.data(), inner.size(), ed_sk.data(), enc_domain, &to_push_decr_size); REQUIRE(to_push_decrypted); CHECK(to_push_decr_size == 216); // 256 - 40 overhead CHECK(printable(to_push_decrypted, to_push_decr_size) == @@ -146,12 +153,12 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea" "49bf122762d7bc1d6d9c02f6d54f8384"_hexbytes; - CHECK(oxenc::to_hex(to_push->config, to_push->config + to_push->config_len) == - to_hex(exp_push1_encrypted)); + inner = {to_push->config + PROTOBUF_DATA_OFFSET, to_push->config_len - PROTOBUF_OVERHEAD}; + CHECK(oxenc::to_hex(inner) == to_hex(exp_push1_encrypted)); // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) to_push_decrypted = config_decrypt( - to_push->config, to_push->config_len, ed_sk.data(), enc_domain, &to_push_decr_size); + inner.data(), inner.size(), ed_sk.data(), enc_domain, &to_push_decr_size); CHECK(to_push_decr_size == 256 - 40); CHECK(printable(to_push_decrypted, to_push_decr_size) == printable(ustring(256 - 40 - exp_push1_decrypted.size(), '\0') + exp_push1_decrypted)); diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp new file mode 100644 index 00000000..72a072f2 --- /dev/null +++ b/tests/test_proto.cpp @@ -0,0 +1,69 @@ +#include +#include +#include + +#include "utils.hpp" + +using namespace session; + +const std::vector groups{ + config::Namespace::UserProfile, + config::Namespace::Contacts, + config::Namespace::ConvoInfoVolatile, + config::Namespace::UserGroups}; + +TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { + auto msg = "Hello from the other side"_bytes; + + SECTION("Wrap/unwrap message types") { + for (auto& n : groups) { + auto shared_config_msg = protos::handle_outgoing(msg, 1, n); + + CHECK(not shared_config_msg.empty()); + + auto shared_config_parsed = protos::handle_incoming(shared_config_msg); + // This will be false, as ::handle_incoming will return the parsed payload if it + // successfully parses a protobuf wrapped message + CHECK_FALSE(shared_config_msg == shared_config_parsed); + // This will return true, as the parsed message will match the payload + CHECK(shared_config_parsed == msg); + } + } + + SECTION("Message type payload comparison") { + auto user_profile_msg = protos::handle_outgoing(msg, 1, config::Namespace::UserProfile); + auto contacts_msg = protos::handle_outgoing(msg, 1, config::Namespace::Contacts); + + auto user_profile_parsed = protos::handle_incoming(user_profile_msg); + auto contacts_parsed = protos::handle_incoming(contacts_msg); + + // All of these will return true, as the parsed messages will be identical to the + // payload, and therefore identical to one another + CHECK(user_profile_parsed == contacts_parsed); + CHECK(user_profile_parsed == msg); + CHECK(contacts_parsed == msg); + } +} + +TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { + auto msg = "Hello from the other side"_bytes; + auto addendum = "jfeejj0ifdoesam"_bytes; + + const auto user_profile_msg = protos::handle_outgoing(msg, 1, config::Namespace::UserProfile); + const auto size = user_profile_msg.size(); + + // Testing three positions: front, inside the payload, and at the end + const std::vector positions{0, size - 4, size}; + + for (auto& p : positions) { + auto msg_copy = user_profile_msg; + msg_copy.insert(p, addendum); + + auto msg_parsed = protos::handle_incoming(msg_copy); + // This will be true, as ::handle_incoming will return the same input string if it + // fails to parse it as a protobuf wrapped message + CHECK(msg_copy == msg_parsed); + // This will be false, as the wrapped message will not match the payload + CHECK_FALSE(msg_parsed == msg); + } +} diff --git a/utils/format.sh b/utils/format.sh index 6c5ac07c..05a783f2 100755 --- a/utils/format.sh +++ b/utils/format.sh @@ -20,7 +20,7 @@ if [ $? -ne 0 ]; then fi cd "$(dirname $0)/../" -readarray -t sources < <(find include src tests | grep -E '\.([hc](pp)?)$' | grep -v '\#' | grep -v Catch2) +readarray -t sources < <(find include proto src tests | grep -E '\.([hc](pp)?)$' | grep -v '\#' | grep -v Catch2 | grep -v -E '\.pb\.(h|cc)$') if [ "$1" = "verify" ] ; then if [ $($binary --output-replacements-xml "${sources[@]}" | grep '' | wc -l) -ne 0 ] ; then exit 2 From a770c1b691f6ad4cf830ba90a910f17dade0633f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 2 Oct 2023 17:04:12 -0300 Subject: [PATCH 088/572] Small fixes/improvements - Removed an unneeded `ustring` argument overload which just calls the `ustring_view` alternative (which already is fine when given a ustring). - Don't eat exceptions from protobuf production. If this throws on *producing* a protobuf then it is indicative of some application bug (unlike input, where we legitimately might not have a protobuf at all). - Switch to protobuf's ParseFromArray to avoid string construction. - Small code simplifications --- include/session/protos.hpp | 4 ---- src/config/base.cpp | 10 +++------- src/protos.cpp | 17 ++++------------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/include/session/protos.hpp b/include/session/protos.hpp index 6d5f219f..317a68b9 100644 --- a/include/session/protos.hpp +++ b/include/session/protos.hpp @@ -7,10 +7,6 @@ namespace session::protos { ustring handle_incoming(ustring_view data); -ustring handle_incoming(ustring data); - ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t); -ustring handle_outgoing(ustring data, int64_t seqno, config::Namespace t); - } // namespace session::protos diff --git a/src/config/base.cpp b/src/config/base.cpp index 538b29cb..798889c1 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -277,13 +277,9 @@ std::tuple> ConfigBase::push() { pad_message(msg); // Prefix pad with nulls encrypt_inplace(msg, key(), encryption_domain()); - if (accepts_protobuf()) { - try { - msg = protos::handle_outgoing(msg, s, storage_namespace()); - } catch (...) { - // do nothing - } - } + if (accepts_protobuf()) + msg = protos::handle_outgoing(msg, s, storage_namespace()); + if (msg.size() > MAX_MESSAGE_SIZE) throw std::length_error{"Config data is too large"}; diff --git a/src/protos.cpp b/src/protos.cpp index 9135a92f..3dc324f5 100644 --- a/src/protos.cpp +++ b/src/protos.cpp @@ -24,8 +24,7 @@ ustring parse_request(const WebSocketProtos::WebSocketRequestMessage& msg) { const auto& data = msg.body(); auto envelope = SessionProtos::Envelope(); - if (auto b = envelope.ParseFromString( - {reinterpret_cast(data.data()), data.size()})) { + if (auto b = envelope.ParseFromArray(data.data(), data.size())) { const auto& content = envelope.content(); auto config = SessionProtos::SharedConfigMessage(); @@ -50,7 +49,7 @@ ustring parse_request(const WebSocketProtos::WebSocketRequestMessage& msg) { ustring handle_incoming(ustring_view data) { auto req = WebSocketProtos::WebSocketMessage(); - if (auto b = req.ParseFromString({reinterpret_cast(data.data()), data.size()})) { + if (auto b = req.ParseFromArray(data.data(), data.size())) { const auto& msg_type = req.type(); switch (msg_type) { @@ -63,11 +62,7 @@ ustring handle_incoming(ustring_view data) { } // if ParseFromString fails, we have a raw (not protobuf encoded) message - return {data.data(), data.size()}; -} - -ustring handle_incoming(ustring data) { - return handle_incoming(ustring_view{data.data(), data.size()}); + return ustring{data}; } ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t) { @@ -77,7 +72,7 @@ ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t) { auto config = SessionProtos::SharedConfigMessage(); config.set_kind(encode_namespace(t)); config.set_seqno(seqno); - *config.mutable_data() = std::string{reinterpret_cast(data.data()), data.size()}; + *config.mutable_data() = from_unsigned_sv(data); auto envelope = SessionProtos::Envelope(); *envelope.mutable_content() = config.SerializeAsString(); @@ -98,8 +93,4 @@ ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t) { return {reinterpret_cast(output.data()), output.size()}; } -ustring handle_outgoing(ustring data, int64_t seqno, config::Namespace t) { - return handle_outgoing(ustring_view{data.data(), data.size()}, seqno, t); -} - } // namespace session::protos From e1bc54b44dbce5a6ba5b733bcc4e1b5d785ab0ec Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 2 Oct 2023 23:54:47 -0300 Subject: [PATCH 089/572] Add session protocol encryption/decryption Part of protobuf handling apparently also requires session protocol decryption, so this adds that to libsession. --- include/session/session_encrypt.hpp | 83 ++++++++++++++++++++++ src/CMakeLists.txt | 1 + src/session_encrypt.cpp | 104 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_session_encrypt.cpp | 84 ++++++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 include/session/session_encrypt.hpp create mode 100644 src/session_encrypt.cpp create mode 100644 tests/test_session_encrypt.cpp diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp new file mode 100644 index 00000000..1f71ef38 --- /dev/null +++ b/include/session/session_encrypt.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "types.hpp" + +// Helper functions for the "Session Protocol" encryption mechanism. This is the encryption used +// for DMs sent from one Session user to another. +// +// Suppose Alice with Ed25519 keys `a`/`A` and derived x25519 keys `x`/`X`, wants to send a mesage +// `M` to Brandy with Ed25519 keys `b`/`B` and derived x25519 keys `y`/`Y` (note that the x25519 +// pubkeys in hex form are session ids, but without the `05` prefix). +// +// First she signs the message, her own *Ed25519* (not X) pubkey, and the recipients pubkey (X, not +// Ed): +// +// SIG = Ed25519-sign(M || A || Y) +// +// Next a data message is composed of `M || A || SIG`, then encrypted for Brandy using: +// +// CIPHERTEXT = crypto_box_seal(M || A || SIG) +// +// (see libsodium for details, but is effectively generating an ephemeral X25519 keypair, making a +// shared secret of that and the recipient key, then encrypting using XSalsa20-Poly1305 from the +// shared secret). +// +// On the decryption side, we do this in reverse and verify via the signature both the sender and +// intended (inner) recipient. First, Brandy opens the ciphertext and extract the message, sender +// Ed pubkey, and signature: +// +// M || A || SIG = crypto_box_seal_open(CIPHERTEXT) +// +// then constructs and verifies the expected signature DATA (recall Y = Brandy's X25519 pubkey): +// +// Ed25519-verify(M || A || Y) +// +// Assuming this passes, we now know that `A` sent the message, and can convert this to a X25519 +// pubkey to work out the Session ID: +// +// X = Ed25519-pubkey-to-curve25519(A) +// +// SENDER = '05' + hex(X) +// +// and thus Brandy now has decrypted, verified data sent by Alice. + +namespace session { + +/// API: crypto/encrypt_for_recipient +/// +/// Performs session protocol encryption, typically for a DM sent between Session users. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be +/// passed as a 32-byte seed, but the 64-byte value is preferrable (to avoid needing to +/// recompute the public key from the seed). +/// - `recipient_pubkey` -- the recipient X25519 pubkey, either as a 0x05-prefixed session ID +/// (33 bytes) or an unprefixed pubkey (32 bytes). +/// - `message` -- the message to encrypt for the recipient. +/// +/// Outputs: +/// - The encrypted ciphertext to send. +/// - Throw if encryption fails or (which typically means invalid keys provided) +ustring encrypt_for_recipient( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); + +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 +/// pubkey, and verifies that the sender Ed25519 signature on the message. +/// +/// Inputs: +/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - pair consisting of the decrypted message content, and the sender Ed25519 pubkey, *if* the +/// message decrypted and validated successfully. Throws on error. +/// +/// To get the sender's session ID, pass the returned pubkey through +/// crypto_sign_ed25519_pk_to_curve25519. +std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); + +} // namespace session diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e5c75be6..7365aea6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,6 +47,7 @@ endif() add_libsession_util_library(crypto blinding.cpp + session_encrypt.cpp xed25519.cpp ) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp new file mode 100644 index 00000000..c7819c38 --- /dev/null +++ b/src/session_encrypt.cpp @@ -0,0 +1,104 @@ +#include "session/session_encrypt.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace session { + +using uc32 = std::array; +using uc64 = std::array; + +ustring encrypt_for_recipient( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { + + uc64 ed_sk_from_seed; + if (ed25519_privkey.size() == 32) { + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + } + if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) + recipient_pubkey.remove_prefix(1); + else if (recipient_pubkey.size() != 32) + throw std::invalid_argument{ + "Invalid recipient_pubkey: expected 32 bytes (33 with 05 prefix)"}; + + ustring buf; + buf.reserve(message.size() + 96); // 32+32 now, but 32+64 when we reuse it for the sealed box + buf += message; + buf += ed25519_privkey.substr(32); + buf += recipient_pubkey; + + uc64 sig; + if (0 != crypto_sign_ed25519_detached( + sig.data(), nullptr, buf.data(), buf.size(), ed25519_privkey.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + // We have M||A||Y for the sig, but now we want M||A||SIG so drop Y then append SIG: + buf.resize(buf.size() - 32); + buf += ustring_view{sig.data(), sig.size()}; + + ustring result; + result.resize(buf.size() + crypto_box_SEALBYTES); + if (0 != crypto_box_seal(result.data(), buf.data(), buf.size(), recipient_pubkey.data())) + throw std::runtime_error{"Sealed box encryption failed"}; + + return result; +} + +std::pair decrypt_incoming( + ustring_view ed25519_privkey, ustring_view ciphertext) { + + uc64 ed_sk_from_seed; + if (ed25519_privkey.size() == 32) { + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + } + + if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) + throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; + const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; + const size_t msg_size = outer_size - 32 - 64; + + uc32 x_sec, x_pub; + crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); + crypto_scalarmult_base(x_pub.data(), x_sec.data()); + + std::pair result; + auto& [buf, sender_ed_pk] = result; + + buf.resize(outer_size); + if (0 != crypto_box_seal_open( + buf.data(), ciphertext.data(), ciphertext.size(), x_pub.data(), x_sec.data())) + throw std::runtime_error{"Decryption failed"}; + + uc64 sig; + sender_ed_pk = buf.substr(msg_size, 32); + std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); + buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify + buf += ustring_view{x_pub.data(), 32}; + + if (0 != crypto_sign_ed25519_verify_detached( + sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) + throw std::runtime_error{"Signature verification failed"}; + + // Everything is good, so just drop A and Y off the message + buf.resize(buf.size() - 32 - 32); + + return result; +} + +} // namespace session diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b80bc963..9615d83e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable(testAll test_group_members.cpp test_onionreq.cpp test_proto.cpp + test_session_encrypt.cpp test_xed25519.cpp ) diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp new file mode 100644 index 00000000..82cee478 --- /dev/null +++ b/tests/test_session_encrypt.cpp @@ -0,0 +1,84 @@ +#include + +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { + + using namespace session; + + 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(), seed.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); + ustring sid_raw; + oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); + REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + REQUIRE(sid_raw == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + std::array ed_pk2, curve_pk2; + std::array ed_sk2; + crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); + REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); + REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + ustring sid_raw2; + oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); + REQUIRE(sid_raw2 == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + + SECTION("full secret, prefixed sid") { + auto enc = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); + + auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[2] ^= 0x02; + CHECK_THROWS(decrypt_incoming(to_sv(ed_sk2), broken)); + } + SECTION("only seed, unprefixed sid") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_recipient( + to_sv(ed_sk), sid_raw2, to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); + + auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[14] ^= 0x80; + CHECK_THROWS(decrypt_incoming(to_sv(ed_sk2), broken)); + } +} From 3a56048535e53d1254caa47d7914a00257e1c8e1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 3 Oct 2023 20:39:41 -0300 Subject: [PATCH 090/572] protobuf, v2 Adds protobuf parsing as Session does it. --- include/session/protos.hpp | 37 +++++- src/config/base.cpp | 16 ++- src/protos.cpp | 209 +++++++++++++++++++++--------- tests/test_config_contacts.cpp | 7 +- tests/test_config_userprofile.cpp | 32 +---- tests/test_proto.cpp | 72 ++++++++-- tests/test_session_encrypt.cpp | 3 +- 7 files changed, 258 insertions(+), 118 deletions(-) diff --git a/include/session/protos.hpp b/include/session/protos.hpp index 317a68b9..6e6e630d 100644 --- a/include/session/protos.hpp +++ b/include/session/protos.hpp @@ -5,8 +5,41 @@ namespace session::protos { -ustring handle_incoming(ustring_view data); +/// API: config/protos::wrap_config +/// +/// Wraps a config message in endless layers of protobuf and unnecessary extra encryption and +/// then more protobuf as required by older clients for older config message types. +/// +/// Inputs: +/// - `ed25519_sk` a 32- or 64-byte, libsodium-style secret key value (if 32 then it is just the +/// seed). +/// - `data` the config data to wrap +/// - `seqno` the seqno value of the data +/// - `ns` the namespace of the config data +/// +/// Outputs: +/// Returns the wrapped config. Will throw on serious errors (e.g. `ed25519_sk` or `ns` are +/// invalid). +ustring wrap_config( + ustring_view ed25519_sk, ustring_view data, int64_t seqno, config::Namespace ns); -ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t); +/// API: config/protos::unwrap_config +/// +/// Unwraps a config message from endless layers of protobuf, extra encryption and then more +/// protobuf as required by older clients for older config message types. +/// +/// Inputs: +/// - `ed25519_sk` a 32- or 64-byte, libsodium-style secret key value (if 32 then it is just the +/// seed). +/// - `data` the incoming data that might be protobuf-wrapped +/// +/// Outputs: +/// +/// Returns the unwrapped, inner config value if this is a proper protobuf-wrapped message; throws +/// std::runtime_error if it is not (thus most likely indicating that this is a raw config value). +/// Throws a std::invalid_argument if the given ed25519_sk is invalid. (It is recommended that only +/// the std::runtime_error is caught for detecting non-wrapped input as the invalid secret key is +/// more serious). +ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namespace ns); } // namespace session::protos diff --git a/src/config/base.cpp b/src/config/base.cpp index 798889c1..150b4238 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -62,14 +62,18 @@ int ConfigBase::merge(const std::vector>& config } int ConfigBase::merge(const std::vector>& configs) { - if (accepts_protobuf()) { + if (accepts_protobuf() && !_keys.empty()) { std::list keep_alive; std::vector> parsed; parsed.reserve(configs.size()); for (auto& [h, c] : configs) { try { - parsed.emplace_back(h, keep_alive.emplace_back(protos::handle_incoming(c))); + auto unwrapped = protos::unwrap_config( + ustring_view{_keys.front().data(), _keys.front().size()}, + c, + storage_namespace()); + parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); } catch (...) { parsed.emplace_back(h, c); } @@ -277,8 +281,12 @@ std::tuple> ConfigBase::push() { pad_message(msg); // Prefix pad with nulls encrypt_inplace(msg, key(), encryption_domain()); - if (accepts_protobuf()) - msg = protos::handle_outgoing(msg, s, storage_namespace()); + if (accepts_protobuf() && !_keys.empty()) + msg = protos::wrap_config( + ustring_view{_keys.front().data(), _keys.front().size()}, + msg, + s, + storage_namespace()); if (msg.size() > MAX_MESSAGE_SIZE) throw std::length_error{"Config data is too large"}; diff --git a/src/protos.cpp b/src/protos.cpp index 3dc324f5..787b794b 100644 --- a/src/protos.cpp +++ b/src/protos.cpp @@ -1,96 +1,179 @@ #include "session/protos.hpp" +#include +#include + +#include +#include + #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" +#include "session/session_encrypt.hpp" namespace session::protos { -SessionProtos::SharedConfigMessage_Kind encode_namespace(session::config::Namespace t) { - switch (t) { - case session::config::Namespace::UserProfile: - return SessionProtos::SharedConfigMessage_Kind_USER_PROFILE; - case session::config::Namespace::Contacts: - return SessionProtos::SharedConfigMessage_Kind_CONTACTS; - case session::config::Namespace::ConvoInfoVolatile: - return SessionProtos::SharedConfigMessage_Kind_CONVO_INFO_VOLATILE; - case session::config::Namespace::UserGroups: - return SessionProtos::SharedConfigMessage_Kind_USER_GROUPS; - default: - throw std::invalid_argument{"Error: cannot encode invalid SharedConfigMessage type"}; - } -} - -ustring parse_request(const WebSocketProtos::WebSocketRequestMessage& msg) { - const auto& data = msg.body(); - auto envelope = SessionProtos::Envelope(); - - if (auto b = envelope.ParseFromArray(data.data(), data.size())) { - const auto& content = envelope.content(); - auto config = SessionProtos::SharedConfigMessage(); - - if (auto c = config.ParseFromString(content)) { - const auto& kind = config.kind(); - const auto& s = config.data(); - - switch (kind) { - case (SessionProtos::SharedConfigMessage_Kind_USER_PROFILE): - case (SessionProtos::SharedConfigMessage_Kind_CONTACTS): - case (SessionProtos::SharedConfigMessage_Kind_CONVO_INFO_VOLATILE): - case (SessionProtos::SharedConfigMessage_Kind_USER_GROUPS): - return {reinterpret_cast(s.data()), s.size()}; - default: throw std::invalid_argument{"Error: received invalid SharedConfigMessage"}; - } +namespace { + + SessionProtos::SharedConfigMessage_Kind encode_namespace(session::config::Namespace t) { + switch (t) { + case session::config::Namespace::UserProfile: + return SessionProtos::SharedConfigMessage_Kind_USER_PROFILE; + case session::config::Namespace::Contacts: + return SessionProtos::SharedConfigMessage_Kind_CONTACTS; + case session::config::Namespace::ConvoInfoVolatile: + return SessionProtos::SharedConfigMessage_Kind_CONVO_INFO_VOLATILE; + case session::config::Namespace::UserGroups: + return SessionProtos::SharedConfigMessage_Kind_USER_GROUPS; + default: + throw std::invalid_argument{ + "Error: cannot encode invalid SharedConfigMessage type"}; } } - throw std::invalid_argument{"Error: received invalid WebSocketRequestMessage"}; -} +} // namespace -ustring handle_incoming(ustring_view data) { - auto req = WebSocketProtos::WebSocketMessage(); +ustring wrap_config( + ustring_view ed25519_sk, ustring_view data, int64_t seqno, config::Namespace t) { + std::array tmp_sk; + if (ed25519_sk.size() == 32) { + std::array ignore_pk; + crypto_sign_ed25519_seed_keypair(ignore_pk.data(), tmp_sk.data(), ed25519_sk.data()); + ed25519_sk = {tmp_sk.data(), 64}; + } else if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "Error: ed25519_sk is not the expected 64-byte Ed25519 secret key"}; - if (auto b = req.ParseFromArray(data.data(), data.size())) { - const auto& msg_type = req.type(); + std::array my_xpk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(my_xpk.data(), ed25519_sk.data() + 32)) + throw std::invalid_argument{ + "Failed to convert Ed25519 pubkey to X25519; invalid secret key?"}; - switch (msg_type) { - case (WebSocketProtos::WebSocketMessage_Type_REQUEST): - return parse_request(req.request()); - case (WebSocketProtos::WebSocketMessage_Type_UNKNOWN): - case (WebSocketProtos::WebSocketMessage_Type_RESPONSE): - throw std::invalid_argument{"Error: received invalid WebSocketRequest"}; - } - } - - // if ParseFromString fails, we have a raw (not protobuf encoded) message - return ustring{data}; -} - -ustring handle_outgoing(ustring_view data, int64_t seqno, config::Namespace t) { if (static_cast(t) > 5) throw std::invalid_argument{"Error: received invalid outgoing SharedConfigMessage type"}; - auto config = SessionProtos::SharedConfigMessage(); - config.set_kind(encode_namespace(t)); - config.set_seqno(seqno); - *config.mutable_data() = from_unsigned_sv(data); - + // Wrap in a SharedConfigMessage inside a Content + SessionProtos::Content config{}; + auto& shconf = *config.mutable_sharedconfigmessage(); + shconf.set_kind(encode_namespace(t)); + shconf.set_seqno(seqno); + *shconf.mutable_data() = from_unsigned_sv(data); + + // Then we serialize that, pad it, and encrypt it. Copying this relevant comment from the + // Session codebase (the comment itself git blames to Signal): + // NOTE: This is dumb. + auto shared_conf = config.SerializeAsString(); + // Okay now let's talk about padding. Remember, though: + // NOTE: This is dumb. + // Okay so to be more specific, padding adds a 0x80 byte followed by any number (including 0) of + // 0x00 bytes. The 0x80 byte, however, is always required (so there is always at least one + // padding byte); for DMs, this gets pushed up to the next multiple of 160, hence the final + // padded length we want, mathematically, is ⌈(x+1)/160⌉ * 160. With integer division, + // ceil(a/b) is (a+b-1)/b, so for a=x+1 we get: (x+1+b-1)/b*b = (x+b)/b*b = (x/b + 1)*b. + // +#if 0 + const size_t unpadded_size = shared_conf.size(); + const size_t padded_size = (unpadded_size / 160 + 1) * 160; + assert(padded_size > shared_conf.size()); + shared_conf.resize(padded_size); + shared_conf[unpadded_size] = 0x80; +#else + // But this is all moot for a config message which is *already* padded to a multiple of 256, so + // just tack on the 0x80 and no 0x00s rather than making it bigger still. + shared_conf += '\x80'; +#endif + + // Now we encrypt using the session protocol encryption, but with sender == recipient == + // ourself. This is unnecessary because the inner content is already encrypted with a value + // derived from our private key, but old Session clients expect this. + // NOTE: This is dumb. + auto enc_shared_conf = encrypt_for_recipient( + ed25519_sk, {my_xpk.data(), my_xpk.size()}, to_unsigned_sv(shared_conf)); + + // This is the point in session client code where this value got base64-encoded, passed to + // another function, which then base64-decoded that value to put into the envelope. We're going + // to skip that step here: fingers crossed!!! + // enc_shared_conf = oxenc::from_base64(oxenc::to_base64(enc_shared_conf)); + // NOTE: This is dumb. + + // Now we just keep on trucking with more protobuf: auto envelope = SessionProtos::Envelope(); - *envelope.mutable_content() = config.SerializeAsString(); + *envelope.mutable_content() = from_unsigned_sv(enc_shared_conf); envelope.set_timestamp(0); envelope.set_type(SessionProtos::Envelope_Type::Envelope_Type_SESSION_MESSAGE); + // And more protobuf (even though this no one cares about anything other than the body in this + // one): + // NOTE: This is dumb. auto webreq = WebSocketProtos::WebSocketRequestMessage(); webreq.set_verb(""); webreq.set_path(""); webreq.set_requestid(0); *webreq.mutable_body() = envelope.SerializeAsString(); + // And then yet more protobuf (even though this no one cares about anything other than the body + // in this one, again): + // NOTE: This is dumb. auto msg = WebSocketProtos::WebSocketMessage(); msg.set_type(WebSocketProtos::WebSocketMessage_Type_REQUEST); *msg.mutable_request() = webreq; - std::string output = msg.SerializeAsString(); - return {reinterpret_cast(output.data()), output.size()}; + return ustring{to_unsigned_sv(msg.SerializeAsString())}; +} + +ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namespace ns) { + // Hurray, we get to undo everything from the above! + + std::array tmp_sk; + if (ed25519_sk.size() == 32) { + std::array ignore_pk; + crypto_sign_ed25519_seed_keypair(ignore_pk.data(), tmp_sk.data(), ed25519_sk.data()); + ed25519_sk = {tmp_sk.data(), 64}; + } else if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "Error: ed25519_sk is not the expected 64-byte Ed25519 secret key"}; + auto ed25519_pk = ed25519_sk.substr(32); + + WebSocketProtos::WebSocketMessage req{}; + + if (!req.ParseFromArray(data.data(), data.size())) + throw std::runtime_error{"Failed to parse WebSocketMessage"}; + + const auto& msg_type = req.type(); + if (req.type() != WebSocketProtos::WebSocketMessage_Type_REQUEST) + throw std::runtime_error{"Error: received invalid WebSocketRequest"}; + + SessionProtos::Envelope envelope{}; + if (!envelope.ParseFromString(req.request().body())) + throw std::runtime_error{"Failed to parse Envelope"}; + + auto [content, sender] = decrypt_incoming(ed25519_sk, to_unsigned_sv(envelope.content())); + if (sender != ed25519_pk) + throw std::runtime_error{"Incoming config data was not from us; ignoring"}; + + if (content.empty()) + throw std::runtime_error{"Incoming config data decrypted to empty string"}; + + if (!(content.back() == 0x00 || content.back() == 0x80)) + throw std::runtime_error{"Incoming config data doesn't have required padding"}; + + if (auto pos = content.find_last_not_of((unsigned char)0); + pos != std::string::npos && content[pos] == 0x80) + content.resize(pos); + else + throw std::runtime_error{"Incoming config data has invalid padding"}; + + SessionProtos::Content config{}; + if (!config.ParseFromArray(content.data(), content.size())) + throw std::runtime_error{"Failed to parse SharedConfig"}; + + if (!config.has_sharedconfigmessage()) + throw std::runtime_error{"Content is missing a SharedConfigMessage"}; + auto& shconf = config.sharedconfigmessage(); + if (shconf.kind() != encode_namespace(ns)) + throw std::runtime_error{"SharedConfig has wrong kind for config namespace"}; + + // if ParseFromString fails, we have a raw (not protobuf encoded) message + return ustring{to_unsigned_sv(shconf.data())}; } } // namespace session::protos diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 93f8248f..565e9785 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -199,7 +199,10 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(seqno == seqno2 + 1); std::tie(seqno2, to_push2, obs2) = contacts2.push(); CHECK(seqno == seqno2); - CHECK(printable(to_push) == printable(to_push2)); + // Disabled check for now: doesn't work with protobuf (because of the non-deterministic + // encryption in the middle of the protobuf wrapping). + // TODO: reenable once protobuf isn't always-on. + // CHECK(printable(to_push) == printable(to_push2)); CHECK(as_set(obs) == make_set("fakehash3a"s, "fakehash3b")); CHECK(as_set(obs2) == make_set("fakehash3a"s, "fakehash3b")); @@ -414,7 +417,7 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { auto [seqno, to_push, obs] = contacts.push(); CHECK(seqno == 1); - CHECK(to_push.size() == 46'112); // TODO: return to 46'080 once we remove protobuf wrapping + CHECK(to_push.size() == 46'080 + 181); // 181 == protobuf overhead auto dump = contacts.dump(); // With tons of duplicate info the push should have been nicely compressible: CHECK(dump.size() > 1'320'000); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 47c1dfc8..50c0c08d 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -56,32 +56,12 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // We don't need to push since we haven't changed anything, so this call is mainly just for // testing: config_push_data* to_push = config_push(conf); - constexpr auto PROTOBUF_OVERHEAD = 28; // To be removed once we no longer protobuf wrap this - constexpr auto PROTOBUF_DATA_OFFSET = 26; REQUIRE(to_push); CHECK(to_push->seqno == 0); - CHECK(to_push->config_len == 256 + PROTOBUF_OVERHEAD); + 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}); - size_t to_push_decr_size; - - // Get the de-protobufed pointer and length: - ustring_view inner{ - to_push->config + PROTOBUF_DATA_OFFSET, to_push->config_len - PROTOBUF_OVERHEAD}; - - unsigned char* to_push_decrypted = config_decrypt( - inner.data(), inner.size(), ed_sk.data(), enc_domain, &to_push_decr_size); - REQUIRE(to_push_decrypted); - CHECK(to_push_decr_size == 216); // 256 - 40 overhead - CHECK(printable(to_push_decrypted, to_push_decr_size) == - printable( - ustring(193, '\0') + // null prefix padding - "d1:#i0e1:&de1:config + PROTOBUF_DATA_OFFSET, to_push->config_len - PROTOBUF_OVERHEAD}; - CHECK(oxenc::to_hex(inner) == to_hex(exp_push1_encrypted)); - - // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) - to_push_decrypted = config_decrypt( - inner.data(), inner.size(), ed_sk.data(), enc_domain, &to_push_decr_size); - CHECK(to_push_decr_size == 256 - 40); - CHECK(printable(to_push_decrypted, to_push_decr_size) == - printable(ustring(256 - 40 - exp_push1_decrypted.size(), '\0') + exp_push1_decrypted)); - // Copy this out; we need to hold onto it to do the confirmation later on seqno_t seqno = to_push->seqno; diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp index 72a072f2..995460a3 100644 --- a/tests/test_proto.cpp +++ b/tests/test_proto.cpp @@ -1,5 +1,9 @@ +#include + #include #include +#include +#include #include #include "utils.hpp" @@ -12,17 +16,26 @@ const std::vector groups{ config::Namespace::ConvoInfoVolatile, config::Namespace::UserGroups}; +const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; +std::array ed_pk_raw; +std::array ed_sk_raw; +ustring_view load_seed() { + crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); + return {ed_sk_raw.data(), ed_sk_raw.size()}; +} +auto ed_sk = load_seed(); + TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { auto msg = "Hello from the other side"_bytes; SECTION("Wrap/unwrap message types") { for (auto& n : groups) { - auto shared_config_msg = protos::handle_outgoing(msg, 1, n); + auto shared_config_msg = protos::wrap_config(ed_sk, msg, 1, n); CHECK(not shared_config_msg.empty()); - auto shared_config_parsed = protos::handle_incoming(shared_config_msg); - // This will be false, as ::handle_incoming will return the parsed payload if it + auto shared_config_parsed = protos::unwrap_config(ed_sk, shared_config_msg, n); + // This will be false, as ::unwrap_config will return the parsed payload if it // successfully parses a protobuf wrapped message CHECK_FALSE(shared_config_msg == shared_config_parsed); // This will return true, as the parsed message will match the payload @@ -31,11 +44,13 @@ TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { } SECTION("Message type payload comparison") { - auto user_profile_msg = protos::handle_outgoing(msg, 1, config::Namespace::UserProfile); - auto contacts_msg = protos::handle_outgoing(msg, 1, config::Namespace::Contacts); + auto user_profile_msg = protos::wrap_config(ed_sk, msg, 1, config::Namespace::UserProfile); + auto contacts_msg = protos::wrap_config(ed_sk, msg, 1, config::Namespace::Contacts); - auto user_profile_parsed = protos::handle_incoming(user_profile_msg); - auto contacts_parsed = protos::handle_incoming(contacts_msg); + auto user_profile_parsed = + protos::unwrap_config(ed_sk, user_profile_msg, config::Namespace::UserProfile); + auto contacts_parsed = + protos::unwrap_config(ed_sk, contacts_msg, config::Namespace::Contacts); // All of these will return true, as the parsed messages will be identical to the // payload, and therefore identical to one another @@ -49,7 +64,8 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { auto msg = "Hello from the other side"_bytes; auto addendum = "jfeejj0ifdoesam"_bytes; - const auto user_profile_msg = protos::handle_outgoing(msg, 1, config::Namespace::UserProfile); + const auto user_profile_msg = + protos::wrap_config(ed_sk, msg, 1, config::Namespace::UserProfile); const auto size = user_profile_msg.size(); // Testing three positions: front, inside the payload, and at the end @@ -59,11 +75,39 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { auto msg_copy = user_profile_msg; msg_copy.insert(p, addendum); - auto msg_parsed = protos::handle_incoming(msg_copy); - // This will be true, as ::handle_incoming will return the same input string if it - // fails to parse it as a protobuf wrapped message - CHECK(msg_copy == msg_parsed); - // This will be false, as the wrapped message will not match the payload - CHECK_FALSE(msg_parsed == msg); + REQUIRE_THROWS(protos::unwrap_config(ed_sk, msg_copy, config::Namespace::UserProfile)); } } + +TEST_CASE("Protobuf old config loading test", "[config][proto][old]") { + + const auto seed = "f887566576de6c16d9ec251d55e24c1400000000000000000000000000000000"_hexbytes; + std::array ed_pk_raw; + std::array ed_sk_raw; + crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); + ustring_view ed_sk{ed_sk_raw.data(), ed_sk_raw.size()}; + + auto old_conf = + "080112c2060a03505554120f2f6170692f76312f6d6573736167651a9f060806120028e1c5a0beaf313801" + "428f065228bb32b820169e0acb266f02efa007276be0668013a278fc9bfc111a40136f63de4206943c0509" + "6155fa480cd0a7f5d27d6297166f5ed5c2a323ecdf7a754308dd385cdce81e7ed3a0a305577838105a0798" + "dd92540f4b8eaa74f8c5720e0a394ce005444322354d6dfe1cb527520145f3794718e42730e15c97f7e45f" + "b53f9f7d3918ee57e5c8462f80ae0d64792c261feb4b9ce06b18a10b3d8f7af8f791b1368bd4ae9bbe0036" + "dc77f547c001e26c9c986269281bc3e8ef38c42ad2a02a9be517fc85c0c8fa4732f79138910f85bba0f898" + "f278d8c2ed3e7d00cc5b4f1eb32ffc9572ec98fac529bec7ad8560dc06fc986516c00232e9618c372c0f57" + "c19283e0424ec91864aad7277e22c085443cc0bfd39c0a83f0a1a8f856850ede7a751bd6206cb6683e462a" + "033ad282e4947adbbe4973e823676ae0a72aa5f0f607f306fe82b91da9b7fe79d4fb4e8a45cb9ad5f20c15" + "1a84073cc62d7ac794fdd2fe57bf49f1089f8644ad9f73d154d14c63d5ca7a07d1b6ab6b5846b2f4785fbf" + "738de23c250a711f54c941fd6f5aac4417125bb2d0321cd9f1b97a31f310d4ea8149732276b8df9869fbc5" + "412c9b7772961fab800a356155549ef54cefb9407d7f10b4323824aa8ea13facc79003b84dae3e5ef0db27" + "5b056f4fbdf54f5f22e62291af8427fc17c3c1b3985f6ee149729d8a5b794b7e374f408eb8f36a76a89680" + "e3c6106a9d5a82f6f04f5d8b603a97140b6469daac0ef32f84cc4ffc05f43c084591b10834b1d16d65ce14" + "15dec77cb5851c338ccbb0d5ae2d2c1e5bc8ba0f59dfbc4575fd446c8486a1ac5370d5da8eb041f2ed560a" + "bc1a6ad6ce6e00369ec5fd5eb0a35411ed24b36ecbf80f1dc6c18452c4b4bfc59131e04400df8986cac95c" + "51bbd320ba901ff6110dad0c70442286cf6220a53c6f9693636a42d5523eeb1e5fb3453169581384fb8a8f" + "3914fb6c01900a4f872f55742b117ddd7bd40c4c5911bb214e28eb9450dbdd0d831a93054c63f9a04bf50c" + "db9aac0032c484062d7ba7bbe64e07bcd633eec8378d5d914732693c5e298f015ebde2ae45769ed319e267" + "f0528f5cc6da268343b6647b20bae6e9ee8d92cca702"_hexbytes; + + CHECK_NOTHROW(protos::unwrap_config(ed_sk, old_conf, config::Namespace::UserProfile)); +} diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 82cee478..5143623d 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -67,8 +67,7 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_recipient( - to_sv(ed_sk), sid_raw2, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); From ff5de02f8ee64131c8154503ba45123407542ae0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 11:15:19 -0300 Subject: [PATCH 091/572] Fix missing libsodium link for protobuf code --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7365aea6..457a28a6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,6 +88,7 @@ target_link_libraries(protos PUBLIC common PRIVATE + libsodium::sodium-internal protobuf::libprotobuf-lite ) target_include_directories(protos PUBLIC ../proto) From 14e3dc024856012d5ce132bcfa1682942d332f08 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 14:03:53 -0300 Subject: [PATCH 092/572] Add deterministic session-encryption; use it for config protobuf This adds a deterministic version of session protocol encryption that differs from crypto_box_seal by using a keyed hash of the sender seed, recipient pubkey, and message to generate the ephemeral key. This effectively neuters such an encrypted message to be a regular crypto_box encryption, which means the sender is later able to decrypt it; this is thus only really a special case for protobuf+encrypted config messages for backwards compatible config message support. --- include/session/session_encrypt.hpp | 45 ++++++++++++++ src/protos.cpp | 2 +- src/session_encrypt.cpp | 93 ++++++++++++++++++++++++++--- tests/test_config_userprofile.cpp | 16 +++++ tests/test_session_encrypt.cpp | 56 +++++++++++++++++ 5 files changed, 204 insertions(+), 8 deletions(-) diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 1f71ef38..b702d16a 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -61,6 +61,51 @@ namespace session { ustring encrypt_for_recipient( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +/// API: crypto/encrypt_for_recipient_deterministic +/// +/// Performs session protocol encryption, but using a deterministic version of crypto_box_seal. +/// +/// Warning: this determinism completely undermines the point of crypto_box_seal (compared to a +/// regular encrypted crypto_box): someone with the same sender Ed25519 keys and message could later +/// regenerate the same ephemeral key and nonce which would allow them to decrypt the sent message, +/// which is intentionally impossible with a crypto_box_seal. This function is thus only +/// recommended for backwards compatibility with decryption mechanisms using that scheme where this +/// specific property is not needed, such as self-directed config messages. +/// +/// Inputs: +/// Identical to `encrypt_for_recipient`. +/// +/// Outputs: +/// Identical to `encrypt_for_recipient`. +ustring encrypt_for_recipient_deterministic( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); + +/// API: crypto/sign_for_recipient +/// +/// Performs the signing steps for session protocol encryption. This is responsible for producing +/// a packed authored, signed message of: +/// +/// MESSAGE || SENDER_ED25519_PUBKEY || SIG +/// +/// where SIG is the signed value of: +/// +/// MESSAGE || SENDER_ED25519_PUBKEY || RECIPIENT_X25519_PUBKEY +/// +/// thus allowing both sender identification, recipient verification, and authentication. +/// +/// This function is mostly for internal use, but is exposed for debugging purposes: it is typically +/// not called directly but rather used by `encrypt_for_recipient` or +/// `encrypt_for_recipient_deterministic`, both of which call this function to construct the inner +/// signed message. +/// +/// Inputs: +/// - `ed25519_privkey` -- the seed (32 bytes) or secret key (64 bytes) of the sender +/// - `recipient_pubkey` -- the recipient X25519 pubkey, which may or may not be prefixed with the +/// 0x05 session id prefix (33 bytes if prefixed, 32 if not prefixed). +/// - `message` -- the message to embed and sign. +ustring sign_for_recipient( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); + /// API: crypto/decrypt_incoming /// /// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 diff --git a/src/protos.cpp b/src/protos.cpp index 787b794b..f3b03048 100644 --- a/src/protos.cpp +++ b/src/protos.cpp @@ -86,7 +86,7 @@ ustring wrap_config( // ourself. This is unnecessary because the inner content is already encrypted with a value // derived from our private key, but old Session clients expect this. // NOTE: This is dumb. - auto enc_shared_conf = encrypt_for_recipient( + auto enc_shared_conf = encrypt_for_recipient_deterministic( ed25519_sk, {my_xpk.data(), my_xpk.size()}, to_unsigned_sv(shared_conf)); // This is the point in session client code where this value got base64-encoded, passed to diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index c7819c38..1e8c0d8a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,6 +1,7 @@ #include "session/session_encrypt.hpp" #include +#include #include #include @@ -9,15 +10,23 @@ #include #include +#include "session/util.hpp" + +using namespace std::literals; + namespace session { +template +using cleared_array = sodium_cleared>; + using uc32 = std::array; using uc64 = std::array; +using cleared_uc32 = cleared_array<32>; +using cleared_uc64 = cleared_array<64>; -ustring encrypt_for_recipient( +ustring sign_for_recipient( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { - - uc64 ed_sk_from_seed; + cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; crypto_sign_ed25519_seed_keypair( @@ -26,6 +35,8 @@ ustring encrypt_for_recipient( } else if (ed25519_privkey.size() != 64) { throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; } + // If prefixed, drop it (and do this for the caller, too) so that everything after this + // doesn't need to worry about whether it is prefixed or not. if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) recipient_pubkey.remove_prefix(1); else if (recipient_pubkey.size() != 32) @@ -47,18 +58,85 @@ ustring encrypt_for_recipient( buf.resize(buf.size() - 32); buf += ustring_view{sig.data(), sig.size()}; + return buf; +} + +static const ustring_view BOX_HASHKEY = to_unsigned_sv("SessionBoxEphemeralHashKey"sv); + +ustring encrypt_for_recipient( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { + + auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); + + if (recipient_pubkey.size() == 33) + recipient_pubkey.remove_prefix(1); // sign_for_recipient already checked that this is the + // proper 0x05 prefix when present. + ustring result; - result.resize(buf.size() + crypto_box_SEALBYTES); - if (0 != crypto_box_seal(result.data(), buf.data(), buf.size(), recipient_pubkey.data())) + result.resize(signed_msg.size() + crypto_box_SEALBYTES); + if (0 != crypto_box_seal( + result.data(), signed_msg.data(), signed_msg.size(), recipient_pubkey.data())) throw std::runtime_error{"Sealed box encryption failed"}; return result; } +ustring encrypt_for_recipient_deterministic( + ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { + + auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); + + if (recipient_pubkey.size() == 33) + recipient_pubkey.remove_prefix(1); // sign_for_recipient already checked that this is the + // proper 0x05 when present. + + // To make our ephemeral seed we're going to hash: SENDER_SEED || RECIPIENT_PK || MESSAGE with a + // keyed blake2b hash. + cleared_array seed; + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, BOX_HASHKEY.data(), BOX_HASHKEY.size(), seed.size()); + crypto_generichash_blake2b_update(&st, ed25519_privkey.data(), 32); + crypto_generichash_blake2b_update(&st, recipient_pubkey.data(), 32); + crypto_generichash_blake2b_update(&st, message.data(), message.size()); + crypto_generichash_blake2b_final(&st, seed.data(), seed.size()); + + cleared_array eph_sk; + cleared_array eph_pk; + + crypto_box_seed_keypair(eph_pk.data(), eph_sk.data(), seed.data()); + + // The nonce for a sealed box is not passed but is implicitly defined as the (unkeyed) blake2b + // hash of: + // EPH_PUBKEY || RECIPIENT_PUBKEY + cleared_array nonce; + crypto_generichash_blake2b_init(&st, nullptr, 0, nonce.size()); + crypto_generichash_blake2b_update(&st, eph_pk.data(), eph_pk.size()); + crypto_generichash_blake2b_update(&st, recipient_pubkey.data(), recipient_pubkey.size()); + crypto_generichash_blake2b_final(&st, nonce.data(), nonce.size()); + + // A sealed box is a regular box (using the ephermal keys and nonce), but with the ephemeral + // pubkey prepended: + static_assert(crypto_box_SEALBYTES == crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES); + + ustring result; + result.resize(crypto_box_SEALBYTES + signed_msg.size()); + std::memcpy(result.data(), eph_pk.data(), crypto_box_PUBLICKEYBYTES); + if (0 != crypto_box_easy( + result.data() + crypto_box_PUBLICKEYBYTES, + signed_msg.data(), + signed_msg.size(), + nonce.data(), + recipient_pubkey.data(), + eph_sk.data())) + throw std::runtime_error{"Crypto box encryption failed"}; + + return result; +} + std::pair decrypt_incoming( ustring_view ed25519_privkey, ustring_view ciphertext) { - uc64 ed_sk_from_seed; + cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; crypto_sign_ed25519_seed_keypair( @@ -73,7 +151,8 @@ std::pair decrypt_incoming( const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; const size_t msg_size = outer_size - 32 - 64; - uc32 x_sec, x_pub; + cleared_uc32 x_sec; + uc32 x_pub; crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); crypto_scalarmult_base(x_pub.data(), x_sec.data()); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 50c0c08d..1ed8451e 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -61,6 +61,22 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { 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}); + + // 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) == + "080112ab030a0012001aa20308062800429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39" + "98af44ca65f967768dd78ee80fffab6f809f6cef49c73a36c82a89622ff0de2ceee06b8c638e2c876fa9047f" + "449dbe24b1fc89281a264fe90abdeffcdd44f797bd4572a6c5ae8d88bf372c3c717943ebd570222206fabf0e" + "e9f3c6756f5d71a32616b1df53d12887961f5c129207a79622ccc1a4bba976886d9a6ddf0fe5d570e5075d01" + "ecd627f656e95f27b4c40d5661b5664cedd3e568206effa1308b0ccd663ca61a6d39c0731891804a8cf5edcf" + "8b98eaa5580c3d436e22156e38455e403869700956c3c1dd0b4470b663e75c98c5b859b53ccef6559215d804" + "9f755be9c2d6b3f4a310f97c496fc392f65b6431dd87788ac61074fd8cd409702e1b839b3f774d38cf8b28f0" + "226c4efa5220ac6ae060793e36e7ef278d42d042f15b21291f3bb29e3158f09d154b93f83fd8a319811a26cb" + "5240d90cbb360fafec0b7eff4c676ae598540813d062dc9468365c73b4cfa2ffd02d48cdcd8f0c71324c6d0a" + "60346a7a0e50af3be64684b37f9e6c831115bf112ddd18acde08eaec376f0872a3952000"); + free(to_push); // These should also be unset: diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 5143623d..a168c6e6 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -81,3 +81,59 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { CHECK_THROWS(decrypt_incoming(to_sv(ed_sk2), broken)); } } + +TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encrypt]") { + + using namespace session; + + 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(), seed.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); + ustring sid_raw; + oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); + REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + REQUIRE(sid_raw == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + std::array ed_pk2, curve_pk2; + std::array ed_sk2; + crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); + REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); + REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + ustring sid_raw2; + oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); + REQUIRE(sid_raw2 == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + + auto enc1 = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + auto enc2 = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + REQUIRE(enc1 != enc2); + + auto enc_det = + encrypt_for_recipient_deterministic(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + CHECK(enc_det != enc1); + CHECK(enc_det != enc2); + CHECK(enc_det.size() == enc1.size()); + CHECK(oxenc::to_hex(enc_det) == + "208f96785db92319bc7a14afecc01e17bde912d17bbb32834c03ea63b1862c2a1b730e0725ef75b2f1a276db" + "584c59a0ed9b5497bcb9f4effa893b5cb8b04dbe7a6ab457ebf972f03b006dd4572980a725399616d40184b8" + "6aa3b7b218bdc6dd7c1adccda8ef4897f0f458492240b39079c27a6c791067ab26a03067a7602b50f0434639" + "906f93e548f909d5286edde365ebddc146"); + + auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc_det); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(from_unsigned_sv(msg) == "hello"); +} From 8a5d71ca2c6612e93e1c83d7ce64f6ec961fa167 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 14:24:00 -0300 Subject: [PATCH 093/572] Move session/protos.hpp -> session/config/protos.hpp The only thing in this header are config-specific, so seems better under `config`. This also resolves some issues with circular library dependencies in the build between config, crypto, and protos. --- include/session/{ => config}/protos.hpp | 6 ++--- src/CMakeLists.txt | 9 +++----- src/config/base.cpp | 2 +- src/{ => config}/protos.cpp | 6 ++--- tests/test_proto.cpp | 30 ++++++++++++------------- 5 files changed, 24 insertions(+), 29 deletions(-) rename include/session/{ => config}/protos.hpp (94%) rename src/{ => config}/protos.cpp (98%) diff --git a/include/session/protos.hpp b/include/session/config/protos.hpp similarity index 94% rename from include/session/protos.hpp rename to include/session/config/protos.hpp index 6e6e630d..d9eec29b 100644 --- a/include/session/protos.hpp +++ b/include/session/config/protos.hpp @@ -1,9 +1,9 @@ #pragma once -#include "session/config/namespaces.hpp" +#include "namespaces.hpp" #include "session/util.hpp" -namespace session::protos { +namespace session::config::protos { /// API: config/protos::wrap_config /// @@ -42,4 +42,4 @@ ustring wrap_config( /// more serious). ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namespace ns); -} // namespace session::protos +} // namespace session::config::protos diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 457a28a6..5b5b38ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,9 +61,10 @@ add_libsession_util_library(config config/encrypt.cpp config/error.c config/groups/info.cpp - config/groups/members.cpp config/groups/keys.cpp + config/groups/members.cpp config/internal.cpp + config/protos.cpp config/user_groups.cpp config/user_profile.cpp fields.cpp @@ -77,7 +78,6 @@ add_libsession_util_library(onionreq ) add_libsession_util_library(protos - protos.cpp ../proto/SessionProtos.pb.cc ../proto/WebSocketResources.pb.cc ) @@ -86,9 +86,6 @@ add_libsession_util_library(protos target_link_libraries(protos PUBLIC - common - PRIVATE - libsodium::sodium-internal protobuf::libprotobuf-lite ) target_include_directories(protos PUBLIC ../proto) @@ -97,7 +94,6 @@ target_link_libraries(crypto PUBLIC common PRIVATE - protos libsodium::sodium-internal ) @@ -105,6 +101,7 @@ target_link_libraries(config PUBLIC crypto common + protos PRIVATE libsodium::sodium-internal libzstd::static diff --git a/src/config/base.cpp b/src/config/base.cpp index 150b4238..c6c29715 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -15,8 +15,8 @@ #include "internal.hpp" #include "session/config/base.h" #include "session/config/encrypt.hpp" +#include "session/config/protos.hpp" #include "session/export.h" -#include "session/protos.hpp" #include "session/util.hpp" using namespace std::literals; diff --git a/src/protos.cpp b/src/config/protos.cpp similarity index 98% rename from src/protos.cpp rename to src/config/protos.cpp index f3b03048..947fda51 100644 --- a/src/protos.cpp +++ b/src/config/protos.cpp @@ -1,4 +1,4 @@ -#include "session/protos.hpp" +#include "session/config/protos.hpp" #include #include @@ -10,7 +10,7 @@ #include "WebSocketResources.pb.h" #include "session/session_encrypt.hpp" -namespace session::protos { +namespace session::config::protos { namespace { @@ -176,4 +176,4 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp return ustring{to_unsigned_sv(shconf.data())}; } -} // namespace session::protos +} // namespace session::config::protos diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp index 995460a3..54abccdc 100644 --- a/tests/test_proto.cpp +++ b/tests/test_proto.cpp @@ -3,18 +3,18 @@ #include #include #include +#include #include -#include #include "utils.hpp" -using namespace session; +using namespace session::config; -const std::vector groups{ - config::Namespace::UserProfile, - config::Namespace::Contacts, - config::Namespace::ConvoInfoVolatile, - config::Namespace::UserGroups}; +const std::vector groups{ + Namespace::UserProfile, + Namespace::Contacts, + Namespace::ConvoInfoVolatile, + Namespace::UserGroups}; const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk_raw; @@ -44,13 +44,12 @@ TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { } SECTION("Message type payload comparison") { - auto user_profile_msg = protos::wrap_config(ed_sk, msg, 1, config::Namespace::UserProfile); - auto contacts_msg = protos::wrap_config(ed_sk, msg, 1, config::Namespace::Contacts); + auto user_profile_msg = protos::wrap_config(ed_sk, msg, 1, Namespace::UserProfile); + auto contacts_msg = protos::wrap_config(ed_sk, msg, 1, Namespace::Contacts); auto user_profile_parsed = - protos::unwrap_config(ed_sk, user_profile_msg, config::Namespace::UserProfile); - auto contacts_parsed = - protos::unwrap_config(ed_sk, contacts_msg, config::Namespace::Contacts); + protos::unwrap_config(ed_sk, user_profile_msg, Namespace::UserProfile); + auto contacts_parsed = protos::unwrap_config(ed_sk, contacts_msg, Namespace::Contacts); // All of these will return true, as the parsed messages will be identical to the // payload, and therefore identical to one another @@ -64,8 +63,7 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { auto msg = "Hello from the other side"_bytes; auto addendum = "jfeejj0ifdoesam"_bytes; - const auto user_profile_msg = - protos::wrap_config(ed_sk, msg, 1, config::Namespace::UserProfile); + const auto user_profile_msg = protos::wrap_config(ed_sk, msg, 1, Namespace::UserProfile); const auto size = user_profile_msg.size(); // Testing three positions: front, inside the payload, and at the end @@ -75,7 +73,7 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { auto msg_copy = user_profile_msg; msg_copy.insert(p, addendum); - REQUIRE_THROWS(protos::unwrap_config(ed_sk, msg_copy, config::Namespace::UserProfile)); + REQUIRE_THROWS(protos::unwrap_config(ed_sk, msg_copy, Namespace::UserProfile)); } } @@ -109,5 +107,5 @@ TEST_CASE("Protobuf old config loading test", "[config][proto][old]") { "db9aac0032c484062d7ba7bbe64e07bcd633eec8378d5d914732693c5e298f015ebde2ae45769ed319e267" "f0528f5cc6da268343b6647b20bae6e9ee8d92cca702"_hexbytes; - CHECK_NOTHROW(protos::unwrap_config(ed_sk, old_conf, config::Namespace::UserProfile)); + CHECK_NOTHROW(protos::unwrap_config(ed_sk, old_conf, Namespace::UserProfile)); } From d49b235c1bdfaa639dcadcedd917ab649062540e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 15:47:02 -0300 Subject: [PATCH 094/572] Allow using system protobuf for shared lib builds Also moves the `protos` build into /protos rather than building it via `../` inside src. --- proto/CMakeLists.txt | 82 +++++++++++++++++++++++++++++++------------- src/CMakeLists.txt | 20 +++-------- tests/CMakeLists.txt | 1 - 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 25630a72..18386102 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -5,34 +5,70 @@ function(check_target target) endif() endfunction() -include(FetchContent) - -FetchContent_Declare( - protobuf - GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git - GIT_TAG v3.21.12 # apparently this must be a tag (not hash) for git_shallow to work? - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE -) +if (BUILD_SHARED_LIBS AND NOT BUILD_STATIC_DEPS) + find_package(PkgConfig REQUIRED) + pkg_check_modules(PROTOBUF_LITE protobuf-lite>=3.21 IMPORTED_TARGET) + if(PROTOBUF_LITE_FOUND) + add_library(protobuf_lite INTERFACE IMPORTED) + target_link_libraries(protobuf_lite INTERFACE PkgConfig::PROTOBUF_LITE) + add_library(protobuf::libprotobuf-lite ALIAS protobuf_lite) + endif() +endif() + +if(NOT TARGET protobuf::libprotobuf-lite) + +# System protobuf not found, or we are building our own deps: + include(FetchContent) + + FetchContent_Declare( + protobuf + GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git + GIT_TAG v3.21.12 # apparently this must be a tag (not hash) for git_shallow to work? + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) -set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) -set(protobuf_INSTALL ON CACHE BOOL "" FORCE) -set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) -set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") -set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) + set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) + set(protobuf_INSTALL ON CACHE BOOL "" FORCE) + set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) + set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) + set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) + set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") + set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) -message(STATUS "Pulling protobuf repository...") + message(STATUS "Pulling protobuf repository...") -FetchContent_MakeAvailable(protobuf) + FetchContent_MakeAvailable(protobuf) -check_target(protobuf::libprotobuf-lite) + check_target(protobuf::libprotobuf-lite) -libsession_static_bundle(protobuf::libprotobuf-lite) + libsession_static_bundle(protobuf::libprotobuf-lite) + +endif() + +add_library(protos + SessionProtos.pb.cc + WebSocketResources.pb.cc) +target_include_directories(protos PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(protos PUBLIC protobuf::libprotobuf-lite) +set_target_properties( + protos PROPERTIES + OUTPUT_NAME session-protos + SOVERSION ${LIBSESSION_LIBVERSION}) + +libsession_static_bundle(protos) + +add_library(libsession::protos ALIAS protos) +export( + TARGETS protos + NAMESPACE libsession:: + FILE libsessionTargets.cmake +) +list(APPEND libsession_export_targets protos) +set(libsession_export_targets "${libsession_export_targets}" PARENT_SCOPE) add_custom_target(regen-protobuf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b5b38ab..6fadb031 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -49,6 +49,7 @@ add_libsession_util_library(crypto blinding.cpp session_encrypt.cpp xed25519.cpp + util.cpp ) add_libsession_util_library(config @@ -68,7 +69,6 @@ add_libsession_util_library(config config/user_groups.cpp config/user_profile.cpp fields.cpp - util.cpp ) add_libsession_util_library(onionreq @@ -77,19 +77,8 @@ add_libsession_util_library(onionreq onionreq/parser.cpp ) -add_libsession_util_library(protos - ../proto/SessionProtos.pb.cc - ../proto/WebSocketResources.pb.cc -) - -target_link_libraries(protos - PUBLIC - protobuf::libprotobuf-lite -) -target_include_directories(protos PUBLIC ../proto) - target_link_libraries(crypto PUBLIC common @@ -101,7 +90,7 @@ target_link_libraries(config PUBLIC crypto common - protos + libsession::protos PRIVATE libsodium::sodium-internal libzstd::static @@ -163,7 +152,8 @@ endforeach() export( TARGETS ${export_targets} common version NAMESPACE libsession:: - FILE libsessionTargets.cmake + APPEND FILE libsessionTargets.cmake ) -set(libsession_export_targets "${export_targets}" PARENT_SCOPE) +list(APPEND libsession_export_targets ${export_targets}) +set(libsession_export_targets "${libsession_export_targets}" PARENT_SCOPE) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9615d83e..52f7f20f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,7 +23,6 @@ add_executable(testAll target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq - libsession::protos libsodium::sodium-internal Catch2::Catch2WithMain) From 8837142cf5235c0283ddf71428088e1aae4d9927 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 22:42:29 -0300 Subject: [PATCH 095/572] Make the onionreq code optional; disable for ios.sh The dependencies are causing headaches right now on iOS, and currently session clients don't need this code so can disable it. --- CMakeLists.txt | 3 +++ src/CMakeLists.txt | 32 +++++++++++++++++--------------- utils/ios.sh | 3 ++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 17286af9..b7d95cc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,9 @@ option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd} option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) +# Provide this as an option for now because GMP and iOS are sometimes unhappy with each other. +option(ENABLE_ONIONREQ "Build with onion request functionality" ON) + if(USE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6fadb031..404a4b2f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,12 +71,6 @@ add_libsession_util_library(config fields.cpp ) -add_libsession_util_library(onionreq - onionreq/channel_encryption.cpp - onionreq/key_types.cpp - onionreq/parser.cpp -) - target_link_libraries(crypto @@ -96,15 +90,23 @@ target_link_libraries(config libzstd::static ) -target_link_libraries(onionreq - PUBLIC - crypto - common - PRIVATE - nlohmann_json::nlohmann_json - libsodium::sodium-internal - nettle -) +if(ENABLE_ONIONREQ) + add_libsession_util_library(onionreq + onionreq/channel_encryption.cpp + onionreq/key_types.cpp + onionreq/parser.cpp + ) + + target_link_libraries(onionreq + PUBLIC + crypto + common + PRIVATE + nlohmann_json::nlohmann_json + libsodium::sodium-internal + nettle + ) +endif() if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") diff --git a/utils/ios.sh b/utils/ios.sh index b3f63085..1634f495 100755 --- a/utils/ios.sh +++ b/utils/ios.sh @@ -106,7 +106,8 @@ for i in "${!TARGET_ARCHS[@]}"; do -DCMAKE_TOOLCHAIN_FILE="${projdir}/external/ios-cmake/ios.toolchain.cmake" \ -DPLATFORM=$platform \ -DDEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET \ - -DENABLE_BITCODE=$ENABLE_BITCODE + -DENABLE_BITCODE=$ENABLE_BITCODE \ + -DENABLE_ONIONREQ=OFF # Temporary until we figure out why ios builds hate gmp done # If needed combine simulator builds into a multi-arch lib From a608b1386e78544f8f6ebc512456e5da1d8d7a40 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 4 Oct 2023 22:59:03 -0300 Subject: [PATCH 096/572] Also disable gmp/nettle in StaticBuild --- cmake/StaticBuild.cmake | 48 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index a5a288f0..fd3748d4 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -233,29 +233,31 @@ elseif(gmp_build_host STREQUAL "") set(gmp_build_host "--build=${CMAKE_LIBRARY_ARCHITECTURE}") endif() -build_external(gmp - CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic - "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" - "LDFLAGS=${apple_ldflags_arch}" ${cross_rc} CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp -) -add_static_target(gmp gmp_external libgmp.a) - -build_external(nettle - CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --libdir=${DEPS_DESTDIR}/lib - --with-pic --disable-openssl - "CC=${deps_cc}" "CXX=${deps_cxx}" - "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" - "CPPFLAGS=-I${DEPS_DESTDIR}/include" - "LDFLAGS=-L${DEPS_DESTDIR}/lib${apple_ldflags_arch}" - - DEPENDS gmp_external - BUILD_BYPRODUCTS - ${DEPS_DESTDIR}/lib/libnettle.a - ${DEPS_DESTDIR}/lib/libhogweed.a - ${DEPS_DESTDIR}/include/nettle/version.h -) -add_static_target(nettle nettle_external libnettle.a gmp) -add_static_target(hogweed nettle_external libhogweed.a nettle) +if(ENABLE_ONIONREQ) + build_external(gmp + CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic + "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" + "LDFLAGS=${apple_ldflags_arch}" ${cross_rc} CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp + ) + add_static_target(gmp gmp_external libgmp.a) + + build_external(nettle + CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --libdir=${DEPS_DESTDIR}/lib + --with-pic --disable-openssl + "CC=${deps_cc}" "CXX=${deps_cxx}" + "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" + "CPPFLAGS=-I${DEPS_DESTDIR}/include" + "LDFLAGS=-L${DEPS_DESTDIR}/lib${apple_ldflags_arch}" + + DEPENDS gmp_external + BUILD_BYPRODUCTS + ${DEPS_DESTDIR}/lib/libnettle.a + ${DEPS_DESTDIR}/lib/libhogweed.a + ${DEPS_DESTDIR}/include/nettle/version.h + ) + add_static_target(nettle nettle_external libnettle.a gmp) + add_static_target(hogweed nettle_external libhogweed.a nettle) +endif() link_libraries(-static-libstdc++) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") From e442c28d4ae2f0bcbffe4d2f35ceecfb7acc314d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 5 Oct 2023 11:08:48 -0300 Subject: [PATCH 097/572] Set timestamp to 1 to pass old Session clients validity check --- src/config/protos.cpp | 2 +- tests/test_config_userprofile.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/protos.cpp b/src/config/protos.cpp index 947fda51..affcc83c 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -98,7 +98,7 @@ ustring wrap_config( // Now we just keep on trucking with more protobuf: auto envelope = SessionProtos::Envelope(); *envelope.mutable_content() = from_unsigned_sv(enc_shared_conf); - envelope.set_timestamp(0); + envelope.set_timestamp(1); // Old session clients with their own unwrapping require this > 0 envelope.set_type(SessionProtos::Envelope_Type::Envelope_Type_SESSION_MESSAGE); // And more protobuf (even though this no one cares about anything other than the body in this diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 1ed8451e..3cadd564 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -66,7 +66,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // 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) == - "080112ab030a0012001aa20308062800429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39" + "080112ab030a0012001aa20308062801429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39" "98af44ca65f967768dd78ee80fffab6f809f6cef49c73a36c82a89622ff0de2ceee06b8c638e2c876fa9047f" "449dbe24b1fc89281a264fe90abdeffcdd44f797bd4572a6c5ae8d88bf372c3c717943ebd570222206fabf0e" "e9f3c6756f5d71a32616b1df53d12887961f5c129207a79622ccc1a4bba976886d9a6ddf0fe5d570e5075d01" From ac96f0e9e1cfcc8f3ba77fab337bf660c094f7a2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 5 Oct 2023 22:57:14 -0300 Subject: [PATCH 098/572] Return good hashes from merge() Session clients required the list of hashes from a merge() to perform various operations based on the messages that were accepted. This updates `merge()` to return a vector of accepted hashes. For the C API, this returns a config_string_list* pointer that needs to be freed, but contains all the good hashes (the same way current_hashes() works). --- include/session/config/base.h | 38 ++++++++++++------ include/session/config/base.hpp | 22 +++++------ src/config/base.cpp | 21 ++++++---- tests/test_bugs.cpp | 2 +- tests/test_config_contacts.cpp | 10 +++-- tests/test_config_convo_info_volatile.cpp | 11 ++++-- tests/test_config_user_groups.cpp | 2 +- tests/test_config_userprofile.cpp | 16 ++++++-- tests/test_group_info.cpp | 32 +++++++-------- tests/test_group_keys.cpp | 48 +++++++++++------------ tests/test_group_members.cpp | 8 ++-- 11 files changed, 124 insertions(+), 86 deletions(-) diff --git a/include/session/config/base.h b/include/session/config/base.h index a14ba528..8707d903 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -91,6 +91,17 @@ LIBSESSION_EXPORT void config_set_logger( /// - `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 @@ -117,13 +128,19 @@ LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf); /// - `count` -- [in] is the length of all three arrays. /// /// Outputs: -/// - `int` -- -LIBSESSION_EXPORT int config_merge( +/// - `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); + size_t count) +#ifdef __GNUC__ + __attribute__((warn_unused_result)) +#endif + ; /// API: base/config_needs_push /// @@ -251,13 +268,6 @@ LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, siz /// - `bool` -- True if config has changed since last call to `dump()` LIBSESSION_EXPORT bool config_needs_dump(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. -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_current_hashes /// /// Obtains the current active hashes. Note that this will be empty if the current hash is unknown @@ -278,8 +288,12 @@ typedef struct config_string_list { /// - `conf` -- [in] Pointer to config_object object /// /// Outputs: -/// - `config_string_list*` -- point to the list of hashes, pointer belongs to the caller -LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf); +/// - `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 /// diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index e1775dc4..27cb2a14 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -768,18 +768,15 @@ class ConfigBase : public ConfigSig { /// parseable). This should not happen (the current config, at least, should always be /// re-parseable). /// - /// Declaration: - /// ```cpp - /// int _merge(const std::vector>& configs); - /// int _merge(const std::vector>& configs); - /// ``` - /// /// Inputs: /// - `configs` -- vector of pairs containing the message hash and the raw message body /// /// Outputs: - /// - `int` -- Returns how many config messages that were successfully parsed - int _merge(const std::vector>& configs); + /// - 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(const std::vector>& configs); /// API: base/ConfigBase::extra_data /// @@ -895,12 +892,15 @@ class ConfigBase : public ConfigSig { /// - `configs` -- vector of pairs containing the message hash and the raw message body /// /// Outputs: - /// - `int` -- Returns how many config messages that were successfully parsed - int merge(const std::vector>& configs); + /// - 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(const std::vector>& configs); // Same as above, but passes the ustring_views to the overload of protos::handle_incoming that // takes ustring_views - int merge(const std::vector>& configs); + std::vector merge(const std::vector>& configs); /// API: base/ConfigBase::is_dirty /// diff --git a/src/config/base.cpp b/src/config/base.cpp index c6c29715..5c0fccc4 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -53,7 +53,8 @@ std::unique_ptr make_config_message(bool from_dirty, Args&&... ar return std::make_unique(std::forward(args)...); } -int ConfigBase::merge(const std::vector>& configs) { +std::vector ConfigBase::merge( + const std::vector>& configs) { std::vector> config_views; config_views.reserve(configs.size()); for (auto& [hash, data] : configs) @@ -61,7 +62,7 @@ int ConfigBase::merge(const std::vector>& config return merge(config_views); } -int ConfigBase::merge(const std::vector>& configs) { +std::vector ConfigBase::merge(const std::vector>& configs) { if (accepts_protobuf() && !_keys.empty()) { std::list keep_alive; std::vector> parsed; @@ -85,7 +86,7 @@ int ConfigBase::merge(const std::vector>& c return _merge(configs); } -int ConfigBase::_merge(const std::vector>& configs) { +std::vector ConfigBase::_merge(const std::vector>& configs) { if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; @@ -239,8 +240,13 @@ int ConfigBase::_merge(const std::vector>& assert(new_conf->unmerged_index() == 0); } - return all_confs.size() - bad_confs.size() - - (mine.empty() ? 0 : 1); // -1 because we don't count the first one (reparsing ourself). + std::vector good_hashes; + good_hashes.reserve(all_hashes.size() - (mine.empty() ? 0 : 1) - bad_confs.size()); + for (size_t i = mine.empty() ? 0 : 1; i < all_hashes.size(); i++) + if (!bad_confs.count(i)) + good_hashes.emplace_back(all_hashes[i]); + + return good_hashes; } std::vector ConfigBase::current_hashes() const { @@ -610,7 +616,7 @@ LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf) { return static_cast(unbox(conf)->storage_namespace()); } -LIBSESSION_EXPORT int config_merge( +LIBSESSION_EXPORT config_string_list* config_merge( config_object* conf, const char** msg_hashes, const unsigned char** configs, @@ -621,7 +627,8 @@ LIBSESSION_EXPORT int config_merge( confs.reserve(count); for (size_t i = 0; i < count; i++) confs.emplace_back(msg_hashes[i], ustring_view{configs[i], lengths[i]}); - return config.merge(confs); + + return make_string_list(config.merge(confs)); } LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { diff --git a/tests/test_bugs.cpp b/tests/test_bugs.cpp index 86ee6ab4..13a7f51a 100644 --- a/tests/test_bugs.cpp +++ b/tests/test_bugs.cpp @@ -52,7 +52,7 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { auto r = c1.merge(std::vector>{ {{"fakehash2", data2}, {"fakehash3", data3}}}); - CHECK(r == 2); + CHECK(r == std::vector{{"fakehash2"s, "fakehash3"s}}); CHECK(c1.needs_dump()); CHECK(c1.needs_push()); // because we have the merge conflict to push CHECK(c1.is_dirty()); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 565e9785..17883d75 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -294,8 +294,10 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { merge_hash[0] = "fakehash1"; merge_data[0] = to_push->config; merge_size[0] = to_push->config_len; - int accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); - REQUIRE(accepted == 1); + 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); @@ -328,7 +330,9 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { 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 == 1); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash2"sv); + free(accepted); config_confirm_pushed(conf2, to_push->seqno, "fakehash2"); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 9e4ddf22..0f1698e6 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -362,8 +362,10 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { hash_data[0] = "hash123"; merge_data[0] = to_push->config; merge_size[0] = to_push->config_len; - int accepted = config_merge(conf, hash_data, merge_data, merge_size, 1); - REQUIRE(accepted == 1); + config_string_list* accepted = config_merge(conf, hash_data, merge_data, merge_size, 1); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "hash123"sv); + free(accepted); config_confirm_pushed(conf2, seqno, "hash123"); free(to_push); @@ -630,7 +632,10 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load merge_data[0] = to_push->config; merge_size[0] = to_push->config_len; - config_merge(conf2, merge_hash, merge_data, merge_size, 1); + config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "hash5235"sv); + free(accepted); free(to_push); CHECK(config_needs_push(conf2)); diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 5e433baa..25425a4e 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -699,7 +699,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { std::vector> to_merge; to_merge.emplace_back("fakehash1", ustring_view{to_push->config, to_push->config_len}); - CHECK(c2.merge(to_merge) == 1); + CHECK(c2.merge(to_merge) == std::vector{{"fakehash1"}}); auto grp = c2.get_legacy_group(definitely_real_id); REQUIRE(grp); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 3cadd564..66c81cac 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -216,8 +216,10 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { merge_hash[0] = "fakehash1"; merge_data[0] = exp_push1_encrypted.data(); merge_size[0] = exp_push1_encrypted.size(); - int accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); - REQUIRE(accepted == 1); + config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash1"sv); + free(accepted); // Our state has changed, so we need to dump: CHECK(config_needs_dump(conf2)); @@ -284,12 +286,18 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { merge_hash[0] = "fakehash2"; merge_data[0] = to_push->config; merge_size[0] = to_push->config_len; - config_merge(conf2, merge_hash, merge_data, merge_size, 1); + 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; - config_merge(conf, merge_hash, merge_data, merge_size, 1); + accepted = config_merge(conf, merge_hash, merge_data, merge_size, 1); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash3"sv); + free(accepted); free(to_push2); // Now after the merge we *will* want to push from both client, since both will have generated a diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index ce21534a..1896b47f 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -67,7 +67,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { std::vector> merge_configs; merge_configs.emplace_back("fakehash1", p1); - CHECK(ginfo2.merge(merge_configs) == 1); + CHECK(ginfo2.merge(merge_configs) == std::vector{{"fakehash1"s}}); CHECK_FALSE(ginfo2.needs_push()); CHECK(ginfo2.get_name() == "GROUP Name"); @@ -95,14 +95,14 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { merge_configs.emplace_back("fakehash2", p2); // This fails because ginfo1 doesn't yet have the new key that ginfo2 used (bbb...) - CHECK(ginfo1.merge(merge_configs) == 0); + CHECK(ginfo1.merge(merge_configs) == std::vector{}); ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); ginfo1.add_key( "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes, /*prepend=*/false); - CHECK(ginfo1.merge(merge_configs) == 1); + CHECK(ginfo1.merge(merge_configs) == std::vector{{"fakehash2"s}}); CHECK(ginfo1.needs_push()); auto [s3, p3, o3] = ginfo1.push(); @@ -121,7 +121,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { merge_configs.clear(); merge_configs.emplace_back("fakehash3", p3); - CHECK(ginfo2.merge(merge_configs) == 1); + CHECK(ginfo2.merge(merge_configs) == std::vector{{"fakehash3"s}}); CHECK(ginfo2.get_name() == "Better name!"); CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345"); CHECK(ginfo2.get_profile_pic().key == @@ -188,7 +188,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { std::vector> merge_configs; merge_configs.emplace_back("fakehash1", to_push); - CHECK(ginfo.merge(merge_configs) == 1); + CHECK(ginfo.merge(merge_configs) == std::vector{{"fakehash1"s}}); CHECK_FALSE(ginfo.needs_push()); groups::Info ginfo_rw2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; @@ -196,7 +196,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw2.add_key(k, false); - CHECK(ginfo_rw2.merge(merge_configs) == 1); + CHECK(ginfo_rw2.merge(merge_configs) == std::vector{{"fakehash1"s}}); CHECK_FALSE(ginfo.needs_push()); CHECK(ginfo.get_name() == "Super Group!!"); @@ -231,7 +231,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { merge_configs.clear(); merge_configs.emplace_back("badhash1", p_bad); - CHECK(ginfo.merge(merge_configs) == 0); + CHECK(ginfo.merge(merge_configs) == std::vector{}); CHECK_FALSE(ginfo.needs_push()); // Now let's get more complicated: we will have *two* valid signers who submit competing updates @@ -250,15 +250,15 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { merge_configs.emplace_back("fakehash2", tp2); merge_configs.emplace_back("fakehash3", tp3); - CHECK(ginfo.merge(merge_configs) == 2); + CHECK(ginfo.merge(merge_configs) == std::vector{{"fakehash2"s, "fakehash3"s}}); CHECK(ginfo.is_clean()); CHECK(s2 == 2); CHECK(s3 == 2); CHECK_FALSE(ginfo.needs_push()); - CHECK(ginfo_rw.merge(merge_configs) == 2); - CHECK(ginfo_rw2.merge(merge_configs) == 2); + CHECK(ginfo_rw.merge(merge_configs) == std::vector{{"fakehash2"s, "fakehash3"s}}); + CHECK(ginfo_rw2.merge(merge_configs) == std::vector{{"fakehash2"s, "fakehash3"s}}); CHECK(ginfo_rw.needs_push()); CHECK(ginfo_rw2.needs_push()); @@ -276,9 +276,9 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { merge_configs.clear(); merge_configs.emplace_back("fakehash23", t23); - CHECK(ginfo.merge(merge_configs) == 1); - CHECK(ginfo_rw.merge(merge_configs) == 1); - CHECK(ginfo_rw2.merge(merge_configs) == 1); + CHECK(ginfo.merge(merge_configs) == std::vector{{"fakehash23"s}}); + CHECK(ginfo_rw.merge(merge_configs) == std::vector{{"fakehash23"s}}); + CHECK(ginfo_rw2.merge(merge_configs) == std::vector{{"fakehash23"s}}); CHECK_FALSE(ginfo.needs_push()); CHECK_FALSE(ginfo_rw.needs_push()); @@ -324,7 +324,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { for (const auto& k : enc_keys2) // Just for testing, as above. ginfo_rw3.add_key(k, false); - CHECK(ginfo_rw3.merge(merge_configs) == 1); + CHECK(ginfo_rw3.merge(merge_configs) == std::vector{{"fakehash23"s}}); CHECK(ginfo_rw3.get_name() == "Super Group 2"); auto [s6, t6, o6] = ginfo_rw3.push(); @@ -349,10 +349,10 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { merge_configs.clear(); merge_configs.emplace_back("fakehash7", t7); // If we don't have the new "bbb" key loaded yet, this will fail: - CHECK(ginfo.merge(merge_configs) == 0); + CHECK(ginfo.merge(merge_configs) == std::vector{}); ginfo.add_key(enc_keys2.front()); - CHECK(ginfo.merge(merge_configs) == 1); + CHECK(ginfo.merge(merge_configs) == std::vector{{"fakehash7"s}}); auto pic = ginfo.get_profile_pic(); CHECK_FALSE(pic.empty()); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 7d7f42c5..e9cb8759 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -173,8 +173,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& a : admins) { a.keys.load_key_message( "keyhash1", new_keys_config1, get_timestamp_ms(), a.info, a.members); - CHECK(a.info.merge(info_configs) == 1); - CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.merge(info_configs) == std::vector{{"fakehash1"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"fakehash1"s}}); CHECK(a.members.size() == 1); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s}}); } @@ -218,8 +218,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& a : admins) { a.keys.load_key_message( "keyhash2", new_keys_config2, get_timestamp_ms(), a.info, a.members); - CHECK(a.info.merge(info_configs) == 1); - CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.merge(info_configs) == std::vector{{"fakehash2"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"fakehash2"s}}); CHECK(a.members.size() == 5); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s}}); } @@ -227,8 +227,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& m : members) { m.keys.load_key_message( "keyhash2", new_keys_config2, get_timestamp_ms(), m.info, m.members); - CHECK(m.info.merge(info_configs) == 1); - CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.info.merge(info_configs) == std::vector{{"fakehash2"s}}); + CHECK(m.members.merge(mem_configs) == std::vector{{"fakehash2"s}}); CHECK(m.members.size() == 5); CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s}}); } @@ -255,8 +255,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& a : admins) { a.keys.load_key_message( "keyhash3", new_keys_config3, get_timestamp_ms(), a.info, a.members); - CHECK(a.info.merge(info_configs) == 1); - CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.merge(info_configs) == std::vector{{"fakehash3"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"fakehash3"s}}); CHECK(a.info.get_name() == "tomatosauce"s); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s}}); @@ -265,8 +265,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& m : members) { m.keys.load_key_message( "keyhash3", new_keys_config3, get_timestamp_ms(), m.info, m.members); - CHECK(m.info.merge(info_configs) == 1); - CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.info.merge(info_configs) == std::vector{{"fakehash3"s}}); + CHECK(m.members.merge(mem_configs) == std::vector{{"fakehash3"s}}); CHECK(m.info.get_name() == "tomatosauce"s); CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}}); } @@ -299,8 +299,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (auto& a : admins) { CHECK(a.keys.load_key_message( "keyhash4", new_keys_config4, get_timestamp_ms(), a.info, a.members)); - CHECK(a.info.merge(info_configs) == 1); - CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.merge(info_configs) == std::vector{{"fakehash4"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"fakehash4"s}}); CHECK(a.members.size() == 3); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s, "keyhash4"s}}); @@ -315,13 +315,13 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s}}); if (i < 2) { // We should still be in the group CHECK(found_key); - CHECK(m.info.merge(info_configs) == 1); - CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.info.merge(info_configs) == std::vector{{"fakehash4"s}}); + CHECK(m.members.merge(mem_configs) == std::vector{{"fakehash4"s}}); CHECK(m.members.size() == 3); } else { CHECK_FALSE(found_key); - CHECK(m.info.merge(info_configs) == 0); - CHECK(m.members.merge(mem_configs) == 0); + CHECK(m.info.merge(info_configs) == std::vector{}); + CHECK(m.members.merge(mem_configs) == std::vector{}); CHECK(m.members.size() == 5); } } @@ -400,8 +400,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.keys.group_keys().size() == 4); } - CHECK(m.info.merge(info_configs) == 1); - CHECK(m.members.merge(mem_configs) == 1); + CHECK(m.info.merge(info_configs) == std::vector{{"fakehash4"s}}); + CHECK(m.members.merge(mem_configs) == std::vector{{"fakehash5"s}}); REQUIRE(m.info.get_name()); CHECK(*m.info.get_name() == "tomatosauce"sv); CHECK(m.members.size() == 5); @@ -466,8 +466,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { get_timestamp_ms() + 10LL * 86400 * 1000, a.info, a.members)); - CHECK(a.info.merge(info_configs) == 1); - CHECK(a.members.merge(mem_configs) == 1); + CHECK(a.info.merge(info_configs) == std::vector{{"ifakehash6"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"mfakehash6"s}}); CHECK(a.members.size() == 5); CHECK(a.keys.current_hashes() == std::unordered_set{ {"keyhash1"s, @@ -497,8 +497,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { get_timestamp_ms() + 71LL * 86400 * 1000, a.info, a.members)); - CHECK(a.info.merge(info_configs) == 2); - CHECK(a.members.merge(mem_configs) == 2); + CHECK(a.info.merge(info_configs) == std::vector{{"ifakehash6"s, "ifakehash7"s}}); + CHECK(a.members.merge(mem_configs) == std::vector{{"mfakehash6"s, "mfakehash7"s}}); CHECK(a.members.size() == 5); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } @@ -517,8 +517,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { get_timestamp_ms() + 71LL * 86400 * 1000, m.info, m.members)); - CHECK(m.info.merge(info_configs) == 2); - CHECK(m.members.merge(mem_configs) == 2); + CHECK(m.info.merge(info_configs) == std::vector{{"ifakehash6"s, "ifakehash7"s}}); + CHECK(m.members.merge(mem_configs) == std::vector{{"mfakehash6"s, "mfakehash7"s}}); CHECK(m.members.size() == 5); CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index d990ab61..242c148d 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -105,7 +105,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { std::vector> merge_configs; merge_configs.emplace_back("fakehash1", p1); - CHECK(gmem2.merge(merge_configs) == 1); + CHECK(gmem2.merge(merge_configs) == std::vector{{"fakehash1"}}); CHECK_FALSE(gmem2.needs_push()); for (int i = 0; i < 25; i++) @@ -166,9 +166,9 @@ TEST_CASE("Group Members", "[config][groups][members]") { auto [s2, p2, o2] = gmem2.push(); gmem2.confirm_pushed(s2, "fakehash2"); merge_configs.emplace_back("fakehash2", p2); // not clearing it first! - CHECK(gmem1.merge(merge_configs) == 1); + CHECK(gmem1.merge(merge_configs) == std::vector{{"fakehash1"s}}); gmem1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); - CHECK(gmem1.merge(merge_configs) == 2); + CHECK(gmem1.merge(merge_configs) == std::vector{{"fakehash1"s, "fakehash2"s}}); CHECK(gmem1.get(sids[23]).value().name == "Member 23"); @@ -220,7 +220,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.confirm_pushed(s3, "fakehash3"); merge_configs.clear(); merge_configs.emplace_back("fakehash3", p3); - CHECK(gmem2.merge(merge_configs) == 1); + CHECK(gmem2.merge(merge_configs) == std::vector{{"fakehash3"s}}); { int i = 0; From 0548e1f47963229979e1da630880eb31a3eab517 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 6 Oct 2023 20:48:49 -0300 Subject: [PATCH 099/572] Fix build host regex to recognize "ios13.0" It was only matching 13 but not 13.0, which was making gmp putting in assembly instructions that ios doesn't support (because gmp only turns that off if it sees "darwin" in the host string). --- cmake/StaticBuild.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index fd3748d4..c677c0a3 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -197,7 +197,7 @@ set(apple_cxxflags_arch) set(apple_ldflags_arch) set(gmp_build_host "${cross_host}") if(APPLE AND CMAKE_CROSSCOMPILING) - if(gmp_build_host MATCHES "^(.*-.*-)ios([0-9]+)(-.*)?$") + if(gmp_build_host MATCHES "^(.*-.*-)ios([0-9.]+)(-.*)?$") set(gmp_build_host "${CMAKE_MATCH_1}darwin${CMAKE_MATCH_2}${CMAKE_MATCH_3}") endif() if(gmp_build_host MATCHES "^(.*-.*-.*)-simulator$") From 6c4e38c4ed7f3a153428491bda588c782f8e0c60 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 6 Oct 2023 20:50:35 -0300 Subject: [PATCH 100/572] Add macos-compatible nproc alternative Without this we were running `make -j` on macos which is a minor fork bomb. Also adds a workaround in case that doesn't work either to fall back to just -j1. --- utils/static-bundle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/static-bundle.sh b/utils/static-bundle.sh index 0eeade5d..7b8ea49d 100755 --- a/utils/static-bundle.sh +++ b/utils/static-bundle.sh @@ -63,7 +63,7 @@ cmake -G 'Unix Makefiles' \ "$@" \ "$projdir" -make -j${JOBS:-$(nproc)} VERBOSE=1 session-util +make -j${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)} VERBOSE=1 session-util if [ -z "$archive" ]; then exit 0 From bce55202348d89889f529d7716b90d15905a0e20 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Oct 2023 20:04:15 -0300 Subject: [PATCH 101/572] Version bump for `dev` 1.1.0 got used for a release with merged-hash-return-values. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d46a2740..a5099c4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ endif() project(libsession-util - VERSION 1.1.0 + VERSION 1.2.0 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) From 577feacbfe4a523c5dc743335e187bf11062120c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Oct 2023 20:08:17 -0300 Subject: [PATCH 102/572] Format --- include/session/config/base.hpp | 6 ++++-- src/config/base.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 27cb2a14..eba4231b 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -776,7 +776,8 @@ class ConfigBase : public ConfigSig { /// 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(const std::vector>& configs); + std::vector _merge( + const std::vector>& configs); /// API: base/ConfigBase::extra_data /// @@ -900,7 +901,8 @@ class ConfigBase : public ConfigSig { // Same as above, but passes the ustring_views to the overload of protos::handle_incoming that // takes ustring_views - std::vector merge(const std::vector>& configs); + std::vector merge( + const std::vector>& configs); /// API: base/ConfigBase::is_dirty /// diff --git a/src/config/base.cpp b/src/config/base.cpp index 5c0fccc4..ae44ffcc 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -62,7 +62,8 @@ std::vector ConfigBase::merge( return merge(config_views); } -std::vector ConfigBase::merge(const std::vector>& configs) { +std::vector ConfigBase::merge( + const std::vector>& configs) { if (accepts_protobuf() && !_keys.empty()) { std::list keep_alive; std::vector> parsed; @@ -86,7 +87,8 @@ std::vector ConfigBase::merge(const std::vector ConfigBase::_merge(const std::vector>& configs) { +std::vector ConfigBase::_merge( + const std::vector>& configs) { if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; From 39b7761a8226299c2bec7ad90babfe21ceff5969 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 5 Oct 2023 22:57:14 -0300 Subject: [PATCH 103/572] Return good hashes from merge() Session clients required the list of hashes from a merge() to perform various operations based on the messages that were accepted. This updates `merge()` to return a vector of accepted hashes. For the C API, this returns a config_string_list* pointer that needs to be freed, but contains all the good hashes (the same way current_hashes() works). --- include/session/config/base.hpp | 38 ++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index eba4231b..138ba51f 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -883,14 +883,37 @@ class ConfigBase : public ConfigSig { /// API: base/ConfigBase::merge /// - /// Wrapper around ConfigBase::_merge to handle protobuf parsing. Currently, in the transition - /// from legacy to new config groups, legacy groups using protobuf must be compatible with - /// new config groups handling raw binary I/O. As a result, this method checks the - /// accepts_protobuf() override for the given class, and either passes the parsed protobuf or - /// the original binary data to _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 + /// - `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 @@ -899,8 +922,7 @@ class ConfigBase : public ConfigSig { /// to be included). The hashes will be in the same order as in the input vector. std::vector merge(const std::vector>& configs); - // Same as above, but passes the ustring_views to the overload of protos::handle_incoming that - // takes ustring_views + // Same as above, but takes values as ustrings (because sometimes that is more convenient). std::vector merge( const std::vector>& configs); From 7f0bcc0bc9a7351a729ee66182e1237b55160cd8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Oct 2023 20:31:41 -0300 Subject: [PATCH 104/572] Properly free config_merge results in C API usage --- tests/test_group_keys.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index e9cb8759..05978fc7 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -679,10 +679,15 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { get_timestamp_ms(), a.info, a.members)); - REQUIRE(config_merge(a.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); + 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"); - REQUIRE(config_merge(a.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); + hashes = config_merge(a.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1); + REQUIRE(hashes->len); + free(hashes); config_confirm_pushed(a.members, new_mem_config1->seqno, "fakehash1"); REQUIRE(groups_members_size(a.members) == 1); @@ -702,8 +707,11 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { get_timestamp_ms(), m.info, m.members)); - REQUIRE_THROWS(config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); - REQUIRE_THROWS(config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); + 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); } @@ -754,10 +762,14 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { get_timestamp_ms(), a.info, a.members)); - REQUIRE(config_merge(a.info, merge_hash2, &merge_data2[0], &merge_size2[0], 1)); + 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"); - - REQUIRE(config_merge(a.members, merge_hash2, &merge_data2[1], &merge_size2[1], 1)); + hashes = config_merge(a.members, merge_hash2, &merge_data2[1], &merge_size2[1], 1); + REQUIRE(hashes->len); + free(hashes); config_confirm_pushed(a.members, new_mem_config2->seqno, "fakehash2"); REQUIRE(groups_members_size(a.members) == 5); From 0c0c5ad5b13ad06a507b73f1e8f8dfe18b24df15 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 19 Oct 2023 11:25:54 +1100 Subject: [PATCH 105/572] Fixed incorrect lengths in subaccount_sign_binary --- src/config/groups/keys.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index d92e4af6..72b427a7 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1668,8 +1668,8 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( assert(auth.subaccount_sig.size() == 64); assert(auth.signature.size() == 64); std::memcpy(subaccount, auth.subaccount.data(), 36); - std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 36); - std::memcpy(signature, auth.signature.data(), 36); + std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 64); + std::memcpy(signature, auth.signature.data(), 64); return true; } catch (const std::exception& e) { set_error(conf, e.what()); From cad07b2aa81b6bb146ac0406849b5efa47dd6ad5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 20 Oct 2023 13:03:52 +1100 Subject: [PATCH 106/572] Fixed an issue where the auth_data wasn't getting cleared correctly --- src/config/user_groups.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 4119d322..9c83b78f 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -432,6 +432,8 @@ void UserGroups::set(const group_info& g) { info["K"] = ustring_view{}; if (g.auth_data.size() == 100) info["s"] = g.auth_data; + else + info["s"] = ustring_view{}; } } From 832bbf53f7cf2fb35aa04e916284053a01109312 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 20 Oct 2023 14:02:38 +1100 Subject: [PATCH 107/572] Added supplement flag to indicate invited member key rotation type --- include/session/config/groups/members.h | 5 +++-- include/session/config/groups/members.hpp | 20 +++++++++++++++++--- src/config/groups/members.cpp | 4 ++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index fa9ad181..3baa4151 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -18,8 +18,9 @@ typedef struct config_group_member { user_profile_pic profile_pic; bool admin; - int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send - int promoted; // same value as `invited`, but for promotion-to-admin + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int promoted; // same value as `invited`, but for promotion-to-admin + bool supplement; } config_group_member; diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 34a06eb3..10cd3696 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -83,6 +83,17 @@ struct member { /// admin. bool admin = 0; + /// API: groups/member::supplement + /// + /// Member variable + /// + /// Flag that is set to indicate to the group that this member was added with a supplemental key + /// rotation so that other admins can trigger the same key rotation method if they send a new + /// invitation to the same member. + /// + /// Note that this should be cleared when a member accepts an invitation. + bool supplement = 0; + // Flags to track an invited user. This value is typically not used directly, but rather via // the `set_invited()`, `invite_pending()` and similar methods. int invite_status = 0; @@ -101,11 +112,14 @@ struct member { /// API: groups/members::set_accepted /// - /// This clears the "invited" flag for this user, thus indicating that the user has accepted an - /// invitation and is now a regular member of the group. + /// This clears the "invited" and "supplement" flags for this user, thus indicating that the user + /// has accepted an invitation and is now a regular member of the group. /// /// Inputs: none - void set_accepted() { invite_status = 0; } + void set_accepted() { + invite_status = 0; + supplement = 0; + } /// API: groups/member::invite_pending /// diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 18b4ece1..becbc074 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -51,6 +51,7 @@ void Members::set(const member& mem) { set_flag(info["A"], mem.admin); set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); + set_flag(info["s"], mem.supplement); } void member::load(const dict& info_dict) { @@ -68,6 +69,7 @@ void member::load(const dict& info_dict) { admin = maybe_int(info_dict, "A").value_or(0); invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); + supplement = maybe_int(info_dict, "s").value_or(0); } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -139,6 +141,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { admin = m.admin; invite_status = (m.invited == INVITE_SENT || m.invited == INVITE_FAILED) ? m.invited : 0; promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED) ? m.promoted : 0; + supplement = m.supplement; } void member::into(config_group_member& m) const { @@ -155,6 +158,7 @@ void member::into(config_group_member& m) const { static_assert(groups::INVITE_FAILED == ::INVITE_FAILED); m.invited = invite_status; m.promoted = promotion_status; + m.supplement = supplement; } void member::set_name(std::string n) { From 0ddfb000ed5d51b25e6fb601402b323a691e33c2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Oct 2023 11:13:30 -0300 Subject: [PATCH 108/572] Erase instead of clear when no supplemental data --- src/config/user_groups.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 9c83b78f..9b08aa0c 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -433,7 +433,7 @@ void UserGroups::set(const group_info& g) { if (g.auth_data.size() == 100) info["s"] = g.auth_data; else - info["s"] = ustring_view{}; + info["s"].erase(); } } From 6a7d18f5fd31aee6276a2236c77355d55f82f78a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Oct 2023 11:29:06 -0300 Subject: [PATCH 109/572] Add field comment; fix bool literals; reformat - Added a description for the new supplement field - Some `bool` values were being set to 0 instead of `false` (this doesn't change anything; `false` is just a bit more obvious). - fix lint failure by running ./utils/format.sh --- include/session/config/groups/members.h | 4 ++-- include/session/config/groups/members.hpp | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 3baa4151..78fe9ea9 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -18,8 +18,8 @@ typedef struct config_group_member { user_profile_pic profile_pic; bool admin; - int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send - int promoted; // same value as `invited`, but for promotion-to-admin + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int promoted; // same value as `invited`, but for promotion-to-admin bool supplement; } config_group_member; diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 10cd3696..c90b8284 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -27,6 +27,11 @@ using namespace std::literals; /// - 2 if an invite was created but failed to send for some reason (and thus can be resent) /// - omitted once an invite is accepted. (This also gets omitted if the `A` admin flag gets /// set). +/// s - invite supplemental keys flag (only set when `I` is set): if set (to 1) then this invite +/// was issued with the intention of sending the user the existing active decryption keys +/// (allowing them to access current messages); if omitted (with `I` set) then the invitation +/// was not meant to give access to past configs/messages (and was presumably issued with a +/// group rekey). /// A - flag set to 1 if the member is an admin, omitted otherwise. /// P - promotion (to admin) status; this will be one of: /// - 1 if a promotion has been sent. @@ -81,7 +86,7 @@ struct member { /// /// See also `promoted()` if you want to check for either an admin or someone being promoted to /// admin. - bool admin = 0; + bool admin = false; /// API: groups/member::supplement /// @@ -92,7 +97,7 @@ struct member { /// invitation to the same member. /// /// Note that this should be cleared when a member accepts an invitation. - bool supplement = 0; + bool supplement = false; // Flags to track an invited user. This value is typically not used directly, but rather via // the `set_invited()`, `invite_pending()` and similar methods. @@ -112,13 +117,13 @@ struct member { /// API: groups/members::set_accepted /// - /// This clears the "invited" and "supplement" flags for this user, thus indicating that the user - /// has accepted an invitation and is now a regular member of the group. + /// This clears the "invited" and "supplement" flags for this user, thus indicating that the + /// user has accepted an invitation and is now a regular member of the group. /// /// Inputs: none void set_accepted() { invite_status = 0; - supplement = 0; + supplement = false; } /// API: groups/member::invite_pending From 156c997c94270e214ef74bfabcc34fe11f001b7c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Oct 2023 21:13:22 -0300 Subject: [PATCH 110/572] Add description to group info --- include/session/config/groups/info.h | 36 +++++++++++++++++ include/session/config/groups/info.hpp | 35 +++++++++++++--- src/config/groups/info.cpp | 56 ++++++++++++++++++++++++++ tests/test_group_keys.cpp | 16 +++++++- 4 files changed, 137 insertions(+), 6 deletions(-) diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 1efc4a75..32da3ae9 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -8,6 +8,9 @@ extern "C" { #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/groups_info_init /// /// Constructs a group info config object and sets a pointer to it in `conf`. @@ -56,6 +59,9 @@ LIBSESSION_EXPORT const char* groups_info_get_name(const config_object* conf); /// 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). /// +/// If the given name is longer than GROUP_INFO_NAME_MAX_LENGTH (100) bytes then it will be +/// truncated. +/// /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `name` -- [in] Pointer to the name as a null-terminated C string @@ -64,6 +70,36 @@ LIBSESSION_EXPORT const char* groups_info_get_name(const config_object* conf); /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_EXPORT int groups_info_set_name(config_object* conf, const char* name); +/// 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_EXPORT const char* groups_info_get_description(const config_object* conf); + +/// 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). +/// +/// If the given description is longer than GROUP_INFO_DESCRIPTION_MAX_LENGTH (2000) bytes then it +/// will be truncated. +/// +/// 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_EXPORT int groups_info_set_description(config_object* conf, const char* description); + /// API: groups_info/groups_info_get_pic /// /// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 9ebdfd7d..4010bf35 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -21,13 +21,19 @@ using namespace std::literals; /// D - delete attachments before - same as above, but specific to attachments. /// E - disappearing message timer (seconds) if the delete-after-send disappearing messages mode is /// enabled for the group. Omitted if disappearing messages is disabled. -/// n - utf8 group name (human-readable) +/// n - utf8 group name (human-readable); may not contain nulls, max length 100. +/// o - utf8 group description (human-readable); may not contain nulls, max length 2000. /// p - group profile url /// q - group profile decryption key (binary) class Info final : public ConfigBase { public: + /// Limits for the name & description strings, in bytes. If longer, we truncate to these + /// lengths: + static constexpr size_t NAME_MAX_LENGTH = 100; // same as base_group_info::NAME_MAX_LENGTH + static constexpr size_t DESCRIPTION_MAX_LENGTH = 2000; + // No default constructor Info() = delete; @@ -98,15 +104,34 @@ class Info final : public ConfigBase { /// /// Sets the group name; if given an empty string then the name is removed. /// - /// Declaration: - /// ```cpp - /// void set_name(std::string_view new_name); - /// ``` + /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes it will be truncated. /// /// Inputs: /// - `new_name` -- The name to be put into the group Info void set_name(std::string_view new_name); + /// API: groups/Info::get_description + /// + /// Returns the group description, or std::nullopt if there is no group description set. + /// + /// If given a description longer than `Info::DESCRIPTION_MAX_LENGTH` (2000) bytes it will be + /// truncated. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::optional` - Returns the group description if it is set + std::optional get_description() const; + + /// API: groups/Info::set_description + /// + /// Sets the optional group description; if given an empty string then an existing description + /// is removed. + /// + /// Inputs: + /// - `new_desc` -- The new description to be put into the group Info + void set_description(std::string_view new_desc); + /// API: groups/Info::get_profile_pic /// /// Gets the group's current profile pic URL and decryption key. The returned object will diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index be29e581..88b3a1eb 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -30,9 +30,23 @@ std::optional Info::get_name() const { } void Info::set_name(std::string_view new_name) { + if (new_name.size() > NAME_MAX_LENGTH) + new_name = new_name.substr(0, NAME_MAX_LENGTH); set_nonempty_str(data["n"], new_name); } +std::optional Info::get_description() const { + if (auto* s = data["o"].string(); s && !s->empty()) + return *s; + return std::nullopt; +} + +void Info::set_description(std::string_view new_desc) { + if (new_desc.size() > DESCRIPTION_MAX_LENGTH) + new_desc = new_desc.substr(0, DESCRIPTION_MAX_LENGTH); + set_nonempty_str(data["o"], new_desc); +} + profile_pic Info::get_profile_pic() const { profile_pic pic{}; if (auto* url = data["p"].string(); url && !url->empty()) @@ -105,6 +119,10 @@ bool Info::is_destroyed() const { using namespace session; 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, @@ -153,6 +171,44 @@ LIBSESSION_C_API int groups_info_set_name(config_object* conf, const char* name) 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) { + try { + unbox(conf)->set_description(description); + } catch (const std::exception& e) { + return set_error(conf, SESSION_ERR_BAD_VALUE, e); + } + 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 diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 05978fc7..c051e1f8 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -238,6 +238,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // change group info, re-key, distribute admin1.info.set_name("tomatosauce"s); + admin1.info.set_description("this is where you go to play in the tomato sauce, I guess"); CHECK(admin1.info.needs_push()); @@ -258,6 +259,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::vector{{"fakehash3"s}}); CHECK(a.members.merge(mem_configs) == std::vector{{"fakehash3"s}}); CHECK(a.info.get_name() == "tomatosauce"s); + CHECK(a.info.get_description() == + "this is where you go to play in the tomato sauce, I guess"s); CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s}}); } @@ -268,6 +271,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.info.merge(info_configs) == std::vector{{"fakehash3"s}}); CHECK(m.members.merge(mem_configs) == std::vector{{"fakehash3"s}}); CHECK(m.info.get_name() == "tomatosauce"s); + CHECK(m.info.get_description() == + "this is where you go to play in the tomato sauce, I guess"s); CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}}); } @@ -532,12 +537,21 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { admin1.info.dump(), admin1.members.dump(), admin1.keys.dump()}; - admin1b.info.set_name("Test New Name"); + admin1b.info.set_name( + "Test New Name Really long " + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); + admin1b.info.set_description(std::string(2050, 'z')); CHECK_NOTHROW(admin1b.info.push()); admin1b.members.set( admin1b.members.get_or_construct("05124076571076017981235497801235098712093870981273590" "8746387172343")); CHECK_NOTHROW(admin1b.members.push()); + + // Test truncation + CHECK(admin1b.info.get_name() == + "Test New Name Really long " + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv"); + CHECK(admin1b.info.get_description() == std::string(2000, 'z')); } TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { From 61fd7c60ef3573784cf5afbee5e3d5e4cb6908f1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Oct 2023 11:42:36 -0300 Subject: [PATCH 111/572] Add supplement tests; clear supplement flag for admins - Remove the supplement flag when we have an admin (or pending promotion to admin): such a user will be able to extract all the encryption keys from pending keys configs once they have the full admin key, and so don't need a supplement. - Modify group members tests to check the supplement flag is working as expected. - Set the .supplement flag in the keys test where we send supplemental keys (this doesn't actually do anything, but better demonstrates the intended usage). --- src/config/groups/members.cpp | 2 +- tests/test_group_keys.cpp | 1 + tests/test_group_members.cpp | 9 ++++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index becbc074..2dce7d5e 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -69,7 +69,7 @@ void member::load(const dict& info_dict) { admin = maybe_int(info_dict, "A").value_or(0); invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); - supplement = maybe_int(info_dict, "s").value_or(0); + supplement = invite_pending() && !promoted() ? maybe_int(info_dict, "s").value_or(0) : 0; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 05978fc7..2638d5f1 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -351,6 +351,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { auto memb = admin1.members.get_or_construct(m.session_id); memb.set_invited(); + memb.supplement = true; memb.name = i == 0 ? "fred" : "JOHN"; admin1.members.set(memb); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 242c148d..1e1e98e7 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -119,6 +119,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.invite_failed()); CHECK_FALSE(m.promotion_pending()); CHECK_FALSE(m.promotion_failed()); + CHECK_FALSE(m.supplement); if (i < 10) { CHECK(m.admin); CHECK(m.name == "Admin " + std::to_string(i)); @@ -147,12 +148,16 @@ TEST_CASE("Group Members", "[config][groups][members]") { } for (int i = 50; i < 55; i++) { auto m = gmem2.get_or_construct(sids[i]); - m.set_invited(); + m.set_invited(); // failed invite + if (i % 2) + m.supplement = true; gmem2.set(m); } for (int i = 55; i < 58; i++) { auto m = gmem2.get_or_construct(sids[i]); m.set_invited(true); + if (i % 2) + m.supplement = true; gmem2.set(m); } for (int i = 58; i < 62; i++) { @@ -187,6 +192,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "http://example.com/" + std::to_string(i) : "")); CHECK(m.invite_pending() == (50 <= i && i < 58)); CHECK(m.invite_failed() == (55 <= i && i < 58)); + CHECK(m.supplement == (i % 2 && 50 < i && i < 58)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); CHECK(m.promotion_pending() == (i >= 58 && i < 62)); CHECK(m.promotion_failed() == (i >= 60 && i < 62)); @@ -237,6 +243,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "http://example.com/" + std::to_string(i) : "")); CHECK(m.invite_pending() == (55 <= i && i < 58)); CHECK(m.invite_failed() == (i == 57)); + CHECK(m.supplement == (i == 55 || i == 57)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); CHECK(m.promotion_pending() == (i >= 59 && i <= 61)); CHECK(m.promotion_failed() == (i >= 60 && i <= 61)); From 6c709c441201f596420b7621a9239a6ba6cff25e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Oct 2023 19:03:43 -0300 Subject: [PATCH 112/572] Switch to oxen-encoding's signature helpers This removes some fragile (though working) bt-dict signature code with equivalent code now in oxen-encoding. --- external/CMakeLists.txt | 2 +- external/oxen-encoding | 2 +- include/session/config.hpp | 7 +-- include/session/config/groups/keys.hpp | 2 + src/config.cpp | 61 +++++++++----------------- src/config/groups/keys.cpp | 35 ++++++--------- 6 files changed, 42 insertions(+), 67 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 9379d4bd..a99ec4d5 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -103,7 +103,7 @@ if(CMAKE_CROSSCOMPILING) endif() -system_or_submodule(OXENC oxenc liboxenc>=1.0.8 oxen-encoding) +system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) if(CMAKE_C_COMPILER_LAUNCHER) diff --git a/external/oxen-encoding b/external/oxen-encoding index 867d0797..a7de6375 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit 867d0797a08361eee613b91060a2ef447d2f9f4d +Subproject commit a7de63756dcc5c31cb899a4b810e6434b1a7c01c diff --git a/include/session/config.hpp b/include/session/config.hpp index 170019c0..1c6cddb4 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -371,9 +371,10 @@ class MutableConfigMessage : public ConfigMessage { /// - `verified_signature` is a pointer to a std::optional array of signature data; if this is /// specified and not nullptr then the optional with be emplaced with the signature bytes if the /// signature successfully validates. -/// - `trust_signature` bypasses the verification and signature requirements, blinding trusting a -/// signature if present. This is intended for use when restoring from a dump (along with a -/// nullptr verifier). +/// - `trust_signature` allows setting `verified_signature` when a signature is present but no +/// verifier is provided to verify it. Without specifying this, `verified_signature` will only be +/// set when the signature actually validates via a `verifier` call. This is primarily used +/// during restoring config dumps. /// /// Outputs: /// - returns with no value on success diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index edf0e220..54aa1551 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -109,6 +109,8 @@ class Keys final : public ConfigSig { void set_verifier(ConfigMessage::verify_callable v) override { verifier_ = std::move(v); } void set_signer(ConfigMessage::sign_callable s) override { signer_ = std::move(s); } + ustring sign(ustring_view data) const; + // Checks for and drops expired keys. void remove_expired(); diff --git a/src/config.cpp b/src/config.cpp index 99957bad..8073256b 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -436,41 +436,24 @@ void verify_config_sig( std::optional>* verified_signature, bool trust_signature) { ustring_view to_verify, sig; - dict.skip_until("~"); - if (!dict.is_finished() && dict.key() == "~") { - // We get the key string_view here because it points into the buffer that we need. - // Currently it will be pointing at the "~", i.e.: - // - // [...previousdata...]1:~64:[sigdata] - // ^-- here - // - // but what we need is the data up to the end of `]`, so we subtract 2 off that to - // figure out the range of the full serialized data that should have been signed: - - auto key = dict.key(); - assert(to_unsigned(key.data()) > config_msg.data() && - to_unsigned(key.data()) < config_msg.data() + config_msg.size()); - to_verify = config_msg.substr(0, to_unsigned(key.data()) - config_msg.data() - 2); - sig = to_unsigned_sv(dict.consume_string_view()); + if (dict.skip_until("~")) { + dict.consume_signature([&](ustring_view to_verify, ustring_view sig) { + if (sig.size() != 64) + throw signature_error{"Config signature is invalid (not 64B)"}; + if (verifier && !verifier(to_verify, sig)) + throw signature_error{"Config signature failed verification"}; + if (verified_signature && (verifier || trust_signature)) { + if (!*verified_signature) + verified_signature->emplace(); + std::memcpy((*verified_signature)->data(), sig.data(), 64); + } + }); + } else if (verifier) { + throw missing_signature{"Config signature is missing"}; } if (!dict.is_finished()) throw config_parse_error{"Invalid config: dict has invalid key(s) after \"~\""}; - - if (verifier || trust_signature) { - if (sig.empty()) { - if (!trust_signature) - throw missing_signature{"Config signature is missing"}; - } else if (sig.size() != 64) - throw signature_error{"Config signature is invalid (not 64B)"}; - else if (verifier && !verifier(to_verify, sig)) - throw signature_error{"Config signature failed verification"}; - else if (verified_signature) { - if (!*verified_signature) - verified_signature->emplace(); - std::memcpy((*verified_signature)->data(), sig.data(), 64); - } - } } bool MutableConfigMessage::prune() { @@ -792,15 +775,13 @@ ustring ConfigMessage::serialize_impl(const oxenc::bt_dict& curr_diff, bool enab reinterpret_cast(verified_signature_->data()), verified_signature_->size()}); } else if (signer && enable_signing) { - auto to_sign = to_unsigned_sv(outer.view()); - // The view contains the trailing "e", but we don't sign it (we are going to append the - // signature there instead): - to_sign.remove_suffix(1); - auto sig = signer(to_sign); - if (sig.size() != 64) - throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; - - outer.append("~", from_unsigned_sv(sig)); + outer.append_signature("~", [this](ustring_view to_sign) { + auto sig = signer(to_sign); + if (sig.size() != 64) + throw std::logic_error{ + "Invalid signature: signing function did not return 64 bytes"}; + return sig; + }); } return ustring{to_unsigned_sv(outer.view())}; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 72b427a7..f35ccfef 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -371,17 +371,9 @@ ustring_view Keys::rekey(Info& info, Members& members) { } } - // Finally we sign the message at put it as the ~ key (which is 0x7f, and thus comes later than - // any other ascii key). - auto to_sign = to_unsigned_sv(d.view()); - // The view contains the trailing "e", but we don't sign it (we are going to append the - // signature there instead): - to_sign.remove_suffix(1); - auto sig = signer_(to_sign); - if (sig.size() != 64) - throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; - - d.append("~", from_unsigned_sv(sig)); + // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than + // any other printable ascii key). + d.append_signature("~", [this](ustring_view to_sign) { return sign(to_sign); }); // Load this key/config/gen into our pending variables pending_gen_ = gen; @@ -402,6 +394,13 @@ ustring_view Keys::rekey(Info& info, Members& members) { return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; } +ustring Keys::sign(ustring_view data) const { + auto sig = signer_(data); + if (sig.size() != 64) + throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; + return sig; +} + ustring Keys::key_supplement(const std::vector& sids) const { if (!admin()) throw std::logic_error{ @@ -522,17 +521,9 @@ ustring Keys::key_supplement(const std::vector& sids) const { d.append("G", keys_.back().generation); - // Finally we sign the message at put it as the ~ key (which is 0x7f, and thus comes later than - // any other ascii key). - auto to_sign = to_unsigned_sv(d.view()); - // The view contains the trailing "e", but we don't sign it (we are going to append the - // signature there instead): - to_sign.remove_suffix(1); - auto sig = signer_(to_sign); - if (sig.size() != 64) - throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; - - d.append("~", from_unsigned_sv(sig)); + // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than + // any other printable ascii key). + d.append_signature("~", [this](ustring_view to_sign) { return sign(to_sign); }); return ustring{to_unsigned_sv(d.view())}; } From 01cd5da699246c627cc1e4527ff3fffbf6003ea1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 3 Nov 2023 18:22:42 +1100 Subject: [PATCH 113/572] Added the `removed_status` value --- include/session/config/groups/members.h | 2 ++ include/session/config/groups/members.hpp | 40 +++++++++++++++++++++++ src/config/groups/members.cpp | 4 +++ tests/test_group_members.cpp | 13 +++++++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 78fe9ea9..7396cc36 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -9,6 +9,7 @@ extern "C" { #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 { char session_id[67]; // in hex; 66 hex chars + null terminator. @@ -20,6 +21,7 @@ typedef struct config_group_member { bool admin; int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send int promoted; // same value as `invited`, but for promotion-to-admin + int removed; // 0 == unset, REMOVED_MEMBER = removed, REMOVED_MEMBER_AND_MESSAGES = remove member and their messages bool supplement; } config_group_member; diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index c90b8284..9a6dd4c5 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -40,6 +40,7 @@ using namespace std::literals; /// - omitted once the promotion is accepted (i.e. once `A` gets set). constexpr int INVITE_SENT = 1, INVITE_FAILED = 2; +constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; /// Struct containing member details struct member { @@ -200,6 +201,45 @@ struct member { /// - `bool` -- true if the member is promoted (or promotion-in-progress) bool promoted() const { return admin || promotion_pending(); } + // Flags to track a removed user. This value is typically not used directly, but + // rather via the `set_removed()`, `is_removed()` and similar methods. + int removed_status = 0; + + /// API: groups/member::set_removed + /// + /// Sets the "removed" flag for this user. This marks the user as pending removal from the + /// group. The optional `messages` parameter can be specified as true if we want to remove + /// any messages sent by the member upon a successful removal. + /// + /// Inputs: + /// - `messages`: can be specified as true to indicate any messages sent by the member + /// should also be removed upon a successful member removal. + void set_removed(bool messages = false) { + removed_status = messages ? REMOVED_MEMBER_AND_MESSAGES : REMOVED_MEMBER; + } + + /// API: groups/member::is_removed + /// + /// Returns true if the user should be removed from the group. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `bool` -- true if the member should be removed from the group + bool is_removed() const { return removed_status > 0; } + + /// API: groups/member::should_remove_messages + /// + /// Returns true if the users messages should be removed after they are + /// successfully removed. + /// + /// Inputs: none. + /// + /// Outputs: + /// - `bool` -- true if the members messages should be removed after they are + /// successfully removed from the group + bool should_remove_messages() const { return removed_status == REMOVED_MEMBER_AND_MESSAGES; } + /// API: groups/member::into /// /// Converts the member info into a C struct. diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 2dce7d5e..36df0770 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -52,6 +52,7 @@ void Members::set(const member& mem) { set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); set_flag(info["s"], mem.supplement); + set_flag(info["R"], mem.removed_status); } void member::load(const dict& info_dict) { @@ -69,6 +70,7 @@ void member::load(const dict& info_dict) { admin = maybe_int(info_dict, "A").value_or(0); invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); + removed_status = maybe_int(info_dict, "R").value_or(0); supplement = invite_pending() && !promoted() ? maybe_int(info_dict, "s").value_or(0) : 0; } @@ -141,6 +143,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { admin = m.admin; invite_status = (m.invited == INVITE_SENT || m.invited == INVITE_FAILED) ? m.invited : 0; promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED) ? m.promoted : 0; + removed_status = (m.removed == REMOVED_MEMBER || m.removed == REMOVED_MEMBER_AND_MESSAGES) ? m.removed : 0; supplement = m.supplement; } @@ -158,6 +161,7 @@ void member::into(config_group_member& m) const { static_assert(groups::INVITE_FAILED == ::INVITE_FAILED); m.invited = invite_status; m.promoted = promotion_status; + m.removed = removed_status; m.supplement = supplement; } diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 1e1e98e7..c30149f8 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -119,6 +119,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.invite_failed()); CHECK_FALSE(m.promotion_pending()); CHECK_FALSE(m.promotion_failed()); + CHECK_FALSE(m.is_removed()); + CHECK_FALSE(m.should_remove_messages()); CHECK_FALSE(m.supplement); if (i < 10) { CHECK(m.admin); @@ -165,6 +167,11 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.set_promoted(i >= 60); gmem2.set(m); } + for (int i = 62; i < 66; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.set_removed(i >= 64); + gmem2.set(m); + } CHECK(gmem2.get(sids[23]).value().name == "Member 23"); @@ -196,6 +203,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); CHECK(m.promotion_pending() == (i >= 58 && i < 62)); CHECK(m.promotion_failed() == (i >= 60 && i < 62)); + CHECK(m.is_removed() == (i >= 62 && i < 66)); + CHECK(m.should_remove_messages() == (i >= 64 && i < 66)); i++; } CHECK(i == 62); @@ -247,10 +256,12 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); CHECK(m.promotion_pending() == (i >= 59 && i <= 61)); CHECK(m.promotion_failed() == (i >= 60 && i <= 61)); + CHECK(m.is_removed() == (i >= 62 && i < 66)); + CHECK(m.should_remove_messages() == (i >= 64 && i < 66)); do i++; while (is_prime100(i)); } - CHECK(i == 62); + CHECK(i == 66); } } From adfc73af7d04fe960668775846c7021810fecd45 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 6 Nov 2023 18:31:41 +1100 Subject: [PATCH 114/572] Fixed an value assignment --- src/config/groups/members.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 36df0770..b8079731 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -52,7 +52,7 @@ void Members::set(const member& mem) { set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); set_flag(info["s"], mem.supplement); - set_flag(info["R"], mem.removed_status); + set_positive_int(info["R"], mem.removed_status); } void member::load(const dict& info_dict) { From 6e61a545ef0b49e2f8206c1302eef6891e48ced5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 Nov 2023 15:11:45 +1100 Subject: [PATCH 115/572] Added a function to retrieve the current GROUP_KEYS generation --- include/session/config/groups/keys.h | 12 ++++++++++++ include/session/config/groups/keys.hpp | 8 ++++++++ src/config/groups/keys.cpp | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index bbe79761..0410c49a 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -292,6 +292,18 @@ LIBSESSION_EXPORT bool groups_keys_key_supplement( unsigned char** message, size_t* message_len); +/// API: groups/groups_keys_current_generation +/// +/// Returns the current generation number for the latest keys message. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Oututs: +/// - `int` -- latest keys generation number on success; returns `-1` on failure. +LIBSESSION_EXPORT int groups_keys_current_generation( + config_group_keys* conf); + /// API: groups/groups_keys_swarm_make_subaccount /// /// Constructs a swarm subaccount signing value that a member can use to access messages in the diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index edf0e220..d845a125 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -320,6 +320,14 @@ class Keys final : public ConfigSig { return key_supplement(std::vector{{std::move(sid)}}); } + /// API: groups/current_generation + /// + /// Returns the current generation number for the latest keys message. + /// + /// Oututs: + /// - `int` -- latest keys generation number. + int current_generation() const { return keys_.empty() ? 0 : keys_.back().generation; } + /// API: groups/Keys::swarm_make_subaccount /// /// Constructs a swarm subaccount signing value that a member can use to access messages in the diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 72b427a7..ca60f4b5 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1574,6 +1574,16 @@ LIBSESSION_C_API bool groups_keys_key_supplement( } } +LIBSESSION_EXPORT int groups_keys_current_generation( + config_group_keys* conf) { + try { + return unbox(conf).current_generation(); + } catch (const std::exception& e) { + set_error(conf, e.what()); + return -1; + } +} + LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( config_group_keys* conf, const char* session_id, From ae6711117d57c66a9d56275f36f6046b46c45a32 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 15 Nov 2023 10:15:36 +1100 Subject: [PATCH 116/572] Fixed a broken test --- tests/test_group_members.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index c30149f8..747b2adb 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -207,7 +207,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(m.should_remove_messages() == (i >= 64 && i < 66)); i++; } - CHECK(i == 62); + CHECK(i == 66); } for (int i = 0; i < 100; i++) { From 345f34683fff8dd4007d936239f1d5cfa932a002 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 23:33:14 -0400 Subject: [PATCH 117/572] Formatting --- include/session/config/groups/keys.h | 3 +-- include/session/config/groups/members.h | 3 ++- src/config/groups/keys.cpp | 3 +-- src/config/groups/members.cpp | 4 +++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 0410c49a..bd2dbac2 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -301,8 +301,7 @@ LIBSESSION_EXPORT bool groups_keys_key_supplement( /// /// Oututs: /// - `int` -- latest keys generation number on success; returns `-1` on failure. -LIBSESSION_EXPORT int groups_keys_current_generation( - config_group_keys* conf); +LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); /// API: groups/groups_keys_swarm_make_subaccount /// diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 7396cc36..a4624b7d 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -21,7 +21,8 @@ typedef struct config_group_member { bool admin; int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send int promoted; // same value as `invited`, but for promotion-to-admin - int removed; // 0 == unset, REMOVED_MEMBER = removed, REMOVED_MEMBER_AND_MESSAGES = remove member and their messages + int removed; // 0 == unset, REMOVED_MEMBER = removed, REMOVED_MEMBER_AND_MESSAGES = remove + // member and their messages bool supplement; } config_group_member; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index ca60f4b5..3e50002c 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1574,8 +1574,7 @@ LIBSESSION_C_API bool groups_keys_key_supplement( } } -LIBSESSION_EXPORT int groups_keys_current_generation( - config_group_keys* conf) { +LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { try { return unbox(conf).current_generation(); } catch (const std::exception& e) { diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index b8079731..8db53d9b 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -143,7 +143,9 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { admin = m.admin; invite_status = (m.invited == INVITE_SENT || m.invited == INVITE_FAILED) ? m.invited : 0; promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED) ? m.promoted : 0; - removed_status = (m.removed == REMOVED_MEMBER || m.removed == REMOVED_MEMBER_AND_MESSAGES) ? m.removed : 0; + removed_status = (m.removed == REMOVED_MEMBER || m.removed == REMOVED_MEMBER_AND_MESSAGES) + ? m.removed + : 0; supplement = m.supplement; } From 61c792bfe6e448cfae184c55e4fcb4483d9d5359 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Nov 2023 12:40:33 -0400 Subject: [PATCH 118/572] Remove unnecessary try/catch None of the calls involved here could actually throw. --- include/session/config/groups/keys.h | 2 +- src/config/groups/keys.cpp | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index bd2dbac2..7e309726 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -300,7 +300,7 @@ LIBSESSION_EXPORT bool groups_keys_key_supplement( /// - `conf` -- [in] Pointer to the config object /// /// Oututs: -/// - `int` -- latest keys generation number on success; returns `-1` on failure. +/// - `int` -- latest keys generation number LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); /// API: groups/groups_keys_swarm_make_subaccount diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 3e50002c..0dbeeaaa 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1575,12 +1575,7 @@ LIBSESSION_C_API bool groups_keys_key_supplement( } LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { - try { - return unbox(conf).current_generation(); - } catch (const std::exception& e) { - set_error(conf, e.what()); - return -1; - } + return unbox(conf).current_generation(); } LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( From f3af485de105b34543fe2872a102c22ac9bae596 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 15 Nov 2023 22:16:17 -0400 Subject: [PATCH 119/572] Add group keys function for key promotion There was no nice way to "promote" a set of group objects from member status to admin status by giving them the key. This adds Keys::load_admin_key to do just that. --- include/session/config/groups/keys.h | 23 +++++++ include/session/config/groups/keys.hpp | 23 +++++++ src/config/groups/keys.cpp | 41 ++++++++++++ tests/test_group_keys.cpp | 90 ++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index bbe79761..9229c6cd 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -120,6 +120,29 @@ LIBSESSION_EXPORT const unsigned char* groups_keys_get_key(const config_group_ke /// - `true` if we have admin keys, `false` otherwise. LIBSESSION_EXPORT bool groups_keys_is_admin(const config_group_keys* conf); +/// API: groups/groups_keys_load_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 +/// - `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); + /// API: groups/groups_keys_rekey /// /// Generates a new encryption key for the group and returns an encrypted key message to be pushed diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 54aa1551..baba1a02 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -258,6 +258,29 @@ class Keys final : public ConfigSig { /// - `true` if this object knows the group's master key bool admin() const { return _sign_sk && _sign_pk; } + /// API: groups/Keys::load_admin_key + /// + /// Loads the group secret key into the Keys object (as well as passing it along to the Info and + /// Members objects). + /// + /// The primary use of this is when accepting a promotion-to-admin: the Keys object would be + /// constructed as a regular member (without the admin key) then this method "upgrades" the + /// object with the group signing key. + /// + /// This will do nothing if the secret key is already known; it will throw if + /// the given secret key does not yield the group's public key. The given key can be either the + /// 32 byte seed, or the libsodium 64 byte "secret key" (which is just the seed and cached + /// public key stuck together). + /// + /// Inputs: + /// - `secret` -- the group's 64-byte secret key or 32-byte seed + /// - `info` and `members` -- will be loaded with the group keys if the key is loaded + /// successfully. + /// + /// 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_admin_key(ustring_view secret, Info& info, Members& members); + /// API: groups/Keys::rekey /// /// Generate a new encryption key for the group and returns an encrypted key message to be diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index f35ccfef..9f557cf1 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -209,6 +209,30 @@ ustring_view Keys::group_enc_key() const { return {key.data(), key.size()}; } +void Keys::load_admin_key(ustring_view seed, Info& info, Members& members) { + if (admin()) + return; + + if (seed.size() == 64) + seed.remove_suffix(32); + else if (seed.size() != 32) + throw std::invalid_argument{ + "Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"}; + + std::array pk; + sodium_cleared> sk; + crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data()); + + if (_sign_pk.has_value() && *_sign_pk != pk) + throw std::runtime_error{ + "Failed to load admin key: given secret key does not match group pubkey"}; + + auto seckey = to_sv(sk); + set_sig_keys(seckey); + info.set_sig_keys(seckey); + members.set_sig_keys(seckey); +} + static std::array compute_xpk(const unsigned char* ed25519_pk) { std::array xpk; if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) @@ -1424,6 +1448,23 @@ 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) { + 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; + } + return true; +} + LIBSESSION_C_API bool groups_keys_rekey( config_group_keys* conf, config_object* info, diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 7ffccc56..475ba217 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -887,3 +887,93 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") } } } + +TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { + + const ustring group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; + const ustring admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + const ustring member1_seed = + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes; + + std::array group_pk; + std::array group_sk; + + crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == + oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); + + pseudo_client admin{admin1_seed, true, group_pk.data(), group_sk.data()}; + pseudo_client member{member1_seed, false, group_pk.data(), std::nullopt}; + + std::vector> configs; + { + auto m = admin.members.get_or_construct(admin.session_id); + m.admin = true; + m.name = "Lrrr"; + admin.members.set(m); + } + { + auto m = admin.members.get_or_construct(member.session_id); + m.admin = false; + m.name = "Nibbler"; + admin.members.set(m); + } + admin.info.set_name("Omicron Persei 8"); + auto [mseq, mdata, mobs] = admin.members.push(); + admin.members.confirm_pushed(mseq, "mpush1"); + auto [iseq, idata, iobs] = admin.info.push(); + admin.info.confirm_pushed(mseq, "ipush1"); + + REQUIRE(admin.keys.pending_config()); + member.keys.load_key_message( + "keyhash1", + *admin.keys.pending_config(), + get_timestamp_ms(), + member.info, + member.members); + admin.keys.load_key_message( + "keyhash1", + *admin.keys.pending_config(), + get_timestamp_ms(), + member.info, + member.members); + + member.keys.load_key_message( + "keyhash2", + admin.keys.key_supplement(member.session_id), + get_timestamp_ms(), + member.info, + member.members); + + configs.emplace_back("mpush1", mdata); + CHECK(member.members.merge(configs) == std::vector{{"mpush1"s}}); + + configs.clear(); + configs.emplace_back("ipush1", idata); + CHECK(member.info.merge(configs) == std::vector{{"ipush1"s}}); + + REQUIRE(admin.keys.admin()); + REQUIRE_FALSE(member.keys.admin()); + REQUIRE(member.info.is_readonly()); + REQUIRE(member.members.is_readonly()); + + member.keys.load_admin_key(to_usv(group_sk), member.info, member.members); + + CHECK(member.keys.admin()); + CHECK_FALSE(member.members.is_readonly()); + CHECK_FALSE(member.info.is_readonly()); + + member.info.set_name("new name"s); + + CHECK(member.info.needs_push()); + auto [iseq2, idata2, iobs2] = member.info.push(); + + configs.clear(); + configs.emplace_back("ihash2", idata2); + + CHECK(admin.info.merge(configs) == std::vector{{"ihash2"s}}); + + CHECK(admin.info.get_name() == "new name"); +} From 2c4a5bc9d21f07a0894c5342f8d088833229da1b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 17:55:09 -0400 Subject: [PATCH 120/572] Add multi-encryption algorithm This allows efficiently encrypting 1-to-N or N-to-N messages in a single message, such as is used in the group keys message. --- include/session/multi_encrypt.hpp | 252 ++++++++++++++++++++++++++++++ include/session/util.hpp | 73 ++++++++- src/CMakeLists.txt | 3 +- src/multi_encrypt.cpp | 110 +++++++++++++ tests/CMakeLists.txt | 1 + tests/test_multi_encrypt.cpp | 189 ++++++++++++++++++++++ 6 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 include/session/multi_encrypt.hpp create mode 100644 src/multi_encrypt.cpp create mode 100644 tests/test_multi_encrypt.cpp diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp new file mode 100644 index 00000000..f762460b --- /dev/null +++ b/include/session/multi_encrypt.hpp @@ -0,0 +1,252 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "types.hpp" +#include "util.hpp" + +// Helper functions for implementing multiply encrypted messages by creating separate copies of the +// message for each message recipient. This is used most prominently in group key update messages +// to send a copy of the symmetric encryption key to each of a set of recipients. +// +// Details: +// - we use xchacha20-poly1305 encryption +// - we use a single nonce for all the encryptions (rather than a separate one for each encrypted +// copy). Since we are use separate keys for each encryption, nonce reuse is not a concern. +// - we support padding with optional junk entries +// - we do not sign or identify the creator of the encrypted values here (i.e. it's the caller's +// responsibility to do that, if needed). +// - the encryption key for sender a/A, recipient b/B is: H(aB || A || B) = H(bA || A || B), and so +// the recipient needs to know the sender's pubkey to decrypt the message. +// - the general idea is for a potential recipient to brute-force attempt to decrypt all the +// messages to see if any work. +// - this approach is really only meant for limited size groups and is not intended for large scale. + +namespace session { + +namespace detail { + + void encrypt_multi_key( + std::array& key_out, + const unsigned char* a, + const unsigned char* A, + const unsigned char* B, + bool encrypting, + std::string_view domain); + + void encrypt_multi_impl( + ustring& out, + ustring_view message, + const unsigned char* key, + const unsigned char* nonce); + + bool decrypt_multi_impl( + ustring& out, + ustring_view ciphertext, + const unsigned char* key, + const unsigned char* nonce); + + inline void validate_multi_fields( + ustring_view nonce, ustring_view privkey, ustring_view pubkey) { + if (nonce.size() < 24) + throw std::logic_error{"nonce must be 24 bytes"}; + if (privkey.size() != 32) + throw std::logic_error{"privkey must be 32 bytes"}; + if (pubkey.size() != 32) + throw std::logic_error{"pubkey requires a 32-byte pubkey"}; + } + +} // namespace detail + +/// API: crypto/encrypt_multiple_message_overhead +/// +/// The number of bytes of overhead required per encrypted copy of the message as produced by +/// `encrypt_for_multiple`. This does not include the nonce or other data (such as the sender) +/// that likely needs to be transmitted as well. +extern const size_t encrypt_multiple_message_overhead; + +/// API: crypto/encrypt_for_multiple +/// +/// Encrypts a message multiple times for multiple recipients. `callable` is invoked once per +/// encrypted (or junk) value, passed as a `ustring_view`. +/// +/// Inputs: +/// - `messages` -- a vector of message bodies to encrypt. Must be either size 1, or of the same +/// size as recipients. If given a single message then that message is re-encrypted for each +/// recipient; if given multiple messages then the nth message is encrypted for the nth recipient. +/// See also session/util.hpp for a convenience function for converting other containers of +/// string-like values to this view vector type. +/// - `nonce` -- must be 24 bytes (can be longer, but only the first 24 will be used). Should be +/// secure random, or a cryptographically secure hash incorporating secret data. The nonce should +/// not be reused (if the same sender/recipient encryption/decryption key. +/// - `privkey` -- the sender's X25519 private key. Must be 32 bytes. *NOT* an Ed25519 secret key: +/// if that's what you have, you need to convert the privkey to X25519 first. +/// - `pubkey` -- the sender's X25519 public key. Can be empty to compute it from `privkey`. +/// - `recipients` -- vector of recipient X25519 public keys. Must be 32 bytes each (remove the 05 +/// if a session id). *NOT* Ed25519 pubkeys; conversion to X25519 may be required if that's what +/// you have. +/// - `domain` -- some unique fixed string known to both sides; this is used in the hashing function +/// used to generate individual keys for domain separation, and so should ideally have a different +/// value in different contexts (i.e. group keys uses one value, kicked messages use another, +/// etc.). *Can* be empty, but should be set to something. +/// - `call` -- this is invoked for each different encrypted value with a ustring_view; the caller +/// must copy as needed as the ustring_view doesn't remain valid past the call. +/// - `ignore_invalid_recipient` -- if given and true then any recipients that appear to have +/// invalid public keys (i.e. the shared key multiplication fails) will be silently ignored (the +/// callback will not be called). If not given (or false) then such a failure for any recipient +/// will raise an exception. +template +void encrypt_for_multiple( + const std::vector messages, + const std::vector recipients, + ustring_view nonce, + ustring_view privkey, + ustring_view pubkey, + std::string_view domain, + F&& call, + bool ignore_invalid_recipient = false) { + + detail::validate_multi_fields(nonce, privkey, pubkey); + + for (const auto& r : recipients) + if (r.size() != 32) + throw std::logic_error{"encrypt_for_multiple requires 32-byte recipients pubkeys"}; + if (messages.size() != 1 && messages.size() != recipients.size()) + throw std::logic_error{ + "encrypt_for_multiple requires either 1 or recipients.size() messages"}; + + size_t max_msg_size = 0; + for (const auto& m : messages) + if (auto sz = m.size(); sz > max_msg_size) + max_msg_size = sz; + + ustring encrypted; + encrypted.reserve(max_msg_size + encrypt_multiple_message_overhead); + + sodium_cleared> key; + auto msg_it = messages.begin(); + for (const auto& r : recipients) { + const auto& m = *msg_it; + if (messages.size() > 1) + ++msg_it; + try { + detail::encrypt_multi_key(key, privkey.data(), pubkey.data(), r.data(), true, domain); + } catch (const std::exception&) { + if (ignore_invalid_recipient) + continue; + else + throw; + } + detail::encrypt_multi_impl(encrypted, m, key.data(), nonce.data()); + call(ustring_view{encrypted}); + } +} + +/// Wrapper for passing a single message for all recipients; all arguments other than the first are +/// identical. +template +void encrypt_for_multiple(ustring_view message, Args&&... args) { + return encrypt_for_multiple( + to_view_vector(&message, &message + 1), std::forward(args)...); +} +template +void encrypt_for_multiple(std::string_view message, Args&&... args) { + return encrypt_for_multiple(to_unsigned_sv(message), std::forward(args)...); +} +template +void encrypt_for_multiple(std::basic_string_view message, Args&&... args) { + return encrypt_for_multiple(to_unsigned_sv(message), std::forward(args)...); +} + +/// API: crypto/decrypt_for_multiple +/// +/// Decryption via a lambda: we call the lambda (which must return a std::optional) +/// repeatedly until we get back a nullopt, and attempt to decrypt each returned value. When +/// decryption succeeds, we return the plaintext to the caller. If none of the fed-in values can be +/// decrypt, we return std::nullopt. +/// +/// Inputs: +/// - `ciphertext` -- callback that returns a std::optional or std::optional +/// when called, containing the next ciphertext; should return std::nullopt when finished. +/// - `nonce` -- the nonce used for encryption/decryption (which must have been provided by the +/// sender alongside the encrypted messages, and is the same as the `nonce` value given to +/// `encrypt_for_multiple`) +/// - `privkey` -- the private X25519 key of the recipient. +/// - `pubkey` -- the public X25519 key of the recipient (for a successful decryption, this will be +/// one of the pubkeys given to `encrypt_for_multiple`. +/// - `sender_pubkey` -- the public X25519 key of the sender (this is the `pubkey` passed into +/// `encrypt_for_multiple`). +/// - `domain` -- the encryption domain; this is typically a hard-coded string, and must be the same +/// as the one used for encryption. +template < + typename NextCiphertext, + typename = std::enable_if_t< + std::is_invocable_r_v, NextCiphertext> || + std::is_invocable_r_v, NextCiphertext> || + std::is_invocable_r_v, NextCiphertext> || + std::is_invocable_r_v, NextCiphertext> || + std::is_invocable_r_v< + std::optional>, + NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext>>> +std::optional decrypt_for_multiple( + NextCiphertext next_ciphertext, + ustring_view nonce, + ustring_view privkey, + ustring_view pubkey, + ustring_view sender_pubkey, + std::string_view domain) { + + detail::validate_multi_fields(nonce, privkey, pubkey); + if (sender_pubkey.size() != 32) + throw std::logic_error{"pubkey requires a 32-byte pubkey"}; + + sodium_cleared> key; + detail::encrypt_multi_key( + key, privkey.data(), pubkey.data(), sender_pubkey.data(), false, domain); + + auto decrypted = std::make_optional(); + + for (auto ciphertext = next_ciphertext(); ciphertext; ciphertext = next_ciphertext()) + if (detail::decrypt_multi_impl(*decrypted, *ciphertext, key.data(), nonce.data())) + return decrypted; + + decrypted.reset(); + return decrypted; +} + +/// API: crypto/decrypt_for_multiple +/// +/// Attempts to decrypt any of the messages produced by `encrypt_for_multiple`. As soon as one +/// decrypts successfully it is returned. If non decrypt you get back std::nullopt. +/// +/// Inputs: +/// - `ciphertexts` -- the encrypted values +/// - `nonce` -- the nonce used for encryption/decryption (which must have been provided by the +/// sender alongside the encrypted messages, and is the same as the `nonce` value given to +/// `encrypt_for_multiple`) +/// - `privkey` -- the private X25519 key of the recipient. +/// - `pubkey` -- the public X25519 key of the recipient (for a successful decryption, this will be +/// one of the pubkeys given to `encrypt_for_multiple`. +/// - `sender_pubkey` -- the public X25519 key of the sender (this is the `pubkey` passed into +/// `encrypt_for_multiple`). +/// - `domain` -- the encryption domain; this is typically a hard-coded string, and must be the same +/// as the one used for encryption. +/// +std::optional decrypt_for_multiple( + const std::vector& ciphertexts, + ustring_view nonce, + ustring_view privkey, + ustring_view pubkey, + ustring_view sender_pubkey, + std::string_view domain); + + + +} // namespace session diff --git a/include/session/util.hpp b/include/session/util.hpp index 5e906a6a..d161198f 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -19,16 +20,35 @@ inline const unsigned char* to_unsigned(const char* x) { inline unsigned char* to_unsigned(char* x) { return reinterpret_cast(x); } +inline const unsigned char* to_unsigned(const std::byte* x) { + return reinterpret_cast(x); +} +inline unsigned char* to_unsigned(std::byte* x) { + return reinterpret_cast(x); +} +// These do nothing, but having them makes template metaprogramming easier: +inline const unsigned char* to_unsigned(const unsigned char* x) { + return x; +} +inline unsigned char* to_unsigned(unsigned char* x) { + return x; +} inline const char* from_unsigned(const unsigned char* x) { return reinterpret_cast(x); } inline char* from_unsigned(unsigned char* x) { return reinterpret_cast(x); } -// Helper function to switch between string_view and ustring_view +// Helper function to switch between basic_string_view and ustring_view inline ustring_view to_unsigned_sv(std::string_view v) { return {to_unsigned(v.data()), v.size()}; } +inline ustring_view to_unsigned_sv(std::basic_string_view v) { + return {to_unsigned(v.data()), v.size()}; +} +inline ustring_view to_unsigned_sv(ustring_view v) { + return v; // no-op, but helps with template metaprogamming +} inline std::string_view from_unsigned_sv(ustring_view v) { return {from_unsigned(v.data()), v.size()}; } @@ -284,4 +304,55 @@ struct sodium_allocator { template using sodium_vector = std::vector>; +template +using string_view_char_type = std::conditional_t< + std::is_convertible_v, + char, + std::conditional_t< + std::is_convertible_v>, + unsigned char, + std::conditional_t< + std::is_convertible_v>, + std::byte, + void>>>; + +template +constexpr bool is_char_array = false; +template +inline constexpr bool is_char_array> = std::is_same_v || std::is_same_v || std::is_same_v; + + +/// Takes a container of string-like binary values and returns a vector of ustring_views viewing +/// those values. This can be used on a container of any type with a `.data()` and a `.size()` +/// where `.data()` is a one-byte value pointer; std::string, std::string_view, ustring, +/// ustring_view, etc. apply, as does std::array of 1-byte char types. +/// +/// This is useful in various libsession functions that require such a vector. Note that the +/// returned vector's views are valid only as the original container remains alive; this is +/// typically used inline rather than stored, such as: +/// +/// session::function_taking_a_view_vector(session::to_view_vector(mydata)); +/// +/// There are two versions of this: the first takes a generic iterator pair; the second takes a +/// single container. +template +std::vector to_view_vector(It begin, It end) { + std::vector vec; + vec.reserve(std::distance(begin, end)); + for (; begin != end; ++begin) { + if constexpr (std::is_same_v, char*>) // C strings + vec.emplace_back(*begin); + else { + static_assert(sizeof(*begin->data()) == 1, "to_view_vector can only be used with containers of string-like types of 1-byte characters"); + vec.emplace_back(reinterpret_cast(begin->data()), begin->size()); + } + } + return vec; +} + +template +std::vector to_view_vector(const Container& c) { + return to_view_vector(c.begin(), c.end()); +} + } // namespace session diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 404a4b2f..9495b694 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,9 +47,10 @@ endif() add_libsession_util_library(crypto blinding.cpp + multi_encrypt.cpp session_encrypt.cpp - xed25519.cpp util.cpp + xed25519.cpp ) add_libsession_util_library(config diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp new file mode 100644 index 00000000..210d621d --- /dev/null +++ b/src/multi_encrypt.cpp @@ -0,0 +1,110 @@ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace session { + +const size_t encrypt_multiple_message_overhead = crypto_aead_xchacha20poly1305_ietf_ABYTES; + +namespace detail { + + void encrypt_multi_key( + std::array& key, + const unsigned char* a, + const unsigned char* A, + const unsigned char* B, + bool encrypting, + std::string_view domain) { + + std::array buf; + if (0 != crypto_scalarmult_curve25519(buf.data(), a, B)) + throw std::invalid_argument{"Unable to compute shared encrypted key: invalid pubkey?"}; + + static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( + &st, + reinterpret_cast(domain.data()), + std::min(domain.size(), crypto_generichash_blake2b_KEYBYTES_MAX), + 32); + + crypto_generichash_blake2b_update(&st, buf.data(), buf.size()); + + // If we're encrypting then a/A == sender, B = recipient + // If we're decrypting then a/A = recipient, B = sender + // We always need the same sR || S || R or rS || S || R, so if we're decrypting we need to + // put B before A in the hash; + const auto* S = encrypting ? A : B; + const auto* R = encrypting ? B : A; + crypto_generichash_blake2b_update(&st, S, 32); + crypto_generichash_blake2b_update(&st, R, 32); + crypto_generichash_blake2b_final(&st, key.data(), 32); + } + + void encrypt_multi_impl( + ustring& out, ustring_view msg, const unsigned char* key, const unsigned char* nonce) { + + // auto key = encrypt_multi_key(a, A, B, true, domain); + + out.resize(msg.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + if (0 != + crypto_aead_xchacha20poly1305_ietf_encrypt( + out.data(), nullptr, msg.data(), msg.size(), nullptr, 0, nullptr, nonce, key)) + throw std::runtime_error{"XChaCha20 encryption failed!"}; + } + + bool decrypt_multi_impl( + ustring& out, + ustring_view ciphertext, + const unsigned char* key, + const unsigned char* nonce) { + + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + return false; + + out.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + out.data(), + nullptr, + nullptr, + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, + nonce, + key); + } + +} // namespace detail + +std::optional decrypt_for_multiple( + const std::vector& ciphertexts, + ustring_view nonce, + ustring_view privkey, + ustring_view pubkey, + ustring_view sender_pubkey, + std::string_view domain) { + + auto it = ciphertexts.begin(); + return decrypt_for_multiple( + [&]() -> std::optional { + if (it == ciphertexts.end()) + return std::nullopt; + return *it++; + }, + nonce, + privkey, + pubkey, + sender_pubkey, + domain); +} +} // namespace session diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 52f7f20f..8a994202 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable(testAll test_group_keys.cpp test_group_info.cpp test_group_members.cpp + test_multi_encrypt.cpp test_onionreq.cpp test_proto.cpp test_session_encrypt.cpp diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp new file mode 100644 index 00000000..1f7916bd --- /dev/null +++ b/tests/test_multi_encrypt.cpp @@ -0,0 +1,189 @@ +#include + +#include +#include +#include + +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; + +using x_pair = std::pair, std::array>; + +// Returns X25519 privkey, pubkey from an Ed25519 seed +x_pair to_x_keys(ustring_view ed_seed) { + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed_seed.data()); + x_pair ret; + auto& [x_priv, x_pub] = ret; + [[maybe_unused]] int rc = crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), ed_pk.data()); + assert(rc == 0); + crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), ed_sk.data()); + return ret; +} + +TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { + + const std::array seeds = { + "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes, + "0123456789abcdef000000000000000000000000000000000000000000000000"_hexbytes, + "0123456789abcdef111111111111111100000000000000000000000000000000"_hexbytes, + "0123456789abcdef222222222222222200000000000000000000000000000000"_hexbytes, + "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; + + std::array x_keys; + for (int i = 0; i < seeds.size(); i++) + x_keys[i] = to_x_keys(seeds[i]); + + CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(oxenc::to_hex(to_usv(x_keys[1].second)) == + "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); + CHECK(oxenc::to_hex(to_usv(x_keys[2].second)) == + "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); + CHECK(oxenc::to_hex(to_usv(x_keys[3].second)) == + "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); + CHECK(oxenc::to_hex(to_usv(x_keys[4].second)) == + "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); + + auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; + + std::vector recipients; + for (auto& [_, pubkey] : x_keys) + recipients.emplace_back(pubkey.data(), pubkey.size()); + + std::vector msgs{{"hello", "cruel", "world"}}; + std::vector encrypted; + session::encrypt_for_multiple( + msgs[0], + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + nonce, + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite", + [&](ustring_view enc) { encrypted.emplace_back(enc); }); + + REQUIRE(encrypted.size() == 3); + CHECK(oxenc::to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); + CHECK(oxenc::to_hex(encrypted[1]) == "b7a15bcd9f7b09445defcae2f1dc5085dd75cb085b"); + CHECK(oxenc::to_hex(encrypted[2]) == "01c4fc2156327735f3fb5063b11ea95f6ebcc5b6cc"); + + auto m1 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[1].first), + to_usv(x_keys[1].second), + to_usv(x_keys[0].second), + "test suite"); + auto m2 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[2].first), + to_usv(x_keys[2].second), + to_usv(x_keys[0].second), + "test suite"); + auto m3 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "test suite"); + auto m3b = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "not test suite"); + auto m4 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[4].first), + to_usv(x_keys[4].second), + to_usv(x_keys[0].second), + "test suite"); + + REQUIRE(m1); + REQUIRE(m2); + REQUIRE(m3); + CHECK_FALSE(m3b); + CHECK_FALSE(m4); + + CHECK(to_sv(*m1) == "hello"); + CHECK(to_sv(*m2) == "hello"); + CHECK(to_sv(*m3) == "hello"); + + encrypted.clear(); + session::encrypt_for_multiple( + session::to_view_vector(msgs.begin(), msgs.end()), + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + nonce, + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite", + [&](ustring_view enc) { encrypted.emplace_back(enc); }); + + REQUIRE(encrypted.size() == 3); + CHECK(oxenc::to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); + CHECK(oxenc::to_hex(encrypted[1]) == "bcb642c49c6da03f70cdaab2ed6666721318afd631"); + CHECK(oxenc::to_hex(encrypted[2]) == "1ecee2215d226817edfdb097f05037eb799309103a"); + + m1 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[1].first), + to_usv(x_keys[1].second), + to_usv(x_keys[0].second), + "test suite"); + m2 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[2].first), + to_usv(x_keys[2].second), + to_usv(x_keys[0].second), + "test suite"); + m3 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "test suite"); + m3b = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "not test suite"); + m4 = session::decrypt_for_multiple( + session::to_view_vector(encrypted), + nonce, + to_usv(x_keys[4].first), + to_usv(x_keys[4].second), + to_usv(x_keys[0].second), + "test suite"); + + REQUIRE(m1); + REQUIRE(m2); + REQUIRE(m3); + CHECK_FALSE(m3b); + CHECK_FALSE(m4); + + CHECK(to_sv(*m1) == "hello"); + CHECK(to_sv(*m2) == "cruel"); + CHECK(to_sv(*m3) == "world"); + + // Mismatch messages & recipients size throws: + CHECK_THROWS(session::encrypt_for_multiple( + session::to_view_vector(msgs.begin(), std::prev(msgs.end())), + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + nonce, + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite", + [&](ustring_view enc) { encrypted.emplace_back(enc); })); +} From f665ec8c3ec0249a2eaeecf2524fa757c2c5e0cc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 19:18:37 -0400 Subject: [PATCH 121/572] Change group keys encryption/decryption to use multi_encrypt This does not change the encryption, but rather just updates it to use the new multi_encrypt for the implementation rather than doing it itself. --- src/config/groups/keys.cpp | 279 ++++++++++++++++++------------------- 1 file changed, 139 insertions(+), 140 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 5bb1e1e5..e11f0b3c 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -21,6 +21,7 @@ #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" +#include "session/multi_encrypt.hpp" #include "session/xed25519.hpp" using namespace std::literals; @@ -233,20 +234,24 @@ void Keys::load_admin_key(ustring_view seed, Info& info, Members& members) { members.set_sig_keys(seckey); } -static std::array compute_xpk(const unsigned char* ed25519_pk) { - std::array xpk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) - throw std::runtime_error{ - "An error occured while attempting to convert Ed25519 pubkey to X25519; " - "is the pubkey valid?"}; - return xpk; -} +namespace { -static constexpr auto seed_hash_key = "SessionGroupKeySeed"sv; -static const ustring_view enc_key_hash_key = to_unsigned_sv("SessionGroupKeyGen"sv); -static constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; -static const ustring_view enc_key_member_hash_key = to_unsigned_sv("SessionGroupKeyMemberKey"sv); -static const ustring_view junk_seed_hash_key = to_unsigned_sv("SessionGroupJunkMembers"sv); + std::array compute_xpk(const unsigned char* ed25519_pk) { + std::array xpk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to X25519; " + "is the pubkey valid?"}; + return xpk; + } + + constexpr auto seed_hash_key = "SessionGroupKeySeed"sv; + const ustring_view enc_key_hash_key = to_unsigned_sv("SessionGroupKeyGen"sv); + constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; + constexpr auto enc_key_member_hash_key = "SessionGroupKeyMemberKey"sv; + const ustring_view junk_seed_hash_key = to_unsigned_sv("SessionGroupJunkMembers"sv); + +} // namespace ustring_view Keys::rekey(Info& info, Members& members) { if (!admin()) @@ -342,37 +347,29 @@ ustring_view Keys::rekey(Info& info, Members& members) { { auto member_keys = d.append_list("k"); int member_count = 0; + std::vector> member_xpk_raw; + std::vector member_xpks; + member_xpk_raw.reserve(members.size()); + member_xpks.reserve(members.size()); for (const auto& m : members) { - auto m_xpk = session_id_pk(m.session_id); - // Calculate the encryption key: H(aB || A || B) - if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) - continue; // The scalarmult failed; maybe a bad session id? - - crypto_generichash_blake2b_init( - &st, - enc_key_member_hash_key.data(), - enc_key_member_hash_key.size(), - member_k.size()); - crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); - crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); - crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); - crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); - - crypto_aead_xchacha20poly1305_ietf_encrypt( - encrypted.data(), - nullptr, - enc_key.data(), - enc_key.size(), - nullptr, - 0, - nullptr, - nonce.data(), - member_k.data()); - - member_keys.append(enc_sv); - member_count++; + member_xpk_raw.push_back(session_id_pk(m.session_id)); + member_xpks.emplace_back(member_xpk_raw.back().data(), member_xpk_raw.back().size()); } + encrypt_for_multiple( + enc_key, + member_xpks, + nonce, + to_sv(group_xsk), + to_sv(group_xpk), + enc_key_member_hash_key, + [&](ustring_view enc_sv) { + member_keys.append(enc_sv); + member_count++; + }, + true // ignore invalid + ); + // Pad it out with junk entries to the next MESSAGE_KEY_MULTIPLE if (member_count % MESSAGE_KEY_MULTIPLE) { int n_junk = MESSAGE_KEY_MULTIPLE - (member_count % MESSAGE_KEY_MULTIPLE); @@ -504,40 +501,29 @@ ustring Keys::key_supplement(const std::vector& sids) const { size_t member_count = 0; - for (auto& sid : sids) { - auto m_xpk = session_id_pk(sid); - - // Calculate the encryption key: H(aB || A || B) - std::array member_k; - if (0 != crypto_scalarmult_curve25519(member_k.data(), group_xsk.data(), m_xpk.data())) - continue; // The scalarmult failed; maybe a bad session id? - - crypto_generichash_blake2b_init( - &st, - enc_key_member_hash_key.data(), - enc_key_member_hash_key.size(), - member_k.size()); - crypto_generichash_blake2b_update(&st, member_k.data(), member_k.size()); - crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); - crypto_generichash_blake2b_update(&st, m_xpk.data(), m_xpk.size()); - crypto_generichash_blake2b_final(&st, member_k.data(), member_k.size()); - - crypto_aead_xchacha20poly1305_ietf_encrypt( - encrypted.data(), - nullptr, - to_unsigned(supp_keys.data()), - supp_keys.size(), - nullptr, - 0, - nullptr, - nonce.data(), - member_k.data()); - - list.append(from_unsigned_sv(encrypted)); - - member_count++; + std::vector> member_xpk_raw; + std::vector member_xpks; + member_xpk_raw.reserve(sids.size()); + member_xpks.reserve(sids.size()); + for (const auto& sid : sids) { + member_xpk_raw.push_back(session_id_pk(sid)); + member_xpks.emplace_back(member_xpk_raw.back().data(), member_xpk_raw.back().size()); } + encrypt_for_multiple( + supp_keys, + member_xpks, + nonce, + to_sv(group_xsk), + to_sv(group_xpk), + enc_key_member_hash_key, + [&](ustring_view encrypted) { + list.append(encrypted); + member_count++; + }, + true // ignore invalid + ); + if (member_count == 0) throw std::runtime_error{ "Unable to construct supplemental messages: invalid session ids given"}; @@ -944,37 +930,22 @@ bool Keys::load_key_message( // value, even if we didn't find a key for us. sodium_cleared> member_dec_key; + sodium_cleared> member_xsk; + std::array member_xpk; if (!admin()) { - sodium_cleared> member_xsk; crypto_sign_ed25519_sk_to_curve25519(member_xsk.data(), user_ed25519_sk.data()); - auto member_xpk = compute_xpk(user_ed25519_sk.data() + 32); - - // Calculate the encryption key: H(bA || A || B) [A = group, B = member] - if (0 != crypto_scalarmult_curve25519( - member_dec_key.data(), member_xsk.data(), group_xpk.data())) - throw std::runtime_error{ - "Unable to compute member decryption key; invalid group or member keys?"}; - - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init( - &st, - enc_key_member_hash_key.data(), - enc_key_member_hash_key.size(), - member_dec_key.size()); - crypto_generichash_blake2b_update(&st, member_dec_key.data(), member_dec_key.size()); - crypto_generichash_blake2b_update(&st, group_xpk.data(), group_xpk.size()); - crypto_generichash_blake2b_update(&st, member_xpk.data(), member_xpk.size()); - crypto_generichash_blake2b_final(&st, member_dec_key.data(), member_dec_key.size()); + member_xpk = compute_xpk(user_ed25519_sk.data() + 32); } if (d.skip_until("+")) { // This is a supplemental keys message, not a full one auto supp = d.consume_list_consumer(); - while (!supp.is_finished()) { + int member_key_pos = -1; - int member_key_count = 0; - for (; !supp.is_finished(); member_key_count++) { + auto next_ciphertext = [&]() -> std::optional { + while (!supp.is_finished()) { + member_key_pos++; auto encrypted = to_unsigned_sv(supp.consume_string_view()); // Expect an encrypted message like this, which has a minimum valid size (if both g // and t are 0 for some reason) of: @@ -987,43 +958,52 @@ bool Keys::load_key_message( // 52 if (encrypted.size() < 52 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw config_value_error{ - "Supplemental key message has invalid key info size at index " + - std::to_string(member_key_count)}; + "Supplemental key message has invalid key info size at " + "index " + + std::to_string(member_key_pos)}; if (!new_keys.empty() || admin()) - continue; // Keep parsing, just to ensure validity of the whole message - - ustring plaintext; - plaintext.resize(encrypted.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - - if (try_decrypting(plaintext.data(), encrypted, nonce, member_dec_key)) { - // Decryption success, we found our key list! - - oxenc::bt_list_consumer key_infos{from_unsigned_sv(plaintext)}; - while (!key_infos.is_finished()) { - auto& new_key = new_keys.emplace_back(); - auto keyinf = key_infos.consume_dict_consumer(); - if (!keyinf.skip_until("g")) - throw config_value_error{ - "Invalid supplemental key message: no `g` generation"}; - new_key.generation = keyinf.consume_integer(); - if (!keyinf.skip_until("k")) - throw config_value_error{ - "Invalid supplemental key message: no `k` key data"}; - auto key_val = keyinf.consume_string_view(); - if (key_val.size() != 32) - throw config_value_error{ - "Invalid supplemental key message: `k` key has wrong size"}; - std::memcpy(new_key.key.data(), key_val.data(), 32); - if (!keyinf.skip_until("t")) - throw config_value_error{ - "Invalid supplemental key message: no `t` timestamp"}; - new_key.timestamp = sys_time_from_ms(keyinf.consume_integer()); - } - } + continue; // Keep parsing, to ensure validity of the whole message + + return encrypted; + } + return std::nullopt; + }; + + if (auto plaintext = decrypt_for_multiple( + next_ciphertext, + nonce, + to_sv(member_xsk), + to_sv(member_xpk), + to_sv(group_xpk), + enc_key_member_hash_key)) { + + // Decryption success, we found our key list! + + oxenc::bt_list_consumer key_infos{from_unsigned_sv(*plaintext)}; + while (!key_infos.is_finished()) { + auto& new_key = new_keys.emplace_back(); + auto keyinf = key_infos.consume_dict_consumer(); + if (!keyinf.skip_until("g")) + throw config_value_error{"Invalid supplemental key message: no `g` generation"}; + new_key.generation = keyinf.consume_integer(); + if (!keyinf.skip_until("k")) + throw config_value_error{"Invalid supplemental key message: no `k` key data"}; + auto key_val = keyinf.consume_string_view(); + if (key_val.size() != 32) + throw config_value_error{ + "Invalid supplemental key message: `k` key has wrong size"}; + std::memcpy(new_key.key.data(), key_val.data(), 32); + if (!keyinf.skip_until("t")) + throw config_value_error{"Invalid supplemental key message: no `t` timestamp"}; + new_key.timestamp = sys_time_from_ms(keyinf.consume_integer()); } } + // Ensure we consume all the ciphertexts (to ensure some message validity, even if we found + // one halfway through). + while (next_ciphertext()) {} + if (!d.skip_until("G")) throw config_value_error{ "Supplemental key message missing required max generation field (G)"}; @@ -1063,26 +1043,45 @@ bool Keys::load_key_message( // the same error conditions for rejecting an invalid config message. if (!d.skip_until("k")) throw config_value_error{"Config is missing member keys list (k)"}; + auto key_list = d.consume_list_consumer(); - int member_key_count = 0; - for (; !key_list.is_finished(); member_key_count++) { - auto member_key = to_unsigned_sv(key_list.consume_string_view()); - if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw config_value_error{ - "Key message has invalid member key length at index " + - std::to_string(member_key_count)}; + int member_key_pos = -1; + auto next_ciphertext = [&]() -> std::optional { + while (!key_list.is_finished()) { + member_key_pos++; + auto member_key = to_unsigned_sv(key_list.consume_string_view()); + if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw config_value_error{ + "Key message has invalid member key length at index " + + std::to_string(member_key_pos)}; - if (found_key) - continue; + if (found_key) + continue; - if (try_decrypting(new_key.key.data(), member_key, nonce, member_dec_key)) { - // Decryption success, we found our key! - found_key = true; + return member_key; } + return std::nullopt; + }; + + if (auto plaintext = decrypt_for_multiple( + next_ciphertext, + nonce, + to_sv(member_xsk), + to_sv(member_xpk), + to_sv(group_xpk), + enc_key_member_hash_key)) { + // Decryption success, we found our key! + assert(plaintext->size() == 32); + std::memcpy(new_key.key.data(), plaintext->data(), 32); + found_key = true; } - if (member_key_count % MESSAGE_KEY_MULTIPLE != 0) + // Parse them all, even once we had a successful decryption, to properly count and fail on + // invalid input: + while (next_ciphertext()) {} + + if (++member_key_pos % MESSAGE_KEY_MULTIPLE != 0) throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; if (!found_key) { From cc0bcfb64895c326a8f928ec3583fa047894180f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 22:36:12 -0400 Subject: [PATCH 122/572] Add "simple" interface to produce a binary blob A common case is wanting to just produce a binary blob suitable for sending across the wire; this adds a few methods to make that easier. --- include/session/multi_encrypt.hpp | 133 +++++++++++++++++++++++++ src/multi_encrypt.cpp | 129 ++++++++++++++++++++++++ tests/test_multi_encrypt.cpp | 159 ++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+) diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index f762460b..7d73dfed 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -247,6 +247,139 @@ std::optional decrypt_for_multiple( ustring_view sender_pubkey, std::string_view domain); +/// API: crypto/encrypt_for_multiple_simple +/// +/// This function performs 1-to-N or N-to-N encryptions (i.e. N encrypted payloads of either the +/// same value, or N separate values) using a random nonce and encodes the resulting encrypted data +/// in a self-contained bt-encoded value suitable for decrypting by a recipient via +/// `decrypt_for_multiple_simple`. +/// +/// In contrast to `encrypt_for_multiple`, this function is less flexible, but easier to use when +/// additional flexibility is not required. +/// +/// Inputs: +/// - `messages` -- vector of messages to encrypt. This vector can either be a single message to +/// separately encrypt the same message for each member, or a vector of the same length as +/// recipients to encrypt a different message for each member. If you have these in some other +/// type of container, session/util.hpp's `session::to_view_vector` is a convenient way to convert +/// compatible containers to this view vector. +/// - `recipients` -- vector of X25519 pubkeys of the recipients. (If sending to Session IDs, these +/// are the 32-byte binary keys after removing the 0x05 prefix byte). +/// - `privkey` -- the X25519 private key of the sender (32 bytes). Note that this is *NOT* the +/// Ed25519 secret key; see the alternative version of the function below if you only have an +/// Ed25519 key. +/// - `pubkey` -- the X25519 public key of the sender (32 bytes). This needs to be known by the +/// recipient in order to decrypt the message; unlike session-protocol encryption, the sender +/// identity is not included in the message. +/// - `domain` -- the encryption domain; this is a short string that uniquely identifies the +/// "domain" of encryption, such as "SessionGroupKickedMessage". The value is arbitrary: what +/// matters is that it is unique for different encryption types, and that both the sender and +/// recipient use the same value. Max length is 64 bytes. Using a domain is encouraged so that +/// the resulting encryption key between a sender and recipient will be different if the same keys +/// are used for encryption of unrelated data types. +/// - `nonce` -- optional; if omitted or empty a random nonce will be generated. If non-empty this +/// should be a 24-byte value; this can be used with a cryptographically secure hash function to +/// construct a deterministic encrypted value. If you don't need that, omit it to use a random +/// one. +/// - `pad` -- if given and greater than 1 then junk encrypted values will be added until there are +/// a multiple of this many encrypted values in total. The size of each junk entry will be the +/// same as the (encrypted) size of the first message; this padding is most useful when all +/// messages are the same size (or the same message) as with variable-sized messages the junk +/// entries will be somewhat identifiable. +/// +/// Outputs: +/// ustring containing bytes that contains the nonce and encoded encrypted messages, suitable for +/// decryption by the recipients with `decrypt_for_multiple_simple`. +ustring encrypt_for_multiple_simple( + const std::vector& messages, + const std::vector& recipients, + ustring_view privkey, + ustring_view pubkey, + std::string_view domain, + std::optional nonce = std::nullopt, + int pad = 0); +/// API: crypto/encrypt_for_multiple_simple +/// +/// This function is the same as the above, except that instead of taking the sender private and +/// public X25519 keys, it takes the single, 64-byte libsodium Ed25519 secret key (which is then +/// converted into the required X25519 keys). +ustring encrypt_for_multiple_simple( + const std::vector& messages, + const std::vector& recipients, + ustring_view ed25519_secret_key, + std::string_view domain, + ustring_view nonce = {}, + int pad = 0); + +/// API: crypto/encrypt_for_multiple_simple +/// +/// Wrapper that takes a *single* message to send to all recipients. This is simply a shortcut for +/// passing a one-element vector into the above versions of the function; all arguments other than +/// the first are identical. +/// +template +ustring encrypt_for_multiple_simple(ustring_view message, Args&&... args) { + return encrypt_for_multiple_simple( + to_view_vector(&message, &message + 1), std::forward(args)...); +} +template +ustring encrypt_for_multiple_simple(std::string_view message, Args&&... args) { + return encrypt_for_multiple_simple(to_unsigned_sv(message), std::forward(args)...); +} +template +ustring encrypt_for_multiple_simple(std::basic_string_view message, Args&&... args) { + return encrypt_for_multiple_simple(to_unsigned_sv(message), std::forward(args)...); +} + +/// API: crypto/decrypt_for_multiple_simple +/// +/// This function attempts to decrypt a message produced by `encrypt_for_multiple_simple`; if +/// encryption (of any of the contained messages) succeeds you get back the message, otherwise if +/// the message failed to parse or decryption of all parts fails, you get back std::nullopt. +/// +/// Inputs: +/// - `encoded` -- the incoming message, produced by encrypt_for_multiple_simple +/// - `privkey` -- the X25519 private key of the receiver (32 bytes). Note that this is *NOT* the +/// Ed25519 secret key; see the alternative version of the function below if you only have an +/// Ed25519 key. +/// - `pubkey` -- the X25519 public key of the receiver (32 bytes). +/// - `sender_pubkey` -- the X25519 public key of the sender (32 bytes). Note that unlike session +/// encryption, the sender's identify is not available in the encrypted message itself. +/// - `domain` -- the encryption domain, which must be the same as the value used in +/// `encrypt_for_multiple_simple`. +/// +/// Outputs: +/// If decryption succeeds, returns a ustring containing the decrypted message, in bytes. If +/// parsing or decryption fails, returns std::nullopt. +std::optional decrypt_for_multiple_simple( + ustring_view encoded, + ustring_view privkey, + ustring_view pubkey, + ustring_view sender_pubkey, + std::string_view domain); + +/// API: crypto/decrypt_for_multiple_simple +/// +/// This is the same as the above, except that instead of taking an X25519 private and public key +/// arguments, it takes a single, 64-byte Ed25519 secret key and converts it to X25519 to perform +/// the decryption. +/// +/// Note that `sender_pubkey` is still an X25519 pubkey for this version of the function. +std::optional decrypt_for_multiple_simple( + ustring_view encoded, + ustring_view ed25519_secret_key, + ustring_view sender_pubkey, + std::string_view domain); + +/// API: crypto/decrypt_for_multiple_simple_ed25519 +/// +/// This is the same as the above, except that it takes both the sender and recipient as Ed25519 +/// keys, converting them on the fly to attempt the decryption. +std::optional decrypt_for_multiple_simple_ed25519( + ustring_view encoded, + ustring_view ed25519_secret_key, + ustring_view sender_ed25519_pubkey, + std::string_view domain); } // namespace session diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 210d621d..6f8f91d0 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -84,6 +84,21 @@ namespace detail { key); } + std::pair>, std::array> x_keys( + ustring_view ed25519_secret_key) { + if (ed25519_secret_key.size() != 64) + throw std::invalid_argument{"Ed25519 secret key is not the expected 64 bytes"}; + + std::pair>, std::array> ret; + auto& [x_priv, x_pub] = ret; + + crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), ed25519_secret_key.data()); + if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), ed25519_secret_key.data() + 32)) + throw std::runtime_error{"Failed to convert Ed25519 key to X25519: invalid secret key"}; + + return ret; + } + } // namespace detail std::optional decrypt_for_multiple( @@ -107,4 +122,118 @@ std::optional decrypt_for_multiple( sender_pubkey, domain); } + +ustring encrypt_for_multiple_simple( + const std::vector& messages, + const std::vector& recipients, + ustring_view privkey, + ustring_view pubkey, + std::string_view domain, + std::optional nonce, + int pad) { + + oxenc::bt_dict_producer d; + + std::array random_nonce; + if (!nonce) { + randombytes_buf(random_nonce.data(), random_nonce.size()); + nonce.emplace(random_nonce.data(), random_nonce.size()); + } else if (nonce->size() != 24) { + throw std::invalid_argument{"Invalid nonce: nonce must be 24 bytes"}; + } + + d.append("#", *nonce); + { + auto enc_list = d.append_list("e"); + + int msg_count = 0; + encrypt_for_multiple( + messages, recipients, *nonce, privkey, pubkey, domain, [&](ustring_view encrypted) { + enc_list.append(encrypted); + msg_count++; + }); + + if (int pad_size = pad > 1 && !messages.empty() ? messages.front().size() : 0) { + ustring junk; + junk.resize(pad_size); + for (; msg_count % pad != 0; msg_count++) { + randombytes_buf(junk.data(), pad_size); + enc_list.append(junk); + } + } + } + + return ustring{d.view()}; +} + +ustring encrypt_for_multiple_simple( + const std::vector& messages, + const std::vector& recipients, + ustring_view ed25519_secret_key, + std::string_view domain, + ustring_view nonce, + int pad) { + + auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); + + return encrypt_for_multiple_simple( + messages, recipients, to_sv(x_privkey), to_sv(x_pubkey), domain, nonce, pad); +} + +std::optional decrypt_for_multiple_simple( + ustring_view encoded, + ustring_view privkey, + ustring_view pubkey, + ustring_view sender_pubkey, + std::string_view domain) { + try { + oxenc::bt_dict_consumer d{encoded}; + auto nonce = d.require("#"); + if (nonce.size() != 24) + return std::nullopt; + auto enc_list = d.require("e"); + + return decrypt_for_multiple( + [&]() -> std::optional { + if (enc_list.is_finished()) + return std::nullopt; + return enc_list.consume(); + }, + nonce, + privkey, + pubkey, + sender_pubkey, + domain); + } catch (...) { + return std::nullopt; + } +} + +std::optional decrypt_for_multiple_simple( + ustring_view encoded, + ustring_view ed25519_secret_key, + ustring_view sender_pubkey, + std::string_view domain) { + + auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); + + return decrypt_for_multiple_simple( + encoded, to_sv(x_privkey), to_sv(x_pubkey), sender_pubkey, domain); +} + +std::optional decrypt_for_multiple_simple_ed25519( + ustring_view encoded, + ustring_view ed25519_secret_key, + ustring_view sender_ed25519_pubkey, + std::string_view domain) { + + std::array sender_pub; + if (sender_ed25519_pubkey.size() != 32) + throw std::invalid_argument{"Invalid sender Ed25519 pubkey: expected 32 bytes"}; + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_pub.data(), sender_ed25519_pubkey.data())) + throw std::runtime_error{"Failed to convert Ed25519 key to X25519: invalid secret key"}; + + return decrypt_for_multiple_simple(encoded, ed25519_secret_key, to_sv(sender_pub), domain); +} + } // namespace session diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 1f7916bd..e4a0f3fb 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -187,3 +188,161 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { "test suite", [&](ustring_view enc) { encrypted.emplace_back(enc); })); } + +TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][simple]") { + + const std::array seeds = { + "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes, + "0123456789abcdef000000000000000000000000000000000000000000000000"_hexbytes, + "0123456789abcdef111111111111111100000000000000000000000000000000"_hexbytes, + "0123456789abcdef222222222222222200000000000000000000000000000000"_hexbytes, + "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; + + std::array x_keys; + for (int i = 0; i < seeds.size(); i++) + x_keys[i] = to_x_keys(seeds[i]); + + CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(oxenc::to_hex(to_usv(x_keys[1].second)) == + "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); + CHECK(oxenc::to_hex(to_usv(x_keys[2].second)) == + "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); + CHECK(oxenc::to_hex(to_usv(x_keys[3].second)) == + "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); + CHECK(oxenc::to_hex(to_usv(x_keys[4].second)) == + "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); + + auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; + + std::vector recipients; + for (auto& [_, pubkey] : x_keys) + recipients.emplace_back(pubkey.data(), pubkey.size()); + + std::vector msgs{{"hello", "cruel", "world"}}; + ustring encrypted = session::encrypt_for_multiple_simple( + msgs[0], + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite"); + + REQUIRE(encrypted.size() == + /* de */ 2 + + /* 1:# 24:...nonce... */ 3 + 27 + + /* 1:e le */ 3 + 2 + + /* XX: then data with overhead */ 3 * + (3 + 5 + crypto_aead_xchacha20poly1305_ietf_ABYTES)); + + // If we encrypt again the value should be different (because of the default randomized nonce): + CHECK(encrypted != session::encrypt_for_multiple_simple( + msgs[0], + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite")); + + auto m1 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[1].first), + to_usv(x_keys[1].second), + to_usv(x_keys[0].second), + "test suite"); + auto m2 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[2].first), + to_usv(x_keys[2].second), + to_usv(x_keys[0].second), + "test suite"); + auto m3 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "test suite"); + auto m3b = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "not test suite"); + auto m4 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[4].first), + to_usv(x_keys[4].second), + to_usv(x_keys[0].second), + "test suite"); + + REQUIRE(m1); + REQUIRE(m2); + REQUIRE(m3); + CHECK_FALSE(m3b); + CHECK_FALSE(m4); + + CHECK(to_sv(*m1) == "hello"); + CHECK(to_sv(*m2) == "hello"); + CHECK(to_sv(*m3) == "hello"); + + encrypted = session::encrypt_for_multiple_simple( + session::to_view_vector(msgs), + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite", + nonce); + + CHECK(printable(encrypted) == + printable( + "d1:#24:" + "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hex + "1:el" + + "21:" + "e64937e5ea201b84f4e88a976dad900d91caaf6a17"_hex + + "21:" + "bcb642c49c6da03f70cdaab2ed6666721318afd631"_hex + + "21:" + "1ecee2215d226817edfdb097f05037eb799309103a"_hex + "ee")); + + m1 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[1].first), + to_usv(x_keys[1].second), + to_usv(x_keys[0].second), + "test suite"); + m2 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[2].first), + to_usv(x_keys[2].second), + to_usv(x_keys[0].second), + "test suite"); + m3 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "test suite"); + m3b = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[3].first), + to_usv(x_keys[3].second), + to_usv(x_keys[0].second), + "not test suite"); + m4 = session::decrypt_for_multiple_simple( + encrypted, + to_usv(x_keys[4].first), + to_usv(x_keys[4].second), + to_usv(x_keys[0].second), + "test suite"); + + REQUIRE(m1); + REQUIRE(m2); + REQUIRE(m3); + CHECK_FALSE(m3b); + CHECK_FALSE(m4); + + CHECK(to_sv(*m1) == "hello"); + CHECK(to_sv(*m2) == "cruel"); + CHECK(to_sv(*m3) == "world"); + + CHECK_THROWS(session::encrypt_for_multiple_simple( + session::to_view_vector(msgs.begin(), std::prev(msgs.end())), + session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite")); +} From 89ade1906debe5934733e3ed6f5c1551fb33ac22 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 23:21:20 -0400 Subject: [PATCH 123/572] Add C wrappers for encrypt/decrypt_for_multiple_simple --- include/session/multi_encrypt.h | 140 ++++++++++++++++++++++++++++++ src/multi_encrypt.cpp | 148 ++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 include/session/multi_encrypt.h diff --git a/include/session/multi_encrypt.h b/include/session/multi_encrypt.h new file mode 100644 index 00000000..f6a39bb4 --- /dev/null +++ b/include/session/multi_encrypt.h @@ -0,0 +1,140 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_encrypt_for_multiple_simple +/// +/// This function performs 1-to-N or N-to-N encryptions (i.e. N encrypted payloads of either the +/// same value, or N separate values) using a random nonce and encodes the resulting encrypted data +/// in a self-contained bt-encoded value suitable for decrypting by a recipient via +/// `session_decrypt_for_multiple_simple`. +/// +/// Inputs: +/// - `out_len` -- pointer to a size_t where the length of the returned buffer will be written. +/// - `messages` -- array of pointers to messages to encrypt. This vector can either be a single +/// message to +/// separately encrypt the same message for each member, or a vector of the same length as +/// recipients to encrypt a different message for each member. +/// - `messages_lengths` -- array of the length of the buffers in `messages`. Must be the same +/// length as `messages`. +/// - `n_messages` -- the number of messages provided. +/// - `recipients` -- array of pointers to recipient X25519 pubkeys. Each pubkey is 32 bytes. +/// These are typically binary Session IDs, not including the 0x05 prefix. +/// - `n_recipients` -- the length of `recipients` +/// - `x25519_privkey` -- the X25519 private key of the sender (32 bytes). Note that this is *NOT* +/// the Ed25519 secret key; see the alternative version of the function below if you only have an +/// Ed25519 key. +/// - `x25519_pubkey` -- the X25519 public key of the sender (32 bytes). This needs to be known by +/// the recipient in order to decrypt the message; unlike session-protocol encryption, the sender +/// identity is not included in the message. +/// - `domain` -- a regular C string that uniquely identifies the "domain" of encryption, such as +/// "SessionGroupKickedMessage". The value is arbitrary: what matters is that it is unique for +/// different encryption types, and that both the sender and recipient use the same value. Max +/// length is 64 bytes. Using a domain is encouraged so that the resulting encryption key between +/// a sender and recipient will be different if the same keys are used for encryption of unrelated +/// data types. +/// - `nonce` -- optional nonce. Typically you should pass `NULL` here, which will cause a random +/// nonce to be used, but a 24-byte nonce can be specified for deterministic encryption. (Note +/// that steps should be taken to ensure the nonce is not reused if specifying a nonce). +/// - `pad` -- if set to a value greater than 1 then junk encrypted values will be added until there +/// are a multiple of this many encrypted values in total. The size of each junk entry will be +/// the same as the (encrypted) size of the first message; this padding is most useful when all +/// messages are the same size (or the same message) as with variable-sized messages the junk +/// entries will be somewhat identifiable. Set to 0 to disable junk entry padding. +/// +/// Outputs: +/// malloced buffer containing the encoded data, or NULL if encryption failed. It is the caller's +/// responsibility to `free()` this buffer (if non-NULL) when done with it! +LIBSESSION_EXPORT unsigned char* session_encrypt_for_multiple_simple( + size_t* out_len, + const unsigned char** messages, + const size_t* message_lengths, + size_t n_messages, + const unsigned char** recipients, + size_t n_recipients, + const unsigned char* x25519_privkey, + const unsigned char* x25519_pubkey, + const char* domain, + const unsigned char* nonce, + int pad); + +/// This does the same as the above, except that it takes a single, 64-byte libsodium-style Ed25519 +/// secret key instead of the x25519 privkey/pubkey argument pair. The X25519 keys are converted +/// from the Ed25519 key on the fly. +LIBSESSION_EXPORT unsigned char* session_encrypt_for_multiple_simple_ed25519( + size_t* out_len, + const unsigned char** messages, + const size_t* message_lengths, + size_t n_messages, + const unsigned char** recipients, + size_t n_recipients, + const unsigned char* ed25519_secret_key, + const char* domain, + const unsigned char* nonce, + int pad); + +/// API: crypto/session_decrypt_for_multiple_simple +/// +/// This function attempts to decrypt a message produced by `session_encrypt_for_multiple_simple`; +/// if encryption (of any of the contained messages) succeeds you get back the message, otherwise if +/// the message failed to parse or decryption of all parts fails, you get back NULL. +/// +/// Inputs: +/// - `out_len` -- pointer to a size_t where the length of the decrypted value will be written *if* +/// decryption succeeds. +/// - `encoded` -- the incoming message, produced by session_encrypt_for_multiple_simple +/// - `encoded_len` -- size of `encoded` +/// - `x25519_privkey` -- the X25519 private key of the receiver (32 bytes). Note that this is +/// *NOT* the Ed25519 secret key; see the alternative version of the function below if you only +/// have an Ed25519 key. +/// - `x25519_pubkey` -- the X25519 public key of the receiver (32 bytes). +/// - `sender_x25519_pubkey` -- the X25519 public key of the sender (32 bytes). Note that unlike +/// session encryption, the sender's identify is not available in the encrypted message itself. +/// - `domain` -- the encryption domain, which must be the same as the value used in +/// `session_encrypt_for_multiple_simple`. +/// +/// Outputs: +/// If decryption succeeds, returns a pointer to a malloc'ed buffer containing the decrypted message +/// data, with length stored in `out_len`. If parsing or decryption fails, returns NULL. If the +/// return is non-NULL it is the responsibility of the caller to free the returned pointer! +LIBSESSION_EXPORT unsigned char* session_decrypt_for_multiple_simple( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* x25519_privkey, + const unsigned char* x25519_pubkey, + const unsigned char* sender_x25519_pubkey, + const char* domain); + +/// Same as above, but takes the recipients privkey/pubkey as a single Ed25519 secret key (64 bytes) +/// instead of a pair of X25519 argumensts. The sender pubkey is still specified as an X25519 +/// pubkey. +LIBSESSION_EXPORT unsigned char* session_decrypt_for_multiple_simple_ed25519_from_x25519( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* ed25519_secret, + const unsigned char* sender_x25519_pubkey, + const char* domain); + +/// Same as above, but takes the recipients privkey/pubkey as a single Ed25519 secret key (64 bytes) +/// instead of a pair of X25519 argumensts, *and* takes the sender's pubkey as an Ed25519 public +/// key. This is the typically the version you want when the "sender" is a group or other +/// non-Session ID known by an Ed25519 pubkey (03... or other non-05 keys) rather than a Session ID. +LIBSESSION_EXPORT unsigned char* session_decrypt_for_multiple_simple_ed25519( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* ed25519_secret, + const unsigned char* sender_ed25519_pubkey, + const char* domain); + +#ifdef __cplusplus +} +#endif diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 6f8f91d0..d64e5158 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -237,3 +238,150 @@ std::optional decrypt_for_multiple_simple_ed25519( } } // namespace session + +using namespace session; + +static unsigned char* to_c_buffer(ustring_view x, size_t* out_len) { + auto* ret = static_cast(malloc(x.size())); + *out_len = x.size(); + std::memcpy(ret, x.data(), x.size()); + return ret; +} + +LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( + size_t* out_len, + const unsigned char** messages, + const size_t* message_lengths, + size_t n_messages, + const unsigned char** recipients, + size_t n_recipients, + const unsigned char* x25519_privkey, + const unsigned char* x25519_pubkey, + const char* domain, + const unsigned char* nonce, + int pad) { + + std::vector msgs, recips; + msgs.reserve(n_messages); + recips.reserve(n_recipients); + for (size_t i = 0; i < n_messages; i++) + msgs.emplace_back(messages[i], message_lengths[i]); + for (size_t i = 0; i < n_recipients; i++) + recips.emplace_back(recipients[i], 32); + std::optional maybe_nonce; + if (nonce) + maybe_nonce.emplace(nonce, 24); + + try { + auto encoded = session::encrypt_for_multiple_simple( + msgs, + recips, + ustring_view{x25519_privkey, 32}, + ustring_view{x25519_pubkey, 32}, + domain, + std::move(maybe_nonce), + pad); + return to_c_buffer(encoded, out_len); + } catch (...) { + return nullptr; + } +} + +LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( + size_t* out_len, + const unsigned char** messages, + const size_t* message_lengths, + size_t n_messages, + const unsigned char** recipients, + size_t n_recipients, + const unsigned char* ed25519_secret_key, + const char* domain, + const unsigned char* nonce, + int pad) { + + try { + auto [priv, pub] = session::detail::x_keys(ustring_view{ed25519_secret_key, 64}); + return session_encrypt_for_multiple_simple( + out_len, + messages, + message_lengths, + n_messages, + recipients, + n_recipients, + priv.data(), + pub.data(), + domain, + nonce, + pad); + } catch (...) { + return nullptr; + } +} + +LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* x25519_privkey, + const unsigned char* x25519_pubkey, + const unsigned char* sender_x25519_pubkey, + const char* domain) { + + try { + if (auto decrypted = session::decrypt_for_multiple_simple( + ustring_view{encoded, encoded_len}, + ustring_view{x25519_privkey, 32}, + ustring_view{x25519_pubkey, 32}, + ustring_view{sender_x25519_pubkey, 32}, + domain)) { + return to_c_buffer(*decrypted, out_len); + } + } catch (...) { + } + + return nullptr; +} + +LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519_from_x25519( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* ed25519_secret, + const unsigned char* sender_x25519_pubkey, + const char* domain) { + + try { + if (auto decrypted = session::decrypt_for_multiple_simple( + ustring_view{encoded, encoded_len}, + ustring_view{ed25519_secret, 64}, + ustring_view{sender_x25519_pubkey, 32}, + domain)) { + return to_c_buffer(*decrypted, out_len); + } + } catch (...) { + } + + return nullptr; +} + +LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519( + size_t* out_len, + const unsigned char* encoded, + size_t encoded_len, + const unsigned char* ed25519_secret, + const unsigned char* sender_ed25519_pubkey, + const char* domain) { + + try { + if (auto decrypted = session::decrypt_for_multiple_simple_ed25519( + ustring_view{encoded, encoded_len}, + ustring_view{ed25519_secret, 64}, + ustring_view{sender_ed25519_pubkey, 32}, + domain)) { + return to_c_buffer(*decrypted, out_len); + } + } catch (...) { + } + + return nullptr; +} From 73c22f2ce5ca0b8fb8b94a2eb6acf0bea8084b2f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Nov 2023 23:33:46 -0400 Subject: [PATCH 124/572] Formatting --- include/session/util.hpp | 12 ++++++++---- src/config/groups/keys.cpp | 6 ++++-- tests/test_multi_encrypt.cpp | 11 ++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index d161198f..5cea9fa4 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -319,8 +319,9 @@ using string_view_char_type = std::conditional_t< template constexpr bool is_char_array = false; template -inline constexpr bool is_char_array> = std::is_same_v || std::is_same_v || std::is_same_v; - +inline constexpr bool is_char_array> = + std::is_same_v || std::is_same_v || + std::is_same_v; /// Takes a container of string-like binary values and returns a vector of ustring_views viewing /// those values. This can be used on a container of any type with a `.data()` and a `.size()` @@ -340,10 +341,13 @@ std::vector to_view_vector(It begin, It end) { std::vector vec; vec.reserve(std::distance(begin, end)); for (; begin != end; ++begin) { - if constexpr (std::is_same_v, char*>) // C strings + if constexpr (std::is_same_v, char*>) // C strings vec.emplace_back(*begin); else { - static_assert(sizeof(*begin->data()) == 1, "to_view_vector can only be used with containers of string-like types of 1-byte characters"); + static_assert( + sizeof(*begin->data()) == 1, + "to_view_vector can only be used with containers of string-like types of " + "1-byte characters"); vec.emplace_back(reinterpret_cast(begin->data()), begin->size()); } } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index e11f0b3c..a3b034ab 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1002,7 +1002,8 @@ bool Keys::load_key_message( // Ensure we consume all the ciphertexts (to ensure some message validity, even if we found // one halfway through). - while (next_ciphertext()) {} + while (next_ciphertext()) { + } if (!d.skip_until("G")) throw config_value_error{ @@ -1079,7 +1080,8 @@ bool Keys::load_key_message( // Parse them all, even once we had a successful decryption, to properly count and fail on // invalid input: - while (next_ciphertext()) {} + while (next_ciphertext()) { + } if (++member_key_pos % MESSAGE_KEY_MULTIPLE != 0) throw config_value_error{"Member key list has wrong size (missing junk key padding?)"}; diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index e4a0f3fb..64e72683 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -236,11 +236,12 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim // If we encrypt again the value should be different (because of the default randomized nonce): CHECK(encrypted != session::encrypt_for_multiple_simple( - msgs[0], - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), - "test suite")); + msgs[0], + session::to_view_vector( + std::next(recipients.begin()), std::prev(recipients.end())), + to_usv(x_keys[0].first), + to_usv(x_keys[0].second), + "test suite")); auto m1 = session::decrypt_for_multiple_simple( encrypted, From 968ef534edb9262d9b39b5e608537aef243bf72d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 23 Nov 2023 17:27:57 +1100 Subject: [PATCH 125/572] Working on exposing onion request encryption functions --- include/session/onionreq/channel_encryption.h | 52 +++++++++++++++++++ include/session/xed25519.hpp | 5 ++ src/onionreq/channel_encryption.cpp | 45 ++++++++++++++++ src/xed25519.cpp | 31 +++++++++++ 4 files changed, 133 insertions(+) create mode 100644 include/session/onionreq/channel_encryption.h diff --git a/include/session/onionreq/channel_encryption.h b/include/session/onionreq/channel_encryption.h new file mode 100644 index 00000000..830eb606 --- /dev/null +++ b/include/session/onionreq/channel_encryption.h @@ -0,0 +1,52 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "../export.h" + +typedef enum ENCRYPT_TYPE { + ENCRYPT_TYPE_AES_GCM = 0, + ENCRYPT_TYPE_X_CHA_CHA_20 = 1, +} ENCRYPT_TYPE; + +/// API: channel_encryption/onion_request_encrypt +/// +/// Wrapper around session::onionreq::channel_encryption::encrypt. message is binary: message +/// has the length provided, pubkey must be exactly 32 bytes. Returns a newly +/// allocated buffer containing the encrypted data, and sets the data's length into +/// `ciphertext_size`. It is the caller's responsibility to `free()` the returned buffer! +/// +/// Declaration: +/// ```cpp +/// UNSIGNED CHAR* onion_request_encrypt( +/// [in] ENCRYPT_TYPE type, +/// [in] const unsigned char* message, +/// [in] size_t mlen, +/// [in] const unsigned char* pubkey, +/// [out] size_t* ciphertext_size +/// ); +/// ``` +/// +/// Inputs: +/// - `type` -- [in] The type of encryption to use +/// - `message` -- [in] The message to encrypted in binary +/// - `mlen` -- [in] Length of the message provided +/// - `pubkey` -- [in] Key, must be binary +/// - `ciphertext_size` -- [out] will contain the size of the returned ciphertext +/// +/// Outputs: +/// - `unsigned char*` -- ciphertext, will be nullptr on error +LIBSESSION_EXPORT unsigned char* onion_request_encrypt( + ENCRYPT_TYPE type, + const unsigned char* message, + size_t mlen, + const unsigned char* pubkey, + size_t* ciphertext_size); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index e389e9d0..11367d52 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -3,10 +3,15 @@ #include #include +#include "util.hpp" + namespace session::xed25519 { using ustring_view = std::basic_string_view; +/// Generates a random XEd25519 key pair. +std::pair>, std::array> random(); + /// XEd25519-signs a message given the curve25519 privkey and message. std::array sign( ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg); diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/channel_encryption.cpp index afcc6b56..0b08f0a5 100644 --- a/src/onionreq/channel_encryption.cpp +++ b/src/onionreq/channel_encryption.cpp @@ -13,6 +13,12 @@ #include #include +#include "session/xed25519.hpp" +#include "session/onionreq/channel_encryption.h" +#include "session/onionreq/key_types.hpp" +#include "session/export.h" +#include "session/util.hpp" + namespace session::onionreq { namespace { @@ -225,3 +231,42 @@ ustring ChannelEncryption::decrypt_xchacha20( } } // namespace session::onionreq + +extern "C" { + +using session::ustring; + +LIBSESSION_C_API unsigned char* onion_request_encrypt( + ENCRYPT_TYPE type, + const unsigned char* message, + size_t mlen, + const unsigned char* pubkey, + size_t* ciphertext_size) { + + ustring ciphertext; + try { + session::onionreq::x25519_pubkey targetPubkey = session::onionreq::x25519_pubkey::from_bytes({pubkey, 32}); + std::pair, std::array> keys = session::xed25519::random(); + auto& [x_priv, x_pub] = keys; + + session::onionreq::ChannelEncryption c{ + session::onionreq::x25519_seckey::from_bytes(x_priv.data()), + session::onionreq::x25519_pubkey::from_bytes(x_pub.data()), + true + }; + + switch (type) { + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: ciphertext = c.encrypt_xchacha20({message, mlen}, targetPubkey); + case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: ciphertext = c.encrypt_aesgcm({message, mlen}, targetPubkey); + } + } catch (...) { + return nullptr; + } + + auto* data = static_cast(std::malloc(ciphertext.size())); + std::memcpy(data, ciphertext.data(), ciphertext.size()); + *ciphertext_size = ciphertext.size(); + return data; +} + +} diff --git a/src/xed25519.cpp b/src/xed25519.cpp index e885ff1a..c18e4038 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -12,6 +12,7 @@ #include #include "session/export.h" +#include "session/util.hpp" namespace session::xed25519 { @@ -79,6 +80,36 @@ namespace { } // namespace +std::pair>, std::array> random() { + // TODO: Is there a way we can generate this directly rather than generating a random ed25519 which then gets converted? + // bytes<32> privateKey; + // randombytes_buf(privateKey.data(), privateKey.size()); + // privateKey[0] &= 248; + // privateKey[31] &= 127; + // privateKey[31] |= 64; + + // bytes<32> publicKey; + // uint8_t basepoint[32] = { 9 }; + // int64_t bp[10], x[10], z[11], zmone[10]; + // uint8_t e[32]; + // int i; + // fexpand(bp, 9); + // curve25519_donna(publicKey.mutableBytes, privateKey.mutableBytes, basepoint); + std::pair>, std::array> ret; + auto& [x_priv, x_pub] = ret; + bytes<32> seed; + randombytes_buf(seed.data(), 16); + + std::array pk; + sodium_cleared> sk; + crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data()); + crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), sk.data()); + if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), sk.data() + 32)) + throw std::runtime_error{"Failed to convert generate random key: invalid secret key generated"}; + + return ret; +} + bytes<64> sign(ustring_view curve25519_privkey, ustring_view msg) { assert(curve25519_privkey.size() == 32); From 8e6a31c63776318d9741dbcbaf67eea1b22d17fa Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 27 Nov 2023 17:27:29 +1100 Subject: [PATCH 126/572] Progressed the onion request prepare and decrypt functions --- .gitignore | 1 + include/session/onionreq/channel_encryption.h | 177 ++++++++-- .../session/onionreq/channel_encryption.hpp | 55 ++- include/session/xed25519.hpp | 5 - src/onionreq/channel_encryption.cpp | 321 ++++++++++++++++-- src/onionreq/parser.cpp | 7 +- src/xed25519.cpp | 30 -- 7 files changed, 494 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index 52afeba1..f13ebfd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build*/ /compile_commands.json /.cache/ +/.vscode/ \ No newline at end of file diff --git a/include/session/onionreq/channel_encryption.h b/include/session/onionreq/channel_encryption.h index 830eb606..abb34570 100644 --- a/include/session/onionreq/channel_encryption.h +++ b/include/session/onionreq/channel_encryption.h @@ -5,6 +5,7 @@ extern "C" { #endif #include +#include #include "../export.h" @@ -13,39 +14,171 @@ typedef enum ENCRYPT_TYPE { ENCRYPT_TYPE_X_CHA_CHA_20 = 1, } ENCRYPT_TYPE; -/// API: channel_encryption/onion_request_encrypt +// typedef struct onion_request_snode_destination { +// char ed25519_pubkey[65]; // in hex; 64 hex chars + null terminator. +// char x25519_pubkey[65]; // in hex; 64 hex chars + null terminator. +// } onion_request_snode_destination; + +// typedef struct onion_request_server_destination { +// char host[268]; // null-terminated (max length 267) +// char target[268]; // null-terminated (max length 267) +// char protocol[5]; // http/https +// uint16_t port; // port to send request to (in unknown then use 443 for HTTPS and 80 for HTTP) +// char x25519_pubkey[65]; // in hex; 64 hex chars + null terminator. +// } onion_request_server_destination; + +/// API: onion_request_prepare_snode_destination +/// +/// Wrapper around session::onionreq::prepare. payload_in is binary: payload_in +/// has the length provided, destination_ed25519_pubkey and destination_x25519_pubkey +/// are both hex strings and must both be exactly 64 characters. Returns a flag indicating +/// success or failure. +/// +/// Declaration: +/// ```cpp +/// UNSIGNED CHAR* onion_request_encrypt( +/// [in] const char* payload_in, +/// [in] const char* destination_ed25519_pubkey, +/// [in] const char* destination_x25519_pubkey, +/// [in] const char** ed25519_pubkeys, +/// [in] const char** x25519_pubkeys, +/// [in] size_t pubkeys_len, +/// [out] unsigned char** payload_out, +/// [out] size_t* payload_out_len, +/// [out] unsigned char* final_x25519_pubkey_out, +/// [out] unsigned char* final_x25519_seckey_out +/// ); +/// ``` +/// +/// Inputs: +/// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `destination_ed25519_pubkey` -- [in] The ed25519 public key for the snode destination +/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the snode destination +/// - `ed25519_pubkeys` -- [in] array of ed25519 public keys for the onion request path +/// - `x25519_pubkeys` -- [in] array of x25519 public keys for the onion request path +/// - `pubkeys_len` -- [in] number or snodes in the path +/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error +/// - `payload_out_len` -- [out] length of payload_out if not null +/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 public key used for the onion request will be written if successful +/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 secret key used for the onion request will be written if successful +/// +/// Outputs: +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// If (and only if) true is returned then `payload_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( + const char* payload_in, + const char* destination_ed25519_pubkey, + const char* destination_x25519_pubkey, + const char** ed25519_pubkeys, + const char** x25519_pubkeys, + size_t pubkeys_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out +); + +/// API: onion_request_prepare_server_destination +/// +/// Wrapper around session::onionreq::prepare. payload_in is binary: payload_in +/// has the length provided, destination_x25519_pubkey is a hex string and must be +/// exactly 64 characters. Returns a flag indicating success or failure. +/// +/// Declaration: +/// ```cpp +/// UNSIGNED CHAR* onion_request_encrypt( +/// [in] const char* payload_in, +/// [in] const char* destination_host, +/// [in] const char* destination_target, +/// [in] const char* destination_protocol, +/// [in] uint16_t destination_port, +/// [in] const char* destination_x25519_pubkey, +/// [in] const char** ed25519_pubkeys, +/// [in] const char** x25519_pubkeys, +/// [in] size_t pubkeys_len, +/// [out] unsigned char** payload_out, +/// [out] size_t* payload_out_len, +/// [out] unsigned char* final_x25519_pubkey_out, +/// [out] unsigned char* final_x25519_seckey_out +/// ); +/// ``` +/// +/// Inputs: +/// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `destination_host` -- [in] The host for the server destination +/// - `destination_target` -- [in] The target (endpoint) for the server destination +/// - `destination_protocol` -- [in] The protocol to use for the +/// - `destination_port` -- [in] The host for the server destination +/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination +/// - `ed25519_pubkeys` -- [in] array of ed25519 public keys for the onion request path +/// - `x25519_pubkeys` -- [in] array of x25519 public keys for the onion request path +/// - `pubkeys_len` -- [in] number or snodes in the path +/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error +/// - `payload_out_len` -- [out] length of payload_out if not null +/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 public key used for the onion request will be written if successful +/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 secret key used for the onion request will be written if successful +/// +/// Outputs: +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// If (and only if) true is returned then `payload_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_prepare_server_destination( + const char* payload_in, + const char* destination_host, + const char* destination_target, + const char* destination_protocol, + uint16_t destination_port, + const char* destination_x25519_pubkey, + const char** ed25519_pubkeys, + const char** x25519_pubkeys, + size_t pubkeys_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out +); + +/// API: onion_request_decrypt /// -/// Wrapper around session::onionreq::channel_encryption::encrypt. message is binary: message -/// has the length provided, pubkey must be exactly 32 bytes. Returns a newly -/// allocated buffer containing the encrypted data, and sets the data's length into -/// `ciphertext_size`. It is the caller's responsibility to `free()` the returned buffer! +/// Wrapper around session::onionreq::decrypt. ciphertext_in is binary. +/// destination_x25519_pubkey is a hex string and must be exactly 64 characters. final_x25519_pubkey +/// and final_x25519_seckey should be in bytes and be exactly 32 bytes. +/// Returns a flag indicating success or failure. /// /// Declaration: /// ```cpp /// UNSIGNED CHAR* onion_request_encrypt( -/// [in] ENCRYPT_TYPE type, -/// [in] const unsigned char* message, -/// [in] size_t mlen, -/// [in] const unsigned char* pubkey, -/// [out] size_t* ciphertext_size +/// [in] const char* payload_in, +/// [in] const char* destination_x25519_pubkey, +/// [in] const char* final_x25519_pubkey, +/// [in] const char* final_x25519_seckey, +/// [out] unsigned char** plaintext_out, +/// [out] size_t* plaintext_out_len /// ); /// ``` /// /// Inputs: -/// - `type` -- [in] The type of encryption to use -/// - `message` -- [in] The message to encrypted in binary -/// - `mlen` -- [in] Length of the message provided -/// - `pubkey` -- [in] Key, must be binary -/// - `ciphertext_size` -- [out] will contain the size of the returned ciphertext +/// - `ciphertext_in` -- [in] The payload to be sent in the onion request +/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination +/// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request +/// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request +/// - `plaintext_out` -- [out] decrypted content contained within ciphertext_in, will be nullptr on error +/// - `plaintext_out_len` -- [out] length of plaintext_out if not null /// /// Outputs: -/// - `unsigned char*` -- ciphertext, will be nullptr on error -LIBSESSION_EXPORT unsigned char* onion_request_encrypt( - ENCRYPT_TYPE type, - const unsigned char* message, - size_t mlen, - const unsigned char* pubkey, - size_t* ciphertext_size); +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// If (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_decrypt( + const unsigned char* ciphertext_in, + const char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len +); #ifdef __cplusplus } diff --git a/include/session/onionreq/channel_encryption.hpp b/include/session/onionreq/channel_encryption.hpp index db37271c..0a60394b 100644 --- a/include/session/onionreq/channel_encryption.hpp +++ b/include/session/onionreq/channel_encryption.hpp @@ -24,6 +24,36 @@ inline constexpr std::string_view to_string(EncryptType type) { return ""sv; } +struct destination { + x25519_pubkey x25519_public_key; + + destination(x25519_pubkey public_key) : x25519_public_key{std::move(public_key)} {} + + virtual ~destination() = default; +}; + +struct snode_destination: public destination { + ed25519_pubkey ed25519_public_key; + + snode_destination(ed25519_pubkey ed25519_public_key, x25519_pubkey public_key) : + destination(public_key), ed25519_public_key{std::move(ed25519_public_key)} {} +}; + +struct server_destination : public destination { + std::string host; + std::string target; + std::string protocol; + std::optional port; + + server_destination(std::string&& host, std::string&& target, std::string&& protocol, std::optional port, + x25519_pubkey public_key) : + destination(public_key), + host{std::move(host)}, + target{std::move(target)}, + protocol{std::move(protocol)}, + port{port} {} +}; + // Encryption/decription class for encryption/decrypting outgoing/incoming messages. class ChannelEncryption { public: @@ -35,12 +65,12 @@ class ChannelEncryption { // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. // `reply` should be false for a client-to-snode message, and true on a returning // snode-to-client message. - ustring encrypt(EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const; - ustring decrypt(EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const; + ustring encrypt(EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const; + ustring decrypt(EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const; // AES-GCM encryption. - ustring encrypt_aesgcm(ustring_view plainText, const x25519_pubkey& pubKey) const; - ustring decrypt_aesgcm(ustring_view cipherText, const x25519_pubkey& pubKey) const; + ustring encrypt_aesgcm(ustring plainText, const x25519_pubkey& pubKey) const; + ustring decrypt_aesgcm(ustring cipherText, const x25519_pubkey& pubKey) const; // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of @@ -50,8 +80,8 @@ class ChannelEncryption { // When Bob (the server) encrypts a method for Alice (the client), he uses shared key // H(bA || A || B) (note that this is *different* that what would result if Bob was a client // sending to Alice the client). - ustring encrypt_xchacha20(ustring_view plaintext, const x25519_pubkey& pubKey) const; - ustring decrypt_xchacha20(ustring_view ciphertext, const x25519_pubkey& pubKey) const; + ustring encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const; + ustring decrypt_xchacha20(ustring ciphertext, const x25519_pubkey& pubKey) const; private: const x25519_seckey private_key_; @@ -59,4 +89,17 @@ class ChannelEncryption { bool server_; // True if we are the server (i.e. the snode). }; +std::pair prepare( + std::string_view payload, + destination& destination, + std::vector> keys, + std::optional enc_type); + +ustring decrypt( + ustring ciphertext, + const x25519_pubkey destinationPubkey, + const x25519_pubkey finalPubkey, + const x25519_seckey finalSeckey, + std::optional enc_type); + } // namespace session::onionreq diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index 11367d52..e389e9d0 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -3,15 +3,10 @@ #include #include -#include "util.hpp" - namespace session::xed25519 { using ustring_view = std::basic_string_view; -/// Generates a random XEd25519 key pair. -std::pair>, std::array> random(); - /// XEd25519-signs a message given the curve25519 privkey and message. std::array sign( ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg); diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/channel_encryption.cpp index 0b08f0a5..af3d346b 100644 --- a/src/onionreq/channel_encryption.cpp +++ b/src/onionreq/channel_encryption.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include +#include #include #include #include @@ -75,6 +77,19 @@ namespace { return key; } + ustring encode_size(uint32_t s) { + ustring str{reinterpret_cast(&s), 4}; + return str; + } + + std::basic_string_view to_uchar(std::string_view sv) { + return {reinterpret_cast(sv.data()), sv.size()}; + } + + std::string from_ustring(ustring us) { + return {reinterpret_cast(us.data()), us.size()}; + } + } // namespace EncryptType parse_enc_type(std::string_view enc_type) { @@ -86,7 +101,7 @@ EncryptType parse_enc_type(std::string_view enc_type) { } ustring ChannelEncryption::encrypt( - EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const { + EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); case EncryptType::aes_gcm: return encrypt_aesgcm(plaintext, pubkey); @@ -95,7 +110,7 @@ ustring ChannelEncryption::encrypt( } ustring ChannelEncryption::decrypt( - EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const { + EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); case EncryptType::aes_gcm: return decrypt_aesgcm(ciphertext, pubkey); @@ -104,8 +119,7 @@ ustring ChannelEncryption::decrypt( } ustring ChannelEncryption::encrypt_aesgcm( - ustring_view plaintext, const x25519_pubkey& pubKey) const { - + ustring plaintext, const x25519_pubkey& pubKey) const { auto key = derive_symmetric_key(private_key_, pubKey); // Initialise cipher context with the key @@ -136,7 +150,8 @@ ustring ChannelEncryption::encrypt_aesgcm( } ustring ChannelEncryption::decrypt_aesgcm( - ustring_view ciphertext, const x25519_pubkey& pubKey) const { + ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) throw std::runtime_error{"ciphertext data is too short"}; @@ -169,7 +184,7 @@ ustring ChannelEncryption::decrypt_aesgcm( } ustring ChannelEncryption::encrypt_xchacha20( - ustring_view plaintext, const x25519_pubkey& pubKey) const { + ustring plaintext, const x25519_pubkey& pubKey) const { ustring ciphertext; ciphertext.resize( @@ -181,7 +196,8 @@ ustring ChannelEncryption::encrypt_xchacha20( // Generate random nonce, and stash it at the beginning of ciphertext: randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - auto* c = ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + auto* c = reinterpret_cast(ciphertext.data()) + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; unsigned long long clen; crypto_aead_xchacha20poly1305_ietf_encrypt( @@ -192,7 +208,7 @@ ustring ChannelEncryption::encrypt_xchacha20( nullptr, 0, // additional data nullptr, // nsec (always unused) - ciphertext.data(), + reinterpret_cast(ciphertext.data()), key.data()); assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); @@ -200,7 +216,8 @@ ustring ChannelEncryption::encrypt_xchacha20( } ustring ChannelEncryption::decrypt_xchacha20( - ustring_view ciphertext, const x25519_pubkey& pubKey) const { + ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -212,7 +229,7 @@ ustring ChannelEncryption::decrypt_xchacha20( ustring plaintext; plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - auto* m = plaintext.data(); + auto* m = reinterpret_cast(plaintext.data()); unsigned long long mlen; if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( m, @@ -230,43 +247,275 @@ ustring ChannelEncryption::decrypt_xchacha20( return plaintext; } +std::pair prepare( + std::string_view payload, + destination& destination, + std::vector> keys, + std::optional enc_type) { + + ustring blob; + + // First hop: + // + // [N][ENCRYPTED]{json} + // + // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. + // The first hop decrypts ENCRYPTED into: + // + // [N][BLOB]{json} + // + // where [N] is the length of the blob and {json} now contains either: + // - a "headers" key with an empty value. This is how we indicate that the request is for this + // node as the final hop, and means that the BLOB is actually JSON it should parse to get the + // request info (which has "method", "params", etc. in it). + // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made + // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a + // blob here, but it is not used and typically empty). + // - "destination" and "ephemeral_key" to forward the request to the next hop. + // + // This later case continues onion routing by giving us something like: + // + // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} + // + // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via + // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go + // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not + // json): + // + // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } + // + // where BLOB is the opaque data received from the previous hop and N is the hop number which + // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, + // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. + // + // On the *return* trip, the message gets encrypted (once!) at the final destination using the + // derived key from the pubkey given to the final hop, base64-encoded, then passed back without + // any onion encryption at all all the way back to the client. + + // Ephemeral keypair: + x25519_pubkey A; + x25519_seckey a; + x25519_pubkey final_pubkey; + x25519_seckey final_seckey; + nlohmann::json final_route; + EncryptType etype = enc_type.value_or(EncryptType::xchacha20); + + { + crypto_box_keypair(A.data(), a.data()); + ChannelEncryption e{a, A, false}; + + // The data we send to the destination differs depending on whether the destination is a server + // or a service node + if (auto server = dynamic_cast(&destination)) { + final_route = { + {"host", server->host}, + {"target", server->target}, + {"method", "POST"}, + {"protocol", server->protocol}, + {"port", server->port.value_or(server->protocol == "https" ? 443 : 80)}, + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(etype)}, + }; + + blob = e.encrypt(etype, to_uchar(payload).data(), server->x25519_public_key); + } else if (auto snode = dynamic_cast(&destination)) { + nlohmann::json control{ + {"headers", ""} + }; + final_route = { + {"destination", snode->ed25519_public_key.hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(etype)}, + }; + + auto data = encode_size(payload.size()); + data += to_uchar(payload); + data += to_uchar(control.dump()); + blob = e.encrypt(etype, data, snode->x25519_public_key); + } else { + throw std::runtime_error{"Invalid destination type"}; + } + + // Save these because we need them again to decrypt the final response: + final_seckey = a; + final_pubkey = A; + } + + for (auto it = keys.rbegin(); it != keys.rend(); ++it) { + // Routing data for this hop: + nlohmann::json routing; + + if (it == keys.rbegin()) { + routing = final_route; + } + else { + routing = { + {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(etype)}, + }; + } + + auto data = encode_size(blob.size()); + data += blob; + data += to_uchar(routing.dump()); + + // Generate eph key for *this* request and encrypt it: + crypto_box_keypair(A.data(), a.data()); + ChannelEncryption e{a, A, false}; + blob = e.encrypt(etype, data, it->second); + } + + // The data going to the first hop needs to be wrapped in one more layer to tell the first hop + // how to decrypt the initial payload: + auto result = encode_size(blob.size()); + result += blob; + result += to_uchar(nlohmann::json{ + {"ephemeral_key", A.hex()}, + {"enc_type", to_string(etype)} + }.dump()); + + return {result, {final_pubkey, final_seckey}}; +} + +ustring decrypt( + ustring ciphertext, + const x25519_pubkey destinationPubkey, + const x25519_pubkey finalPubkey, + const x25519_seckey finalSeckey, + std::optional enc_type) { + ChannelEncryption d{finalSeckey, finalPubkey, false}; + EncryptType etype = enc_type.value_or(EncryptType::xchacha20); + + return d.decrypt(etype, ciphertext, destinationPubkey); +} + } // namespace session::onionreq extern "C" { using session::ustring; -LIBSESSION_C_API unsigned char* onion_request_encrypt( - ENCRYPT_TYPE type, - const unsigned char* message, - size_t mlen, - const unsigned char* pubkey, - size_t* ciphertext_size) { +LIBSESSION_C_API bool onion_request_prepare_snode_destination( + const char* payload_in, + const char* destination_ed25519_pubkey, + const char* destination_x25519_pubkey, + const char** ed25519_pubkeys, + const char** x25519_pubkeys, + size_t pubkeys_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out +) { + assert(payload_in && destination_ed25519_pubkey && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys); + + session::onionreq::snode_destination destination = { + session::onionreq::ed25519_pubkey::from_hex({destination_ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}) + }; + std::vector> keys; + for (size_t i = 0; i < pubkeys_len; i++) + keys.emplace_back( + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkeys[i], 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkeys[i], 64}) + ); + + try { + auto result = session::onionreq::prepare( + payload_in, + destination, + keys, + session::onionreq::EncryptType::aes_gcm// xchacha20 + ); + + auto [payload, final_key_pair] = result; + *payload_out = static_cast(malloc(payload.size())); + *payload_out_len = payload.size(); + std::memcpy(*payload_out, payload.data(), payload.size()); + std::memcpy(final_x25519_pubkey_out, final_key_pair.first.data(), final_key_pair.first.size()); + std::memcpy(final_x25519_seckey_out, final_key_pair.second.data(), final_key_pair.second.size()); + return true; + } + catch (...) { + return false; + } +} - ustring ciphertext; +LIBSESSION_C_API bool onion_request_prepare_server_destination( + const char* payload_in, + const char* destination_host, + const char* destination_target, + const char* destination_protocol, + uint16_t destination_port, + const char* destination_x25519_pubkey, + const char** ed25519_pubkeys, + const char** x25519_pubkeys, + size_t pubkeys_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out +) { + assert(payload_in && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys); + + session::onionreq::server_destination destination = { + destination_host, + destination_target, + destination_protocol, + destination_port, + session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}) + }; + std::vector> keys; + for (size_t i = 0; i < pubkeys_len; i++) + keys.emplace_back( + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkeys[i], 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkeys[i], 64}) + ); + try { - session::onionreq::x25519_pubkey targetPubkey = session::onionreq::x25519_pubkey::from_bytes({pubkey, 32}); - std::pair, std::array> keys = session::xed25519::random(); - auto& [x_priv, x_pub] = keys; - - session::onionreq::ChannelEncryption c{ - session::onionreq::x25519_seckey::from_bytes(x_priv.data()), - session::onionreq::x25519_pubkey::from_bytes(x_pub.data()), - true - }; + auto result = session::onionreq::prepare( + payload_in, + destination, + keys, + session::onionreq::EncryptType::aes_gcm// xchacha20 + ); - switch (type) { - case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: ciphertext = c.encrypt_xchacha20({message, mlen}, targetPubkey); - case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: ciphertext = c.encrypt_aesgcm({message, mlen}, targetPubkey); - } - } catch (...) { - return nullptr; + auto [payload, final_key_pair] = result; + *payload_out = static_cast(malloc(payload.size())); + *payload_out_len = payload.size(); + std::memcpy(*payload_out, payload.data(), payload.size()); + std::memcpy(final_x25519_pubkey_out, final_key_pair.first.data(), final_key_pair.first.size()); + std::memcpy(final_x25519_seckey_out, final_key_pair.second.data(), final_key_pair.second.size()); + return true; } + catch (...) { + return false; + } +} - auto* data = static_cast(std::malloc(ciphertext.size())); - std::memcpy(data, ciphertext.data(), ciphertext.size()); - *ciphertext_size = ciphertext.size(); - return data; +LIBSESSION_C_API bool onion_request_decrypt( + const unsigned char* ciphertext_in, + const char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len +) { + assert(ciphertext_in && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey); + + try { + auto result = session::onionreq::decrypt( + ciphertext_in, + session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), + session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), + session::onionreq::EncryptType::xchacha20 + ); + } + catch (...) { + return false; + } } } diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index a1167a34..f563be89 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -35,12 +35,13 @@ OnionReqParser::OnionReqParser( remote_pk = parse_x25519_pubkey(itr->get()); else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - - payload_ = enc.decrypt(enc_type, ciphertext, remote_pk); + + auto plaintext = enc.decrypt(enc_type, reinterpret_cast(ciphertext.data()), remote_pk); + payload_ = {to_unsigned(plaintext.data()), plaintext.size()}; } ustring OnionReqParser::encrypt_reply(ustring_view reply) const { - return enc.encrypt(enc_type, reply, remote_pk); + return enc.encrypt(enc_type, reinterpret_cast(reply.data()), remote_pk); } } // namespace session::onionreq diff --git a/src/xed25519.cpp b/src/xed25519.cpp index c18e4038..420512aa 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -80,36 +80,6 @@ namespace { } // namespace -std::pair>, std::array> random() { - // TODO: Is there a way we can generate this directly rather than generating a random ed25519 which then gets converted? - // bytes<32> privateKey; - // randombytes_buf(privateKey.data(), privateKey.size()); - // privateKey[0] &= 248; - // privateKey[31] &= 127; - // privateKey[31] |= 64; - - // bytes<32> publicKey; - // uint8_t basepoint[32] = { 9 }; - // int64_t bp[10], x[10], z[11], zmone[10]; - // uint8_t e[32]; - // int i; - // fexpand(bp, 9); - // curve25519_donna(publicKey.mutableBytes, privateKey.mutableBytes, basepoint); - std::pair>, std::array> ret; - auto& [x_priv, x_pub] = ret; - bytes<32> seed; - randombytes_buf(seed.data(), 16); - - std::array pk; - sodium_cleared> sk; - crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data()); - crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), sk.data()); - if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), sk.data() + 32)) - throw std::runtime_error{"Failed to convert generate random key: invalid secret key generated"}; - - return ret; -} - bytes<64> sign(ustring_view curve25519_privkey, ustring_view msg) { assert(curve25519_privkey.size() == 32); From de56efdcc8f48d499b6d521433a2b0a9edac01ba Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 28 Nov 2023 18:07:42 +1100 Subject: [PATCH 127/572] Got the onion requests working via the C interface --- include/session/onionreq/channel_encryption.h | 39 +++++++----- .../session/onionreq/channel_encryption.hpp | 2 +- src/onionreq/channel_encryption.cpp | 61 ++++++++++--------- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/include/session/onionreq/channel_encryption.h b/include/session/onionreq/channel_encryption.h index abb34570..528c3170 100644 --- a/include/session/onionreq/channel_encryption.h +++ b/include/session/onionreq/channel_encryption.h @@ -36,8 +36,9 @@ typedef enum ENCRYPT_TYPE { /// /// Declaration: /// ```cpp -/// UNSIGNED CHAR* onion_request_encrypt( -/// [in] const char* payload_in, +/// UNSIGNED CHAR* onion_request_prepare_snode_destination( +/// [in] const unsigned char* payload_in, +/// [in] size_t payload_in_len, /// [in] const char* destination_ed25519_pubkey, /// [in] const char* destination_x25519_pubkey, /// [in] const char** ed25519_pubkeys, @@ -52,6 +53,7 @@ typedef enum ENCRYPT_TYPE { /// /// Inputs: /// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `payload_in_len` -- [in] The length of the payload_in /// - `destination_ed25519_pubkey` -- [in] The ed25519 public key for the snode destination /// - `destination_x25519_pubkey` -- [in] The x25519 public key for the snode destination /// - `ed25519_pubkeys` -- [in] array of ed25519 public keys for the onion request path @@ -68,7 +70,8 @@ typedef enum ENCRYPT_TYPE { /// - `bool` -- True if the onion request was successfully constructed, false if it failed. /// If (and only if) true is returned then `payload_out` must be freed when done with it. LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( - const char* payload_in, + const unsigned char* payload_in, + size_t payload_in_len, const char* destination_ed25519_pubkey, const char* destination_x25519_pubkey, const char** ed25519_pubkeys, @@ -88,8 +91,9 @@ LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( /// /// Declaration: /// ```cpp -/// UNSIGNED CHAR* onion_request_encrypt( -/// [in] const char* payload_in, +/// UNSIGNED CHAR* onion_request_prepare_server_destination( +/// [in] const unsigned char* payload_in, +/// [in] size_t payload_in_len, /// [in] const char* destination_host, /// [in] const char* destination_target, /// [in] const char* destination_protocol, @@ -107,6 +111,7 @@ LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( /// /// Inputs: /// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `payload_in_len` -- [in] The length of the payload_in /// - `destination_host` -- [in] The host for the server destination /// - `destination_target` -- [in] The target (endpoint) for the server destination /// - `destination_protocol` -- [in] The protocol to use for the @@ -126,7 +131,8 @@ LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( /// - `bool` -- True if the onion request was successfully constructed, false if it failed. /// If (and only if) true is returned then `payload_out` must be freed when done with it. LIBSESSION_EXPORT bool onion_request_prepare_server_destination( - const char* payload_in, + const unsigned char* payload_in, + size_t payload_in_len, const char* destination_host, const char* destination_target, const char* destination_protocol, @@ -144,14 +150,15 @@ LIBSESSION_EXPORT bool onion_request_prepare_server_destination( /// API: onion_request_decrypt /// /// Wrapper around session::onionreq::decrypt. ciphertext_in is binary. -/// destination_x25519_pubkey is a hex string and must be exactly 64 characters. final_x25519_pubkey -/// and final_x25519_seckey should be in bytes and be exactly 32 bytes. -/// Returns a flag indicating success or failure. +/// destination_x25519_pubkey, final_x25519_pubkey and final_x25519_seckey +/// should be in bytes and be exactly 32 bytes. Returns a flag indicating +/// success or failure. /// /// Declaration: /// ```cpp -/// UNSIGNED CHAR* onion_request_encrypt( -/// [in] const char* payload_in, +/// bool onion_request_decrypt( +/// [in] const unsigned char* ciphertext, +/// [in] size_t ciphertext_len, /// [in] const char* destination_x25519_pubkey, /// [in] const char* final_x25519_pubkey, /// [in] const char* final_x25519_seckey, @@ -161,19 +168,21 @@ LIBSESSION_EXPORT bool onion_request_prepare_server_destination( /// ``` /// /// Inputs: -/// - `ciphertext_in` -- [in] The payload to be sent in the onion request +/// - `ciphertext` -- [in] The onion request response data +/// - `ciphertext_len` -- [in] The length of ciphertext /// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination /// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request /// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request -/// - `plaintext_out` -- [out] decrypted content contained within ciphertext_in, will be nullptr on error +/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on error /// - `plaintext_out_len` -- [out] length of plaintext_out if not null /// /// Outputs: /// - `bool` -- True if the onion request was successfully constructed, false if it failed. /// If (and only if) true is returned then `plaintext_out` must be freed when done with it. LIBSESSION_EXPORT bool onion_request_decrypt( - const unsigned char* ciphertext_in, - const char* destination_x25519_pubkey, + const unsigned char* ciphertext, + size_t ciphertext_len, + unsigned char* destination_x25519_pubkey, unsigned char* final_x25519_pubkey, unsigned char* final_x25519_seckey, unsigned char** plaintext_out, diff --git a/include/session/onionreq/channel_encryption.hpp b/include/session/onionreq/channel_encryption.hpp index 0a60394b..7a9e5326 100644 --- a/include/session/onionreq/channel_encryption.hpp +++ b/include/session/onionreq/channel_encryption.hpp @@ -90,7 +90,7 @@ class ChannelEncryption { }; std::pair prepare( - std::string_view payload, + ustring payload, destination& destination, std::vector> keys, std::optional enc_type); diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/channel_encryption.cpp index af3d346b..fa1ab0d6 100644 --- a/src/onionreq/channel_encryption.cpp +++ b/src/onionreq/channel_encryption.cpp @@ -1,6 +1,7 @@ #include "session/onionreq/channel_encryption.hpp" #include +#include #include #include #include @@ -78,16 +79,10 @@ namespace { } ustring encode_size(uint32_t s) { - ustring str{reinterpret_cast(&s), 4}; - return str; - } - - std::basic_string_view to_uchar(std::string_view sv) { - return {reinterpret_cast(sv.data()), sv.size()}; - } - - std::string from_ustring(ustring us) { - return {reinterpret_cast(us.data()), us.size()}; + ustring result; + result.resize(4); + oxenc::write_host_as_little(s, result.data()); + return result; } } // namespace @@ -248,7 +243,7 @@ ustring ChannelEncryption::decrypt_xchacha20( } std::pair prepare( - std::string_view payload, + ustring payload, destination& destination, std::vector> keys, std::optional enc_type) { @@ -317,7 +312,7 @@ std::pair prepare( {"enc_type", to_string(etype)}, }; - blob = e.encrypt(etype, to_uchar(payload).data(), server->x25519_public_key); + blob = e.encrypt(etype, payload.data(), server->x25519_public_key); } else if (auto snode = dynamic_cast(&destination)) { nlohmann::json control{ {"headers", ""} @@ -329,8 +324,8 @@ std::pair prepare( }; auto data = encode_size(payload.size()); - data += to_uchar(payload); - data += to_uchar(control.dump()); + data += payload; + data += to_unsigned_sv(control.dump()); blob = e.encrypt(etype, data, snode->x25519_public_key); } else { throw std::runtime_error{"Invalid destination type"}; @@ -358,7 +353,7 @@ std::pair prepare( auto data = encode_size(blob.size()); data += blob; - data += to_uchar(routing.dump()); + data += to_unsigned_sv(routing.dump()); // Generate eph key for *this* request and encrypt it: crypto_box_keypair(A.data(), a.data()); @@ -370,7 +365,7 @@ std::pair prepare( // how to decrypt the initial payload: auto result = encode_size(blob.size()); result += blob; - result += to_uchar(nlohmann::json{ + result += to_unsigned_sv(nlohmann::json{ {"ephemeral_key", A.hex()}, {"enc_type", to_string(etype)} }.dump()); @@ -397,7 +392,8 @@ extern "C" { using session::ustring; LIBSESSION_C_API bool onion_request_prepare_snode_destination( - const char* payload_in, + const unsigned char* payload_in, + size_t payload_in_len, const char* destination_ed25519_pubkey, const char* destination_x25519_pubkey, const char** ed25519_pubkeys, @@ -408,7 +404,7 @@ LIBSESSION_C_API bool onion_request_prepare_snode_destination( unsigned char* final_x25519_pubkey_out, unsigned char* final_x25519_seckey_out ) { - assert(payload_in && destination_ed25519_pubkey && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys); + assert(payload_in && destination_ed25519_pubkey && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys && payload_in_len > 0 && pubkeys_len > 0); session::onionreq::snode_destination destination = { session::onionreq::ed25519_pubkey::from_hex({destination_ed25519_pubkey, 64}), @@ -423,10 +419,10 @@ LIBSESSION_C_API bool onion_request_prepare_snode_destination( try { auto result = session::onionreq::prepare( - payload_in, + ustring{payload_in, payload_in_len}, destination, keys, - session::onionreq::EncryptType::aes_gcm// xchacha20 + session::onionreq::EncryptType::xchacha20 ); auto [payload, final_key_pair] = result; @@ -443,7 +439,8 @@ LIBSESSION_C_API bool onion_request_prepare_snode_destination( } LIBSESSION_C_API bool onion_request_prepare_server_destination( - const char* payload_in, + const unsigned char* payload_in, + size_t payload_in_len, const char* destination_host, const char* destination_target, const char* destination_protocol, @@ -457,7 +454,7 @@ LIBSESSION_C_API bool onion_request_prepare_server_destination( unsigned char* final_x25519_pubkey_out, unsigned char* final_x25519_seckey_out ) { - assert(payload_in && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys); + assert(payload_in && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys && pubkeys_len > 0); session::onionreq::server_destination destination = { destination_host, @@ -475,10 +472,10 @@ LIBSESSION_C_API bool onion_request_prepare_server_destination( try { auto result = session::onionreq::prepare( - payload_in, + ustring{payload_in, payload_in_len}, destination, keys, - session::onionreq::EncryptType::aes_gcm// xchacha20 + session::onionreq::EncryptType::xchacha20 ); auto [payload, final_key_pair] = result; @@ -487,6 +484,7 @@ LIBSESSION_C_API bool onion_request_prepare_server_destination( std::memcpy(*payload_out, payload.data(), payload.size()); std::memcpy(final_x25519_pubkey_out, final_key_pair.first.data(), final_key_pair.first.size()); std::memcpy(final_x25519_seckey_out, final_key_pair.second.data(), final_key_pair.second.size()); + return true; } catch (...) { @@ -495,23 +493,28 @@ LIBSESSION_C_API bool onion_request_prepare_server_destination( } LIBSESSION_C_API bool onion_request_decrypt( - const unsigned char* ciphertext_in, - const char* destination_x25519_pubkey, + const unsigned char* ciphertext, + size_t ciphertext_len, + unsigned char* destination_x25519_pubkey, unsigned char* final_x25519_pubkey, unsigned char* final_x25519_seckey, unsigned char** plaintext_out, size_t* plaintext_out_len ) { - assert(ciphertext_in && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey); + assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && ciphertext_len > 0); try { auto result = session::onionreq::decrypt( - ciphertext_in, - session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}), + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32}), session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), session::onionreq::EncryptType::xchacha20 ); + *plaintext_out = static_cast(malloc(result.size())); + *plaintext_out_len = result.size(); + std::memcpy(*plaintext_out, result.data(), result.size()); + return true; } catch (...) { return false; From 10c073a7d8b9540a42d7d86fdf376ce1c086b3d7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 29 Nov 2023 17:03:37 +1100 Subject: [PATCH 128/572] Updated the onion request interface to be builder-based --- include/session/onionreq/builder.h | 178 ++++++ include/session/onionreq/builder.hpp | 80 +++ include/session/onionreq/channel_encryption.h | 194 ------- .../session/onionreq/channel_encryption.hpp | 105 ---- include/session/onionreq/hop_encryption.hpp | 46 ++ include/session/onionreq/parser.hpp | 4 +- include/session/onionreq/response_parser.h | 61 ++ include/session/onionreq/response_parser.hpp | 31 ++ src/CMakeLists.txt | 4 +- src/onionreq/builder.cpp | 290 ++++++++++ src/onionreq/channel_encryption.cpp | 524 ------------------ src/onionreq/hop_encryption.cpp | 230 ++++++++ src/onionreq/response_parser.cpp | 89 +++ tests/test_onionreq.cpp | 6 +- 14 files changed, 1013 insertions(+), 829 deletions(-) create mode 100644 include/session/onionreq/builder.h create mode 100644 include/session/onionreq/builder.hpp delete mode 100644 include/session/onionreq/channel_encryption.h delete mode 100644 include/session/onionreq/channel_encryption.hpp create mode 100644 include/session/onionreq/hop_encryption.hpp create mode 100644 include/session/onionreq/response_parser.h create mode 100644 include/session/onionreq/response_parser.hpp create mode 100644 src/onionreq/builder.cpp delete mode 100644 src/onionreq/channel_encryption.cpp create mode 100644 src/onionreq/hop_encryption.cpp create mode 100644 src/onionreq/response_parser.cpp diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h new file mode 100644 index 00000000..de378c50 --- /dev/null +++ b/include/session/onionreq/builder.h @@ -0,0 +1,178 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "../export.h" + +typedef enum ENCRYPT_TYPE { + ENCRYPT_TYPE_AES_GCM = 0, + ENCRYPT_TYPE_X_CHA_CHA_20 = 1, +} ENCRYPT_TYPE; + +typedef struct onion_request_builder_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; + + ENCRYPT_TYPE enc_type; +} onion_request_builder_object; + +/// API: groups/onion_request_builder_init +/// +/// Constructs an onion request builder and sets a pointer to it in `builder`. +/// +/// When done with the object the `builder` must be destroyed by either passing the pointer to +/// onion_request_builder_free() or onion_request_builder_build(). +/// +/// Inputs: +/// - `builder` -- [out] Pointer to the builder object +LIBSESSION_EXPORT void onion_request_builder_init( + onion_request_builder_object** builder); + +/// API: onion_request_builder_set_enc_type +/// +/// Wrapper around session::onionreq::Builder::onion_request_builder_set_enc_type. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_enc_type( +/// [in] onion_request_builder_object* builder +/// [in] ENCRYPT_TYPE enc_type +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `enc_type` -- [in] The encryption type to use in the onion request +LIBSESSION_EXPORT void onion_request_builder_set_enc_type( + onion_request_builder_object* builder, + ENCRYPT_TYPE enc_type); + +/// API: onion_request_builder_set_snode_destination +/// +/// Wrapper around session::onionreq::Builder::set_snode_destination. ed25519_pubkey and +/// x25519_pubkey are both hex strings and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_snode_destination( +/// [in] onion_request_builder_object* builder +/// [in] const char* ed25519_pubkey, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); + +/// API: onion_request_builder_set_server_destination +/// +/// Wrapper around session::onionreq::Builder::set_server_destination. x25519_pubkey +/// is a hex string and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_server_destination( +/// [in] onion_request_builder_object* builder +/// [in] const char* host, +/// [in] const char* target, +/// [in] const char* protocol, +/// [in] uint16_t port, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `host` -- [in] The host for the server destination +/// - `target` -- [in] The target (endpoint) for the server destination +/// - `protocol` -- [in] The protocol to use for the +/// - `port` -- [in] The host for the server destination +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +LIBSESSION_EXPORT void onion_request_builder_set_server_destination( + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey); + +/// API: onion_request_builder_add_hop +/// +/// Wrapper around session::onionreq::Builder::add_hop. ed25519_pubkey and +/// x25519_pubkey are both hex strings and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_add_hop( +/// [in] onion_request_builder_object* builder +/// [in] const char* ed25519_pubkey, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode hop +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode hop +LIBSESSION_EXPORT void onion_request_builder_add_hop( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); + +/// API: onion_request_builder_build +/// +/// Wrapper around session::onionreq::Builder::build. payload_in is binary: payload_in +/// has the length provided, destination_ed25519_pubkey and destination_x25519_pubkey +/// are both hex strings and must both be exactly 64 characters. Returns a flag indicating +/// success or failure. +/// +/// Declaration: +/// ```cpp +/// bool onion_request_builder_build( +/// [in] onion_request_builder_object* builder +/// [in] const unsigned char* payload_in, +/// [in] size_t payload_in_len, +/// [out] unsigned char** payload_out, +/// [out] size_t* payload_out_len, +/// [out] unsigned char* final_x25519_pubkey_out, +/// [out] unsigned char* final_x25519_seckey_out +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `payload_in_len` -- [in] The length of the payload_in +/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error +/// - `payload_out_len` -- [out] length of payload_out if not null +/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 public key used for the onion request will be written if successful +/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 secret key used for the onion request will be written if successful +/// +/// Outputs: +/// - `bool` -- True if the onion request payload was successfully constructed, false if it failed. +/// If (and only if) true is returned then `payload_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_builder_build( + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp new file mode 100644 index 00000000..011b4b9e --- /dev/null +++ b/include/session/onionreq/builder.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +#include "key_types.hpp" + +namespace session::onionreq { + +enum class EncryptType { + aes_gcm, + xchacha20, +}; + +// Takes the encryption type as a string, returns the EncryptType value (or throws if invalid). +// Supported values: aes-gcm and xchacha20. gcm is accepted as an aliases for aes-gcm. +EncryptType parse_enc_type(std::string_view enc_type); + +inline constexpr std::string_view to_string(EncryptType type) { + switch (type) { + case EncryptType::xchacha20: return "xchacha20"sv; + case EncryptType::aes_gcm: return "aes-gcm"sv; + } + return ""sv; +} + +// Builder class for preparing onion request payloads. +class Builder { + public: + EncryptType enc_type; + std::optional destination_x25519_public_key = std::nullopt; + std::optional final_hop_x25519_keypair = std::nullopt; + + Builder(EncryptType enc_type_ = EncryptType::xchacha20) : enc_type{enc_type_} {} + + void set_enc_type(EncryptType enc_type_) { enc_type = enc_type_; } + + void set_snode_destination(ed25519_pubkey ed25519_public_key, x25519_pubkey x25519_public_key) { + destination_x25519_public_key.reset(); + ed25519_public_key_.reset(); + destination_x25519_public_key.emplace(x25519_public_key); + ed25519_public_key_.emplace(ed25519_public_key); + } + + void set_server_destination(std::string host, std::string target, std::string protocol, std::optional port, + x25519_pubkey x25519_public_key) { + destination_x25519_public_key.reset(); + + host_.emplace(host); + target_.emplace(target); + protocol_.emplace(protocol); + + if (port) + port_.emplace(*port); + + destination_x25519_public_key.emplace(x25519_public_key); + } + + void add_hop(std::pair keys) { + hops_.push_back(keys); + } + + ustring build(ustring payload); + + private: + std::vector> hops_ = {}; + + // Snode request values + + std::optional ed25519_public_key_ = std::nullopt; + + // Proxied request values + + std::optional host_ = std::nullopt; + std::optional target_ = std::nullopt; + std::optional protocol_ = std::nullopt; + std::optional port_ = std::nullopt; +}; + +} // namespace session::onionreq diff --git a/include/session/onionreq/channel_encryption.h b/include/session/onionreq/channel_encryption.h deleted file mode 100644 index 528c3170..00000000 --- a/include/session/onionreq/channel_encryption.h +++ /dev/null @@ -1,194 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -#include "../export.h" - -typedef enum ENCRYPT_TYPE { - ENCRYPT_TYPE_AES_GCM = 0, - ENCRYPT_TYPE_X_CHA_CHA_20 = 1, -} ENCRYPT_TYPE; - -// typedef struct onion_request_snode_destination { -// char ed25519_pubkey[65]; // in hex; 64 hex chars + null terminator. -// char x25519_pubkey[65]; // in hex; 64 hex chars + null terminator. -// } onion_request_snode_destination; - -// typedef struct onion_request_server_destination { -// char host[268]; // null-terminated (max length 267) -// char target[268]; // null-terminated (max length 267) -// char protocol[5]; // http/https -// uint16_t port; // port to send request to (in unknown then use 443 for HTTPS and 80 for HTTP) -// char x25519_pubkey[65]; // in hex; 64 hex chars + null terminator. -// } onion_request_server_destination; - -/// API: onion_request_prepare_snode_destination -/// -/// Wrapper around session::onionreq::prepare. payload_in is binary: payload_in -/// has the length provided, destination_ed25519_pubkey and destination_x25519_pubkey -/// are both hex strings and must both be exactly 64 characters. Returns a flag indicating -/// success or failure. -/// -/// Declaration: -/// ```cpp -/// UNSIGNED CHAR* onion_request_prepare_snode_destination( -/// [in] const unsigned char* payload_in, -/// [in] size_t payload_in_len, -/// [in] const char* destination_ed25519_pubkey, -/// [in] const char* destination_x25519_pubkey, -/// [in] const char** ed25519_pubkeys, -/// [in] const char** x25519_pubkeys, -/// [in] size_t pubkeys_len, -/// [out] unsigned char** payload_out, -/// [out] size_t* payload_out_len, -/// [out] unsigned char* final_x25519_pubkey_out, -/// [out] unsigned char* final_x25519_seckey_out -/// ); -/// ``` -/// -/// Inputs: -/// - `payload_in` -- [in] The payload to be sent in the onion request -/// - `payload_in_len` -- [in] The length of the payload_in -/// - `destination_ed25519_pubkey` -- [in] The ed25519 public key for the snode destination -/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the snode destination -/// - `ed25519_pubkeys` -- [in] array of ed25519 public keys for the onion request path -/// - `x25519_pubkeys` -- [in] array of x25519 public keys for the onion request path -/// - `pubkeys_len` -- [in] number or snodes in the path -/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error -/// - `payload_out_len` -- [out] length of payload_out if not null -/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final -/// x25519 public key used for the onion request will be written if successful -/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final -/// x25519 secret key used for the onion request will be written if successful -/// -/// Outputs: -/// - `bool` -- True if the onion request was successfully constructed, false if it failed. -/// If (and only if) true is returned then `payload_out` must be freed when done with it. -LIBSESSION_EXPORT bool onion_request_prepare_snode_destination( - const unsigned char* payload_in, - size_t payload_in_len, - const char* destination_ed25519_pubkey, - const char* destination_x25519_pubkey, - const char** ed25519_pubkeys, - const char** x25519_pubkeys, - size_t pubkeys_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out -); - -/// API: onion_request_prepare_server_destination -/// -/// Wrapper around session::onionreq::prepare. payload_in is binary: payload_in -/// has the length provided, destination_x25519_pubkey is a hex string and must be -/// exactly 64 characters. Returns a flag indicating success or failure. -/// -/// Declaration: -/// ```cpp -/// UNSIGNED CHAR* onion_request_prepare_server_destination( -/// [in] const unsigned char* payload_in, -/// [in] size_t payload_in_len, -/// [in] const char* destination_host, -/// [in] const char* destination_target, -/// [in] const char* destination_protocol, -/// [in] uint16_t destination_port, -/// [in] const char* destination_x25519_pubkey, -/// [in] const char** ed25519_pubkeys, -/// [in] const char** x25519_pubkeys, -/// [in] size_t pubkeys_len, -/// [out] unsigned char** payload_out, -/// [out] size_t* payload_out_len, -/// [out] unsigned char* final_x25519_pubkey_out, -/// [out] unsigned char* final_x25519_seckey_out -/// ); -/// ``` -/// -/// Inputs: -/// - `payload_in` -- [in] The payload to be sent in the onion request -/// - `payload_in_len` -- [in] The length of the payload_in -/// - `destination_host` -- [in] The host for the server destination -/// - `destination_target` -- [in] The target (endpoint) for the server destination -/// - `destination_protocol` -- [in] The protocol to use for the -/// - `destination_port` -- [in] The host for the server destination -/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination -/// - `ed25519_pubkeys` -- [in] array of ed25519 public keys for the onion request path -/// - `x25519_pubkeys` -- [in] array of x25519 public keys for the onion request path -/// - `pubkeys_len` -- [in] number or snodes in the path -/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error -/// - `payload_out_len` -- [out] length of payload_out if not null -/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final -/// x25519 public key used for the onion request will be written if successful -/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final -/// x25519 secret key used for the onion request will be written if successful -/// -/// Outputs: -/// - `bool` -- True if the onion request was successfully constructed, false if it failed. -/// If (and only if) true is returned then `payload_out` must be freed when done with it. -LIBSESSION_EXPORT bool onion_request_prepare_server_destination( - const unsigned char* payload_in, - size_t payload_in_len, - const char* destination_host, - const char* destination_target, - const char* destination_protocol, - uint16_t destination_port, - const char* destination_x25519_pubkey, - const char** ed25519_pubkeys, - const char** x25519_pubkeys, - size_t pubkeys_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out -); - -/// API: onion_request_decrypt -/// -/// Wrapper around session::onionreq::decrypt. ciphertext_in is binary. -/// destination_x25519_pubkey, final_x25519_pubkey and final_x25519_seckey -/// should be in bytes and be exactly 32 bytes. Returns a flag indicating -/// success or failure. -/// -/// Declaration: -/// ```cpp -/// bool onion_request_decrypt( -/// [in] const unsigned char* ciphertext, -/// [in] size_t ciphertext_len, -/// [in] const char* destination_x25519_pubkey, -/// [in] const char* final_x25519_pubkey, -/// [in] const char* final_x25519_seckey, -/// [out] unsigned char** plaintext_out, -/// [out] size_t* plaintext_out_len -/// ); -/// ``` -/// -/// Inputs: -/// - `ciphertext` -- [in] The onion request response data -/// - `ciphertext_len` -- [in] The length of ciphertext -/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination -/// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request -/// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request -/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on error -/// - `plaintext_out_len` -- [out] length of plaintext_out if not null -/// -/// Outputs: -/// - `bool` -- True if the onion request was successfully constructed, false if it failed. -/// If (and only if) true is returned then `plaintext_out` must be freed when done with it. -LIBSESSION_EXPORT bool onion_request_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - unsigned char* destination_x25519_pubkey, - unsigned char* final_x25519_pubkey, - unsigned char* final_x25519_seckey, - unsigned char** plaintext_out, - size_t* plaintext_out_len -); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/include/session/onionreq/channel_encryption.hpp b/include/session/onionreq/channel_encryption.hpp deleted file mode 100644 index 7a9e5326..00000000 --- a/include/session/onionreq/channel_encryption.hpp +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -#include -#include - -#include "key_types.hpp" - -namespace session::onionreq { - -enum class EncryptType { - aes_gcm, - xchacha20, -}; - -// Takes the encryption type as a string, returns the EncryptType value (or throws if invalid). -// Supported values: aes-gcm and xchacha20. gcm is accepted as an aliases for aes-gcm. -EncryptType parse_enc_type(std::string_view enc_type); - -inline constexpr std::string_view to_string(EncryptType type) { - switch (type) { - case EncryptType::xchacha20: return "xchacha20"sv; - case EncryptType::aes_gcm: return "aes-gcm"sv; - } - return ""sv; -} - -struct destination { - x25519_pubkey x25519_public_key; - - destination(x25519_pubkey public_key) : x25519_public_key{std::move(public_key)} {} - - virtual ~destination() = default; -}; - -struct snode_destination: public destination { - ed25519_pubkey ed25519_public_key; - - snode_destination(ed25519_pubkey ed25519_public_key, x25519_pubkey public_key) : - destination(public_key), ed25519_public_key{std::move(ed25519_public_key)} {} -}; - -struct server_destination : public destination { - std::string host; - std::string target; - std::string protocol; - std::optional port; - - server_destination(std::string&& host, std::string&& target, std::string&& protocol, std::optional port, - x25519_pubkey public_key) : - destination(public_key), - host{std::move(host)}, - target{std::move(target)}, - protocol{std::move(protocol)}, - port{port} {} -}; - -// Encryption/decription class for encryption/decrypting outgoing/incoming messages. -class ChannelEncryption { - public: - ChannelEncryption(x25519_seckey private_key, x25519_pubkey public_key, bool server = true) : - private_key_{std::move(private_key)}, - public_key_{std::move(public_key)}, - server_{server} {} - - // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. - // `reply` should be false for a client-to-snode message, and true on a returning - // snode-to-client message. - ustring encrypt(EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const; - ustring decrypt(EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const; - - // AES-GCM encryption. - ustring encrypt_aesgcm(ustring plainText, const x25519_pubkey& pubKey) const; - ustring decrypt_aesgcm(ustring cipherText, const x25519_pubkey& pubKey) const; - - // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a - // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of - // H(aB || A || B), which Bob can compute when receiving as H(bA || A || B). The returned value - // always has the crypto_aead_xchacha20poly1305_ietf_NPUBBYTES nonce prepended to the beginning. - // - // When Bob (the server) encrypts a method for Alice (the client), he uses shared key - // H(bA || A || B) (note that this is *different* that what would result if Bob was a client - // sending to Alice the client). - ustring encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const; - ustring decrypt_xchacha20(ustring ciphertext, const x25519_pubkey& pubKey) const; - - private: - const x25519_seckey private_key_; - const x25519_pubkey public_key_; - bool server_; // True if we are the server (i.e. the snode). -}; - -std::pair prepare( - ustring payload, - destination& destination, - std::vector> keys, - std::optional enc_type); - -ustring decrypt( - ustring ciphertext, - const x25519_pubkey destinationPubkey, - const x25519_pubkey finalPubkey, - const x25519_seckey finalSeckey, - std::optional enc_type); - -} // namespace session::onionreq diff --git a/include/session/onionreq/hop_encryption.hpp b/include/session/onionreq/hop_encryption.hpp new file mode 100644 index 00000000..fcb18136 --- /dev/null +++ b/include/session/onionreq/hop_encryption.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include "builder.hpp" +#include "key_types.hpp" + +namespace session::onionreq { + +// Encryption/decription class for encryption/decrypting outgoing/incoming messages. +class HopEncryption { + public: + HopEncryption(x25519_seckey private_key, x25519_pubkey public_key, bool server = true) : + private_key_{std::move(private_key)}, + public_key_{std::move(public_key)}, + server_{server} {} + + // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. + // `reply` should be false for a client-to-snode message, and true on a returning + // snode-to-client message. + ustring encrypt(EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const; + ustring decrypt(EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const; + + // AES-GCM encryption. + ustring encrypt_aesgcm(ustring plainText, const x25519_pubkey& pubKey) const; + ustring decrypt_aesgcm(ustring cipherText, const x25519_pubkey& pubKey) const; + + // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a + // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of + // H(aB || A || B), which Bob can compute when receiving as H(bA || A || B). The returned value + // always has the crypto_aead_xchacha20poly1305_ietf_NPUBBYTES nonce prepended to the beginning. + // + // When Bob (the server) encrypts a method for Alice (the client), he uses shared key + // H(bA || A || B) (note that this is *different* that what would result if Bob was a client + // sending to Alice the client). + ustring encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const; + ustring decrypt_xchacha20(ustring ciphertext, const x25519_pubkey& pubKey) const; + + private: + const x25519_seckey private_key_; + const x25519_pubkey public_key_; + bool server_; // True if we are the server (i.e. the snode). +}; + +} // namespace session::onionreq diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 70a938ec..8d2d290e 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -1,6 +1,6 @@ #include -#include "session/onionreq/channel_encryption.hpp" +#include "session/onionreq/hop_encryption.hpp" #include "session/types.hpp" namespace session::onionreq { @@ -11,7 +11,7 @@ constexpr size_t DEFAULT_MAX_SIZE = 10'485'760; // 10 MiB class OnionReqParser { private: x25519_keypair keys; - ChannelEncryption enc; + HopEncryption enc; EncryptType enc_type = EncryptType::aes_gcm; x25519_pubkey remote_pk; ustring payload_; diff --git a/include/session/onionreq/response_parser.h b/include/session/onionreq/response_parser.h new file mode 100644 index 00000000..9313f304 --- /dev/null +++ b/include/session/onionreq/response_parser.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "../export.h" +#include "builder.h" + +/// API: onion_request_decrypt +/// +/// Wrapper around session::onionreq::ResponseParser. ciphertext_in is binary. +/// enc_type should be set to ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20 if it's not +/// set when creating the builder destination_x25519_pubkey, final_x25519_pubkey +/// and final_x25519_seckey should be in bytes and be exactly 32 bytes. Returns a +/// flag indicating success or failure. +/// +/// Declaration: +/// ```cpp +/// bool onion_request_decrypt( +/// [in] const unsigned char* ciphertext, +/// [in] size_t ciphertext_len, +/// [in] ENCRYPT_TYPE enc_type, +/// [in] const char* destination_x25519_pubkey, +/// [in] const char* final_x25519_pubkey, +/// [in] const char* final_x25519_seckey, +/// [out] unsigned char** plaintext_out, +/// [out] size_t* plaintext_out_len +/// ); +/// ``` +/// +/// Inputs: +/// - `ciphertext` -- [in] The onion request response data +/// - `ciphertext_len` -- [in] The length of ciphertext +/// - `enc_type` -- [in] The encryption type which was used for the onion request +/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination +/// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request +/// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request +/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on error +/// - `plaintext_out_len` -- [out] length of plaintext_out if not null +/// +/// Outputs: +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// If (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len +); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp new file mode 100644 index 00000000..0f078647 --- /dev/null +++ b/include/session/onionreq/response_parser.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "hop_encryption.hpp" +#include "key_types.hpp" + +namespace session::onionreq { + +class ResponseParser { + public: + /// Constructs a parser, parsing the given request sent to us. Throws if parsing or decryption + /// fails. + ResponseParser(session::onionreq::Builder builder); + ResponseParser( + x25519_pubkey destination_x25519_public_key, + x25519_keypair x25519_keypair, + EncryptType enc_type = EncryptType::xchacha20) : + destination_x25519_public_key_{std::move(destination_x25519_public_key)}, + x25519_keypair_{std::move(x25519_keypair)}, + enc_type_{enc_type} {} + + ustring decrypt(ustring ciphertext) const; + + private: + x25519_pubkey destination_x25519_public_key_; + x25519_keypair x25519_keypair_; + EncryptType enc_type_; +}; + +} // namespace session::onionreq diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9495b694..2ca2f846 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -93,9 +93,11 @@ target_link_libraries(config if(ENABLE_ONIONREQ) add_libsession_util_library(onionreq - onionreq/channel_encryption.cpp + onionreq/builder.cpp + onionreq/hop_encryption.cpp onionreq/key_types.cpp onionreq/parser.cpp + onionreq/response_parser.cpp ) target_link_libraries(onionreq diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp new file mode 100644 index 00000000..2a868356 --- /dev/null +++ b/src/onionreq/builder.cpp @@ -0,0 +1,290 @@ +#include "session/onionreq/builder.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "session/xed25519.hpp" +#include "session/onionreq/builder.h" +#include "session/onionreq/hop_encryption.hpp" +#include "session/onionreq/key_types.hpp" +#include "session/export.h" +#include "session/util.hpp" + +namespace session::onionreq { + +namespace { + + ustring encode_size(uint32_t s) { + ustring result; + result.resize(4); + oxenc::write_host_as_little(s, result.data()); + return result; + } +} // namespace + + EncryptType parse_enc_type(std::string_view enc_type) { + if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") + return EncryptType::xchacha20; + if (enc_type == "aes-gcm" || enc_type == "gcm") + return EncryptType::aes_gcm; + throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; + } + + ustring Builder::build(ustring payload) { + ustring blob; + + // First hop: + // + // [N][ENCRYPTED]{json} + // + // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. + // The first hop decrypts ENCRYPTED into: + // + // [N][BLOB]{json} + // + // where [N] is the length of the blob and {json} now contains either: + // - a "headers" key with an empty value. This is how we indicate that the request is for this + // node as the final hop, and means that the BLOB is actually JSON it should parse to get the + // request info (which has "method", "params", etc. in it). + // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made + // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a + // blob here, but it is not used and typically empty). + // - "destination" and "ephemeral_key" to forward the request to the next hop. + // + // This later case continues onion routing by giving us something like: + // + // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} + // + // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via + // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go + // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not + // json): + // + // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } + // + // where BLOB is the opaque data received from the previous hop and N is the hop number which + // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, + // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. + // + // On the *return* trip, the message gets encrypted (once!) at the final destination using the + // derived key from the pubkey given to the final hop, base64-encoded, then passed back without + // any onion encryption at all all the way back to the client. + + // Ephemeral keypair: + x25519_pubkey A; + x25519_seckey a; + nlohmann::json final_route; + + { + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + + // The data we send to the destination differs depending on whether the destination is a server + // or a service node + if (host_ && target_ && protocol_ && destination_x25519_public_key) { + final_route = { + {"host", host_.value()}, + {"target", target_.value()}, + {"method", "POST"}, + {"protocol", protocol_.value()}, + {"port", port_.value_or(protocol_.value() == "https" ? 443 : 80)}, + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + + blob = e.encrypt(enc_type, payload.data(), *destination_x25519_public_key); + } else if (ed25519_public_key_ && destination_x25519_public_key) { + nlohmann::json control{ + {"headers", ""} + }; + final_route = { + {"destination", ed25519_public_key_.value().hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + + auto data = encode_size(payload.size()); + data += payload; + data += to_unsigned_sv(control.dump()); + blob = e.encrypt(enc_type, data, *destination_x25519_public_key); + } else { + throw std::runtime_error{"Destination not set"}; + } + + // Save these because we need them again to decrypt the final response: + final_hop_x25519_keypair.reset(); + final_hop_x25519_keypair.emplace(A, a); + } + + for (auto it = hops_.rbegin(); it != hops_.rend(); ++it) { + // Routing data for this hop: + nlohmann::json routing; + + if (it == hops_.rbegin()) { + routing = final_route; + } + else { + routing = { + {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + } + + auto data = encode_size(blob.size()); + data += blob; + data += to_unsigned_sv(routing.dump()); + + // Generate eph key for *this* request and encrypt it: + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + blob = e.encrypt(enc_type, data, it->second); + } + + // The data going to the first hop needs to be wrapped in one more layer to tell the first hop + // how to decrypt the initial payload: + auto result = encode_size(blob.size()); + result += blob; + result += to_unsigned_sv(nlohmann::json{ + {"ephemeral_key", A.hex()}, + {"enc_type", to_string(enc_type)} + }.dump()); + + return result; + } +} // namespace session::onionreq + +extern "C" { + +using session::ustring; + +namespace { + +session::onionreq::Builder& unbox(onion_request_builder_object* builder) { + assert(builder && builder->internals); + return *static_cast(builder->internals); +} + +} + +LIBSESSION_C_API void onion_request_builder_init( + onion_request_builder_object** builder) { + auto c = std::make_unique(); + auto c_builder = std::make_unique(); + c_builder->internals = c.release(); + *builder = c_builder.release(); +} + +LIBSESSION_C_API void onion_request_builder_set_enc_type( + onion_request_builder_object* builder, + ENCRYPT_TYPE enc_type +) { + assert(builder); + + switch (enc_type) { + case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: + unbox(builder).set_enc_type(session::onionreq::EncryptType::aes_gcm); + break; + + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: + unbox(builder).set_enc_type(session::onionreq::EncryptType::xchacha20); + break; + + default: throw std::runtime_error{"Invalid encryption type"}; + } +} + +LIBSESSION_C_API void onion_request_builder_set_snode_destination( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey +) { + assert(builder && ed25519_pubkey && x25519_pubkey); + + unbox(builder).set_snode_destination( + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) + ); +} + +LIBSESSION_C_API void onion_request_builder_set_server_destination( + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey +) { + assert(builder && host && target && protocol && x25519_pubkey); + + unbox(builder).set_server_destination( + host, + target, + protocol, + port, + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) + ); +} + +LIBSESSION_C_API void onion_request_builder_add_hop( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey +) { + assert(builder && ed25519_pubkey && x25519_pubkey); + + unbox(builder).add_hop({ + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) + }); +} + +LIBSESSION_C_API bool onion_request_builder_build( + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out +) { + assert(builder && payload_in); + + try { + auto unboxed_builder = unbox(builder); + auto payload = unboxed_builder.build(ustring{payload_in, payload_in_len}); + + if (unboxed_builder.final_hop_x25519_keypair) { + auto key_pair = unboxed_builder.final_hop_x25519_keypair.value(); + std::memcpy(final_x25519_pubkey_out, key_pair.first.data(), key_pair.first.size()); + std::memcpy(final_x25519_seckey_out, key_pair.second.data(), key_pair.second.size()); + } + else { + throw std::runtime_error{"Final keypair not generated"}; + } + + *payload_out = static_cast(malloc(payload.size())); + *payload_out_len = payload.size(); + std::memcpy(*payload_out, payload.data(), payload.size()); + + return true; + } + catch (...) { + return false; + } +} + +} \ No newline at end of file diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/channel_encryption.cpp deleted file mode 100644 index fa1ab0d6..00000000 --- a/src/onionreq/channel_encryption.cpp +++ /dev/null @@ -1,524 +0,0 @@ -#include "session/onionreq/channel_encryption.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "session/xed25519.hpp" -#include "session/onionreq/channel_encryption.h" -#include "session/onionreq/key_types.hpp" -#include "session/export.h" -#include "session/util.hpp" - -namespace session::onionreq { - -namespace { - - // Derive shared secret from our (ephemeral) `seckey` and the other party's `pubkey` - std::array calculate_shared_secret( - const x25519_seckey& seckey, const x25519_pubkey& pubkey) { - std::array secret; - if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) - throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); - return secret; - } - - constexpr std::string_view salt{"LOKI"}; - - std::array derive_symmetric_key( - const x25519_seckey& seckey, const x25519_pubkey& pubkey) { - auto key = calculate_shared_secret(seckey, pubkey); - - auto usalt = to_unsigned_sv(salt); - - crypto_auth_hmacsha256_state state; - - crypto_auth_hmacsha256_init(&state, usalt.data(), usalt.size()); - crypto_auth_hmacsha256_update(&state, key.data(), key.size()); - crypto_auth_hmacsha256_final(&state, key.data()); - - return key; - } - - // More robust shared secret calculation, used when using xchacha20-poly1305 encryption. (This - // could be used for AES-GCM as well, but would break backwards compatibility with existing - // Session clients). - std::array xchacha20_shared_key( - const x25519_pubkey& local_pub, - const x25519_seckey& local_sec, - const x25519_pubkey& remote_pub, - bool local_first) { - std::array key; - static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES >= crypto_scalarmult_BYTES); - if (0 != crypto_scalarmult( - key.data(), - local_sec.data(), - remote_pub.data())) // Use key as tmp storage for aB - throw std::runtime_error{"Failed to compute shared key for xchacha20"}; - crypto_generichash_state h; - crypto_generichash_init(&h, nullptr, 0, key.size()); - crypto_generichash_update(&h, key.data(), crypto_scalarmult_BYTES); - crypto_generichash_update( - &h, (local_first ? local_pub : remote_pub).data(), local_pub.size()); - crypto_generichash_update( - &h, (local_first ? remote_pub : local_pub).data(), local_pub.size()); - crypto_generichash_final(&h, key.data(), key.size()); - return key; - } - - ustring encode_size(uint32_t s) { - ustring result; - result.resize(4); - oxenc::write_host_as_little(s, result.data()); - return result; - } - -} // namespace - -EncryptType parse_enc_type(std::string_view enc_type) { - if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") - return EncryptType::xchacha20; - if (enc_type == "aes-gcm" || enc_type == "gcm") - return EncryptType::aes_gcm; - throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; -} - -ustring ChannelEncryption::encrypt( - EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const { - switch (type) { - case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); - case EncryptType::aes_gcm: return encrypt_aesgcm(plaintext, pubkey); - } - throw std::runtime_error{"Invalid encryption type"}; -} - -ustring ChannelEncryption::decrypt( - EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const { - switch (type) { - case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); - case EncryptType::aes_gcm: return decrypt_aesgcm(ciphertext, pubkey); - } - throw std::runtime_error{"Invalid decryption type"}; -} - -ustring ChannelEncryption::encrypt_aesgcm( - ustring plaintext, const x25519_pubkey& pubKey) const { - auto key = derive_symmetric_key(private_key_, pubKey); - - // Initialise cipher context with the key - struct gcm_aes256_ctx ctx; - static_assert(key.size() == AES256_KEY_SIZE); - gcm_aes256_set_key(&ctx, key.data()); - - ustring output; - output.resize(GCM_IV_SIZE + plaintext.size() + GCM_DIGEST_SIZE); - - // Start the output with the random IV, and load it into ctx - auto* o = output.data(); - randombytes_buf(o, GCM_IV_SIZE); - gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, o); - o += GCM_IV_SIZE; - - // Append encrypted data - gcm_aes256_encrypt(&ctx, plaintext.size(), o, plaintext.data()); - o += plaintext.size(); - - // Append digest - gcm_aes256_digest(&ctx, GCM_DIGEST_SIZE, o); - o += GCM_DIGEST_SIZE; - - assert(o == output.data() + output.size()); - - return output; -} - -ustring ChannelEncryption::decrypt_aesgcm( - ustring ciphertext_, const x25519_pubkey& pubKey) const { - ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; - - if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) - throw std::runtime_error{"ciphertext data is too short"}; - - auto key = derive_symmetric_key(private_key_, pubKey); - - // Initialise cipher context with the key - struct gcm_aes256_ctx ctx; - static_assert(key.size() == AES256_KEY_SIZE); - gcm_aes256_set_key(&ctx, key.data()); - - gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, ciphertext.data()); - - ciphertext.remove_prefix(GCM_IV_SIZE); - auto digest_in = ciphertext.substr(ciphertext.size() - GCM_DIGEST_SIZE); - ciphertext.remove_suffix(GCM_DIGEST_SIZE); - - ustring plaintext; - plaintext.resize(ciphertext.size()); - - gcm_aes256_decrypt(&ctx, ciphertext.size(), plaintext.data(), ciphertext.data()); - - std::array digest_out; - gcm_aes256_digest(&ctx, digest_out.size(), digest_out.data()); - - if (sodium_memcmp(digest_out.data(), digest_in.data(), GCM_DIGEST_SIZE) != 0) - throw std::runtime_error{"Decryption failed (AES256-GCM)"}; - - return plaintext; -} - -ustring ChannelEncryption::encrypt_xchacha20( - ustring plaintext, const x25519_pubkey& pubKey) const { - - ustring ciphertext; - ciphertext.resize( - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + - crypto_aead_xchacha20poly1305_ietf_ABYTES); - - const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); - - // Generate random nonce, and stash it at the beginning of ciphertext: - randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - - auto* c = reinterpret_cast(ciphertext.data()) + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; - unsigned long long clen; - - crypto_aead_xchacha20poly1305_ietf_encrypt( - c, - &clen, - plaintext.data(), - plaintext.size(), - nullptr, - 0, // additional data - nullptr, // nsec (always unused) - reinterpret_cast(ciphertext.data()), - key.data()); - assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); - ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); - return ciphertext; -} - -ustring ChannelEncryption::decrypt_xchacha20( - ustring ciphertext_, const x25519_pubkey& pubKey) const { - ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; - - // Extract nonce from the beginning of the ciphertext: - auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - ciphertext.remove_prefix(nonce.size()); - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw std::runtime_error{"Invalid ciphertext: too short"}; - - const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); - - ustring plaintext; - plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - auto* m = reinterpret_cast(plaintext.data()); - unsigned long long mlen; - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - m, - &mlen, - nullptr, // nsec (always unused) - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, // additional data - nonce.data(), - key.data())) - throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; - assert(mlen <= plaintext.size()); - plaintext.resize(mlen); - return plaintext; -} - -std::pair prepare( - ustring payload, - destination& destination, - std::vector> keys, - std::optional enc_type) { - - ustring blob; - - // First hop: - // - // [N][ENCRYPTED]{json} - // - // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. - // The first hop decrypts ENCRYPTED into: - // - // [N][BLOB]{json} - // - // where [N] is the length of the blob and {json} now contains either: - // - a "headers" key with an empty value. This is how we indicate that the request is for this - // node as the final hop, and means that the BLOB is actually JSON it should parse to get the - // request info (which has "method", "params", etc. in it). - // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made - // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a - // blob here, but it is not used and typically empty). - // - "destination" and "ephemeral_key" to forward the request to the next hop. - // - // This later case continues onion routing by giving us something like: - // - // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} - // - // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via - // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go - // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not - // json): - // - // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } - // - // where BLOB is the opaque data received from the previous hop and N is the hop number which - // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, - // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. - // - // On the *return* trip, the message gets encrypted (once!) at the final destination using the - // derived key from the pubkey given to the final hop, base64-encoded, then passed back without - // any onion encryption at all all the way back to the client. - - // Ephemeral keypair: - x25519_pubkey A; - x25519_seckey a; - x25519_pubkey final_pubkey; - x25519_seckey final_seckey; - nlohmann::json final_route; - EncryptType etype = enc_type.value_or(EncryptType::xchacha20); - - { - crypto_box_keypair(A.data(), a.data()); - ChannelEncryption e{a, A, false}; - - // The data we send to the destination differs depending on whether the destination is a server - // or a service node - if (auto server = dynamic_cast(&destination)) { - final_route = { - {"host", server->host}, - {"target", server->target}, - {"method", "POST"}, - {"protocol", server->protocol}, - {"port", server->port.value_or(server->protocol == "https" ? 443 : 80)}, - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use - {"enc_type", to_string(etype)}, - }; - - blob = e.encrypt(etype, payload.data(), server->x25519_public_key); - } else if (auto snode = dynamic_cast(&destination)) { - nlohmann::json control{ - {"headers", ""} - }; - final_route = { - {"destination", snode->ed25519_public_key.hex()}, // Next hop's ed25519 key - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use - {"enc_type", to_string(etype)}, - }; - - auto data = encode_size(payload.size()); - data += payload; - data += to_unsigned_sv(control.dump()); - blob = e.encrypt(etype, data, snode->x25519_public_key); - } else { - throw std::runtime_error{"Invalid destination type"}; - } - - // Save these because we need them again to decrypt the final response: - final_seckey = a; - final_pubkey = A; - } - - for (auto it = keys.rbegin(); it != keys.rend(); ++it) { - // Routing data for this hop: - nlohmann::json routing; - - if (it == keys.rbegin()) { - routing = final_route; - } - else { - routing = { - {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use - {"enc_type", to_string(etype)}, - }; - } - - auto data = encode_size(blob.size()); - data += blob; - data += to_unsigned_sv(routing.dump()); - - // Generate eph key for *this* request and encrypt it: - crypto_box_keypair(A.data(), a.data()); - ChannelEncryption e{a, A, false}; - blob = e.encrypt(etype, data, it->second); - } - - // The data going to the first hop needs to be wrapped in one more layer to tell the first hop - // how to decrypt the initial payload: - auto result = encode_size(blob.size()); - result += blob; - result += to_unsigned_sv(nlohmann::json{ - {"ephemeral_key", A.hex()}, - {"enc_type", to_string(etype)} - }.dump()); - - return {result, {final_pubkey, final_seckey}}; -} - -ustring decrypt( - ustring ciphertext, - const x25519_pubkey destinationPubkey, - const x25519_pubkey finalPubkey, - const x25519_seckey finalSeckey, - std::optional enc_type) { - ChannelEncryption d{finalSeckey, finalPubkey, false}; - EncryptType etype = enc_type.value_or(EncryptType::xchacha20); - - return d.decrypt(etype, ciphertext, destinationPubkey); -} - -} // namespace session::onionreq - -extern "C" { - -using session::ustring; - -LIBSESSION_C_API bool onion_request_prepare_snode_destination( - const unsigned char* payload_in, - size_t payload_in_len, - const char* destination_ed25519_pubkey, - const char* destination_x25519_pubkey, - const char** ed25519_pubkeys, - const char** x25519_pubkeys, - size_t pubkeys_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out -) { - assert(payload_in && destination_ed25519_pubkey && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys && payload_in_len > 0 && pubkeys_len > 0); - - session::onionreq::snode_destination destination = { - session::onionreq::ed25519_pubkey::from_hex({destination_ed25519_pubkey, 64}), - session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}) - }; - std::vector> keys; - for (size_t i = 0; i < pubkeys_len; i++) - keys.emplace_back( - session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkeys[i], 64}), - session::onionreq::x25519_pubkey::from_hex({x25519_pubkeys[i], 64}) - ); - - try { - auto result = session::onionreq::prepare( - ustring{payload_in, payload_in_len}, - destination, - keys, - session::onionreq::EncryptType::xchacha20 - ); - - auto [payload, final_key_pair] = result; - *payload_out = static_cast(malloc(payload.size())); - *payload_out_len = payload.size(); - std::memcpy(*payload_out, payload.data(), payload.size()); - std::memcpy(final_x25519_pubkey_out, final_key_pair.first.data(), final_key_pair.first.size()); - std::memcpy(final_x25519_seckey_out, final_key_pair.second.data(), final_key_pair.second.size()); - return true; - } - catch (...) { - return false; - } -} - -LIBSESSION_C_API bool onion_request_prepare_server_destination( - const unsigned char* payload_in, - size_t payload_in_len, - const char* destination_host, - const char* destination_target, - const char* destination_protocol, - uint16_t destination_port, - const char* destination_x25519_pubkey, - const char** ed25519_pubkeys, - const char** x25519_pubkeys, - size_t pubkeys_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out -) { - assert(payload_in && destination_x25519_pubkey && ed25519_pubkeys && x25519_pubkeys && pubkeys_len > 0); - - session::onionreq::server_destination destination = { - destination_host, - destination_target, - destination_protocol, - destination_port, - session::onionreq::x25519_pubkey::from_hex({destination_x25519_pubkey, 64}) - }; - std::vector> keys; - for (size_t i = 0; i < pubkeys_len; i++) - keys.emplace_back( - session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkeys[i], 64}), - session::onionreq::x25519_pubkey::from_hex({x25519_pubkeys[i], 64}) - ); - - try { - auto result = session::onionreq::prepare( - ustring{payload_in, payload_in_len}, - destination, - keys, - session::onionreq::EncryptType::xchacha20 - ); - - auto [payload, final_key_pair] = result; - *payload_out = static_cast(malloc(payload.size())); - *payload_out_len = payload.size(); - std::memcpy(*payload_out, payload.data(), payload.size()); - std::memcpy(final_x25519_pubkey_out, final_key_pair.first.data(), final_key_pair.first.size()); - std::memcpy(final_x25519_seckey_out, final_key_pair.second.data(), final_key_pair.second.size()); - - return true; - } - catch (...) { - return false; - } -} - -LIBSESSION_C_API bool onion_request_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - unsigned char* destination_x25519_pubkey, - unsigned char* final_x25519_pubkey, - unsigned char* final_x25519_seckey, - unsigned char** plaintext_out, - size_t* plaintext_out_len -) { - assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && ciphertext_len > 0); - - try { - auto result = session::onionreq::decrypt( - ustring{ciphertext, ciphertext_len}, - session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32}), - session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), - session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), - session::onionreq::EncryptType::xchacha20 - ); - *plaintext_out = static_cast(malloc(result.size())); - *plaintext_out_len = result.size(); - std::memcpy(*plaintext_out, result.data(), result.size()); - return true; - } - catch (...) { - return false; - } -} - -} diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp new file mode 100644 index 00000000..aa25348d --- /dev/null +++ b/src/onionreq/hop_encryption.cpp @@ -0,0 +1,230 @@ +#include "session/onionreq/hop_encryption.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "session/xed25519.hpp" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/key_types.hpp" +#include "session/export.h" +#include "session/util.hpp" + +namespace session::onionreq { + +namespace { + + // Derive shared secret from our (ephemeral) `seckey` and the other party's `pubkey` + std::array calculate_shared_secret( + const x25519_seckey& seckey, const x25519_pubkey& pubkey) { + std::array secret; + if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) + throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); + return secret; + } + + constexpr std::string_view salt{"LOKI"}; + + std::array derive_symmetric_key( + const x25519_seckey& seckey, const x25519_pubkey& pubkey) { + auto key = calculate_shared_secret(seckey, pubkey); + + auto usalt = to_unsigned_sv(salt); + + crypto_auth_hmacsha256_state state; + + crypto_auth_hmacsha256_init(&state, usalt.data(), usalt.size()); + crypto_auth_hmacsha256_update(&state, key.data(), key.size()); + crypto_auth_hmacsha256_final(&state, key.data()); + + return key; + } + + // More robust shared secret calculation, used when using xchacha20-poly1305 encryption. (This + // could be used for AES-GCM as well, but would break backwards compatibility with existing + // Session clients). + std::array xchacha20_shared_key( + const x25519_pubkey& local_pub, + const x25519_seckey& local_sec, + const x25519_pubkey& remote_pub, + bool local_first) { + std::array key; + static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES >= crypto_scalarmult_BYTES); + if (0 != crypto_scalarmult( + key.data(), + local_sec.data(), + remote_pub.data())) // Use key as tmp storage for aB + throw std::runtime_error{"Failed to compute shared key for xchacha20"}; + crypto_generichash_state h; + crypto_generichash_init(&h, nullptr, 0, key.size()); + crypto_generichash_update(&h, key.data(), crypto_scalarmult_BYTES); + crypto_generichash_update( + &h, (local_first ? local_pub : remote_pub).data(), local_pub.size()); + crypto_generichash_update( + &h, (local_first ? remote_pub : local_pub).data(), local_pub.size()); + crypto_generichash_final(&h, key.data(), key.size()); + return key; + } + +} // namespace + +ustring HopEncryption::encrypt( + EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const { + switch (type) { + case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); + case EncryptType::aes_gcm: return encrypt_aesgcm(plaintext, pubkey); + } + throw std::runtime_error{"Invalid encryption type"}; +} + +ustring HopEncryption::decrypt( + EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const { + switch (type) { + case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); + case EncryptType::aes_gcm: return decrypt_aesgcm(ciphertext, pubkey); + } + throw std::runtime_error{"Invalid decryption type"}; +} + +ustring HopEncryption::encrypt_aesgcm( + ustring plaintext, const x25519_pubkey& pubKey) const { + auto key = derive_symmetric_key(private_key_, pubKey); + + // Initialise cipher context with the key + struct gcm_aes256_ctx ctx; + static_assert(key.size() == AES256_KEY_SIZE); + gcm_aes256_set_key(&ctx, key.data()); + + ustring output; + output.resize(GCM_IV_SIZE + plaintext.size() + GCM_DIGEST_SIZE); + + // Start the output with the random IV, and load it into ctx + auto* o = output.data(); + randombytes_buf(o, GCM_IV_SIZE); + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, o); + o += GCM_IV_SIZE; + + // Append encrypted data + gcm_aes256_encrypt(&ctx, plaintext.size(), o, plaintext.data()); + o += plaintext.size(); + + // Append digest + gcm_aes256_digest(&ctx, GCM_DIGEST_SIZE, o); + o += GCM_DIGEST_SIZE; + + assert(o == output.data() + output.size()); + + return output; +} + +ustring HopEncryption::decrypt_aesgcm( + ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; + + if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) + throw std::runtime_error{"ciphertext data is too short"}; + + auto key = derive_symmetric_key(private_key_, pubKey); + + // Initialise cipher context with the key + struct gcm_aes256_ctx ctx; + static_assert(key.size() == AES256_KEY_SIZE); + gcm_aes256_set_key(&ctx, key.data()); + + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, ciphertext.data()); + + ciphertext.remove_prefix(GCM_IV_SIZE); + auto digest_in = ciphertext.substr(ciphertext.size() - GCM_DIGEST_SIZE); + ciphertext.remove_suffix(GCM_DIGEST_SIZE); + + ustring plaintext; + plaintext.resize(ciphertext.size()); + + gcm_aes256_decrypt(&ctx, ciphertext.size(), plaintext.data(), ciphertext.data()); + + std::array digest_out; + gcm_aes256_digest(&ctx, digest_out.size(), digest_out.data()); + + if (sodium_memcmp(digest_out.data(), digest_in.data(), GCM_DIGEST_SIZE) != 0) + throw std::runtime_error{"Decryption failed (AES256-GCM)"}; + + return plaintext; +} + +ustring HopEncryption::encrypt_xchacha20( + ustring plaintext, const x25519_pubkey& pubKey) const { + + ustring ciphertext; + ciphertext.resize( + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); + + // Generate random nonce, and stash it at the beginning of ciphertext: + randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + auto* c = reinterpret_cast(ciphertext.data()) + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + unsigned long long clen; + + crypto_aead_xchacha20poly1305_ietf_encrypt( + c, + &clen, + plaintext.data(), + plaintext.size(), + nullptr, + 0, // additional data + nullptr, // nsec (always unused) + reinterpret_cast(ciphertext.data()), + key.data()); + assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); + ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); + return ciphertext; +} + +ustring HopEncryption::decrypt_xchacha20( + ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; + + // Extract nonce from the beginning of the ciphertext: + auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext.remove_prefix(nonce.size()); + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::runtime_error{"Invalid ciphertext: too short"}; + + const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); + + ustring plaintext; + plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + auto* m = reinterpret_cast(plaintext.data()); + unsigned long long mlen; + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + m, + &mlen, + nullptr, // nsec (always unused) + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, // additional data + nonce.data(), + key.data())) + throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; + assert(mlen <= plaintext.size()); + plaintext.resize(mlen); + return plaintext; +} + +} // namespace session::onionreq diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp new file mode 100644 index 00000000..2a212013 --- /dev/null +++ b/src/onionreq/response_parser.cpp @@ -0,0 +1,89 @@ +#include "session/onionreq/response_parser.hpp" + +#include +#include + +#include "session/onionreq/builder.h" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/hop_encryption.hpp" +#include + +#include "session/export.h" + +namespace session::onionreq { + +ResponseParser::ResponseParser( + session::onionreq::Builder builder +) { + if (!builder.destination_x25519_public_key.has_value()) + throw std::runtime_error{"Builder does not contain destination x25519 public key"}; + if (!builder.final_hop_x25519_keypair.has_value()) + throw std::runtime_error{"Builder does not contain final keypair"}; + + enc_type_ = builder.enc_type; + destination_x25519_public_key_ = builder.destination_x25519_public_key.value(); + x25519_keypair_ = builder.final_hop_x25519_keypair.value(); +} + +ustring ResponseParser::decrypt(ustring ciphertext) const { + HopEncryption d{x25519_keypair_.second, x25519_keypair_.first}; + + return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); +} + +} // namespace session::onionreq + +extern "C" { + +using session::ustring; + +LIBSESSION_C_API bool onion_request_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len +) { + assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && ciphertext_len > 0); + + try { + auto enc_type = session::onionreq::EncryptType::xchacha20; + + switch (enc_type_) { + case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: + enc_type = session::onionreq::EncryptType::aes_gcm; + break; + + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: + enc_type = session::onionreq::EncryptType::xchacha20; + break; + + default: throw std::runtime_error{"Invalid decryption type " + std::to_string(enc_type_)}; + } + + session::onionreq::HopEncryption d{ + session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), + session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), + false + }; + + auto result = d.decrypt( + enc_type, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32}) + ); + + *plaintext_out = static_cast(malloc(result.size())); + *plaintext_out_len = result.size(); + std::memcpy(*plaintext_out, result.data(), result.size()); + return true; + } + catch (...) { + return false; + } +} + +} diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 93465674..a4c42dd3 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include "utils.hpp" @@ -33,7 +33,7 @@ TEST_CASE("Onion request encryption", "[encryption][onionreq]") { "9e1a3abe60eff3ea5c23556ccfe225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" "7d1638d765db75de02b032"_hexbytes; - ChannelEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; + HopEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; CHECK(from_unsigned_sv(e.decrypt_aesgcm(enc_gcm, x25519_pubkey::from_bytes(A))) == "Hello world"); @@ -73,7 +73,7 @@ TEST_CASE("Onion request parser", "[onionreq][parser]") { auto aes_reply = parser_gcm.encrypt_reply(to_unsigned_sv("Goodbye world")); CHECK(aes_reply.size() == 12 + 13 + 16); - ChannelEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; + HopEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; CHECK(from_unsigned_sv(e.decrypt_aesgcm(aes_reply, x25519_pubkey::from_bytes(B))) == "Goodbye world"); From 973e78dda3561e99aec460e8cd0e5364cebd4bdd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 4 Dec 2023 17:59:57 +1100 Subject: [PATCH 129/572] Exposed required crypto functions, started on SessionBlindingProtocol --- include/session/blinding.h | 37 +++ include/session/blinding.hpp | 34 ++- include/session/curve25519.h | 57 ++++ include/session/curve25519.hpp | 35 +++ include/session/ed25519.h | 94 +++++++ include/session/ed25519.hpp | 53 ++++ include/session/onionreq/response_parser.h | 4 +- include/session/session_encrypt.h | 131 ++++++++++ include/session/session_encrypt.hpp | 50 +++- include/session/xchacha.h | 42 +++ include/session/xchacha.hpp | 23 ++ include/session/xed25519.h | 14 +- src/CMakeLists.txt | 3 + src/blinding.cpp | 287 +++++++++++++++++++++ src/curve25519.cpp | 94 +++++++ src/ed25519.cpp | 137 ++++++++++ src/session_encrypt.cpp | 238 ++++++++++++++++- src/xchacha.cpp | 63 +++++ src/xed25519.cpp | 18 +- 19 files changed, 1375 insertions(+), 39 deletions(-) create mode 100644 include/session/blinding.h create mode 100644 include/session/curve25519.h create mode 100644 include/session/curve25519.hpp create mode 100644 include/session/ed25519.h create mode 100644 include/session/ed25519.hpp create mode 100644 include/session/session_encrypt.h create mode 100644 include/session/xchacha.h create mode 100644 include/session/xchacha.hpp create mode 100644 src/curve25519.cpp create mode 100644 src/ed25519.cpp create mode 100644 src/xchacha.cpp diff --git a/include/session/blinding.h b/include/session/blinding.h new file mode 100644 index 00000000..ecf450f4 --- /dev/null +++ b/include/session/blinding.h @@ -0,0 +1,37 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +LIBSESSION_EXPORT bool session_blind15_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out); + +LIBSESSION_EXPORT bool session_blind25_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out); + +LIBSESSION_EXPORT bool session_blind15_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out); + +LIBSESSION_EXPORT bool session_id_matches_blinded_id( + const unsigned char* session_id, + const unsigned char* blinded_id, + const unsigned char* server_pk); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 164621d5..e192a472 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -60,15 +60,39 @@ namespace session { /// be 05-prefixed (33 bytes) or unprefixed (32 bytes). std::array blind25_factor(ustring_view session_id, ustring_view server_pk); -/// Computes the 25-blinded id from a session id and server pubkey. Values accepted and -/// returned are hex-encoded. -std::string blind25_id(std::string_view session_id, std::string_view server_pk); - /// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a /// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). ustring blind25_id(ustring_view session_id, ustring_view server_pk); +/// Computes a 15-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk); + +/// Computes a 25-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk); + +/// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would +/// be returned from blind15_key_pair(). +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message); + /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). /// @@ -78,5 +102,7 @@ ustring blind25_id(ustring_view session_id, ustring_view server_pk); /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, + std::string_view server_pk); } // namespace session diff --git a/include/session/curve25519.h b/include/session/curve25519.h new file mode 100644 index 00000000..f903ab38 --- /dev/null +++ b/include/session/curve25519.h @@ -0,0 +1,57 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_curve25519_key_pair +/// +/// Generates a random curve25519 key pair. +/// +/// Inputs: +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the private key will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_curve25519_key_pair( + unsigned char* curve25519_pk_out, + unsigned char* curve25519_sk_out); + +/// API: crypto/session_to_curve25519_pubkey +/// +/// Generates a curve25519 public key for an ed25519 public key. +/// +/// Inputs: +/// - `ed25519_pubkey` -- the ed25519 public key. +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. +/// +/// Outputs: +/// - `bool` -- True if the public key was successfully generated, false if failed. +LIBSESSION_EXPORT bool session_to_curve25519_pubkey( + const unsigned char* ed25519_pubkey, + unsigned char* curve25519_pk_out); + +/// API: crypto/session_to_curve25519_seckey +/// +/// Generates a curve25519 secret key given given either a libsodium-style secret key, 64 +/// bytes. Can also be passed as a 32-byte seed. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the libsodium-style secret key, 64 bytes. Can also be +/// passed as a 32-byte seed. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the secret key will be written. +/// +/// Outputs: +/// - `bool` -- True if the secret key was successfully generated, false if failed. +LIBSESSION_EXPORT bool session_to_curve25519_seckey( + const unsigned char* ed25519_seckey, + unsigned char* curve25519_sk_out); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp new file mode 100644 index 00000000..07876ef7 --- /dev/null +++ b/include/session/curve25519.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "types.hpp" + +namespace session::curve25519 { + +/// Generates a random curve25519 key pair +std::pair, std::array> curve25519_key_pair(); + +/// API: curve25519/to_curve25519_pubkey +/// +/// Generates a curve25519 public key for an ed25519 public key. +/// +/// Inputs: +/// - `ed25519_pubkey` -- the ed25519 public key. +/// +/// Outputs: +/// - The curve25519 public key +std::array to_curve25519_pubkey( + ustring_view ed25519_pubkey); + +/// API: curve25519/to_curve25519_seckey +/// +/// Generates a curve25519 secret key given given a libsodium-style secret key, 64 +/// bytes. +/// +/// Inputs: +/// - `ed25519_seckey` -- the libsodium-style secret key, 64 bytes. +/// +/// Outputs: +/// - The curve25519 secret key +std::array to_curve25519_seckey( + ustring_view ed25519_seckey); + +} // namespace session::ed25519 diff --git a/include/session/ed25519.h b/include/session/ed25519.h new file mode 100644 index 00000000..954d6bee --- /dev/null +++ b/include/session/ed25519.h @@ -0,0 +1,94 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_ed25519_key_pair +/// +/// Generates a random ed25519 key pair. +/// +/// Inputs: +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_key_pair( + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out); + +/// API: crypto/session_ed25519_key_pair_seed +/// +/// Generates a ed25519 key pair for a 32 byte seed. +/// +/// Inputs: +/// - `ed25519_seed` -- [in] the 32 byte seed. +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( + const unsigned char* ed25519_seed, + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out); + +/// API: crypto/session_seed_for_ed_privkey +/// +/// Returns the seed for an ed25519 key pair given either the libsodium-style secret key, 64 +/// bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- [in] the libsodium-style secret key of the sender, 64 bytes. +/// - `ed25519_seed_out` -- [out] pointer to a buffer of 32 bytes where the seed will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_seed_for_ed_privkey( + const unsigned char* ed25519_privkey, + unsigned char* ed25519_seed_out); + +/// API: crypto/session_ed25519_sign +/// +/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- [in] the libsodium-style secret key of the sender, 64 bytes. +/// - `msg` -- [in] the data to generate a signature for. +/// - `msg_len` -- [in] the length of the `msg` data. +/// - `ed25519_sig_out` -- [out] pointer to a buffer of 64 bytes where the signature will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_sign( + const unsigned char* ed25519_privkey, + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out); + +/// API: crypto/session_ed25519_verify +/// +/// Verify a message and signature for a given pubkey. +/// +/// Inputs: +/// - `sig` -- [in] the signature to verify, 64 bytes. +/// - `pubkey` -- [in] the pubkey for the secret key that was used to generate the signature, 32 bytes. +/// - `msg` -- [in] the data to verify the signature for. +/// - `msg_len` -- [in] the length of the `msg` data. +/// +/// Outputs: +/// - A flag indicating whether the signature is valid +LIBSESSION_EXPORT bool session_ed25519_verify( + const unsigned char* sig, + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp new file mode 100644 index 00000000..e7a1e7f7 --- /dev/null +++ b/include/session/ed25519.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "types.hpp" + +namespace session::ed25519 { + +/// Generates a random Ed25519 key pair +std::pair, std::array> ed25519_key_pair(); + +/// Given an Ed25519 seed this returns the associated Ed25519 key pair +std::pair, std::array> ed25519_key_pair( + ustring_view ed25519_seed); + +/// API: ed25519/seed_for_ed_privkey +/// +/// Returns the seed for an ed25519 key pair given either the libsodium-style secret key, 64 +/// bytes. If a 32-byte value is provided it is assumed to be the seed and the value will just +/// be returned directly. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be +/// passed as a 32-byte seed. +/// +/// Outputs: +/// - The ed25519 seed +ustring seed_for_ed_privkey(ustring_view ed25519_privkey); + +/// API: ed25519/sign +/// +/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key, 64 bytes. +/// - `msg` -- the data to generate a signature for. +/// +/// Outputs: +/// - The ed25519 signature +ustring sign(ustring_view ed25519_privkey, ustring_view msg); + +/// API: ed25519/verify +/// +/// Verify a message and signature for a given pubkey. +/// +/// Inputs: +/// - `sig` -- the signature to verify, 64 bytes. +/// - `pubkey` -- the pubkey for the secret key that was used to generate the signature, 32 bytes. +/// - `msg` -- the data to verify the signature for. +/// +/// Outputs: +/// - A flag indicating whether the signature is valid +bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg); + +} // namespace session::ed25519 diff --git a/include/session/onionreq/response_parser.h b/include/session/onionreq/response_parser.h index 9313f304..865325de 100644 --- a/include/session/onionreq/response_parser.h +++ b/include/session/onionreq/response_parser.h @@ -23,7 +23,7 @@ extern "C" { /// bool onion_request_decrypt( /// [in] const unsigned char* ciphertext, /// [in] size_t ciphertext_len, -/// [in] ENCRYPT_TYPE enc_type, +/// [in] ENCRYPT_TYPE enc_type_, /// [in] const char* destination_x25519_pubkey, /// [in] const char* final_x25519_pubkey, /// [in] const char* final_x25519_seckey, @@ -35,7 +35,7 @@ extern "C" { /// Inputs: /// - `ciphertext` -- [in] The onion request response data /// - `ciphertext_len` -- [in] The length of ciphertext -/// - `enc_type` -- [in] The encryption type which was used for the onion request +/// - `enc_type_` -- [in] The encryption type which was used for the onion request /// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination /// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request /// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h new file mode 100644 index 00000000..adf0972a --- /dev/null +++ b/include/session/session_encrypt.h @@ -0,0 +1,131 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_encrypt_for_recipient_deterministic +/// +/// This function attempts to encrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `plaintext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `plaintext_len` -- [in] Length of `plaintext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `recipient_pubkey` -- [in] the x25519 public key of the recipient (32 bytes). +/// - `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 *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* recipient_pubkey, + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: crypto/session_encrypt_for_blinded_recipient +/// +/// This function attempts to encrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `plaintext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `plaintext_len` -- [in] Length of `plaintext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `open_group_pubkey` -- [in] the public key of the open group server to route +/// the blinded message through (32 bytes). +/// - `recipient_blinded_id` -- [in] the blinded id of the recipient including the blinding +/// prefix (33 bytes), 'blind15' or 'blind25' encryption will be chosed based on this value. +/// - `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 *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_encrypt_for_blinded_recipient( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* recipient_blinded_id, + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: crypto/session_decrypt_incoming +/// +/// This function attempts to decrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the receiver (64 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted 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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_decrypt_incoming( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len); + +/// API: crypto/session_decrypt_incoming_legacy_group +/// +/// This function attempts to decrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `x25519_pubkey` -- [in] the x25519 public key of the receiver (32 bytes). +/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (32 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted 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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* x25519_pubkey, + const unsigned char* x25519_seckey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index b702d16a..028bbe38 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -80,6 +80,27 @@ ustring encrypt_for_recipient( ustring encrypt_for_recipient_deterministic( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +/// API: crypto/session_encrypt_for_blinded_recipient +/// +/// This function attempts to encrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be +/// passed as a 32-byte seed, but the 64-byte value is preferrable (to avoid needing to +/// recompute the public key from the seed). +/// - `recipient_pubkey` -- the recipient blinded id, either 0x15-prefixed or 0x25-prefixed +/// (33 bytes). +/// - `message` -- the message to encrypt for the recipient. +/// +/// Outputs: +/// - The encrypted ciphertext to send. +/// - Throw if encryption fails or (which typically means invalid keys provided) +ustring session_encrypt_for_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view open_group_pubkey, + ustring_view recipient_blinded_id, + ustring_view message); + /// API: crypto/sign_for_recipient /// /// Performs the signing steps for session protocol encryption. This is responsible for producing @@ -108,8 +129,8 @@ ustring sign_for_recipient( /// API: crypto/decrypt_incoming /// -/// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 -/// pubkey, and verifies that the sender Ed25519 signature on the message. +/// Inverse of `encrypt_for_recipient`: this decrypts the message, verifies that the sender Ed25519 +/// signature on the message and converts the extracted sender's Ed25519 pubkey into a session ID. /// /// Inputs: /// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte @@ -118,11 +139,26 @@ ustring sign_for_recipient( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - pair consisting of the decrypted message content, and the sender Ed25519 pubkey, *if* the -/// message decrypted and validated successfully. Throws on error. +/// - `std::pair` -- the session ID (in hex) and the plaintext binary +/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); + +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, verifies that the sender Ed25519 +/// signature on the message and converts the extracted sender's Ed25519 pubkey into a session ID. +/// This function is used for decrypting legacy group messages which only have an x25519 key pair, +/// the Ed25519 version of this function should be preferred where possible. /// -/// To get the sender's session ID, pass the returned pubkey through -/// crypto_sign_ed25519_pk_to_curve25519. -std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); +/// Inputs: +/// - `x25519_pubkey` -- the 32 byte x25519 public key of the recipient. +/// - `x25519_seckey` -- the 32 byte x25519 private key of the recipient. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the session ID (in hex) and the plaintext binary +/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); } // namespace session diff --git a/include/session/xchacha.h b/include/session/xchacha.h new file mode 100644 index 00000000..9997bf54 --- /dev/null +++ b/include/session/xchacha.h @@ -0,0 +1,42 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_xchacha_decrypt +/// +/// Wrapper around the crypto_aead_xchacha20poly1305_ietf_decrypt function. +/// +/// Inputs: +/// - `ciphertext` -- [in] the data to be decrypted. +/// - `ciphertext_len` -- [in] length of the `ciphertext` data. +/// - `seckey` -- [in] the secret key used to encrypt the data. +/// - `seckey_len` -- [in] length of the `seckey`. +/// - `nonce` -- [in] the nonce used to encrypt the data. +/// - `plaintext_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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_xchacha_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + const unsigned char* seckey, + size_t seckey_len, + const unsigned char* nonce, + unsigned char** plaintext_out, + size_t* plaintext_out_len); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/xchacha.hpp b/include/session/xchacha.hpp new file mode 100644 index 00000000..92d3582b --- /dev/null +++ b/include/session/xchacha.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "types.hpp" + +namespace session::xchacha { + +/// API: xchacha/decrypt +/// +/// Wrapper around the crypto_aead_xchacha20poly1305_ietf_decrypt function. +/// +/// Inputs: +/// - `ciphertext` -- the data to be decrypted. +/// - `seckey` -- the secret key used to encrypt the data. +/// - `nonce` -- the nonce used to encrypt the data. +/// +/// Outputs: +/// - the plaintext binary data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. +ustring decrypt( + ustring_view ciphertext, + ustring_view seckey, + ustring_view nonce); + +} // namespace session::xchacha diff --git a/include/session/xed25519.h b/include/session/xed25519.h index 5348dafa..4963ce89 100644 --- a/include/session/xed25519.h +++ b/include/session/xed25519.h @@ -4,28 +4,32 @@ extern "C" { #endif +#include + +#include "export.h" + /// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature /// to `sig` on success and returns 0. Returns non-zero on failure. -__attribute__((warn_unused_result)) int session_xed25519_sign( +LIBSESSION_EXPORT bool session_xed25519_sign( unsigned char* signature /* 64 byte buffer */, const unsigned char* curve25519_privkey /* 32 bytes */, const unsigned char* msg, - const unsigned int msg_len); + size_t msg_len); /// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and /// message. Returns 0 if the signature verifies successfully, non-zero on failure. -__attribute__((warn_unused_result)) int session_xed25519_verify( +LIBSESSION_EXPORT bool session_xed25519_verify( const unsigned char* signature /* 64 bytes */, const unsigned char* pubkey /* 32-bytes */, const unsigned char* msg, - const unsigned int msg_len); + size_t msg_len); /// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into /// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result /// in a given curve25519 pubkey: this always returns the positive value. You can get the other /// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`. /// Returns 0 on success, non-0 on failure. -__attribute__((warn_unused_result)) int session_xed25519_pubkey( +LIBSESSION_EXPORT bool session_xed25519_pubkey( unsigned char* ed25519_pubkey /* 32-byte output buffer */, const unsigned char* curve25519_pubkey /* 32 bytes */); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2ca2f846..1f5536f1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,9 +47,12 @@ endif() add_libsession_util_library(crypto blinding.cpp + curve25519.cpp + ed25519.cpp multi_encrypt.cpp session_encrypt.cpp util.cpp + xchacha.cpp xed25519.cpp ) diff --git a/src/blinding.cpp b/src/blinding.cpp index b9783895..441907ea 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -10,6 +10,7 @@ #include #include "session/xed25519.hpp" +#include "session/export.h" namespace session { @@ -19,6 +20,20 @@ using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; +std::array blind15_factor(ustring_view server_pk) { + assert(server_pk.size() == 32); + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, nullptr, 0, 64); + crypto_generichash_blake2b_update(&st, server_pk.data(), server_pk.size()); + uc64 blind_hash; + crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + + uc32 k; + crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); + return k; +} + std::array blind25_factor(ustring_view session_id, ustring_view server_pk) { assert(session_id.size() == 32 || session_id.size() == 33); assert(server_pk.size() == 32); @@ -53,6 +68,30 @@ namespace { } // namespace +ustring blind15_id(ustring_view session_id, ustring_view server_pk) { + if (session_id.size() == 33) { + if (session_id[0] != 0x05) + throw std::invalid_argument{"blind15_id: session_id must start with 0x05"}; + session_id.remove_prefix(1); + } else if (session_id.size() != 32) { + throw std::invalid_argument{"blind15_id: session_id must be 32 or 33 bytes"}; + } + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_id: server_pk must be 32 bytes"}; + + ustring result; + result.resize(33); + result[0] = 0x15; + + auto k = blind15_factor(server_pk); + auto ed_pk = xed25519::pubkey(session_id); + + if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pk.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + + return result; +} + ustring blind25_id(ustring_view session_id, ustring_view server_pk) { if (session_id.size() == 33) { if (session_id[0] != 0x05) @@ -87,6 +126,89 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) return oxenc::to_hex(blinded.begin(), blinded.end()); } +std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; + + /// Generate the blinding factor + uc32 k = blind15_factor(server_pk); + + /// Generate a scalar for the private key + uc32 x_sk; + if (0 != crypto_sign_ed25519_sk_to_curve25519(x_sk.data(), ed25519_sk.data())) + throw std::runtime_error{ + "blind15_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; + + // Turn a, A into their blinded versions + uc32 a; + uc32 A; + crypto_core_ed25519_scalar_mul(a.data(), k.data(), x_sk.data()); + crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + + return {{A.data(), 32}, {a.data(), 32}}; +} + +std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; + + ustring_view S{ed25519_sk.data() + 32, 32}; + + uc33 session_id; + session_id[0] = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) + throw std::runtime_error{ + "blind25_sign: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; + + ustring_view X{session_id.data() + 1, 32}; + + /// Generate the blinding factor + auto k = blind25_factor(X, {server_pk.data(), server_pk.size()}); + + /// Generate a scalar for the private key + uc32 x_sk; + if (0 != crypto_sign_ed25519_sk_to_curve25519(x_sk.data(), ed25519_sk.data())) + throw std::runtime_error{ + "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; + + // Turn a, A into their blinded versions + uc32 a; + uc32 A; + std::memcpy(A.data(), S.data(), 32); + if (S[31] & 0x80) { + // Ed25519 pubkey is negative, so we need to negate `z` to make things come out right + crypto_core_ed25519_scalar_negate(a.data(), x_sk.data()); + A[31] &= 0x7f; + } else + std::memcpy(a.data(), x_sk.data(), 32); + + // Turn a, A into their blinded versions + crypto_core_ed25519_scalar_mul(a.data(), k.data(), a.data()); + crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + + return {{A.data(), 32}, {a.data(), 32}}; +} + static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); @@ -179,4 +301,169 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } +ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind15_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + uc32 server_pk; + if (server_pk_in.size() == 32) + std::memcpy(server_pk.data(), server_pk_in.data(), 32); + else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) + oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); + else + throw std::invalid_argument{"blind15_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + + auto [blind_15_pk, blind_15_sk] = blind15_key_pair(ed25519_sk, {server_pk.data(), 32}); + + // H_rh = sha512(s.encode()).digest()[32:] + uc64 hrh; + crypto_hash_sha512_state st1; + crypto_hash_sha512_init(&st1); + crypto_hash_sha512_update(&st1, ed25519_sk.data(), 64); + crypto_hash_sha512_final(&st1, hrh.data()); + + // r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts)) + auto hrh_suffix = hrh.data() + 32; + uc32 r; + uc64 r_hash; + crypto_hash_sha512_state st2; + crypto_hash_sha512_init(&st2); + crypto_hash_sha512_update(&st2, hrh_suffix, 32); + crypto_hash_sha512_update(&st2, blind_15_pk.data(), blind_15_pk.size()); + crypto_hash_sha512_update(&st2, message.data(), message.size()); + crypto_hash_sha512_final(&st2, r_hash.data()); + crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); + + // sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) + ustring result; + result.resize(64); + auto* sig_R = result.data(); + auto* sig_S = result.data() + 32; + crypto_scalarmult_ed25519_base_noclamp(sig_R, r.data()); + + // HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts)) + uc64 hram; + crypto_hash_sha512_state st3; + crypto_hash_sha512_init(&st3); + crypto_hash_sha512_update(&st3, sig_R, 32); + crypto_hash_sha512_update(&st3, blind_15_pk.data(), blind_15_pk.size()); + crypto_hash_sha512_update(&st3, message.data(), message.size()); + crypto_hash_sha512_final(&st3, hram.data()); + + // sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka)) + crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) + crypto_core_ed25519_scalar_mul(sig_S, sig_S, blind_15_sk.data()); // S = H(R||A||M) a + crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a + + return result; +} + +bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, + std::string_view server_pk) { + ustring converted_blind_id1; + converted_blind_id1.resize(33); + + switch (blinded_id[0]) { + case 0x15: + converted_blind_id1 = blind15_id(to_unsigned_sv(session_id), to_unsigned_sv(server_pk)); + break; + + case 0x25: + converted_blind_id1 = blind25_id(to_unsigned_sv(session_id), to_unsigned_sv(server_pk)); + break; + + default: throw std::invalid_argument{"Invalid blinded_id: must start with 0x15 or 0x25"}; + } + + // For the negative, what we're going to get out of the above is simply the negative of + // converted_blind_id1, so flip the sign bit to get converted_blind_id2 + auto converted_blind_id2 = converted_blind_id1; + converted_blind_id2[32] &= 0x7f; + + return ( + to_unsigned_sv(blinded_id) == converted_blind_id1 || + to_unsigned_sv(blinded_id) == converted_blind_id2 + ); +} + } // namespace session + +using namespace session; + +LIBSESSION_C_API bool session_blind15_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out +) { + try { + auto result = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_blind25_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out +) { + try { + auto result = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_blind15_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out +) { + try { + auto result = session::blind15_sign( + {ed25519_seckey, 64}, + {from_unsigned(server_pk), 32}, + {msg, msg_len} + ); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_id_matches_blinded_id( + const unsigned char* session_id, + const unsigned char* blinded_id, + const unsigned char* server_pk +) { + try { + return session::session_id_matches_blinded_id( + {from_unsigned(session_id), 33}, + {from_unsigned(blinded_id), 33}, + {from_unsigned(server_pk), 32} + ); + } catch (...) { + return false; + } +} \ No newline at end of file diff --git a/src/curve25519.cpp b/src/curve25519.cpp new file mode 100644 index 00000000..89167e49 --- /dev/null +++ b/src/curve25519.cpp @@ -0,0 +1,94 @@ +#include "session/ed25519.hpp" + +#include +#include + +#include "session/export.h" + +namespace session::curve25519 { + +std::pair, std::array> curve25519_key_pair() { + std::array curve_pk; + std::array curve_sk; + crypto_box_keypair(curve_pk.data(), curve_sk.data()); + + return {curve_pk, curve_sk}; +} + +std::array to_curve25519_pubkey( + ustring_view ed25519_pubkey +) { + if (ed25519_pubkey.size() != 32) { + throw std::invalid_argument{"Invalid ed25519_pubkey: expected 32 bytes"}; + } + + std::array curve_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed25519_pubkey.data())) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to curve25519; " + "is the pubkey valid?"}; + + return curve_pk; +} + +std::array to_curve25519_seckey( + ustring_view ed25519_seckey +) { + if (ed25519_seckey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_seckey: expected 64 bytes"}; + } + + std::array curve_sk; + if (0 != crypto_sign_ed25519_sk_to_curve25519(curve_sk.data(), ed25519_seckey.data())) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to curve25519; " + "is the seckey valid?"}; + + return curve_sk; +} + +} // namespace session::curve25519 + +using namespace session; + +LIBSESSION_C_API bool session_curve25519_key_pair( + unsigned char* curve25519_pk_out, + unsigned char* curve25519_sk_out +) { + try { + auto result = session::curve25519::curve25519_key_pair(); + auto [curve_pk, curve_sk] = result; + std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); + std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_to_curve25519_pubkey( + const unsigned char* ed25519_pubkey, + unsigned char* curve25519_pk_out +) { + try { + auto curve_pk = session::curve25519::to_curve25519_pubkey(ustring_view{ed25519_pubkey, 32}); + std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_to_curve25519_seckey( + const unsigned char* ed25519_seckey, + unsigned char* curve25519_sk_out +) { + try { + auto curve_sk = session::curve25519::to_curve25519_seckey(ustring_view{ed25519_seckey, 64}); + std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + return true; + } catch (...) { + return false; + } +} diff --git a/src/ed25519.cpp b/src/ed25519.cpp new file mode 100644 index 00000000..0a319994 --- /dev/null +++ b/src/ed25519.cpp @@ -0,0 +1,137 @@ +#include "session/ed25519.hpp" + +#include +#include + +#include "session/export.h" + +namespace session::ed25519 { + +std::pair, std::array> ed25519_key_pair() { + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_keypair(ed_pk.data(), ed_sk.data()); + + return {ed_pk, ed_sk}; +} + +std::pair, std::array> ed25519_key_pair( + ustring_view ed25519_seed +) { + if (ed25519_seed.size() != 32) { + throw std::invalid_argument{"Invalid ed25519_seed: expected 32 bytes"}; + } + + std::array ed_pk; + std::array ed_sk; + + crypto_sign_ed25519_seed_keypair( + ed_pk.data(), ed_sk.data(), ed25519_seed.data()); + + return {ed_pk, ed_sk}; +} + +ustring seed_for_ed_privkey( + ustring_view ed25519_privkey) { + ustring result; + result.reserve(32); + + if (ed25519_privkey.size() == 32) { + // Assume we we actually given the seed so just return it + result += ed25519_privkey.data(); + return result; + } else if (ed25519_privkey.size() == 64) { + // The first 32 bytes of a 64 byte ed25519 private key are the seed + result += ed25519_privkey.substr(0, 32); + return result; + } + + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; +} + +ustring sign(ustring_view ed25519_privkey, ustring_view msg) { + std::array sig; + if (0 != crypto_sign_ed25519_detached( + sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + return {sig.data(), sig.size()}; +} + +bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { + return (0 == crypto_sign_ed25519_verify_detached( + sig.data(), msg.data(), msg.size(), pubkey.data())); +} + +} // namespace session::ed25519 + +using namespace session; + +LIBSESSION_C_API bool session_ed25519_key_pair( + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out +) { + try { + auto result = session::ed25519::ed25519_key_pair(); + auto [ed_pk, ed_sk] = result; + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_key_pair_seed( + const unsigned char* ed25519_seed, + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out +) { + try { + auto result = session::ed25519::ed25519_key_pair(ustring_view{ed25519_seed, 32}); + auto [ed_pk, ed_sk] = result; + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_seed_for_ed_privkey( + const unsigned char* ed25519_privkey, + unsigned char* ed25519_seed_out +) { + try { + auto result = session::ed25519::seed_for_ed_privkey(ustring_view{ed25519_privkey, 64}); + std::memcpy(ed25519_seed_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_sign( + const unsigned char* ed25519_privkey, + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out +) { + try { + auto result = session::ed25519::sign(ustring_view{ed25519_privkey, 64}, ustring_view{msg, msg_len}); + std::memcpy(ed25519_sig_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + + +LIBSESSION_C_API bool session_ed25519_verify( + const unsigned char* sig, + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len +) { + return session::ed25519::verify(ustring_view{sig, 64}, ustring_view{pubkey, 32}, ustring_view{msg, msg_len}); +} diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 1e8c0d8a..a68a403c 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,9 +1,14 @@ #include "session/session_encrypt.hpp" +#include #include #include #include +#include #include +#include +#include +#include #include #include @@ -11,6 +16,7 @@ #include #include "session/util.hpp" +#include "session/blinding.hpp" using namespace std::literals; @@ -133,7 +139,94 @@ ustring encrypt_for_recipient_deterministic( return result; } -std::pair decrypt_incoming( +ustring encrypt_for_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view recipient_blinded_id, + ustring_view message +) { + if (ed25519_privkey.size() == 64) + ed25519_privkey.remove_suffix(32); + else if (ed25519_privkey.size() != 32) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; + if (recipient_blinded_id.size() != 33) + throw std::invalid_argument{"Invalid recipient_blinded_id: expected 33 bytes"}; + + uc32 ed_pk; + cleared_uc64 ed_sk_from_seed; + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + + // Generate the blinded key pair & shared encryption key + std::pair blinded_key_pair; + + switch (recipient_blinded_id[0]) { + case 0x15: + blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); + break; + + case 0x25: + blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); + break; + + default: throw std::invalid_argument{"Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; + } + + // Remove the blinding prefix + recipient_blinded_id = {recipient_blinded_id.data() + 1, 32}; + auto [blinded_pk, blinded_sk] = blinded_key_pair; + + // Calculate the shared encryption key, sending from A to B: + // + // BLAKE2b(a kB || kA || kB) + // + // The receiver can calulate the same value via: + // + // BLAKE2b(b kA || kA || kB) + // + // Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to + // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the + // same secret scalar secret (and so this is just the most convenient way to get 'a' out of + // a sodium Ed25519 secret key) + cleared_uc32 a, ka; + uc32 blind_hash; + crypto_generichash_blake2b_state st; + crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_privkey.data()); + crypto_scalarmult_ed25519_noclamp(ka.data(), a.data(), recipient_blinded_id.data()); + crypto_generichash_blake2b_init(&st, nullptr, 0, 32); + crypto_generichash_blake2b_update(&st, ka.data(), ka.size()); + crypto_generichash_blake2b_update(&st, blinded_pk.data(), blinded_pk.size()); + crypto_generichash_blake2b_update(&st, recipient_blinded_id.data(), recipient_blinded_id.size()); + crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + + // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) + ustring buf; + buf.reserve(message.size() + 32); + buf += message; + buf += ustring_view{ed_pk.data(), 32}; + + // Encrypt using xchacha20-poly1305 + cleared_array nonce; + randombytes_buf(nonce.data(), nonce.size()); + + ustring ciphertext; + unsigned long long outlen = 0; + ciphertext.resize( + 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( + ciphertext.data() + 1, &outlen, buf.data(), buf.size(), nullptr, 0, nullptr, + nonce.data(), blind_hash.data())) + throw std::runtime_error{"Crypto aead encryption failed"}; + + // data = b'\x00' + ciphertext + nonce + assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy(ciphertext.data() + 1 + outlen, nonce.data(), nonce.size()); +} + +std::pair decrypt_incoming( ustring_view ed25519_privkey, ustring_view ciphertext) { cleared_uc64 ed_sk_from_seed; @@ -146,38 +239,159 @@ std::pair decrypt_incoming( throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; } - if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) - throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; - const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; - const size_t msg_size = outer_size - 32 - 64; - cleared_uc32 x_sec; uc32 x_pub; crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); crypto_scalarmult_base(x_pub.data(), x_sec.data()); - std::pair result; - auto& [buf, sender_ed_pk] = result; + return decrypt_incoming({x_pub.data(), 32}, {x_sec.data(), 32}, ciphertext); +} + +std::pair decrypt_incoming( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { + + if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) + throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; + const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; + const size_t msg_size = outer_size - 32 - 64; + + std::pair result; + auto& [sender_session_id, buf] = result; buf.resize(outer_size); if (0 != crypto_box_seal_open( - buf.data(), ciphertext.data(), ciphertext.size(), x_pub.data(), x_sec.data())) + buf.data(), ciphertext.data(), ciphertext.size(), x25519_pubkey.data(), x25519_seckey.data())) throw std::runtime_error{"Decryption failed"}; uc64 sig; - sender_ed_pk = buf.substr(msg_size, 32); + auto sender_ed_pk = buf.substr(msg_size, 32); std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify - buf += ustring_view{x_pub.data(), 32}; + buf += ustring_view{x25519_pubkey.data(), 32}; if (0 != crypto_sign_ed25519_verify_detached( sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) throw std::runtime_error{"Signature verification failed"}; - // Everything is good, so just drop A and Y off the message + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519( + sender_x_pk.data(), sender_ed_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 buf.resize(buf.size() - 32 - 32); + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); return result; } } // namespace session + +using namespace session; + +LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* recipient_pubkey, + unsigned char** ciphertext_out, + size_t* ciphertext_len +) { + try { + auto ciphertext = session::encrypt_for_recipient_deterministic( + ustring_view{ed25519_privkey, 64}, + ustring_view{recipient_pubkey, 32}, + ustring_view{plaintext_in, plaintext_len} + ); + + *ciphertext_out = static_cast(malloc(ciphertext.size())); + *ciphertext_len = ciphertext.size(); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* recipient_blinded_id, + unsigned char** ciphertext_out, + size_t* ciphertext_len +) { + try { + auto ciphertext = session::encrypt_for_blinded_recipient( + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{recipient_blinded_id, 33}, + ustring_view{plaintext_in, plaintext_len} + ); + + *ciphertext_out = static_cast(malloc(ciphertext.size())); + *ciphertext_len = ciphertext.size(); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_incoming( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len +) { + try { + auto result = session::decrypt_incoming( + ustring_view{ed25519_privkey, 64}, + ustring_view{ciphertext_in, ciphertext_len} + ); + auto [session_id, plaintext] = result; + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* x25519_pubkey, + const unsigned char* x25519_seckey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len +) { + try { + auto result = session::decrypt_incoming( + ustring_view{x25519_pubkey, 32}, + ustring_view{x25519_seckey, 32}, + ustring_view{ciphertext_in, ciphertext_len} + ); + auto [session_id, plaintext] = result; + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} \ No newline at end of file diff --git a/src/xchacha.cpp b/src/xchacha.cpp new file mode 100644 index 00000000..cc0591f4 --- /dev/null +++ b/src/xchacha.cpp @@ -0,0 +1,63 @@ +#include "session/xchacha.hpp" + +#include + +#include +#include +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::xchacha { + +ustring decrypt( + ustring_view ciphertext, + ustring_view seckey, + ustring_view nonce +) { + if (ciphertext.size() < 16) + throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + + ustring buf; + unsigned long long buf_len = 0; + buf.resize(ciphertext.size() - 16); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), &buf_len, nullptr, ciphertext.data(), ciphertext.size(), + nullptr, 0, nonce.data(), seckey.data())) + throw std::runtime_error{"Failed to encrypt; perhaps the secret key is invalid?"}; + + return {buf.data(), buf_len}; +} + +} // namespace session::xchacha + +// using session::xchacha::ustring_view; + +extern "C" { + +LIBSESSION_C_API bool session_xchacha_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + const unsigned char* seckey, + size_t seckey_len, + const unsigned char* nonce, + unsigned char** plaintext_out, + size_t* plaintext_out_len) { + try { + auto plaintext = session::xchacha::decrypt( + {ciphertext, ciphertext_len}, + {seckey, seckey_len}, + {nonce, 24} + ); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_out_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +} // extern "C" diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 420512aa..652ee7fd 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -156,39 +156,39 @@ using session::xed25519::ustring_view; extern "C" { -LIBSESSION_EXPORT int session_xed25519_sign( +LIBSESSION_C_API bool session_xed25519_sign( unsigned char* signature, const unsigned char* curve25519_privkey, const unsigned char* msg, - const unsigned int msg_len) { + size_t msg_len) { assert(signature != NULL); try { auto sig = session::xed25519::sign({curve25519_privkey, 32}, {msg, msg_len}); std::memcpy(signature, sig.data(), sig.size()); - return 0; + return true; } catch (...) { + return false; } - return 1; } -LIBSESSION_EXPORT int session_xed25519_verify( +LIBSESSION_C_API bool session_xed25519_verify( const unsigned char* signature, const unsigned char* pubkey, const unsigned char* msg, - const unsigned int msg_len) { + size_t msg_len) { return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}) ? 0 : 1; } -LIBSESSION_EXPORT int session_xed25519_pubkey( +LIBSESSION_C_API bool session_xed25519_pubkey( unsigned char* ed25519_pubkey, const unsigned char* curve25519_pubkey) { assert(ed25519_pubkey != NULL); try { auto edpk = session::xed25519::pubkey({curve25519_pubkey, 32}); std::memcpy(ed25519_pubkey, edpk.data(), edpk.size()); - return 0; + return true; } catch (...) { + return false; } - return 1; } } // extern "C" From 01a1f4625e61996c63005bc2d1017d61cfa242bb Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 5 Dec 2023 16:44:12 +1100 Subject: [PATCH 130/572] Got SessionBlindingProtocol for blind15 working, blind25 still WIP --- include/session/blinding.h | 78 ++++++++++++ include/session/blinding.hpp | 15 ++- include/session/session_encrypt.h | 11 ++ include/session/session_encrypt.hpp | 11 +- src/blinding.cpp | 21 ++++ src/onionreq/builder.cpp | 8 +- src/session_encrypt.cpp | 179 ++++++++++++++++++++++++++-- src/xchacha.cpp | 6 +- 8 files changed, 305 insertions(+), 24 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index ecf450f4..769752ba 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -8,18 +8,63 @@ extern "C" { #include "export.h" +/// API: crypto/session_blind15_key_pair +/// +/// This function attempts to generate a blind15 key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_key_pair( const unsigned char* ed25519_seckey, const unsigned char* server_pk, unsigned char* blinded_pk_out, unsigned char* blinded_sk_out); +/// API: crypto/session_blind25_key_pair +/// +/// This function attempts to generate a blind25 key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind25_key_pair( const unsigned char* ed25519_seckey, const unsigned char* server_pk, unsigned char* blinded_pk_out, unsigned char* blinded_sk_out); +/// API: crypto/session_blind15_sign +/// +/// This function attempts to generate a signature for a message using a blind15 private key. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `msg` -- [in] Pointer to a data buffer containing the message to generate a signature for. +/// - `msg_len` -- [in] Length of `msg` +/// - `blinded_sig_out` -- [out] pointer to a buffer of at least 64 bytes where the signature will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the signature was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_sign( const unsigned char* ed25519_seckey, const unsigned char* server_pk, @@ -27,6 +72,39 @@ LIBSESSION_EXPORT bool session_blind15_sign( size_t msg_len, unsigned char* blinded_sig_out); +/// API: crypto/session_blind25_sign +/// +/// This function attempts to generate a signature for a message using a blind25 private key. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `msg` -- [in] Pointer to a data buffer containing the message to generate a signature for. +/// - `msg_len` -- [in] Length of `msg` +/// - `blinded_sig_out` -- [out] pointer to a buffer of at least 64 bytes where the signature will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the signature was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind25_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out); + +/// API: crypto/session_blind25_sign +/// +/// This function attempts to generate a signature for a message using a blind25 private key. +/// +/// Inputs: +/// - `session_id` -- [in] the session_id to compare (either 32 bytes with a 0x05 prefix, or 33 bytes). +/// - `blinded_id` -- [in] the blinded_id to compare, can be either 0x15 or 0x25 blinded (33 bytes). +/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (32 bytes). +/// +/// Outputs: +/// - `bool` -- True if the session_id matches the blinded_id, false if not. LIBSESSION_EXPORT bool session_id_matches_blinded_id( const unsigned char* session_id, const unsigned char* blinded_id, diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index e192a472..09bb7f61 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -55,9 +55,13 @@ namespace session { /// /// This (R, S) signature is then Ed25519-verifiable using pubkey kA. +/// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is +/// exposed for debugging/testing. Takes server pk in bytes, not hex. +std::array blind15_factor(ustring_view server_pk); + /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is -/// exposed for debugging/testing. Takes session id and pk in bytes, not hex. session id can -/// be 05-prefixed (33 bytes) or unprefixed (32 bytes). +/// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session +/// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). std::array blind25_factor(ustring_view session_id, ustring_view server_pk); /// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a @@ -102,6 +106,13 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); + +/// Takes in a standard session_id and returns a flag indicating whether it matches the given +/// blinded_id for a given server_pk. +/// +/// Takes either a 0x15 or 0x25 blinded_id (33 bytes) and the server pubkey (32 bytes). +/// +/// Returns a flag indicating whether the session_id matches the blinded_id. bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, std::string_view server_pk); diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index adf0972a..c884a2eb 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -126,6 +126,17 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( unsigned char** plaintext_out, size_t* plaintext_len); +LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* sender_id, + const unsigned char* recipient_id, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len); + #ifdef __cplusplus } #endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 028bbe38..9bd1dca5 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -95,9 +95,9 @@ ustring encrypt_for_recipient_deterministic( /// Outputs: /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) -ustring session_encrypt_for_blinded_recipient( +ustring encrypt_for_blinded_recipient( ustring_view ed25519_privkey, - ustring_view open_group_pubkey, + ustring_view server_pk, ustring_view recipient_blinded_id, ustring_view message); @@ -161,4 +161,11 @@ std::pair decrypt_incoming(ustring_view ed25519_privkey, u std::pair decrypt_incoming( ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); +std::pair decrypt_from_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view sender_id, + ustring_view recipient_id, + ustring_view ciphertext); + } // namespace session diff --git a/src/blinding.cpp b/src/blinding.cpp index 441907ea..10dafce9 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -452,6 +452,27 @@ LIBSESSION_C_API bool session_blind15_sign( } } +LIBSESSION_C_API bool session_blind25_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out +) { + try { + auto result = session::blind25_sign( + {ed25519_seckey, 64}, + {from_unsigned(server_pk), 32}, + {msg, msg_len} + ); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_id_matches_blinded_id( const unsigned char* session_id, const unsigned char* blinded_id, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 2a868356..1e1a6b43 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -167,10 +167,6 @@ namespace { } } // namespace session::onionreq -extern "C" { - -using session::ustring; - namespace { session::onionreq::Builder& unbox(onion_request_builder_object* builder) { @@ -180,6 +176,10 @@ session::onionreq::Builder& unbox(onion_request_builder_object* builder) { } +extern "C" { + +using session::ustring; + LIBSESSION_C_API void onion_request_builder_init( onion_request_builder_object** builder) { auto c = std::make_unique(); diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index a68a403c..ca25bef0 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -26,6 +26,7 @@ template using cleared_array = sodium_cleared>; using uc32 = std::array; +using uc33 = std::array; using uc64 = std::array; using cleared_uc32 = cleared_array<32>; using cleared_uc64 = cleared_array<64>; @@ -175,8 +176,8 @@ ustring encrypt_for_blinded_recipient( } // Remove the blinding prefix - recipient_blinded_id = {recipient_blinded_id.data() + 1, 32}; - auto [blinded_pk, blinded_sk] = blinded_key_pair; + ustring kB = {recipient_blinded_id.data() + 1, 32}; + ustring kA = blinded_key_pair.first; // Calculate the shared encryption key, sending from A to B: // @@ -190,16 +191,18 @@ ustring encrypt_for_blinded_recipient( // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the // same secret scalar secret (and so this is just the most convenient way to get 'a' out of // a sodium Ed25519 secret key) - cleared_uc32 a, ka; - uc32 blind_hash; + cleared_uc32 a, sharedSecret; + uc32 enc_key; crypto_generichash_blake2b_state st; crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_privkey.data()); - crypto_scalarmult_ed25519_noclamp(ka.data(), a.data(), recipient_blinded_id.data()); + if (0 != crypto_scalarmult_ed25519_noclamp(sharedSecret.data(), a.data(), kB.data())) + throw std::runtime_error{"Shared secret generation failed"}; + crypto_generichash_blake2b_init(&st, nullptr, 0, 32); - crypto_generichash_blake2b_update(&st, ka.data(), ka.size()); - crypto_generichash_blake2b_update(&st, blinded_pk.data(), blinded_pk.size()); - crypto_generichash_blake2b_update(&st, recipient_blinded_id.data(), recipient_blinded_id.size()); - crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + crypto_generichash_blake2b_update(&st, sharedSecret.data(), sharedSecret.size()); + crypto_generichash_blake2b_update(&st, kA.data(), kA.size()); + crypto_generichash_blake2b_update(&st, kB.data(), kB.size()); + crypto_generichash_blake2b_final(&st, enc_key.data(), enc_key.size()); // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) ustring buf; @@ -214,16 +217,19 @@ ustring encrypt_for_blinded_recipient( ustring ciphertext; unsigned long long outlen = 0; ciphertext.resize( - 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data() + 1, &outlen, buf.data(), buf.size(), nullptr, 0, nullptr, - nonce.data(), blind_hash.data())) + ciphertext.data(), &outlen, buf.data(), buf.size(), nullptr, 0, nullptr, + nonce.data(), enc_key.data())) throw std::runtime_error{"Crypto aead encryption failed"}; // data = b'\x00' + ciphertext + nonce + ciphertext.insert(ciphertext.begin(), 0); assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::memcpy(ciphertext.data() + 1 + outlen, nonce.data(), nonce.size()); + std::memcpy(ciphertext.data() + (1 + outlen), nonce.data(), nonce.size()); + + return ciphertext; } std::pair decrypt_incoming( @@ -290,6 +296,122 @@ std::pair decrypt_incoming( return result; } +std::pair decrypt_from_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view sender_id, + ustring_view recipient_id, + ustring_view ciphertext +) { + if (ed25519_privkey.size() == 32) { + uc32 ignore_pk; + cleared_uc64 ed_sk_from_seed; + crypto_sign_ed25519_seed_keypair( + ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() != 64) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; + if (sender_id.size() != 33) + throw std::invalid_argument{"Invalid sender_id: expected 33 bytes"}; + if (recipient_id.size() != 33) + throw std::invalid_argument{"Invalid recipient_id: expected 33 bytes"}; + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid ciphertext: too short to contain valid encrypted data"}; + + std::pair result; + auto& [sender_session_id, buf] = result; + + // Determine whether it's an incoming or outgoing message + ustring_view kA = {sender_id.data() + 1, 32}; + ustring_view kB = {recipient_id.data() + 1, 32}; + std::pair blinded_key_pair; + + if (recipient_id[0] == 0x15 && sender_id[0] == 0x15) { + blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); + } else if (recipient_id[0] == 0x25 && sender_id[0] == 0x25) { + blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); + } else + throw std::invalid_argument{"Both sender_id and recipient_id must start with the same 0x15 or 0x25 prefix"}; + + // Calculate the shared encryption key, sending from A to B: + // + // BLAKE2b(a kB || kA || kB) + // + // The receiver can calulate the same value via: + // + // BLAKE2b(b kA || kA || kB) + // + // Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to + // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the + // same secret scalar secret (and so this is just the most convenient way to get 'a' out of + // a sodium Ed25519 secret key) + cleared_uc32 a, sharedSecret; + uc32 dec_key; + crypto_generichash_blake2b_state st; + ustring_view dst = (ustring{sender_id.data() + 1, 32} == blinded_key_pair.first ? kB : kA); + crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_privkey.data()); + if (0 != crypto_scalarmult_ed25519_noclamp(sharedSecret.data(), a.data(), dst.data())) + throw std::runtime_error{"Shared secret generation failed"}; + + crypto_generichash_blake2b_init(&st, nullptr, 0, 32); + crypto_generichash_blake2b_update(&st, sharedSecret.data(), sharedSecret.size()); + crypto_generichash_blake2b_update(&st, kA.data(), kA.size()); + crypto_generichash_blake2b_update(&st, kB.data(), kB.size()); + crypto_generichash_blake2b_final(&st, dec_key.data(), dec_key.size()); + + // v, ct, nc = data[0], data[1:-24], data[-24:] + if (ciphertext[0] != 0) + throw std::invalid_argument{"Invalid ciphertext: version is not 0"}; + + ustring nonce; + const size_t msg_size = (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + unsigned long long buf_len = 0; + buf.resize(msg_size); + nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy(nonce.data(), ciphertext.data() + msg_size + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES, + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), &buf_len, nullptr, ciphertext.data() + 1, + ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, 0, nonce.data(), dec_key.data())) + + // Ensure the length is correct + if (buf.size() <= 32) + throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; + + // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key + uc32 sender_ed_pk; + std::memcpy(sender_ed_pk.data(), buf.data() + (buf.size() - 32), 32); + + // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message + uc32 blindingFactor; + cleared_uc32 extracted_kA; + + if (sender_id[0] == 0x15) + blindingFactor = blind15_factor(server_pk); + else + blindingFactor = blind25_factor({sender_x_pk.data(), 32}, server_pk); // TODO: Need to confirm this... + + if (0 != crypto_scalarmult_ed25519_noclamp(extracted_kA.data(), blindingFactor.data(), sender_ed_pk.data())) + throw std::runtime_error{"Shared secret generation for verification failed"}; + if (kA != ustring_view{extracted_kA.data(), 32}) + throw std::runtime_error{"Shared secret does not match encoded public key"}; + }; + + // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix to + // the sender session ID + buf.resize(buf.size() - 32); + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + + return result; +} + } // namespace session using namespace session; @@ -386,6 +508,37 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( ); auto [session_id, plaintext] = result; + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* sender_id, + const unsigned char* recipient_id, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len +) { + try { + auto result = session::decrypt_from_blinded_recipient( + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{sender_id, 33}, + ustring_view{recipient_id, 33}, + ustring_view{ciphertext_in, ciphertext_len} + ); + auto [session_id, plaintext] = result; + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/xchacha.cpp b/src/xchacha.cpp index cc0591f4..0d8afc1c 100644 --- a/src/xchacha.cpp +++ b/src/xchacha.cpp @@ -16,17 +16,17 @@ ustring decrypt( ustring_view seckey, ustring_view nonce ) { - if (ciphertext.size() < 16) + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; ustring buf; unsigned long long buf_len = 0; - buf.resize(ciphertext.size() - 16); + buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( buf.data(), &buf_len, nullptr, ciphertext.data(), ciphertext.size(), nullptr, 0, nonce.data(), seckey.data())) - throw std::runtime_error{"Failed to encrypt; perhaps the secret key is invalid?"}; + throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; return {buf.data(), buf_len}; } From d1c0ed057f205ee601cacf709952126fa842617a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 6 Dec 2023 16:17:27 +1100 Subject: [PATCH 131/572] Exposed hash & random generation, ONS, PN & blind25 message decryption --- include/session/hash.h | 33 ++++++++ include/session/hash.hpp | 20 +++++ include/session/random.h | 24 ++++++ include/session/random.hpp | 18 +++++ include/session/session_encrypt.h | 74 +++++++++++++++++ include/session/session_encrypt.hpp | 32 ++++++++ include/session/xchacha.h | 42 ---------- include/session/xchacha.hpp | 23 ------ src/CMakeLists.txt | 3 +- src/hash.cpp | 46 +++++++++++ src/random.cpp | 29 +++++++ src/session_encrypt.cpp | 120 +++++++++++++++++++++++++++- src/xchacha.cpp | 63 --------------- 13 files changed, 396 insertions(+), 131 deletions(-) create mode 100644 include/session/hash.h create mode 100644 include/session/hash.hpp create mode 100644 include/session/random.h create mode 100644 include/session/random.hpp delete mode 100644 include/session/xchacha.h delete mode 100644 include/session/xchacha.hpp create mode 100644 src/hash.cpp create mode 100644 src/random.cpp delete mode 100644 src/xchacha.cpp diff --git a/include/session/hash.h b/include/session/hash.h new file mode 100644 index 00000000..03777629 --- /dev/null +++ b/include/session/hash.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_hash +/// +/// Wrapper around the crypto_generichash_blake2b function. +/// +/// Inputs: +/// - `size` -- [in] length of the hash to be generated. +/// - `msg_in` -- [in] the message a hash should be generated for. +/// - `msg_len` -- [in] length of `msg_in`. +/// - `key_in` -- [in] an optional key to be used when generating the hash. +/// - `key_len` -- [in] length of `key_in`. +/// - `hash_out` -- [out] pointer to a buffer of at least `size` bytes where the +/// hash will be written. +LIBSESSION_EXPORT void session_hash( + size_t size, + const unsigned char* msg_in, + size_t msg_len, + const unsigned char* key_in, + size_t key_len, + unsigned char* hash_out); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/hash.hpp b/include/session/hash.hpp new file mode 100644 index 00000000..344f88c8 --- /dev/null +++ b/include/session/hash.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "types.hpp" + +namespace session::hash { + +/// API: hash/hash +/// +/// Wrapper around the crypto_generichash_blake2b function. +/// +/// Inputs: +/// - `size` -- length of the hash to be generated. +/// - `msg` -- the message to generate a hash for. +/// - `key` -- an optional key to be used when generating the hash. +/// +/// Outputs: +/// - a `size` byte hash. +ustring hash(const size_t size, ustring_view msg, std::optional key); + +} // namespace session::hash diff --git a/include/session/random.h b/include/session/random.h new file mode 100644 index 00000000..2d560e38 --- /dev/null +++ b/include/session/random.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_random +/// +/// Wrapper around the randombytes_buf function. +/// +/// Inputs: +/// - `size` -- [in] number of bytes to be generated. +/// +/// Outputs: +/// - `unsigned char*` -- pointer to random bytes of `size` bytes. +LIBSESSION_EXPORT unsigned char* session_random(size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/random.hpp b/include/session/random.hpp new file mode 100644 index 00000000..72fc8b89 --- /dev/null +++ b/include/session/random.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "types.hpp" + +namespace session::random { + +/// API: random/random +/// +/// Wrapper around the randombytes_buf function. +/// +/// Inputs: +/// - `size` -- the number of random bytes to be generated. +/// +/// Outputs: +/// - random bytes of the specified length. +ustring random(size_t size); + +} // namespace session::random diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index c884a2eb..972c79e9 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -126,6 +126,33 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( unsigned char** plaintext_out, size_t* plaintext_len); +/// API: crypto/session_decrypt_for_blinded_recipient +/// +/// This function attempts to decrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the receiver (64 bytes). +/// - `open_group_pubkey` -- [in] the public key of the open group server to route +/// the blinded message through (32 bytes). +/// - `sender_id` -- [in] the blinded id of the sender including the blinding prefix (33 bytes), +/// 'blind15' or 'blind25' decryption will be chosed based on this value. +/// - `recipient_id` -- [in] the blinded id of the recipient including the blinding prefix (33 bytes), +/// must match the same 'blind15' or 'blind25' type of the `sender_id`. +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted 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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( const unsigned char* ciphertext_in, size_t ciphertext_len, @@ -137,6 +164,53 @@ LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( unsigned char** plaintext_out, size_t* plaintext_len); +/// API: crypto/session_decrypt_ons_response +/// +/// This function attempts to decrypt an ONS response. +/// +/// Inputs: +/// - `lowercase_name_in` -- [in] Pointer to a buffer containing the lowercase name used to trigger the response. +/// - `name_len` -- [in] Length of `name_in`. +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in`. +/// - `nonce_in` -- [in] Pointer to a data buffer containing the nonce (24 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id will be written if decryption was successful. +/// +/// Outputs: +/// - `bool` -- True if the session ID was successfully decrypted, false if decryption failed. +LIBSESSION_EXPORT bool session_decrypt_ons_response( + const char* lowercase_name_in, + size_t name_len, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* nonce_in, + char* session_id_out); + +/// API: crypto/session_decrypt_push_notification +/// +/// Decrypts a push notification payload. +/// +/// Inputs: +/// - `payload_in` -- [in] the payload included in the push notification. +/// - `payload_len` -- [in] Length of `payload_in`. +/// - `enc_key_in` -- [in] the device encryption key used when subscribing for push notifications (32 bytes). +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted 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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the decryption was successful, false if decryption failed. +LIBSESSION_EXPORT bool session_decrypt_push_notification( + const unsigned char* payload_in, + size_t payload_len, + const unsigned char* enc_key_in, + unsigned char** plaintext_out, + size_t* plaintext_len); + #ifdef __cplusplus } #endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 9bd1dca5..7bed78cd 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -168,4 +168,36 @@ std::pair decrypt_from_blinded_recipient( ustring_view recipient_id, ustring_view ciphertext); +/// API: crypto/decrypt_ons_response +/// +/// Decrypts the response of an ONS lookup. +/// +/// Inputs: +/// - `lowercase_name` -- the lowercase name which was looked to up to retrieve this response. +/// - `ciphertext` -- ciphertext returned from the server. +/// - `nonce` -- the nonce returned from the server +/// +/// Outputs: +/// - `std::string` -- the session ID (in hex) returned from the server, *if* the server returned +/// a session ID. Throws on error/failure. +std::string decrypt_ons_response( + std::string_view lowercase_name, + ustring_view ciphertext, + ustring_view nonce); + +/// API: crypto/decrypt_push_notification +/// +/// Decrypts a push notification payload. +/// +/// Inputs: +/// - `payload` -- the payload included in the push notification. +/// - `enc_key` -- the device encryption key used when subscribing for push notifications (32 bytes). +/// +/// Outputs: +/// - `ustring` -- the decrypted push notification payload, *if* the decryption was +/// successful. Throws on error/failure. +ustring decrypt_push_notification( + ustring_view payload, + ustring_view enc_key); + } // namespace session diff --git a/include/session/xchacha.h b/include/session/xchacha.h deleted file mode 100644 index 9997bf54..00000000 --- a/include/session/xchacha.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -#include "export.h" - -/// API: crypto/session_xchacha_decrypt -/// -/// Wrapper around the crypto_aead_xchacha20poly1305_ietf_decrypt function. -/// -/// Inputs: -/// - `ciphertext` -- [in] the data to be decrypted. -/// - `ciphertext_len` -- [in] length of the `ciphertext` data. -/// - `seckey` -- [in] the secret key used to encrypt the data. -/// - `seckey_len` -- [in] length of the `seckey`. -/// - `nonce` -- [in] the nonce used to encrypt the data. -/// - `plaintext_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 *unless* the function returns -/// 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. -/// -/// Outputs: -/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If -/// (and only if) true is returned then `plaintext_out` must be freed when done with it. -LIBSESSION_EXPORT bool session_xchacha_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - const unsigned char* seckey, - size_t seckey_len, - const unsigned char* nonce, - unsigned char** plaintext_out, - size_t* plaintext_out_len); - -#ifdef __cplusplus -} -#endif diff --git a/include/session/xchacha.hpp b/include/session/xchacha.hpp deleted file mode 100644 index 92d3582b..00000000 --- a/include/session/xchacha.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "types.hpp" - -namespace session::xchacha { - -/// API: xchacha/decrypt -/// -/// Wrapper around the crypto_aead_xchacha20poly1305_ietf_decrypt function. -/// -/// Inputs: -/// - `ciphertext` -- the data to be decrypted. -/// - `seckey` -- the secret key used to encrypt the data. -/// - `nonce` -- the nonce used to encrypt the data. -/// -/// Outputs: -/// - the plaintext binary data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. -ustring decrypt( - ustring_view ciphertext, - ustring_view seckey, - ustring_view nonce); - -} // namespace session::xchacha diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f5536f1..6c3f1158 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -49,10 +49,11 @@ add_libsession_util_library(crypto blinding.cpp curve25519.cpp ed25519.cpp + hash.cpp multi_encrypt.cpp + random.cpp session_encrypt.cpp util.cpp - xchacha.cpp xed25519.cpp ) diff --git a/src/hash.cpp b/src/hash.cpp new file mode 100644 index 00000000..dcf6db6d --- /dev/null +++ b/src/hash.cpp @@ -0,0 +1,46 @@ +#include "session/hash.hpp" + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::hash { + +ustring hash(const size_t size, ustring_view msg, std::optional key) { + unsigned char result[size]; + + if (key) + crypto_generichash_blake2b(result, size, msg.data(), msg.size(), + static_cast(*key).data(), static_cast(*key).size()); + else + crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); + + return {result, size}; +} + +} // namespace session::hash + +using session::ustring; +using session::ustring_view; + +extern "C" { + +LIBSESSION_C_API void session_hash( + size_t size, + const unsigned char* msg_in, + size_t msg_len, + const unsigned char* key_in, + size_t key_len, + unsigned char* hash_out +) { + std::optional key; + + if (key_in && key_len) + key = {key_in, key_len}; + + ustring result = session::hash::hash(size, {msg_in, msg_len}, key); + std::memcpy(hash_out, result.data(), size); +} + +} // extern "C" diff --git a/src/random.cpp b/src/random.cpp new file mode 100644 index 00000000..693dd489 --- /dev/null +++ b/src/random.cpp @@ -0,0 +1,29 @@ +#include "session/random.hpp" + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::random { + +ustring random(size_t size) { + ustring result; + result.resize(size); + randombytes_buf(result.data(), size); + + return result; +} + +} // namespace session::random + +extern "C" { + +LIBSESSION_C_API unsigned char* session_random(size_t size) { + auto result = session::random::random(size); + auto* ret = static_cast(malloc(size)); + std::memcpy(ret, result.data(), result.size()); + return ret; +} + +} // extern "C" diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index ca25bef0..58742a4b 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -387,14 +388,27 @@ std::pair decrypt_from_blinded_recipient( uc32 sender_ed_pk; std::memcpy(sender_ed_pk.data(), buf.data() + (buf.size() - 32), 32); + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message uc32 blindingFactor; cleared_uc32 extracted_kA; if (sender_id[0] == 0x15) blindingFactor = blind15_factor(server_pk); - else - blindingFactor = blind25_factor({sender_x_pk.data(), 32}, server_pk); // TODO: Need to confirm this... + else { + blindingFactor = blind25_factor({sender_x_pk.data(), 32}, server_pk); + + // sender_ed_pk is negative, so we need to negate `blindingFactor` to make things come out right + if (sender_ed_pk[31] & 0x80) { + uc32 blindingFactor_neg; + crypto_core_ed25519_scalar_negate(blindingFactor_neg.data(), blindingFactor.data()); + std::memcpy(blindingFactor.data(), blindingFactor_neg.data(), 32); + } + } if (0 != crypto_scalarmult_ed25519_noclamp(extracted_kA.data(), blindingFactor.data(), sender_ed_pk.data())) throw std::runtime_error{"Shared secret generation for verification failed"}; @@ -412,6 +426,64 @@ std::pair decrypt_from_blinded_recipient( return result; } +std::string decrypt_ons_response(std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + + // Hash the ONS name using BLAKE2b + // + // xchacha-based encryption + // key = H(name, key=H(name)) + uc32 key; + uc32 name_hash; + auto name_bytes = to_unsigned(lowercase_name.data()); + crypto_generichash_blake2b(name_hash.data(), name_hash.size(), name_bytes, lowercase_name.size(), nullptr, 0); + crypto_generichash_blake2b(key.data(), key.size(), name_bytes, lowercase_name.size(), name_hash.data(), name_hash.size()); + + ustring buf; + unsigned long long buf_len = 0; + buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), &buf_len, nullptr, ciphertext.data(), ciphertext.size(), + nullptr, 0, nonce.data(), key.data())) + throw std::runtime_error{"Failed to decrypt"}; + + if (buf_len != 33) + throw std::runtime_error{"Invalid decrypted value: expected to be 33 bytes"}; + + std::string session_id = oxenc::to_hex(buf.begin(), buf.end()); + return session_id; +} + +ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { + if (payload.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; + if (enc_key.size() != 32) + throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; + + ustring buf; + ustring nonce; + const size_t msg_size = (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + unsigned long long buf_len = 0; + buf.resize(msg_size); + nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy(nonce.data(), payload.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), &buf_len, nullptr, payload.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + payload.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, nullptr, 0, + nonce.data(), enc_key.data())) + throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; + + // Removing any null padding bytes from the end + if (auto pos = buf.find_last_not_of('\0'); pos != std::string::npos) + buf.resize(pos + 1); + + return buf; +} + } // namespace session using namespace session; @@ -540,6 +612,50 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( auto [session_id, plaintext] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_ons_response( + const char* name_in, + size_t name_len, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* nonce_in, + char* session_id_out +) { + try { + auto session_id = session::decrypt_ons_response( + std::string_view{name_in, name_len}, + ustring_view{ciphertext_in, ciphertext_len}, + ustring_view{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES} + ); + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_push_notification( + const unsigned char* payload_in, + size_t payload_len, + const unsigned char* enc_key_in, + unsigned char** plaintext_out, + size_t* plaintext_len +) { + try { + auto plaintext = session::decrypt_push_notification( + ustring_view{payload_in, payload_len}, + ustring_view{enc_key_in, 32} + ); + *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); diff --git a/src/xchacha.cpp b/src/xchacha.cpp deleted file mode 100644 index 0d8afc1c..00000000 --- a/src/xchacha.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "session/xchacha.hpp" - -#include - -#include -#include -#include - -#include "session/export.h" -#include "session/util.hpp" - -namespace session::xchacha { - -ustring decrypt( - ustring_view ciphertext, - ustring_view seckey, - ustring_view nonce -) { - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; - - ustring buf; - unsigned long long buf_len = 0; - buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), &buf_len, nullptr, ciphertext.data(), ciphertext.size(), - nullptr, 0, nonce.data(), seckey.data())) - throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; - - return {buf.data(), buf_len}; -} - -} // namespace session::xchacha - -// using session::xchacha::ustring_view; - -extern "C" { - -LIBSESSION_C_API bool session_xchacha_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - const unsigned char* seckey, - size_t seckey_len, - const unsigned char* nonce, - unsigned char** plaintext_out, - size_t* plaintext_out_len) { - try { - auto plaintext = session::xchacha::decrypt( - {ciphertext, ciphertext_len}, - {seckey, seckey_len}, - {nonce, 24} - ); - *plaintext_out = static_cast(malloc(plaintext.size())); - *plaintext_out_len = plaintext.size(); - std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); - return true; - } catch (...) { - return false; - } -} - -} // extern "C" From 4d3a3f272c8de41ad832526dd5055bd3f8b7ad41 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Dec 2023 14:38:20 +1100 Subject: [PATCH 132/572] Added some error checking to the hash generation --- include/session/hash.h | 5 ++++- src/hash.cpp | 35 +++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/include/session/hash.h b/include/session/hash.h index 03777629..e45bdd59 100644 --- a/include/session/hash.h +++ b/include/session/hash.h @@ -20,7 +20,10 @@ extern "C" { /// - `key_len` -- [in] length of `key_in`. /// - `hash_out` -- [out] pointer to a buffer of at least `size` bytes where the /// hash will be written. -LIBSESSION_EXPORT void session_hash( +/// +/// Outputs: +/// - `bool` -- True if the generation was successful, false if generation failed. +LIBSESSION_EXPORT bool session_hash( size_t size, const unsigned char* msg_in, size_t msg_len, diff --git a/src/hash.cpp b/src/hash.cpp index dcf6db6d..1944ad5b 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -8,13 +8,23 @@ namespace session::hash { ustring hash(const size_t size, ustring_view msg, std::optional key) { + if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) + throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; + + if (key && static_cast(*key).size() > crypto_generichash_blake2b_BYTES_MAX) + throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; + + auto result_code = 0; unsigned char result[size]; if (key) - crypto_generichash_blake2b(result, size, msg.data(), msg.size(), + result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), static_cast(*key).data(), static_cast(*key).size()); else - crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); + result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); + + if (result_code != 0) + throw std::runtime_error{"Hash generation failed"}; return {result, size}; } @@ -26,7 +36,7 @@ using session::ustring_view; extern "C" { -LIBSESSION_C_API void session_hash( +LIBSESSION_C_API bool session_hash( size_t size, const unsigned char* msg_in, size_t msg_len, @@ -34,13 +44,18 @@ LIBSESSION_C_API void session_hash( size_t key_len, unsigned char* hash_out ) { - std::optional key; - - if (key_in && key_len) - key = {key_in, key_len}; - - ustring result = session::hash::hash(size, {msg_in, msg_len}, key); - std::memcpy(hash_out, result.data(), size); + try { + std::optional key; + + if (key_in && key_len) + key = {key_in, key_len}; + + ustring result = session::hash::hash(size, {msg_in, msg_len}, key); + std::memcpy(hash_out, result.data(), size); + return true; + } catch (...) { + return false; + } } } // extern "C" From bb99bf51227fd8d8878da748cd47df671e8ac72c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Dec 2023 17:44:18 +1100 Subject: [PATCH 133/572] Fixed mistake with sessionId to blindedId comparison, tweaks to docs --- include/session/blinding.h | 36 ++++++++++++++--------------- include/session/curve25519.h | 14 +++++------ include/session/ed25519.h | 20 ++++++++-------- include/session/session_encrypt.h | 36 ++++++++++++++--------------- include/session/session_encrypt.hpp | 18 +++++++++++++++ src/blinding.cpp | 2 +- 6 files changed, 72 insertions(+), 54 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 769752ba..3e56f7ae 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -24,10 +24,10 @@ extern "C" { /// Outputs: /// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_key_pair( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - unsigned char* blinded_pk_out, - unsigned char* blinded_sk_out); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); /// API: crypto/session_blind25_key_pair /// @@ -45,10 +45,10 @@ LIBSESSION_EXPORT bool session_blind15_key_pair( /// Outputs: /// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind25_key_pair( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - unsigned char* blinded_pk_out, - unsigned char* blinded_sk_out); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); /// API: crypto/session_blind15_sign /// @@ -66,11 +66,11 @@ LIBSESSION_EXPORT bool session_blind25_key_pair( /// Outputs: /// - `bool` -- True if the signature was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_sign( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ const unsigned char* msg, size_t msg_len, - unsigned char* blinded_sig_out); + unsigned char* blinded_sig_out /* 64 byte output buffer */); /// API: crypto/session_blind25_sign /// @@ -88,27 +88,27 @@ LIBSESSION_EXPORT bool session_blind15_sign( /// Outputs: /// - `bool` -- True if the signature was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind25_sign( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ const unsigned char* msg, size_t msg_len, - unsigned char* blinded_sig_out); + unsigned char* blinded_sig_out /* 64 byte output buffer */); /// API: crypto/session_blind25_sign /// /// This function attempts to generate a signature for a message using a blind25 private key. /// /// Inputs: -/// - `session_id` -- [in] the session_id to compare (either 32 bytes with a 0x05 prefix, or 33 bytes). +/// - `session_id` -- [in] the session_id to compare (either 32 bytes, or 33 bytes with a 0x05 prefix). /// - `blinded_id` -- [in] the blinded_id to compare, can be either 0x15 or 0x25 blinded (33 bytes). /// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (32 bytes). /// /// Outputs: /// - `bool` -- True if the session_id matches the blinded_id, false if not. LIBSESSION_EXPORT bool session_id_matches_blinded_id( - const unsigned char* session_id, - const unsigned char* blinded_id, - const unsigned char* server_pk); + const unsigned char* session_id, /* 32 or 33 bytes */ + const unsigned char* blinded_id, /* 33 bytes */ + const unsigned char* server_pk /* 32 bytes */); #ifdef __cplusplus } diff --git a/include/session/curve25519.h b/include/session/curve25519.h index f903ab38..958b6e61 100644 --- a/include/session/curve25519.h +++ b/include/session/curve25519.h @@ -19,22 +19,22 @@ extern "C" { /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_curve25519_key_pair( - unsigned char* curve25519_pk_out, - unsigned char* curve25519_sk_out); + unsigned char* curve25519_pk_out, /* 32 byte output buffer */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); /// API: crypto/session_to_curve25519_pubkey /// /// Generates a curve25519 public key for an ed25519 public key. /// /// Inputs: -/// - `ed25519_pubkey` -- the ed25519 public key. +/// - `ed25519_pubkey` -- the ed25519 public key (32 bytes). /// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. /// /// Outputs: /// - `bool` -- True if the public key was successfully generated, false if failed. LIBSESSION_EXPORT bool session_to_curve25519_pubkey( - const unsigned char* ed25519_pubkey, - unsigned char* curve25519_pk_out); + const unsigned char* ed25519_pubkey, /* 32 bytes */ + unsigned char* curve25519_pk_out /* 32 byte output buffer */); /// API: crypto/session_to_curve25519_seckey /// @@ -49,8 +49,8 @@ LIBSESSION_EXPORT bool session_to_curve25519_pubkey( /// Outputs: /// - `bool` -- True if the secret key was successfully generated, false if failed. LIBSESSION_EXPORT bool session_to_curve25519_seckey( - const unsigned char* ed25519_seckey, - unsigned char* curve25519_sk_out); + const unsigned char* ed25519_seckey, /* 64 bytes */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); #ifdef __cplusplus } diff --git a/include/session/ed25519.h b/include/session/ed25519.h index 954d6bee..0072296e 100644 --- a/include/session/ed25519.h +++ b/include/session/ed25519.h @@ -19,8 +19,8 @@ extern "C" { /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_key_pair( - unsigned char* ed25519_pk_out, - unsigned char* ed25519_sk_out); + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); /// API: crypto/session_ed25519_key_pair_seed /// @@ -34,9 +34,9 @@ LIBSESSION_EXPORT bool session_ed25519_key_pair( /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( - const unsigned char* ed25519_seed, - unsigned char* ed25519_pk_out, - unsigned char* ed25519_sk_out); + const unsigned char* ed25519_seed, /* 32 bytes */ + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); /// API: crypto/session_seed_for_ed_privkey /// @@ -50,8 +50,8 @@ LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_seed_for_ed_privkey( - const unsigned char* ed25519_privkey, - unsigned char* ed25519_seed_out); + const unsigned char* ed25519_privkey, /* 64 bytes */ + unsigned char* ed25519_seed_out /* 32 byte output buffer */); /// API: crypto/session_ed25519_sign /// @@ -66,10 +66,10 @@ LIBSESSION_EXPORT bool session_seed_for_ed_privkey( /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_sign( - const unsigned char* ed25519_privkey, + const unsigned char* ed25519_privkey, /* 64 bytes */ const unsigned char* msg, size_t msg_len, - unsigned char* ed25519_sig_out); + unsigned char* ed25519_sig_out /* 64 byte output buffer */); /// API: crypto/session_ed25519_verify /// @@ -84,7 +84,7 @@ LIBSESSION_EXPORT bool session_ed25519_sign( /// Outputs: /// - A flag indicating whether the signature is valid LIBSESSION_EXPORT bool session_ed25519_verify( - const unsigned char* sig, + const unsigned char* sig, /* 64 bytes */ const unsigned char* pubkey, const unsigned char* msg, size_t msg_len); diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 972c79e9..eeba222a 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -30,8 +30,8 @@ extern "C" { LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* ed25519_privkey, - const unsigned char* recipient_pubkey, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* recipient_pubkey, /* 32 bytes */ unsigned char** ciphertext_out, size_t* ciphertext_len); @@ -60,9 +60,9 @@ LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( LIBSESSION_EXPORT bool session_encrypt_for_blinded_recipient( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* ed25519_privkey, - const unsigned char* open_group_pubkey, - const unsigned char* recipient_blinded_id, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* open_group_pubkey, /* 32 bytes */ + const unsigned char* recipient_blinded_id, /* 33 bytes */ unsigned char** ciphertext_out, size_t* ciphertext_len); @@ -90,8 +90,8 @@ LIBSESSION_EXPORT bool session_encrypt_for_blinded_recipient( LIBSESSION_EXPORT bool session_decrypt_incoming( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* ed25519_privkey, - char* session_id_out, + const unsigned char* ed25519_privkey, /* 64 bytes */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -120,9 +120,9 @@ LIBSESSION_EXPORT bool session_decrypt_incoming( LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* x25519_pubkey, - const unsigned char* x25519_seckey, - char* session_id_out, + const unsigned char* x25519_pubkey, /* 32 bytes */ + const unsigned char* x25519_seckey, /* 32 bytes */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -156,11 +156,11 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* ed25519_privkey, - const unsigned char* open_group_pubkey, - const unsigned char* sender_id, - const unsigned char* recipient_id, - char* session_id_out, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* open_group_pubkey, /* 32 bytes */ + const unsigned char* sender_id, /* 33 bytes */ + const unsigned char* recipient_id, /* 33 bytes */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -184,8 +184,8 @@ LIBSESSION_EXPORT bool session_decrypt_ons_response( size_t name_len, const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* nonce_in, - char* session_id_out); + const unsigned char* nonce_in, /* 24 bytes */ + char* session_id_out /* 67 byte output buffer */); /// API: crypto/session_decrypt_push_notification /// @@ -207,7 +207,7 @@ LIBSESSION_EXPORT bool session_decrypt_ons_response( LIBSESSION_EXPORT bool session_decrypt_push_notification( const unsigned char* payload_in, size_t payload_len, - const unsigned char* enc_key_in, + const unsigned char* enc_key_in, /* 32 bytes */ unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 7bed78cd..32c36462 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -161,6 +161,24 @@ std::pair decrypt_incoming(ustring_view ed25519_privkey, u std::pair decrypt_incoming( ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); +/// API: crypto/decrypt_from_blinded_recipient +/// +/// This function attempts to decrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `server_pk` -- the public key of the open group server to route the blinded message through (32 bytes). +/// - `sender_id` -- the blinded id of the sender including the blinding prefix (33 bytes), +/// 'blind15' or 'blind25' decryption will be chosed based on this value. +/// - `recipient_id` -- the blinded id of the recipient including the blinding prefix (33 bytes), +/// must match the same 'blind15' or 'blind25' type of the `sender_id`. +/// - `ciphertext` -- Pointer to a data buffer containing the encrypted data. +/// +/// Outputs: +/// - `std::pair` -- the session ID (in hex) and the plaintext binary +/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. std::pair decrypt_from_blinded_recipient( ustring_view ed25519_privkey, ustring_view server_pk, diff --git a/src/blinding.cpp b/src/blinding.cpp index 10dafce9..54b20152 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -385,7 +385,7 @@ bool session_id_matches_blinded_id(std::string_view session_id, std::string_view // For the negative, what we're going to get out of the above is simply the negative of // converted_blind_id1, so flip the sign bit to get converted_blind_id2 auto converted_blind_id2 = converted_blind_id1; - converted_blind_id2[32] &= 0x7f; + converted_blind_id2[32] ^= 0x80; return ( to_unsigned_sv(blinded_id) == converted_blind_id1 || From b8db49d7fe7beba2443b1eabdf483711ee595bba Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 8 Dec 2023 18:01:15 +1100 Subject: [PATCH 134/572] Fixed some bugs and started working on unit tests --- include/session/blinding.hpp | 4 + include/session/session_encrypt.hpp | 55 +++++++++++--- src/session_encrypt.cpp | 80 +++++++++++++------- src/xed25519.cpp | 2 +- tests/test_session_encrypt.cpp | 111 +++++++++++++++++++++++++++- tests/test_xed25519.cpp | 18 ++--- 6 files changed, 222 insertions(+), 48 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 09bb7f61..688657a1 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -64,6 +64,10 @@ std::array blind15_factor(ustring_view server_pk); /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). std::array blind25_factor(ustring_view session_id, ustring_view server_pk); +/// Computes the 25-blinded id from a session id and server pubkey. Values accepted and +/// returned are hex-encoded. +std::string blind25_id(std::string_view session_id, std::string_view server_pk); + /// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a /// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 32c36462..271170c7 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -127,6 +127,42 @@ ustring encrypt_for_blinded_recipient( ustring sign_for_recipient( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 +/// pubkey, and verifies that the sender Ed25519 signature on the message. +/// +/// Inputs: +/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming( + ustring_view ed25519_privkey, ustring_view ciphertext); + +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 +/// pubkey, and verifies that the sender Ed25519 signature on the message. This function is used +/// for decrypting legacy group messages which only have an x25519 key pair, the Ed25519 version +/// of this function should be preferred where possible. +/// +/// Inputs: +/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); + /// API: crypto/decrypt_incoming /// /// Inverse of `encrypt_for_recipient`: this decrypts the message, verifies that the sender Ed25519 @@ -139,9 +175,10 @@ ustring sign_for_recipient( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the session ID (in hex) and the plaintext binary -/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming_session_id( + ustring_view ed25519_privkey, ustring_view ciphertext); /// API: crypto/decrypt_incoming /// @@ -156,9 +193,9 @@ std::pair decrypt_incoming(ustring_view ed25519_privkey, u /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the session ID (in hex) and the plaintext binary -/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_incoming( +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming_session_id( ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); /// API: crypto/decrypt_from_blinded_recipient @@ -177,9 +214,9 @@ std::pair decrypt_incoming( /// - `ciphertext` -- Pointer to a data buffer containing the encrypted data. /// /// Outputs: -/// - `std::pair` -- the session ID (in hex) and the plaintext binary -/// data that was encrypted, *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_from_blinded_recipient( +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_from_blinded_recipient( ustring_view ed25519_privkey, ustring_view server_pk, ustring_view sender_id, diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 58742a4b..bfb521f6 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -233,9 +233,50 @@ ustring encrypt_for_blinded_recipient( return ciphertext; } -std::pair decrypt_incoming( +std::pair decrypt_incoming_session_id( ustring_view ed25519_privkey, ustring_view ciphertext) { + auto [buf, sender_ed_pk] = decrypt_incoming(ed25519_privkey, ciphertext); + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519( + sender_x_pk.data(), sender_ed_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 + std::string sender_session_id; + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + + return {buf, sender_session_id}; +} + +std::pair decrypt_incoming_session_id( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { + auto [buf, sender_ed_pk] = decrypt_incoming(x25519_pubkey, x25519_seckey, ciphertext); + + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519( + sender_x_pk.data(), sender_ed_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 + std::string sender_session_id; + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + + return {buf, sender_session_id}; +} + +std::pair decrypt_incoming( + ustring_view ed25519_privkey, ustring_view ciphertext) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -254,7 +295,7 @@ std::pair decrypt_incoming( return decrypt_incoming({x_pub.data(), 32}, {x_sec.data(), 32}, ciphertext); } -std::pair decrypt_incoming( +std::pair decrypt_incoming( ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) @@ -262,8 +303,8 @@ std::pair decrypt_incoming( const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; const size_t msg_size = outer_size - 32 - 64; - std::pair result; - auto& [sender_session_id, buf] = result; + std::pair result; + auto& [buf, sender_ed_pk] = result; buf.resize(outer_size); if (0 != crypto_box_seal_open( @@ -271,7 +312,7 @@ std::pair decrypt_incoming( throw std::runtime_error{"Decryption failed"}; uc64 sig; - auto sender_ed_pk = buf.substr(msg_size, 32); + sender_ed_pk = buf.substr(msg_size, 32); std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify buf += ustring_view{x25519_pubkey.data(), 32}; @@ -280,24 +321,13 @@ std::pair decrypt_incoming( sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) throw std::runtime_error{"Signature verification failed"}; - // Convert the sender_ed_pk to the sender's session ID - std::array sender_x_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519( - sender_x_pk.data(), sender_ed_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 + // Everything is good, so just drop A and Y off the message buf.resize(buf.size() - 32 - 32); - sender_session_id.reserve(66); - sender_session_id += "05"; - oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); return result; } -std::pair decrypt_from_blinded_recipient( +std::pair decrypt_from_blinded_recipient( ustring_view ed25519_privkey, ustring_view server_pk, ustring_view sender_id, @@ -321,8 +351,8 @@ std::pair decrypt_from_blinded_recipient( if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: too short to contain valid encrypted data"}; - std::pair result; - auto& [sender_session_id, buf] = result; + std::pair result; + auto& [buf, sender_session_id] = result; // Determine whether it's an incoming or outgoing message ustring_view kA = {sender_id.data() + 1, 32}; @@ -547,11 +577,11 @@ LIBSESSION_C_API bool session_decrypt_incoming( size_t* plaintext_len ) { try { - auto result = session::decrypt_incoming( + auto result = session::decrypt_incoming_session_id( ustring_view{ed25519_privkey, 64}, ustring_view{ciphertext_in, ciphertext_len} ); - auto [session_id, plaintext] = result; + auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); *plaintext_out = static_cast(malloc(plaintext.size())); @@ -573,12 +603,12 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( size_t* plaintext_len ) { try { - auto result = session::decrypt_incoming( + auto result = session::decrypt_incoming_session_id( ustring_view{x25519_pubkey, 32}, ustring_view{x25519_seckey, 32}, ustring_view{ciphertext_in, ciphertext_len} ); - auto [session_id, plaintext] = result; + auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); *plaintext_out = static_cast(malloc(plaintext.size())); @@ -609,7 +639,7 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( ustring_view{recipient_id, 33}, ustring_view{ciphertext_in, ciphertext_len} ); - auto [session_id, plaintext] = result; + auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); *plaintext_out = static_cast(malloc(plaintext.size())); diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 652ee7fd..a311e258 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -176,7 +176,7 @@ LIBSESSION_C_API bool session_xed25519_verify( const unsigned char* pubkey, const unsigned char* msg, size_t msg_len) { - return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}) ? 0 : 1; + return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}); } LIBSESSION_C_API bool session_xed25519_pubkey( diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index a168c6e6..b0419ef6 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "utils.hpp" @@ -67,7 +68,7 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_recipient({to_sv(ed_sk).data(), 32}, sid_raw2, to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); @@ -137,3 +138,111 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); CHECK(from_unsigned_sv(msg) == "hello"); } + +TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][encrypt]") { + + using namespace session; + + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto server_pk = "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hexbytes; + std::array ed_pk, curve_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); + ustring sid_raw; + oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); + REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + REQUIRE(sid_raw == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); + auto [blind15_pk, blind15_sk] = blind15_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); + std::array blind15_pk_prefixed; + blind15_pk_prefixed[0] = 0x15; + memcpy(blind15_pk_prefixed.data() + 1, blind15_pk.data(), 32); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + std::array ed_pk2, curve_pk2; + std::array ed_sk2; + crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); + REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); + REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + ustring sid_raw2; + oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); + REQUIRE(sid_raw2 == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); + std::array blind15_pk2_prefixed; + blind15_pk2_prefixed[0] = 0x15; + memcpy(blind15_pk2_prefixed.data() + 1, blind15_pk2.data(), 32); + + SECTION("full secret, recipient decrypt") { + auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + blind15_pk, {blind15_pk2_prefixed.data(), 33}, enc)); + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, blind15_pk2, enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + } + SECTION("full secret, sender decrypt") { + auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + blind15_pk, {blind15_pk2_prefixed.data(), 33}, enc)); + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, blind15_pk2, enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + } + SECTION("only seed, recipient decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient({to_sv(ed_sk).data(), 32}, to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + CHECK(oxenc::to_hex(sender) == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + } +} diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 67089302..337ad46f 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -159,10 +159,8 @@ TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { const auto msg = view("hello world"); std::array xed_sig1, xed_sig2; - rc = session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); REQUIRE(rc == 0); @@ -186,13 +184,9 @@ TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { const auto msg = view("hello world"); std::array xed_sig1, xed_sig2; - rc = session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); - rc = session_xed25519_verify(xed_sig1.data(), xpub1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_verify(xed_sig2.data(), xpub2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_verify(xed_sig1.data(), xpub1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_verify(xed_sig2.data(), xpub2.data(), msg.data(), msg.size())); } From 6e3ec74f53c7b4876883915ee42e825fdb50771e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 11 Dec 2023 12:10:48 +1100 Subject: [PATCH 135/572] Fixed the broken tests --- src/onionreq/parser.cpp | 4 ++-- src/session_encrypt.cpp | 2 +- tests/test_session_encrypt.cpp | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index f563be89..76ac1c65 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -36,12 +36,12 @@ OnionReqParser::OnionReqParser( else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - auto plaintext = enc.decrypt(enc_type, reinterpret_cast(ciphertext.data()), remote_pk); + auto plaintext = enc.decrypt(enc_type, {ciphertext.data(), ciphertext.size()}, remote_pk); payload_ = {to_unsigned(plaintext.data()), plaintext.size()}; } ustring OnionReqParser::encrypt_reply(ustring_view reply) const { - return enc.encrypt(enc_type, reinterpret_cast(reply.data()), remote_pk); + return enc.encrypt(enc_type, {reply.data(), reply.size()}, remote_pk); } } // namespace session::onionreq diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index bfb521f6..a1becb53 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -334,9 +334,9 @@ std::pair decrypt_from_blinded_recipient( ustring_view recipient_id, ustring_view ciphertext ) { + cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; - cleared_uc64 ed_sk_from_seed; crypto_sign_ed25519_seed_keypair( ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index b0419ef6..74507fb2 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -235,9 +235,10 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto enc = encrypt_for_blinded_recipient({to_sv(ed_sk).data(), 32}, to_unsigned_sv(server_pk), {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); - auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + + auto [msg, sender] = decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); - CHECK(oxenc::to_hex(sender) == sid); + CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == lorem_ipsum); auto broken = enc; From daca3003f268137d4e602af753528408a0b70d6f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 11 Dec 2023 17:00:35 +1100 Subject: [PATCH 136/572] Started adding unit tests for new crypto functions --- include/session/ed25519.hpp | 2 +- src/ed25519.cpp | 47 +++++++++++++------- tests/CMakeLists.txt | 2 + tests/test_curve25519.cpp | 50 +++++++++++++++++++++ tests/test_ed25519.cpp | 86 +++++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 tests/test_curve25519.cpp create mode 100644 tests/test_ed25519.cpp diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index e7a1e7f7..ddaf3014 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -23,7 +23,7 @@ std::pair, std::array> ed25519_ /// /// Outputs: /// - The ed25519 seed -ustring seed_for_ed_privkey(ustring_view ed25519_privkey); +std::array seed_for_ed_privkey(ustring_view ed25519_privkey); /// API: ed25519/sign /// diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 0a319994..017b9ba7 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -4,9 +4,16 @@ #include #include "session/export.h" +#include "session/util.hpp" namespace session::ed25519 { +template +using cleared_array = sodium_cleared>; + +using uc32 = std::array; +using cleared_uc64 = cleared_array<64>; + std::pair, std::array> ed25519_key_pair() { std::array ed_pk; std::array ed_sk; @@ -31,25 +38,30 @@ std::pair, std::array> ed25519_ return {ed_pk, ed_sk}; } -ustring seed_for_ed_privkey( - ustring_view ed25519_privkey) { - ustring result; - result.reserve(32); +std::array seed_for_ed_privkey(ustring_view ed25519_privkey) { + std::array seed; - if (ed25519_privkey.size() == 32) { - // Assume we we actually given the seed so just return it - result += ed25519_privkey.data(); - return result; - } else if (ed25519_privkey.size() == 64) { - // The first 32 bytes of a 64 byte ed25519 private key are the seed - result += ed25519_privkey.substr(0, 32); - return result; - } - - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (ed25519_privkey.size() == 32 || ed25519_privkey.size() == 64) + // The first 32 bytes of a 64 byte ed25519 private key are the seed, otherwise + // if the provided value is 32 bytes we just assume we were given a seed + std::memcpy(seed.data(), ed25519_privkey.data(), 32); + else + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + + return seed; } ustring sign(ustring_view ed25519_privkey, ustring_view msg) { + cleared_uc64 ed_sk_from_seed; + if (ed25519_privkey.size() == 32) { + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + } + std::array sig; if (0 != crypto_sign_ed25519_detached( sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) @@ -59,6 +71,11 @@ ustring sign(ustring_view ed25519_privkey, ustring_view msg) { } bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { + if (sig.size() != 64) + throw std::invalid_argument{"Invalid sig: expected 64 bytes"}; + if (pubkey.size() != 32) + throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; + return (0 == crypto_sign_ed25519_verify_detached( sig.data(), msg.data(), msg.size(), pubkey.data())); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a994202..34974e1c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,8 @@ add_executable(testAll test_configdata.cpp test_config_contacts.cpp test_config_convo_info_volatile.cpp + test_curve25519.cpp + test_ed25519.cpp test_encrypt.cpp test_group_keys.cpp test_group_info.cpp diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp new file mode 100644 index 00000000..b4fb6cf1 --- /dev/null +++ b/tests/test_curve25519.cpp @@ -0,0 +1,50 @@ +#include +#include + +#include + +#include "session/curve25519.h" +#include "session/curve25519.hpp" + +#include "utils.hpp" + +TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { + auto kp1 = session::curve25519::curve25519_key_pair(); + auto kp2 = session::curve25519::curve25519_key_pair(); + + REQUIRE(kp1.first.size() == 32); + REQUIRE(kp1.second.size() == 64); + REQUIRE(kp1.first != kp2.first); + REQUIRE(kp1.second != kp2.second); +} + +TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { + using namespace session; + + auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + + auto x_pk1 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk1)); + auto x_pk2 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk2)); + + REQUIRE(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + REQUIRE(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); +} + +TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { + using namespace session; + + auto ed_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; + auto x_sk1 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk1)); + auto x_sk2 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk2)); + + REQUIRE(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == + "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); + REQUIRE(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == + "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); +} diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp new file mode 100644 index 00000000..550152bd --- /dev/null +++ b/tests/test_ed25519.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include + +#include "session/ed25519.h" +#include "session/ed25519.hpp" + +#include "utils.hpp" + +TEST_CASE("Ed25519 key pair generation", "[ed25519][keypair]") { + // Generate two random key pairs and make sure they don't match + auto kp1 = session::ed25519::ed25519_key_pair(); + auto kp2 = session::ed25519::ed25519_key_pair(); + + REQUIRE(kp1.first.size() == 32); + REQUIRE(kp1.second.size() == 64); + REQUIRE(kp1.first != kp2.first); + REQUIRE(kp1.second != kp2.second); +} + +TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { + using namespace session; + + auto ed_seed1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_seed2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_seed_invalid = "010203040506070809"_hexbytes; + + auto kp1 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed1)); + auto kp2 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed2)); + CHECK_THROWS(session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed_invalid))); + + REQUIRE(kp1.first.size() == 32); + REQUIRE(kp1.second.size() == 64); + REQUIRE(kp1.first != kp2.first); + REQUIRE(kp1.second != kp2.second); + REQUIRE(oxenc::to_hex(kp1.first.begin(), kp1.first.end()) == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + REQUIRE(oxenc::to_hex(kp2.first.begin(), kp2.first.end()) == + "cd83ca3d13ad8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"); + + auto kp_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"; + auto kp_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"; + REQUIRE(oxenc::to_hex(kp1.second.begin(), kp1.second.end()) == kp_sk1); + REQUIRE(oxenc::to_hex(kp2.second.begin(), kp2.second.end()) == kp_sk2); +} + +TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { + using namespace session; + + auto ed_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_sk_invalid = "010203040506070809"_hexbytes; + + auto seed1 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk1)); + auto seed2 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk2)); + CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk_invalid))); + + REQUIRE(oxenc::to_hex(seed1.begin(), seed1.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(seed2.begin(), seed2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); +} + +TEST_CASE("Ed25519", "[ed25519][signature]") { + using namespace session; + + auto ed_seed = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk = "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_invalid = "010203040506070809"_hexbytes; + + auto sig1 = session::ed25519::sign(to_unsigned_sv(ed_seed), to_unsigned_sv("hello")); + CHECK_THROWS(session::ed25519::sign(to_unsigned_sv(ed_invalid), to_unsigned_sv("hello"))); + + auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" + "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; + REQUIRE(oxenc::to_hex(sig1.begin(), sig1.end()) == + expected_sig_hex); + + REQUIRE(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_unsigned_sv("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_unsigned_sv("hello"))); +} From 5c64ae8750f206e1f3425937de15c8eafe40448a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Dec 2023 17:03:46 +1100 Subject: [PATCH 137/572] Added more unit tests --- include/session/blinding.h | 12 +-- include/session/blinding.hpp | 11 ++- src/blinding.cpp | 98 +++++++++++++------- src/session_encrypt.cpp | 2 + tests/CMakeLists.txt | 2 + tests/test_blinding.cpp | 164 +++++++++++++++++++++++++++++++++ tests/test_curve25519.cpp | 16 ++-- tests/test_ed25519.cpp | 32 +++---- tests/test_hash.cpp | 42 +++++++++ tests/test_random.cpp | 17 ++++ tests/test_session_encrypt.cpp | 108 +++++++++++++++++++++- 11 files changed, 433 insertions(+), 71 deletions(-) create mode 100644 tests/test_hash.cpp create mode 100644 tests/test_random.cpp diff --git a/include/session/blinding.h b/include/session/blinding.h index 3e56f7ae..1c7bb4ca 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -99,16 +99,16 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// This function attempts to generate a signature for a message using a blind25 private key. /// /// Inputs: -/// - `session_id` -- [in] the session_id to compare (either 32 bytes, or 33 bytes with a 0x05 prefix). -/// - `blinded_id` -- [in] the blinded_id to compare, can be either 0x15 or 0x25 blinded (33 bytes). -/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (32 bytes). +/// - `session_id` -- [in] the session_id to compare (66 bytes with a 05 prefix). +/// - `blinded_id` -- [in] the blinded_id to compare, can be either 15 or 25 blinded (66 bytes). +/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (64 bytes). /// /// Outputs: /// - `bool` -- True if the session_id matches the blinded_id, false if not. LIBSESSION_EXPORT bool session_id_matches_blinded_id( - const unsigned char* session_id, /* 32 or 33 bytes */ - const unsigned char* blinded_id, /* 33 bytes */ - const unsigned char* server_pk /* 32 bytes */); + const char* session_id, /* 66 bytes */ + const char* blinded_id, /* 66 bytes */ + const char* server_pk /* 64 bytes */); #ifdef __cplusplus } diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 688657a1..efcf2810 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -64,6 +64,15 @@ std::array blind15_factor(ustring_view server_pk); /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). std::array blind25_factor(ustring_view session_id, ustring_view server_pk); +/// Computes the 15-blinded id from a session id and server pubkey. Values accepted and +/// returned are hex-encoded. +ustring blind15_id(ustring_view session_id, ustring_view server_pk); + +/// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a +/// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may +/// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). +std::string blind15_id(std::string_view session_id, std::string_view server_pk); + /// Computes the 25-blinded id from a session id and server pubkey. Values accepted and /// returned are hex-encoded. std::string blind25_id(std::string_view session_id, std::string_view server_pk); @@ -114,7 +123,7 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin /// Takes in a standard session_id and returns a flag indicating whether it matches the given /// blinded_id for a given server_pk. /// -/// Takes either a 0x15 or 0x25 blinded_id (33 bytes) and the server pubkey (32 bytes). +/// Takes either a 15 or 25 blinded_id (66 bytes) and the server pubkey (64 bytes). /// /// Returns a flag indicating whether the session_id matches the blinded_id. bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, diff --git a/src/blinding.cpp b/src/blinding.cpp index 54b20152..bd9d13e3 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -56,6 +56,16 @@ std::array blind25_factor(ustring_view session_id, ustring_vi namespace { + void blind15_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { + auto k = blind15_factor(server_pk); + if (session_id.size() == 33) + session_id.remove_prefix(1); + auto ed_pk = xed25519::pubkey(session_id); + if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + out[0] = 0x15; + } + void blind25_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { auto k = blind25_factor(session_id, server_pk); if (session_id.size() == 33) @@ -81,17 +91,28 @@ ustring blind15_id(ustring_view session_id, ustring_view server_pk) { ustring result; result.resize(33); - result[0] = 0x15; - - auto k = blind15_factor(server_pk); - auto ed_pk = xed25519::pubkey(session_id); - - if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pk.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - + blind15_id_impl(session_id, server_pk, result.data()); return result; } +std::string blind15_id(std::string_view session_id, std::string_view server_pk) { + if (session_id.size() != 66 || !oxenc::is_hex(session_id)) + throw std::invalid_argument{"blind15_id: session_id must be hex (66 digits)"}; + if (session_id[0] != '0' || session_id[1] != '5') + throw std::invalid_argument{"blind15_id: session_id must start with 05"}; + if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) + throw std::invalid_argument{"blind15_id: server_pk must be hex (64 digits)"}; + + uc33 raw_sid; + oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); + uc32 raw_server_pk; + oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); + + uc33 blinded; + blind15_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); + return oxenc::to_hex(blinded.begin(), blinded.end()); +} + ustring blind25_id(ustring_view session_id, ustring_view server_pk) { if (session_id.size() == 33) { if (session_id[0] != 0x05) @@ -367,30 +388,37 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, std::string_view server_pk) { - ustring converted_blind_id1; - converted_blind_id1.resize(33); - - switch (blinded_id[0]) { - case 0x15: - converted_blind_id1 = blind15_id(to_unsigned_sv(session_id), to_unsigned_sv(server_pk)); - break; + if (session_id.size() != 66 || !oxenc::is_hex(session_id)) + throw std::invalid_argument{"session_id_matches_blinded_id: session_id must be hex (66 digits)"}; + if (session_id[0] != '0' || session_id[1] != '5') + throw std::invalid_argument{"session_id_matches_blinded_id: session_id must start with 05"}; + if (blinded_id[1] != '5' && (blinded_id[0] != '1' || blinded_id[0] != '2')) + throw std::invalid_argument{"session_id_matches_blinded_id: blinded_id must start with 15 or 25"}; + if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) + throw std::invalid_argument{"session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; - case 0x25: - converted_blind_id1 = blind25_id(to_unsigned_sv(session_id), to_unsigned_sv(server_pk)); - break; + std::string converted_blind_id1, converted_blind_id2; + ustring converted_blind_id1_raw; - default: throw std::invalid_argument{"Invalid blinded_id: must start with 0x15 or 0x25"}; + switch (blinded_id[0]) { + case '1': + converted_blind_id1 = blind15_id(session_id, server_pk); + + // For the negative, what we're going to get out of the above is simply the negative of + // converted_blind_id1, so flip the sign bit to get converted_blind_id2 + oxenc::from_hex(converted_blind_id1.begin(), converted_blind_id1.end(), std::back_inserter(converted_blind_id1_raw)); + converted_blind_id1_raw[32] ^= 0x80; + converted_blind_id2 = oxenc::to_hex(converted_blind_id1_raw); + + return ( + blinded_id == converted_blind_id1 || + blinded_id == converted_blind_id2 + ); + + // blind25 doesn't run into the negative issue that blind15 did + case '2': return blinded_id == blind25_id(session_id, server_pk); + default: throw std::invalid_argument{"Invalid blinded_id: must start with 15 or 25"}; } - - // For the negative, what we're going to get out of the above is simply the negative of - // converted_blind_id1, so flip the sign bit to get converted_blind_id2 - auto converted_blind_id2 = converted_blind_id1; - converted_blind_id2[32] ^= 0x80; - - return ( - to_unsigned_sv(blinded_id) == converted_blind_id1 || - to_unsigned_sv(blinded_id) == converted_blind_id2 - ); } } // namespace session @@ -474,15 +502,15 @@ LIBSESSION_C_API bool session_blind25_sign( } LIBSESSION_C_API bool session_id_matches_blinded_id( - const unsigned char* session_id, - const unsigned char* blinded_id, - const unsigned char* server_pk + const char* session_id, + const char* blinded_id, + const char* server_pk ) { try { return session::session_id_matches_blinded_id( - {from_unsigned(session_id), 33}, - {from_unsigned(blinded_id), 33}, - {from_unsigned(server_pk), 32} + {session_id, 66}, + {blinded_id, 66}, + {server_pk, 64} ); } catch (...) { return false; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index a1becb53..088c7999 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -459,6 +459,8 @@ std::pair decrypt_from_blinded_recipient( std::string decrypt_ons_response(std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) + throw std::invalid_argument{"Invalid nonce: expected to be 24 bytes"}; // Hash the ONS name using BLAKE2b // diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 34974e1c..a6a1d127 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,9 +16,11 @@ add_executable(testAll test_group_keys.cpp test_group_info.cpp test_group_members.cpp + test_hash.cpp test_multi_encrypt.cpp test_onionreq.cpp test_proto.cpp + test_random.cpp test_session_encrypt.cpp test_xed25519.cpp ) diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index b8d8bd51..132b9aa7 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -162,3 +162,167 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 4, to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); } + +TEST_CASE("Communities 15xxx-blinded pubkey derivation", "[blinding15][pubkey]") { + REQUIRE(sodium_init() >= 0); + + ustring session_id1_raw, session_id2_raw; + oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); + oxenc::from_hex(session_id2.begin(), session_id2.end(), std::back_inserter(session_id2_raw)); + CHECK(oxenc::to_hex(blind15_id( + session_id1_raw, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); + CHECK(oxenc::to_hex(blind15_id( + session_id2_raw, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "1561e070286ff7a71f167e92b18c709882b148d8238c8872caf414b301ba0564fd"); + CHECK(oxenc::to_hex(blind15_id( + session_id1_raw.substr(1), + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); +} + +TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { + REQUIRE(sodium_init() >= 0); + + std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; + auto b15_1 = blind15_id(session_id1, server_pks[0]); + auto b15_2 = blind15_id(session_id1, server_pks[1]); + auto b15_3 = blind15_id(session_id2, server_pks[2]); + auto b15_4 = blind15_id(session_id2, server_pks[3]); + auto b15_5 = blind15_id(session_id2, server_pks[4]); + auto b15_6 = blind15_id(session_id1, server_pks[5]); + + // The `seed2` results in a negative Ed25519 pubkey which would result in an invalid signature + // since the sessionId always used the positive curve, as a result we need to flip the sign bit + // on the blinded id to ensure everything works nicely + ustring b15_3_raw, b15_4_raw, b15_5_raw; + oxenc::from_hex(b15_3.begin(), b15_3.end(), std::back_inserter(b15_3_raw)); + oxenc::from_hex(b15_4.begin(), b15_4.end(), std::back_inserter(b15_4_raw)); + oxenc::from_hex(b15_5.begin(), b15_5.end(), std::back_inserter(b15_5_raw)); + b15_3_raw[32] ^= 0x80; + b15_4_raw[32] ^= 0x80; + b15_5_raw[32] ^= 0x80; + b15_3 = oxenc::to_hex(b15_3_raw); + b15_4 = oxenc::to_hex(b15_4_raw); + b15_5 = oxenc::to_hex(b15_5_raw); + + auto sig1 = blind15_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); + CHECK(oxenc::to_hex(sig1) == + "1a5ade20b43af0e16b3e591d6f86303938d7557c0ac54469dd4f5aea759f82d22cafa42587251756e133acdd" + "dd8cbec2f707a9ce09a49f2193f46a91502c5006"); + CHECK(0 == crypto_sign_verify_detached( + sig1.data(), + to_unsigned("hello"), + 5, + to_unsigned(oxenc::from_hex(b15_1).data()) + 1)); + + auto sig2 = blind15_sign(to_usv(seed1), server_pks[1], to_unsigned_sv("world")); + CHECK(oxenc::to_hex(sig2) == + "d357f74c5ec5536840aec575051f71fdb22d70f35ef31db1715f5f694842de3b39aa647c84aa8e28ec56eb76" + "2d237c9e030639c83f429826d419ac719cd4df03"); + CHECK(0 == crypto_sign_verify_detached( + sig2.data(), + to_unsigned("world"), + 5, + to_unsigned(oxenc::from_hex(b15_2).data()) + 1)); + + auto sig3 = blind15_sign(to_usv(seed2), server_pks[2], to_unsigned_sv("this")); + CHECK(oxenc::to_hex(sig3) == + "dacf91dfb411e99cd8ef4cb07b195b49289cf1a724fef122c73462818560bc29832a98d870ec4feb79dedca5" + "b59aba6a466d3ce8f3e35adf25a1813f6989fd0a"); + CHECK(0 == crypto_sign_verify_detached( + sig3.data(), + to_unsigned("this"), + 4, + to_unsigned(oxenc::from_hex(b15_3).data()) + 1)); + + auto sig4 = blind15_sign(to_usv(seed2), server_pks[3], to_unsigned_sv("is")); + CHECK(oxenc::to_hex(sig4) == + "8339ea9887d3e44131e33403df160539cdc7a0a8107772172c311e95773660a0d39ed0a6c2b2c794dde1fdc6" + "40943e403497aa02c4d1a21a7d9030742beabb05"); + CHECK(0 == crypto_sign_verify_detached( + sig4.data(), + to_unsigned("is"), + 2, + to_unsigned(oxenc::from_hex(b15_4).data()) + 1)); + + auto sig5 = blind15_sign(to_usv(seed2), server_pks[4], to_unsigned_sv("")); + CHECK(oxenc::to_hex(sig5) == + "8b0d6447decff3a21ec1809141580139c4a51e24977b0605fe7984439993f5377ebc9681e4962593108d03cc" + "8b6873c5c5ba8c30287188137d2dee9ab10afd0f"); + CHECK(0 == + crypto_sign_verify_detached( + sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b15_5).data()) + 1)); + + auto sig6 = blind15_sign(to_usv(seed1), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6) == + "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" + "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); + CHECK(0 == crypto_sign_verify_detached( + sig6.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); + + // Test that it works when given just the seed instead of the whole sk: + auto sig6b = blind15_sign(to_usv(seed1).substr(0, 32), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6b) == + "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" + "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); + CHECK(0 == crypto_sign_verify_detached( + sig6b.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); +} + + +TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { + std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; + auto b15_1 = blind15_id(session_id1, server_pks[0]); + auto b15_2 = blind15_id(session_id1, server_pks[1]); + auto b15_3 = blind15_id(session_id2, server_pks[2]); + auto b15_4 = blind15_id(session_id2, server_pks[3]); + auto b15_5 = blind15_id(session_id2, server_pks[4]); + auto b15_6 = blind15_id(session_id1, server_pks[5]); + auto b25_1 = blind25_id(session_id1, server_pks[0]); + auto b25_2 = blind25_id(session_id1, server_pks[1]); + auto b25_3 = blind25_id(session_id2, server_pks[2]); + auto b25_4 = blind25_id(session_id2, server_pks[3]); + auto b25_5 = blind25_id(session_id2, server_pks[4]); + auto b25_6 = blind25_id(session_id1, server_pks[5]); + + CHECK(session_id_matches_blinded_id(session_id1, b15_1, server_pks[0])); + CHECK(session_id_matches_blinded_id(session_id1, b15_2, server_pks[1])); + CHECK(session_id_matches_blinded_id(session_id2, b15_3, server_pks[2])); + CHECK(session_id_matches_blinded_id(session_id2, b15_4, server_pks[3])); + CHECK(session_id_matches_blinded_id(session_id2, b15_5, server_pks[4])); + CHECK(session_id_matches_blinded_id(session_id1, b15_6, server_pks[5])); + CHECK(session_id_matches_blinded_id(session_id1, b25_1, server_pks[0])); + CHECK(session_id_matches_blinded_id(session_id1, b25_2, server_pks[1])); + CHECK(session_id_matches_blinded_id(session_id2, b25_3, server_pks[2])); + CHECK(session_id_matches_blinded_id(session_id2, b25_4, server_pks[3])); + CHECK(session_id_matches_blinded_id(session_id2, b25_5, server_pks[4])); + CHECK(session_id_matches_blinded_id(session_id1, b25_6, server_pks[5])); + + auto invalid_session_id = "9" + session_id1.substr(1, 65); + auto invalid_blinded_id = "9" + b15_1.substr(1, 65); + auto invalid_server_pk = server_pks[0].substr(0, 60); + CHECK_THROWS(session_id_matches_blinded_id(invalid_session_id, b15_1, server_pks[0])); + CHECK_THROWS(session_id_matches_blinded_id(session_id1, invalid_blinded_id, server_pks[0])); + CHECK_THROWS(session_id_matches_blinded_id(session_id1, invalid_blinded_id, invalid_server_pk)); +} diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp index b4fb6cf1..cf00256c 100644 --- a/tests/test_curve25519.cpp +++ b/tests/test_curve25519.cpp @@ -12,10 +12,10 @@ TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { auto kp1 = session::curve25519::curve25519_key_pair(); auto kp2 = session::curve25519::curve25519_key_pair(); - REQUIRE(kp1.first.size() == 32); - REQUIRE(kp1.second.size() == 64); - REQUIRE(kp1.first != kp2.first); - REQUIRE(kp1.second != kp2.second); + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); } TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { @@ -27,9 +27,9 @@ TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { auto x_pk1 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk1)); auto x_pk2 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk2)); - REQUIRE(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == + CHECK(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - REQUIRE(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == + CHECK(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); } @@ -43,8 +43,8 @@ TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { auto x_sk1 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk1)); auto x_sk2 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk2)); - REQUIRE(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == + CHECK(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); - REQUIRE(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == + CHECK(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); } diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index 550152bd..c084ce38 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -13,10 +13,10 @@ TEST_CASE("Ed25519 key pair generation", "[ed25519][keypair]") { auto kp1 = session::ed25519::ed25519_key_pair(); auto kp2 = session::ed25519::ed25519_key_pair(); - REQUIRE(kp1.first.size() == 32); - REQUIRE(kp1.second.size() == 64); - REQUIRE(kp1.first != kp2.first); - REQUIRE(kp1.second != kp2.second); + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); } TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { @@ -30,21 +30,21 @@ TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { auto kp2 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed2)); CHECK_THROWS(session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed_invalid))); - REQUIRE(kp1.first.size() == 32); - REQUIRE(kp1.second.size() == 64); - REQUIRE(kp1.first != kp2.first); - REQUIRE(kp1.second != kp2.second); - REQUIRE(oxenc::to_hex(kp1.first.begin(), kp1.first.end()) == + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); + CHECK(oxenc::to_hex(kp1.first.begin(), kp1.first.end()) == "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); - REQUIRE(oxenc::to_hex(kp2.first.begin(), kp2.first.end()) == + CHECK(oxenc::to_hex(kp2.first.begin(), kp2.first.end()) == "cd83ca3d13ad8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"); auto kp_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"; auto kp_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"; - REQUIRE(oxenc::to_hex(kp1.second.begin(), kp1.second.end()) == kp_sk1); - REQUIRE(oxenc::to_hex(kp2.second.begin(), kp2.second.end()) == kp_sk2); + CHECK(oxenc::to_hex(kp1.second.begin(), kp1.second.end()) == kp_sk1); + CHECK(oxenc::to_hex(kp2.second.begin(), kp2.second.end()) == kp_sk2); } TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { @@ -59,9 +59,9 @@ TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { auto seed2 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk2)); CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk_invalid))); - REQUIRE(oxenc::to_hex(seed1.begin(), seed1.end()) == + CHECK(oxenc::to_hex(seed1.begin(), seed1.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); - REQUIRE(oxenc::to_hex(seed2.begin(), seed2.end()) == + CHECK(oxenc::to_hex(seed2.begin(), seed2.end()) == "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); } @@ -77,10 +77,10 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; - REQUIRE(oxenc::to_hex(sig1.begin(), sig1.end()) == + CHECK(oxenc::to_hex(sig1.begin(), sig1.end()) == expected_sig_hex); - REQUIRE(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); + CHECK(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_unsigned_sv("hello"))); CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_unsigned_sv("hello"))); } diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp new file mode 100644 index 00000000..861b8a5e --- /dev/null +++ b/tests/test_hash.cpp @@ -0,0 +1,42 @@ +#include + +#include "session/hash.h" +#include "session/hash.hpp" + +#include "utils.hpp" + +TEST_CASE("Hash generation", "[hash][hash]") { + auto hash1 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); + auto hash2 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); + auto hash3 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); + auto hash4 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); + auto hash5 = session::hash::hash(64, to_usv("TestMessage"), std::nullopt); + auto hash6 = session::hash::hash(64, to_usv("TestMessage"), to_usv("TestKey")); + CHECK_THROWS(session::hash::hash(10, to_usv("TestMessage"), std::nullopt)); + CHECK_THROWS(session::hash::hash(100, to_usv("TestMessage"), std::nullopt)); + CHECK_THROWS(session::hash::hash(32, to_usv("TestMessage"), + to_usv("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLong"))); + + CHECK(hash1.size() == 32); + CHECK(hash2.size() == 32); + CHECK(hash3.size() == 32); + CHECK(hash4.size() == 32); + CHECK(hash5.size() == 64); + CHECK(hash6.size() == 64); + CHECK(hash1 == hash2); + CHECK(hash1 != hash3); + CHECK(hash3 == hash4); + CHECK(hash1 != hash5); + CHECK(hash3 != hash6); + CHECK(oxenc::to_hex(hash1) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash2) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash3) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(oxenc::to_hex(hash4) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + + auto expected_hash5 = "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a39c02db69c44" + "16d5c45acc2e9469b7f274992b2858f3bb2746becb48c8b56ce4b"; + auto expected_hash6 = "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e877874c9059edf5" + "3d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"; + CHECK(oxenc::to_hex(hash5) == expected_hash5); + CHECK(oxenc::to_hex(hash6) == expected_hash6); +} diff --git a/tests/test_random.cpp b/tests/test_random.cpp new file mode 100644 index 00000000..2e509d4e --- /dev/null +++ b/tests/test_random.cpp @@ -0,0 +1,17 @@ +#include + +#include "session/random.h" +#include "session/random.hpp" + +#include "utils.hpp" + +TEST_CASE("Random generation", "[random][random]") { + auto rand1 = session::random::random(10); + auto rand2 = session::random::random(10); + auto rand3 = session::random::random(20); + + CHECK(rand1.size() == 10); + CHECK(rand2.size() == 10); + CHECK(rand3.size() == 20); + CHECK(rand1 != rand2); +} diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 74507fb2..3b599898 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -160,9 +160,12 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e REQUIRE(sid_raw == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); auto [blind15_pk, blind15_sk] = blind15_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); - std::array blind15_pk_prefixed; + auto [blind25_pk, blind25_sk] = blind25_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); + std::array blind15_pk_prefixed, blind25_pk_prefixed; blind15_pk_prefixed[0] = 0x15; + blind25_pk_prefixed[0] = 0x25; memcpy(blind15_pk_prefixed.data() + 1, blind15_pk.data(), 32); + memcpy(blind25_pk_prefixed.data() + 1, blind25_pk.data(), 32); const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; std::array ed_pk2, curve_pk2; @@ -180,11 +183,14 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e REQUIRE(sid_raw2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); - std::array blind15_pk2_prefixed; + auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); + std::array blind15_pk2_prefixed, blind25_pk2_prefixed; blind15_pk2_prefixed[0] = 0x15; + blind25_pk2_prefixed[0] = 0x25; memcpy(blind15_pk2_prefixed.data() + 1, blind15_pk2.data(), 32); + memcpy(blind25_pk2_prefixed.data() + 1, blind25_pk2.data(), 32); - SECTION("full secret, recipient decrypt") { + SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); @@ -204,7 +210,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); } - SECTION("full secret, sender decrypt") { + SECTION("blind15, full secret, sender decrypt") { auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); @@ -224,7 +230,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); } - SECTION("only seed, recipient decrypt") { + SECTION("blind15, only seed, recipient decrypt") { constexpr auto lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " @@ -246,4 +252,96 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK_THROWS(decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); } + SECTION("blind25, full secret, recipient decrypt") { + auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + blind25_pk, {blind25_pk2_prefixed.data(), 33}, enc)); + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, blind25_pk2, enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + } + SECTION("blind25, full secret, sender decrypt") { + auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + blind25_pk, {blind25_pk2_prefixed.data(), 33}, enc)); + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, blind25_pk2, enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + } + SECTION("blind25, only seed, recipient decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient({to_sv(ed_sk).data(), 32}, to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + auto [msg, sender] = decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + } +} + +TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { + using namespace session; + + std::string_view name = "test"; + auto ciphertext = "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" + "1580d9a8c9b8a64cacfec97"_hexbytes; + auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + ustring sid_data = "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; + + CHECK(decrypt_ons_response(name, ciphertext, nonce) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK_THROWS(decrypt_ons_response(name, to_unsigned_sv("invalid"), nonce)); + CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); +} + +TEST_CASE("Session push notification decryption", "[session-notification][decrypt]") { + using namespace session; + + auto payload = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto payload_padded = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + + CHECK(decrypt_push_notification(payload, enc_key) == to_unsigned("TestMessage")); + CHECK(decrypt_push_notification(payload_padded, enc_key) == to_unsigned("TestMessage")); + CHECK_THROWS(decrypt_push_notification(to_unsigned_sv("invalid"), enc_key)); + CHECK_THROWS(decrypt_push_notification(payload, to_unsigned_sv("invalid"))); } From cf4f83352316535f1c73e97745f9ee55822a6886 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 13 Dec 2023 09:11:16 +1100 Subject: [PATCH 138/572] Added some missing includes and fixed a build error --- include/session/curve25519.hpp | 2 ++ include/session/ed25519.hpp | 2 ++ include/session/hash.hpp | 2 ++ src/session_encrypt.cpp | 1 - 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index 07876ef7..fce70932 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "types.hpp" namespace session::curve25519 { diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index ddaf3014..ddd61097 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "types.hpp" namespace session::ed25519 { diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 344f88c8..231a65aa 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "types.hpp" namespace session::hash { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 088c7999..cad42549 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -444,7 +444,6 @@ std::pair decrypt_from_blinded_recipient( throw std::runtime_error{"Shared secret generation for verification failed"}; if (kA != ustring_view{extracted_kA.data(), 32}) throw std::runtime_error{"Shared secret does not match encoded public key"}; - }; // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix to // the sender session ID From efe8cc985290470e157b6e950e57fda21a287c55 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 13 Dec 2023 09:49:00 +1100 Subject: [PATCH 139/572] More import fixes --- src/curve25519.cpp | 5 ++++- src/ed25519.cpp | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 89167e49..cc76c168 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,9 +1,12 @@ -#include "session/ed25519.hpp" +#include "session/curve25519.hpp" #include #include +#include + #include "session/export.h" +#include "session/util.hpp" namespace session::curve25519 { diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 017b9ba7..bc8ce23f 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "session/export.h" #include "session/util.hpp" From 2e2a79ad188d22a57b77f4f9a7e64e55c7c8edb4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 13 Dec 2023 11:55:56 +1100 Subject: [PATCH 140/572] Formatting --- include/session/blinding.h | 45 +-- include/session/blinding.hpp | 4 +- include/session/curve25519.h | 24 +- include/session/curve25519.hpp | 8 +- include/session/ed25519.h | 48 +-- include/session/ed25519.hpp | 2 +- include/session/onionreq/builder.h | 52 ++- include/session/onionreq/builder.hpp | 12 +- include/session/onionreq/response_parser.h | 26 +- include/session/onionreq/response_parser.hpp | 12 +- include/session/session_encrypt.h | 33 +- include/session/session_encrypt.hpp | 26 +- src/blinding.cpp | 97 +++--- src/curve25519.cpp | 20 +- src/ed25519.cpp | 54 ++- src/hash.cpp | 24 +- src/onionreq/builder.cpp | 327 +++++++++---------- src/onionreq/hop_encryption.cpp | 22 +- src/onionreq/parser.cpp | 2 +- src/onionreq/response_parser.cpp | 53 ++- src/session_encrypt.cpp | 260 ++++++++------- tests/test_blinding.cpp | 3 +- tests/test_curve25519.cpp | 25 +- tests/test_ed25519.cpp | 42 +-- tests/test_hash.cpp | 32 +- tests/test_random.cpp | 3 +- tests/test_session_encrypt.cpp | 254 +++++++++----- 27 files changed, 805 insertions(+), 705 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 1c7bb4ca..715ec8d9 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -24,10 +24,10 @@ extern "C" { /// Outputs: /// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_key_pair( - const unsigned char* ed25519_seckey, /* 64 bytes */ - const unsigned char* server_pk, /* 32 bytes */ - unsigned char* blinded_pk_out, /* 32 byte output buffer */ - unsigned char* blinded_sk_out /* 32 byte output buffer */); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); /// API: crypto/session_blind25_key_pair /// @@ -45,10 +45,10 @@ LIBSESSION_EXPORT bool session_blind15_key_pair( /// Outputs: /// - `bool` -- True if the key was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind25_key_pair( - const unsigned char* ed25519_seckey, /* 64 bytes */ - const unsigned char* server_pk, /* 32 bytes */ - unsigned char* blinded_pk_out, /* 32 byte output buffer */ - unsigned char* blinded_sk_out /* 32 byte output buffer */); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); /// API: crypto/session_blind15_sign /// @@ -66,11 +66,11 @@ LIBSESSION_EXPORT bool session_blind25_key_pair( /// Outputs: /// - `bool` -- True if the signature was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind15_sign( - const unsigned char* ed25519_seckey, /* 64 bytes */ - const unsigned char* server_pk, /* 32 bytes */ - const unsigned char* msg, - size_t msg_len, - unsigned char* blinded_sig_out /* 64 byte output buffer */); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out /* 64 byte output buffer */); /// API: crypto/session_blind25_sign /// @@ -88,11 +88,11 @@ LIBSESSION_EXPORT bool session_blind15_sign( /// Outputs: /// - `bool` -- True if the signature was successfully generated, false if generation failed. LIBSESSION_EXPORT bool session_blind25_sign( - const unsigned char* ed25519_seckey, /* 64 bytes */ - const unsigned char* server_pk, /* 32 bytes */ - const unsigned char* msg, - size_t msg_len, - unsigned char* blinded_sig_out /* 64 byte output buffer */); + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out /* 64 byte output buffer */); /// API: crypto/session_blind25_sign /// @@ -101,14 +101,15 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// Inputs: /// - `session_id` -- [in] the session_id to compare (66 bytes with a 05 prefix). /// - `blinded_id` -- [in] the blinded_id to compare, can be either 15 or 25 blinded (66 bytes). -/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (64 +/// bytes). /// /// Outputs: /// - `bool` -- True if the session_id matches the blinded_id, false if not. LIBSESSION_EXPORT bool session_id_matches_blinded_id( - const char* session_id, /* 66 bytes */ - const char* blinded_id, /* 66 bytes */ - const char* server_pk /* 64 bytes */); + const char* session_id, /* 66 bytes */ + const char* blinded_id, /* 66 bytes */ + const char* server_pk /* 64 bytes */); #ifdef __cplusplus } diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index efcf2810..22504820 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -126,7 +126,7 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin /// Takes either a 15 or 25 blinded_id (66 bytes) and the server pubkey (64 bytes). /// /// Returns a flag indicating whether the session_id matches the blinded_id. -bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, - std::string_view server_pk); +bool session_id_matches_blinded_id( + std::string_view session_id, std::string_view blinded_id, std::string_view server_pk); } // namespace session diff --git a/include/session/curve25519.h b/include/session/curve25519.h index 958b6e61..7d77b175 100644 --- a/include/session/curve25519.h +++ b/include/session/curve25519.h @@ -13,14 +13,16 @@ extern "C" { /// Generates a random curve25519 key pair. /// /// Inputs: -/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. -/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the private key will be written. +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the private key will be +/// written. /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_curve25519_key_pair( - unsigned char* curve25519_pk_out, /* 32 byte output buffer */ - unsigned char* curve25519_sk_out /* 32 byte output buffer */); + unsigned char* curve25519_pk_out, /* 32 byte output buffer */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); /// API: crypto/session_to_curve25519_pubkey /// @@ -28,13 +30,14 @@ LIBSESSION_EXPORT bool session_curve25519_key_pair( /// /// Inputs: /// - `ed25519_pubkey` -- the ed25519 public key (32 bytes). -/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. /// /// Outputs: /// - `bool` -- True if the public key was successfully generated, false if failed. LIBSESSION_EXPORT bool session_to_curve25519_pubkey( - const unsigned char* ed25519_pubkey, /* 32 bytes */ - unsigned char* curve25519_pk_out /* 32 byte output buffer */); + const unsigned char* ed25519_pubkey, /* 32 bytes */ + unsigned char* curve25519_pk_out /* 32 byte output buffer */); /// API: crypto/session_to_curve25519_seckey /// @@ -44,13 +47,14 @@ LIBSESSION_EXPORT bool session_to_curve25519_pubkey( /// Inputs: /// - `ed25519_seckey` -- [in] the libsodium-style secret key, 64 bytes. Can also be /// passed as a 32-byte seed. -/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the secret key will be written. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the secret key will be +/// written. /// /// Outputs: /// - `bool` -- True if the secret key was successfully generated, false if failed. LIBSESSION_EXPORT bool session_to_curve25519_seckey( - const unsigned char* ed25519_seckey, /* 64 bytes */ - unsigned char* curve25519_sk_out /* 32 byte output buffer */); + const unsigned char* ed25519_seckey, /* 64 bytes */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); #ifdef __cplusplus } diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index fce70932..34a437ad 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -18,8 +18,7 @@ std::pair, std::array> curve255 /// /// Outputs: /// - The curve25519 public key -std::array to_curve25519_pubkey( - ustring_view ed25519_pubkey); +std::array to_curve25519_pubkey(ustring_view ed25519_pubkey); /// API: curve25519/to_curve25519_seckey /// @@ -31,7 +30,6 @@ std::array to_curve25519_pubkey( /// /// Outputs: /// - The curve25519 secret key -std::array to_curve25519_seckey( - ustring_view ed25519_seckey); +std::array to_curve25519_seckey(ustring_view ed25519_seckey); -} // namespace session::ed25519 +} // namespace session::curve25519 diff --git a/include/session/ed25519.h b/include/session/ed25519.h index 0072296e..82abd521 100644 --- a/include/session/ed25519.h +++ b/include/session/ed25519.h @@ -13,14 +13,16 @@ extern "C" { /// Generates a random ed25519 key pair. /// /// Inputs: -/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. -/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be written. +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be +/// written. /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_key_pair( - unsigned char* ed25519_pk_out, /* 32 byte output buffer */ - unsigned char* ed25519_sk_out /* 64 byte output buffer */); + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); /// API: crypto/session_ed25519_key_pair_seed /// @@ -28,15 +30,17 @@ LIBSESSION_EXPORT bool session_ed25519_key_pair( /// /// Inputs: /// - `ed25519_seed` -- [in] the 32 byte seed. -/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be written. -/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be written. +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be +/// written. /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( - const unsigned char* ed25519_seed, /* 32 bytes */ - unsigned char* ed25519_pk_out, /* 32 byte output buffer */ - unsigned char* ed25519_sk_out /* 64 byte output buffer */); + const unsigned char* ed25519_seed, /* 32 bytes */ + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); /// API: crypto/session_seed_for_ed_privkey /// @@ -50,8 +54,8 @@ LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_seed_for_ed_privkey( - const unsigned char* ed25519_privkey, /* 64 bytes */ - unsigned char* ed25519_seed_out /* 32 byte output buffer */); + const unsigned char* ed25519_privkey, /* 64 bytes */ + unsigned char* ed25519_seed_out /* 32 byte output buffer */); /// API: crypto/session_ed25519_sign /// @@ -61,15 +65,16 @@ LIBSESSION_EXPORT bool session_seed_for_ed_privkey( /// - `ed25519_privkey` -- [in] the libsodium-style secret key of the sender, 64 bytes. /// - `msg` -- [in] the data to generate a signature for. /// - `msg_len` -- [in] the length of the `msg` data. -/// - `ed25519_sig_out` -- [out] pointer to a buffer of 64 bytes where the signature will be written. +/// - `ed25519_sig_out` -- [out] pointer to a buffer of 64 bytes where the signature will be +/// written. /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. LIBSESSION_EXPORT bool session_ed25519_sign( - const unsigned char* ed25519_privkey, /* 64 bytes */ - const unsigned char* msg, - size_t msg_len, - unsigned char* ed25519_sig_out /* 64 byte output buffer */); + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out /* 64 byte output buffer */); /// API: crypto/session_ed25519_verify /// @@ -77,17 +82,18 @@ LIBSESSION_EXPORT bool session_ed25519_sign( /// /// Inputs: /// - `sig` -- [in] the signature to verify, 64 bytes. -/// - `pubkey` -- [in] the pubkey for the secret key that was used to generate the signature, 32 bytes. +/// - `pubkey` -- [in] the pubkey for the secret key that was used to generate the signature, 32 +/// bytes. /// - `msg` -- [in] the data to verify the signature for. /// - `msg_len` -- [in] the length of the `msg` data. /// /// Outputs: /// - A flag indicating whether the signature is valid LIBSESSION_EXPORT bool session_ed25519_verify( - const unsigned char* sig, /* 64 bytes */ - const unsigned char* pubkey, - const unsigned char* msg, - size_t msg_len); + const unsigned char* sig, /* 64 bytes */ + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len); #ifdef __cplusplus } diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index ddd61097..e40144af 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -11,7 +11,7 @@ std::pair, std::array> ed25519_ /// Given an Ed25519 seed this returns the associated Ed25519 key pair std::pair, std::array> ed25519_key_pair( - ustring_view ed25519_seed); + ustring_view ed25519_seed); /// API: ed25519/seed_for_ed_privkey /// diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index de378c50..4cb72a1e 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -4,8 +4,8 @@ extern "C" { #endif -#include #include +#include #include "../export.h" @@ -17,7 +17,7 @@ typedef enum ENCRYPT_TYPE { typedef struct onion_request_builder_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; - + ENCRYPT_TYPE enc_type; } onion_request_builder_object; @@ -30,8 +30,7 @@ typedef struct onion_request_builder_object { /// /// Inputs: /// - `builder` -- [out] Pointer to the builder object -LIBSESSION_EXPORT void onion_request_builder_init( - onion_request_builder_object** builder); +LIBSESSION_EXPORT void onion_request_builder_init(onion_request_builder_object** builder); /// API: onion_request_builder_set_enc_type /// @@ -49,8 +48,7 @@ LIBSESSION_EXPORT void onion_request_builder_init( /// - `builder` -- [in] Pointer to the builder object /// - `enc_type` -- [in] The encryption type to use in the onion request LIBSESSION_EXPORT void onion_request_builder_set_enc_type( - onion_request_builder_object* builder, - ENCRYPT_TYPE enc_type); + onion_request_builder_object* builder, ENCRYPT_TYPE enc_type); /// API: onion_request_builder_set_snode_destination /// @@ -71,9 +69,9 @@ LIBSESSION_EXPORT void onion_request_builder_set_enc_type( /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( - onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey); + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); /// API: onion_request_builder_set_server_destination /// @@ -96,16 +94,16 @@ LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( /// - `builder` -- [in] Pointer to the builder object /// - `host` -- [in] The host for the server destination /// - `target` -- [in] The target (endpoint) for the server destination -/// - `protocol` -- [in] The protocol to use for the +/// - `protocol` -- [in] The protocol to use for the /// - `port` -- [in] The host for the server destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination LIBSESSION_EXPORT void onion_request_builder_set_server_destination( - onion_request_builder_object* builder, - const char* host, - const char* target, - const char* protocol, - uint16_t port, - const char* x25519_pubkey); + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey); /// API: onion_request_builder_add_hop /// @@ -126,9 +124,9 @@ LIBSESSION_EXPORT void onion_request_builder_set_server_destination( /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode hop /// - `x25519_pubkey` -- [in] The x25519 public key for the snode hop LIBSESSION_EXPORT void onion_request_builder_add_hop( - onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey); + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); /// API: onion_request_builder_build /// @@ -162,16 +160,16 @@ LIBSESSION_EXPORT void onion_request_builder_add_hop( /// x25519 secret key used for the onion request will be written if successful /// /// Outputs: -/// - `bool` -- True if the onion request payload was successfully constructed, false if it failed. +/// - `bool` -- True if the onion request payload was successfully constructed, false if it failed. /// If (and only if) true is returned then `payload_out` must be freed when done with it. LIBSESSION_EXPORT bool onion_request_builder_build( - onion_request_builder_object* builder, - const unsigned char* payload_in, - size_t payload_in_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out); + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out); #ifdef __cplusplus } diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 011b4b9e..0ebad565 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -42,8 +42,12 @@ class Builder { ed25519_public_key_.emplace(ed25519_public_key); } - void set_server_destination(std::string host, std::string target, std::string protocol, std::optional port, - x25519_pubkey x25519_public_key) { + void set_server_destination( + std::string host, + std::string target, + std::string protocol, + std::optional port, + x25519_pubkey x25519_public_key) { destination_x25519_public_key.reset(); host_.emplace(host); @@ -56,9 +60,7 @@ class Builder { destination_x25519_public_key.emplace(x25519_public_key); } - void add_hop(std::pair keys) { - hops_.push_back(keys); - } + void add_hop(std::pair keys) { hops_.push_back(keys); } ustring build(ustring payload); diff --git a/include/session/onionreq/response_parser.h b/include/session/onionreq/response_parser.h index 865325de..77237d37 100644 --- a/include/session/onionreq/response_parser.h +++ b/include/session/onionreq/response_parser.h @@ -4,8 +4,8 @@ extern "C" { #endif -#include #include +#include #include "../export.h" #include "builder.h" @@ -39,23 +39,23 @@ extern "C" { /// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination /// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request /// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request -/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on error +/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on +/// error /// - `plaintext_out_len` -- [out] length of plaintext_out if not null /// /// Outputs: -/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. /// If (and only if) true is returned then `plaintext_out` must be freed when done with it. LIBSESSION_EXPORT bool onion_request_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - ENCRYPT_TYPE enc_type_, - unsigned char* destination_x25519_pubkey, - unsigned char* final_x25519_pubkey, - unsigned char* final_x25519_seckey, - unsigned char** plaintext_out, - size_t* plaintext_out_len -); - + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp index 0f078647..9f0c764d 100644 --- a/include/session/onionreq/response_parser.hpp +++ b/include/session/onionreq/response_parser.hpp @@ -13,12 +13,12 @@ class ResponseParser { /// fails. ResponseParser(session::onionreq::Builder builder); ResponseParser( - x25519_pubkey destination_x25519_public_key, - x25519_keypair x25519_keypair, - EncryptType enc_type = EncryptType::xchacha20) : - destination_x25519_public_key_{std::move(destination_x25519_public_key)}, - x25519_keypair_{std::move(x25519_keypair)}, - enc_type_{enc_type} {} + x25519_pubkey destination_x25519_public_key, + x25519_keypair x25519_keypair, + EncryptType enc_type = EncryptType::xchacha20) : + destination_x25519_public_key_{std::move(destination_x25519_public_key)}, + x25519_keypair_{std::move(x25519_keypair)}, + enc_type_{enc_type} {} ustring decrypt(ustring ciphertext) const; diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index eeba222a..b91628e3 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -30,7 +30,7 @@ extern "C" { LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* ed25519_privkey, /* 64 bytes */ const unsigned char* recipient_pubkey, /* 32 bytes */ unsigned char** ciphertext_out, size_t* ciphertext_len); @@ -60,8 +60,8 @@ LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( LIBSESSION_EXPORT bool session_encrypt_for_blinded_recipient( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* ed25519_privkey, /* 64 bytes */ - const unsigned char* open_group_pubkey, /* 32 bytes */ + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* open_group_pubkey, /* 32 bytes */ const unsigned char* recipient_blinded_id, /* 33 bytes */ unsigned char** ciphertext_out, size_t* ciphertext_len); @@ -91,7 +91,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming( const unsigned char* ciphertext_in, size_t ciphertext_len, const unsigned char* ed25519_privkey, /* 64 bytes */ - char* session_id_out, /* 67 byte output buffer */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -122,7 +122,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( size_t ciphertext_len, const unsigned char* x25519_pubkey, /* 32 bytes */ const unsigned char* x25519_seckey, /* 32 bytes */ - char* session_id_out, /* 67 byte output buffer */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -138,7 +138,8 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( /// the blinded message through (32 bytes). /// - `sender_id` -- [in] the blinded id of the sender including the blinding prefix (33 bytes), /// 'blind15' or 'blind25' decryption will be chosed based on this value. -/// - `recipient_id` -- [in] the blinded id of the recipient including the blinding prefix (33 bytes), +/// - `recipient_id` -- [in] the blinded id of the recipient including the blinding prefix (33 +/// bytes), /// must match the same 'blind15' or 'blind25' type of the `sender_id`. /// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, /// hex-encoded session_id of the message's author will be written if decryption/verification was @@ -156,11 +157,11 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* ed25519_privkey, /* 64 bytes */ const unsigned char* open_group_pubkey, /* 32 bytes */ - const unsigned char* sender_id, /* 33 bytes */ - const unsigned char* recipient_id, /* 33 bytes */ - char* session_id_out, /* 67 byte output buffer */ + const unsigned char* sender_id, /* 33 bytes */ + const unsigned char* recipient_id, /* 33 bytes */ + char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); @@ -169,7 +170,8 @@ LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( /// This function attempts to decrypt an ONS response. /// /// Inputs: -/// - `lowercase_name_in` -- [in] Pointer to a buffer containing the lowercase name used to trigger the response. +/// - `lowercase_name_in` -- [in] Pointer to a buffer containing the lowercase name used to trigger +/// the response. /// - `name_len` -- [in] Length of `name_in`. /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. /// - `ciphertext_len` -- [in] Length of `ciphertext_in`. @@ -184,8 +186,8 @@ LIBSESSION_EXPORT bool session_decrypt_ons_response( size_t name_len, const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* nonce_in, /* 24 bytes */ - char* session_id_out /* 67 byte output buffer */); + const unsigned char* nonce_in, /* 24 bytes */ + char* session_id_out /* 67 byte output buffer */); /// API: crypto/session_decrypt_push_notification /// @@ -194,7 +196,8 @@ LIBSESSION_EXPORT bool session_decrypt_ons_response( /// Inputs: /// - `payload_in` -- [in] the payload included in the push notification. /// - `payload_len` -- [in] Length of `payload_in`. -/// - `enc_key_in` -- [in] the device encryption key used when subscribing for push notifications (32 bytes). +/// - `enc_key_in` -- [in] the device encryption key used when subscribing for push notifications +/// (32 bytes). /// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// decrypted 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 *unless* the function returns @@ -207,7 +210,7 @@ LIBSESSION_EXPORT bool session_decrypt_ons_response( LIBSESSION_EXPORT bool session_decrypt_push_notification( const unsigned char* payload_in, size_t payload_len, - const unsigned char* enc_key_in, /* 32 bytes */ + const unsigned char* enc_key_in, /* 32 bytes */ unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 271170c7..c51b823f 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -140,9 +140,9 @@ ustring sign_for_recipient( /// /// Outputs: /// - `std::pair` -- the plaintext binary data that was encrypted and the -/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_incoming( - ustring_view ed25519_privkey, ustring_view ciphertext); +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on +/// error. +std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); /// API: crypto/decrypt_incoming /// @@ -159,7 +159,8 @@ std::pair decrypt_incoming( /// /// Outputs: /// - `std::pair` -- the plaintext binary data that was encrypted and the -/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on error. +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on +/// error. std::pair decrypt_incoming( ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); @@ -203,10 +204,12 @@ std::pair decrypt_incoming_session_id( /// This function attempts to decrypt a message using the SessionBlindingProtocol. /// /// Inputs: -/// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a 64-byte +/// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a +/// 64-byte /// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey /// from the seed. -/// - `server_pk` -- the public key of the open group server to route the blinded message through (32 bytes). +/// - `server_pk` -- the public key of the open group server to route the blinded message through +/// (32 bytes). /// - `sender_id` -- the blinded id of the sender including the blinding prefix (33 bytes), /// 'blind15' or 'blind25' decryption will be chosed based on this value. /// - `recipient_id` -- the blinded id of the recipient including the blinding prefix (33 bytes), @@ -236,9 +239,7 @@ std::pair decrypt_from_blinded_recipient( /// - `std::string` -- the session ID (in hex) returned from the server, *if* the server returned /// a session ID. Throws on error/failure. std::string decrypt_ons_response( - std::string_view lowercase_name, - ustring_view ciphertext, - ustring_view nonce); + std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce); /// API: crypto/decrypt_push_notification /// @@ -246,13 +247,12 @@ std::string decrypt_ons_response( /// /// Inputs: /// - `payload` -- the payload included in the push notification. -/// - `enc_key` -- the device encryption key used when subscribing for push notifications (32 bytes). +/// - `enc_key` -- the device encryption key used when subscribing for push notifications (32 +/// bytes). /// /// Outputs: /// - `ustring` -- the decrypted push notification payload, *if* the decryption was /// successful. Throws on error/failure. -ustring decrypt_push_notification( - ustring_view payload, - ustring_view enc_key); +ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key); } // namespace session diff --git a/src/blinding.cpp b/src/blinding.cpp index bd9d13e3..7a179212 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -9,8 +9,8 @@ #include #include -#include "session/xed25519.hpp" #include "session/export.h" +#include "session/xed25519.hpp" namespace session { @@ -361,7 +361,7 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust crypto_hash_sha512_update(&st2, message.data(), message.size()); crypto_hash_sha512_final(&st2, r_hash.data()); crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); - + // sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) ustring result; result.resize(64); @@ -379,23 +379,26 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust crypto_hash_sha512_final(&st3, hram.data()); // sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka)) - crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) + crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) crypto_core_ed25519_scalar_mul(sig_S, sig_S, blind_15_sk.data()); // S = H(R||A||M) a - crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a - + crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a + return result; } -bool session_id_matches_blinded_id(std::string_view session_id, std::string_view blinded_id, - std::string_view server_pk) { +bool session_id_matches_blinded_id( + std::string_view session_id, std::string_view blinded_id, std::string_view server_pk) { if (session_id.size() != 66 || !oxenc::is_hex(session_id)) - throw std::invalid_argument{"session_id_matches_blinded_id: session_id must be hex (66 digits)"}; + throw std::invalid_argument{ + "session_id_matches_blinded_id: session_id must be hex (66 digits)"}; if (session_id[0] != '0' || session_id[1] != '5') throw std::invalid_argument{"session_id_matches_blinded_id: session_id must start with 05"}; if (blinded_id[1] != '5' && (blinded_id[0] != '1' || blinded_id[0] != '2')) - throw std::invalid_argument{"session_id_matches_blinded_id: blinded_id must start with 15 or 25"}; + throw std::invalid_argument{ + "session_id_matches_blinded_id: blinded_id must start with 15 or 25"}; if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) - throw std::invalid_argument{"session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; + throw std::invalid_argument{ + "session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; std::string converted_blind_id1, converted_blind_id2; ustring converted_blind_id1_raw; @@ -403,17 +406,17 @@ bool session_id_matches_blinded_id(std::string_view session_id, std::string_view switch (blinded_id[0]) { case '1': converted_blind_id1 = blind15_id(session_id, server_pk); - + // For the negative, what we're going to get out of the above is simply the negative of // converted_blind_id1, so flip the sign bit to get converted_blind_id2 - oxenc::from_hex(converted_blind_id1.begin(), converted_blind_id1.end(), std::back_inserter(converted_blind_id1_raw)); + oxenc::from_hex( + converted_blind_id1.begin(), + converted_blind_id1.end(), + std::back_inserter(converted_blind_id1_raw)); converted_blind_id1_raw[32] ^= 0x80; converted_blind_id2 = oxenc::to_hex(converted_blind_id1_raw); - - return ( - blinded_id == converted_blind_id1 || - blinded_id == converted_blind_id2 - ); + + return (blinded_id == converted_blind_id1 || blinded_id == converted_blind_id2); // blind25 doesn't run into the negative issue that blind15 did case '2': return blinded_id == blind25_id(session_id, server_pk); @@ -426,11 +429,10 @@ bool session_id_matches_blinded_id(std::string_view session_id, std::string_view using namespace session; LIBSESSION_C_API bool session_blind15_key_pair( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - unsigned char* blinded_pk_out, - unsigned char* blinded_sk_out -) { + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { try { auto result = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); auto [b_pk, b_sk] = result; @@ -443,11 +445,10 @@ LIBSESSION_C_API bool session_blind15_key_pair( } LIBSESSION_C_API bool session_blind25_key_pair( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - unsigned char* blinded_pk_out, - unsigned char* blinded_sk_out -) { + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { try { auto result = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); auto [b_pk, b_sk] = result; @@ -460,18 +461,14 @@ LIBSESSION_C_API bool session_blind25_key_pair( } LIBSESSION_C_API bool session_blind15_sign( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - const unsigned char* msg, - size_t msg_len, - unsigned char* blinded_sig_out -) { + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out) { try { auto result = session::blind15_sign( - {ed25519_seckey, 64}, - {from_unsigned(server_pk), 32}, - {msg, msg_len} - ); + {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; @@ -481,18 +478,14 @@ LIBSESSION_C_API bool session_blind15_sign( } LIBSESSION_C_API bool session_blind25_sign( - const unsigned char* ed25519_seckey, - const unsigned char* server_pk, - const unsigned char* msg, - size_t msg_len, - unsigned char* blinded_sig_out -) { + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out) { try { auto result = session::blind25_sign( - {ed25519_seckey, 64}, - {from_unsigned(server_pk), 32}, - {msg, msg_len} - ); + {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; @@ -502,16 +495,10 @@ LIBSESSION_C_API bool session_blind25_sign( } LIBSESSION_C_API bool session_id_matches_blinded_id( - const char* session_id, - const char* blinded_id, - const char* server_pk -) { + const char* session_id, const char* blinded_id, const char* server_pk) { try { return session::session_id_matches_blinded_id( - {session_id, 66}, - {blinded_id, 66}, - {server_pk, 64} - ); + {session_id, 66}, {blinded_id, 66}, {server_pk, 64}); } catch (...) { return false; } diff --git a/src/curve25519.cpp b/src/curve25519.cpp index cc76c168..81870cc3 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -18,9 +18,7 @@ std::pair, std::array> curve255 return {curve_pk, curve_sk}; } -std::array to_curve25519_pubkey( - ustring_view ed25519_pubkey -) { +std::array to_curve25519_pubkey(ustring_view ed25519_pubkey) { if (ed25519_pubkey.size() != 32) { throw std::invalid_argument{"Invalid ed25519_pubkey: expected 32 bytes"}; } @@ -35,9 +33,7 @@ std::array to_curve25519_pubkey( return curve_pk; } -std::array to_curve25519_seckey( - ustring_view ed25519_seckey -) { +std::array to_curve25519_seckey(ustring_view ed25519_seckey) { if (ed25519_seckey.size() != 64) { throw std::invalid_argument{"Invalid ed25519_seckey: expected 64 bytes"}; } @@ -56,9 +52,7 @@ std::array to_curve25519_seckey( using namespace session; LIBSESSION_C_API bool session_curve25519_key_pair( - unsigned char* curve25519_pk_out, - unsigned char* curve25519_sk_out -) { + unsigned char* curve25519_pk_out, unsigned char* curve25519_sk_out) { try { auto result = session::curve25519::curve25519_key_pair(); auto [curve_pk, curve_sk] = result; @@ -71,9 +65,7 @@ LIBSESSION_C_API bool session_curve25519_key_pair( } LIBSESSION_C_API bool session_to_curve25519_pubkey( - const unsigned char* ed25519_pubkey, - unsigned char* curve25519_pk_out -) { + const unsigned char* ed25519_pubkey, unsigned char* curve25519_pk_out) { try { auto curve_pk = session::curve25519::to_curve25519_pubkey(ustring_view{ed25519_pubkey, 32}); std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); @@ -84,9 +76,7 @@ LIBSESSION_C_API bool session_to_curve25519_pubkey( } LIBSESSION_C_API bool session_to_curve25519_seckey( - const unsigned char* ed25519_seckey, - unsigned char* curve25519_sk_out -) { + const unsigned char* ed25519_seckey, unsigned char* curve25519_sk_out) { try { auto curve_sk = session::curve25519::to_curve25519_seckey(ustring_view{ed25519_seckey, 64}); std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); diff --git a/src/ed25519.cpp b/src/ed25519.cpp index bc8ce23f..87297b09 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -25,24 +25,22 @@ std::pair, std::array> ed25519_ } std::pair, std::array> ed25519_key_pair( - ustring_view ed25519_seed -) { + ustring_view ed25519_seed) { if (ed25519_seed.size() != 32) { throw std::invalid_argument{"Invalid ed25519_seed: expected 32 bytes"}; } std::array ed_pk; std::array ed_sk; - - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), ed25519_seed.data()); + + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed25519_seed.data()); return {ed_pk, ed_sk}; } std::array seed_for_ed_privkey(ustring_view ed25519_privkey) { std::array seed; - + if (ed25519_privkey.size() == 32 || ed25519_privkey.size() == 64) // The first 32 bytes of a 64 byte ed25519 private key are the seed, otherwise // if the provided value is 32 bytes we just assume we were given a seed @@ -78,8 +76,8 @@ bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { if (pubkey.size() != 32) throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; - return (0 == crypto_sign_ed25519_verify_detached( - sig.data(), msg.data(), msg.size(), pubkey.data())); + return (0 == + crypto_sign_ed25519_verify_detached(sig.data(), msg.data(), msg.size(), pubkey.data())); } } // namespace session::ed25519 @@ -87,9 +85,7 @@ bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { using namespace session; LIBSESSION_C_API bool session_ed25519_key_pair( - unsigned char* ed25519_pk_out, - unsigned char* ed25519_sk_out -) { + unsigned char* ed25519_pk_out, unsigned char* ed25519_sk_out) { try { auto result = session::ed25519::ed25519_key_pair(); auto [ed_pk, ed_sk] = result; @@ -102,10 +98,9 @@ LIBSESSION_C_API bool session_ed25519_key_pair( } LIBSESSION_C_API bool session_ed25519_key_pair_seed( - const unsigned char* ed25519_seed, - unsigned char* ed25519_pk_out, - unsigned char* ed25519_sk_out -) { + const unsigned char* ed25519_seed, + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out) { try { auto result = session::ed25519::ed25519_key_pair(ustring_view{ed25519_seed, 32}); auto [ed_pk, ed_sk] = result; @@ -118,9 +113,7 @@ LIBSESSION_C_API bool session_ed25519_key_pair_seed( } LIBSESSION_C_API bool session_seed_for_ed_privkey( - const unsigned char* ed25519_privkey, - unsigned char* ed25519_seed_out -) { + const unsigned char* ed25519_privkey, unsigned char* ed25519_seed_out) { try { auto result = session::ed25519::seed_for_ed_privkey(ustring_view{ed25519_privkey, 64}); std::memcpy(ed25519_seed_out, result.data(), result.size()); @@ -131,13 +124,13 @@ LIBSESSION_C_API bool session_seed_for_ed_privkey( } LIBSESSION_C_API bool session_ed25519_sign( - const unsigned char* ed25519_privkey, - const unsigned char* msg, - size_t msg_len, - unsigned char* ed25519_sig_out -) { + const unsigned char* ed25519_privkey, + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out) { try { - auto result = session::ed25519::sign(ustring_view{ed25519_privkey, 64}, ustring_view{msg, msg_len}); + auto result = session::ed25519::sign( + ustring_view{ed25519_privkey, 64}, ustring_view{msg, msg_len}); std::memcpy(ed25519_sig_out, result.data(), result.size()); return true; } catch (...) { @@ -145,12 +138,11 @@ LIBSESSION_C_API bool session_ed25519_sign( } } - LIBSESSION_C_API bool session_ed25519_verify( - const unsigned char* sig, - const unsigned char* pubkey, - const unsigned char* msg, - size_t msg_len -) { - return session::ed25519::verify(ustring_view{sig, 64}, ustring_view{pubkey, 32}, ustring_view{msg, msg_len}); + const unsigned char* sig, + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len) { + return session::ed25519::verify( + ustring_view{sig, 64}, ustring_view{pubkey, 32}, ustring_view{msg, msg_len}); } diff --git a/src/hash.cpp b/src/hash.cpp index 1944ad5b..fd16949f 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -16,10 +16,15 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke auto result_code = 0; unsigned char result[size]; - + if (key) - result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), - static_cast(*key).data(), static_cast(*key).size()); + result_code = crypto_generichash_blake2b( + result, + size, + msg.data(), + msg.size(), + static_cast(*key).data(), + static_cast(*key).size()); else result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); @@ -37,13 +42,12 @@ using session::ustring_view; extern "C" { LIBSESSION_C_API bool session_hash( - size_t size, - const unsigned char* msg_in, - size_t msg_len, - const unsigned char* key_in, - size_t key_len, - unsigned char* hash_out -) { + size_t size, + const unsigned char* msg_in, + size_t msg_len, + const unsigned char* key_in, + size_t key_len, + unsigned char* hash_out) { try { std::optional key; diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 1e1a6b43..54143d14 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -3,25 +3,25 @@ #include #include #include -#include #include #include +#include #include #include #include #include -#include #include #include #include +#include -#include "session/xed25519.hpp" +#include "session/export.h" #include "session/onionreq/builder.h" #include "session/onionreq/hop_encryption.hpp" #include "session/onionreq/key_types.hpp" -#include "session/export.h" #include "session/util.hpp" +#include "session/xed25519.hpp" namespace session::onionreq { @@ -35,136 +35,134 @@ namespace { } } // namespace - EncryptType parse_enc_type(std::string_view enc_type) { - if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") - return EncryptType::xchacha20; - if (enc_type == "aes-gcm" || enc_type == "gcm") - return EncryptType::aes_gcm; - throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; - } +EncryptType parse_enc_type(std::string_view enc_type) { + if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") + return EncryptType::xchacha20; + if (enc_type == "aes-gcm" || enc_type == "gcm") + return EncryptType::aes_gcm; + throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; +} - ustring Builder::build(ustring payload) { - ustring blob; - - // First hop: - // - // [N][ENCRYPTED]{json} - // - // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. - // The first hop decrypts ENCRYPTED into: - // - // [N][BLOB]{json} - // - // where [N] is the length of the blob and {json} now contains either: - // - a "headers" key with an empty value. This is how we indicate that the request is for this - // node as the final hop, and means that the BLOB is actually JSON it should parse to get the - // request info (which has "method", "params", etc. in it). - // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made - // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a - // blob here, but it is not used and typically empty). - // - "destination" and "ephemeral_key" to forward the request to the next hop. - // - // This later case continues onion routing by giving us something like: - // - // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} - // - // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via - // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go - // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not - // json): - // - // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } - // - // where BLOB is the opaque data received from the previous hop and N is the hop number which - // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, - // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. - // - // On the *return* trip, the message gets encrypted (once!) at the final destination using the - // derived key from the pubkey given to the final hop, base64-encoded, then passed back without - // any onion encryption at all all the way back to the client. - - // Ephemeral keypair: - x25519_pubkey A; - x25519_seckey a; - nlohmann::json final_route; - - { - crypto_box_keypair(A.data(), a.data()); - HopEncryption e{a, A, false}; - - // The data we send to the destination differs depending on whether the destination is a server - // or a service node - if (host_ && target_ && protocol_ && destination_x25519_public_key) { - final_route = { +ustring Builder::build(ustring payload) { + ustring blob; + + // First hop: + // + // [N][ENCRYPTED]{json} + // + // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. + // The first hop decrypts ENCRYPTED into: + // + // [N][BLOB]{json} + // + // where [N] is the length of the blob and {json} now contains either: + // - a "headers" key with an empty value. This is how we indicate that the request is for this + // node as the final hop, and means that the BLOB is actually JSON it should parse to get the + // request info (which has "method", "params", etc. in it). + // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made + // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a + // blob here, but it is not used and typically empty). + // - "destination" and "ephemeral_key" to forward the request to the next hop. + // + // This later case continues onion routing by giving us something like: + // + // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} + // + // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via + // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go + // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not + // json): + // + // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } + // + // where BLOB is the opaque data received from the previous hop and N is the hop number which + // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, + // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. + // + // On the *return* trip, the message gets encrypted (once!) at the final destination using the + // derived key from the pubkey given to the final hop, base64-encoded, then passed back without + // any onion encryption at all all the way back to the client. + + // Ephemeral keypair: + x25519_pubkey A; + x25519_seckey a; + nlohmann::json final_route; + + { + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + + // The data we send to the destination differs depending on whether the destination is a + // server or a service node + if (host_ && target_ && protocol_ && destination_x25519_public_key) { + final_route = { {"host", host_.value()}, {"target", target_.value()}, {"method", "POST"}, {"protocol", protocol_.value()}, {"port", port_.value_or(protocol_.value() == "https" ? 443 : 80)}, - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use {"enc_type", to_string(enc_type)}, - }; - - blob = e.encrypt(enc_type, payload.data(), *destination_x25519_public_key); - } else if (ed25519_public_key_ && destination_x25519_public_key) { - nlohmann::json control{ - {"headers", ""} - }; - final_route = { - {"destination", ed25519_public_key_.value().hex()}, // Next hop's ed25519 key - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use + }; + + blob = e.encrypt(enc_type, payload.data(), *destination_x25519_public_key); + } else if (ed25519_public_key_ && destination_x25519_public_key) { + nlohmann::json control{{"headers", ""}}; + final_route = { + {"destination", ed25519_public_key_.value().hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use {"enc_type", to_string(enc_type)}, - }; - - auto data = encode_size(payload.size()); - data += payload; - data += to_unsigned_sv(control.dump()); - blob = e.encrypt(enc_type, data, *destination_x25519_public_key); - } else { - throw std::runtime_error{"Destination not set"}; - } - - // Save these because we need them again to decrypt the final response: - final_hop_x25519_keypair.reset(); - final_hop_x25519_keypair.emplace(A, a); + }; + + auto data = encode_size(payload.size()); + data += payload; + data += to_unsigned_sv(control.dump()); + blob = e.encrypt(enc_type, data, *destination_x25519_public_key); + } else { + throw std::runtime_error{"Destination not set"}; } - - for (auto it = hops_.rbegin(); it != hops_.rend(); ++it) { - // Routing data for this hop: - nlohmann::json routing; - - if (it == hops_.rbegin()) { - routing = final_route; - } - else { - routing = { - {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key - {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the *next* hop to use - {"enc_type", to_string(enc_type)}, - }; - } - auto data = encode_size(blob.size()); - data += blob; - data += to_unsigned_sv(routing.dump()); + // Save these because we need them again to decrypt the final response: + final_hop_x25519_keypair.reset(); + final_hop_x25519_keypair.emplace(A, a); + } - // Generate eph key for *this* request and encrypt it: - crypto_box_keypair(A.data(), a.data()); - HopEncryption e{a, A, false}; - blob = e.encrypt(enc_type, data, it->second); + for (auto it = hops_.rbegin(); it != hops_.rend(); ++it) { + // Routing data for this hop: + nlohmann::json routing; + + if (it == hops_.rbegin()) { + routing = final_route; + } else { + routing = { + {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use + {"enc_type", to_string(enc_type)}, + }; } - // The data going to the first hop needs to be wrapped in one more layer to tell the first hop - // how to decrypt the initial payload: - auto result = encode_size(blob.size()); - result += blob; - result += to_unsigned_sv(nlohmann::json{ - {"ephemeral_key", A.hex()}, - {"enc_type", to_string(enc_type)} - }.dump()); + auto data = encode_size(blob.size()); + data += blob; + data += to_unsigned_sv(routing.dump()); - return result; + // Generate eph key for *this* request and encrypt it: + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + blob = e.encrypt(enc_type, data, it->second); } + + // The data going to the first hop needs to be wrapped in one more layer to tell the first hop + // how to decrypt the initial payload: + auto result = encode_size(blob.size()); + result += blob; + result += to_unsigned_sv( + nlohmann::json{{"ephemeral_key", A.hex()}, {"enc_type", to_string(enc_type)}}.dump()); + + return result; +} } // namespace session::onionreq namespace { @@ -174,14 +172,13 @@ session::onionreq::Builder& unbox(onion_request_builder_object* builder) { return *static_cast(builder->internals); } -} +} // namespace extern "C" { using session::ustring; -LIBSESSION_C_API void onion_request_builder_init( - onion_request_builder_object** builder) { +LIBSESSION_C_API void onion_request_builder_init(onion_request_builder_object** builder) { auto c = std::make_unique(); auto c_builder = std::make_unique(); c_builder->internals = c.release(); @@ -189,78 +186,69 @@ LIBSESSION_C_API void onion_request_builder_init( } LIBSESSION_C_API void onion_request_builder_set_enc_type( - onion_request_builder_object* builder, - ENCRYPT_TYPE enc_type -) { + onion_request_builder_object* builder, ENCRYPT_TYPE enc_type) { assert(builder); - + switch (enc_type) { case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: unbox(builder).set_enc_type(session::onionreq::EncryptType::aes_gcm); break; - + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: unbox(builder).set_enc_type(session::onionreq::EncryptType::xchacha20); break; - + default: throw std::runtime_error{"Invalid encryption type"}; } } LIBSESSION_C_API void onion_request_builder_set_snode_destination( - onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey -) { + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey) { assert(builder && ed25519_pubkey && x25519_pubkey); - + unbox(builder).set_snode_destination( - session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) - ); + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); } LIBSESSION_C_API void onion_request_builder_set_server_destination( - onion_request_builder_object* builder, - const char* host, - const char* target, - const char* protocol, - uint16_t port, - const char* x25519_pubkey -) { + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey) { assert(builder && host && target && protocol && x25519_pubkey); unbox(builder).set_server_destination( - host, - target, - protocol, - port, - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) - ); + host, + target, + protocol, + port, + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); } LIBSESSION_C_API void onion_request_builder_add_hop( - onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey -) { + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey) { assert(builder && ed25519_pubkey && x25519_pubkey); - unbox(builder).add_hop({ - session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}) - }); + unbox(builder).add_hop( + {session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})}); } LIBSESSION_C_API bool onion_request_builder_build( - onion_request_builder_object* builder, - const unsigned char* payload_in, - size_t payload_in_len, - unsigned char** payload_out, - size_t* payload_out_len, - unsigned char* final_x25519_pubkey_out, - unsigned char* final_x25519_seckey_out -) { + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out) { assert(builder && payload_in); try { @@ -271,20 +259,17 @@ LIBSESSION_C_API bool onion_request_builder_build( auto key_pair = unboxed_builder.final_hop_x25519_keypair.value(); std::memcpy(final_x25519_pubkey_out, key_pair.first.data(), key_pair.first.size()); std::memcpy(final_x25519_seckey_out, key_pair.second.data(), key_pair.second.size()); - } - else { + } else { throw std::runtime_error{"Final keypair not generated"}; } - + *payload_out = static_cast(malloc(payload.size())); *payload_out_len = payload.size(); std::memcpy(*payload_out, payload.data(), payload.size()); return true; - } - catch (...) { + } catch (...) { return false; } } - } \ No newline at end of file diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index aa25348d..d15ef641 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -3,24 +3,24 @@ #include #include #include -#include #include #include +#include #include #include #include #include -#include #include #include #include +#include -#include "session/xed25519.hpp" +#include "session/export.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" -#include "session/export.h" #include "session/util.hpp" +#include "session/xed25519.hpp" namespace session::onionreq { @@ -98,8 +98,7 @@ ustring HopEncryption::decrypt( throw std::runtime_error{"Invalid decryption type"}; } -ustring HopEncryption::encrypt_aesgcm( - ustring plaintext, const x25519_pubkey& pubKey) const { +ustring HopEncryption::encrypt_aesgcm(ustring plaintext, const x25519_pubkey& pubKey) const { auto key = derive_symmetric_key(private_key_, pubKey); // Initialise cipher context with the key @@ -129,8 +128,7 @@ ustring HopEncryption::encrypt_aesgcm( return output; } -ustring HopEncryption::decrypt_aesgcm( - ustring ciphertext_, const x25519_pubkey& pubKey) const { +ustring HopEncryption::decrypt_aesgcm(ustring ciphertext_, const x25519_pubkey& pubKey) const { ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) @@ -163,8 +161,7 @@ ustring HopEncryption::decrypt_aesgcm( return plaintext; } -ustring HopEncryption::encrypt_xchacha20( - ustring plaintext, const x25519_pubkey& pubKey) const { +ustring HopEncryption::encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const { ustring ciphertext; ciphertext.resize( @@ -177,7 +174,7 @@ ustring HopEncryption::encrypt_xchacha20( randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); auto* c = reinterpret_cast(ciphertext.data()) + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; unsigned long long clen; crypto_aead_xchacha20poly1305_ietf_encrypt( @@ -195,8 +192,7 @@ ustring HopEncryption::encrypt_xchacha20( return ciphertext; } -ustring HopEncryption::decrypt_xchacha20( - ustring ciphertext_, const x25519_pubkey& pubKey) const { +ustring HopEncryption::decrypt_xchacha20(ustring ciphertext_, const x25519_pubkey& pubKey) const { ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; // Extract nonce from the beginning of the ciphertext: diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index 76ac1c65..4c8448c5 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -35,7 +35,7 @@ OnionReqParser::OnionReqParser( remote_pk = parse_x25519_pubkey(itr->get()); else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - + auto plaintext = enc.decrypt(enc_type, {ciphertext.data(), ciphertext.size()}, remote_pk); payload_ = {to_unsigned(plaintext.data()), plaintext.size()}; } diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 2a212013..cfcc1dc3 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -3,23 +3,21 @@ #include #include -#include "session/onionreq/builder.h" -#include "session/onionreq/builder.hpp" -#include "session/onionreq/hop_encryption.hpp" #include #include "session/export.h" +#include "session/onionreq/builder.h" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/hop_encryption.hpp" namespace session::onionreq { -ResponseParser::ResponseParser( - session::onionreq::Builder builder -) { +ResponseParser::ResponseParser(session::onionreq::Builder builder) { if (!builder.destination_x25519_public_key.has_value()) throw std::runtime_error{"Builder does not contain destination x25519 public key"}; if (!builder.final_hop_x25519_keypair.has_value()) throw std::runtime_error{"Builder does not contain final keypair"}; - + enc_type_ = builder.enc_type; destination_x25519_public_key_ = builder.destination_x25519_public_key.value(); x25519_keypair_ = builder.final_hop_x25519_keypair.value(); @@ -38,16 +36,16 @@ extern "C" { using session::ustring; LIBSESSION_C_API bool onion_request_decrypt( - const unsigned char* ciphertext, - size_t ciphertext_len, - ENCRYPT_TYPE enc_type_, - unsigned char* destination_x25519_pubkey, - unsigned char* final_x25519_pubkey, - unsigned char* final_x25519_seckey, - unsigned char** plaintext_out, - size_t* plaintext_out_len -) { - assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && ciphertext_len > 0); + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len) { + assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && + ciphertext_len > 0); try { auto enc_type = session::onionreq::EncryptType::xchacha20; @@ -61,29 +59,26 @@ LIBSESSION_C_API bool onion_request_decrypt( enc_type = session::onionreq::EncryptType::xchacha20; break; - default: throw std::runtime_error{"Invalid decryption type " + std::to_string(enc_type_)}; + default: + throw std::runtime_error{"Invalid decryption type " + std::to_string(enc_type_)}; } session::onionreq::HopEncryption d{ - session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), - session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), - false - }; + session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), + session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), + false}; auto result = d.decrypt( - enc_type, - ustring{ciphertext, ciphertext_len}, - session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32}) - ); + enc_type, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); *plaintext_out = static_cast(malloc(result.size())); *plaintext_out_len = result.size(); std::memcpy(*plaintext_out, result.data(), result.size()); return true; - } - catch (...) { + } catch (...) { return false; } } - } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index cad42549..10cac8b3 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,14 +1,14 @@ #include "session/session_encrypt.hpp" #include +#include +#include #include #include #include #include #include #include -#include -#include #include #include @@ -16,8 +16,8 @@ #include #include -#include "session/util.hpp" #include "session/blinding.hpp" +#include "session/util.hpp" using namespace std::literals; @@ -142,11 +142,10 @@ ustring encrypt_for_recipient_deterministic( } ustring encrypt_for_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view recipient_blinded_id, - ustring_view message -) { + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view recipient_blinded_id, + ustring_view message) { if (ed25519_privkey.size() == 64) ed25519_privkey.remove_suffix(32); else if (ed25519_privkey.size() != 32) @@ -165,15 +164,13 @@ ustring encrypt_for_blinded_recipient( std::pair blinded_key_pair; switch (recipient_blinded_id[0]) { - case 0x15: - blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); - break; + case 0x15: blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); break; - case 0x25: - blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); - break; + case 0x25: blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); break; - default: throw std::invalid_argument{"Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; + default: + throw std::invalid_argument{ + "Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; } // Remove the blinding prefix @@ -210,7 +207,7 @@ ustring encrypt_for_blinded_recipient( buf.reserve(message.size() + 32); buf += message; buf += ustring_view{ed_pk.data(), 32}; - + // Encrypt using xchacha20-poly1305 cleared_array nonce; randombytes_buf(nonce.data(), nonce.size()); @@ -221,8 +218,15 @@ ustring encrypt_for_blinded_recipient( buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data(), &outlen, buf.data(), buf.size(), nullptr, 0, nullptr, - nonce.data(), enc_key.data())) + ciphertext.data(), + &outlen, + buf.data(), + buf.size(), + nullptr, + 0, + nullptr, + nonce.data(), + enc_key.data())) throw std::runtime_error{"Crypto aead encryption failed"}; // data = b'\x00' + ciphertext + nonce @@ -240,8 +244,7 @@ std::pair decrypt_incoming_session_id( // Convert the sender_ed_pk to the sender's session ID std::array sender_x_pk; - if (0 != crypto_sign_ed25519_pk_to_curve25519( - sender_x_pk.data(), sender_ed_pk.data())) + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_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 @@ -261,8 +264,7 @@ std::pair decrypt_incoming_session_id( // Convert the sender_ed_pk to the sender's session ID std::array sender_x_pk; - if (0 != crypto_sign_ed25519_pk_to_curve25519( - sender_x_pk.data(), sender_ed_pk.data())) + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_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 @@ -308,7 +310,11 @@ std::pair decrypt_incoming( buf.resize(outer_size); if (0 != crypto_box_seal_open( - buf.data(), ciphertext.data(), ciphertext.size(), x25519_pubkey.data(), x25519_seckey.data())) + buf.data(), + ciphertext.data(), + ciphertext.size(), + x25519_pubkey.data(), + x25519_seckey.data())) throw std::runtime_error{"Decryption failed"}; uc64 sig; @@ -328,12 +334,11 @@ std::pair decrypt_incoming( } std::pair decrypt_from_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view sender_id, - ustring_view recipient_id, - ustring_view ciphertext -) { + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view sender_id, + ustring_view recipient_id, + ustring_view ciphertext) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -348,8 +353,10 @@ std::pair decrypt_from_blinded_recipient( throw std::invalid_argument{"Invalid sender_id: expected 33 bytes"}; if (recipient_id.size() != 33) throw std::invalid_argument{"Invalid recipient_id: expected 33 bytes"}; - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw std::invalid_argument{"Invalid ciphertext: too short to contain valid encrypted data"}; + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{ + "Invalid ciphertext: too short to contain valid encrypted data"}; std::pair result; auto& [buf, sender_session_id] = result; @@ -364,7 +371,8 @@ std::pair decrypt_from_blinded_recipient( } else if (recipient_id[0] == 0x25 && sender_id[0] == 0x25) { blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); } else - throw std::invalid_argument{"Both sender_id and recipient_id must start with the same 0x15 or 0x25 prefix"}; + throw std::invalid_argument{ + "Both sender_id and recipient_id must start with the same 0x15 or 0x25 prefix"}; // Calculate the shared encryption key, sending from A to B: // @@ -397,22 +405,31 @@ std::pair decrypt_from_blinded_recipient( throw std::invalid_argument{"Invalid ciphertext: version is not 0"}; ustring nonce; - const size_t msg_size = (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + const size_t msg_size = + (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); unsigned long long buf_len = 0; buf.resize(msg_size); nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::memcpy(nonce.data(), ciphertext.data() + msg_size + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES, - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy( + nonce.data(), + ciphertext.data() + msg_size + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES, + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), &buf_len, nullptr, ciphertext.data() + 1, - ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - nullptr, 0, nonce.data(), dec_key.data())) + buf.data(), + &buf_len, + nullptr, + ciphertext.data() + 1, + ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, + 0, + nonce.data(), + dec_key.data())) - // Ensure the length is correct - if (buf.size() <= 32) - throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; + // Ensure the length is correct + if (buf.size() <= 32) + throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key uc32 sender_ed_pk; @@ -432,7 +449,8 @@ std::pair decrypt_from_blinded_recipient( else { blindingFactor = blind25_factor({sender_x_pk.data(), 32}, server_pk); - // sender_ed_pk is negative, so we need to negate `blindingFactor` to make things come out right + // sender_ed_pk is negative, so we need to negate `blindingFactor` to make things come out + // right if (sender_ed_pk[31] & 0x80) { uc32 blindingFactor_neg; crypto_core_ed25519_scalar_negate(blindingFactor_neg.data(), blindingFactor.data()); @@ -440,13 +458,14 @@ std::pair decrypt_from_blinded_recipient( } } - if (0 != crypto_scalarmult_ed25519_noclamp(extracted_kA.data(), blindingFactor.data(), sender_ed_pk.data())) + if (0 != crypto_scalarmult_ed25519_noclamp( + extracted_kA.data(), blindingFactor.data(), sender_ed_pk.data())) throw std::runtime_error{"Shared secret generation for verification failed"}; if (kA != ustring_view{extracted_kA.data(), 32}) throw std::runtime_error{"Shared secret does not match encoded public key"}; - // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix to - // the sender session ID + // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix + // to the sender session ID buf.resize(buf.size() - 32); sender_session_id.reserve(66); sender_session_id += "05"; @@ -455,12 +474,13 @@ std::pair decrypt_from_blinded_recipient( return result; } -std::string decrypt_ons_response(std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { +std::string decrypt_ons_response( + std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) throw std::invalid_argument{"Invalid nonce: expected to be 24 bytes"}; - + // Hash the ONS name using BLAKE2b // // xchacha-based encryption @@ -468,16 +488,30 @@ std::string decrypt_ons_response(std::string_view lowercase_name, ustring_view c uc32 key; uc32 name_hash; auto name_bytes = to_unsigned(lowercase_name.data()); - crypto_generichash_blake2b(name_hash.data(), name_hash.size(), name_bytes, lowercase_name.size(), nullptr, 0); - crypto_generichash_blake2b(key.data(), key.size(), name_bytes, lowercase_name.size(), name_hash.data(), name_hash.size()); + crypto_generichash_blake2b( + name_hash.data(), name_hash.size(), name_bytes, lowercase_name.size(), nullptr, 0); + crypto_generichash_blake2b( + key.data(), + key.size(), + name_bytes, + lowercase_name.size(), + name_hash.data(), + name_hash.size()); ustring buf; unsigned long long buf_len = 0; buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), &buf_len, nullptr, ciphertext.data(), ciphertext.size(), - nullptr, 0, nonce.data(), key.data())) + buf.data(), + &buf_len, + nullptr, + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, + nonce.data(), + key.data())) throw std::runtime_error{"Failed to decrypt"}; if (buf_len != 33) @@ -488,24 +522,32 @@ std::string decrypt_ons_response(std::string_view lowercase_name, ustring_view c } ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { - if (payload.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (payload.size() < + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; if (enc_key.size() != 32) throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; ustring buf; ustring nonce; - const size_t msg_size = (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + const size_t msg_size = + (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); unsigned long long buf_len = 0; buf.resize(msg_size); nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); std::memcpy(nonce.data(), payload.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), &buf_len, nullptr, payload.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - payload.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, nullptr, 0, - nonce.data(), enc_key.data())) + buf.data(), + &buf_len, + nullptr, + payload.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + payload.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, + 0, + nonce.data(), + enc_key.data())) throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; // Removing any null padding bytes from the end @@ -520,19 +562,17 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { using namespace session; LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( - const unsigned char* plaintext_in, - size_t plaintext_len, - const unsigned char* ed25519_privkey, - const unsigned char* recipient_pubkey, - unsigned char** ciphertext_out, - size_t* ciphertext_len -) { + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* recipient_pubkey, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_recipient_deterministic( - ustring_view{ed25519_privkey, 64}, - ustring_view{recipient_pubkey, 32}, - ustring_view{plaintext_in, plaintext_len} - ); + ustring_view{ed25519_privkey, 64}, + ustring_view{recipient_pubkey, 32}, + ustring_view{plaintext_in, plaintext_len}); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -550,15 +590,13 @@ LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( const unsigned char* open_group_pubkey, const unsigned char* recipient_blinded_id, unsigned char** ciphertext_out, - size_t* ciphertext_len -) { + size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_blinded_recipient( - ustring_view{ed25519_privkey, 64}, - ustring_view{open_group_pubkey, 32}, - ustring_view{recipient_blinded_id, 33}, - ustring_view{plaintext_in, plaintext_len} - ); + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{recipient_blinded_id, 33}, + ustring_view{plaintext_in, plaintext_len}); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -570,18 +608,15 @@ LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( } LIBSESSION_C_API bool session_decrypt_incoming( - const unsigned char* ciphertext_in, - size_t ciphertext_len, - const unsigned char* ed25519_privkey, - char* session_id_out, - unsigned char** plaintext_out, - size_t* plaintext_len -) { + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - ustring_view{ed25519_privkey, 64}, - ustring_view{ciphertext_in, ciphertext_len} - ); + ustring_view{ed25519_privkey, 64}, ustring_view{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -595,20 +630,18 @@ LIBSESSION_C_API bool session_decrypt_incoming( } LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( - const unsigned char* ciphertext_in, - size_t ciphertext_len, - const unsigned char* x25519_pubkey, - const unsigned char* x25519_seckey, - char* session_id_out, - unsigned char** plaintext_out, - size_t* plaintext_len -) { + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* x25519_pubkey, + const unsigned char* x25519_seckey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - ustring_view{x25519_pubkey, 32}, - ustring_view{x25519_seckey, 32}, - ustring_view{ciphertext_in, ciphertext_len} - ); + ustring_view{x25519_pubkey, 32}, + ustring_view{x25519_seckey, 32}, + ustring_view{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -630,16 +663,14 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( const unsigned char* recipient_id, char* session_id_out, unsigned char** plaintext_out, - size_t* plaintext_len -) { + size_t* plaintext_len) { try { auto result = session::decrypt_from_blinded_recipient( - ustring_view{ed25519_privkey, 64}, - ustring_view{open_group_pubkey, 32}, - ustring_view{sender_id, 33}, - ustring_view{recipient_id, 33}, - ustring_view{ciphertext_in, ciphertext_len} - ); + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{sender_id, 33}, + ustring_view{recipient_id, 33}, + ustring_view{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -658,14 +689,12 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* ciphertext_in, size_t ciphertext_len, const unsigned char* nonce_in, - char* session_id_out -) { + char* session_id_out) { try { auto session_id = session::decrypt_ons_response( - std::string_view{name_in, name_len}, - ustring_view{ciphertext_in, ciphertext_len}, - ustring_view{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES} - ); + std::string_view{name_in, name_len}, + ustring_view{ciphertext_in, ciphertext_len}, + ustring_view{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -679,13 +708,10 @@ LIBSESSION_C_API bool session_decrypt_push_notification( size_t payload_len, const unsigned char* enc_key_in, unsigned char** plaintext_out, - size_t* plaintext_len -) { + size_t* plaintext_len) { try { auto plaintext = session::decrypt_push_notification( - ustring_view{payload_in, payload_len}, - ustring_view{enc_key_in, 32} - ); + ustring_view{payload_in, payload_len}, ustring_view{enc_key_in, 32}); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 132b9aa7..ad323446 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -284,9 +284,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); } - TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { - std::array server_pks = { + std::array server_pks = { "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp index cf00256c..3acf6aa4 100644 --- a/tests/test_curve25519.cpp +++ b/tests/test_curve25519.cpp @@ -1,17 +1,16 @@ #include -#include #include +#include #include "session/curve25519.h" #include "session/curve25519.hpp" - #include "utils.hpp" TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { auto kp1 = session::curve25519::curve25519_key_pair(); auto kp2 = session::curve25519::curve25519_key_pair(); - + CHECK(kp1.first.size() == 32); CHECK(kp1.second.size() == 64); CHECK(kp1.first != kp2.first); @@ -23,28 +22,30 @@ TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - + auto x_pk1 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk1)); auto x_pk2 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk2)); CHECK(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == - "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == - "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); } TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { using namespace session; - auto ed_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" - "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; + auto ed_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk2 = + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; auto x_sk1 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk1)); auto x_sk2 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk2)); CHECK(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == - "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); + "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); CHECK(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == - "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); + "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); } diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index c084ce38..31ecb520 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -1,18 +1,17 @@ #include -#include #include +#include #include "session/ed25519.h" #include "session/ed25519.hpp" - #include "utils.hpp" TEST_CASE("Ed25519 key pair generation", "[ed25519][keypair]") { // Generate two random key pairs and make sure they don't match auto kp1 = session::ed25519::ed25519_key_pair(); auto kp2 = session::ed25519::ed25519_key_pair(); - + CHECK(kp1.first.size() == 32); CHECK(kp1.second.size() == 64); CHECK(kp1.first != kp2.first); @@ -29,20 +28,22 @@ TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { auto kp1 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed1)); auto kp2 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed2)); CHECK_THROWS(session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed_invalid))); - + CHECK(kp1.first.size() == 32); CHECK(kp1.second.size() == 64); CHECK(kp1.first != kp2.first); CHECK(kp1.second != kp2.second); CHECK(oxenc::to_hex(kp1.first.begin(), kp1.first.end()) == - "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); CHECK(oxenc::to_hex(kp2.first.begin(), kp2.first.end()) == - "cd83ca3d13ad8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"); - - auto kp_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"; - auto kp_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" - "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"; + "cd83ca3d13ad8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"); + + auto kp_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"; + auto kp_sk2 = + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"; CHECK(oxenc::to_hex(kp1.second.begin(), kp1.second.end()) == kp_sk1); CHECK(oxenc::to_hex(kp2.second.begin(), kp2.second.end()) == kp_sk2); } @@ -50,19 +51,20 @@ TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { using namespace session; - auto ed_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; auto ed_sk_invalid = "010203040506070809"_hexbytes; auto seed1 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk1)); auto seed2 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk2)); CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk_invalid))); - + CHECK(oxenc::to_hex(seed1.begin(), seed1.end()) == - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); CHECK(oxenc::to_hex(seed2.begin(), seed2.end()) == - "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); } TEST_CASE("Ed25519", "[ed25519][signature]") { @@ -75,10 +77,10 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { auto sig1 = session::ed25519::sign(to_unsigned_sv(ed_seed), to_unsigned_sv("hello")); CHECK_THROWS(session::ed25519::sign(to_unsigned_sv(ed_invalid), to_unsigned_sv("hello"))); - auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" - "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; - CHECK(oxenc::to_hex(sig1.begin(), sig1.end()) == - expected_sig_hex); + auto expected_sig_hex = + "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" + "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; + CHECK(oxenc::to_hex(sig1.begin(), sig1.end()) == expected_sig_hex); CHECK(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_unsigned_sv("hello"))); diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index 861b8a5e..626cd57d 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -2,7 +2,6 @@ #include "session/hash.h" #include "session/hash.hpp" - #include "utils.hpp" TEST_CASE("Hash generation", "[hash][hash]") { @@ -14,9 +13,12 @@ TEST_CASE("Hash generation", "[hash][hash]") { auto hash6 = session::hash::hash(64, to_usv("TestMessage"), to_usv("TestKey")); CHECK_THROWS(session::hash::hash(10, to_usv("TestMessage"), std::nullopt)); CHECK_THROWS(session::hash::hash(100, to_usv("TestMessage"), std::nullopt)); - CHECK_THROWS(session::hash::hash(32, to_usv("TestMessage"), - to_usv("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLong"))); - + CHECK_THROWS(session::hash::hash( + 32, + to_usv("TestMessage"), + to_usv("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLon" + "g"))); + CHECK(hash1.size() == 32); CHECK(hash2.size() == 32); CHECK(hash3.size() == 32); @@ -28,15 +30,21 @@ TEST_CASE("Hash generation", "[hash][hash]") { CHECK(hash3 == hash4); CHECK(hash1 != hash5); CHECK(hash3 != hash6); - CHECK(oxenc::to_hex(hash1) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); - CHECK(oxenc::to_hex(hash2) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); - CHECK(oxenc::to_hex(hash3) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); - CHECK(oxenc::to_hex(hash4) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(oxenc::to_hex(hash1) == + "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash2) == + "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash3) == + "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(oxenc::to_hex(hash4) == + "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); - auto expected_hash5 = "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a39c02db69c44" - "16d5c45acc2e9469b7f274992b2858f3bb2746becb48c8b56ce4b"; - auto expected_hash6 = "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e877874c9059edf5" - "3d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"; + auto expected_hash5 = + "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a39c02db69c44" + "16d5c45acc2e9469b7f274992b2858f3bb2746becb48c8b56ce4b"; + auto expected_hash6 = + "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e877874c9059edf5" + "3d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"; CHECK(oxenc::to_hex(hash5) == expected_hash5); CHECK(oxenc::to_hex(hash6) == expected_hash6); } diff --git a/tests/test_random.cpp b/tests/test_random.cpp index 2e509d4e..c02ad515 100644 --- a/tests/test_random.cpp +++ b/tests/test_random.cpp @@ -2,14 +2,13 @@ #include "session/random.h" #include "session/random.hpp" - #include "utils.hpp" TEST_CASE("Random generation", "[random][random]") { auto rand1 = session::random::random(10); auto rand2 = session::random::random(10); auto rand3 = session::random::random(20); - + CHECK(rand1.size() == 10); CHECK(rand2.size() == 10); CHECK(rand3.size() == 20); diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 3b599898..9b549e70 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,8 +1,8 @@ #include #include -#include #include +#include #include #include "utils.hpp" @@ -68,7 +68,8 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_recipient({to_sv(ed_sk).data(), 32}, sid_raw2, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_recipient( + {to_sv(ed_sk).data(), 32}, sid_raw2, to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); @@ -144,7 +145,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e using namespace session; const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - const auto server_pk = "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hexbytes; + const auto server_pk = + "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hexbytes; std::array ed_pk, curve_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); @@ -191,44 +193,82 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e memcpy(blind25_pk2_prefixed.data() + 1, blind25_pk2.data(), 32); SECTION("blind15, full secret, recipient decrypt") { - auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - blind15_pk, {blind15_pk2_prefixed.data(), 33}, enc)); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, blind15_pk2, enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + blind15_pk, + {blind15_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + blind15_pk2, + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); } SECTION("blind15, full secret, sender decrypt") { - auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - blind15_pk, {blind15_pk2_prefixed.data(), 33}, enc)); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, blind15_pk2, enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + blind15_pk, + {blind15_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + blind15_pk2, + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); } SECTION("blind15, only seed, recipient decrypt") { constexpr auto lorem_ipsum = @@ -238,59 +278,108 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_blinded_recipient({to_sv(ed_sk).data(), 32}, to_unsigned_sv(server_pk), - {blind15_pk2_prefixed.data(), 33}, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); - auto [msg, sender] = decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == lorem_ipsum); auto broken = enc; - broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); } SECTION("blind25, full secret, recipient decrypt") { - auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); - - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - blind25_pk, {blind25_pk2_prefixed.data(), 33}, enc)); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, blind25_pk2, enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + blind25_pk, + {blind25_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + blind25_pk2, + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk2), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); } SECTION("blind25, full secret, sender decrypt") { - auto enc = encrypt_for_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv("hello")); + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); CHECK(from_unsigned_sv(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - blind25_pk, {blind25_pk2_prefixed.data(), 33}, enc)); - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, blind25_pk2, enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + blind25_pk, + {blind25_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + blind25_pk2, + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient(to_sv(ed_sk), to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); } SECTION("blind25, only seed, recipient decrypt") { constexpr auto lorem_ipsum = @@ -300,19 +389,30 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_blinded_recipient({to_sv(ed_sk).data(), 32}, to_unsigned_sv(server_pk), - {blind25_pk2_prefixed.data(), 33}, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); - auto [msg, sender] = decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); CHECK(sender == sid); CHECK(from_unsigned_sv(msg) == lorem_ipsum); auto broken = enc; - broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient({to_sv(ed_sk2).data(), 32}, to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); } } @@ -320,13 +420,15 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { using namespace session; std::string_view name = "test"; - auto ciphertext = "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" - "1580d9a8c9b8a64cacfec97"_hexbytes; + auto ciphertext = + "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" + "1580d9a8c9b8a64cacfec97"_hexbytes; auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; - ustring sid_data = "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; + ustring sid_data = + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; - CHECK(decrypt_ons_response(name, ciphertext, nonce) == - "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(decrypt_ons_response(name, ciphertext, nonce) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK_THROWS(decrypt_ons_response(name, to_unsigned_sv("invalid"), nonce)); CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); } @@ -334,10 +436,12 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { TEST_CASE("Session push notification decryption", "[session-notification][decrypt]") { using namespace session; - auto payload = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; - auto payload_padded = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto payload = + "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto payload_padded = + "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; CHECK(decrypt_push_notification(payload, enc_key) == to_unsigned("TestMessage")); From fb3837576bfa10a99a7a7dbb83482e52fd236190 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Jan 2024 15:55:50 -0400 Subject: [PATCH 141/572] 25 blinding fixes and improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 25 blinding encryption design had a flaw in that it used H(a(kB) || kA || kB) as the shared secret, and then assumed that the other side could compute `kbA || kA || kB`, but with 25 blinding that doesn't work because *each side* has a different `k` value (since k depends on both the server_id and the session_id), so this was actually computing: H(a(jB) || kA || jB) when encrypting, and H(b(kA) || kA || jB) when decrypting (where `j` is B's blinding factor). This amends the encryption to instead use: H(ak(jB) || kA || jB) for encryption and H(bj(kA) || kA || jB) for decryption, which works because we end up with `kjaB` or `kjbA` for the first term, which are equal (since `aB == bA`). This also makes various other small fixes and improvements along the way to the encryption code to get the 15 and 25 encryption and decryption tests working (and make the code a bit easier): - The hex version of blind15_id now returns a pair of the two alternative ids, which simplifies calling code from needing to worry about flipping the sign bit. - added blinded[12]5_from_ed functions that are similar to the non-`from_ed` versions, but can work more efficiently and precisely (most notably, when we know the actual ed pubkey we can always return the correctly signed blinded15 id; and when doing 25, having the ed pubkey saves us from needing to invert the session id back to an ed pubkey). - Changed the types of blind[12]5_key_pair so that the returned private scalar gets properly memory cleared when destroyed, and moved these types (eg uc32, cleared_uc64) "higher up" in the include hierarchy. - Changed `blind[12]5_key_pair` to optionally return (via pointer) the blinding factor. For the 25 version we actually return ±k, depending on whether an extra negation is needed to cancel out a negative pubkey, so that the returned value can be used directly without having to worry about the negation elsewhere. - removed the "sender decrypt" blinded tests because we don't expose a way for the sender to perform such decryption. (It's technically possible to do, but doesn't seem like something we need). --- include/session/blinding.hpp | 60 ++++++-- include/session/util.hpp | 10 ++ src/blinding.cpp | 180 +++++++++++++--------- src/session_encrypt.cpp | 274 ++++++++++++++++++--------------- tests/test_blinding.cpp | 39 ++--- tests/test_session_encrypt.cpp | 107 ++----------- 6 files changed, 339 insertions(+), 331 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 22504820..7374d9b7 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -57,21 +57,23 @@ namespace session { /// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes server pk in bytes, not hex. -std::array blind15_factor(ustring_view server_pk); +uc32 blind15_factor(ustring_view server_pk); /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). -std::array blind25_factor(ustring_view session_id, ustring_view server_pk); +uc32 blind25_factor(ustring_view session_id, ustring_view server_pk); -/// Computes the 15-blinded id from a session id and server pubkey. Values accepted and -/// returned are hex-encoded. -ustring blind15_id(ustring_view session_id, ustring_view server_pk); +/// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted +/// and returned are hex-encoded. +std::array blind15_id(std::string_view session_id, std::string_view server_pk); -/// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a -/// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may -/// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). -std::string blind15_id(std::string_view session_id, std::string_view server_pk); +/// Similar to the above, but takes the session id and pubkey as byte values instead of hex, and +/// returns a single 33-byte value (instead of a 66-digit hex value). Unlike the string version, +/// session_id here may be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). Only +/// the *positive* possible ID is returned: the alternative can be computed by flipping the highest +/// bit of byte 32, i.e.: `result[32] ^= 0x80`. +ustring blind15_id(ustring_view session_id, ustring_view server_pk); /// Computes the 25-blinded id from a session id and server pubkey. Values accepted and /// returned are hex-encoded. @@ -82,23 +84,55 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk); /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). ustring blind25_id(ustring_view session_id, ustring_view server_pk); +/// Computes the 15-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 +/// pubkey behind a (X25519) Session ID. Unlike blind15_id, knowing the true Ed25519 pubkey allows +/// thie method to compute the correct sign and so using this does not require considering that the +/// resulting blinded ID might need to have a sign flipped. +/// +/// If the `session_id` is a non-null pointer then it must point at an empty string to be populated +/// with the session_id associated with `ed_pubkey`. This is here for consistency with +/// `blinded25_id_from_ed`, but unlike the 25 version, this value is not read if non-empty, and is +/// not an optimization (that is: it is purely for convenience and is no more efficient to use this +/// than it is to compute it yourself). +ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); + +/// Computes the 25-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 +/// pubkey behind a (X25519) Session ID. This will be the same as blind25_id (if given the X25519 +/// pubkey that the Ed25519 converts to), but is more efficient when the Ed25519 pubkey is already +/// known. +/// +/// The session_id argument is provided to optimize input or output of the session ID derived from +/// the Ed25519 pubkey: if already computed, this argument can be a pointer to a 33-byte string +/// containing the precomputed value (to avoid needing to compute it again). If unknown but needed +/// then a pointer to an empty string can be given to computed and stored the value here. Otherwise +/// (if omitted or nullptr) then the value will temporarily computed within the function. +ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); + /// Computes a 15-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 -/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). +/// +/// Can optionally also return the blinding factor, k, by providing a pointer to a uc32 (or +/// cleared_uc32); if non-nullptr then k will be written to it. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk); +std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk, uc32* k = nullptr); /// Computes a 25-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 -/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). +/// +/// Can optionally also return the blinding factor, k', by providing a pointer to a uc32 (or +/// cleared_uc32); if non-nullptr then k' will be written to it, where k' = ±k. Here, `k'` can be +/// negative to cancel out a negative in the true pubkey, which the remote client will always assume +/// is not present when it does a Session ID -> Ed25519 conversion for blinding purposes. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk); +std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). diff --git a/include/session/util.hpp b/include/session/util.hpp index 5cea9fa4..65ae5fb2 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -144,6 +145,15 @@ struct sodium_cleared : T { ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } }; +template +using cleared_array = sodium_cleared>; + +using uc32 = std::array; +using uc33 = std::array; +using uc64 = std::array; +using cleared_uc32 = cleared_array<32>; +using cleared_uc64 = cleared_array<64>; + // This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation // and freeing via libsodium. It is slower and heavier than a regular allocation type but takes // extra precautions, intended for storing sensitive values. diff --git a/src/blinding.cpp b/src/blinding.cpp index 7a179212..a3b8c46d 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -95,7 +95,7 @@ ustring blind15_id(ustring_view session_id, ustring_view server_pk) { return result; } -std::string blind15_id(std::string_view session_id, std::string_view server_pk) { +std::array blind15_id(std::string_view session_id, std::string_view server_pk) { if (session_id.size() != 66 || !oxenc::is_hex(session_id)) throw std::invalid_argument{"blind15_id: session_id must be hex (66 digits)"}; if (session_id[0] != '0' || session_id[1] != '5') @@ -110,7 +110,11 @@ std::string blind15_id(std::string_view session_id, std::string_view server_pk) uc33 blinded; blind15_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); - return oxenc::to_hex(blinded.begin(), blinded.end()); + std::array result; + result[0] = oxenc::to_hex(blinded.begin(), blinded.end()); + blinded.back() ^= 0x80; + result[1] = oxenc::to_hex(blinded.begin(), blinded.end()); + return result; } ustring blind25_id(ustring_view session_id, ustring_view server_pk) { @@ -147,7 +151,68 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) return oxenc::to_hex(blinded.begin(), blinded.end()); } -std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk) { +ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { + if (ed_pubkey.size() != 32) + throw std::invalid_argument{"blind15_id_from_ed: ed_pubkey must be 32 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_id_from_ed: server_pk must be 32 bytes"}; + if (session_id && !session_id->empty()) + throw std::invalid_argument{"blind15_id_from_ed: session_id pointer must be an empty string"}; + + if (session_id) { + session_id->resize(33); + session_id->front() = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) + throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; + } + + ustring result; + result.resize(33); + auto k = blind15_factor(server_pk); + if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pubkey.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + result[0] = 0x15; + return result; +} + +ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { + if (ed_pubkey.size() != 32) + throw std::invalid_argument{"blind25_id_from_ed: ed_pubkey must be 32 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"blind25_id_from_ed: server_pk must be 32 bytes"}; + if (session_id && session_id->size() != 0 && session_id->size() != 33) + throw std::invalid_argument{"blind25_id_from_ed: session_id pointer must be 0 or 33 bytes"}; + + ustring tmp_session_id; + if (!session_id) + session_id = &tmp_session_id; + if (session_id->size() == 0) { + session_id->resize(33); + session_id->front() = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) + throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; + } + + auto k = blind25_factor(*session_id, server_pk); + + ustring result; + result.resize(33); + // Blinded25 ids are always constructed using the absolute value of the ed pubkey, so if + // negative we need to clear the sign bit to make it positive before computing the blinded + // pubkey. + uc32 pos_ed_pubkey; + std::memcpy(pos_ed_pubkey.data(), ed_pubkey.data(), 32); + pos_ed_pubkey[31] &= 0x7f; + + if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), pos_ed_pubkey.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + result[0] = 0x25; + return result; +} + + +std::pair blind15_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -161,25 +226,29 @@ std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_vi if (server_pk.size() != 32) throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; - /// Generate the blinding factor - uc32 k = blind15_factor(server_pk); + std::pair result; + auto& [A, a] = result; + + /// Generate the blinding factor (storing into `*k`, if a pointer was provided) + uc32 k_tmp; + if (!k) + k = &k_tmp; + *k = blind15_factor(server_pk); /// Generate a scalar for the private key - uc32 x_sk; - if (0 != crypto_sign_ed25519_sk_to_curve25519(x_sk.data(), ed25519_sk.data())) + if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) throw std::runtime_error{ "blind15_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; // Turn a, A into their blinded versions - uc32 a; - uc32 A; - crypto_core_ed25519_scalar_mul(a.data(), k.data(), x_sk.data()); + crypto_core_ed25519_scalar_mul(a.data(), k->data(), a.data()); crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); - return {{A.data(), 32}, {a.data(), 32}}; + return result; } -std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk) { +std::pair blind25_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -193,41 +262,40 @@ std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_vi if (server_pk.size() != 32) throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; - ustring_view S{ed25519_sk.data() + 32, 32}; - uc33 session_id; session_id[0] = 0x05; if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) throw std::runtime_error{ - "blind25_sign: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; + "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; ustring_view X{session_id.data() + 1, 32}; - /// Generate the blinding factor - auto k = blind25_factor(X, {server_pk.data(), server_pk.size()}); + /// Generate the blinding factor (storing into `*k`, if a pointer was provided) + uc32 k_tmp; + if (!k_prime) + k_prime = &k_tmp; + *k_prime = blind25_factor(X, {server_pk.data(), server_pk.size()}); - /// Generate a scalar for the private key - uc32 x_sk; - if (0 != crypto_sign_ed25519_sk_to_curve25519(x_sk.data(), ed25519_sk.data())) + // For a negative pubkey we use k' = -k so that k'A == kA when A is positive, and k'A = -kA = + // k|A| when A is negative. + if (*(ed25519_sk.data() + 63) & 0x80) + crypto_core_ed25519_scalar_negate(k_prime->data(), k_prime->data()); + + std::pair result; + auto& [A, a] = result; + + // Generate the private key (scalar), a; (the sodium function naming here is misleading; this + // call actually has nothing to do with conversion to X25519, it just so happens that the + // conversion method is the easiest way to get `a` out of libsodium). + if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) throw std::runtime_error{ "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; // Turn a, A into their blinded versions - uc32 a; - uc32 A; - std::memcpy(A.data(), S.data(), 32); - if (S[31] & 0x80) { - // Ed25519 pubkey is negative, so we need to negate `z` to make things come out right - crypto_core_ed25519_scalar_negate(a.data(), x_sk.data()); - A[31] &= 0x7f; - } else - std::memcpy(a.data(), x_sk.data(), 32); - - // Turn a, A into their blinded versions - crypto_core_ed25519_scalar_mul(a.data(), k.data(), a.data()); + crypto_core_ed25519_scalar_mul(a.data(), k_prime->data(), a.data()); crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); - return {{A.data(), 32}, {a.data(), 32}}; + return result; } static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); @@ -251,34 +319,7 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust else throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; - ustring_view S{ed25519_sk.data() + 32, 32}; - - uc32 z; - crypto_sign_ed25519_sk_to_curve25519(z.data(), ed25519_sk.data()); - - uc33 session_id; - session_id[0] = 0x05; - if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) - throw std::runtime_error{ - "blind25_sign: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; - - ustring_view X{session_id.data() + 1, 32}; - - auto k = blind25_factor(X, {server_pk.data(), server_pk.size()}); - - uc32 a; - uc32 A; - std::memcpy(A.data(), S.data(), 32); - if (S[31] & 0x80) { - // Ed25519 pubkey is negative, so we need to negate `z` to make things come out right - crypto_core_ed25519_scalar_negate(a.data(), z.data()); - A[31] &= 0x7f; - } else - std::memcpy(a.data(), z.data(), 32); - - // Turn a, A into their blinded versions - crypto_core_ed25519_scalar_mul(a.data(), k.data(), a.data()); - crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + auto [A, a] = blind25_key_pair(ed25519_sk, to_sv(server_pk)); uc32 seedhash; crypto_generichash_blake2b( @@ -404,19 +445,10 @@ bool session_id_matches_blinded_id( ustring converted_blind_id1_raw; switch (blinded_id[0]) { - case '1': - converted_blind_id1 = blind15_id(session_id, server_pk); - - // For the negative, what we're going to get out of the above is simply the negative of - // converted_blind_id1, so flip the sign bit to get converted_blind_id2 - oxenc::from_hex( - converted_blind_id1.begin(), - converted_blind_id1.end(), - std::back_inserter(converted_blind_id1_raw)); - converted_blind_id1_raw[32] ^= 0x80; - converted_blind_id2 = oxenc::to_hex(converted_blind_id1_raw); - + case '1': { + auto [converted_blind_id1, converted_blind_id2] = blind15_id(session_id, server_pk); return (blinded_id == converted_blind_id1 || blinded_id == converted_blind_id2); + } // blind25 doesn't run into the negative issue that blind15 did case '2': return blinded_id == blind25_id(session_id, server_pk); @@ -502,4 +534,4 @@ LIBSESSION_C_API bool session_id_matches_blinded_id( } catch (...) { return false; } -} \ No newline at end of file +} diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 10cac8b3..8f01ae49 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -23,14 +23,9 @@ using namespace std::literals; namespace session { -template -using cleared_array = sodium_cleared>; - -using uc32 = std::array; -using uc33 = std::array; -using uc64 = std::array; -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; +// Version tag we prepend to encrypted-for-blinded-user messages. This is here so we can detect if +// some future version changes the format (and if not even try to load it). +inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; ustring sign_for_recipient( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { @@ -141,28 +136,108 @@ ustring encrypt_for_recipient_deterministic( return result; } +// Calculate the shared encryption key, sending from blinded sender kS (k = S's blinding factor) to +// blinded receiver jR (j = R's blinding factor). +// +// The sender knows s, k, S, and jR, but not j/R individually. +// The receiver knows r, j, R, and kS, but not k/S individually. +// +// From the sender's perspective, then, we compute: +// +// BLAKE2b(s k[jR] || kS || [jR]) +// +// The receiver can calulate the same value via: +// +// BLAKE2b(r j[kS] || [kS] || jR) +// +// (which will be the same because sR = rS, and so skjR = kjsR = kjrS = rjkS). +// +// For 15 blinding, however, the blinding factor depended only on the SOGS server pubkey, and so `j +// = k`, and so for *15* keys we don't do the double-blinding (i.e. the first terms above drop the +// double-blinding factors and become just sjR and rkS). +// +// Arguments. "A" and "B" here are either sender and receiver, or receiver and sender, depending on +// the value of `sending`. +// +// seed -- A's 32-byte secret key (can also be 64 bytes; only the first 32 are used). +// kA -- A's 33-byte blinded id, beginning with 0x15 or 0x25 +// jB -- A's 33-byte blinded id, beginning with 0x15 or 0x25 (must be the same prefix as kA). +// server_pk -- the server's pubkey (needed to compute A's `k` value) +// sending -- true if this for a message from A to B, false if this is from B to A. +static cleared_uc32 blinded_shared_secret( + ustring_view seed, ustring_view kA, ustring_view jB, ustring_view server_pk, bool sending) { + + // Because we're doing this generically, we use notation a/A/k for ourselves and b/jB for the + // other person; this notion keeps everything exactly as above *except* for the concatenation in + // the BLAKE2b hashed value: there we have to use kA || jB if we are the sender, but reverse the + // order to jB || kA if we are the receiver. + + std::pair blinded_key_pair; + cleared_uc32 k; + + if (seed.size() != 64 && seed.size() != 32) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; + if (kA.size() != 33) + throw std::invalid_argument{"Invalid local blinded id: expected 33 bytes"}; + if (jB.size() != 33) + throw std::invalid_argument{"Invalid remote blinded id: expected 33 bytes"}; + if (kA[0] == 0x15 && jB[0] == 0x15) + blinded_key_pair = blind15_key_pair(seed, server_pk, &k); + else if (kA[0] == 0x25 && jB[0] == 0x25) + blinded_key_pair = blind25_key_pair(seed, server_pk, &k); + else + throw std::invalid_argument{"Both ids must start with the same 0x15 or 0x25 prefix"}; + + bool blind25 = kA[0] == 0x25; + + kA.remove_prefix(1); + jB.remove_prefix(1); + + cleared_uc32 ka; + // Not really switching to x25519 here, this is just an easy way to compute `a` + crypto_sign_ed25519_sk_to_curve25519(ka.data(), seed.data()); + + if (blind25) + // Multiply a by k, so that we end up computing kajB = kjaB, which the other side can + // compute as jkbA. + crypto_core_ed25519_scalar_mul(ka.data(), ka.data(), k.data()); + // Else for 15 blinding we leave "ka" as just a, because j=k and so we don't need the + // double-blind. + + cleared_uc32 shared_secret; + if (0 != crypto_scalarmult_ed25519_noclamp(shared_secret.data(), ka.data(), jB.data())) + throw std::runtime_error{"Shared secret generation failed"}; + + auto& sender = sending ? kA : jB; + auto& recipient = sending ? jB : kA; + + // H(kjsR || kS || jR): + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, nullptr, 0, 32); + crypto_generichash_blake2b_update(&st, shared_secret.data(), shared_secret.size()); + crypto_generichash_blake2b_update(&st, sender.data(), sender.size()); + crypto_generichash_blake2b_update(&st, recipient.data(), recipient.size()); + crypto_generichash_blake2b_final(&st, shared_secret.data(), shared_secret.size()); + + return shared_secret; +} + ustring encrypt_for_blinded_recipient( ustring_view ed25519_privkey, ustring_view server_pk, ustring_view recipient_blinded_id, ustring_view message) { - if (ed25519_privkey.size() == 64) - ed25519_privkey.remove_suffix(32); - else if (ed25519_privkey.size() != 32) + if (ed25519_privkey.size() != 64 && ed25519_privkey.size() != 32) throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; if (server_pk.size() != 32) throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; if (recipient_blinded_id.size() != 33) throw std::invalid_argument{"Invalid recipient_blinded_id: expected 33 bytes"}; - uc32 ed_pk; - cleared_uc64 ed_sk_from_seed; - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - // Generate the blinded key pair & shared encryption key - std::pair blinded_key_pair; - + std::pair blinded_key_pair; switch (recipient_blinded_id[0]) { case 0x15: blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); break; @@ -172,41 +247,29 @@ ustring encrypt_for_blinded_recipient( throw std::invalid_argument{ "Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; } + ustring blinded_id; + blinded_id.reserve(33); + blinded_id += recipient_blinded_id[0]; + blinded_id.append(blinded_key_pair.first.begin(), blinded_key_pair.first.end()); - // Remove the blinding prefix - ustring kB = {recipient_blinded_id.data() + 1, 32}; - ustring kA = blinded_key_pair.first; - - // Calculate the shared encryption key, sending from A to B: - // - // BLAKE2b(a kB || kA || kB) - // - // The receiver can calulate the same value via: - // - // BLAKE2b(b kA || kA || kB) - // - // Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to - // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the - // same secret scalar secret (and so this is just the most convenient way to get 'a' out of - // a sodium Ed25519 secret key) - cleared_uc32 a, sharedSecret; - uc32 enc_key; - crypto_generichash_blake2b_state st; - crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_privkey.data()); - if (0 != crypto_scalarmult_ed25519_noclamp(sharedSecret.data(), a.data(), kB.data())) - throw std::runtime_error{"Shared secret generation failed"}; - - crypto_generichash_blake2b_init(&st, nullptr, 0, 32); - crypto_generichash_blake2b_update(&st, sharedSecret.data(), sharedSecret.size()); - crypto_generichash_blake2b_update(&st, kA.data(), kA.size()); - crypto_generichash_blake2b_update(&st, kB.data(), kB.size()); - crypto_generichash_blake2b_final(&st, enc_key.data(), enc_key.size()); + auto enc_key = blinded_shared_secret( + ed25519_privkey, blinded_id, recipient_blinded_id, server_pk, true); // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) ustring buf; buf.reserve(message.size() + 32); buf += message; - buf += ustring_view{ed_pk.data(), 32}; + + // append A (pubkey) + if (ed25519_privkey.size() == 64) { + buf += ed25519_privkey.substr(32); + } else { + cleared_uc64 ed_sk_from_seed; + uc32 ed_pk_buf; + crypto_sign_ed25519_seed_keypair( + ed_pk_buf.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + buf += to_sv(ed_pk_buf); + } // Encrypt using xchacha20-poly1305 cleared_array nonce; @@ -215,10 +278,15 @@ ustring encrypt_for_blinded_recipient( ustring ciphertext; unsigned long long outlen = 0; ciphertext.resize( - buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + + 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + // Prepend with a version byte, so that the recipient can reliably detect if a future version is + // no longer encrypting things the way it expects. + ciphertext[0] = BLINDED_ENCRYPT_VERSION; + if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data(), + ciphertext.data() + 1, &outlen, buf.data(), buf.size(), @@ -229,9 +297,9 @@ ustring encrypt_for_blinded_recipient( enc_key.data())) throw std::runtime_error{"Crypto aead encryption failed"}; - // data = b'\x00' + ciphertext + nonce - ciphertext.insert(ciphertext.begin(), 0); assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + // append the nonce, so that we have: data = b'\x00' + ciphertext + nonce std::memcpy(ciphertext.data() + (1 + outlen), nonce.data(), nonce.size()); return ciphertext; @@ -347,69 +415,33 @@ std::pair decrypt_from_blinded_recipient( ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; } else if (ed25519_privkey.size() != 64) throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - if (server_pk.size() != 32) - throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; - if (sender_id.size() != 33) - throw std::invalid_argument{"Invalid sender_id: expected 33 bytes"}; - if (recipient_id.size() != 33) - throw std::invalid_argument{"Invalid recipient_id: expected 33 bytes"}; if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; + auto dec_key = + blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); + std::pair result; auto& [buf, sender_session_id] = result; - // Determine whether it's an incoming or outgoing message - ustring_view kA = {sender_id.data() + 1, 32}; - ustring_view kB = {recipient_id.data() + 1, 32}; - std::pair blinded_key_pair; - - if (recipient_id[0] == 0x15 && sender_id[0] == 0x15) { - blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); - } else if (recipient_id[0] == 0x25 && sender_id[0] == 0x25) { - blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); - } else - throw std::invalid_argument{ - "Both sender_id and recipient_id must start with the same 0x15 or 0x25 prefix"}; - - // Calculate the shared encryption key, sending from A to B: - // - // BLAKE2b(a kB || kA || kB) - // - // The receiver can calulate the same value via: - // - // BLAKE2b(b kA || kA || kB) - // - // Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to - // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the - // same secret scalar secret (and so this is just the most convenient way to get 'a' out of - // a sodium Ed25519 secret key) - cleared_uc32 a, sharedSecret; - uc32 dec_key; - crypto_generichash_blake2b_state st; - ustring_view dst = (ustring{sender_id.data() + 1, 32} == blinded_key_pair.first ? kB : kA); - crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_privkey.data()); - if (0 != crypto_scalarmult_ed25519_noclamp(sharedSecret.data(), a.data(), dst.data())) - throw std::runtime_error{"Shared secret generation failed"}; - - crypto_generichash_blake2b_init(&st, nullptr, 0, 32); - crypto_generichash_blake2b_update(&st, sharedSecret.data(), sharedSecret.size()); - crypto_generichash_blake2b_update(&st, kA.data(), kA.size()); - crypto_generichash_blake2b_update(&st, kB.data(), kB.size()); - crypto_generichash_blake2b_final(&st, dec_key.data(), dec_key.size()); - // v, ct, nc = data[0], data[1:-24], data[-24:] - if (ciphertext[0] != 0) - throw std::invalid_argument{"Invalid ciphertext: version is not 0"}; + if (ciphertext[0] != BLINDED_ENCRYPT_VERSION) + throw std::invalid_argument{ + "Invalid ciphertext: version is not " + std::to_string(BLINDED_ENCRYPT_VERSION)}; ustring nonce; const size_t msg_size = (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - unsigned long long buf_len = 0; + + if (msg_size < 32) + throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; buf.resize(msg_size); + + unsigned long long buf_len = 0; + nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); std::memcpy( nonce.data(), @@ -421,48 +453,38 @@ std::pair decrypt_from_blinded_recipient( &buf_len, nullptr, ciphertext.data() + 1, - ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + msg_size + crypto_aead_xchacha20poly1305_ietf_ABYTES, nullptr, 0, nonce.data(), dec_key.data())) + throw std::invalid_argument{"Decryption failed"}; - // Ensure the length is correct - if (buf.size() <= 32) - throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; + assert(buf_len == buf.size()); // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key uc32 sender_ed_pk; std::memcpy(sender_ed_pk.data(), buf.data() + (buf.size() - 32), 32); // Convert the sender_ed_pk to the sender's session ID - std::array sender_x_pk; + uc32 sender_x_pk; if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + ustring session_id; // Gets populated by the following ..._from_ed calls + // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message - uc32 blindingFactor; - cleared_uc32 extracted_kA; - - if (sender_id[0] == 0x15) - blindingFactor = blind15_factor(server_pk); - else { - blindingFactor = blind25_factor({sender_x_pk.data(), 32}, server_pk); - - // sender_ed_pk is negative, so we need to negate `blindingFactor` to make things come out - // right - if (sender_ed_pk[31] & 0x80) { - uc32 blindingFactor_neg; - crypto_core_ed25519_scalar_negate(blindingFactor_neg.data(), blindingFactor.data()); - std::memcpy(blindingFactor.data(), blindingFactor_neg.data(), 32); - } + auto extracted_sender = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id) + : blinded15_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id); + + bool matched = sender_id == extracted_sender; + if (!matched && extracted_sender[0] == 0x15) { + // With 15-blinding we might need the negative instead: + extracted_sender[31] ^= 0x80; + matched = sender_id == extracted_sender; } - - if (0 != crypto_scalarmult_ed25519_noclamp( - extracted_kA.data(), blindingFactor.data(), sender_ed_pk.data())) - throw std::runtime_error{"Shared secret generation for verification failed"}; - if (kA != ustring_view{extracted_kA.data(), 32}) - throw std::runtime_error{"Shared secret does not match encoded public key"}; + if (!matched) + throw std::runtime_error{"Blinded sender id does not match the actual sender"}; // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix // to the sender session ID @@ -720,4 +742,4 @@ LIBSESSION_C_API bool session_decrypt_push_notification( } catch (...) { return false; } -} \ No newline at end of file +} diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index ad323446..0b347735 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -193,26 +193,13 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; - auto b15_1 = blind15_id(session_id1, server_pks[0]); - auto b15_2 = blind15_id(session_id1, server_pks[1]); - auto b15_3 = blind15_id(session_id2, server_pks[2]); - auto b15_4 = blind15_id(session_id2, server_pks[3]); - auto b15_5 = blind15_id(session_id2, server_pks[4]); - auto b15_6 = blind15_id(session_id1, server_pks[5]); - - // The `seed2` results in a negative Ed25519 pubkey which would result in an invalid signature - // since the sessionId always used the positive curve, as a result we need to flip the sign bit - // on the blinded id to ensure everything works nicely - ustring b15_3_raw, b15_4_raw, b15_5_raw; - oxenc::from_hex(b15_3.begin(), b15_3.end(), std::back_inserter(b15_3_raw)); - oxenc::from_hex(b15_4.begin(), b15_4.end(), std::back_inserter(b15_4_raw)); - oxenc::from_hex(b15_5.begin(), b15_5.end(), std::back_inserter(b15_5_raw)); - b15_3_raw[32] ^= 0x80; - b15_4_raw[32] ^= 0x80; - b15_5_raw[32] ^= 0x80; - b15_3 = oxenc::to_hex(b15_3_raw); - b15_4 = oxenc::to_hex(b15_4_raw); - b15_5 = oxenc::to_hex(b15_5_raw); + auto b15_1 = blind15_id(session_id1, server_pks[0])[0]; + auto b15_2 = blind15_id(session_id1, server_pks[1])[0]; + // session_id2 has a negative pubkey, so these next three need the negative [1] instead: + auto b15_3 = blind15_id(session_id2, server_pks[2])[1]; + auto b15_4 = blind15_id(session_id2, server_pks[3])[1]; + auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; + auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; auto sig1 = blind15_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); CHECK(oxenc::to_hex(sig1) == @@ -292,12 +279,12 @@ TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; - auto b15_1 = blind15_id(session_id1, server_pks[0]); - auto b15_2 = blind15_id(session_id1, server_pks[1]); - auto b15_3 = blind15_id(session_id2, server_pks[2]); - auto b15_4 = blind15_id(session_id2, server_pks[3]); - auto b15_5 = blind15_id(session_id2, server_pks[4]); - auto b15_6 = blind15_id(session_id1, server_pks[5]); + auto b15_1 = blind15_id(session_id1, server_pks[0])[0]; + auto b15_2 = blind15_id(session_id1, server_pks[1])[0]; + auto b15_3 = blind15_id(session_id2, server_pks[2])[1]; + auto b15_4 = blind15_id(session_id2, server_pks[3])[1]; + auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; + auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; auto b25_1 = blind25_id(session_id1, server_pks[0]); auto b25_2 = blind25_id(session_id1, server_pks[1]); auto b25_3 = blind25_id(session_id2, server_pks[2]); diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 9b549e70..3490e139 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -140,6 +140,13 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry CHECK(from_unsigned_sv(msg) == "hello"); } +static std::array prefixed(unsigned char prefix, const session::uc32& pubkey) { + std::array result; + result[0] = prefix; + std::memcpy(result.data() + 1, pubkey.data(), 32); + return result; +} + TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][encrypt]") { using namespace session; @@ -163,11 +170,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); auto [blind15_pk, blind15_sk] = blind15_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); auto [blind25_pk, blind25_sk] = blind25_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); - std::array blind15_pk_prefixed, blind25_pk_prefixed; - blind15_pk_prefixed[0] = 0x15; - blind25_pk_prefixed[0] = 0x25; - memcpy(blind15_pk_prefixed.data() + 1, blind15_pk.data(), 32); - memcpy(blind25_pk_prefixed.data() + 1, blind25_pk.data(), 32); + auto blind15_pk_prefixed = prefixed(0x15, blind15_pk); + auto blind25_pk_prefixed = prefixed(0x25, blind25_pk); const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; std::array ed_pk2, curve_pk2; @@ -186,11 +190,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); - std::array blind15_pk2_prefixed, blind25_pk2_prefixed; - blind15_pk2_prefixed[0] = 0x15; - blind25_pk2_prefixed[0] = 0x25; - memcpy(blind15_pk2_prefixed.data() + 1, blind15_pk2.data(), 32); - memcpy(blind25_pk2_prefixed.data() + 1, blind25_pk2.data(), 32); + auto blind15_pk2_prefixed = prefixed(0x15, blind15_pk2); + auto blind25_pk2_prefixed = prefixed(0x25, blind25_pk2); SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( @@ -203,14 +204,14 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK_THROWS(decrypt_from_blinded_recipient( to_sv(ed_sk2), to_unsigned_sv(server_pk), - blind15_pk, + to_sv(blind15_pk), {blind15_pk2_prefixed.data(), 33}, enc)); CHECK_THROWS(decrypt_from_blinded_recipient( to_sv(ed_sk2), to_unsigned_sv(server_pk), {blind15_pk_prefixed.data(), 33}, - blind15_pk2, + to_sv(blind15_pk2), enc)); auto [msg, sender] = decrypt_from_blinded_recipient( @@ -231,45 +232,6 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e {blind15_pk2_prefixed.data(), 33}, broken)); } - SECTION("blind15, full secret, sender decrypt") { - auto enc = encrypt_for_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind15_pk2_prefixed.data(), 33}, - to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); - - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - blind15_pk, - {blind15_pk2_prefixed.data(), 33}, - enc)); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, - blind15_pk2, - enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, - enc); - CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == "hello"); - - auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, - broken)); - } SECTION("blind15, only seed, recipient decrypt") { constexpr auto lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " @@ -314,14 +276,14 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK_THROWS(decrypt_from_blinded_recipient( to_sv(ed_sk2), to_unsigned_sv(server_pk), - blind25_pk, + to_sv(blind25_pk), {blind25_pk2_prefixed.data(), 33}, enc)); CHECK_THROWS(decrypt_from_blinded_recipient( to_sv(ed_sk2), to_unsigned_sv(server_pk), {blind25_pk_prefixed.data(), 33}, - blind25_pk2, + to_sv(blind25_pk2), enc)); auto [msg, sender] = decrypt_from_blinded_recipient( @@ -342,45 +304,6 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e {blind25_pk2_prefixed.data(), 33}, broken)); } - SECTION("blind25, full secret, sender decrypt") { - auto enc = encrypt_for_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind25_pk2_prefixed.data(), 33}, - to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); - - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - blind25_pk, - {blind25_pk2_prefixed.data(), 33}, - enc)); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, - blind25_pk2, - enc)); - - auto [msg, sender] = decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, - enc); - CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == "hello"); - - auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce - CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, - broken)); - } SECTION("blind25, only seed, recipient decrypt") { constexpr auto lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " From 1d16662aa7989ce67b94ebbfa2cd168ccd124be4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 10 Jan 2024 13:27:44 +1100 Subject: [PATCH 142/572] Updated decrypt_from_blinded_recipient to support outbox messages --- src/session_encrypt.cpp | 18 ++++++--- tests/test_session_encrypt.cpp | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 8f01ae49..9bae08e8 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -407,21 +407,29 @@ std::pair decrypt_from_blinded_recipient( ustring_view sender_id, ustring_view recipient_id, ustring_view ciphertext) { + uc32 ed_pk_from_seed; cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { - uc32 ignore_pk; crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed_pk_from_seed.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - } else if (ed25519_privkey.size() != 64) + } else if (ed25519_privkey.size() == 64) + std::memcpy(ed_pk_from_seed.data(), ed25519_privkey.data() + 32, 32); + else throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; - auto dec_key = - blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); + cleared_uc32 dec_key; + auto blinded_id = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_sv(ed_pk_from_seed), server_pk) + : blinded15_id_from_ed(to_sv(ed_pk_from_seed), server_pk); + + if (sender_id == blinded_id) + dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); + else + dec_key = blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); std::pair result; auto& [buf, sender_session_id] = result; diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 3490e139..8c286b69 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -232,6 +232,39 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e {blind15_pk2_prefixed.data(), 33}, broken)); } + SECTION("blind15, only seed, sender decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); + } SECTION("blind15, only seed, recipient decrypt") { constexpr auto lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " @@ -265,6 +298,45 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e {blind15_pk2_prefixed.data(), 33}, broken)); } + SECTION("blind25, full secret, sender decrypt") { + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + to_sv(blind25_pk), + {blind25_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + to_sv(blind25_pk2), + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); + } SECTION("blind25, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( to_sv(ed_sk), From 0be33f018555b5c2228cd84eb96e1f2eab231a48 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 10 Jan 2024 13:40:30 +1100 Subject: [PATCH 143/572] Updated comment and formatting --- include/session/blinding.hpp | 12 ++++++++---- include/session/session_encrypt.hpp | 4 +++- src/blinding.cpp | 6 +++--- src/session_encrypt.cpp | 13 ++++++++----- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 7374d9b7..c6e41cf5 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -94,7 +94,8 @@ ustring blind25_id(ustring_view session_id, ustring_view server_pk); /// `blinded25_id_from_ed`, but unlike the 25 version, this value is not read if non-empty, and is /// not an optimization (that is: it is purely for convenience and is no more efficient to use this /// than it is to compute it yourself). -ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); +ustring blinded15_id_from_ed( + ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); /// Computes the 25-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 /// pubkey behind a (X25519) Session ID. This will be the same as blind25_id (if given the X25519 @@ -106,7 +107,8 @@ ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust /// containing the precomputed value (to avoid needing to compute it again). If unknown but needed /// then a pointer to an empty string can be given to computed and stored the value here. Otherwise /// (if omitted or nullptr) then the value will temporarily computed within the function. -ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); +ustring blinded25_id_from_ed( + ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); /// Computes a 15-blinded key pair. /// @@ -118,7 +120,8 @@ ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_view server_pk, uc32* k = nullptr); +std::pair blind15_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k = nullptr); /// Computes a 25-blinded key pair. /// @@ -132,7 +135,8 @@ std::pair blind15_key_pair(ustring_view ed25519_sk, ustring_ /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind25_key_pair(ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); +std::pair blind25_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index c51b823f..6a185a73 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -201,7 +201,9 @@ std::pair decrypt_incoming_session_id( /// API: crypto/decrypt_from_blinded_recipient /// -/// This function attempts to decrypt a message using the SessionBlindingProtocol. +/// This function attempts to decrypt a message using the SessionBlindingProtocol. If the +/// `sender_id` matches the `blinded_id` generated from the `ed25519_privkey` this function assumes +/// the `ciphertext` is an outgoing message and decrypts it as such. /// /// Inputs: /// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a diff --git a/src/blinding.cpp b/src/blinding.cpp index a3b8c46d..a9892fe8 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -157,7 +157,8 @@ ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust if (server_pk.size() != 32) throw std::invalid_argument{"blind15_id_from_ed: server_pk must be 32 bytes"}; if (session_id && !session_id->empty()) - throw std::invalid_argument{"blind15_id_from_ed: session_id pointer must be an empty string"}; + throw std::invalid_argument{ + "blind15_id_from_ed: session_id pointer must be an empty string"}; if (session_id) { session_id->resize(33); @@ -205,12 +206,11 @@ ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust pos_ed_pubkey[31] &= 0x7f; if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), pos_ed_pubkey.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; result[0] = 0x25; return result; } - std::pair blind15_key_pair( ustring_view ed25519_sk, ustring_view server_pk, uc32* k) { std::array ed_sk_tmp; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 9bae08e8..88bc4e7a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -423,8 +423,9 @@ std::pair decrypt_from_blinded_recipient( "Invalid ciphertext: too short to contain valid encrypted data"}; cleared_uc32 dec_key; - auto blinded_id = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_sv(ed_pk_from_seed), server_pk) - : blinded15_id_from_ed(to_sv(ed_pk_from_seed), server_pk); + auto blinded_id = recipient_id[0] == 0x25 + ? blinded25_id_from_ed(to_sv(ed_pk_from_seed), server_pk) + : blinded15_id_from_ed(to_sv(ed_pk_from_seed), server_pk); if (sender_id == blinded_id) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); @@ -479,11 +480,13 @@ std::pair decrypt_from_blinded_recipient( if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - ustring session_id; // Gets populated by the following ..._from_ed calls + ustring session_id; // Gets populated by the following ..._from_ed calls // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message - auto extracted_sender = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id) - : blinded15_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id); + auto extracted_sender = + recipient_id[0] == 0x25 + ? blinded25_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id) + : blinded15_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id); bool matched = sender_id == extracted_sender; if (!matched && extracted_sender[0] == 0x15) { From 2d02be101d4f5f56372fb690d9fcdea98c00bfa6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 17 Jan 2024 13:41:27 -0400 Subject: [PATCH 144/572] ConvoInfoVolatile: fix new group iteration The all-convo iterators were not properly using the new groups iterators. This fixes it, and DRYs the nearly-identical iterator handling code a bit. --- .../session/config/convo_info_volatile.hpp | 11 +-- src/config/convo_info_volatile.cpp | 84 ++++++++++--------- tests/test_config_convo_info_volatile.cpp | 38 +++++---- 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 24c3605c..b9884e68 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -21,6 +21,7 @@ struct convo_info_volatile_legacy_group; namespace session::config { class ConvoInfoVolatile; +class val_loader; /// keys used in this config, either currently or in the past (so that we don't reuse): /// @@ -63,6 +64,8 @@ namespace convo { protected: void load(const dict& info_dict); + friend class session::config::val_loader; + friend class session::config::ConvoInfoVolatile; }; struct one_to_one : base { @@ -87,8 +90,6 @@ namespace convo { // Internal ctor/method for C API implementations: one_to_one(const struct convo_info_volatile_1to1& c); // From c struct void into(convo_info_volatile_1to1& c) const; // Into c struct - - friend class session::config::ConvoInfoVolatile; }; struct community : config::community, base { @@ -123,9 +124,6 @@ namespace convo { // Internal ctor/method for C API implementations: group(const struct convo_info_volatile_group& c); // From c struct void into(convo_info_volatile_group& c) const; // Into c struct - - private: - friend class session::config::ConvoInfoVolatile; }; struct legacy_group : base { @@ -149,9 +147,6 @@ namespace convo { // Internal ctor/method for C API implementations: legacy_group(const struct convo_info_volatile_legacy_group& c); // From c struct void into(convo_info_volatile_legacy_group& c) const; // Into c struct - - private: - friend class session::config::ConvoInfoVolatile; }; using any = std::variant; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 742549c5..421a8666 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -393,34 +393,51 @@ ConvoInfoVolatile::iterator::iterator( _load_val(); } +class val_loader { + public: + template + static bool load( + std::shared_ptr& val, + std::optional& it, + std::optional& end, + char prefix) { + while (it) { + if (*it == *end) { + it.reset(); + end.reset(); + return false; + } + + auto& [k, v] = **it; + + if (k.size() == 33 && k[0] == prefix) { + if (auto* info_dict = std::get_if(&v)) { + val = std::make_shared(ConvoType{oxenc::to_hex(k)}); + std::get(*val).load(*info_dict); + return true; + } + } + ++*it; + } + return false; + } +}; + /// 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). We also span across three different iterators: first -/// we exhaust _it_11, then _it_comm, then _it_lgroup. +/// find one that is valid (or hit the end). We also span across four different iterators: we +/// exhaust, in order: _it_11, _it_group, _it_comm, _it_lgroup. /// /// We *always* call this after incrementing the iterator (and after iterator initialization), and -/// this is responsible for making sure that _it_11, _it_comm, etc. are only set to non-nullopt if +/// this is responsible for making sure that _it_11, _it_group, etc. are only set to non-nullopt if /// the respective sub-iterator is *not* at the end (and resetting them when we hit the end). Thus, /// after calling this, our "end" condition will be simply that all of the three iterators are /// nullopt. void ConvoInfoVolatile::iterator::_load_val() { - while (_it_11) { - if (*_it_11 == *_end_11) { - _it_11.reset(); - _end_11.reset(); - break; - } - - auto& [k, v] = **_it_11; + if (val_loader::load(_val, _it_11, _end_11, 0x05)) + return; - if (k.size() == 33 && k[0] == 0x05) { - if (auto* info_dict = std::get_if(&v)) { - _val = std::make_shared(convo::one_to_one{oxenc::to_hex(k)}); - std::get(*_val).load(*info_dict); - return; - } - } - ++*_it_11; - } + if (val_loader::load(_val, _it_group, _end_group, 0x03)) + return; if (_it_comm) { if (_it_comm->load(_val)) @@ -429,37 +446,24 @@ void ConvoInfoVolatile::iterator::_load_val() { _it_comm.reset(); } - while (_it_lgroup) { - if (*_it_lgroup == *_end_lgroup) { - _it_lgroup.reset(); - _end_lgroup.reset(); - break; - } - - auto& [k, v] = **_it_lgroup; - - if (k.size() == 33 && k[0] == 0x05) { - if (auto* info_dict = std::get_if(&v)) { - _val = std::make_shared(convo::legacy_group{oxenc::to_hex(k)}); - std::get(*_val).load(*info_dict); - return; - } - } - ++*_it_lgroup; - } + if (val_loader::load(_val, _it_lgroup, _end_lgroup, 0x05)) + return; } bool ConvoInfoVolatile::iterator::operator==(const iterator& other) const { - return _it_11 == other._it_11 && _it_comm == other._it_comm && _it_lgroup == other._it_lgroup; + return _it_11 == other._it_11 && _it_group == other._it_group && _it_comm == other._it_comm && + _it_lgroup == other._it_lgroup; } bool ConvoInfoVolatile::iterator::done() const { - return !_it_11 && (!_it_comm || _it_comm->done()) && !_it_lgroup; + return !_it_11 && !_it_group && (!_it_comm || _it_comm->done()) && !_it_lgroup; } ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { if (_it_11) ++*_it_11; + else if (_it_group) + ++*_it_group; else if (_it_comm && !_it_comm->done()) _it_comm->advance(); else { diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 0f1698e6..85395a5a 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -155,10 +155,19 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(std::get(convos.push()) == seqno); using session::config::convo::community; + using session::config::convo::group; using session::config::convo::legacy_group; using session::config::convo::one_to_one; - std::vector seen; + std::vector seen, expected; + for (const auto& e : + {"1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", + "gr: 030111101001001000101010011011010010101010111010000110100001210000", + "comm: http://example.org:5678/r/sudokuroom", + "lgr: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}) + expected.emplace_back(e); + for (auto* conv : {&convos, &convos2}) { // Iterate through and make sure we got everything we expected seen.clear(); @@ -166,26 +175,23 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(conv->size_1to1() == 2); CHECK(conv->size_communities() == 1); CHECK(conv->size_legacy_groups() == 1); + CHECK(conv->size_groups() == 1); CHECK_FALSE(conv->empty()); for (const auto& convo : *conv) { if (auto* c = std::get_if(&convo)) seen.push_back("1-to-1: "s + c->session_id); + else if (auto* c = std::get_if(&convo)) + seen.push_back("gr: " + c->id); else if (auto* c = std::get_if(&convo)) seen.push_back( - "og: " + std::string{c->base_url()} + "/r/" + std::string{c->room()}); + "comm: " + std::string{c->base_url()} + "/r/" + std::string{c->room()}); else if (auto* c = std::get_if(&convo)) - seen.push_back("cl: " + c->id); + seen.push_back("lgr: " + c->id); + else + seen.push_back("unknown convo type!"); } - CHECK(seen == std::vector{ - {"1-to-1: " - "051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: " - "055000000000000000000000000000000000000000000000000000000000000000", - "og: http://example.org:5678/r/sudokuroom", - "cl: " - "05ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - "c"}}); + CHECK(seen == expected); } CHECK_FALSE(convos.needs_push()); @@ -388,9 +394,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { if (convo_info_volatile_it_is_1to1(it, &c1)) { seen.push_back("1-to-1: "s + c1.session_id); } else if (convo_info_volatile_it_is_community(it, &c2)) { - seen.push_back("og: "s + c2.base_url + "/r/" + c2.room); + seen.push_back("comm: "s + c2.base_url + "/r/" + c2.room); } else if (convo_info_volatile_it_is_legacy_group(it, &c3)) { - seen.push_back("cl: "s + c3.group_id); + seen.push_back("lgr: "s + c3.group_id); } } convo_info_volatile_iterator_free(it); @@ -400,8 +406,8 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "051111111111111111111111111111111111111111111111111111111111111111", "1-to-1: " "055000000000000000000000000000000000000000000000000000000000000000", - "og: http://example.org:5678/r/sudokuroom", - "cl: " + "comm: http://example.org:5678/r/sudokuroom", + "lgr: " "05ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" "c"}}); } From 71380a1df0b98f2a29fb432cb042dd1e50346088 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 24 Jan 2024 15:57:59 -0400 Subject: [PATCH 145/572] Fix Android build logic Modern cmake + modern android versions let us be a bit less hacky with getting things for the Android build system, so do that. This should also fix compilation for Android on macos. This also fixes it to properly use ccache for android builds (previously the ccache prefix on the compiler was set too early and being obliterated under Android). --- cmake/StaticBuild.cmake | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index c677c0a3..6435e5f0 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -32,12 +32,6 @@ target_include_directories(libsession-external-libs SYSTEM BEFORE INTERFACE ${DE set(deps_cc "${CMAKE_C_COMPILER}") set(deps_cxx "${CMAKE_CXX_COMPILER}") -if(CMAKE_C_COMPILER_LAUNCHER) - set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") -endif() -if(CMAKE_CXX_COMPILER_LAUNCHER) - set(deps_cxx "${CMAKE_CXX_COMPILER_LAUNCHER} ${deps_cxx}") -endif() function(expand_urls output source_file) @@ -76,48 +70,51 @@ if(CMAKE_CROSSCOMPILING) endif() if(ANDROID) set(android_toolchain_suffix linux-android) - set(android_compiler_suffix linux-android23) + set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) - set(android_machine x86_64) set(cross_host "--host=x86_64-linux-android") set(android_compiler_prefix x86_64) - set(android_compiler_suffix linux-android23) + set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) set(android_toolchain_prefix x86_64) set(android_toolchain_suffix linux-android) elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86) - set(android_machine x86) set(cross_host "--host=i686-linux-android") set(android_compiler_prefix i686) - set(android_compiler_suffix linux-android23) + set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) set(android_toolchain_prefix i686) set(android_toolchain_suffix linux-android) elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a) - set(android_machine arm) set(cross_host "--host=armv7a-linux-androideabi") set(android_compiler_prefix armv7a) - set(android_compiler_suffix linux-androideabi23) + set(android_compiler_suffix linux-androideabi${ANDROID_PLATFORM_LEVEL}) set(android_toolchain_prefix arm) set(android_toolchain_suffix linux-androideabi) elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a) - set(android_machine arm64) set(cross_host "--host=aarch64-linux-android") set(android_compiler_prefix aarch64) - set(android_compiler_suffix linux-android23) + set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) set(android_toolchain_prefix aarch64) set(android_toolchain_suffix linux-android) else() message(FATAL_ERROR "unknown android arch: ${CMAKE_ANDROID_ARCH_ABI}") endif() - set(deps_cc "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang") - set(deps_cxx "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang++") - set(deps_ld "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_toolchain_suffix}-ld") - set(deps_ranlib "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ranlib") - set(deps_ar "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ar") + set(deps_cc "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang") + set(deps_cxx "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang++") + set(deps_ld "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_toolchain_suffix}-ld") + set(deps_ranlib "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ranlib") + set(deps_ar "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ar") endif() set(deps_CFLAGS "-O2") set(deps_CXXFLAGS "-O2") +if(CMAKE_C_COMPILER_LAUNCHER) + set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") +endif() +if(CMAKE_CXX_COMPILER_LAUNCHER) + set(deps_cxx "${CMAKE_CXX_COMPILER_LAUNCHER} ${deps_cxx}") +endif() + if(WITH_LTO) set(deps_CFLAGS "${deps_CFLAGS} -flto") endif() From 41ad0e16d8e5ac8db91abaa8d56d4800811fec3f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 25 Jan 2024 12:04:28 +1100 Subject: [PATCH 146/572] Fixed a ResponseParser bug and fallback to AES_GCM for legacy PN server --- src/onionreq/response_parser.cpp | 34 ++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index cfcc1dc3..c51de231 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -24,9 +24,18 @@ ResponseParser::ResponseParser(session::onionreq::Builder builder) { } ustring ResponseParser::decrypt(ustring ciphertext) const { - HopEncryption d{x25519_keypair_.second, x25519_keypair_.first}; + HopEncryption d{x25519_keypair_.second, x25519_keypair_.first, false}; - return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this workaround can be removed once the legacy PN server is removed + try { + return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); + } catch (const std::exception& e) { + if (enc_type_ == session::onionreq::EncryptType::xchacha20) + return d.decrypt(session::onionreq::EncryptType::aes_gcm, ciphertext, + destination_x25519_public_key_); + else + throw e; + } } } // namespace session::onionreq @@ -68,10 +77,23 @@ LIBSESSION_C_API bool onion_request_decrypt( session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), false}; - auto result = d.decrypt( - enc_type, - ustring{ciphertext, ciphertext_len}, - session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + ustring result; + + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this workaround can be removed once the legacy PN server is removed + try { + result = d.decrypt( + enc_type, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + } catch (...) { + if (enc_type == session::onionreq::EncryptType::xchacha20) + result = d.decrypt( + session::onionreq::EncryptType::aes_gcm, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + else + return false; + } *plaintext_out = static_cast(malloc(result.size())); *plaintext_out_len = result.size(); From c7b7dea1a8610285488fe93e0911e52a081573b6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 25 Jan 2024 12:29:28 +1100 Subject: [PATCH 147/572] Formatting --- src/onionreq/response_parser.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index c51de231..2ace3237 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -26,13 +26,17 @@ ResponseParser::ResponseParser(session::onionreq::Builder builder) { ustring ResponseParser::decrypt(ustring ciphertext) const { HopEncryption d{x25519_keypair_.second, x25519_keypair_.first, false}; - // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this workaround can be removed once the legacy PN server is removed + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an + // error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this + // workaround can be removed once the legacy PN server is removed try { return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); } catch (const std::exception& e) { if (enc_type_ == session::onionreq::EncryptType::xchacha20) - return d.decrypt(session::onionreq::EncryptType::aes_gcm, ciphertext, - destination_x25519_public_key_); + return d.decrypt( + session::onionreq::EncryptType::aes_gcm, + ciphertext, + destination_x25519_public_key_); else throw e; } @@ -79,7 +83,9 @@ LIBSESSION_C_API bool onion_request_decrypt( ustring result; - // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this workaround can be removed once the legacy PN server is removed + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an + // error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this + // workaround can be removed once the legacy PN server is removed try { result = d.decrypt( enc_type, @@ -90,7 +96,8 @@ LIBSESSION_C_API bool onion_request_decrypt( result = d.decrypt( session::onionreq::EncryptType::aes_gcm, ustring{ciphertext, ciphertext_len}, - session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + session::onionreq::x25519_pubkey::from_bytes( + {destination_x25519_pubkey, 32})); else return false; } From b1841b7fb093040330501014cff1c7283a2eaeb0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 28 Feb 2024 13:25:21 +1100 Subject: [PATCH 148/572] Added missing 'mute_until' setting between C/C++ contact types --- src/config/contacts.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index fa61e1ed..385b1ea0 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -131,6 +131,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) @@ -153,6 +154,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) From 826972eb974e38e395c329b1cc4fc59221055034 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 4 Mar 2024 12:12:24 +1100 Subject: [PATCH 149/572] Made length checks in explicit setters throw, truncating contact name --- src/config/contacts.cpp | 2 +- src/config/groups/info.cpp | 4 ++-- src/config/user_profile.cpp | 3 +++ tests/test_group_keys.cpp | 16 ++++++++-------- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index fa61e1ed..ec3fd940 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -41,7 +41,7 @@ contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { void contact_info::set_name(std::string n) { if (n.size() > MAX_NAME_LENGTH) - throw std::invalid_argument{"Invalid contact name: exceeds maximum length"}; + n = n.substr(0, MAX_NAME_LENGTH); name = std::move(n); } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 88b3a1eb..b7c0d2c1 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -31,7 +31,7 @@ std::optional Info::get_name() const { void Info::set_name(std::string_view new_name) { if (new_name.size() > NAME_MAX_LENGTH) - new_name = new_name.substr(0, NAME_MAX_LENGTH); + throw std::invalid_argument{"Invalid group name: exceeds maximum length"}; set_nonempty_str(data["n"], new_name); } @@ -43,7 +43,7 @@ std::optional Info::get_description() const { void Info::set_description(std::string_view new_desc) { if (new_desc.size() > DESCRIPTION_MAX_LENGTH) - new_desc = new_desc.substr(0, DESCRIPTION_MAX_LENGTH); + throw std::invalid_argument{"Invalid group description: exceeds maximum length"}; set_nonempty_str(data["o"], new_desc); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 1c5a3df2..d226eef7 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -4,6 +4,7 @@ #include "internal.hpp" #include "session/config/error.h" +#include "session/config/contacts.hpp" #include "session/config/user_profile.hpp" #include "session/export.h" #include "session/types.hpp" @@ -39,6 +40,8 @@ LIBSESSION_C_API const char* user_profile_get_name(const config_object* conf) { } void UserProfile::set_name(std::string_view new_name) { + if (new_name.size() > contact_info::MAX_NAME_LENGTH) + throw std::invalid_argument{"Invalid profile name: exceeds maximum length"}; set_nonempty_str(data["n"], new_name); } LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 475ba217..26f801e6 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -538,21 +538,21 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { admin1.info.dump(), admin1.members.dump(), admin1.keys.dump()}; - admin1b.info.set_name( + admin1b.info.set_name("Test New Name"); + admin1b.info.set_description("Test New Desc"); + CHECK_THROWS(admin1b.info.set_name( "Test New Name Really long " - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); - admin1b.info.set_description(std::string(2050, 'z')); + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")); + CHECK_THROWS(admin1b.info.set_description(std::string(2050, 'z'))); CHECK_NOTHROW(admin1b.info.push()); admin1b.members.set( admin1b.members.get_or_construct("05124076571076017981235497801235098712093870981273590" "8746387172343")); CHECK_NOTHROW(admin1b.members.push()); - // Test truncation - CHECK(admin1b.info.get_name() == - "Test New Name Really long " - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv"); - CHECK(admin1b.info.get_description() == std::string(2000, 'z')); + // Test values weren't overrided + CHECK(admin1b.info.get_name() == "Test New Name"); + CHECK(admin1b.info.get_description() == "Test New Desc"); } TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { From fb7565fe7e4064ae94638f87ebb9870803d49e97 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Mar 2024 09:52:56 +1100 Subject: [PATCH 150/572] Formatting --- src/config/user_profile.cpp | 2 +- tests/test_group_keys.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index d226eef7..baa3219c 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -3,8 +3,8 @@ #include #include "internal.hpp" -#include "session/config/error.h" #include "session/config/contacts.hpp" +#include "session/config/error.h" #include "session/config/user_profile.hpp" #include "session/export.h" #include "session/types.hpp" diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 26f801e6..1b84c223 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -540,9 +540,10 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { admin1.keys.dump()}; admin1b.info.set_name("Test New Name"); admin1b.info.set_description("Test New Desc"); - CHECK_THROWS(admin1b.info.set_name( - "Test New Name Really long " - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")); + CHECK_THROWS( + admin1b.info.set_name("Test New Name Really long " + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl" + "mnopqrstuvwxyz")); CHECK_THROWS(admin1b.info.set_description(std::string(2050, 'z'))); CHECK_NOTHROW(admin1b.info.push()); admin1b.members.set( From 92190246e5bbf5c60a901f86e4f28431fe3a791d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Mar 2024 11:11:23 +1100 Subject: [PATCH 151/572] [WIP] Started working on libQuic functions --- .gitignore | 3 +- .gitmodules | 3 ++ CMakeLists.txt | 6 +-- cmake/StaticBuild.cmake | 41 --------------- external/CMakeLists.txt | 7 +-- external/oxen-encoding | 2 +- external/oxen-libquic | 1 + include/session/network.h | 30 +++++++++++ include/session/network.hpp | 16 ++++++ src/CMakeLists.txt | 38 +++++++------- src/network.cpp | 101 ++++++++++++++++++++++++++++++++++++ utils/ios.sh | 3 +- 12 files changed, 177 insertions(+), 74 deletions(-) create mode 160000 external/oxen-libquic create mode 100644 include/session/network.h create mode 100644 include/session/network.hpp create mode 100644 src/network.cpp diff --git a/.gitignore b/.gitignore index f13ebfd0..d3ec1829 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/.gitmodules b/.gitmodules index e8d2e5df..536aad48 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "external/nlohmann-json"] path = external/nlohmann-json url = https://github.com/nlohmann/json.git +[submodule "external/oxen-libquic"] + path = external/oxen-libquic + url = https://github.com/oxen-io/oxen-libquic.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 1026503a..249a06a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.14...3.23) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Has to be set before `project()`, and ignored on non-macos: -set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13 CACHE STRING "macOS deployment target (Apple clang only)") +set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "macOS deployment target (Apple clang only)") set(LANGS C CXX) find_program(CCACHE_PROGRAM ccache) @@ -16,7 +16,6 @@ if(CCACHE_PROGRAM) endforeach() endif() - project(libsession-util VERSION 1.2.0 DESCRIPTION "Session client utility library" @@ -81,9 +80,6 @@ option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd} option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) -# Provide this as an option for now because GMP and iOS are sometimes unhappy with each other. -option(ENABLE_ONIONREQ "Build with onion request functionality" ON) - if(USE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 6435e5f0..49863f8c 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,21 +5,6 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") -set(GMP_VERSION 6.3.0 CACHE STRING "gmp version") -set(GMP_MIRROR ${LOCAL_MIRROR} https://gmplib.org/download/gmp - CACHE STRING "gmp mirror(s)") -set(GMP_SOURCE gmp-${GMP_VERSION}.tar.xz) -set(GMP_HASH SHA512=e85a0dab5195889948a3462189f0e0598d331d3457612e2d3350799dba2e244316d256f8161df5219538eb003e4b5343f989aaa00f96321559063ed8c8f29fd2 - CACHE STRING "gmp source hash") - -set(NETTLE_VERSION 3.9.1 CACHE STRING "nettle version") -set(NETTLE_MIRROR ${LOCAL_MIRROR} https://ftp.gnu.org/gnu/nettle - CACHE STRING "nettle mirror(s)") -set(NETTLE_SOURCE nettle-${NETTLE_VERSION}.tar.gz) -set(NETTLE_HASH SHA512=5939c4b43cf9ff6c6272245b85f123c81f8f4e37089fa4f39a00a570016d837f6e706a33226e4bbfc531b02a55b2756ff312461225ed88de338a73069e031ced - CACHE STRING "nettle source hash") - - include(ExternalProject) set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) @@ -230,32 +215,6 @@ elseif(gmp_build_host STREQUAL "") set(gmp_build_host "--build=${CMAKE_LIBRARY_ARCHITECTURE}") endif() -if(ENABLE_ONIONREQ) - build_external(gmp - CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic - "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" - "LDFLAGS=${apple_ldflags_arch}" ${cross_rc} CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp - ) - add_static_target(gmp gmp_external libgmp.a) - - build_external(nettle - CONFIGURE_COMMAND ./configure ${gmp_build_host} --disable-shared --prefix=${DEPS_DESTDIR} --libdir=${DEPS_DESTDIR}/lib - --with-pic --disable-openssl - "CC=${deps_cc}" "CXX=${deps_cxx}" - "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" - "CPPFLAGS=-I${DEPS_DESTDIR}/include" - "LDFLAGS=-L${DEPS_DESTDIR}/lib${apple_ldflags_arch}" - - DEPENDS gmp_external - BUILD_BYPRODUCTS - ${DEPS_DESTDIR}/lib/libnettle.a - ${DEPS_DESTDIR}/lib/libhogweed.a - ${DEPS_DESTDIR}/include/nettle/version.h - ) - add_static_target(nettle nettle_external libnettle.a gmp) - add_static_target(hogweed nettle_external libhogweed.a nettle) -endif() - link_libraries(-static-libstdc++) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") link_libraries(-static-libgcc) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a99ec4d5..fdbaa711 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -65,11 +65,6 @@ set(cross_host "") set(cross_rc "") if(CMAKE_CROSSCOMPILING) if(APPLE_TARGET_TRIPLE) - if(PLATFORM MATCHES "OS64" OR PLATFORM MATCHES "SIMULATORARM64") - set(APPLE_TARGET_TRIPLE aarch64-apple-ios) - elseif(PLATFORM MATCHES "SIMULATOR64") - set(APPLE_TARGET_TRIPLE x86_64-apple-ios) - endif() set(cross_host "--host=${APPLE_TARGET_TRIPLE}") elseif(ANDROID) if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) @@ -105,6 +100,8 @@ endif() system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) +set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") +system_or_submodule(OXENQUIC quic liboxenquic>=1.0.1 oxen-libquic) if(CMAKE_C_COMPILER_LAUNCHER) set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") diff --git a/external/oxen-encoding b/external/oxen-encoding index a7de6375..9dd7fb39 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit a7de63756dcc5c31cb899a4b810e6434b1a7c01c +Subproject commit 9dd7fb39d49666c20f62c6b63399a711fe3ac7f7 diff --git a/external/oxen-libquic b/external/oxen-libquic new file mode 160000 index 00000000..29072432 --- /dev/null +++ b/external/oxen-libquic @@ -0,0 +1 @@ +Subproject commit 2907243218b1b310bad7738dd8e15673635f36c9 diff --git a/include/session/network.h b/include/session/network.h new file mode 100644 index 00000000..727c2ce0 --- /dev/null +++ b/include/session/network.h @@ -0,0 +1,30 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "export.h" + +typedef struct remote_address { + char pubkey[67]; // in hex; 66 hex chars + null terminator. + char ip[16]; // 15 chars + null terminator. + uint16_t port; +} remote_address; + +LIBSESSION_EXPORT void network_send_request( + const unsigned char* ed25519_secretkey_bytes, + const remote_address remote, + const char* endpoint, + size_t endpoint_size, + const unsigned char* body, + size_t body_size, + void (*callback)(bool success, int16_t status_code, const char* response, size_t response_size, void*), + void* ctx); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/network.hpp b/include/session/network.hpp new file mode 100644 index 00000000..0b2ff4cd --- /dev/null +++ b/include/session/network.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "session/types.hpp" + +namespace session::network { + +void send_request( + ustring_view ed_sk, + oxen::quic::RemoteAddress target, + std::string endpoint, + ustring body, + std::function response)> handle_response); + +} // namespace session::network \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c3f1158..3edf5298 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -95,25 +95,25 @@ target_link_libraries(config libzstd::static ) -if(ENABLE_ONIONREQ) - add_libsession_util_library(onionreq - onionreq/builder.cpp - onionreq/hop_encryption.cpp - onionreq/key_types.cpp - onionreq/parser.cpp - onionreq/response_parser.cpp - ) - - target_link_libraries(onionreq - PUBLIC - crypto - common - PRIVATE - nlohmann_json::nlohmann_json - libsodium::sodium-internal - nettle - ) -endif() +add_libsession_util_library(onionreq + onionreq/builder.cpp + onionreq/hop_encryption.cpp + onionreq/key_types.cpp + onionreq/parser.cpp + onionreq/response_parser.cpp + network.cpp +) + +target_link_libraries(onionreq + PUBLIC + crypto + common + PRIVATE + quic::quic + nlohmann_json::nlohmann_json + libsodium::sodium-internal + nettle +) if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") diff --git a/src/network.cpp b/src/network.cpp new file mode 100644 index 00000000..3255d492 --- /dev/null +++ b/src/network.cpp @@ -0,0 +1,101 @@ +#include "session/network.hpp" + +#include +#include +#include +#include + +#include "session/export.h" +#include "session/network.h" +#include "session/util.hpp" + +using namespace session; +using namespace oxen::quic; +using namespace oxenc::literals; + +namespace session::network { + +constexpr auto ALPN = "oxenstorage"sv; +const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; + +void send_request( + ustring_view ed_sk, + RemoteAddress target, + std::string endpoint, + std::optional body, + std::function response)> handle_response) { + Network net; + std::promise sns_prom; + auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(ed_sk))); + auto ep = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); + auto c = ep->connect(target, creds); + auto s = c->open_stream(); + bstring_view payload = {}; + + if (body) + payload = convert_sv(from_unsigned_sv(*body)); + + s->command(std::move(endpoint), payload, [&target, &sns_prom](message resp) { + try { + if (resp.is_error()) + throw std::runtime_error{"Failed to fetch service node list from seed node"}; + + sns_prom.set_value(nlohmann::json::parse(resp.body())); + } catch (...) { + sns_prom.set_exception(std::current_exception()); + } + }); + + nlohmann::json sns; + try { + sns = sns_prom.get_future().get(); + if (!(sns.is_array() && sns.size() == 2 && sns[0].get() == 200)) { + handle_response(false, sns[0].get(), sns.dump()); // TODO: Check for response data + return; + } + + handle_response(true, sns[0].get(), sns.dump()); + + } catch (const std::exception& e) { + std::cerr << "\e[3mFailed to obtain service node list: " << e.what() << "\e[0m\n"; + // result.clear(); + handle_response(false, -1, e.what()); + } +} + +} // namespace session::network + +extern "C" { + +using namespace session::network; + +LIBSESSION_C_API void network_send_request( + const unsigned char* ed25519_secretkey_bytes, + const remote_address remote, + const char* endpoint, + size_t endpoint_size, + const unsigned char* body_, + size_t body_size, + void (*callback)(bool success, int16_t status_code, const char* response, size_t response_size, void*), + void* ctx) { + assert(ed25519_secretkey_bytes && endpoint && callback); + try { + std::optional body; + if (body_size > 0) + body = {body_, body_size}; + + send_request( + {ed25519_secretkey_bytes, 66}, + {remote.pubkey, remote.ip, remote.port}, + {endpoint, endpoint_size}, + body, + [callback, ctx](bool success, int16_t status_code, std::optional response) { + callback(success, status_code, response->data(), response->size(), ctx); + }); + } catch (const std::exception& e) { + std::string_view error = e.what(); + callback(false, -1, e.what(), error.size(), ctx); + } +} + +} // extern "C" \ No newline at end of file diff --git a/utils/ios.sh b/utils/ios.sh index 1634f495..b3f63085 100755 --- a/utils/ios.sh +++ b/utils/ios.sh @@ -106,8 +106,7 @@ for i in "${!TARGET_ARCHS[@]}"; do -DCMAKE_TOOLCHAIN_FILE="${projdir}/external/ios-cmake/ios.toolchain.cmake" \ -DPLATFORM=$platform \ -DDEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET \ - -DENABLE_BITCODE=$ENABLE_BITCODE \ - -DENABLE_ONIONREQ=OFF # Temporary until we figure out why ios builds hate gmp + -DENABLE_BITCODE=$ENABLE_BITCODE done # If needed combine simulator builds into a multi-arch lib From f837f61e3b6c46e8079a4367c40cc7de86bd29b2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Mar 2024 11:40:47 +1100 Subject: [PATCH 152/572] Removed the default rekey from the Keys init --- src/config/groups/keys.cpp | 10 +++++----- tests/test_group_keys.cpp | 8 +++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3b034ab..31743728 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -59,8 +59,6 @@ Keys::Keys( auto key_list = group_keys(); members.replace_keys(key_list, /*dirty=*/false); info.replace_keys(key_list, /*dirty=*/false); - } else if (admin()) { - rekey(info, members); } } @@ -1472,7 +1470,7 @@ LIBSESSION_C_API bool groups_keys_rekey( config_object* members, const unsigned char** out, size_t* outlen) { - assert(info && members && out && outlen); + assert(info && members); auto& keys = unbox(conf); ustring_view to_push; try { @@ -1481,8 +1479,10 @@ LIBSESSION_C_API bool groups_keys_rekey( set_error(conf, e.what()); return false; } - *out = to_push.data(); - *outlen = to_push.size(); + if (out && outlen) { + *out = to_push.data(); + *outlen = to_push.size(); + } return true; } diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 475ba217..5cc3c3b4 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -83,7 +83,10 @@ struct pseudo_client { admin ? std::make_optional({*gsk, 64}) : std::nullopt, keys_dump, info, - members} {} + members} { + if (gsk) + keys.rekey(info, members); + } }; TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { @@ -588,6 +591,9 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { 0, NULL); REQUIRE(rv == 0); + + if (is_admin) + REQUIRE(groups_keys_rekey(keys, info, members, nullptr, nullptr)); } ~pseudo_client() { From 90dd3382c655b7b87e2b7a2dc875e9166c41a2df Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Mar 2024 15:50:37 +1100 Subject: [PATCH 153/572] Updated the group keys init docs --- include/session/config/groups/keys.h | 5 +++++ include/session/config/groups/keys.hpp | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 9dab2ac7..1a1b034f 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -31,6 +31,11 @@ typedef struct config_group_keys { /// `config_free()` and similar methods from `session/config/base.h`; instead it must be managed by /// the functions declared in the header. /// +/// When no dump is provided the initial config_group_keys object will be created with no keys +/// loaded at all, these will be loaded later into this and the info/members objects when loading +/// keys via received config messages. If this is a brand new group then groups_keys_rekey() MUST be +/// called, otherwise the group will be in an invalid state. +/// /// 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. diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index f7dd59ff..71149385 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -164,6 +164,11 @@ class Keys final : public ConfigSig { /// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass /// `std::nullopt` as the last argument. /// + /// When no dump is provided the initial Keys object will be created with no keys loaded at all, + /// these will be loaded later into this and the info/members objects when loading keys via + /// received config messages. If this is a brand new group then rekey() MUST be called, + /// otherwise the group will be in an invalid state. + /// /// Inputs: /// - `user_ed25519_secretkey` is the ed25519 secret key backing the current user's session ID, /// and is used to decrypt incoming keys. It is required. @@ -175,10 +180,6 @@ class Keys final : public ConfigSig { /// - `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()`. /// - `info` and `members` -- will be loaded with the group keys, if present in the dump. - /// Otherwise, if this is an admin Keys object, with a new one constructed for the initial - /// Keys object; or with no keys loaded at all if this is a non-admin, non-dump construction. - /// (Keys will also be loaded later into this and the info/members objects, when rekey()ing or - /// loading keys via received config messages). Keys(ustring_view user_ed25519_secretkey, ustring_view group_ed25519_pubkey, std::optional group_ed25519_secretkey, From 57be9b99b9990fed6a7759753d499c6690220b29 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Mar 2024 17:31:20 +1100 Subject: [PATCH 154/572] Test to ensure libQuic is linked when running unit tests --- external/oxen-libquic | 2 +- include/session/network.h | 21 ++++++++------ include/session/network.hpp | 11 +++---- src/CMakeLists.txt | 2 +- src/network.cpp | 57 +++++++++++++++++++++---------------- tests/test_onionreq.cpp | 6 ++++ 6 files changed, 60 insertions(+), 39 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 29072432..2088630e 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 2907243218b1b310bad7738dd8e15673635f36c9 +Subproject commit 2088630e2eef8c81961d1c3623613bb7592f946b diff --git a/include/session/network.h b/include/session/network.h index 727c2ce0..8eeb4446 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -16,14 +16,19 @@ typedef struct remote_address { } remote_address; LIBSESSION_EXPORT void network_send_request( - const unsigned char* ed25519_secretkey_bytes, - const remote_address remote, - const char* endpoint, - size_t endpoint_size, - const unsigned char* body, - size_t body_size, - void (*callback)(bool success, int16_t status_code, const char* response, size_t response_size, void*), - void* ctx); + const unsigned char* ed25519_secretkey_bytes, + const remote_address remote, + const char* endpoint, + size_t endpoint_size, + const unsigned char* body, + size_t body_size, + void (*callback)( + bool success, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx); #ifdef __cplusplus } diff --git a/include/session/network.hpp b/include/session/network.hpp index 0b2ff4cd..d0858f82 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -7,10 +7,11 @@ namespace session::network { void send_request( - ustring_view ed_sk, - oxen::quic::RemoteAddress target, - std::string endpoint, - ustring body, - std::function response)> handle_response); + ustring_view ed_sk, + oxen::quic::RemoteAddress target, + std::string endpoint, + ustring body, + std::function response)> + handle_response); } // namespace session::network \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3edf5298..0b1a3f7b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -108,8 +108,8 @@ target_link_libraries(onionreq PUBLIC crypto common + quic PRIVATE - quic::quic nlohmann_json::nlohmann_json libsodium::sodium-internal nettle diff --git a/src/network.cpp b/src/network.cpp index 3255d492..44ebae6f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,10 +1,11 @@ #include "session/network.hpp" -#include -#include #include #include +#include +#include + #include "session/export.h" #include "session/network.h" #include "session/util.hpp" @@ -19,11 +20,12 @@ constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; void send_request( - ustring_view ed_sk, - RemoteAddress target, - std::string endpoint, - std::optional body, - std::function response)> handle_response) { + ustring_view ed_sk, + RemoteAddress target, + std::string endpoint, + std::optional body, + std::function response)> + handle_response) { Network net; std::promise sns_prom; auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(ed_sk))); @@ -50,10 +52,11 @@ void send_request( try { sns = sns_prom.get_future().get(); if (!(sns.is_array() && sns.size() == 2 && sns[0].get() == 200)) { - handle_response(false, sns[0].get(), sns.dump()); // TODO: Check for response data + handle_response( + false, sns[0].get(), sns.dump()); // TODO: Check for response data return; } - + handle_response(true, sns[0].get(), sns.dump()); } catch (const std::exception& e) { @@ -70,14 +73,19 @@ extern "C" { using namespace session::network; LIBSESSION_C_API void network_send_request( - const unsigned char* ed25519_secretkey_bytes, - const remote_address remote, - const char* endpoint, - size_t endpoint_size, - const unsigned char* body_, - size_t body_size, - void (*callback)(bool success, int16_t status_code, const char* response, size_t response_size, void*), - void* ctx) { + const unsigned char* ed25519_secretkey_bytes, + const remote_address remote, + const char* endpoint, + size_t endpoint_size, + const unsigned char* body_, + size_t body_size, + void (*callback)( + bool success, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx) { assert(ed25519_secretkey_bytes && endpoint && callback); try { std::optional body; @@ -85,13 +93,14 @@ LIBSESSION_C_API void network_send_request( body = {body_, body_size}; send_request( - {ed25519_secretkey_bytes, 66}, - {remote.pubkey, remote.ip, remote.port}, - {endpoint, endpoint_size}, - body, - [callback, ctx](bool success, int16_t status_code, std::optional response) { - callback(success, status_code, response->data(), response->size(), ctx); - }); + {ed25519_secretkey_bytes, 66}, + {remote.pubkey, remote.ip, remote.port}, + {endpoint, endpoint_size}, + body, + [callback, ctx]( + bool success, int16_t status_code, std::optional response) { + callback(success, status_code, response->data(), response->size(), ctx); + }); } catch (const std::exception& e) { std::string_view error = e.what(); callback(false, -1, e.what(), error.size(), ctx); diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index a4c42dd3..a729de2e 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -7,6 +8,11 @@ using namespace session; using namespace session::onionreq; +// TODO: Remove this +TEST_CASE("Quic Linking test", "[oxen][quic]") { + oxen::quic::RemoteAddress test = oxen::quic::RemoteAddress{"1234"}; +} + TEST_CASE("Onion request encryption", "[encryption][onionreq]") { auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytes; From df9438d6f5c31167d30fe1b3deb5cca5ea0516df Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 15 Mar 2024 10:19:12 +1100 Subject: [PATCH 155/572] Fixed an incorrect function definition --- include/session/network.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index d0858f82..e22670d6 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -10,7 +10,7 @@ void send_request( ustring_view ed_sk, oxen::quic::RemoteAddress target, std::string endpoint, - ustring body, + std::optional body, std::function response)> handle_response); From 2f9aaa2b0b2980ef1c68c2a87984914afa40919c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 15 Mar 2024 17:14:16 +1100 Subject: [PATCH 156/572] Added changes from bindle-static-deps PR --- cmake/AddStaticBundleLib.cmake | 23 ++++++++++++++++++++--- external/CMakeLists.txt | 3 +++ external/oxen-libquic | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cmake/AddStaticBundleLib.cmake b/cmake/AddStaticBundleLib.cmake index 8c482c47..f4718d3f 100644 --- a/cmake/AddStaticBundleLib.cmake +++ b/cmake/AddStaticBundleLib.cmake @@ -1,6 +1,11 @@ set(LIBSESSION_STATIC_BUNDLE_LIBS "" CACHE INTERNAL "list of libs to go into the static bundle lib") +function(_libsession_static_bundle_append tgt) + list(APPEND LIBSESSION_STATIC_BUNDLE_LIBS "${tgt}") + set(LIBSESSION_STATIC_BUNDLE_LIBS "${LIBSESSION_STATIC_BUNDLE_LIBS}" CACHE INTERNAL "") +endfunction() + # Call as: # # libsession_static_bundle(target [target2 ...]) @@ -8,7 +13,19 @@ set(LIBSESSION_STATIC_BUNDLE_LIBS "" CACHE INTERNAL "list of libs to go into the # to append the given target(s) to the list of libraries that will be combined to make the static # bundled libsession-util.a. function(libsession_static_bundle) - list(APPEND LIBSESSION_STATIC_BUNDLE_LIBS "${ARGN}") - list(REMOVE_DUPLICATES LIBSESSION_STATIC_BUNDLE_LIBS) - set(LIBSESSION_STATIC_BUNDLE_LIBS "${LIBSESSION_STATIC_BUNDLE_LIBS}" CACHE INTERNAL "") + foreach(tgt IN LISTS ARGN) + if(TARGET "${tgt}" AND NOT "${tgt}" IN_LIST LIBSESSION_STATIC_BUNDLE_LIBS) + get_target_property(tgt_type ${tgt} TYPE) + + if(tgt_type STREQUAL STATIC_LIBRARY) + message(STATUS "Adding ${tgt} to libsession-util bundled library list") + _libsession_static_bundle_append("${tgt}") + endif() + + get_target_property(tgt_link_deps ${tgt} LINK_LIBRARIES) + if(tgt_link_deps) + libsession_static_bundle(${tgt_link_deps}) + endif() + endif() + endforeach() endfunction() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index fdbaa711..f1c444fa 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -57,6 +57,9 @@ macro(system_or_submodule BIGNAME smallname pkgconf subdir) if(NOT TARGET ${smallname}::${smallname}) add_library(${smallname}::${smallname} ALIAS ${smallname}) endif() + if(BUILD_STATIC_DEPS AND STATIC_BUNDLE) + libsession_static_bundle(${smallname}::${smallname}) + endif() endmacro() diff --git a/external/oxen-libquic b/external/oxen-libquic index 2088630e..7efe9ecf 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 2088630e2eef8c81961d1c3623613bb7592f946b +Subproject commit 7efe9ecf9654e8f014dbfe99dcd5ec1771e4966b From f88ac8779a5a6a6cb7f3cb20f85de79b9a39d580 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 18 Mar 2024 18:20:10 +1100 Subject: [PATCH 157/572] [WIP] Latest tweaks, still have linker errors... --- external/CMakeLists.txt | 3 ++- external/oxen-libquic | 2 +- src/network.cpp | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index f1c444fa..53a3fbf3 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -100,7 +100,8 @@ if(CMAKE_CROSSCOMPILING) endif() endif() - +set(OXENC_BUILD_TESTS OFF CACHE BOOL "") +set(OXENC_BUILD_DOCS OFF CACHE BOOL "") system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") diff --git a/external/oxen-libquic b/external/oxen-libquic index 7efe9ecf..c0bda5fe 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 7efe9ecf9654e8f014dbfe99dcd5ec1771e4966b +Subproject commit c0bda5fe41e9763da83d74f94306ce0c1f0aefbe diff --git a/src/network.cpp b/src/network.cpp index 44ebae6f..17227158 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,5 +1,7 @@ #include "session/network.hpp" +#include +#include #include #include @@ -12,7 +14,7 @@ using namespace session; using namespace oxen::quic; -using namespace oxenc::literals; +using namespace std::literals; namespace session::network { From 7386ad36bd41080c3770c5aefc948b960e9e0f64 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 19 Mar 2024 10:15:59 +1100 Subject: [PATCH 158/572] Added a call to test_onionreq.cpp to ensure it calls some libQuic methods --- tests/test_onionreq.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index a729de2e..e5022189 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -11,6 +11,15 @@ using namespace session::onionreq; // TODO: Remove this TEST_CASE("Quic Linking test", "[oxen][quic]") { oxen::quic::RemoteAddress test = oxen::quic::RemoteAddress{"1234"}; + + session::network::send_request( + to_usv(""), + test, + "test", + std::nullopt, + [](bool success, int16_t status_code, std::optional response) { + + }); } TEST_CASE("Onion request encryption", "[encryption][onionreq]") { From 0b48055f5f00e15a2fae41fa846f8c9acc2628a7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 19 Mar 2024 17:19:03 +1100 Subject: [PATCH 159/572] Tweaks to libQuic to get iOS build linking correctly --- external/oxen-libquic | 2 +- src/CMakeLists.txt | 12 ------------ src/network.cpp | 2 +- tests/CMakeLists.txt | 2 +- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index c0bda5fe..20bd5b34 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit c0bda5fe41e9763da83d74f94306ce0c1f0aefbe +Subproject commit 20bd5b34a8e6221d6afc0629d15b8c18333e7eba diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0b1a3f7b..2a6150a5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,17 +34,6 @@ macro(add_libsession_util_library name) endmacro() -if(NOT BUILD_STATIC_DEPS) - find_package(PkgConfig REQUIRED) - - if(NOT TARGET nettle) - pkg_check_modules(NETTLE nettle IMPORTED_TARGET REQUIRED) - add_library(nettle INTERFACE IMPORTED) - target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) - endif() -endif() - - add_libsession_util_library(crypto blinding.cpp curve25519.cpp @@ -112,7 +101,6 @@ target_link_libraries(onionreq PRIVATE nlohmann_json::nlohmann_json libsodium::sodium-internal - nettle ) diff --git a/src/network.cpp b/src/network.cpp index 17227158..76461d9a 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -95,7 +95,7 @@ LIBSESSION_C_API void network_send_request( body = {body_, body_size}; send_request( - {ed25519_secretkey_bytes, 66}, + {ed25519_secretkey_bytes, 64}, {remote.pubkey, remote.ip, remote.port}, {endpoint, endpoint_size}, body, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a6a1d127..e1eed8ed 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,6 @@ target_link_libraries(swarm-auth-test PRIVATE config) if(STATIC_BUNDLE) add_executable(static-bundle-test static_bundle.cpp) target_include_directories(static-bundle-test PUBLIC ../include) - target_link_libraries(static-bundle-test PRIVATE "${PROJECT_BINARY_DIR}/libsession-util.a" oxenc::oxenc) + target_link_libraries(static-bundle-test PRIVATE "${PROJECT_BINARY_DIR}/libsession-util.a" oxenc::oxenc quic) add_dependencies(static-bundle-test session-util) endif() From 6dab3b99208b9be410952174e72cb38bb0dedb27 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Mar 2024 17:02:34 +1100 Subject: [PATCH 160/572] Started working on quic-based onion request functions --- include/session/network.h | 46 +++- include/session/network.hpp | 18 +- include/session/onionreq/builder.h | 24 +- include/session/onionreq/builder.hpp | 91 +++++-- src/network.cpp | 376 ++++++++++++++++++++++++--- src/onionreq/builder.cpp | 107 +++++++- 6 files changed, 584 insertions(+), 78 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index 8eeb4446..879af053 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -8,13 +8,16 @@ extern "C" { #include #include "export.h" +#include "onionreq/builder.h" typedef struct remote_address { - char pubkey[67]; // in hex; 66 hex chars + null terminator. - char ip[16]; // 15 chars + null terminator. + char pubkey[65]; // in hex; 64 hex chars + null terminator. + char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. uint16_t port; } remote_address; +LIBSESSION_EXPORT void network_add_logger(void (*callback)(const char*, size_t)); + LIBSESSION_EXPORT void network_send_request( const unsigned char* ed25519_secretkey_bytes, const remote_address remote, @@ -24,6 +27,45 @@ LIBSESSION_EXPORT void network_send_request( size_t body_size, void (*callback)( bool success, + bool timeout, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx); + +LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( + const onion_request_path path, + const unsigned char* ed25519_secretkey_bytes, + const onion_request_service_node node, + const unsigned char* body, + size_t body_size, + void (*callback)( + bool success, + bool timeout, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx); + +LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( + const onion_request_path path, + const unsigned char* ed25519_secretkey_bytes, + const char* method, + const char* host, + const char* target, + const char* protocol, + const char* x25519_pubkey, + uint16_t port, + const char** headers_, + const char** header_values, + size_t headers_size, + const unsigned char* body, + size_t body_size, + void (*callback)( + bool success, + bool timeout, int16_t status_code, const char* response, size_t response_size, diff --git a/include/session/network.hpp b/include/session/network.hpp index e22670d6..45f4df75 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -2,16 +2,28 @@ #include +#include "session/onionreq/builder.hpp" +#include "session/onionreq/key_types.hpp" #include "session/types.hpp" namespace session::network { +using network_response_callback_t = std::function response)>; + void send_request( ustring_view ed_sk, oxen::quic::RemoteAddress target, std::string endpoint, - std::optional body, - std::function response)> - handle_response); + std::optional body, + network_response_callback_t handle_response); + +template +void send_onion_request( + const session::onionreq::onion_path path, + const Destination destination, + const std::optional body, + const ustring_view ed_sk, + network_response_callback_t handle_response); } // namespace session::network \ No newline at end of file diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 4cb72a1e..3fb1e662 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -21,6 +21,20 @@ typedef struct onion_request_builder_object { ENCRYPT_TYPE enc_type; } onion_request_builder_object; +typedef struct onion_request_service_node { + const char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. + const uint16_t lmq_port; + const char* x25519_pubkey_hex; // 64 chars + const char* ed25519_pubkey_hex; // 64 chars + const uint8_t failure_count; +} onion_request_service_node; + +typedef struct onion_request_path { + const onion_request_service_node* nodes; + const size_t nodes_count; + const uint8_t failure_count; +} onion_request_path; + /// API: groups/onion_request_builder_init /// /// Constructs an onion request builder and sets a pointer to it in `builder`. @@ -66,12 +80,16 @@ LIBSESSION_EXPORT void onion_request_builder_set_enc_type( /// /// Inputs: /// - `builder` -- [in] Pointer to the builder object +/// - `ip` -- [in] The IP address for the snode destination +/// - `lmq_port` -- [in] The LMQ port request for the snode destination /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey); + const char* ip, + const uint16_t lmq_port, + const char* x25519_pubkey, + const char* ed25519_pubkey); /// API: onion_request_builder_set_server_destination /// @@ -95,6 +113,7 @@ LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( /// - `host` -- [in] The host for the server destination /// - `target` -- [in] The target (endpoint) for the server destination /// - `protocol` -- [in] The protocol to use for the +/// - `method` -- [in] The HTTP method to use for the server destination /// - `port` -- [in] The host for the server destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination LIBSESSION_EXPORT void onion_request_builder_set_server_destination( @@ -102,6 +121,7 @@ LIBSESSION_EXPORT void onion_request_builder_set_server_destination( const char* host, const char* target, const char* protocol, + const char* method, uint16_t port, const char* x25519_pubkey); diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 0ebad565..69393c0b 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -7,6 +7,70 @@ namespace session::onionreq { +struct service_node { + std::string ip; + uint16_t lmq_port; + session::onionreq::x25519_pubkey x25519_pubkey; + session::onionreq::ed25519_pubkey ed25519_pubkey; + uint8_t failure_count; + + service_node( + std::string ip, + uint16_t lmq_port, + session::onionreq::x25519_pubkey x25519_pubkey, + session::onionreq::ed25519_pubkey ed25519_pubkey, + uint8_t failure_count) : + ip{std::move(ip)}, + lmq_port{std::move(lmq_port)}, + x25519_pubkey{std::move(x25519_pubkey)}, + ed25519_pubkey{std::move(ed25519_pubkey)}, + failure_count{failure_count} {} +}; + +struct onion_path { + std::vector nodes; + uint8_t failure_count; +}; + +class SnodeDestination { + public: + service_node node; + + // SnodeDestination(service_node node) : node{std::move(node)} {} + + ustring generate_payload(std::optional body) const; +}; + +class ServerDestination { + public: + std::string host; + std::string target; + std::string protocol; + session::onionreq::x25519_pubkey x25519_pubkey; + std::string method; + std::optional port; + std::optional>> headers; + + ServerDestination( + std::string host, + std::string target, + std::string protocol, + session::onionreq::x25519_pubkey x25519_pubkey, + std::string method = "GET", + std::optional port = std::nullopt, + std::optional>> headers = + std::nullopt) : + host{std::move(host)}, + target{std::move(target)}, + protocol{std::move(protocol)}, + x25519_pubkey{std::move(x25519_pubkey)}, + port{std::move(port)}, + headers{std::move(headers)}, + method{std::move(method)} {} + + ustring generate_payload(std::optional body) const; +}; + enum class EncryptType { aes_gcm, xchacha20, @@ -35,30 +99,11 @@ class Builder { void set_enc_type(EncryptType enc_type_) { enc_type = enc_type_; } - void set_snode_destination(ed25519_pubkey ed25519_public_key, x25519_pubkey x25519_public_key) { - destination_x25519_public_key.reset(); - ed25519_public_key_.reset(); - destination_x25519_public_key.emplace(x25519_public_key); - ed25519_public_key_.emplace(ed25519_public_key); - } + template + void set_destination(Destination destination); - void set_server_destination( - std::string host, - std::string target, - std::string protocol, - std::optional port, - x25519_pubkey x25519_public_key) { - destination_x25519_public_key.reset(); - - host_.emplace(host); - target_.emplace(target); - protocol_.emplace(protocol); - - if (port) - port_.emplace(*port); - - destination_x25519_public_key.emplace(x25519_public_key); - } + template + ustring generate_payload(Destination destination, std::optional body) const; void add_hop(std::pair keys) { hops_.push_back(keys); } diff --git a/src/network.cpp b/src/network.cpp index 76461d9a..5ecd1fdd 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,23 +1,42 @@ #include "session/network.hpp" -#include -#include +#include #include #include #include +#include +#include #include +#include +#include #include "session/export.h" #include "session/network.h" +#include "session/onionreq/builder.h" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/key_types.hpp" +#include "session/onionreq/response_parser.hpp" #include "session/util.hpp" using namespace session; using namespace oxen::quic; +using namespace session::onionreq; using namespace std::literals; namespace session::network { +namespace { + ustring encode_size(uint32_t s) { + ustring result; + result.resize(4); + oxenc::write_host_as_little(s, result.data()); + return result; + } +} // namespace + +class Timeout : public std::exception {}; + constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; @@ -25,46 +44,193 @@ void send_request( ustring_view ed_sk, RemoteAddress target, std::string endpoint, - std::optional body, - std::function response)> - handle_response) { - Network net; - std::promise sns_prom; - auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(ed_sk))); - auto ep = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); - auto c = ep->connect(target, creds); - auto s = c->open_stream(); - bstring_view payload = {}; - - if (body) - payload = convert_sv(from_unsigned_sv(*body)); - - s->command(std::move(endpoint), payload, [&target, &sns_prom](message resp) { + std::optional body, + network_response_callback_t handle_response) { + try { + Network net; + std::promise prom; + auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(ed_sk))); + auto ep = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); + auto c = ep->connect(target, creds); + auto s = c->open_stream(); + bstring_view payload = {}; + + if (body) + payload = *body; + + s->command(std::move(endpoint), payload, [&target, &prom](message resp) { + try { + if (resp.timed_out) + throw Timeout{}; + + std::string body = resp.body_str(); + if (resp.is_error() && !body.empty()) + throw std::runtime_error{"Failed to fetch response with error: " + body}; + else if (resp.is_error()) + throw std::runtime_error{"Failed to fetch response"}; + + prom.set_value(body); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }); + + std::string response = prom.get_future().get(); + int16_t status_code = 200; + std::string response_data; + try { - if (resp.is_error()) - throw std::runtime_error{"Failed to fetch service node list from seed node"}; + nlohmann::json response_json = nlohmann::json::parse(response); - sns_prom.set_value(nlohmann::json::parse(resp.body())); + if (response_json.is_array() && response_json.size() == 2) { + status_code = response_json[0].get(); + response_data = response_json[1].dump(); + } else + response_data = response; } catch (...) { - sns_prom.set_exception(std::current_exception()); + response_data = response; } - }); - nlohmann::json sns; + handle_response(true, false, status_code, response_data); + } catch (const Timeout&) { + handle_response(false, true, -1, "Request timed out"); + } catch (const std::exception& e) { + handle_response(false, false, -1, e.what()); + } +} + +template +void process_response( + const Builder builder, + const Destination destination, + const std::string response, + network_response_callback_t handle_response) { + handle_response(false, false, -1, "Invalid destination."); +} + +template <> +void process_response( + const Builder builder, + const SnodeDestination destination, + const std::string response, + network_response_callback_t handle_response) { + // The SnodeDestination runs via V3 onion requests try { - sns = sns_prom.get_future().get(); - if (!(sns.is_array() && sns.size() == 2 && sns[0].get() == 200)) { - handle_response( - false, sns[0].get(), sns.dump()); // TODO: Check for response data - return; + std::string base64_iv_and_ciphertext; + + try { + nlohmann::json response_json = nlohmann::json::parse(response); + + if (!response_json.contains("result") || !response_json["result"].is_string()) + throw std::runtime_error{"JSON missing result field."}; + + base64_iv_and_ciphertext = response_json["result"].get(); + } catch (...) { + base64_iv_and_ciphertext = response; } - handle_response(true, sns[0].get(), sns.dump()); + if (!oxenc::is_base64(base64_iv_and_ciphertext)) + throw std::runtime_error{"Invalid base64 encoded IV and ciphertext."}; + + ustring iv_and_ciphertext; + oxenc::from_base64( + base64_iv_and_ciphertext.begin(), + base64_iv_and_ciphertext.end(), + std::back_inserter(iv_and_ciphertext)); + auto parser = ResponseParser(builder); + auto result = parser.decrypt(iv_and_ciphertext); + auto result_json = nlohmann::json::parse(result); + int status_code; + + if (result_json.contains("status_code") && result_json["status_code"].is_number()) + status_code = result_json["status_code"].get(); + else if (result_json.contains("status") && result_json["status"].is_number()) + status_code = result_json["status"].get(); + else + throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; + + if (result_json.contains("body") && result_json["body"].is_string()) + handle_response(true, false, status_code, result_json["body"].get()); + else + handle_response(true, false, status_code, result_json.dump()); + } catch (const std::exception& e) { + oxen::log::info(log_cat, "Triggered callback SnodeDestinationEnd: {}", e.what()); + handle_response(false, false, -1, e.what()); + } +} + +template <> +void process_response( + const Builder builder, + const ServerDestination destination, + const std::string response, + network_response_callback_t handle_response) { + // The ServerDestination runs via V4 onion requests + try { + auto parser = ResponseParser(builder); + ustring response_data; + oxenc::from_hex(response.begin(), response.end(), std::back_inserter(response_data)); + auto result = parser.decrypt(response_data); + // Process the bencoded response + oxenc::bt_dict_consumer result_bencode{result}; } catch (const std::exception& e) { - std::cerr << "\e[3mFailed to obtain service node list: " << e.what() << "\e[0m\n"; - // result.clear(); - handle_response(false, -1, e.what()); + oxen::log::info(log_cat, "Triggered callback SnodeDestinationEnd: {}", e.what()); + handle_response(false, false, -1, e.what()); + } +} + +template +void send_onion_request( + const onion_path path, + const Destination destination, + const std::optional body, + const ustring_view ed_sk, + network_response_callback_t handle_response) { + if (path.nodes.empty()) { + handle_response(false, false, -1, "No nodes in the path"); + return; + } + + try { + // Construct the onion request + auto builder = Builder(); + builder.set_destination(destination); + + for (const auto& node : path.nodes) + builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); + + auto payload = builder.generate_payload(destination, body); + auto onion_req_payload = builder.build(payload); + bstring_view quic_payload = bstring_view{ + reinterpret_cast(onion_req_payload.data()), + onion_req_payload.size()}; + + send_request( + ed_sk, + RemoteAddress{ + path.nodes[0].ed25519_pubkey.view(), + path.nodes[0].ip, + path.nodes[0].lmq_port}, + "onion_req", + quic_payload, + [builder = std::move(builder), + destination = std::move(destination), + callback = std::move(handle_response)]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + if (!response.has_value()) { + callback(success, timeout, status_code, response); + return; + } + + process_response(builder, destination, *response, callback); + }); + } catch (const std::exception& e) { + oxen::log::info(log_cat, "Triggered callback3: {}", e.what()); + handle_response(false, false, -1, e.what()); } } @@ -83,29 +249,161 @@ LIBSESSION_C_API void network_send_request( size_t body_size, void (*callback)( bool success, + bool timeout, int16_t status_code, const char* response, size_t response_size, void*), void* ctx) { assert(ed25519_secretkey_bytes && endpoint && callback); + + std::optional body; + if (body_size > 0) + body = bstring_view{reinterpret_cast(body_), body_size}; + + std::string_view remote_pubkey_hex = {remote.pubkey, 64}; + session::ustring remote_pubkey; + oxenc::from_hex( + remote_pubkey_hex.begin(), remote_pubkey_hex.end(), std::back_inserter(remote_pubkey)); + + send_request( + {ed25519_secretkey_bytes, 64}, + {remote_pubkey, remote.ip, remote.port}, + {endpoint, endpoint_size}, + body, + [callback, ctx]( + bool success, + bool timeout, + int status_code, + std::optional response) { + callback(success, timeout, status_code, response->data(), response->size(), ctx); + }); +} + +LIBSESSION_C_API void network_send_onion_request_to_snode_destination( + const onion_request_path path_, + const unsigned char* ed25519_secretkey_bytes, + const onion_request_service_node node, + const unsigned char* body_, + size_t body_size, + void (*callback)( + bool success, + bool timeout, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx) { + assert(ed25519_secretkey_bytes && callback); + try { + std::vector nodes; + for (size_t i = 0; i < path_.nodes_count; i++) + nodes.emplace_back( + path_.nodes[i].ip, + path_.nodes[i].lmq_port, + x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), + path_.nodes[i].failure_count); + + session::onionreq::onion_path path = {nodes, path_.failure_count}; + std::optional body; if (body_size > 0) body = {body_, body_size}; - send_request( + send_onion_request( + path, + SnodeDestination{ + {node.ip, + node.lmq_port, + x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), + node.failure_count}}, + body, {ed25519_secretkey_bytes, 64}, - {remote.pubkey, remote.ip, remote.port}, - {endpoint, endpoint_size}, + [callback, ctx]( + bool success, + bool timeout, + int status_code, + std::optional response) { + callback( + success, timeout, status_code, response->data(), response->size(), ctx); + }); + } catch (const std::exception& e) { + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + } +} + +LIBSESSION_C_API void network_send_onion_request_to_server_destination( + const onion_request_path path_, + const unsigned char* ed25519_secretkey_bytes, + const char* method, + const char* host, + const char* target, + const char* protocol, + const char* x25519_pubkey, + uint16_t port, + const char** headers_, + const char** header_values, + size_t headers_size, + const unsigned char* body_, + size_t body_size, + void (*callback)( + bool success, + bool timeout, + int16_t status_code, + const char* response, + size_t response_size, + void*), + void* ctx) { + assert(ed25519_secretkey_bytes && host && target && protocol && x25519_pubkey && callback); + + try { + std::vector nodes; + for (size_t i = 0; i < path_.nodes_count; i++) + nodes.emplace_back( + path_.nodes[i].ip, + path_.nodes[i].lmq_port, + x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), + path_.nodes[i].failure_count); + + session::onionreq::onion_path path = {nodes, path_.failure_count}; + std::optional>> headers; + if (headers_size > 0) { + headers = std::vector>{}; + + for (size_t i = 0; i < headers_size; i++) + headers->emplace_back(headers_[i], header_values[i]); + } + + std::optional body; + if (body_size > 0) + body = {body_, body_size}; + + send_onion_request( + path, + ServerDestination{ + host, + target, + protocol, + x25519_pubkey::from_hex({x25519_pubkey, 64}), + method, + port, + headers}, body, + {ed25519_secretkey_bytes, 64}, [callback, ctx]( - bool success, int16_t status_code, std::optional response) { - callback(success, status_code, response->data(), response->size(), ctx); + bool success, + bool timeout, + int status_code, + std::optional response) { + callback( + success, timeout, status_code, response->data(), response->size(), ctx); }); } catch (const std::exception& e) { - std::string_view error = e.what(); - callback(false, -1, e.what(), error.size(), ctx); + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 54143d14..81ff646f 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -43,6 +43,80 @@ EncryptType parse_enc_type(std::string_view enc_type) { throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; } +template +void Builder::set_destination(Destination destination) { + throw std::runtime_error{"Invalid destination."}; +} + +template <> +void Builder::set_destination(SnodeDestination destination) { + destination_x25519_public_key.reset(); + ed25519_public_key_.reset(); + destination_x25519_public_key.emplace(destination.node.x25519_pubkey); + ed25519_public_key_.emplace(destination.node.ed25519_pubkey); +} + +template <> +void Builder::set_destination(ServerDestination destination) { + destination_x25519_public_key.reset(); + + host_.emplace(destination.host); + target_.emplace(destination.target); + protocol_.emplace(destination.protocol); + + if (destination.port.has_value()) + port_.emplace(destination.port.value()); + + destination_x25519_public_key.emplace(destination.x25519_pubkey); +} + +template +ustring Builder::generate_payload(Destination destination, std::optional body) const { + throw std::runtime_error{"Invalid destination."}; +} + +template <> +ustring Builder::generate_payload(SnodeDestination destination, std::optional body) const { + return {reinterpret_cast(body->data()), body->size()}; +} + +template <> +ustring Builder::generate_payload( + ServerDestination destination, std::optional body) const { + auto headers_json = nlohmann::json::array(); + for (const auto& [key, value] : destination.headers.value()) + headers_json.push_back({key, value}); + + if (body.has_value() && !headers_json.contains("Content-Type")) + headers_json.push_back({"Content-Type", "application/json"}); + + // Some platforms might automatically add this header, but we don't want to include it + headers_json.erase("User-Agent"); + + // Generate the Bencoded payload in the form + // `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e` + nlohmann::json request_info{ + {"method", destination.method}, + {"endpoint", destination.target}, + {"headers", headers_json}}; + ustring_view request_info_data = to_unsigned_sv(request_info.dump()); + ustring result; + result += to_unsigned_sv("l"); + result += encode_size(request_info_data.size()); + result += to_unsigned_sv(":"); + result += request_info_data; + + if (body.has_value()) { + result += encode_size(body->size()); + result += to_unsigned_sv(":"); + result += {reinterpret_cast(body->data()), body->size()}; + } + + result += to_unsigned_sv("e"); + + return result; +} + ustring Builder::build(ustring payload) { ustring blob; @@ -121,7 +195,13 @@ ustring Builder::build(ustring payload) { data += to_unsigned_sv(control.dump()); blob = e.encrypt(enc_type, data, *destination_x25519_public_key); } else { - throw std::runtime_error{"Destination not set"}; + if (!destination_x25519_public_key.has_value()) + throw std::runtime_error{"Destination not set: No destination x25519 public key"}; + if (!ed25519_public_key_.has_value()) + throw std::runtime_error{"Destination not set: No destination ed25519 public key"}; + throw std::runtime_error{ + "Destination not set: " + host_.value_or("N/A") + ", " + + target_.value_or("N/A") + ", " + protocol_.value_or("N/A")}; } // Save these because we need them again to decrypt the final response: @@ -204,13 +284,19 @@ LIBSESSION_C_API void onion_request_builder_set_enc_type( LIBSESSION_C_API void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, - const char* ed25519_pubkey, - const char* x25519_pubkey) { - assert(builder && ed25519_pubkey && x25519_pubkey); - - unbox(builder).set_snode_destination( + const char* ip, + const uint16_t lmq_port, + const char* x25519_pubkey, + const char* ed25519_pubkey) { + assert(builder && ip && x25519_pubkey && ed25519_pubkey); + + auto node = session::onionreq::service_node{ + ip, + lmq_port, + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); + 0}; + unbox(builder).set_destination(session::onionreq::SnodeDestination{node}); } LIBSESSION_C_API void onion_request_builder_set_server_destination( @@ -218,16 +304,19 @@ LIBSESSION_C_API void onion_request_builder_set_server_destination( const char* host, const char* target, const char* protocol, + const char* method, uint16_t port, const char* x25519_pubkey) { assert(builder && host && target && protocol && x25519_pubkey); - unbox(builder).set_server_destination( + unbox(builder).set_destination(session::onionreq::ServerDestination{ host, target, protocol, + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), + method, port, - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); + std::nullopt}); } LIBSESSION_C_API void onion_request_builder_add_hop( From a3ec4c271fe24000fd0f38ef226bcdf35767c981 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Mar 2024 14:53:34 +1100 Subject: [PATCH 161/572] Fixed up a bunch of libQuic onion routing issues --- external/oxen-libquic | 2 +- include/session/network.h | 10 +- include/session/network.hpp | 8 +- include/session/onionreq/builder.h | 41 ++-- include/session/onionreq/builder.hpp | 21 +- include/session/onionreq/hop_encryption.hpp | 3 + include/session/onionreq/response_parser.hpp | 2 + src/network.cpp | 212 +++++++++++++++---- src/onionreq/builder.cpp | 78 +++---- src/onionreq/hop_encryption.cpp | 20 +- src/onionreq/response_parser.cpp | 6 +- tests/test_onionreq.cpp | 14 -- 12 files changed, 280 insertions(+), 137 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 20bd5b34..381602cc 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 20bd5b34a8e6221d6afc0629d15b8c18333e7eba +Subproject commit 381602cc9d842ff253ef8d8c8e9d91f4175a038a diff --git a/include/session/network.h b/include/session/network.h index 879af053..1049c03d 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -16,8 +16,6 @@ typedef struct remote_address { uint16_t port; } remote_address; -LIBSESSION_EXPORT void network_add_logger(void (*callback)(const char*, size_t)); - LIBSESSION_EXPORT void network_send_request( const unsigned char* ed25519_secretkey_bytes, const remote_address remote, @@ -46,6 +44,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( int16_t status_code, const char* response, size_t response_size, + onion_request_path updated_failures_path, void*), void* ctx); @@ -53,11 +52,11 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( const onion_request_path path, const unsigned char* ed25519_secretkey_bytes, const char* method, - const char* host, - const char* target, const char* protocol, - const char* x25519_pubkey, + const char* host, + const char* endpoint, uint16_t port, + const char* x25519_pubkey, const char** headers_, const char** header_values, size_t headers_size, @@ -69,6 +68,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, + onion_request_path updated_failures_path, void*), void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index 45f4df75..1c07121f 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -10,6 +10,12 @@ namespace session::network { using network_response_callback_t = std::function response)>; +using network_onion_response_callback_t = std::function response, + session::onionreq::onion_path updated_failures_path)>; void send_request( ustring_view ed_sk, @@ -24,6 +30,6 @@ void send_onion_request( const Destination destination, const std::optional body, const ustring_view ed_sk, - network_response_callback_t handle_response); + network_onion_response_callback_t handle_response); } // namespace session::network \ No newline at end of file diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 3fb1e662..3d688970 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -22,17 +22,18 @@ typedef struct onion_request_builder_object { } onion_request_builder_object; typedef struct onion_request_service_node { - const char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. - const uint16_t lmq_port; - const char* x25519_pubkey_hex; // 64 chars - const char* ed25519_pubkey_hex; // 64 chars - const uint8_t failure_count; + char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. + uint16_t lmq_port; + char x25519_pubkey_hex[64]; + char ed25519_pubkey_hex[64]; + uint8_t failure_count; + bool invalid; } onion_request_service_node; typedef struct onion_request_path { const onion_request_service_node* nodes; const size_t nodes_count; - const uint8_t failure_count; + uint8_t failure_count; } onion_request_path; /// API: groups/onion_request_builder_init @@ -96,31 +97,19 @@ LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( /// Wrapper around session::onionreq::Builder::set_server_destination. x25519_pubkey /// is a hex string and must both be exactly 64 characters. /// -/// Declaration: -/// ```cpp -/// void onion_request_builder_set_server_destination( -/// [in] onion_request_builder_object* builder -/// [in] const char* host, -/// [in] const char* target, -/// [in] const char* protocol, -/// [in] uint16_t port, -/// [in] const char* x25519_pubkey -/// ); -/// ``` -/// /// Inputs: /// - `builder` -- [in] Pointer to the builder object -/// - `host` -- [in] The host for the server destination -/// - `target` -- [in] The target (endpoint) for the server destination -/// - `protocol` -- [in] The protocol to use for the -/// - `method` -- [in] The HTTP method to use for the server destination -/// - `port` -- [in] The host for the server destination -/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +/// - `protocol` -- [in] The protocol to use +/// - `host` -- [in] The server host +/// - `endpoint` -- [in] The endpoint to call +/// - `method` -- [in] The HTTP method to use +/// - `port` -- [in] The port to use +/// - `x25519_pubkey` -- [in] The x25519 public key for server LIBSESSION_EXPORT void onion_request_builder_set_server_destination( onion_request_builder_object* builder, - const char* host, - const char* target, const char* protocol, + const char* host, + const char* endpoint, const char* method, uint16_t port, const char* x25519_pubkey); diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 69393c0b..ae97546e 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -13,18 +13,21 @@ struct service_node { session::onionreq::x25519_pubkey x25519_pubkey; session::onionreq::ed25519_pubkey ed25519_pubkey; uint8_t failure_count; + bool invalid; service_node( std::string ip, uint16_t lmq_port, session::onionreq::x25519_pubkey x25519_pubkey, session::onionreq::ed25519_pubkey ed25519_pubkey, - uint8_t failure_count) : + uint8_t failure_count, + bool invalid = false) : ip{std::move(ip)}, lmq_port{std::move(lmq_port)}, x25519_pubkey{std::move(x25519_pubkey)}, ed25519_pubkey{std::move(ed25519_pubkey)}, - failure_count{failure_count} {} + failure_count{failure_count}, + invalid{invalid} {} }; struct onion_path { @@ -43,26 +46,26 @@ class SnodeDestination { class ServerDestination { public: - std::string host; - std::string target; std::string protocol; + std::string host; + std::string endpoint; session::onionreq::x25519_pubkey x25519_pubkey; std::string method; std::optional port; std::optional>> headers; ServerDestination( - std::string host, - std::string target, std::string protocol, + std::string host, + std::string endpoint, session::onionreq::x25519_pubkey x25519_pubkey, std::string method = "GET", std::optional port = std::nullopt, std::optional>> headers = std::nullopt) : - host{std::move(host)}, - target{std::move(target)}, protocol{std::move(protocol)}, + host{std::move(host)}, + endpoint{std::move(endpoint)}, x25519_pubkey{std::move(x25519_pubkey)}, port{std::move(port)}, headers{std::move(headers)}, @@ -103,7 +106,7 @@ class Builder { void set_destination(Destination destination); template - ustring generate_payload(Destination destination, std::optional body) const; + std::string generate_payload(Destination destination, std::optional body) const; void add_hop(std::pair keys) { hops_.push_back(keys); } diff --git a/include/session/onionreq/hop_encryption.hpp b/include/session/onionreq/hop_encryption.hpp index fcb18136..ae57c6b5 100644 --- a/include/session/onionreq/hop_encryption.hpp +++ b/include/session/onionreq/hop_encryption.hpp @@ -16,6 +16,9 @@ class HopEncryption { public_key_{std::move(public_key)}, server_{server} {} + // Returns true if the response is long enough to be a valid response. + static bool response_long_enough(EncryptType type, size_t response_size); + // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. // `reply` should be false for a client-to-snode message, and true on a returning // snode-to-client message. diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp index 9f0c764d..be6b3576 100644 --- a/include/session/onionreq/response_parser.hpp +++ b/include/session/onionreq/response_parser.hpp @@ -20,6 +20,8 @@ class ResponseParser { x25519_keypair_{std::move(x25519_keypair)}, enc_type_{enc_type} {} + static bool response_long_enough(EncryptType enc_type, size_t response_size); + ustring decrypt(ustring ciphertext) const; private: diff --git a/src/network.cpp b/src/network.cpp index 5ecd1fdd..7def9ca7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -5,8 +5,6 @@ #include #include -#include -#include #include #include #include @@ -37,6 +35,12 @@ namespace { class Timeout : public std::exception {}; +// The number of times a path can fail before it's replaced. +const uint16_t path_failure_threshold = 3; + +// The number of times a snode can fail before it's replaced. +const uint16_t snode_failure_threshold = 3; + constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; @@ -101,19 +105,21 @@ void send_request( template void process_response( + const onion_path path, const Builder builder, const Destination destination, const std::string response, - network_response_callback_t handle_response) { - handle_response(false, false, -1, "Invalid destination."); + network_onion_response_callback_t handle_response) { + handle_response(false, false, -1, "Invalid destination.", path); } template <> void process_response( + const onion_path path, const Builder builder, const SnodeDestination destination, const std::string response, - network_response_callback_t handle_response) { + network_onion_response_callback_t handle_response) { // The SnodeDestination runs via V3 onion requests try { std::string base64_iv_and_ciphertext; @@ -140,43 +146,70 @@ void process_response( auto parser = ResponseParser(builder); auto result = parser.decrypt(iv_and_ciphertext); auto result_json = nlohmann::json::parse(result); - int status_code; + int16_t status_code; if (result_json.contains("status_code") && result_json["status_code"].is_number()) - status_code = result_json["status_code"].get(); + status_code = result_json["status_code"].get(); else if (result_json.contains("status") && result_json["status"].is_number()) - status_code = result_json["status"].get(); + status_code = result_json["status"].get(); else throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; if (result_json.contains("body") && result_json["body"].is_string()) - handle_response(true, false, status_code, result_json["body"].get()); + handle_response(true, false, status_code, result_json["body"].get(), path); else - handle_response(true, false, status_code, result_json.dump()); + handle_response(true, false, status_code, result_json.dump(), path); } catch (const std::exception& e) { - oxen::log::info(log_cat, "Triggered callback SnodeDestinationEnd: {}", e.what()); - handle_response(false, false, -1, e.what()); + handle_response(false, false, -1, e.what(), path); } } template <> void process_response( + const onion_path path, const Builder builder, const ServerDestination destination, const std::string response, - network_response_callback_t handle_response) { + network_onion_response_callback_t handle_response) { // The ServerDestination runs via V4 onion requests try { + ustring response_data = {to_unsigned(response.data()), response.size()}; auto parser = ResponseParser(builder); - ustring response_data; - oxenc::from_hex(response.begin(), response.end(), std::back_inserter(response_data)); auto result = parser.decrypt(response_data); // Process the bencoded response - oxenc::bt_dict_consumer result_bencode{result}; + auto result_sv = from_unsigned_sv(result.data()); + oxenc::bt_list_consumer result_bencode{result}; + + if (result_bencode.is_finished() || !result_bencode.is_string()) + throw std::runtime_error{"Invalid bencoded response"}; + + auto response_info_string = result_bencode.consume_string(); + auto response_info_json = nlohmann::json::parse(response_info_string); + int16_t status_code; + + if (response_info_json.contains("code") && response_info_json["code"].is_number()) + status_code = response_info_json["code"].get(); + else + throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; + + // If we have a status code that is not in the 2xx range, return the error + if (status_code < 200 || status_code > 299) { + if (result_bencode.is_finished()) { + handle_response(true, false, status_code, std::string(result_sv), path); + return; + } + + std::string message = result_bencode.consume_string(); + handle_response(true, false, status_code, message, path); + return; + } + + auto response_string = result_bencode.consume_string(); + auto response_json = nlohmann::json::parse(response_string); + handle_response(true, false, status_code, response_json.dump(), path); } catch (const std::exception& e) { - oxen::log::info(log_cat, "Triggered callback SnodeDestinationEnd: {}", e.what()); - handle_response(false, false, -1, e.what()); + handle_response(false, false, -1, e.what(), path); } } @@ -186,9 +219,9 @@ void send_onion_request( const Destination destination, const std::optional body, const ustring_view ed_sk, - network_response_callback_t handle_response) { + network_onion_response_callback_t handle_response) { if (path.nodes.empty()) { - handle_response(false, false, -1, "No nodes in the path"); + handle_response(false, false, -1, "No nodes in the path", path); return; } @@ -201,11 +234,10 @@ void send_onion_request( builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); auto payload = builder.generate_payload(destination, body); - auto onion_req_payload = builder.build(payload); + auto onion_req_payload = builder.build(to_unsigned(payload.data())); bstring_view quic_payload = bstring_view{ reinterpret_cast(onion_req_payload.data()), onion_req_payload.size()}; - send_request( ed_sk, RemoteAddress{ @@ -215,23 +247,100 @@ void send_onion_request( "onion_req", quic_payload, [builder = std::move(builder), + path = std::move(path), destination = std::move(destination), callback = std::move(handle_response)]( bool success, bool timeout, int16_t status_code, std::optional response) { - if (!response.has_value()) { - callback(success, timeout, status_code, response); + onion_path updated_path = path; + + if (!success || timeout || + !ResponseParser::response_long_enough(builder.enc_type, response->size())) { + switch (status_code) { + // A 404 or a 400 is likely due to a bad/missing SOGS or file so + // shouldn't mark a path or snode as invalid + case 400: break; + case 404: break; + + // The user's clock is out of sync with the service node network (a + // snode will return 406, but V4 onion requests returns a 425) + case 406: break; + case 425: break; + + // The snode isn't associated with the given public key anymore (the + // client needs to update the swarm, the response might contain updated + // swarm data) + case 421: updated_path.nodes[0].invalid = true; break; + + default: + std::string node_not_found_prefix = "Next node not found: "; + + if (response.has_value() && + response->substr(0, node_not_found_prefix.size()) == + node_not_found_prefix) { + std::string ed25519PublicKey = + response->substr(response->find(":") + 1); + auto snode = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&ed25519PublicKey](const auto& node) { + return node.ed25519_pubkey.hex() == + ed25519PublicKey; + }); + + // The node is invalid so mark is as such so it can be dropped + snode->invalid = true; + } else { + // Increment the path failure count + updated_path.failure_count += 1; + + // Increment the failure count for each snode in the path + // (skipping the first as it would be dropped if the path is + // dropped) + for (auto it = updated_path.nodes.begin() + 1; + it != updated_path.nodes.end(); + ++it) { + it->failure_count += 1; + + if (it->failure_count >= snode_failure_threshold) + it->invalid = true; + } + + // If the path has failed too many times, drop the guard snode + if (updated_path.failure_count >= path_failure_threshold) + updated_path.nodes[0].invalid = true; + } + break; + } + + callback(success, timeout, status_code, response, updated_path); return; } - process_response(builder, destination, *response, callback); + process_response(updated_path, builder, destination, *response, callback); }); } catch (const std::exception& e) { - oxen::log::info(log_cat, "Triggered callback3: {}", e.what()); - handle_response(false, false, -1, e.what()); + handle_response(false, false, -1, e.what(), path); + } +} + +std::vector convert_service_nodes(const std::vector nodes) { + std::vector converted_nodes; + for (const auto& node : nodes) { + onion_request_service_node converted_node; + strncpy(converted_node.ip, node.ip.c_str(), sizeof(converted_node.ip) - 1); + converted_node.ip[sizeof(converted_node.ip) - 1] = '\0'; // Ensure null termination + strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); + strncpy(converted_node.ed25519_pubkey_hex, node.ed25519_pubkey.hex().c_str(), 64); + converted_node.lmq_port = node.lmq_port; + converted_node.failure_count = node.failure_count; + converted_node.invalid = node.invalid; + converted_nodes.push_back(converted_node); } + + return converted_nodes; } } // namespace session::network @@ -240,6 +349,11 @@ extern "C" { using namespace session::network; +LIBSESSION_C_API void network_add_logger(void (*callback)(const char*, size_t)) { + assert(callback); + add_network_logger([callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); +} + LIBSESSION_C_API void network_send_request( const unsigned char* ed25519_secretkey_bytes, const remote_address remote, @@ -292,6 +406,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( int16_t status_code, const char* response, size_t response_size, + onion_request_path updated_failures_path, void*), void* ctx) { assert(ed25519_secretkey_bytes && callback); @@ -326,12 +441,21 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( bool success, bool timeout, int status_code, - std::optional response) { + std::optional response, + session::onionreq::onion_path updated_failures_path) { + auto nodes = session::network::convert_service_nodes(updated_failures_path.nodes); + auto updated_path = onion_request_path{nodes.data(), nodes.size(), updated_failures_path.failure_count}; callback( - success, timeout, status_code, response->data(), response->size(), ctx); + success, + timeout, + status_code, + response->data(), + response->size(), + updated_path, + ctx); }); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, e.what(), std::strlen(e.what()), path_, ctx); } } @@ -339,11 +463,11 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( const onion_request_path path_, const unsigned char* ed25519_secretkey_bytes, const char* method, - const char* host, - const char* target, const char* protocol, - const char* x25519_pubkey, + const char* host, + const char* endpoint, uint16_t port, + const char* x25519_pubkey, const char** headers_, const char** header_values, size_t headers_size, @@ -355,9 +479,10 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, + onion_request_path updated_failures_path, void*), void* ctx) { - assert(ed25519_secretkey_bytes && host && target && protocol && x25519_pubkey && callback); + assert(ed25519_secretkey_bytes && method && protocol && host && endpoint && x25519_pubkey && callback); try { std::vector nodes; @@ -385,9 +510,9 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( send_onion_request( path, ServerDestination{ - host, - target, protocol, + host, + endpoint, x25519_pubkey::from_hex({x25519_pubkey, 64}), method, port, @@ -398,12 +523,21 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( bool success, bool timeout, int status_code, - std::optional response) { + std::optional response, + session::onionreq::onion_path updated_failures_path) { + auto nodes = session::network::convert_service_nodes(updated_failures_path.nodes); + auto updated_path = onion_request_path{nodes.data(), nodes.size(), updated_failures_path.failure_count}; callback( - success, timeout, status_code, response->data(), response->size(), ctx); + success, + timeout, + status_code, + response->data(), + response->size(), + updated_path, + ctx); }); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, e.what(), std::strlen(e.what()), path_, ctx); } } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 81ff646f..7cbd49f0 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -1,6 +1,7 @@ #include "session/onionreq/builder.hpp" #include +#include #include #include #include @@ -61,7 +62,7 @@ void Builder::set_destination(ServerDestination destination) { destination_x25519_public_key.reset(); host_.emplace(destination.host); - target_.emplace(destination.target); + target_.emplace("/oxen/v4/lsrpc"); // All servers support V4 onion requests protocol_.emplace(destination.protocol); if (destination.port.has_value()) @@ -71,50 +72,53 @@ void Builder::set_destination(ServerDestination destination) { } template -ustring Builder::generate_payload(Destination destination, std::optional body) const { +std::string Builder::generate_payload(Destination destination, std::optional body) const { throw std::runtime_error{"Invalid destination."}; } template <> -ustring Builder::generate_payload(SnodeDestination destination, std::optional body) const { - return {reinterpret_cast(body->data()), body->size()}; +std::string Builder::generate_payload( + SnodeDestination destination, std::optional body) const { + if (body.has_value()) + return std::string(from_unsigned_sv(*body)); + return ""; } template <> -ustring Builder::generate_payload( +std::string Builder::generate_payload( ServerDestination destination, std::optional body) const { - auto headers_json = nlohmann::json::array(); - for (const auto& [key, value] : destination.headers.value()) - headers_json.push_back({key, value}); + auto headers_json = nlohmann::json::object(); + + if (destination.headers) + for (const auto& [key, value] : destination.headers.value()) { + // Some platforms might automatically add this header, but we don't want to include it + if (key != "User-Agent") + headers_json[key] = value; + } if (body.has_value() && !headers_json.contains("Content-Type")) - headers_json.push_back({"Content-Type", "application/json"}); + headers_json["Content-Type"] = "application/json"; + + // Need to ensure the endpoint has a leading forward slash so add it if it's missing + auto endpoint = destination.endpoint; - // Some platforms might automatically add this header, but we don't want to include it - headers_json.erase("User-Agent"); + if (!endpoint.empty() && endpoint.front() != '/') + endpoint = '/' + endpoint; - // Generate the Bencoded payload in the form - // `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e` + // Structure the request information nlohmann::json request_info{ {"method", destination.method}, - {"endpoint", destination.target}, + {"endpoint", endpoint}, {"headers", headers_json}}; - ustring_view request_info_data = to_unsigned_sv(request_info.dump()); - ustring result; - result += to_unsigned_sv("l"); - result += encode_size(request_info_data.size()); - result += to_unsigned_sv(":"); - result += request_info_data; - - if (body.has_value()) { - result += encode_size(body->size()); - result += to_unsigned_sv(":"); - result += {reinterpret_cast(body->data()), body->size()}; - } + auto request_info_dump = request_info.dump(); + std::vector payload{request_info_dump}; - result += to_unsigned_sv("e"); + // If we were given a body, add it to the payload - return result; + if (body.has_value()) + payload.emplace_back(std::string(from_unsigned(body->data()), body->size())); + + return oxenc::bt_serialize(payload); } ustring Builder::build(ustring payload) { @@ -170,11 +174,11 @@ ustring Builder::build(ustring payload) { // server or a service node if (host_ && target_ && protocol_ && destination_x25519_public_key) { final_route = { - {"host", host_.value()}, - {"target", target_.value()}, + {"host", *host_}, + {"target", *target_}, {"method", "POST"}, - {"protocol", protocol_.value()}, - {"port", port_.value_or(protocol_.value() == "https" ? 443 : 80)}, + {"protocol", *protocol_}, + {"port", port_.value_or(*protocol_ == "https" ? 443 : 80)}, {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the // *next* hop to use {"enc_type", to_string(enc_type)}, @@ -301,18 +305,18 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( LIBSESSION_C_API void onion_request_builder_set_server_destination( onion_request_builder_object* builder, - const char* host, - const char* target, const char* protocol, + const char* host, + const char* endpoint, const char* method, uint16_t port, const char* x25519_pubkey) { - assert(builder && host && target && protocol && x25519_pubkey); + assert(builder && protocol && host && endpoint && x25519_pubkey); unbox(builder).set_destination(session::onionreq::ServerDestination{ - host, - target, protocol, + host, + endpoint, session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), method, port, diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index d15ef641..deb48c14 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -80,6 +80,15 @@ namespace { } // namespace +bool HopEncryption::response_long_enough(EncryptType type, size_t response_size) { + switch (type) { + case EncryptType::xchacha20: + return (response_size >= crypto_aead_xchacha20poly1305_ietf_ABYTES); + case EncryptType::aes_gcm: return (response_size >= GCM_IV_SIZE + GCM_DIGEST_SIZE); + } + return false; +} + ustring HopEncryption::encrypt( EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const { switch (type) { @@ -131,8 +140,9 @@ ustring HopEncryption::encrypt_aesgcm(ustring plaintext, const x25519_pubkey& pu ustring HopEncryption::decrypt_aesgcm(ustring ciphertext_, const x25519_pubkey& pubKey) const { ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; - if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) - throw std::runtime_error{"ciphertext data is too short"}; + if (!response_long_enough(EncryptType::aes_gcm, ciphertext_.size())) + throw std::invalid_argument{ + "Ciphertext data is too short: " + std::string(from_unsigned(ciphertext_.data()))}; auto key = derive_symmetric_key(private_key_, pubKey); @@ -198,8 +208,10 @@ ustring HopEncryption::decrypt_xchacha20(ustring ciphertext_, const x25519_pubke // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); ciphertext.remove_prefix(nonce.size()); - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw std::runtime_error{"Invalid ciphertext: too short"}; + + if (!response_long_enough(EncryptType::xchacha20, ciphertext_.size())) + throw std::invalid_argument{ + "Ciphertext data is too short: " + std::string(from_unsigned(ciphertext_.data()))}; const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 2ace3237..eda4aea7 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -23,6 +23,10 @@ ResponseParser::ResponseParser(session::onionreq::Builder builder) { x25519_keypair_ = builder.final_hop_x25519_keypair.value(); } +bool ResponseParser::response_long_enough(EncryptType enc_type, size_t response_size) { + return HopEncryption::response_long_enough(enc_type, response_size); +} + ustring ResponseParser::decrypt(ustring ciphertext) const { HopEncryption d{x25519_keypair_.second, x25519_keypair_.first, false}; @@ -38,7 +42,7 @@ ustring ResponseParser::decrypt(ustring ciphertext) const { ciphertext, destination_x25519_public_key_); else - throw e; + throw; } } diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index e5022189..0f892538 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -8,20 +8,6 @@ using namespace session; using namespace session::onionreq; -// TODO: Remove this -TEST_CASE("Quic Linking test", "[oxen][quic]") { - oxen::quic::RemoteAddress test = oxen::quic::RemoteAddress{"1234"}; - - session::network::send_request( - to_usv(""), - test, - "test", - std::nullopt, - [](bool success, int16_t status_code, std::optional response) { - - }); -} - TEST_CASE("Onion request encryption", "[encryption][onionreq]") { auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytes; From 30adf76ef5f1a83247754d26ffc0345e3d1ddc8b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Mar 2024 16:41:39 +1100 Subject: [PATCH 162/572] Fixed support for non-JSON response body --- src/network.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 7def9ca7..c1b01de1 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -185,8 +185,8 @@ void process_response( throw std::runtime_error{"Invalid bencoded response"}; auto response_info_string = result_bencode.consume_string(); - auto response_info_json = nlohmann::json::parse(response_info_string); int16_t status_code; + nlohmann::json response_info_json = nlohmann::json::parse(response_info_string); if (response_info_json.contains("code") && response_info_json["code"].is_number()) status_code = response_info_json["code"].get(); @@ -205,9 +205,15 @@ void process_response( return; } + // If there is no body just return the success status + if (result_bencode.is_finished()) { + handle_response(true, false, status_code, std::nullopt, path); + return; + } + + // Otherwise process the response auto response_string = result_bencode.consume_string(); - auto response_json = nlohmann::json::parse(response_string); - handle_response(true, false, status_code, response_json.dump(), path); + handle_response(true, false, status_code, response_string, path); } catch (const std::exception& e) { handle_response(false, false, -1, e.what(), path); } From 4e79b252d480c24e1cb543c5a65d4d4f5a7b5fdd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Mar 2024 17:38:07 +1100 Subject: [PATCH 163/572] Updated the error handling to better handle 421 errors (auto retry) --- include/session/network.h | 33 +- include/session/network.hpp | 30 +- include/session/network_service_node.h | 23 + include/session/network_service_node.hpp | 39 ++ include/session/onionreq/builder.h | 14 +- include/session/onionreq/builder.hpp | 31 +- src/network.cpp | 567 ++++++++++++++++------- src/onionreq/builder.cpp | 12 +- 8 files changed, 537 insertions(+), 212 deletions(-) create mode 100644 include/session/network_service_node.h create mode 100644 include/session/network_service_node.hpp diff --git a/include/session/network.h b/include/session/network.h index 1049c03d..962fda3f 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -8,34 +8,47 @@ extern "C" { #include #include "export.h" +#include "network_service_node.h" #include "onionreq/builder.h" -typedef struct remote_address { - char pubkey[65]; // in hex; 64 hex chars + null terminator. - char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. - uint16_t port; -} remote_address; +typedef enum SERVICE_NODE_CHANGE_TYPE { + SERVICE_NODE_CHANGE_TYPE_NONE = 0, + SERVICE_NODE_CHANGE_TYPE_INVALID_PATH = 1, + SERVICE_NODE_CHANGE_TYPE_REPLACE_SWARM = 2, + SERVICE_NODE_CHANGE_TYPE_UPDATE_PATH = 3, +} SERVICE_NODE_CHANGE_TYPE; + +typedef struct network_service_node_changes { + SERVICE_NODE_CHANGE_TYPE type; + network_service_node* nodes; + size_t nodes_count; + uint8_t failure_count; +} network_service_node_changes; + +LIBSESSION_EXPORT void network_add_logger(void (*callback)(const char*, size_t)); LIBSESSION_EXPORT void network_send_request( const unsigned char* ed25519_secretkey_bytes, - const remote_address remote, + const network_service_node destination, const char* endpoint, - size_t endpoint_size, const unsigned char* body, size_t body_size, + const network_service_node* swarm, + const size_t swarm_count, void (*callback)( bool success, bool timeout, int16_t status_code, const char* response, size_t response_size, + network_service_node_changes changes, void*), void* ctx); LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( const onion_request_path path, const unsigned char* ed25519_secretkey_bytes, - const onion_request_service_node node, + const onion_request_service_node_destination node, const unsigned char* body, size_t body_size, void (*callback)( @@ -44,7 +57,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( int16_t status_code, const char* response, size_t response_size, - onion_request_path updated_failures_path, + network_service_node_changes changes, void*), void* ctx); @@ -68,7 +81,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, - onion_request_path updated_failures_path, + network_service_node_changes changes, void*), void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index 1c07121f..690ea6b8 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -2,26 +2,39 @@ #include +#include "network_service_node.hpp" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/types.hpp" namespace session::network { +enum class ServiceNodeChangeType { + none = 0, + invalid_path = 1, + replace_swarm = 2, + update_path = 3, +}; + +struct service_node_changes { + ServiceNodeChangeType type = ServiceNodeChangeType::none; + std::vector nodes = {}; + uint8_t path_failure_count = 0; +}; + using network_response_callback_t = std::function response)>; -using network_onion_response_callback_t = std::function response, - session::onionreq::onion_path updated_failures_path)>; + service_node_changes changes)>; void send_request( - ustring_view ed_sk, - oxen::quic::RemoteAddress target, - std::string endpoint, - std::optional body, + const ustring_view ed_sk, + const session::network::service_node target, + const std::string endpoint, + const std::optional body, + const std::optional> swarm, network_response_callback_t handle_response); template @@ -30,6 +43,7 @@ void send_onion_request( const Destination destination, const std::optional body, const ustring_view ed_sk, - network_onion_response_callback_t handle_response); + const bool is_retry, + network_response_callback_t handle_response); } // namespace session::network \ No newline at end of file diff --git a/include/session/network_service_node.h b/include/session/network_service_node.h new file mode 100644 index 00000000..950c06cc --- /dev/null +++ b/include/session/network_service_node.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef struct network_service_node { + char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. + uint16_t lmq_port; + char x25519_pubkey_hex[64]; + char ed25519_pubkey_hex[64]; + + uint8_t failure_count; + bool invalid; +} network_service_node; + +#ifdef __cplusplus +} +#endif diff --git a/include/session/network_service_node.hpp b/include/session/network_service_node.hpp new file mode 100644 index 00000000..0932089b --- /dev/null +++ b/include/session/network_service_node.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "onionreq/key_types.hpp" + +namespace session::network { + +struct service_node { + std::string ip; + uint16_t lmq_port; + session::onionreq::x25519_pubkey x25519_pubkey; + session::onionreq::ed25519_pubkey ed25519_pubkey; + uint8_t failure_count; + bool invalid; + + service_node( + std::string ip, + uint16_t lmq_port, + session::onionreq::x25519_pubkey x25519_pubkey, + session::onionreq::ed25519_pubkey ed25519_pubkey, + uint8_t failure_count, + bool invalid) : + ip{std::move(ip)}, + lmq_port{lmq_port}, + x25519_pubkey{std::move(x25519_pubkey)}, + ed25519_pubkey{std::move(ed25519_pubkey)}, + failure_count{failure_count}, + invalid{invalid} {} + + bool operator==(const service_node& other) const { + return ip == other.ip && lmq_port == other.lmq_port && + x25519_pubkey == other.x25519_pubkey && ed25519_pubkey == other.ed25519_pubkey && + failure_count == other.failure_count && invalid == other.invalid; + } +}; + +} // namespace session::network diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 3d688970..2ed48292 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -8,6 +8,7 @@ extern "C" { #include #include "../export.h" +#include "../network_service_node.h" typedef enum ENCRYPT_TYPE { ENCRYPT_TYPE_AES_GCM = 0, @@ -21,17 +22,20 @@ typedef struct onion_request_builder_object { ENCRYPT_TYPE enc_type; } onion_request_builder_object; -typedef struct onion_request_service_node { +typedef struct onion_request_service_node_destination { char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. uint16_t lmq_port; char x25519_pubkey_hex[64]; char ed25519_pubkey_hex[64]; + uint8_t failure_count; bool invalid; -} onion_request_service_node; + const network_service_node* swarm; + const size_t swarm_count; +} onion_request_service_node_destination; typedef struct onion_request_path { - const onion_request_service_node* nodes; + const network_service_node* nodes; const size_t nodes_count; uint8_t failure_count; } onion_request_path; @@ -85,12 +89,14 @@ LIBSESSION_EXPORT void onion_request_builder_set_enc_type( /// - `lmq_port` -- [in] The LMQ port request for the snode destination /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +/// - `failure_count` -- [in] The number of times requests to this service node have failed LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, const char* ip, const uint16_t lmq_port, const char* x25519_pubkey, - const char* ed25519_pubkey); + const char* ed25519_pubkey, + const uint8_t failure_count); /// API: onion_request_builder_set_server_destination /// diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index ae97546e..9b2ad6bd 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -3,43 +3,20 @@ #include #include +#include "../network_service_node.hpp" #include "key_types.hpp" namespace session::onionreq { -struct service_node { - std::string ip; - uint16_t lmq_port; - session::onionreq::x25519_pubkey x25519_pubkey; - session::onionreq::ed25519_pubkey ed25519_pubkey; - uint8_t failure_count; - bool invalid; - - service_node( - std::string ip, - uint16_t lmq_port, - session::onionreq::x25519_pubkey x25519_pubkey, - session::onionreq::ed25519_pubkey ed25519_pubkey, - uint8_t failure_count, - bool invalid = false) : - ip{std::move(ip)}, - lmq_port{std::move(lmq_port)}, - x25519_pubkey{std::move(x25519_pubkey)}, - ed25519_pubkey{std::move(ed25519_pubkey)}, - failure_count{failure_count}, - invalid{invalid} {} -}; - struct onion_path { - std::vector nodes; + std::vector nodes; uint8_t failure_count; }; class SnodeDestination { public: - service_node node; - - // SnodeDestination(service_node node) : node{std::move(node)} {} + session::network::service_node node; + std::optional> swarm; ustring generate_payload(std::optional body) const; }; diff --git a/src/network.cpp b/src/network.cpp index c1b01de1..be22b15b 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -5,12 +5,17 @@ #include #include +#include +#include #include +#include #include #include #include "session/export.h" #include "session/network.h" +#include "session/network_service_node.h" +#include "session/network_service_node.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" @@ -25,12 +30,15 @@ using namespace std::literals; namespace session::network { namespace { - ustring encode_size(uint32_t s) { - ustring result; - result.resize(4); - oxenc::write_host_as_little(s, result.data()); - return result; - } + struct request_info { + const ustring_view ed_sk; + const service_node target; + const std::string endpoint; + const std::optional body; + const std::optional> swarm; + const std::optional path; + const bool is_retry = false; + }; } // namespace class Timeout : public std::exception {}; @@ -41,28 +49,224 @@ const uint16_t path_failure_threshold = 3; // The number of times a snode can fail before it's replaced. const uint16_t snode_failure_threshold = 3; +constexpr auto node_not_found_prefix = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; +std::shared_ptr buffer; -void send_request( - ustring_view ed_sk, - RemoteAddress target, - std::string endpoint, - std::optional body, +void send_request(const request_info info, network_response_callback_t handle_response); + +void add_network_logger(std::function callback) { + buffer = std::make_shared(100UL, callback); + oxen::log::add_sink(buffer); +} + +void handle_errors( + const int16_t status_code, + const std::optional response, + const request_info info, network_response_callback_t handle_response) { + switch (status_code) { + // A 404 or a 400 is likely due to a bad/missing SOGS or file so + // shouldn't mark a path or snode as invalid + case 400: + case 404: + return handle_response(false, false, status_code, response, service_node_changes{}); + + // The user's clock is out of sync with the service node network (a + // snode will return 406, but V4 onion requests returns a 425) + case 406: + case 425: + return handle_response(false, false, status_code, response, service_node_changes{}); + + // The snode is reporting that it isn't associated with the given public key anymore. If + // this is the first 421 then we want to try another node in the swarm (just in case it was + // reported incorrectly). If this is the second occurrence of the 421 then the client needs + // to update the swarm (the response might contain updated swarm data) + case 421: + + try { + // If there is no response data or no swarm informaiton was provided then we should + // just replace the swarm + if (!response || !info.swarm) + throw std::invalid_argument{"Unable to handle redirect."}; + + // If this was the first 421 then we want to retry using another node in the swarm + // to get confirmation that we should switch to a different swarm + if (!info.is_retry) { + std::random_device rd; + std::mt19937 g(rd()); + std::vector swarm_copy = *info.swarm; + std::shuffle(swarm_copy.begin(), swarm_copy.end(), g); + + std::optional random_node; + + for (const auto& node : swarm_copy) { + if (node == info.target) + continue; + + random_node = node; + break; + } + + if (!random_node) + throw std::invalid_argument{"No other nodes in the swarm."}; + + if (info.path) { + oxen::log::info(log_cat, "retry onion request"); + return send_onion_request( + *info.path, + SnodeDestination{*random_node, *info.swarm}, + info.body, + info.ed_sk, + true, + handle_response); + } + + return send_request( + {info.ed_sk, + *random_node, + info.endpoint, + info.body, + info.swarm, + std::nullopt, + true}, + handle_response); + } + + if (!response) + throw std::invalid_argument{"No response data."}; + + auto response_json = nlohmann::json::parse(*response); + auto snodes = response_json["snodes"]; + + if (!snodes.is_array()) + throw std::invalid_argument{"Invalid JSON response."}; + + std::vector swarm; + + for (auto snode : snodes) + swarm.emplace_back( + snode["ip"].get(), + snode["port_omq"].get(), + x25519_pubkey::from_hex(snode["pubkey_x25519"].get()), + ed25519_pubkey::from_hex(snode["pubkey_ed25519"].get()), + 0, + false); + + if (swarm.empty()) + throw std::invalid_argument{"No snodes in the response."}; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ServiceNodeChangeType::replace_swarm, swarm}); + } catch (...) { + auto updated_path = info.path.value_or(onion_path{{info.target}, 0}); + updated_path.failure_count += 1; + + // If the path has failed too many times, drop the guard snode and increment the + // failure count of each node in the path + if (updated_path.failure_count >= path_failure_threshold) { + updated_path.nodes[0].invalid = true; + + for (auto it : updated_path.nodes) { + it.failure_count += 1; + + if (it.failure_count >= snode_failure_threshold) + it.invalid = true; + } + } + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_path, + updated_path.nodes, + updated_path.failure_count}); + } + + default: + auto updated_path = info.path.value_or(onion_path{{info.target}, 0}); + bool found_invalid_node = false; + + if (response && starts_with(*response, node_not_found_prefix)) { + std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; + + if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { + session::onionreq::ed25519_pubkey edpk = + session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); + + auto snode_it = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); + + // Increment the failure count for the snode + if (snode_it != updated_path.nodes.end()) { + snode_it->failure_count += 1; + found_invalid_node = true; + + if (snode_it->failure_count >= snode_failure_threshold) + snode_it->invalid = true; + } + } + } + + // If we didn't find the specific node that was invalid then increment the path failure + // count + if (!found_invalid_node) { + // Increment the path failure count + updated_path.failure_count += 1; + + // If the path has failed too many times, drop the guard snode and increment the + // failure count of each node in the path + if (updated_path.failure_count >= path_failure_threshold) { + updated_path.nodes[0].invalid = true; + + for (auto it : updated_path.nodes) { + it.failure_count += 1; + + if (it.failure_count >= snode_failure_threshold) + it.invalid = true; + } + } + } + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_path, + updated_path.nodes, + updated_path.failure_count}); + } +} + +void send_request(const request_info info, network_response_callback_t handle_response) { try { Network net; std::promise prom; - auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(ed_sk))); + auto remote = RemoteAddress{ + info.target.ed25519_pubkey.view(), info.target.ip, info.target.lmq_port}; + auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(info.ed_sk))); auto ep = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); - auto c = ep->connect(target, creds); + auto c = ep->connect(remote, creds); auto s = c->open_stream(); bstring_view payload = {}; - if (body) - payload = *body; + if (info.body) + payload = bstring_view{ + reinterpret_cast(info.body->data()), info.body->size()}; - s->command(std::move(endpoint), payload, [&target, &prom](message resp) { + s->command(info.endpoint, payload, [&info, &prom](message resp) { try { if (resp.timed_out) throw Timeout{}; @@ -79,6 +283,7 @@ void send_request( } }); + // Default to a 200 success if the response is empty but didn't timeout or error std::string response = prom.get_future().get(); int16_t status_code = 200; std::string response_data; @@ -95,31 +300,57 @@ void send_request( response_data = response; } - handle_response(true, false, status_code, response_data); + // If we have a status code that is not in the 2xx range, return the error + if (status_code < 200 || status_code > 299) + return handle_errors(status_code, response_data, info, handle_response); + + handle_response(true, false, status_code, response_data, service_node_changes{}); } catch (const Timeout&) { - handle_response(false, true, -1, "Request timed out"); + handle_response(false, true, -1, "Request timed out", service_node_changes{}); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what()); + handle_response(false, false, -1, e.what(), service_node_changes{}); } } +void send_request( + const ustring_view ed_sk, + const session::network::service_node target, + const std::string endpoint, + const std::optional body, + const std::optional> swarm, + network_response_callback_t handle_response) { + send_request({ed_sk, target, endpoint, body, swarm, std::nullopt}, handle_response); +} + +template +std::optional> swarm_for_destination( + const Destination destination) { + return std::nullopt; +} + +template <> +std::optional> swarm_for_destination( + const SnodeDestination destination) { + return destination.swarm; +} + template void process_response( - const onion_path path, const Builder builder, const Destination destination, const std::string response, - network_onion_response_callback_t handle_response) { - handle_response(false, false, -1, "Invalid destination.", path); + const request_info info, + network_response_callback_t handle_response) { + handle_response(false, false, -1, "Invalid destination.", service_node_changes{}); } template <> void process_response( - const onion_path path, const Builder builder, const SnodeDestination destination, const std::string response, - network_onion_response_callback_t handle_response) { + const request_info info, + network_response_callback_t handle_response) { // The SnodeDestination runs via V3 onion requests try { std::string base64_iv_and_ciphertext; @@ -147,6 +378,7 @@ void process_response( auto result = parser.decrypt(iv_and_ciphertext); auto result_json = nlohmann::json::parse(result); int16_t status_code; + std::string body; if (result_json.contains("status_code") && result_json["status_code"].is_number()) status_code = result_json["status_code"].get(); @@ -156,21 +388,27 @@ void process_response( throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; if (result_json.contains("body") && result_json["body"].is_string()) - handle_response(true, false, status_code, result_json["body"].get(), path); + body = result_json["body"].get(); else - handle_response(true, false, status_code, result_json.dump(), path); + body = result_json.dump(); + + // If we got a non 2xx status code, return the error + if (status_code < 200 || status_code > 299) + return handle_errors(status_code, body, info, handle_response); + + handle_response(true, false, status_code, body, service_node_changes{}); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), path); + handle_response(false, false, -1, e.what(), service_node_changes{}); } } template <> void process_response( - const onion_path path, const Builder builder, const ServerDestination destination, const std::string response, - network_onion_response_callback_t handle_response) { + const request_info info, + network_response_callback_t handle_response) { // The ServerDestination runs via V4 onion requests try { ustring response_data = {to_unsigned(response.data()), response.size()}; @@ -195,27 +433,22 @@ void process_response( // If we have a status code that is not in the 2xx range, return the error if (status_code < 200 || status_code > 299) { - if (result_bencode.is_finished()) { - handle_response(true, false, status_code, std::string(result_sv), path); - return; - } + if (result_bencode.is_finished()) + return handle_errors(status_code, std::nullopt, info, handle_response); - std::string message = result_bencode.consume_string(); - handle_response(true, false, status_code, message, path); - return; + return handle_errors( + status_code, result_bencode.consume_string(), info, handle_response); } // If there is no body just return the success status - if (result_bencode.is_finished()) { - handle_response(true, false, status_code, std::nullopt, path); - return; - } + if (result_bencode.is_finished()) + return handle_response(true, false, status_code, std::nullopt, service_node_changes{}); - // Otherwise process the response - auto response_string = result_bencode.consume_string(); - handle_response(true, false, status_code, response_string, path); + // Otherwise return the result + handle_response( + true, false, status_code, result_bencode.consume_string(), service_node_changes{}); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), path); + handle_response(false, false, -1, e.what(), service_node_changes{}); } } @@ -225,9 +458,15 @@ void send_onion_request( const Destination destination, const std::optional body, const ustring_view ed_sk, - network_onion_response_callback_t handle_response) { + const bool is_retry, + network_response_callback_t handle_response) { if (path.nodes.empty()) { - handle_response(false, false, -1, "No nodes in the path", path); + handle_response( + false, + false, + -1, + "No nodes in the path", + service_node_changes{ServiceNodeChangeType::invalid_path}); return; } @@ -241,101 +480,43 @@ void send_onion_request( auto payload = builder.generate_payload(destination, body); auto onion_req_payload = builder.build(to_unsigned(payload.data())); - bstring_view quic_payload = bstring_view{ - reinterpret_cast(onion_req_payload.data()), - onion_req_payload.size()}; - send_request( + + request_info info = { ed_sk, - RemoteAddress{ - path.nodes[0].ed25519_pubkey.view(), - path.nodes[0].ip, - path.nodes[0].lmq_port}, + path.nodes[0], "onion_req", - quic_payload, + onion_req_payload, + swarm_for_destination(destination), + path, + is_retry}; + + send_request( + info, [builder = std::move(builder), - path = std::move(path), + info, destination = std::move(destination), callback = std::move(handle_response)]( bool success, bool timeout, int16_t status_code, - std::optional response) { - onion_path updated_path = path; - + std::optional response, + service_node_changes changes) { if (!success || timeout || - !ResponseParser::response_long_enough(builder.enc_type, response->size())) { - switch (status_code) { - // A 404 or a 400 is likely due to a bad/missing SOGS or file so - // shouldn't mark a path or snode as invalid - case 400: break; - case 404: break; - - // The user's clock is out of sync with the service node network (a - // snode will return 406, but V4 onion requests returns a 425) - case 406: break; - case 425: break; - - // The snode isn't associated with the given public key anymore (the - // client needs to update the swarm, the response might contain updated - // swarm data) - case 421: updated_path.nodes[0].invalid = true; break; - - default: - std::string node_not_found_prefix = "Next node not found: "; - - if (response.has_value() && - response->substr(0, node_not_found_prefix.size()) == - node_not_found_prefix) { - std::string ed25519PublicKey = - response->substr(response->find(":") + 1); - auto snode = std::find_if( - updated_path.nodes.begin(), - updated_path.nodes.end(), - [&ed25519PublicKey](const auto& node) { - return node.ed25519_pubkey.hex() == - ed25519PublicKey; - }); - - // The node is invalid so mark is as such so it can be dropped - snode->invalid = true; - } else { - // Increment the path failure count - updated_path.failure_count += 1; - - // Increment the failure count for each snode in the path - // (skipping the first as it would be dropped if the path is - // dropped) - for (auto it = updated_path.nodes.begin() + 1; - it != updated_path.nodes.end(); - ++it) { - it->failure_count += 1; - - if (it->failure_count >= snode_failure_threshold) - it->invalid = true; - } - - // If the path has failed too many times, drop the guard snode - if (updated_path.failure_count >= path_failure_threshold) - updated_path.nodes[0].invalid = true; - } - break; - } - - callback(success, timeout, status_code, response, updated_path); - return; - } + !ResponseParser::response_long_enough(builder.enc_type, response->size())) + return handle_errors(status_code, response, info, callback); - process_response(updated_path, builder, destination, *response, callback); + process_response(builder, destination, *response, info, callback); }); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), path); + handle_response(false, false, -1, e.what(), service_node_changes{}); } } -std::vector convert_service_nodes(const std::vector nodes) { - std::vector converted_nodes; +std::vector convert_service_nodes( + const std::vector nodes) { + std::vector converted_nodes; for (const auto& node : nodes) { - onion_request_service_node converted_node; + network_service_node converted_node; strncpy(converted_node.ip, node.ip.c_str(), sizeof(converted_node.ip) - 1); converted_node.ip[sizeof(converted_node.ip) - 1] = '\0'; // Ensure null termination strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); @@ -362,48 +543,78 @@ LIBSESSION_C_API void network_add_logger(void (*callback)(const char*, size_t)) LIBSESSION_C_API void network_send_request( const unsigned char* ed25519_secretkey_bytes, - const remote_address remote, + const network_service_node destination, const char* endpoint, - size_t endpoint_size, const unsigned char* body_, size_t body_size, + const network_service_node* swarm_, + const size_t swarm_count, void (*callback)( bool success, bool timeout, int16_t status_code, const char* response, size_t response_size, + network_service_node_changes changes, void*), void* ctx) { assert(ed25519_secretkey_bytes && endpoint && callback); - std::optional body; + std::optional body; if (body_size > 0) - body = bstring_view{reinterpret_cast(body_), body_size}; - - std::string_view remote_pubkey_hex = {remote.pubkey, 64}; - session::ustring remote_pubkey; - oxenc::from_hex( - remote_pubkey_hex.begin(), remote_pubkey_hex.end(), std::back_inserter(remote_pubkey)); + body = {body_, body_size}; + + std::optional> swarm; + if (swarm_count > 0) + for (size_t i = 0; i < swarm_count; i++) + swarm->emplace_back( + swarm_[i].ip, + swarm_[i].lmq_port, + x25519_pubkey::from_hex({swarm_[i].x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({swarm_[i].ed25519_pubkey_hex, 64}), + swarm_[i].failure_count, + false); send_request( {ed25519_secretkey_bytes, 64}, - {remote_pubkey, remote.ip, remote.port}, - {endpoint, endpoint_size}, + session::network::service_node{ + destination.ip, + destination.lmq_port, + x25519_pubkey::from_hex({destination.x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({destination.ed25519_pubkey_hex, 64}), + destination.failure_count, + false}, + endpoint, body, + swarm, [callback, ctx]( bool success, bool timeout, int status_code, - std::optional response) { - callback(success, timeout, status_code, response->data(), response->size(), ctx); + std::optional response, + service_node_changes changes) { + auto c_nodes = session::network::convert_service_nodes(changes.nodes); + auto c_changes = network_service_node_changes{ + static_cast(changes.type), + c_nodes.data(), + c_nodes.size(), + changes.path_failure_count}; + + callback( + success, + timeout, + status_code, + response->data(), + response->size(), + c_changes, + ctx); }); } LIBSESSION_C_API void network_send_onion_request_to_snode_destination( const onion_request_path path_, const unsigned char* ed25519_secretkey_bytes, - const onion_request_service_node node, + const onion_request_service_node_destination node, const unsigned char* body_, size_t body_size, void (*callback)( @@ -412,20 +623,21 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( int16_t status_code, const char* response, size_t response_size, - onion_request_path updated_failures_path, + network_service_node_changes changes, void*), void* ctx) { assert(ed25519_secretkey_bytes && callback); try { - std::vector nodes; + std::vector nodes; for (size_t i = 0; i < path_.nodes_count; i++) nodes.emplace_back( path_.nodes[i].ip, path_.nodes[i].lmq_port, x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), - path_.nodes[i].failure_count); + path_.nodes[i].failure_count, + false); session::onionreq::onion_path path = {nodes, path_.failure_count}; @@ -433,6 +645,17 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( if (body_size > 0) body = {body_, body_size}; + std::optional> swarm; + if (node.swarm_count > 0) + for (size_t i = 0; i < node.swarm_count; i++) + swarm->emplace_back( + node.swarm[i].ip, + node.swarm[i].lmq_port, + x25519_pubkey::from_hex({node.swarm[i].x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({node.swarm[i].ed25519_pubkey_hex, 64}), + node.swarm[i].failure_count, + false); + send_onion_request( path, SnodeDestination{ @@ -440,28 +663,43 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( node.lmq_port, x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), - node.failure_count}}, + node.failure_count, + false}, + swarm}, body, {ed25519_secretkey_bytes, 64}, + false, [callback, ctx]( bool success, bool timeout, int status_code, std::optional response, - session::onionreq::onion_path updated_failures_path) { - auto nodes = session::network::convert_service_nodes(updated_failures_path.nodes); - auto updated_path = onion_request_path{nodes.data(), nodes.size(), updated_failures_path.failure_count}; + service_node_changes changes) { + auto c_nodes = session::network::convert_service_nodes(changes.nodes); + auto c_changes = network_service_node_changes{ + static_cast(changes.type), + c_nodes.data(), + c_nodes.size(), + changes.path_failure_count}; + callback( success, timeout, status_code, response->data(), response->size(), - updated_path, + c_changes, ctx); }); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), path_, ctx); + callback( + false, + false, + -1, + e.what(), + std::strlen(e.what()), + network_service_node_changes{}, + ctx); } } @@ -485,20 +723,22 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, - onion_request_path updated_failures_path, + network_service_node_changes changes, void*), void* ctx) { - assert(ed25519_secretkey_bytes && method && protocol && host && endpoint && x25519_pubkey && callback); + assert(ed25519_secretkey_bytes && method && protocol && host && endpoint && x25519_pubkey && + callback); try { - std::vector nodes; + std::vector nodes; for (size_t i = 0; i < path_.nodes_count; i++) nodes.emplace_back( path_.nodes[i].ip, path_.nodes[i].lmq_port, x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), - path_.nodes[i].failure_count); + path_.nodes[i].failure_count, + false); session::onionreq::onion_path path = {nodes, path_.failure_count}; std::optional>> headers; @@ -525,25 +765,38 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers}, body, {ed25519_secretkey_bytes, 64}, + false, [callback, ctx]( bool success, bool timeout, int status_code, std::optional response, - session::onionreq::onion_path updated_failures_path) { - auto nodes = session::network::convert_service_nodes(updated_failures_path.nodes); - auto updated_path = onion_request_path{nodes.data(), nodes.size(), updated_failures_path.failure_count}; + service_node_changes changes) { + auto c_nodes = session::network::convert_service_nodes(changes.nodes); + auto c_changes = network_service_node_changes{ + static_cast(changes.type), + c_nodes.data(), + c_nodes.size(), + changes.path_failure_count}; + callback( success, timeout, status_code, response->data(), response->size(), - updated_path, + c_changes, ctx); }); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), path_, ctx); + callback( + false, + false, + -1, + e.what(), + std::strlen(e.what()), + network_service_node_changes{}, + ctx); } } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 7cbd49f0..7ba10194 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -107,9 +107,7 @@ std::string Builder::generate_payload( // Structure the request information nlohmann::json request_info{ - {"method", destination.method}, - {"endpoint", endpoint}, - {"headers", headers_json}}; + {"method", destination.method}, {"endpoint", endpoint}, {"headers", headers_json}}; auto request_info_dump = request_info.dump(); std::vector payload{request_info_dump}; @@ -291,15 +289,17 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( const char* ip, const uint16_t lmq_port, const char* x25519_pubkey, - const char* ed25519_pubkey) { + const char* ed25519_pubkey, + const uint8_t failure_count) { assert(builder && ip && x25519_pubkey && ed25519_pubkey); - auto node = session::onionreq::service_node{ + auto node = session::network::service_node{ ip, lmq_port, session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), - 0}; + failure_count, + false}; unbox(builder).set_destination(session::onionreq::SnodeDestination{node}); } From a8c49871a7982f731651860ee0a84fc2e68f289c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 2 Apr 2024 16:32:55 +1100 Subject: [PATCH 164/572] Fixed file uploads and requests with query params --- include/session/network.h | 7 +- include/session/network.hpp | 2 + include/session/network_service_node.h | 2 +- include/session/onionreq/builder.hpp | 19 +++-- src/network.cpp | 99 ++++++++++++++++++++------ src/onionreq/builder.cpp | 30 +++++--- 6 files changed, 116 insertions(+), 43 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index 962fda3f..55477512 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -16,6 +16,7 @@ typedef enum SERVICE_NODE_CHANGE_TYPE { SERVICE_NODE_CHANGE_TYPE_INVALID_PATH = 1, SERVICE_NODE_CHANGE_TYPE_REPLACE_SWARM = 2, SERVICE_NODE_CHANGE_TYPE_UPDATE_PATH = 3, + SERVICE_NODE_CHANGE_TYPE_UPDATE_NODE = 4, } SERVICE_NODE_CHANGE_TYPE; typedef struct network_service_node_changes { @@ -23,6 +24,7 @@ typedef struct network_service_node_changes { network_service_node* nodes; size_t nodes_count; uint8_t failure_count; + bool invalid; } network_service_node_changes; LIBSESSION_EXPORT void network_add_logger(void (*callback)(const char*, size_t)); @@ -70,7 +72,10 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( const char* endpoint, uint16_t port, const char* x25519_pubkey, - const char** headers_, + const char** query_param_keys, + const char** query_param_values, + size_t query_params_size, + const char** headers, const char** header_values, size_t headers_size, const unsigned char* body, diff --git a/include/session/network.hpp b/include/session/network.hpp index 690ea6b8..390d3b7b 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -14,12 +14,14 @@ enum class ServiceNodeChangeType { invalid_path = 1, replace_swarm = 2, update_path = 3, + update_node = 4, }; struct service_node_changes { ServiceNodeChangeType type = ServiceNodeChangeType::none; std::vector nodes = {}; uint8_t path_failure_count = 0; + bool path_invalid = false; }; using network_response_callback_t = std::function #include #include -#include typedef struct network_service_node { char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 9b2ad6bd..c418123d 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -13,16 +13,12 @@ struct onion_path { uint8_t failure_count; }; -class SnodeDestination { - public: +struct SnodeDestination { session::network::service_node node; std::optional> swarm; - - ustring generate_payload(std::optional body) const; }; -class ServerDestination { - public: +struct ServerDestination { std::string protocol; std::string host; std::string endpoint; @@ -30,6 +26,7 @@ class ServerDestination { std::string method; std::optional port; std::optional>> headers; + std::optional>> query_params; ServerDestination( std::string protocol, @@ -38,17 +35,17 @@ class ServerDestination { session::onionreq::x25519_pubkey x25519_pubkey, std::string method = "GET", std::optional port = std::nullopt, - std::optional>> headers = + std::optional>> headers = std::nullopt, + std::optional>> query_params = std::nullopt) : protocol{std::move(protocol)}, host{std::move(host)}, endpoint{std::move(endpoint)}, x25519_pubkey{std::move(x25519_pubkey)}, + method{std::move(method)}, port{std::move(port)}, headers{std::move(headers)}, - method{std::move(method)} {} - - ustring generate_payload(std::optional body) const; + query_params{std::move(query_params)} {} }; enum class EncryptType { @@ -83,7 +80,7 @@ class Builder { void set_destination(Destination destination); template - std::string generate_payload(Destination destination, std::optional body) const; + ustring generate_payload(Destination destination, std::optional body) const; void add_hop(std::pair keys) { hops_.push_back(keys); } diff --git a/src/network.cpp b/src/network.cpp index be22b15b..b7b38b34 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -82,13 +82,13 @@ void handle_errors( // The snode is reporting that it isn't associated with the given public key anymore. If // this is the first 421 then we want to try another node in the swarm (just in case it was // reported incorrectly). If this is the second occurrence of the 421 then the client needs - // to update the swarm (the response might contain updated swarm data) + // to update the swarm (if the response contains updated swarm data), or increment the path + // failure count. case 421: - try { // If there is no response data or no swarm informaiton was provided then we should // just replace the swarm - if (!response || !info.swarm) + if (!info.swarm) throw std::invalid_argument{"Unable to handle redirect."}; // If this was the first 421 then we want to retry using another node in the swarm @@ -112,8 +112,7 @@ void handle_errors( if (!random_node) throw std::invalid_argument{"No other nodes in the swarm."}; - if (info.path) { - oxen::log::info(log_cat, "retry onion request"); + if (info.path) return send_onion_request( *info.path, SnodeDestination{*random_node, *info.swarm}, @@ -121,7 +120,6 @@ void handle_errors( info.ed_sk, true, handle_response); - } return send_request( {info.ed_sk, @@ -164,11 +162,29 @@ void handle_errors( response, service_node_changes{ServiceNodeChangeType::replace_swarm, swarm}); } catch (...) { - auto updated_path = info.path.value_or(onion_path{{info.target}, 0}); + // If we don't have a path then this is a direct request so we can only update the + // failure count for the target node + if (!info.path) { + auto updated_node = info.target; + updated_node.failure_count += 1; + + if (updated_node.failure_count >= snode_failure_threshold) + updated_node.invalid = true; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_node, {updated_node}}); + } + + auto updated_path = *info.path; updated_path.failure_count += 1; - // If the path has failed too many times, drop the guard snode and increment the - // failure count of each node in the path + // If the path has failed too many times we want to drop the guard snode (marking it + // as invalid) and increment the failure count of each node in the path if (updated_path.failure_count >= path_failure_threshold) { updated_path.nodes[0].invalid = true; @@ -188,11 +204,29 @@ void handle_errors( service_node_changes{ ServiceNodeChangeType::update_path, updated_path.nodes, - updated_path.failure_count}); + updated_path.failure_count, + (updated_path.failure_count >= path_failure_threshold)}); } default: - auto updated_path = info.path.value_or(onion_path{{info.target}, 0}); + // If we don't have a path then this is a direct request so we can only update the + // failure count for the target node + if (!info.path) { + auto updated_node = info.target; + updated_node.failure_count += 1; + + if (updated_node.failure_count >= snode_failure_threshold) + updated_node.invalid = true; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ServiceNodeChangeType::update_node, {updated_node}}); + } + + auto updated_path = *info.path; bool found_invalid_node = false; if (response && starts_with(*response, node_not_found_prefix)) { @@ -224,8 +258,8 @@ void handle_errors( // Increment the path failure count updated_path.failure_count += 1; - // If the path has failed too many times, drop the guard snode and increment the - // failure count of each node in the path + // If the path has failed too many times we want to drop the guard snode (marking it + // as invalid) and increment the failure count of each node in the path if (updated_path.failure_count >= path_failure_threshold) { updated_path.nodes[0].invalid = true; @@ -246,7 +280,8 @@ void handle_errors( service_node_changes{ ServiceNodeChangeType::update_path, updated_path.nodes, - updated_path.failure_count}); + updated_path.failure_count, + (updated_path.failure_count >= path_failure_threshold)}); } } @@ -479,7 +514,7 @@ void send_onion_request( builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); auto payload = builder.generate_payload(destination, body); - auto onion_req_payload = builder.build(to_unsigned(payload.data())); + auto onion_req_payload = builder.build(payload); request_info info = { ed_sk, @@ -565,7 +600,11 @@ LIBSESSION_C_API void network_send_request( body = {body_, body_size}; std::optional> swarm; - if (swarm_count > 0) + + if (swarm_count > 0) { + swarm = std::vector{}; + swarm->reserve(swarm_count); + for (size_t i = 0; i < swarm_count; i++) swarm->emplace_back( swarm_[i].ip, @@ -574,6 +613,7 @@ LIBSESSION_C_API void network_send_request( ed25519_pubkey::from_hex({swarm_[i].ed25519_pubkey_hex, 64}), swarm_[i].failure_count, false); + } send_request( {ed25519_secretkey_bytes, 64}, @@ -646,7 +686,11 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( body = {body_, body_size}; std::optional> swarm; - if (node.swarm_count > 0) + + if (node.swarm_count > 0) { + swarm = std::vector{}; + swarm->reserve(node.swarm_count); + for (size_t i = 0; i < node.swarm_count; i++) swarm->emplace_back( node.swarm[i].ip, @@ -655,6 +699,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( ed25519_pubkey::from_hex({node.swarm[i].ed25519_pubkey_hex, 64}), node.swarm[i].failure_count, false); + } send_onion_request( path, @@ -680,7 +725,8 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( static_cast(changes.type), c_nodes.data(), c_nodes.size(), - changes.path_failure_count}; + changes.path_failure_count, + changes.path_invalid}; callback( success, @@ -712,6 +758,9 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( const char* endpoint, uint16_t port, const char* x25519_pubkey, + const char** query_param_keys, + const char** query_param_values, + size_t query_params_size, const char** headers_, const char** header_values, size_t headers_size, @@ -749,6 +798,14 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers->emplace_back(headers_[i], header_values[i]); } + std::optional>> query_params; + if (query_params_size > 0) { + query_params = std::vector>{}; + + for (size_t i = 0; i < query_params_size; i++) + query_params->emplace_back(query_param_keys[i], query_param_values[i]); + } + std::optional body; if (body_size > 0) body = {body_, body_size}; @@ -762,7 +819,8 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( x25519_pubkey::from_hex({x25519_pubkey, 64}), method, port, - headers}, + headers, + query_params}, body, {ed25519_secretkey_bytes, 64}, false, @@ -777,7 +835,8 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( static_cast(changes.type), c_nodes.data(), c_nodes.size(), - changes.path_failure_count}; + changes.path_failure_count, + changes.path_invalid}; callback( success, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 7ba10194..4595b291 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -72,20 +72,17 @@ void Builder::set_destination(ServerDestination destination) { } template -std::string Builder::generate_payload(Destination destination, std::optional body) const { +ustring Builder::generate_payload(Destination destination, std::optional body) const { throw std::runtime_error{"Invalid destination."}; } template <> -std::string Builder::generate_payload( - SnodeDestination destination, std::optional body) const { - if (body.has_value()) - return std::string(from_unsigned_sv(*body)); - return ""; +ustring Builder::generate_payload(SnodeDestination destination, std::optional body) const { + return body.value_or(ustring{}); } template <> -std::string Builder::generate_payload( +ustring Builder::generate_payload( ServerDestination destination, std::optional body) const { auto headers_json = nlohmann::json::object(); @@ -105,6 +102,18 @@ std::string Builder::generate_payload( if (!endpoint.empty() && endpoint.front() != '/') endpoint = '/' + endpoint; + // If we have query parameters, add them to the endpoint to be included in the payload + if (destination.query_params && !destination.query_params->empty()) { + std::string query_string = ""; + + for (auto& query : *destination.query_params) { + query_string += query.first + "=" + query.second + "&"; + } + + // Drop the extra '&' from the end + endpoint += "?" + query_string.substr(0, query_string.size() - 1); + } + // Structure the request information nlohmann::json request_info{ {"method", destination.method}, {"endpoint", endpoint}, {"headers", headers_json}}; @@ -112,11 +121,11 @@ std::string Builder::generate_payload( std::vector payload{request_info_dump}; // If we were given a body, add it to the payload - if (body.has_value()) payload.emplace_back(std::string(from_unsigned(body->data()), body->size())); - return oxenc::bt_serialize(payload); + auto result = oxenc::bt_serialize(payload); + return {to_unsigned(result.data()), result.size()}; } ustring Builder::build(ustring payload) { @@ -182,7 +191,7 @@ ustring Builder::build(ustring payload) { {"enc_type", to_string(enc_type)}, }; - blob = e.encrypt(enc_type, payload.data(), *destination_x25519_public_key); + blob = e.encrypt(enc_type, payload, *destination_x25519_public_key); } else if (ed25519_public_key_ && destination_x25519_public_key) { nlohmann::json control{{"headers", ""}}; final_route = { @@ -320,6 +329,7 @@ LIBSESSION_C_API void onion_request_builder_set_server_destination( session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), method, port, + std::nullopt, std::nullopt}); } From 7b5084836eac46a16d1638432c77a94e3206e905 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Apr 2024 17:11:53 +1100 Subject: [PATCH 165/572] Fixed a couple of bugs and added some unit tests --- external/oxen-libquic | 2 +- include/session/network.hpp | 16 ++ include/session/util.hpp | 2 +- src/network.cpp | 16 +- tests/CMakeLists.txt | 2 + tests/test_network.cpp | 432 ++++++++++++++++++++++++++++++++++++ 6 files changed, 454 insertions(+), 16 deletions(-) create mode 100644 tests/test_network.cpp diff --git a/external/oxen-libquic b/external/oxen-libquic index 381602cc..04ecc083 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 381602cc9d842ff253ef8d8c8e9d91f4175a038a +Subproject commit 04ecc08366abb7c23e2179ebf02f8d0379b2b9d9 diff --git a/include/session/network.hpp b/include/session/network.hpp index 390d3b7b..970dbe3a 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -24,6 +24,16 @@ struct service_node_changes { bool path_invalid = false; }; +struct request_info { + const ustring_view ed_sk; + const service_node target; + const std::string endpoint; + const std::optional body; + const std::optional> swarm; + const std::optional path; + const bool is_retry; +}; + using network_response_callback_t = std::function response, service_node_changes changes)>; +void handle_errors( + const int16_t status_code, + const std::optional response, + const request_info info, + network_response_callback_t handle_response); + void send_request( const ustring_view ed_sk, const session::network::service_node target, diff --git a/include/session/util.hpp b/include/session/util.hpp index 65ae5fb2..cefe52dd 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -80,7 +80,7 @@ inline bool string_iequal(std::string_view s1, std::string_view s2) { // C++20 starts_/ends_with backport inline constexpr bool starts_with(std::string_view str, std::string_view prefix) { - return str.size() >= prefix.size() && str.substr(prefix.size()) == prefix; + return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix; } inline constexpr bool end_with(std::string_view str, std::string_view suffix) { diff --git a/src/network.cpp b/src/network.cpp index b7b38b34..8d45659b 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -29,18 +29,6 @@ using namespace std::literals; namespace session::network { -namespace { - struct request_info { - const ustring_view ed_sk; - const service_node target; - const std::string endpoint; - const std::optional body; - const std::optional> swarm; - const std::optional path; - const bool is_retry = false; - }; -} // namespace - class Timeout : public std::exception {}; // The number of times a path can fail before it's replaced. @@ -188,7 +176,7 @@ void handle_errors( if (updated_path.failure_count >= path_failure_threshold) { updated_path.nodes[0].invalid = true; - for (auto it : updated_path.nodes) { + for (auto& it : updated_path.nodes) { it.failure_count += 1; if (it.failure_count >= snode_failure_threshold) @@ -263,7 +251,7 @@ void handle_errors( if (updated_path.failure_count >= path_failure_threshold) { updated_path.nodes[0].invalid = true; - for (auto it : updated_path.nodes) { + for (auto& it : updated_path.nodes) { it.failure_count += 1; if (it.failure_count >= snode_failure_threshold) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e1eed8ed..7ed73d6a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(testAll test_group_members.cpp test_hash.cpp test_multi_encrypt.cpp + test_network.cpp test_onionreq.cpp test_proto.cpp test_random.cpp @@ -29,6 +30,7 @@ target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq libsodium::sodium-internal + nlohmann_json::nlohmann_json Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) diff --git a/tests/test_network.cpp b/tests/test_network.cpp new file mode 100644 index 00000000..fede803c --- /dev/null +++ b/tests/test_network.cpp @@ -0,0 +1,432 @@ +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace session; +using namespace session::onionreq; +using namespace session::network; + +TEST_CASE("Network error handling", "[network]") { + struct Result { + bool success; + bool timeout; + int16_t status_code; + std::optional response; + service_node_changes changes; + }; + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto x_pk = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; + auto x_pk2 = "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes; + auto target = service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk), + ed25519_pubkey::from_bytes(ed_pk), + 0, + false}; + auto mock_request = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, std::nullopt, false}; + Result result; + + // Check the handling of the codes which make no changes + auto codes_with_no_changes = {400, 404, 406, 425}; + + for (auto code : codes_with_no_changes) { + handle_errors( + code, + std::nullopt, + mock_request, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == code); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::none); + } + + // Check general error handling with no provided path (first failure) + handle_errors( + 500, + std::nullopt, + mock_request, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::update_node); + REQUIRE(result.changes.nodes.size() == 1); + CHECK(result.changes.nodes[0].ip == target.ip); + CHECK(result.changes.nodes[0].lmq_port == target.lmq_port); + CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); + CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); + CHECK(result.changes.nodes[0].failure_count == 1); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.path_failure_count == 0); + + // Check general error handling with no provided path (too many failures) + auto mock_request2 = request_info{ + ed_sk, + service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk), + ed25519_pubkey::from_bytes(ed_pk), + 9, + false}, + "test", + std::nullopt, + std::nullopt, + std::nullopt, + false}; + handle_errors( + 500, + std::nullopt, + mock_request2, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::update_node); + REQUIRE(result.changes.nodes.size() == 1); + CHECK(result.changes.nodes[0].failure_count == 10); + CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.path_failure_count == 0); + + // Check general error handling with a path but no response (first failure) + auto path = onion_path{{target}, 0}; + auto mock_request3 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + handle_errors( + 500, + std::nullopt, + mock_request3, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 1); + CHECK(result.changes.nodes[0].failure_count == 0); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.path_failure_count == 1); + + // Check general error handling with a path but no response (too many path failures) + path = onion_path{ + {target, + service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk), + ed25519_pubkey::from_bytes(ed_pk), + 0, + false}}, + 9}; + auto mock_request4 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + handle_errors( + 500, + std::nullopt, + mock_request4, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 2); + CHECK(result.changes.nodes[0].failure_count == 1); + CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.nodes[1].failure_count == 1); + CHECK(result.changes.nodes[1].invalid == false); + CHECK(result.changes.path_failure_count == 10); + + // Check general error handling with a path but no response (too many path & node failures) + path = onion_path{ + {target, + service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk), + ed25519_pubkey::from_bytes(ed_pk), + 9, + false}}, + 9}; + auto mock_request5 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + handle_errors( + 500, + std::nullopt, + mock_request5, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 2); + CHECK(result.changes.nodes[0].failure_count == 1); + CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.nodes[1].failure_count == 10); + CHECK(result.changes.nodes[1].invalid == true); + CHECK(result.changes.path_failure_count == 10); + + // Check general error handling with a path and a random response (first failure) + path = onion_path{{target}, 0}; + auto mock_request6 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + auto response = std::string{"Test"}; + handle_errors( + 500, + response, + mock_request6, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK(result.response == response); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 1); + CHECK(result.changes.nodes[0].failure_count == 0); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.path_failure_count == 1); + + // Check general error handling with a path and specific node failure (first failure) + path = onion_path{ + {target, + service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk2), + ed25519_pubkey::from_bytes(ed_pk2), + 0, + false}}, + 0}; + auto mock_request7 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); + handle_errors( + 500, + response, + mock_request7, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK(result.response == response); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 2); + CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); + CHECK(result.changes.nodes[0].failure_count == 0); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); + CHECK(result.changes.nodes[1].failure_count == 1); + CHECK(result.changes.nodes[1].invalid == false); + CHECK(result.changes.path_failure_count == 0); + + // Check general error handling with a path and specific node failure (too many failures) + path = onion_path{ + {target, + service_node{ + "0.0.0.0", + 0, + x25519_pubkey::from_bytes(x_pk2), + ed25519_pubkey::from_bytes(ed_pk2), + 9, + false}}, + 0}; + auto mock_request8 = + request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); + handle_errors( + 500, + response, + mock_request8, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 500); + CHECK(result.response == response); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + REQUIRE(result.changes.nodes.size() == 2); + CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); + CHECK(result.changes.nodes[0].failure_count == 0); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); + CHECK(result.changes.nodes[1].failure_count == 10); + CHECK(result.changes.nodes[1].invalid == true); + CHECK(result.changes.path_failure_count == 0); + + // Check a 421 with no swarm data throws (no good way to handle this case) + handle_errors( + 421, + std::nullopt, + mock_request, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 421); + CHECK(result.changes.type == ServiceNodeChangeType::update_node); + + // Check the retry request of a 421 with no response data throws (no good way to handle this + // case) + auto mock_request9 = request_info{ed_sk, target, "test", std::nullopt, std::vector{target}, path, true}; + handle_errors( + 421, + std::nullopt, + mock_request9, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 421); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + + // Check the retry request of a 421 with non-swarm response data throws (no good way to handle + // this case) + handle_errors( + 421, + "Test", + mock_request9, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 421); + CHECK(result.changes.type == ServiceNodeChangeType::update_path); + + // Check the retry request of a 421 instructs to replace the swarm + auto snodes = nlohmann::json::array(); + snodes.push_back( + {{"ip", "1.1.1.1"}, + {"port_omq", 1}, + {"pubkey_x25519", x25519_pubkey::from_bytes(x_pk).hex()}, + {"pubkey_ed25519", x25519_pubkey::from_bytes(ed_pk).hex()}}); + nlohmann::json swarm_json{{"snodes", snodes}}; + response = swarm_json.dump(); + handle_errors( + 421, + response, + mock_request9, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + + CHECK(result.success == false); + CHECK(result.timeout == false); + CHECK(result.status_code == 421); + CHECK(result.changes.type == ServiceNodeChangeType::replace_swarm); + REQUIRE(result.changes.nodes.size() == 1); + CHECK(result.changes.nodes[0].ip == "1.1.1.1"); + CHECK(result.changes.nodes[0].lmq_port == 1); + CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); + CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); + CHECK(result.changes.nodes[0].failure_count == 0); + CHECK(result.changes.nodes[0].invalid == false); + CHECK(result.changes.path_failure_count == 0); +} From aeb86c29d2e0cb0a21d612a3638322b08be97e71 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Apr 2024 09:27:21 +1100 Subject: [PATCH 166/572] Updated to the latest libquic commit --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 04ecc083..7b2e0d03 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 04ecc08366abb7c23e2179ebf02f8d0379b2b9d9 +Subproject commit 7b2e0d03d95a05bfc5e12e0e90e8b64c03e4f274 From cbc37c8fc5bed11cfd13c69779dafb4ad45c26a7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 5 Apr 2024 12:49:07 -0300 Subject: [PATCH 167/572] Fix link dependency recursion for interface libraries LINK_LIBRARIES apparently isn't set for pure interface libraries, so we weren't picking up some needed libraries found via interface targets when building a bundled static lib; this fixes it by following INTERFACE_LINK_LIBRARIES when we recurse into an interface library. --- cmake/AddStaticBundleLib.cmake | 7 ++++++- external/CMakeLists.txt | 1 + external/oxen-libquic | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmake/AddStaticBundleLib.cmake b/cmake/AddStaticBundleLib.cmake index f4718d3f..2e37c0b6 100644 --- a/cmake/AddStaticBundleLib.cmake +++ b/cmake/AddStaticBundleLib.cmake @@ -22,7 +22,12 @@ function(libsession_static_bundle) _libsession_static_bundle_append("${tgt}") endif() - get_target_property(tgt_link_deps ${tgt} LINK_LIBRARIES) + if(tgt_type STREQUAL INTERFACE_LIBRARY) + get_target_property(tgt_link_deps ${tgt} INTERFACE_LINK_LIBRARIES) + else() + get_target_property(tgt_link_deps ${tgt} LINK_LIBRARIES) + endif() + if(tgt_link_deps) libsession_static_bundle(${tgt_link_deps}) endif() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 53a3fbf3..4a432aa8 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -27,6 +27,7 @@ if(SUBMODULE_CHECK) check_submodule(ios-cmake) check_submodule(libsodium-internal) check_submodule(oxen-encoding) + check_submodule(oxen-libquic) check_submodule(nlohmann-json) check_submodule(zstd) endif() diff --git a/external/oxen-libquic b/external/oxen-libquic index 7b2e0d03..f1ddb319 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 7b2e0d03d95a05bfc5e12e0e90e8b64c03e4f274 +Subproject commit f1ddb319d389838e09e3525a12c3b7e3c4662850 From 5cdc346e9e633a1f80d388184fe00e7ca9d6ee9c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 5 Apr 2024 18:37:40 -0300 Subject: [PATCH 168/572] Bump to latest libquic with xcode/ios build updates --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index f1ddb319..a272af6d 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit f1ddb319d389838e09e3525a12c3b7e3c4662850 +Subproject commit a272af6d18af1c39cdd740051a4a326660159886 From 0e192e3a9bae02469876d1bca07b7e3ad5007f09 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 6 Apr 2024 10:53:26 +1100 Subject: [PATCH 169/572] Ran formatter and fixed test build (not sure we want the change though) --- tests/CMakeLists.txt | 2 -- tests/test_network.cpp | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ed73d6a..0dadb8e5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,8 +33,6 @@ target_link_libraries(testAll PRIVATE nlohmann_json::nlohmann_json Catch2::Catch2WithMain) -add_custom_target(check COMMAND testAll) - add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index fede803c..12f3adbe 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -9,7 +9,7 @@ using namespace session; using namespace session::onionreq; using namespace session::network; -TEST_CASE("Network error handling", "[network]") { +namespace { struct Result { bool success; bool timeout; @@ -17,6 +17,9 @@ TEST_CASE("Network error handling", "[network]") { std::optional response; service_node_changes changes; }; +} // namespace + +TEST_CASE("Network error handling", "[network]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; auto ed_sk = From 93c57273fe12acb7dccca40b4d2ac108be98983d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 6 Apr 2024 10:55:35 +1100 Subject: [PATCH 170/572] Added an if statement around the check custom target --- tests/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0dadb8e5..3cfe7234 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,10 @@ target_link_libraries(testAll PRIVATE nlohmann_json::nlohmann_json Catch2::Catch2WithMain) +if(NOT TARGET check) + add_custom_target(check COMMAND testAll) +endif() + add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) From b5737a902e18b700f0705e5ff1c89a5ed87ad397 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 6 Apr 2024 11:20:34 +1100 Subject: [PATCH 171/572] Ran the formatter --- tests/test_network.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 12f3adbe..c4b5e26e 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -10,14 +10,14 @@ using namespace session::onionreq; using namespace session::network; namespace { - struct Result { - bool success; - bool timeout; - int16_t status_code; - std::optional response; - service_node_changes changes; - }; -} // namespace +struct Result { + bool success; + bool timeout; + int16_t status_code; + std::optional response; + service_node_changes changes; +}; +} // namespace TEST_CASE("Network error handling", "[network]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; @@ -361,7 +361,8 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with no response data throws (no good way to handle this // case) - auto mock_request9 = request_info{ed_sk, target, "test", std::nullopt, std::vector{target}, path, true}; + auto mock_request9 = request_info{ + ed_sk, target, "test", std::nullopt, std::vector{target}, path, true}; handle_errors( 421, std::nullopt, From e727de26ceacc98bb686fc0545cbc7375c2d83f4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 08:41:05 +1000 Subject: [PATCH 172/572] Added a couple of basic standard network request unit tests --- tests/test_network.cpp | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c4b5e26e..9a50d8e9 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -434,3 +436,87 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.nodes[0].invalid == false); CHECK(result.changes.path_failure_count == 0); } + +TEST_CASE("Network direct request", "[send_request][network]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto test_service_node = service_node{ + "144.76.164.202", + 35400, + x25519_pubkey::from_bytes( + "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"_hexbytes), + ed25519_pubkey::from_bytes( + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes), + 0, + false}; + Result result; + + send_request( + ed_sk, + test_service_node, + "info", + std::nullopt, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response, + service_node_changes changes) { + result = {success, timeout, status_code, response, changes}; + }); + CHECK(result.success == true); + CHECK(result.timeout == false); + CHECK(result.status_code == 200); + CHECK(result.changes.type == ServiceNodeChangeType::none); + REQUIRE(result.response.has_value()); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); +} + +TEST_CASE("Network direct request C API", "[network_send_request][network]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto test_service_node = network_service_node{}; + test_service_node.lmq_port = 35400; + std::strcpy(test_service_node.ip, "144.76.164.202"); + std::strcpy(test_service_node.x25519_pubkey_hex, "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"); + std::strcpy(test_service_node.ed25519_pubkey_hex, "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); + + network_send_request( + ed_sk.data(), + test_service_node, + "info", + nullptr, + 0, + nullptr, + 0, + [](bool success, + bool timeout, + int16_t status_code, + const char* c_response, + size_t response_size, + network_service_node_changes changes, + void*) { + CHECK(success == true); + CHECK(timeout == false); + CHECK(status_code == 200); + CHECK(changes.type == SERVICE_NODE_CHANGE_TYPE_NONE); + REQUIRE(response_size != 0); + + auto response_str = std::string(c_response, response_size); + REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); + + auto response = nlohmann::json::parse(response_str); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); + }, + nullptr); +} From edfbd20c1064fef68919006c6928e90aabd8635b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 08:56:03 +1000 Subject: [PATCH 173/572] Drop old "lokinet-ci-" docker image prefix --- .drone.jsonnet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 5b574798..71be9ec5 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -15,7 +15,7 @@ local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; local default_deps_nocxx = ['nlohmann-json3-dev']; local default_deps = ['g++'] + default_deps_nocxx; -local docker_base = 'registry.oxen.rocks/lokinet-ci-'; +local docker_base = 'registry.oxen.rocks/'; // Do something on a debian-like system local debian_pipeline(name, @@ -317,7 +317,7 @@ local static_build(name, debian_build('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), // Windows builds (x64) - windows_cross_pipeline('Windows (amd64)', docker_base + 'debian-win32-cross-wine'), + windows_cross_pipeline('Windows (amd64)', docker_base + 'debian-win32-cross'), // Macos builds: mac_builder('macOS (Release)'), From fd6838e19c25ab8cbf45422c2992382474ecc399 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 09:21:26 +1000 Subject: [PATCH 174/572] Added gnutls-bin to the default deps --- .drone.jsonnet | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 71be9ec5..c32fc965 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,5 +1,3 @@ -local docker_base = 'registry.oxen.rocks/lokinet-ci-'; - local submodule_commands = [ 'git fetch --tags', 'git submodule update --init --recursive --depth=1 --jobs=4', @@ -12,7 +10,7 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local default_deps_nocxx = ['nlohmann-json3-dev']; +local default_deps_nocxx = ['nlohmann-json3-dev', 'gnutls-bin']; local default_deps = ['g++'] + default_deps_nocxx; local docker_base = 'registry.oxen.rocks/'; From e7249fd3c03f96c119c3cd8e5fa0fa5bf4647773 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 09:56:15 +1000 Subject: [PATCH 175/572] More tweaks to the default deps trying to fix CI builds --- .drone.jsonnet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index c32fc965..c97a7c67 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,7 +10,11 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local default_deps_nocxx = ['nlohmann-json3-dev', 'gnutls-bin']; +local default_deps_nocxx = [ + 'nlohmann-json3-dev', + 'gnutls-bin', + 'libgnutls28-dev', +]; local default_deps = ['g++'] + default_deps_nocxx; local docker_base = 'registry.oxen.rocks/'; From 6084fe60816d6d0d18336e3469fc0ead494a14f3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 11:03:11 +1000 Subject: [PATCH 176/572] Fix warnings and ran formatter --- include/session/network_service_node.h | 4 +-- include/session/onionreq/builder.h | 4 +-- src/network.cpp | 43 +++++++++++--------------- src/onionreq/builder.cpp | 2 +- tests/test_network.cpp | 8 +++-- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/include/session/network_service_node.h b/include/session/network_service_node.h index 0f0889d6..e81de1d4 100644 --- a/include/session/network_service_node.h +++ b/include/session/network_service_node.h @@ -11,8 +11,8 @@ extern "C" { typedef struct network_service_node { char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. uint16_t lmq_port; - char x25519_pubkey_hex[64]; - char ed25519_pubkey_hex[64]; + char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. + char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. uint8_t failure_count; bool invalid; diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 2ed48292..60be767f 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -25,8 +25,8 @@ typedef struct onion_request_builder_object { typedef struct onion_request_service_node_destination { char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. uint16_t lmq_port; - char x25519_pubkey_hex[64]; - char ed25519_pubkey_hex[64]; + char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. + char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. uint8_t failure_count; bool invalid; diff --git a/src/network.cpp b/src/network.cpp index 8d45659b..5dceb8ab 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -289,7 +289,7 @@ void send_request(const request_info info, network_response_callback_t handle_re payload = bstring_view{ reinterpret_cast(info.body->data()), info.body->size()}; - s->command(info.endpoint, payload, [&info, &prom](message resp) { + s->command(info.endpoint, payload, [&prom](message resp) { try { if (resp.timed_out) throw Timeout{}; @@ -342,12 +342,12 @@ void send_request( const std::optional body, const std::optional> swarm, network_response_callback_t handle_response) { - send_request({ed_sk, target, endpoint, body, swarm, std::nullopt}, handle_response); + send_request({ed_sk, target, endpoint, body, swarm, std::nullopt, false}, handle_response); } template std::optional> swarm_for_destination( - const Destination destination) { + const Destination) { return std::nullopt; } @@ -357,24 +357,12 @@ std::optional> swarm_for_destination return destination.swarm; } -template -void process_response( +// The SnodeDestination runs via V3 onion requests +void process_snode_response( const Builder builder, - const Destination destination, const std::string response, const request_info info, network_response_callback_t handle_response) { - handle_response(false, false, -1, "Invalid destination.", service_node_changes{}); -} - -template <> -void process_response( - const Builder builder, - const SnodeDestination destination, - const std::string response, - const request_info info, - network_response_callback_t handle_response) { - // The SnodeDestination runs via V3 onion requests try { std::string base64_iv_and_ciphertext; @@ -425,21 +413,18 @@ void process_response( } } -template <> -void process_response( +// The ServerDestination runs via V4 onion requests +void process_server_response( const Builder builder, - const ServerDestination destination, const std::string response, const request_info info, network_response_callback_t handle_response) { - // The ServerDestination runs via V4 onion requests try { ustring response_data = {to_unsigned(response.data()), response.size()}; auto parser = ResponseParser(builder); auto result = parser.decrypt(response_data); // Process the bencoded response - auto result_sv = from_unsigned_sv(result.data()); oxenc::bt_list_consumer result_bencode{result}; if (result_bencode.is_finished() || !result_bencode.is_string()) @@ -523,12 +508,17 @@ void send_onion_request( bool timeout, int16_t status_code, std::optional response, - service_node_changes changes) { + service_node_changes) { if (!success || timeout || !ResponseParser::response_long_enough(builder.enc_type, response->size())) return handle_errors(status_code, response, info, callback); - process_response(builder, destination, *response, info, callback); + if constexpr (std::is_same_v) + process_snode_response(builder, *response, info, callback); + else if constexpr (std::is_same_v) + process_server_response(builder, *response, info, callback); + else + callback(false, false, -1, "Invalid destination.", service_node_changes{}); }); } catch (const std::exception& e) { handle_response(false, false, -1, e.what(), service_node_changes{}); @@ -544,6 +534,8 @@ std::vector convert_service_nodes( converted_node.ip[sizeof(converted_node.ip) - 1] = '\0'; // Ensure null termination strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); strncpy(converted_node.ed25519_pubkey_hex, node.ed25519_pubkey.hex().c_str(), 64); + converted_node.x25519_pubkey_hex[64] = '\0'; // Ensure null termination + converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.lmq_port = node.lmq_port; converted_node.failure_count = node.failure_count; converted_node.invalid = node.invalid; @@ -626,7 +618,8 @@ LIBSESSION_C_API void network_send_request( static_cast(changes.type), c_nodes.data(), c_nodes.size(), - changes.path_failure_count}; + changes.path_failure_count, + changes.path_invalid}; callback( success, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 4595b291..d319e01c 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -309,7 +309,7 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), failure_count, false}; - unbox(builder).set_destination(session::onionreq::SnodeDestination{node}); + unbox(builder).set_destination(session::onionreq::SnodeDestination{node, std::nullopt}); } LIBSESSION_C_API void onion_request_builder_set_server_destination( diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 9a50d8e9..5ea69087 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -486,8 +486,12 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { auto test_service_node = network_service_node{}; test_service_node.lmq_port = 35400; std::strcpy(test_service_node.ip, "144.76.164.202"); - std::strcpy(test_service_node.x25519_pubkey_hex, "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"); - std::strcpy(test_service_node.ed25519_pubkey_hex, "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); + std::strcpy( + test_service_node.x25519_pubkey_hex, + "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"); + std::strcpy( + test_service_node.ed25519_pubkey_hex, + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); network_send_request( ed_sk.data(), From ec0332bcf8bd8181698a235779ab0d021a55d380 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Apr 2024 11:27:59 +1000 Subject: [PATCH 177/572] Tweaked the onionreq builder to fix more warnings --- include/session/onionreq/builder.hpp | 8 +++-- src/network.cpp | 2 +- src/onionreq/builder.cpp | 46 +++++++++++++++------------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index c418123d..f3063b61 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -79,11 +79,9 @@ class Builder { template void set_destination(Destination destination); - template - ustring generate_payload(Destination destination, std::optional body) const; - void add_hop(std::pair keys) { hops_.push_back(keys); } + ustring generate_payload(std::optional body) const; ustring build(ustring payload); private: @@ -97,8 +95,12 @@ class Builder { std::optional host_ = std::nullopt; std::optional target_ = std::nullopt; + std::optional endpoint_ = std::nullopt; std::optional protocol_ = std::nullopt; + std::optional method_ = std::nullopt; std::optional port_ = std::nullopt; + std::optional>> headers_ = std::nullopt; + std::optional>> query_params_ = std::nullopt; }; } // namespace session::onionreq diff --git a/src/network.cpp b/src/network.cpp index 5dceb8ab..72b0eaf3 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -486,7 +486,7 @@ void send_onion_request( for (const auto& node : path.nodes) builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); - auto payload = builder.generate_payload(destination, body); + auto payload = builder.generate_payload(body); auto onion_req_payload = builder.build(payload); request_info info = { diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index d319e01c..184fa30a 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -63,52 +63,54 @@ void Builder::set_destination(ServerDestination destination) { host_.emplace(destination.host); target_.emplace("/oxen/v4/lsrpc"); // All servers support V4 onion requests + endpoint_.emplace(destination.endpoint); protocol_.emplace(destination.protocol); + method_.emplace(destination.method); - if (destination.port.has_value()) - port_.emplace(destination.port.value()); + if (destination.port) + port_.emplace(*destination.port); - destination_x25519_public_key.emplace(destination.x25519_pubkey); -} + if (destination.headers) + headers_.emplace(*destination.headers); -template -ustring Builder::generate_payload(Destination destination, std::optional body) const { - throw std::runtime_error{"Invalid destination."}; -} + if (destination.query_params) + headers_.emplace(*destination.query_params); -template <> -ustring Builder::generate_payload(SnodeDestination destination, std::optional body) const { - return body.value_or(ustring{}); + destination_x25519_public_key.emplace(destination.x25519_pubkey); } -template <> -ustring Builder::generate_payload( - ServerDestination destination, std::optional body) const { +ustring Builder::generate_payload(std::optional body) const { + // If we don't have the data required for a server request, then assume it's targeting a + // service node and, therefore, the `body` is the payload + if (!host_ || !target_ || !endpoint_ || !protocol_ || !method_ || + !destination_x25519_public_key) + return body.value_or(ustring{}); + + // Otherwise generate the payload for a server request auto headers_json = nlohmann::json::object(); - if (destination.headers) - for (const auto& [key, value] : destination.headers.value()) { + if (headers_) + for (const auto& [key, value] : *headers_) { // Some platforms might automatically add this header, but we don't want to include it if (key != "User-Agent") headers_json[key] = value; } - if (body.has_value() && !headers_json.contains("Content-Type")) + if (body && !headers_json.contains("Content-Type")) headers_json["Content-Type"] = "application/json"; // Need to ensure the endpoint has a leading forward slash so add it if it's missing - auto endpoint = destination.endpoint; + auto endpoint = *endpoint_; if (!endpoint.empty() && endpoint.front() != '/') endpoint = '/' + endpoint; // If we have query parameters, add them to the endpoint to be included in the payload - if (destination.query_params && !destination.query_params->empty()) { + if (query_params_ && !query_params_->empty()) { std::string query_string = ""; - for (auto& query : *destination.query_params) { + for (auto& query : *query_params_) query_string += query.first + "=" + query.second + "&"; - } // Drop the extra '&' from the end endpoint += "?" + query_string.substr(0, query_string.size() - 1); @@ -116,7 +118,7 @@ ustring Builder::generate_payload( // Structure the request information nlohmann::json request_info{ - {"method", destination.method}, {"endpoint", endpoint}, {"headers", headers_json}}; + {"method", *method_}, {"endpoint", endpoint}, {"headers", headers_json}}; auto request_info_dump = request_info.dump(); std::vector payload{request_info_dump}; From 8cebd1859331ab3475efbc209ad43c317ca3171c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Apr 2024 10:39:58 +1000 Subject: [PATCH 178/572] Changes to resolve warnings in CI builds --- include/session/config/base.hpp | 2 +- src/onionreq/builder.cpp | 2 +- tests/test_config_convo_info_volatile.cpp | 3 --- tests/test_config_user_groups.cpp | 4 ---- tests/test_config_userprofile.cpp | 4 ++-- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 138ba51f..a7e5486f 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -801,7 +801,7 @@ class ConfigBase : public ConfigSig { /// /// Inputs: /// - `extra` -- bt_dict containing a previous dump of data - virtual void load_extra_data(oxenc::bt_dict extra) {} + virtual void load_extra_data([[maybe_unused]] oxenc::bt_dict extra) {} /// API: base/ConfigBase::load_key /// diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 184fa30a..11af4617 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -45,7 +45,7 @@ EncryptType parse_enc_type(std::string_view enc_type) { } template -void Builder::set_destination(Destination destination) { +void Builder::set_destination(Destination /*destination*/) { throw std::runtime_error{"Invalid destination."}; } diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 85395a5a..07be219b 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -492,9 +492,6 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { auto pk = some_pubkey(x); return "05" + oxenc::to_hex(pk.begin(), pk.end()); }; - auto some_og_url = [&](unsigned char x) -> std::string { - return "https://example.com/r/room"s + std::to_string(x); - }; const auto now = std::chrono::system_clock::now() - 1ms; auto unix_timestamp = [&now](int days_ago) -> int64_t { return std::chrono::duration_cast( diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 25425a4e..c29e42e3 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -445,10 +445,6 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { constexpr auto definitely_real_id = "035000000000000000000000000000000000000000000000000000000000000000"sv; - int64_t now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - CHECK_FALSE(groups.get_group(definitely_real_id)); CHECK(groups.empty()); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 66c81cac..34ca80db 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -99,8 +99,8 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(name == "Kallie"sv); pic = user_profile_get_pic(conf); - REQUIRE(pic.url); - REQUIRE(pic.key); + REQUIRE(pic.url != ""s); + REQUIRE(pic.key != to_usv(""s)); CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv); CHECK(ustring_view{pic.key, 32} == "secret78901234567890123456789012"_bytes); From c0536ff08547ec559e7a84e7305d5a7d35dc4800 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Apr 2024 10:53:13 +1000 Subject: [PATCH 179/572] Additional CI error fixes and attempting to suppress Catch2 warnings --- tests/CMakeLists.txt | 11 ++++++++++- tests/test_group_keys.cpp | 14 ++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3cfe7234..8b1a3f21 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,12 +26,21 @@ add_executable(testAll test_xed25519.cpp ) +add_library(Catch2Wrapper INTERFACE) +target_link_libraries(Catch2Wrapper INTERFACE Catch2::Catch2WithMain) + +if(MSVC) + target_compile_options(Catch2Wrapper INTERFACE /W0) +else() + target_compile_options(Catch2Wrapper INTERFACE -w) +endif() + target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq libsodium::sodium-internal nlohmann_json::nlohmann_json - Catch2::Catch2WithMain) + Catch2Wrapper) if(NOT TARGET check) add_custom_target(check COMMAND testAll) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 475ba217..52418640 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -23,8 +23,6 @@ using namespace std::literals; using namespace oxenc::literals; -static constexpr int64_t created_ts = 1680064059; - using namespace session::config; static std::array sk_from_seed(ustring_view seed) { @@ -195,7 +193,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { mem_configs.clear(); // add non-admin members, re-key, distribute - for (int i = 0; i < members.size(); ++i) { + for (size_t i = 0; i < members.size(); ++i) { auto m = admin1.members.get_or_construct(members[i].session_id); m.admin = false; m.name = "Member" + std::to_string(i); @@ -311,7 +309,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s, "keyhash4"s}}); } - for (int i = 0; i < members.size(); i++) { + for (size_t i = 0; i < members.size(); i++) { auto& m = members[i]; bool found_key = m.keys.load_key_message( "keyhash4", new_keys_config2, get_timestamp_ms(), m.info, m.members); @@ -509,7 +507,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } - for (int i = 0; i < members.size(); i++) { + for (size_t i = 0; i < members.size(); i++) { auto& m = members[i]; CHECK(m.keys.load_key_message( "keyhash6", @@ -722,7 +720,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { get_timestamp_ms(), m.info, m.members)); - config_string_list* hashes; + [[maybe_unused]] config_string_list* hashes; REQUIRE_THROWS( hashes = config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); REQUIRE_THROWS( @@ -734,7 +732,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { free(new_info_config1); free(new_mem_config1); - for (int i = 0; i < members.size(); ++i) { + for (size_t i = 0; i < members.size(); ++i) { config_group_member new_mem; REQUIRE(groups_members_get_or_construct( @@ -874,7 +872,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") member.info.id, to_usv(member.secret_key), auth_data)); // Try flipping a bit in each position of the auth data and make sure it fails to validate: - for (int i = 0; i < auth_data.size(); i++) { + for (size_t i = 0; i < auth_data.size(); i++) { for (int b = 0; b < 8; b++) { if (i == 35 && b == 7) // This is the sign bit of k, which can be flipped but gets // flipped back when dealing with the missing X->Ed conversion From 67921bde295dfbea3203ff9f6570599312bbd4e7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 15:54:20 -0300 Subject: [PATCH 180/572] Bump to c++20 oxen-logging already requires this, and libquic and other deps are also likely moving to c++20 in the near future. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 249a06a1..4ed6fa80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ else() endif() -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) From 48f067690fe92d7d4f11d2038767a273199191d7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 16:27:45 -0300 Subject: [PATCH 181/572] Find and link to nettle The onionreq code requires nettle (for AES encryption). If we are doing a static deps build we'll already have it (via libquic deps), but otherwise we need to find and link to it. --- CMakeLists.txt | 13 +++++++++++++ src/CMakeLists.txt | 1 + 2 files changed, 14 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ed6fa80..cff8e6bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,19 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(external) + +if(NOT TARGET nettle::nettle) + if(BUILD_STATIC_DEPS) + message(FATAL_ERROR "Internal error: nettle::nettle target (expected via libquic BUILD_STATIC_DEPS) not found") + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(NETTLE REQUIRED IMPORTED_TARGET nettle) + add_library(nettle INTERFACE) + target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) + add_library(nettle::nettle ALIAS nettle) + endif() +endif() + add_subdirectory(src) add_subdirectory(proto) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a6150a5..a3adc489 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -101,6 +101,7 @@ target_link_libraries(onionreq PRIVATE nlohmann_json::nlohmann_json libsodium::sodium-internal + nettle::nettle ) From 5b8880c462e13f3ad99bd497e09aba0d9e376a04 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 18:26:34 -0300 Subject: [PATCH 182/572] Bump libquic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index a272af6d..2c500815 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit a272af6d18af1c39cdd740051a4a326660159886 +Subproject commit 2c500815084cadcaa623bf31771e902546c013a0 From fc1212531636458090bdfe64283e6bbee01b6a0a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 18:32:09 -0300 Subject: [PATCH 183/572] Disable shared libs for llvm build --- .drone.jsonnet | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index c97a7c67..7a2ddc9b 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -78,6 +78,7 @@ local debian_build(name, lto=false, werror=true, cmake_extra='', + shared_libs=true, jobs=6, tests=true, oxen_repo=false, @@ -96,7 +97,7 @@ local debian_build(name, 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE=' + build_type + ' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + - '-DBUILD_SHARED_LIBS=ON ' + + (if shared_libs then '-DBUILD_SHARED_LIBS=ON ' else '') + '-DUSE_LTO=' + (if lto then 'ON ' else 'OFF ') + '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') + cmake_extra, @@ -173,6 +174,7 @@ local full_llvm(version) = debian_build( docker_base + 'debian-sid-clang', deps=['clang-' + version, ' lld-' + version, ' libc++-' + version + '-dev', 'libc++abi-' + version + '-dev'] + default_deps_nocxx, + shared_libs=false, cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' -DCMAKE_CXX_FLAGS="-stdlib=libc++ -fcolor-diagnostics" ' + From 5728829afc4117b69244fbf3507c15749b5b7c2b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 18:42:28 -0300 Subject: [PATCH 184/572] Bump nlohmann-json to latest Fixes a compilation warning/error under clang+llvm. --- external/nlohmann-json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/nlohmann-json b/external/nlohmann-json index bc889afb..9cca280a 160000 --- a/external/nlohmann-json +++ b/external/nlohmann-json @@ -1 +1 @@ -Subproject commit bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d +Subproject commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 From 7b067a32d3433f925b3ccb3f9b2dd2ab9b8633b0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:05:13 -0300 Subject: [PATCH 185/572] Move some apple jobs to om2g CI --- .drone.jsonnet | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 7a2ddc9b..1450a0d9 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -186,6 +186,7 @@ local full_llvm(version) = debian_build( // Macos build local mac_pipeline(name, + arch='amd64', allow_fail=false, build=['echo "Error: drone build argument not set"', 'exit 1'], extra_steps=[]) @@ -193,7 +194,7 @@ local mac_pipeline(name, kind: 'pipeline', type: 'exec', name: name, - platform: { os: 'darwin', arch: 'amd64' }, + platform: { os: 'darwin', arch: arch }, steps: [ { name: 'submodules', commands: submodule_commands }, { @@ -209,6 +210,7 @@ local mac_pipeline(name, ] + extra_steps, }; local mac_builder(name, + arch='amd64', build_type='Release', werror=true, lto=false, @@ -216,7 +218,7 @@ local mac_builder(name, jobs=6, tests=true, allow_fail=false) - = mac_pipeline(name, allow_fail=allow_fail, build=[ + = mac_pipeline(name, arch=arch, allow_fail=allow_fail, build=[ 'mkdir build', 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fcolor-diagnostics -DCMAKE_BUILD_TYPE=' + build_type + ' ' + @@ -324,8 +326,9 @@ local static_build(name, windows_cross_pipeline('Windows (amd64)', docker_base + 'debian-win32-cross'), // Macos builds: - mac_builder('macOS (Release)'), - mac_builder('macOS (Debug)', build_type='Debug'), + mac_builder('macOS Intel (Release)'), + mac_builder('macOS Arm64 (Release)', arch='arm64'), + mac_builder('macOS Arm64 (Debug)', arch='arm64', build_type='Debug'), // Static lib builds static_build('Static Linux amd64', docker_base + 'debian-stable', 'libsession-util-linux-amd64-TAG.tar.xz'), @@ -359,7 +362,7 @@ local static_build(name, 'cd build-macos && ../utils/ci/drone-static-upload.sh', ]), - mac_pipeline('Static iOS', build=[ + mac_pipeline('Static iOS', arch='arm64', build=[ 'export JOBS=6', './utils/ios.sh libsession-util-ios-TAG', 'cd build-ios && ../utils/ci/drone-static-upload.sh', From 43eae3a967adc75a4476d943b514f78533ac7d3b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:05:30 -0300 Subject: [PATCH 186/572] Add some linux builds; fix bionic - Add debian oldstable (bullseye) - Add debian testing - Use oxen repo for bionic --- .drone.jsonnet | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index 1450a0d9..f14cc8cd 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -307,15 +307,18 @@ local static_build(name, // Various debian builds debian_build('Debian sid (amd64)', docker_base + 'debian-sid'), debian_build('Debian sid/Debug (amd64)', docker_base + 'debian-sid', build_type='Debug'), + debian_build('Debian testing', docker_base + 'debian-testing'), clang(16), full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), + debian_build('Debian 11', docker_base + 'debian-bullseye'), debian_build('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic (amd64)', docker_base + 'ubuntu-bionic', deps=['g++-8'], kitware_repo='bionic', + oxen_repo=true, cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), // ARM builds (ARM64 and armhf) From 11a08049f31a154ee71d380008c077b85650a014 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:08:32 -0300 Subject: [PATCH 187/572] drone job name lipstick Remove `(amd64)` from various job names as its a sane default to assume if not otherwise noted. --- .drone.jsonnet | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index f14cc8cd..ea57d05d 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -163,14 +163,14 @@ local windows_cross_pipeline(name, ); local clang(version) = debian_build( - 'Debian sid/clang-' + version + ' (amd64)', + 'Debian sid/clang-' + version, docker_base + 'debian-sid-clang', deps=['clang-' + version] + default_deps_nocxx, cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' ' ); local full_llvm(version) = debian_build( - 'Debian sid/llvm-' + version + ' (amd64)', + 'Debian sid/llvm-' + version, docker_base + 'debian-sid-clang', deps=['clang-' + version, ' lld-' + version, ' libc++-' + version + '-dev', 'libc++abi-' + version + '-dev'] + default_deps_nocxx, @@ -305,16 +305,16 @@ local static_build(name, }, // Various debian builds - debian_build('Debian sid (amd64)', docker_base + 'debian-sid'), - debian_build('Debian sid/Debug (amd64)', docker_base + 'debian-sid', build_type='Debug'), + debian_build('Debian sid', docker_base + 'debian-sid'), + debian_build('Debian sid/Debug', docker_base + 'debian-sid', build_type='Debug'), debian_build('Debian testing', docker_base + 'debian-testing'), clang(16), full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), debian_build('Debian 11', docker_base + 'debian-bullseye'), - debian_build('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'), - debian_build('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'), - debian_build('Ubuntu bionic (amd64)', + debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), + debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), + debian_build('Ubuntu bionic', docker_base + 'ubuntu-bionic', deps=['g++-8'], kitware_repo='bionic', @@ -326,7 +326,7 @@ local static_build(name, debian_build('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), // Windows builds (x64) - windows_cross_pipeline('Windows (amd64)', docker_base + 'debian-win32-cross'), + windows_cross_pipeline('Windows', docker_base + 'debian-win32-cross'), // Macos builds: mac_builder('macOS Intel (Release)'), @@ -334,10 +334,10 @@ local static_build(name, mac_builder('macOS Arm64 (Debug)', arch='arm64', build_type='Debug'), // Static lib builds - static_build('Static Linux amd64', docker_base + 'debian-stable', 'libsession-util-linux-amd64-TAG.tar.xz'), - static_build('Static Linux i386', docker_base + 'debian-stable', 'libsession-util-linux-i386-TAG.tar.xz'), - static_build('Static Linux arm64', docker_base + 'debian-stable', 'libsession-util-linux-arm64-TAG.tar.xz', arch='arm64'), - static_build('Static Linux armhf', docker_base + 'debian-stable/arm32v7', 'libsession-util-linux-armhf-TAG.tar.xz', arch='arm64'), + static_build('Static Linux/amd64', docker_base + 'debian-stable', 'libsession-util-linux-amd64-TAG.tar.xz'), + static_build('Static Linux/i386', docker_base + 'debian-stable', 'libsession-util-linux-i386-TAG.tar.xz'), + static_build('Static Linux/arm64', docker_base + 'debian-stable', 'libsession-util-linux-arm64-TAG.tar.xz', arch='arm64'), + static_build('Static Linux/armhf', docker_base + 'debian-stable/arm32v7', 'libsession-util-linux-armhf-TAG.tar.xz', arch='arm64'), static_build('Static Windows x64', docker_base + 'debian-win32-cross', 'libsession-util-windows-x64-TAG.zip', From 1683e589f462915371497de0806204b932b9ee14 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:12:17 -0300 Subject: [PATCH 188/572] bullseye backports --- .drone.jsonnet | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index ea57d05d..bc469231 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -19,6 +19,12 @@ local default_deps = ['g++'] + default_deps_nocxx; local docker_base = 'registry.oxen.rocks/'; +local debian_backports(distro, pkgs) = [ + 'echo "deb http://deb.debian.org/debian ' + distro + '-backports main" >/etc/apt/sources.list.d/' + distro + '-backports.list', + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' install -y ' + std.join(' ', std.map(function(x) x + '/' + distro + '-backports', pkgs)), +]; + // Do something on a debian-like system local debian_pipeline(name, image, @@ -27,7 +33,9 @@ local debian_pipeline(name, oxen_repo=false, kitware_repo=''/* ubuntu codename, if wanted */, allow_fail=false, + cmake_pkg='cmake', build=['echo "Error: drone build argument not set"', 'exit 1'], + extra_setup=[], extra_steps=[]) = { kind: 'pipeline', @@ -61,9 +69,9 @@ local debian_pipeline(name, 'echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ ' + kitware_repo + ' main" >/etc/apt/sources.list.d/kitware.list', 'eatmydata ' + apt_get_quiet + ' update', ] else [] - ) + [ + ) + extra_setup + [ 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y cmake make git ccache ca-certificates ' + std.join(' ', deps), + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + cmake_pkg + ' make git ccache ca-certificates ' + std.join(' ', deps), ] + build, }, ] + extra_steps, @@ -83,6 +91,7 @@ local debian_build(name, tests=true, oxen_repo=false, kitware_repo=''/* ubuntu codename, if wanted */, + extra_setup=[], allow_fail=false) = debian_pipeline( name, @@ -103,6 +112,7 @@ local debian_build(name, cmake_extra, 'make VERBOSE=1 -j' + jobs, ], + extra_setup=extra_setup, extra_steps=(if tests then [{ name: 'tests', @@ -311,7 +321,7 @@ local static_build(name, clang(16), full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), - debian_build('Debian 11', docker_base + 'debian-bullseye'), + debian_build('Debian 11', docker_base + 'debian-bullseye', extra_setup=debian_backports('bullseye', ['cmake'])), debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic', From 94fa3ad468082bb4b8ee700b99a7818cf9a95d7c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:16:34 -0300 Subject: [PATCH 189/572] Add ngtcp2 CI dep For shared builds we can use it rather than needing to rebuild ngtcp2. --- .drone.jsonnet | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index bc469231..cc97902f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -14,6 +14,7 @@ local default_deps_nocxx = [ 'nlohmann-json3-dev', 'gnutls-bin', 'libgnutls28-dev', + 'ngtcp2-dev', ]; local default_deps = ['g++'] + default_deps_nocxx; @@ -326,7 +327,7 @@ local static_build(name, debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic', docker_base + 'ubuntu-bionic', - deps=['g++-8'], + deps=['g++-8', 'libgnutls28-dev', 'ngtcp2-dev'], kitware_repo='bionic', oxen_repo=true, cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), From f23bf010e0bde3afc28772e868ecba50884552f3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:20:07 -0300 Subject: [PATCH 190/572] make oxen repo default --- .drone.jsonnet | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index cc97902f..3fb2d34a 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -31,7 +31,7 @@ local debian_pipeline(name, image, arch='amd64', deps=default_deps, - oxen_repo=false, + oxen_repo=true, kitware_repo=''/* ubuntu codename, if wanted */, allow_fail=false, cmake_pkg='cmake', @@ -90,7 +90,7 @@ local debian_build(name, shared_libs=true, jobs=6, tests=true, - oxen_repo=false, + oxen_repo=true, kitware_repo=''/* ubuntu codename, if wanted */, extra_setup=[], allow_fail=false) @@ -266,6 +266,7 @@ local static_build(name, image, arch=arch, deps=deps, + oxen_repo=oxen_repo, build=[ 'export JOBS=' + jobs, './utils/static-bundle.sh build ' + archive_name + ' -DSTATIC_LIBSTD=ON ' + cmake_extra, From 3c7a9f1c50892d724f47eacf16e6a2ab6148e759 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 19:21:39 -0300 Subject: [PATCH 191/572] fix libngtcp2-dev package name and depend on crypto package --- .drone.jsonnet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 3fb2d34a..742c69e9 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -14,7 +14,7 @@ local default_deps_nocxx = [ 'nlohmann-json3-dev', 'gnutls-bin', 'libgnutls28-dev', - 'ngtcp2-dev', + 'libngtcp2-crypto-gnutls-dev', ]; local default_deps = ['g++'] + default_deps_nocxx; @@ -328,7 +328,7 @@ local static_build(name, debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic', docker_base + 'ubuntu-bionic', - deps=['g++-8', 'libgnutls28-dev', 'ngtcp2-dev'], + deps=['g++-8', 'libgnutls28-dev', 'libngtcp2-crypto-gnutls-dev'], kitware_repo='bionic', oxen_repo=true, cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), From 019c1e9eeabce72e5f54ac2d9560be462a6696f9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 22:04:35 -0300 Subject: [PATCH 192/572] Add test image deps --- .drone.jsonnet | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 742c69e9..9674994d 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,14 +10,17 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; +local libngtcp2_deps = ['libngtcp2-dev', 'libngtcp2-crypto-gnutls-dev']; + local default_deps_nocxx = [ 'nlohmann-json3-dev', - 'gnutls-bin', 'libgnutls28-dev', - 'libngtcp2-crypto-gnutls-dev', -]; +] + libngtcp2_deps; + local default_deps = ['g++'] + default_deps_nocxx; +local default_test_deps = ['libngtcp2-crypto-gnutls8', 'libngtcp2-16', 'libgnutls30']; + local docker_base = 'registry.oxen.rocks/'; local debian_backports(distro, pkgs) = [ @@ -83,6 +86,7 @@ local debian_build(name, image, arch='amd64', deps=default_deps, + test_deps=default_test_deps, build_type='Release', lto=false, werror=true, @@ -120,10 +124,23 @@ local debian_build(name, image: image, pull: 'always', [if allow_fail then 'failure']: 'ignore', - commands: [ - 'cd build', - './tests/testAll --colour-mode ansi -d yes', - ], + commands: + [apt_get_quiet + ' install -y eatmydata'] + ( + if std.length(test_deps) > 0 then ( + if oxen_repo then [ + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y lsb-release', + 'cp utils/deb.oxen.io.gpg /etc/apt/trusted.gpg.d', + 'echo deb http://deb.oxen.io $$(lsb_release -sc) main >/etc/apt/sources.list.d/oxen.list', + ] else [] + ) + [ + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', test_deps), + ] else [] + ) + [ + 'cd build', + './tests/testAll --colour-mode ansi -d yes', + ], }] else []) ); // windows cross compile on debian @@ -328,9 +345,8 @@ local static_build(name, debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic', docker_base + 'ubuntu-bionic', - deps=['g++-8', 'libgnutls28-dev', 'libngtcp2-crypto-gnutls-dev'], + deps=['g++-8', 'libgnutls28-dev'] + libngtcp2_deps, kitware_repo='bionic', - oxen_repo=true, cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), // ARM builds (ARM64 and armhf) From d7ac1e27879fcff16bc8eb9708830433787f7dff Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 22:09:41 -0300 Subject: [PATCH 193/572] more dep fixing --- .drone.jsonnet | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 9674994d..53c8d864 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,16 +10,15 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local libngtcp2_deps = ['libngtcp2-dev', 'libngtcp2-crypto-gnutls-dev']; +local libngtcp2_deps = ['libgnutls28-dev', 'libngtcp2-dev', 'libngtcp2-crypto-gnutls-dev']; local default_deps_nocxx = [ 'nlohmann-json3-dev', - 'libgnutls28-dev', ] + libngtcp2_deps; local default_deps = ['g++'] + default_deps_nocxx; -local default_test_deps = ['libngtcp2-crypto-gnutls8', 'libngtcp2-16', 'libgnutls30']; +local default_test_deps = libngtcp2_deps; local docker_base = 'registry.oxen.rocks/'; @@ -345,7 +344,7 @@ local static_build(name, debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), debian_build('Ubuntu bionic', docker_base + 'ubuntu-bionic', - deps=['g++-8', 'libgnutls28-dev'] + libngtcp2_deps, + deps=['g++-8'] + libngtcp2_deps, kitware_repo='bionic', cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), From 35494386d092fa5aa73082107bee455e16cd7d1c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 22:25:53 -0300 Subject: [PATCH 194/572] libquic bump --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 2c500815..18e6e2ba 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 2c500815084cadcaa623bf31771e902546c013a0 +Subproject commit 18e6e2bada5182bef8bf715407a0b58fa2dfa440 From 3aea8a58da35e2a0b5d9d3cef053aac20eea7a81 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 22:44:25 -0300 Subject: [PATCH 195/572] Remove bionic; disable win-x86 bionic can't cope with the C++20 that we use. Win (x86) build is broken for unknown reasons, so comment it out for now as it isn't that important. --- .drone.jsonnet | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 53c8d864..d863fe49 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -342,11 +342,6 @@ local static_build(name, debian_build('Debian 11', docker_base + 'debian-bullseye', extra_setup=debian_backports('bullseye', ['cmake'])), debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), - debian_build('Ubuntu bionic', - docker_base + 'ubuntu-bionic', - deps=['g++-8'] + libngtcp2_deps, - kitware_repo='bionic', - cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), // ARM builds (ARM64 and armhf) debian_build('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64', jobs=4), @@ -370,11 +365,13 @@ local static_build(name, 'libsession-util-windows-x64-TAG.zip', deps=['g++-mingw-w64-x86-64-posix'], cmake_extra='-DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw-x86-64-toolchain.cmake'), + /* currently broken: static_build('Static Windows x86', docker_base + 'debian-win32-cross', 'libsession-util-windows-x86-TAG.zip', deps=['g++-mingw-w64-i686-posix'], cmake_extra='-DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw-i686-toolchain.cmake'), + */ debian_pipeline( 'Static Android', docker_base + 'android', From 6c786907dfd0f732661e0a0f54cd6aa19dc8bb14 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 23:01:21 -0300 Subject: [PATCH 196/572] libquic bump --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 18e6e2ba..5ac844f8 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 18e6e2bada5182bef8bf715407a0b58fa2dfa440 +Subproject commit 5ac844f802cee66fae34fe0e938c72f7344e9e2b From ffe6211d355e31dea131272a2dd7936b0d995139 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 9 Apr 2024 23:06:24 -0300 Subject: [PATCH 197/572] use CHECK(thing) and CHECK_FALSE(thing) instead of == true/false Catch2 gives more informative diagnostics when using CHECK(thing) or CHECK_FALSE(thing) for direct boolean values. --- tests/test_network.cpp | 86 +++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 5ea69087..9ca5d341 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -57,8 +57,8 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == code); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::none); @@ -78,8 +78,8 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::update_node); @@ -89,7 +89,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 1); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.path_failure_count == 0); // Check general error handling with no provided path (too many failures) @@ -120,14 +120,14 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::update_node); REQUIRE(result.changes.nodes.size() == 1); CHECK(result.changes.nodes[0].failure_count == 10); - CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.nodes[0].invalid); CHECK(result.changes.path_failure_count == 0); // Check general error handling with a path but no response (first failure) @@ -147,14 +147,14 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 1); CHECK(result.changes.nodes[0].failure_count == 0); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.path_failure_count == 1); // Check general error handling with a path but no response (too many path failures) @@ -183,16 +183,16 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 2); CHECK(result.changes.nodes[0].failure_count == 1); - CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.nodes[0].invalid); CHECK(result.changes.nodes[1].failure_count == 1); - CHECK(result.changes.nodes[1].invalid == false); + CHECK_FALSE(result.changes.nodes[1].invalid); CHECK(result.changes.path_failure_count == 10); // Check general error handling with a path but no response (too many path & node failures) @@ -221,16 +221,16 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 2); CHECK(result.changes.nodes[0].failure_count == 1); - CHECK(result.changes.nodes[0].invalid == true); + CHECK(result.changes.nodes[0].invalid); CHECK(result.changes.nodes[1].failure_count == 10); - CHECK(result.changes.nodes[1].invalid == true); + CHECK(result.changes.nodes[1].invalid); CHECK(result.changes.path_failure_count == 10); // Check general error handling with a path and a random response (first failure) @@ -251,14 +251,14 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(result.response == response); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 1); CHECK(result.changes.nodes[0].failure_count == 0); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.path_failure_count == 1); // Check general error handling with a path and specific node failure (first failure) @@ -288,18 +288,18 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(result.response == response); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 2); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 0); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); CHECK(result.changes.nodes[1].failure_count == 1); - CHECK(result.changes.nodes[1].invalid == false); + CHECK_FALSE(result.changes.nodes[1].invalid); CHECK(result.changes.path_failure_count == 0); // Check general error handling with a path and specific node failure (too many failures) @@ -329,18 +329,18 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(result.response == response); CHECK(result.changes.type == ServiceNodeChangeType::update_path); REQUIRE(result.changes.nodes.size() == 2); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 0); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); CHECK(result.changes.nodes[1].failure_count == 10); - CHECK(result.changes.nodes[1].invalid == true); + CHECK(result.changes.nodes[1].invalid); CHECK(result.changes.path_failure_count == 0); // Check a 421 with no swarm data throws (no good way to handle this case) @@ -356,8 +356,8 @@ TEST_CASE("Network error handling", "[network]") { service_node_changes changes) { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(result.changes.type == ServiceNodeChangeType::update_node); @@ -377,8 +377,8 @@ TEST_CASE("Network error handling", "[network]") { service_node_changes changes) { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(result.changes.type == ServiceNodeChangeType::update_path); @@ -396,8 +396,8 @@ TEST_CASE("Network error handling", "[network]") { service_node_changes changes) { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(result.changes.type == ServiceNodeChangeType::update_path); @@ -423,8 +423,8 @@ TEST_CASE("Network error handling", "[network]") { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == false); - CHECK(result.timeout == false); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(result.changes.type == ServiceNodeChangeType::replace_swarm); REQUIRE(result.changes.nodes.size() == 1); @@ -433,7 +433,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 0); - CHECK(result.changes.nodes[0].invalid == false); + CHECK_FALSE(result.changes.nodes[0].invalid); CHECK(result.changes.path_failure_count == 0); } @@ -466,8 +466,8 @@ TEST_CASE("Network direct request", "[send_request][network]") { service_node_changes changes) { result = {success, timeout, status_code, response, changes}; }); - CHECK(result.success == true); - CHECK(result.timeout == false); + CHECK(result.success); + CHECK_FALSE(result.timeout); CHECK(result.status_code == 200); CHECK(result.changes.type == ServiceNodeChangeType::none); REQUIRE(result.response.has_value()); @@ -508,8 +508,8 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { size_t response_size, network_service_node_changes changes, void*) { - CHECK(success == true); - CHECK(timeout == false); + CHECK(success); + CHECK_FALSE(timeout); CHECK(status_code == 200); CHECK(changes.type == SERVICE_NODE_CHANGE_TYPE_NONE); REQUIRE(response_size != 0); From 13b67314e76ad063d819bd141ad1e62a21a3a7f1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 10 Apr 2024 12:00:00 -0300 Subject: [PATCH 198/572] libquic bump --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 5ac844f8..bf35e372 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 5ac844f802cee66fae34fe0e938c72f7344e9e2b +Subproject commit bf35e372b361df208dc4e8e3d9855adce6eb2de5 From e734a5cefbc76d4fd895a6df846125d75a43a01d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 12 Apr 2024 16:22:12 +1000 Subject: [PATCH 199/572] Reworked network to be instance-based Updated the network functions to be on a Network class Updated the logic to store the quic::Network and quic::Endpoint instances Added logic to store the current paths on the Network instance Added a shared_ptr to the quic::connection_interface against it's associated path --- external/oxen-libquic | 2 +- include/session/network.h | 157 ++++- include/session/network.hpp | 193 +++++- include/session/network_service_node.h | 4 +- include/session/network_service_node.hpp | 14 +- include/session/onionreq/builder.h | 33 +- include/session/onionreq/builder.hpp | 5 - include/session/util.hpp | 16 + src/network.cpp | 821 +++++++++++++++-------- src/onionreq/builder.cpp | 10 +- src/util.cpp | 23 + tests/test_network.cpp | 106 +-- 12 files changed, 968 insertions(+), 416 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index bf35e372..74b78140 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit bf35e372b361df208dc4e8e3d9855adce6eb2de5 +Subproject commit 74b78140588ffcd495263db3f0ae9788142ca51b diff --git a/include/session/network.h b/include/session/network.h index 55477512..7e531228 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -11,6 +11,11 @@ extern "C" { #include "network_service_node.h" #include "onionreq/builder.h" +typedef struct network_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} network_object; + typedef enum SERVICE_NODE_CHANGE_TYPE { SERVICE_NODE_CHANGE_TYPE_NONE = 0, SERVICE_NODE_CHANGE_TYPE_INVALID_PATH = 1, @@ -27,10 +32,115 @@ typedef struct network_service_node_changes { bool invalid; } network_service_node_changes; -LIBSESSION_EXPORT void network_add_logger(void (*callback)(const char*, size_t)); +typedef struct onion_request_path { + const network_service_node* nodes; + const size_t nodes_count; + uint8_t failure_count; +} onion_request_path; + +/// API: network/network_init +/// +/// Constructs a new network object. +/// +/// When done with the object the `network_object` must be destroyed by passing the pointer to +/// network_free(). +/// +/// Inputs: +/// - `network` -- [out] Pointer to the network object +/// - `ed25519_secretkey_bytes` -- [in] must be the 64-byte libsodium "secret key" value. 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: +/// - `bool` -- Returns true on success; returns false and write the exception message as a C-string +/// into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool network_init( + network_object** network, const unsigned char* ed25519_secretkey_bytes, char* error) + __attribute__((warn_unused_result)); + +/// API: network/network_free +/// +/// Frees a network object. +/// +/// Inputs: +/// - `network` -- [in] Pointer to network_object object +LIBSESSION_EXPORT void network_free(network_object* network); + +/// API: network/network_add_logger +/// +/// Adds a logger to the network object. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `callback` -- [in] callback to be called when a new message should be logged. +LIBSESSION_EXPORT void network_add_logger( + network_object* network, void (*callback)(const char*, size_t)); + +/// API: network/network_add_path +/// +/// Adds a path to the list on the network object that is randomly selected from when making an +/// onion request. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `path` -- [in] the path of service nodes to be added as an option to the network. +/// - `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 on success; returns false and write the exception message as a C-string +/// into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool network_add_path( + network_object* network, const onion_request_path path, char* error); + +/// API: network/network_remove_path +/// +/// Removes a path from the list on the network object that is randomly selected from when making an +/// onion request. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `path` -- [in] the path of service nodes to be removed from the network. +/// - `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 on success; returns false and write the exception message as a C-string +/// into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool network_remove_path( + network_object* network, const network_service_node node, char* error); + +/// API: network/network_remove_all_paths +/// +/// Removes all paths from the list on the network object that are randomly selected from when making an +/// onion request. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +LIBSESSION_EXPORT void network_remove_all_paths(network_object* network); +/// API: network/network_send_request +/// +/// Sends a request directly to the provided service node. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `destination` -- [in] address information about the service node the request should be sent +/// to. +/// - `endpoint` -- [in] endpoint for the request. +/// - `body` -- [in] data to send to the specified endpoint. +/// - `body_size` -- [in] size of the `body`. +/// - `swarm` -- [in] current swarm information for the destination service node. Set to NULL if not +/// used. +/// - `swarm_count` -- [in] number of service nodes included in the `swarm`. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_request( - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const network_service_node destination, const char* endpoint, const unsigned char* body, @@ -47,9 +157,19 @@ LIBSESSION_EXPORT void network_send_request( void*), void* ctx); +/// API: network/network_send_onion_request_to_snode_destination +/// +/// Sends a request via onion routing to the provided service node. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `node` -- [in] address information about the service node the request should be sent to. +/// - `body` -- [in] data to send to the specified node. +/// - `body_size` -- [in] size of the `body`. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( - const onion_request_path path, - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const onion_request_service_node_destination node, const unsigned char* body, size_t body_size, @@ -63,9 +183,34 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( void*), void* ctx); +/// API: network/network_send_onion_request_to_server_destination +/// +/// Sends a request via onion routing to the provided server. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `method` -- [in] the HTTP method to use for performing the request on the server. +/// - `protocol` -- [in] the protocol to use for performing the request on the server. +/// - `host` -- [in] the server host. +/// - `endpoint` -- [in] the endpoint to call on the server. +/// - `port` -- [in] the port to send the request to on the server. +/// - `x25519_pubkey` -- [in] the x25519 pubkey of the server. +/// - `query_param_keys` -- [in] array of keys for any query params to send to the server, must be +/// the same size as `query_param_values`. Set to NULL if unused. +/// - `query_param_values` -- [in] array of values for any query params to send to the server, must +/// be the same size as `query_param_keys`. Set to NULL if unused. +/// - `query_params_size` -- [in] The number of query params provided. +/// - `headers` -- [in] array of keys for any headers to send to the server, must be the same size +/// as `header_values`. Set to NULL if unused. +/// - `header_values` -- [in] array of values for any headers to send to the server, must be the +/// same size as `headers`. Set to NULL if unused. +/// - `headers_size` -- [in] The number of headers provided. +/// - `body` -- [in] data to send to the specified endpoint. +/// - `body_size` -- [in] size of the `body`. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( - const onion_request_path path, - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const char* method, const char* protocol, const char* host, diff --git a/include/session/network.hpp b/include/session/network.hpp index 970dbe3a..34d5526f 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "network_service_node.hpp" @@ -17,6 +18,12 @@ enum class ServiceNodeChangeType { update_node = 4, }; +struct onion_path { + std::shared_ptr conn; + std::vector nodes; + uint8_t failure_count; +}; + struct service_node_changes { ServiceNodeChangeType type = ServiceNodeChangeType::none; std::vector nodes = {}; @@ -25,12 +32,11 @@ struct service_node_changes { }; struct request_info { - const ustring_view ed_sk; const service_node target; const std::string endpoint; const std::optional body; const std::optional> swarm; - const std::optional path; + const std::optional path; const bool is_retry; }; @@ -41,27 +47,162 @@ using network_response_callback_t = std::function response, service_node_changes changes)>; -void handle_errors( - const int16_t status_code, - const std::optional response, - const request_info info, - network_response_callback_t handle_response); - -void send_request( - const ustring_view ed_sk, - const session::network::service_node target, - const std::string endpoint, - const std::optional body, - const std::optional> swarm, - network_response_callback_t handle_response); - -template -void send_onion_request( - const session::onionreq::onion_path path, - const Destination destination, - const std::optional body, - const ustring_view ed_sk, - const bool is_retry, - network_response_callback_t handle_response); - -} // namespace session::network \ No newline at end of file +class Network { + private: + oxen::quic::Network net; + std::shared_ptr creds; + std::vector paths; + + std::shared_ptr endpoint; + std::shared_ptr buffer; + + public: + // Constructs a new network for the given credentials, all requests should be made via a single + // Network instance. + Network(const session::onionreq::ed25519_seckey ed25519_seckey); + + /// API: network/add_logger + /// + /// Adds a logger to the network object. + /// + /// Inputs: + /// - `callback` -- [in] callback to be called when a new message should be logged. + void add_logger(std::function callback); + + /// API: network/add_path + /// + /// Adds a path to the list on the network object that is randomly selected from when making an + /// onion request. + /// + /// Inputs: + /// - `nodes` -- [in] nodes which make up the path to be added. + /// - `failure_count` -- [in] number of times the path has previously failed to complete a request. + void add_path(std::vector nodes, uint8_t failure_count); + + /// API: network/remove_path + /// + /// Removes a path from the list on the network object that is randomly selected from when making an + /// onion request. + /// + /// Inputs: + /// - `node` -- [in] first node in the path to be removed. + void remove_path(session::network::service_node node); + + /// API: network/remove_all_paths + /// + /// Removes all paths from the list on the network object that are randomly selected from when making an + /// onion request. + void remove_all_paths(); + + /// API: network/send_request + /// + /// Send a request via the network. + /// + /// Inputs: + /// - `info` -- [in] wrapper around all of the information required to send a request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void send_request(const request_info info, network_response_callback_t handle_response); + + /// API: network/send_request + /// + /// Sends a request directly to the provided service node. + /// + /// Inputs: + /// - `target` -- [in] the address information for the service node to send the request to. + /// - `endpoint` -- [in] endpoint for the request. + /// - `body` -- [in] data to send to the specified endpoint. + /// - `swarm` -- [in] current swarm information for the destination service node. Set to NULL if + /// not used. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void send_request( + const session::network::service_node target, + const std::string endpoint, + const std::optional body, + const std::optional> swarm, + network_response_callback_t handle_response); + + /// API: network/send_onion_request + /// + /// Sends a request via onion routing to the provided service node or server destination. + /// + /// Inputs: + /// - `path` -- [in] the path of service nodes that the request should be routed through. + /// - `destination` -- [in] service node or server destination information. + /// - `body` -- [in] data to send to the specified destination. + /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used + /// for internal purposes for cases which should retry automatically (like receiving a `421`) in + /// order to prevent subsequent retries. + /// - `handle_response` -- [in] callback to be called with the result of the request. + template + void send_onion_request( + const Destination destination, + const std::optional body, + const bool is_retry, + network_response_callback_t handle_response); + + /// API: network/handle_errors + /// + /// Processes a non-success response to automatically perform any standard operations based on + /// the errors returned from the service node network. + /// + /// Inputs: + /// - `status_code` -- [in] the status code returned from the network. + /// - `response` -- [in, optional] response data returned from the network. + /// - `info` -- [in] the information for the request that was made. + /// - `handle_response` -- [in] callback to be called with updated response information after + /// processing the error. + void handle_errors( + const int16_t status_code, + const std::optional response, + const request_info info, + network_response_callback_t handle_response); + + private: + std::shared_ptr get_connection(const service_node target); + + /// API: network/get_btstream + /// + /// Retrieves the `BTRequestStream` for the given target if there is an existing stream, + /// otherwise creates a new stream. + /// + /// Inputs: + /// - `target` -- [in] the service node we plan to send a request to. + /// + /// Outputs: + /// - a shared pointer to the `BTRequestStream` for the target service node. + std::shared_ptr get_btstream(const service_node target); + + /// API: network/process_snode_response + /// + /// Processes the response from an onion request sent to a service node destination. + /// + /// Inputs: + /// - `builder` -- [in] the builder that was used to build the onion request. + /// - `response` -- [in] the response data returned from the destination. + /// - `info` -- [in] the information for the request that was made. + /// - `handle_response` -- [in] callback to be called with updated response information after + /// processing the error. + void process_snode_response( + const session::onionreq::Builder builder, + const std::string response, + const request_info info, + network_response_callback_t handle_response); + + /// API: network/process_server_response + /// + /// Processes the response from an onion request sent to a server destination. + /// + /// Inputs: + /// - `builder` -- [in] the builder that was used to build the onion request. + /// - `response` -- [in] the response data returned from the destination. + /// - `info` -- [in] the information for the request that was made. + /// - `handle_response` -- [in] callback to be called with updated response information after + /// processing the error. + void process_server_response( + const session::onionreq::Builder builder, + const std::string response, + const request_info info, + network_response_callback_t handle_response); +}; + +} // namespace session::network diff --git a/include/session/network_service_node.h b/include/session/network_service_node.h index e81de1d4..02020890 100644 --- a/include/session/network_service_node.h +++ b/include/session/network_service_node.h @@ -9,8 +9,8 @@ extern "C" { #include typedef struct network_service_node { - char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. - uint16_t lmq_port; + uint8_t ip[4]; + uint16_t quic_port; char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. diff --git a/include/session/network_service_node.hpp b/include/session/network_service_node.hpp index 0932089b..6e149105 100644 --- a/include/session/network_service_node.hpp +++ b/include/session/network_service_node.hpp @@ -8,29 +8,29 @@ namespace session::network { struct service_node { - std::string ip; - uint16_t lmq_port; + std::array ip; + uint16_t quic_port; session::onionreq::x25519_pubkey x25519_pubkey; session::onionreq::ed25519_pubkey ed25519_pubkey; uint8_t failure_count; bool invalid; service_node( - std::string ip, - uint16_t lmq_port, + std::array ip, + uint16_t quic_port, session::onionreq::x25519_pubkey x25519_pubkey, session::onionreq::ed25519_pubkey ed25519_pubkey, uint8_t failure_count, bool invalid) : - ip{std::move(ip)}, - lmq_port{lmq_port}, + ip{ip}, + quic_port{quic_port}, x25519_pubkey{std::move(x25519_pubkey)}, ed25519_pubkey{std::move(ed25519_pubkey)}, failure_count{failure_count}, invalid{invalid} {} bool operator==(const service_node& other) const { - return ip == other.ip && lmq_port == other.lmq_port && + return ip == other.ip && quic_port == other.quic_port && x25519_pubkey == other.x25519_pubkey && ed25519_pubkey == other.ed25519_pubkey && failure_count == other.failure_count && invalid == other.invalid; } diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 60be767f..8718bf59 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -23,8 +23,8 @@ typedef struct onion_request_builder_object { } onion_request_builder_object; typedef struct onion_request_service_node_destination { - char ip[40]; // IPv4 is 15 chars, IPv6 is 39 chars, + null terminator. - uint16_t lmq_port; + uint8_t ip[4]; + uint16_t quic_port; char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. @@ -34,12 +34,6 @@ typedef struct onion_request_service_node_destination { const size_t swarm_count; } onion_request_service_node_destination; -typedef struct onion_request_path { - const network_service_node* nodes; - const size_t nodes_count; - uint8_t failure_count; -} onion_request_path; - /// API: groups/onion_request_builder_init /// /// Constructs an onion request builder and sets a pointer to it in `builder`. @@ -55,14 +49,6 @@ LIBSESSION_EXPORT void onion_request_builder_init(onion_request_builder_object** /// /// Wrapper around session::onionreq::Builder::onion_request_builder_set_enc_type. /// -/// Declaration: -/// ```cpp -/// void onion_request_builder_set_enc_type( -/// [in] onion_request_builder_object* builder -/// [in] ENCRYPT_TYPE enc_type -/// ); -/// ``` -/// /// Inputs: /// - `builder` -- [in] Pointer to the builder object /// - `enc_type` -- [in] The encryption type to use in the onion request @@ -74,26 +60,17 @@ LIBSESSION_EXPORT void onion_request_builder_set_enc_type( /// Wrapper around session::onionreq::Builder::set_snode_destination. ed25519_pubkey and /// x25519_pubkey are both hex strings and must both be exactly 64 characters. /// -/// Declaration: -/// ```cpp -/// void onion_request_builder_set_snode_destination( -/// [in] onion_request_builder_object* builder -/// [in] const char* ed25519_pubkey, -/// [in] const char* x25519_pubkey -/// ); -/// ``` -/// /// Inputs: /// - `builder` -- [in] Pointer to the builder object /// - `ip` -- [in] The IP address for the snode destination -/// - `lmq_port` -- [in] The LMQ port request for the snode destination +/// - `quic_port` -- [in] The Quic port request for the snode destination /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination /// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination /// - `failure_count` -- [in] The number of times requests to this service node have failed LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, - const char* ip, - const uint16_t lmq_port, + const uint8_t ip[4], + const uint16_t quic_port, const char* x25519_pubkey, const char* ed25519_pubkey, const uint8_t failure_count); diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index f3063b61..24c603be 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -8,11 +8,6 @@ namespace session::onionreq { -struct onion_path { - std::vector nodes; - uint8_t failure_count; -}; - struct SnodeDestination { session::network::service_node node; std::optional> swarm; diff --git a/include/session/util.hpp b/include/session/util.hpp index cefe52dd..a8303b79 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -369,4 +369,20 @@ std::vector to_view_vector(const Container& c) { return to_view_vector(c.begin(), c.end()); } +/// Splits a string on some delimiter string and returns a vector of string_view's pointing into the +/// pieces of the original string. The pieces are valid only as long as the original string remains +/// valid. Leading and trailing empty substrings are not removed. If delim is empty you get back a +/// vector of string_views each viewing one character. If `trim` is true then leading and trailing +/// empty values will be suppressed. +/// +/// auto v = split("ab--c----de", "--"); // v is {"ab", "c", "", "de"} +/// auto v = split("abc", ""); // v is {"a", "b", "c"} +/// auto v = split("abc", "c"); // v is {"ab", ""} +/// auto v = split("abc", "c", true); // v is {"ab"} +/// auto v = split("-a--b--", "-"); // v is {"", "a", "", "b", "", ""} +/// auto v = split("-a--b--", "-", true); // v is {"a", "", "b"} +/// +std::vector split( + std::string_view str, std::string_view delim, bool trim = false); + } // namespace session diff --git a/src/network.cpp b/src/network.cpp index 72b0eaf3..a6697d4f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include #include @@ -29,267 +31,187 @@ using namespace std::literals; namespace session::network { -class Timeout : public std::exception {}; +namespace { -// The number of times a path can fail before it's replaced. -const uint16_t path_failure_threshold = 3; + inline auto log_cat = oxen::log::Cat("network"); -// The number of times a snode can fail before it's replaced. -const uint16_t snode_failure_threshold = 3; + class Timeout : public std::exception {}; -constexpr auto node_not_found_prefix = "Next node not found: "sv; -constexpr auto ALPN = "oxenstorage"sv; -const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; -std::shared_ptr buffer; + const std::chrono::seconds default_timeout = 5s; -void send_request(const request_info info, network_response_callback_t handle_response); + // The number of times a path can fail before it's replaced. + const uint16_t path_failure_threshold = 3; -void add_network_logger(std::function callback) { - buffer = std::make_shared(100UL, callback); - oxen::log::add_sink(buffer); -} - -void handle_errors( - const int16_t status_code, - const std::optional response, - const request_info info, - network_response_callback_t handle_response) { - switch (status_code) { - // A 404 or a 400 is likely due to a bad/missing SOGS or file so - // shouldn't mark a path or snode as invalid - case 400: - case 404: - return handle_response(false, false, status_code, response, service_node_changes{}); + // The number of times a snode can fail before it's replaced. + const uint16_t snode_failure_threshold = 3; - // The user's clock is out of sync with the service node network (a - // snode will return 406, but V4 onion requests returns a 425) - case 406: - case 425: - return handle_response(false, false, status_code, response, service_node_changes{}); - - // The snode is reporting that it isn't associated with the given public key anymore. If - // this is the first 421 then we want to try another node in the swarm (just in case it was - // reported incorrectly). If this is the second occurrence of the 421 then the client needs - // to update the swarm (if the response contains updated swarm data), or increment the path - // failure count. - case 421: - try { - // If there is no response data or no swarm informaiton was provided then we should - // just replace the swarm - if (!info.swarm) - throw std::invalid_argument{"Unable to handle redirect."}; + constexpr auto node_not_found_prefix = "Next node not found: "sv; + constexpr auto ALPN = "oxenstorage"sv; + const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; - // If this was the first 421 then we want to retry using another node in the swarm - // to get confirmation that we should switch to a different swarm - if (!info.is_retry) { - std::random_device rd; - std::mt19937 g(rd()); - std::vector swarm_copy = *info.swarm; - std::shuffle(swarm_copy.begin(), swarm_copy.end(), g); - - std::optional random_node; - - for (const auto& node : swarm_copy) { - if (node == info.target) - continue; - - random_node = node; - break; - } - - if (!random_node) - throw std::invalid_argument{"No other nodes in the swarm."}; + std::array split_ipv4(std::string_view ip) { + std::array quad; + auto nums = split(ip, "."); + if (nums.size() != 4) + throw "Invalid IPv4 address"; + for (int i = 0; i < 4; i++) { + auto end = nums[i].data() + nums[i].size(); + if (auto [p, ec] = std::from_chars(nums[i].data(), end, quad[i]); + ec != std::errc{} || p != end) + throw "Invalid malformed IPv4 address"; + } - if (info.path) - return send_onion_request( - *info.path, - SnodeDestination{*random_node, *info.swarm}, - info.body, - info.ed_sk, - true, - handle_response); + return quad; + } +} // namespace - return send_request( - {info.ed_sk, - *random_node, - info.endpoint, - info.body, - info.swarm, - std::nullopt, - true}, - handle_response); - } +Network::Network(const session::onionreq::ed25519_seckey ed25519_seckey) { + creds = GNUTLSCreds::make_from_ed_seckey(std::string(ed25519_seckey.view())); + endpoint = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); +} - if (!response) - throw std::invalid_argument{"No response data."}; +void Network::add_logger(std::function callback) { + buffer = std::make_shared(100UL, callback); + oxen::log::add_sink(buffer); +} - auto response_json = nlohmann::json::parse(*response); - auto snodes = response_json["snodes"]; +void Network::add_path(std::vector nodes, uint8_t failure_count) { + if (nodes.empty()) + throw std::invalid_argument{"No nodes in the path"}; - if (!snodes.is_array()) - throw std::invalid_argument{"Invalid JSON response."}; + auto existing_path = net.call_get([this, node = nodes.front()]() -> std::optional { + auto target_path = std::find_if(paths.begin(), paths.end(), [node](const auto& path) { + return !path.nodes.empty() && node == path.nodes.front(); + }); - std::vector swarm; + if (target_path == paths.end()) + return std::nullopt; - for (auto snode : snodes) - swarm.emplace_back( - snode["ip"].get(), - snode["port_omq"].get(), - x25519_pubkey::from_hex(snode["pubkey_x25519"].get()), - ed25519_pubkey::from_hex(snode["pubkey_ed25519"].get()), - 0, - false); + return *target_path; + }); - if (swarm.empty()) - throw std::invalid_argument{"No snodes in the response."}; + if (existing_path) + throw std::invalid_argument{"Cannot have multiple paths with the same starting node"}; - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ServiceNodeChangeType::replace_swarm, swarm}); - } catch (...) { - // If we don't have a path then this is a direct request so we can only update the - // failure count for the target node - if (!info.path) { - auto updated_node = info.target; - updated_node.failure_count += 1; + auto c = get_connection(nodes.front()); + net.call([this, c, nodes, failure_count]() mutable { + paths.emplace_back(onion_path{std::move(c), std::move(nodes), failure_count}); + }); +} - if (updated_node.failure_count >= snode_failure_threshold) - updated_node.invalid = true; +void Network::remove_path(session::network::service_node node) { + auto it = std::find_if(paths.begin(), paths.end(), [&node](const auto& path) { + return path.nodes[0] == node; + }); - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_node, {updated_node}}); - } + if (it != paths.end()) + paths.erase(it); +} - auto updated_path = *info.path; - updated_path.failure_count += 1; +void Network::remove_all_paths() { + paths.clear(); +} - // If the path has failed too many times we want to drop the guard snode (marking it - // as invalid) and increment the failure count of each node in the path - if (updated_path.failure_count >= path_failure_threshold) { - updated_path.nodes[0].invalid = true; +std::shared_ptr Network::get_connection( + const service_node target) { + std::stringstream ss; + for (size_t i = 0; i < target.ip.size(); ++i) { + if (i != 0) + ss << "."; + ss << static_cast(target.ip[i]); + } + auto remote = RemoteAddress{target.ed25519_pubkey.view(), ss.str(), target.quic_port}; + + return endpoint->connect( + remote, + creds, + oxen::quic::opt::keep_alive{10s}, + [this, &target](connection_interface& conn, uint64_t) { + auto target_path = + std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + return !path.nodes.empty() && target == path.nodes.front(); + }); + + if (target_path != paths.end() && target_path->conn && + conn.reference_id() == target_path->conn->reference_id()) + target_path->conn.reset(); + }); +} - for (auto& it : updated_path.nodes) { - it.failure_count += 1; +std::shared_ptr Network::get_btstream(const service_node target) { + auto has_target_path = net.call_get([this, &target]() -> bool { + auto target_path = std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + return !path.nodes.empty() && target == path.nodes.front(); + }); - if (it.failure_count >= snode_failure_threshold) - it.invalid = true; + return target_path != paths.end(); + }); + + // If we are targeting one of the paths then wait for `default_timeout` to give it a chance to + // create an active connection + std::chrono::milliseconds wait_time_ms = 0ms; + while (has_target_path && wait_time_ms < default_timeout) { + auto result = net.call_get( + [this, &target]() -> std::pair, bool> { + auto target_path = + std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + return !path.nodes.empty() && target == path.nodes.front(); + }); + + if (target_path != paths.end() && target_path->conn && + target_path->conn->remote_key() != to_usv(target.ed25519_pubkey.view())) { + if (auto str = + target_path->conn->maybe_stream(0)) + return {str, true}; + + return {nullptr, true}; } - } - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_path, - updated_path.nodes, - updated_path.failure_count, - (updated_path.failure_count >= path_failure_threshold)}); - } - - default: - // If we don't have a path then this is a direct request so we can only update the - // failure count for the target node - if (!info.path) { - auto updated_node = info.target; - updated_node.failure_count += 1; - - if (updated_node.failure_count >= snode_failure_threshold) - updated_node.invalid = true; - - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ServiceNodeChangeType::update_node, {updated_node}}); - } - - auto updated_path = *info.path; - bool found_invalid_node = false; - - if (response && starts_with(*response, node_not_found_prefix)) { - std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; - - if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { - session::onionreq::ed25519_pubkey edpk = - session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); - - auto snode_it = std::find_if( - updated_path.nodes.begin(), - updated_path.nodes.end(), - [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); + return {nullptr, false}; + }); - // Increment the failure count for the snode - if (snode_it != updated_path.nodes.end()) { - snode_it->failure_count += 1; - found_invalid_node = true; + // If we were able to get a valid connection then return it + if (result.first) + return result.first; - if (snode_it->failure_count >= snode_failure_threshold) - snode_it->invalid = true; - } - } - } + // Otherwise if there was no connection at all then return nullptr immediately so a new + // connection gets created (this can happen if the `conn` times out or fails initially so we + // don't want to bother looping in that case) + if (!result.second) + break; - // If we didn't find the specific node that was invalid then increment the path failure - // count - if (!found_invalid_node) { - // Increment the path failure count - updated_path.failure_count += 1; + std::this_thread::sleep_for(100ms); + wait_time_ms += 100ms; + } - // If the path has failed too many times we want to drop the guard snode (marking it - // as invalid) and increment the failure count of each node in the path - if (updated_path.failure_count >= path_failure_threshold) { - updated_path.nodes[0].invalid = true; + // We weren't able to get an existing connection so we need to create a new one + auto c = get_connection(target); + net.call([this, c, &target]() mutable { + auto target_path = std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + return !path.nodes.empty() && target == path.nodes.front(); + }); - for (auto& it : updated_path.nodes) { - it.failure_count += 1; + if (target_path == paths.end()) + return; - if (it.failure_count >= snode_failure_threshold) - it.invalid = true; - } - } - } + target_path->conn = std::move(c); + }); + std::__1::shared_ptr str = + c->open_stream(); - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_path, - updated_path.nodes, - updated_path.failure_count, - (updated_path.failure_count >= path_failure_threshold)}); - } + return str; } -void send_request(const request_info info, network_response_callback_t handle_response) { +void Network::send_request(const request_info info, network_response_callback_t handle_response) { try { - Network net; std::promise prom; - auto remote = RemoteAddress{ - info.target.ed25519_pubkey.view(), info.target.ip, info.target.lmq_port}; - auto creds = GNUTLSCreds::make_from_ed_seckey(std::string(from_unsigned_sv(info.ed_sk))); - auto ep = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); - auto c = ep->connect(remote, creds); - auto s = c->open_stream(); bstring_view payload = {}; if (info.body) payload = bstring_view{ reinterpret_cast(info.body->data()), info.body->size()}; - s->command(info.endpoint, payload, [&prom](message resp) { + get_btstream(info.target)->command(info.endpoint, payload, [&prom](message resp) { try { if (resp.timed_out) throw Timeout{}; @@ -335,14 +257,13 @@ void send_request(const request_info info, network_response_callback_t handle_re } } -void send_request( - const ustring_view ed_sk, +void Network::send_request( const session::network::service_node target, const std::string endpoint, const std::optional body, const std::optional> swarm, network_response_callback_t handle_response) { - send_request({ed_sk, target, endpoint, body, swarm, std::nullopt, false}, handle_response); + send_request({target, endpoint, body, swarm, std::nullopt, false}, handle_response); } template @@ -358,7 +279,7 @@ std::optional> swarm_for_destination } // The SnodeDestination runs via V3 onion requests -void process_snode_response( +void Network::process_snode_response( const Builder builder, const std::string response, const request_info info, @@ -414,7 +335,7 @@ void process_snode_response( } // The ServerDestination runs via V4 onion requests -void process_server_response( +void Network::process_server_response( const Builder builder, const std::string response, const request_info info, @@ -461,23 +382,61 @@ void process_server_response( } template -void send_onion_request( - const onion_path path, +std::vector valid_paths_for_destination( + const std::vector paths, const Destination /*destination*/) { + return paths; +} + +template <> +std::vector valid_paths_for_destination( + const std::vector paths, const SnodeDestination destination) { + std::vector valid_paths = paths; + valid_paths.erase( + std::remove_if( + valid_paths.begin(), + valid_paths.end(), + [&destination](const onion_path& path) { + return std::any_of( + path.nodes.begin(), + path.nodes.end(), + [&destination](const service_node& node) { + return node == destination.node; + }); + }), + valid_paths.end()); + return valid_paths; +} + +template +void Network::send_onion_request( const Destination destination, const std::optional body, - const ustring_view ed_sk, const bool is_retry, network_response_callback_t handle_response) { - if (path.nodes.empty()) { + // Select a random path + auto paths = net.call_get([this]() -> std::vector { return this->paths; }); + auto valid_paths = valid_paths_for_destination(paths, destination); + onion_path path; + + if (valid_paths.empty()) { handle_response( false, false, -1, - "No nodes in the path", + (paths.empty() ? "No onion paths" : "No valid onion paths"), service_node_changes{ServiceNodeChangeType::invalid_path}); return; } + if (valid_paths.size() == 1) + path = valid_paths.front(); + else { + std::random_device rd; + std::uniform_int_distribution dist(0, valid_paths.size() - 1); + uint32_t random_index = dist(rd); + path = valid_paths[random_index]; + } + try { // Construct the onion request auto builder = Builder(); @@ -490,7 +449,6 @@ void send_onion_request( auto onion_req_payload = builder.build(payload); request_info info = { - ed_sk, path.nodes[0], "onion_req", onion_req_payload, @@ -500,7 +458,8 @@ void send_onion_request( send_request( info, - [builder = std::move(builder), + [this, + builder = std::move(builder), info, destination = std::move(destination), callback = std::move(handle_response)]( @@ -525,18 +484,239 @@ void send_onion_request( } } +void Network::handle_errors( + const int16_t status_code, + const std::optional response, + const request_info info, + network_response_callback_t handle_response) { + switch (status_code) { + // A 404 or a 400 is likely due to a bad/missing SOGS or file so + // shouldn't mark a path or snode as invalid + case 400: + case 404: + return handle_response(false, false, status_code, response, service_node_changes{}); + + // The user's clock is out of sync with the service node network (a + // snode will return 406, but V4 onion requests returns a 425) + case 406: + case 425: + return handle_response(false, false, status_code, response, service_node_changes{}); + + // The snode is reporting that it isn't associated with the given public key anymore. If + // this is the first 421 then we want to try another node in the swarm (just in case it was + // reported incorrectly). If this is the second occurrence of the 421 then the client needs + // to update the swarm (if the response contains updated swarm data), or increment the path + // failure count. + case 421: + try { + // If there is no response data or no swarm informaiton was provided then we should + // just replace the swarm + if (!info.swarm) + throw std::invalid_argument{"Unable to handle redirect."}; + + // If this was the first 421 then we want to retry using another node in the swarm + // to get confirmation that we should switch to a different swarm + if (!info.is_retry) { + std::random_device rd; + std::mt19937 g(rd()); + std::vector swarm_copy = *info.swarm; + std::shuffle(swarm_copy.begin(), swarm_copy.end(), g); + + std::optional random_node; + + for (const auto& node : swarm_copy) { + if (node == info.target) + continue; + + random_node = node; + break; + } + + if (!random_node) + throw std::invalid_argument{"No other nodes in the swarm."}; + + if (info.path) + return send_onion_request( + SnodeDestination{*random_node, *info.swarm}, + info.body, + true, + handle_response); + + return send_request( + {*random_node, + info.endpoint, + info.body, + info.swarm, + std::nullopt, + true}, + handle_response); + } + + if (!response) + throw std::invalid_argument{"No response data."}; + + auto response_json = nlohmann::json::parse(*response); + auto snodes = response_json["snodes"]; + + if (!snodes.is_array()) + throw std::invalid_argument{"Invalid JSON response."}; + + std::vector swarm; + + for (auto snode : snodes) { + swarm.emplace_back( + split_ipv4(snode["ip"].get()), + snode["port_omq"].get(), + x25519_pubkey::from_hex(snode["pubkey_x25519"].get()), + ed25519_pubkey::from_hex(snode["pubkey_ed25519"].get()), + 0, + false); + } + + if (swarm.empty()) + throw std::invalid_argument{"No snodes in the response."}; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ServiceNodeChangeType::replace_swarm, swarm}); + } catch (...) { + // If we don't have a path then this is a direct request so we can only update the + // failure count for the target node + if (!info.path) { + auto updated_node = info.target; + updated_node.failure_count += 1; + + if (updated_node.failure_count >= snode_failure_threshold) + updated_node.invalid = true; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_node, {updated_node}}); + } + + auto updated_path = *info.path; + updated_path.failure_count += 1; + + // If the path has failed too many times we want to drop the guard snode (marking it + // as invalid) and increment the failure count of each node in the path + if (updated_path.failure_count >= path_failure_threshold) { + updated_path.nodes[0].invalid = true; + + for (auto& it : updated_path.nodes) { + it.failure_count += 1; + + if (it.failure_count >= snode_failure_threshold) + it.invalid = true; + } + } + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_path, + updated_path.nodes, + updated_path.failure_count, + (updated_path.failure_count >= path_failure_threshold)}); + } + + default: + // If we don't have a path then this is a direct request so we can only update the + // failure count for the target node + if (!info.path) { + auto updated_node = info.target; + updated_node.failure_count += 1; + + if (updated_node.failure_count >= snode_failure_threshold) + updated_node.invalid = true; + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ServiceNodeChangeType::update_node, {updated_node}}); + } + + auto updated_path = *info.path; + bool found_invalid_node = false; + + if (response && starts_with(*response, node_not_found_prefix)) { + std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; + + if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { + session::onionreq::ed25519_pubkey edpk = + session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); + + auto snode_it = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); + + // Increment the failure count for the snode + if (snode_it != updated_path.nodes.end()) { + snode_it->failure_count += 1; + found_invalid_node = true; + + if (snode_it->failure_count >= snode_failure_threshold) + snode_it->invalid = true; + } + } + } + + // If we didn't find the specific node that was invalid then increment the path failure + // count + if (!found_invalid_node) { + // Increment the path failure count + updated_path.failure_count += 1; + + // If the path has failed too many times we want to drop the guard snode (marking it + // as invalid) and increment the failure count of each node in the path + if (updated_path.failure_count >= path_failure_threshold) { + updated_path.nodes[0].invalid = true; + + for (auto& it : updated_path.nodes) { + it.failure_count += 1; + + if (it.failure_count >= snode_failure_threshold) + it.invalid = true; + } + } + } + + return handle_response( + false, + false, + status_code, + response, + service_node_changes{ + ServiceNodeChangeType::update_path, + updated_path.nodes, + updated_path.failure_count, + (updated_path.failure_count >= path_failure_threshold)}); + } +} + std::vector convert_service_nodes( const std::vector nodes) { std::vector converted_nodes; for (const auto& node : nodes) { network_service_node converted_node; - strncpy(converted_node.ip, node.ip.c_str(), sizeof(converted_node.ip) - 1); - converted_node.ip[sizeof(converted_node.ip) - 1] = '\0'; // Ensure null termination + std::memcpy(converted_node.ip, node.ip.data(), sizeof(converted_node.ip)); strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); strncpy(converted_node.ed25519_pubkey_hex, node.ed25519_pubkey.hex().c_str(), 64); converted_node.x25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination - converted_node.lmq_port = node.lmq_port; + converted_node.quic_port = node.quic_port; converted_node.failure_count = node.failure_count; converted_node.invalid = node.invalid; converted_nodes.push_back(converted_node); @@ -547,17 +727,103 @@ std::vector convert_service_nodes( } // namespace session::network +namespace { + +inline session::network::Network& unbox(network_object* network_) { + assert(network_ && network_->internals); + return *static_cast(network_->internals); +} + +inline bool set_error(char* error, const std::exception& e) { + if (!error) + return false; + + std::string msg = e.what(); + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + return false; +} + +} // namespace + extern "C" { using namespace session::network; -LIBSESSION_C_API void network_add_logger(void (*callback)(const char*, size_t)) { +LIBSESSION_C_API bool network_init( + network_object** network, const unsigned char* ed25519_secretkey_bytes, char* error) { + try { + auto n = std::make_unique( + ed25519_seckey::from_bytes({ed25519_secretkey_bytes, 64})); + auto n_object = std::make_unique(); + + n_object->internals = n.release(); + *network = n_object.release(); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API void network_free(network_object* network) { + delete network; +} + +LIBSESSION_C_API void network_add_logger( + network_object* network, void (*callback)(const char*, size_t)) { assert(callback); - add_network_logger([callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); + unbox(network).add_logger( + [callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); +} + +LIBSESSION_C_API bool network_add_path( + network_object* network, const onion_request_path path, char* error) { + try { + std::vector nodes; + for (size_t i = 0; i < path.nodes_count; i++) { + std::array ip; + std::memcpy(ip.data(), path.nodes[i].ip, ip.size()); + nodes.emplace_back( + ip, + path.nodes[i].quic_port, + x25519_pubkey::from_hex({path.nodes[i].x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({path.nodes[i].ed25519_pubkey_hex, 64}), + path.nodes[i].failure_count, + false); + } + + unbox(network).add_path(nodes, path.failure_count); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API bool network_remove_path( + network_object* network, const network_service_node node, char* error) { + try { + std::array ip; + std::memcpy(ip.data(), node.ip, ip.size()); + unbox(network).remove_path(session::network::service_node{ + ip, + node.quic_port, + x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), + ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), + node.failure_count, + false}); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API void network_remove_all_paths(network_object* network) { + unbox(network).remove_all_paths(); } LIBSESSION_C_API void network_send_request( - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const network_service_node destination, const char* endpoint, const unsigned char* body_, @@ -573,7 +839,7 @@ LIBSESSION_C_API void network_send_request( network_service_node_changes changes, void*), void* ctx) { - assert(ed25519_secretkey_bytes && endpoint && callback); + assert(endpoint && callback); std::optional body; if (body_size > 0) @@ -585,21 +851,26 @@ LIBSESSION_C_API void network_send_request( swarm = std::vector{}; swarm->reserve(swarm_count); - for (size_t i = 0; i < swarm_count; i++) + for (size_t i = 0; i < swarm_count; i++) { + std::array ip; + std::memcpy(ip.data(), swarm_[i].ip, ip.size()); swarm->emplace_back( - swarm_[i].ip, - swarm_[i].lmq_port, + ip, + swarm_[i].quic_port, x25519_pubkey::from_hex({swarm_[i].x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({swarm_[i].ed25519_pubkey_hex, 64}), swarm_[i].failure_count, false); + } } - send_request( - {ed25519_secretkey_bytes, 64}, + std::array ip; + std::memcpy(ip.data(), destination.ip, ip.size()); + + unbox(network).send_request( session::network::service_node{ - destination.ip, - destination.lmq_port, + ip, + destination.quic_port, x25519_pubkey::from_hex({destination.x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({destination.ed25519_pubkey_hex, 64}), destination.failure_count, @@ -633,8 +904,7 @@ LIBSESSION_C_API void network_send_request( } LIBSESSION_C_API void network_send_onion_request_to_snode_destination( - const onion_request_path path_, - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const onion_request_service_node_destination node, const unsigned char* body_, size_t body_size, @@ -647,21 +917,9 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( network_service_node_changes changes, void*), void* ctx) { - assert(ed25519_secretkey_bytes && callback); + assert(callback); try { - std::vector nodes; - for (size_t i = 0; i < path_.nodes_count; i++) - nodes.emplace_back( - path_.nodes[i].ip, - path_.nodes[i].lmq_port, - x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), - path_.nodes[i].failure_count, - false); - - session::onionreq::onion_path path = {nodes, path_.failure_count}; - std::optional body; if (body_size > 0) body = {body_, body_size}; @@ -672,28 +930,32 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( swarm = std::vector{}; swarm->reserve(node.swarm_count); - for (size_t i = 0; i < node.swarm_count; i++) + for (size_t i = 0; i < node.swarm_count; i++) { + std::array ip; + std::memcpy(ip.data(), node.swarm[i].ip, ip.size()); swarm->emplace_back( - node.swarm[i].ip, - node.swarm[i].lmq_port, + ip, + node.swarm[i].quic_port, x25519_pubkey::from_hex({node.swarm[i].x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({node.swarm[i].ed25519_pubkey_hex, 64}), node.swarm[i].failure_count, false); + } } - send_onion_request( - path, + std::array ip; + std::memcpy(ip.data(), node.ip, ip.size()); + + unbox(network).send_onion_request( SnodeDestination{ - {node.ip, - node.lmq_port, + {ip, + node.quic_port, x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), node.failure_count, false}, swarm}, body, - {ed25519_secretkey_bytes, 64}, false, [callback, ctx]( bool success, @@ -731,8 +993,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( } LIBSESSION_C_API void network_send_onion_request_to_server_destination( - const onion_request_path path_, - const unsigned char* ed25519_secretkey_bytes, + network_object* network, const char* method, const char* protocol, const char* host, @@ -756,21 +1017,9 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( network_service_node_changes changes, void*), void* ctx) { - assert(ed25519_secretkey_bytes && method && protocol && host && endpoint && x25519_pubkey && - callback); + assert(method && protocol && host && endpoint && x25519_pubkey && callback); try { - std::vector nodes; - for (size_t i = 0; i < path_.nodes_count; i++) - nodes.emplace_back( - path_.nodes[i].ip, - path_.nodes[i].lmq_port, - x25519_pubkey::from_hex({path_.nodes[i].x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({path_.nodes[i].ed25519_pubkey_hex, 64}), - path_.nodes[i].failure_count, - false); - - session::onionreq::onion_path path = {nodes, path_.failure_count}; std::optional>> headers; if (headers_size > 0) { headers = std::vector>{}; @@ -791,8 +1040,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( if (body_size > 0) body = {body_, body_size}; - send_onion_request( - path, + unbox(network).send_onion_request( ServerDestination{ protocol, host, @@ -803,7 +1051,6 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers, query_params}, body, - {ed25519_secretkey_bytes, 64}, false, [callback, ctx]( bool success, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 11af4617..4dfccd44 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -297,16 +297,18 @@ LIBSESSION_C_API void onion_request_builder_set_enc_type( LIBSESSION_C_API void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, - const char* ip, - const uint16_t lmq_port, + const uint8_t ip[4], + const uint16_t quic_port, const char* x25519_pubkey, const char* ed25519_pubkey, const uint8_t failure_count) { assert(builder && ip && x25519_pubkey && ed25519_pubkey); + std::array target_ip; + std::memcpy(target_ip.data(), ip, target_ip.size()); auto node = session::network::service_node{ - ip, - lmq_port, + target_ip, + quic_port, session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), failure_count, diff --git a/src/util.cpp b/src/util.cpp index 8a4d5b44..4a786b9a 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -19,4 +19,27 @@ void sodium_zero_buffer(void* ptr, size_t size) { sodium_memzero(ptr, size); } +std::vector split(std::string_view str, const std::string_view delim, bool trim) { + std::vector results; + // Special case for empty delimiter: splits on each character boundary: + if (delim.empty()) { + results.reserve(str.size()); + for (size_t i = 0; i < str.size(); i++) + results.emplace_back(str.data() + i, 1); + return results; + } + + for (size_t pos = str.find(delim); pos != std::string_view::npos; pos = str.find(delim)) { + if (!trim || !results.empty() || pos > 0) + results.push_back(str.substr(0, pos)); + str.remove_prefix(pos + delim.size()); + } + if (!trim || str.size()) + results.push_back(str); + else + while (!results.empty() && results.back().empty()) + results.pop_back(); + return results; +} + } // namespace session diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 9ca5d341..b689ee50 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -30,21 +30,22 @@ TEST_CASE("Network error handling", "[network]") { auto x_pk = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; auto x_pk2 = "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes; auto target = service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk), ed25519_pubkey::from_bytes(ed_pk), 0, false}; auto mock_request = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, std::nullopt, false}; + request_info{target, "test", std::nullopt, std::nullopt, std::nullopt, false}; Result result; + auto network = Network(ed25519_seckey::from_bytes(ed_sk)); // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; for (auto code : codes_with_no_changes) { - handle_errors( + network.handle_errors( code, std::nullopt, mock_request, @@ -65,7 +66,7 @@ TEST_CASE("Network error handling", "[network]") { } // Check general error handling with no provided path (first failure) - handle_errors( + network.handle_errors( 500, std::nullopt, mock_request, @@ -85,7 +86,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.type == ServiceNodeChangeType::update_node); REQUIRE(result.changes.nodes.size() == 1); CHECK(result.changes.nodes[0].ip == target.ip); - CHECK(result.changes.nodes[0].lmq_port == target.lmq_port); + CHECK(result.changes.nodes[0].quic_port == target.quic_port); CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 1); @@ -94,9 +95,8 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with no provided path (too many failures) auto mock_request2 = request_info{ - ed_sk, service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk), ed25519_pubkey::from_bytes(ed_pk), @@ -107,7 +107,7 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt, std::nullopt, false}; - handle_errors( + network.handle_errors( 500, std::nullopt, mock_request2, @@ -131,10 +131,9 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.path_failure_count == 0); // Check general error handling with a path but no response (first failure) - auto path = onion_path{{target}, 0}; - auto mock_request3 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; - handle_errors( + auto path = onion_path{nullptr, {target}, 0}; + auto mock_request3 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; + network.handle_errors( 500, std::nullopt, mock_request3, @@ -159,18 +158,18 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path but no response (too many path failures) path = onion_path{ + nullptr, {target, service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk), ed25519_pubkey::from_bytes(ed_pk), 0, false}}, 9}; - auto mock_request4 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; - handle_errors( + auto mock_request4 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; + network.handle_errors( 500, std::nullopt, mock_request4, @@ -197,18 +196,18 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path but no response (too many path & node failures) path = onion_path{ + nullptr, {target, service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk), ed25519_pubkey::from_bytes(ed_pk), 9, false}}, 9}; - auto mock_request5 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; - handle_errors( + auto mock_request5 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; + network.handle_errors( 500, std::nullopt, mock_request5, @@ -234,11 +233,10 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.path_failure_count == 10); // Check general error handling with a path and a random response (first failure) - path = onion_path{{target}, 0}; - auto mock_request6 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + path = onion_path{nullptr, {target}, 0}; + auto mock_request6 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; auto response = std::string{"Test"}; - handle_errors( + network.handle_errors( 500, response, mock_request6, @@ -263,19 +261,19 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (first failure) path = onion_path{ + nullptr, {target, service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk2), ed25519_pubkey::from_bytes(ed_pk2), 0, false}}, 0}; - auto mock_request7 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + auto mock_request7 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - handle_errors( + network.handle_errors( 500, response, mock_request7, @@ -304,19 +302,19 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (too many failures) path = onion_path{ + nullptr, {target, service_node{ - "0.0.0.0", + {0, 0, 0, 0}, 0, x25519_pubkey::from_bytes(x_pk2), ed25519_pubkey::from_bytes(ed_pk2), 9, false}}, 0}; - auto mock_request8 = - request_info{ed_sk, target, "test", std::nullopt, std::nullopt, path, false}; + auto mock_request8 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - handle_errors( + network.handle_errors( 500, response, mock_request8, @@ -344,7 +342,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.changes.path_failure_count == 0); // Check a 421 with no swarm data throws (no good way to handle this case) - handle_errors( + network.handle_errors( 421, std::nullopt, mock_request, @@ -364,8 +362,8 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with no response data throws (no good way to handle this // case) auto mock_request9 = request_info{ - ed_sk, target, "test", std::nullopt, std::vector{target}, path, true}; - handle_errors( + target, "test", std::nullopt, std::vector{target}, path, true}; + network.handle_errors( 421, std::nullopt, mock_request9, @@ -384,7 +382,7 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with non-swarm response data throws (no good way to handle // this case) - handle_errors( + network.handle_errors( 421, "Test", mock_request9, @@ -410,7 +408,7 @@ TEST_CASE("Network error handling", "[network]") { {"pubkey_ed25519", x25519_pubkey::from_bytes(ed_pk).hex()}}); nlohmann::json swarm_json{{"snodes", snodes}}; response = swarm_json.dump(); - handle_errors( + network.handle_errors( 421, response, mock_request9, @@ -428,8 +426,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 421); CHECK(result.changes.type == ServiceNodeChangeType::replace_swarm); REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].ip == "1.1.1.1"); - CHECK(result.changes.nodes[0].lmq_port == 1); + CHECK(result.changes.nodes[0].ip == std::array{{1, 1, 1, 1}}); + CHECK(result.changes.nodes[0].quic_port == 1); CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); CHECK(result.changes.nodes[0].failure_count == 0); @@ -442,7 +440,7 @@ TEST_CASE("Network direct request", "[send_request][network]") { "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto test_service_node = service_node{ - "144.76.164.202", + {144, 76, 164, 202}, 35400, x25519_pubkey::from_bytes( "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"_hexbytes), @@ -450,22 +448,26 @@ TEST_CASE("Network direct request", "[send_request][network]") { "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes), 0, false}; - Result result; + auto network = Network(ed25519_seckey::from_bytes(ed_sk)); + std::promise result_promise; - send_request( - ed_sk, + network.send_request( test_service_node, "info", std::nullopt, std::nullopt, - [&result]( + [&result_promise]( bool success, bool timeout, int16_t status_code, std::optional response, service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; + result_promise.set_value({success, timeout, status_code, response, changes}); }); + + // Wait for the result to be set + auto result = result_promise.get_future().get(); + CHECK(result.success); CHECK_FALSE(result.timeout); CHECK(result.status_code == 200); @@ -483,9 +485,12 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { auto ed_sk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + network_object* network; + network_init(&network, ed_sk.data(), nullptr); + std::array target_ip = {144, 76, 164, 202}; auto test_service_node = network_service_node{}; - test_service_node.lmq_port = 35400; - std::strcpy(test_service_node.ip, "144.76.164.202"); + test_service_node.quic_port = 35400; + std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); std::strcpy( test_service_node.x25519_pubkey_hex, "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"); @@ -494,7 +499,7 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); network_send_request( - ed_sk.data(), + network, test_service_node, "info", nullptr, @@ -507,7 +512,7 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { const char* c_response, size_t response_size, network_service_node_changes changes, - void*) { + void* ctx) { CHECK(success); CHECK_FALSE(timeout); CHECK(status_code == 200); @@ -521,6 +526,7 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { CHECK(response.contains("hf")); CHECK(response.contains("t")); CHECK(response.contains("version")); + network_free(static_cast(ctx)); }, - nullptr); + network); } From 1c4667ba0c56c924d4e957743d1324be2c899040 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 12 Apr 2024 18:37:42 +1000 Subject: [PATCH 200/572] Added a function to replace the secret key used for the network --- include/session/network.h | 17 +++++++++++++++++ include/session/network.hpp | 9 +++++++++ src/network.cpp | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/include/session/network.h b/include/session/network.h index 7e531228..f207c103 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -78,6 +78,23 @@ LIBSESSION_EXPORT void network_free(network_object* network); LIBSESSION_EXPORT void network_add_logger( network_object* network, void (*callback)(const char*, size_t)); +/// API: network/network_replace_key +/// +/// Replaces the secret key used to make network connections. Note: This will result in existing path +/// connections being removed and new ones created with the updated key on the next use. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `ed25519_seckey` -- [in] new ed25519 secret key to be used. +/// - `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 on success; returns false and write the exception message as a C-string +/// into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool network_replace_key(network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error); + /// API: network/network_add_path /// /// Adds a path to the list on the network object that is randomly selected from when making an diff --git a/include/session/network.hpp b/include/session/network.hpp index 34d5526f..3293c66c 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -69,6 +69,15 @@ class Network { /// - `callback` -- [in] callback to be called when a new message should be logged. void add_logger(std::function callback); + /// API: network/replace_key + /// + /// Replaces the secret key used to make network connections. Note: This will result in existing path + /// connections being removed and new ones created with the updated key on the next use. + /// + /// Inputs: + /// - `ed25519_seckey` -- [in] new ed25519 secret key to be used. + void replace_key(const session::onionreq::ed25519_seckey ed25519_seckey); + /// API: network/add_path /// /// Adds a path to the list on the network object that is randomly selected from when making an diff --git a/src/network.cpp b/src/network.cpp index a6697d4f..1a8dea41 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -75,6 +75,16 @@ void Network::add_logger(std::function callback) { oxen::log::add_sink(buffer); } +void Network::replace_key(const session::onionreq::ed25519_seckey ed25519_seckey) { + creds = GNUTLSCreds::make_from_ed_seckey(std::string(ed25519_seckey.view())); + + // Since the key is getting replaced we need to remove any connections from the paths + net.call([this]() mutable { + for (auto& path : paths) + path.conn.reset(); + }); +} + void Network::add_path(std::vector nodes, uint8_t failure_count) { if (nodes.empty()) throw std::invalid_argument{"No nodes in the path"}; @@ -777,6 +787,16 @@ LIBSESSION_C_API void network_add_logger( [callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); } +LIBSESSION_C_API bool network_replace_key(network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error) { + try { + unbox(network).replace_key(ed25519_seckey::from_bytes({ed25519_secretkey_bytes, 64})); + return true; + } + catch (const std::exception& e) { + return set_error(error, e); + } +} + LIBSESSION_C_API bool network_add_path( network_object* network, const onion_request_path path, char* error) { try { From 7651967104845db16e6a58f70635c01f7f4c2033 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Apr 2024 13:30:31 +1000 Subject: [PATCH 201/572] Fixed up lambda variable capture, ip formatting --- external/oxen-libquic | 2 +- src/network.cpp | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 74b78140..f8abbaaa 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 74b78140588ffcd495263db3f0ae9788142ca51b +Subproject commit f8abbaaa99b49f57258bedb84a8b46e3306ba73b diff --git a/src/network.cpp b/src/network.cpp index 1a8dea41..df175a62 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -9,8 +9,8 @@ #include #include #include +#include #include -#include #include #include @@ -28,6 +28,7 @@ using namespace session; using namespace oxen::quic; using namespace session::onionreq; using namespace std::literals; +using namespace oxen::log::literals; namespace session::network { @@ -90,7 +91,7 @@ void Network::add_path(std::vector nodes, uint8_ throw std::invalid_argument{"No nodes in the path"}; auto existing_path = net.call_get([this, node = nodes.front()]() -> std::optional { - auto target_path = std::find_if(paths.begin(), paths.end(), [node](const auto& path) { + auto target_path = std::find_if(paths.begin(), paths.end(), [&node](const auto& path) { return !path.nodes.empty() && node == path.nodes.front(); }); @@ -124,21 +125,16 @@ void Network::remove_all_paths() { std::shared_ptr Network::get_connection( const service_node target) { - std::stringstream ss; - for (size_t i = 0; i < target.ip.size(); ++i) { - if (i != 0) - ss << "."; - ss << static_cast(target.ip[i]); - } - auto remote = RemoteAddress{target.ed25519_pubkey.view(), ss.str(), target.quic_port}; + auto remote_ip = "{}"_format(fmt::join(target.ip, ".")); + auto remote = RemoteAddress{target.ed25519_pubkey.view(), remote_ip, target.quic_port}; return endpoint->connect( remote, creds, oxen::quic::opt::keep_alive{10s}, - [this, &target](connection_interface& conn, uint64_t) { + [this, target](connection_interface& conn, uint64_t) { auto target_path = - std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); @@ -150,7 +146,7 @@ std::shared_ptr Network::get_connection( std::shared_ptr Network::get_btstream(const service_node target) { auto has_target_path = net.call_get([this, &target]() -> bool { - auto target_path = std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); @@ -164,7 +160,7 @@ std::shared_ptr Network::get_btstream(const service auto result = net.call_get( [this, &target]() -> std::pair, bool> { auto target_path = - std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); @@ -197,7 +193,7 @@ std::shared_ptr Network::get_btstream(const service // We weren't able to get an existing connection so we need to create a new one auto c = get_connection(target); net.call([this, c, &target]() mutable { - auto target_path = std::find_if(paths.begin(), paths.end(), [target](const auto& path) { + auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); @@ -405,7 +401,7 @@ std::vector valid_paths_for_destination( std::remove_if( valid_paths.begin(), valid_paths.end(), - [&destination](const onion_path& path) { + [destination](const onion_path& path) { return std::any_of( path.nodes.begin(), path.nodes.end(), From 2bf82914af607e7b47d30fbaee62528c55ef7911 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Apr 2024 08:02:49 +1000 Subject: [PATCH 202/572] Updated to the latest stable libQuic commit --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index f8abbaaa..f7a02381 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit f8abbaaa99b49f57258bedb84a8b46e3306ba73b +Subproject commit f7a02381cc1e977614f7edda39f643e012a930d0 From d598b2e1d4a176ef9682f6ff92bdf4b065a0f4db Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Apr 2024 08:37:11 +1000 Subject: [PATCH 203/572] Ran the formatter and fixed a weird namespace mistake --- include/session/network.h | 11 ++++++----- include/session/network.hpp | 15 ++++++++------- src/network.cpp | 10 +++++----- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index f207c103..920c5cf4 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -80,8 +80,8 @@ LIBSESSION_EXPORT void network_add_logger( /// API: network/network_replace_key /// -/// Replaces the secret key used to make network connections. Note: This will result in existing path -/// connections being removed and new ones created with the updated key on the next use. +/// Replaces the secret key used to make network connections. Note: This will result in existing +/// path connections being removed and new ones created with the updated key on the next use. /// /// Inputs: /// - `network` -- [in] Pointer to the network object @@ -93,7 +93,8 @@ LIBSESSION_EXPORT void network_add_logger( /// Outputs: /// - `bool` -- Returns true on success; returns false and write the exception message as a C-string /// into `error` (if not NULL) on failure. -LIBSESSION_EXPORT bool network_replace_key(network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error); +LIBSESSION_EXPORT bool network_replace_key( + network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error); /// API: network/network_add_path /// @@ -133,8 +134,8 @@ LIBSESSION_EXPORT bool network_remove_path( /// API: network/network_remove_all_paths /// -/// Removes all paths from the list on the network object that are randomly selected from when making an -/// onion request. +/// Removes all paths from the list on the network object that are randomly selected from when +/// making an onion request. /// /// Inputs: /// - `network` -- [in] Pointer to the network object diff --git a/include/session/network.hpp b/include/session/network.hpp index 3293c66c..63c47755 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -71,8 +71,8 @@ class Network { /// API: network/replace_key /// - /// Replaces the secret key used to make network connections. Note: This will result in existing path - /// connections being removed and new ones created with the updated key on the next use. + /// Replaces the secret key used to make network connections. Note: This will result in existing + /// path connections being removed and new ones created with the updated key on the next use. /// /// Inputs: /// - `ed25519_seckey` -- [in] new ed25519 secret key to be used. @@ -85,13 +85,14 @@ class Network { /// /// Inputs: /// - `nodes` -- [in] nodes which make up the path to be added. - /// - `failure_count` -- [in] number of times the path has previously failed to complete a request. + /// - `failure_count` -- [in] number of times the path has previously failed to complete a + /// request. void add_path(std::vector nodes, uint8_t failure_count); /// API: network/remove_path /// - /// Removes a path from the list on the network object that is randomly selected from when making an - /// onion request. + /// Removes a path from the list on the network object that is randomly selected from when + /// making an onion request. /// /// Inputs: /// - `node` -- [in] first node in the path to be removed. @@ -99,8 +100,8 @@ class Network { /// API: network/remove_all_paths /// - /// Removes all paths from the list on the network object that are randomly selected from when making an - /// onion request. + /// Removes all paths from the list on the network object that are randomly selected from when + /// making an onion request. void remove_all_paths(); /// API: network/send_request diff --git a/src/network.cpp b/src/network.cpp index df175a62..b97309df 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -6,10 +6,10 @@ #include #include +#include #include #include #include -#include #include #include #include @@ -202,7 +202,7 @@ std::shared_ptr Network::get_btstream(const service target_path->conn = std::move(c); }); - std::__1::shared_ptr str = + std::shared_ptr str = c->open_stream(); return str; @@ -783,12 +783,12 @@ LIBSESSION_C_API void network_add_logger( [callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); } -LIBSESSION_C_API bool network_replace_key(network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error) { +LIBSESSION_C_API bool network_replace_key( + network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error) { try { unbox(network).replace_key(ed25519_seckey::from_bytes({ed25519_secretkey_bytes, 64})); return true; - } - catch (const std::exception& e) { + } catch (const std::exception& e) { return set_error(error, e); } } From 0829d2b366fc33599e748d0f4fa281d22f3bddd9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 15 Apr 2024 17:26:44 -0300 Subject: [PATCH 204/572] Switch to libquic `stable` branch && bump required system lib version --- external/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 4a432aa8..502767c8 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -106,7 +106,7 @@ set(OXENC_BUILD_DOCS OFF CACHE BOOL "") system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") -system_or_submodule(OXENQUIC quic liboxenquic>=1.0.1 oxen-libquic) +system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) if(CMAKE_C_COMPILER_LAUNCHER) set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") From 3aa53856cf0e76f8e4f77fad43fd8a7775c8baf2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 15 Apr 2024 17:41:17 -0300 Subject: [PATCH 205/572] Fix oxen::logging dependencies - Fixes compilation failure when libquic comes from a system library, where we wouldn't get oxen::logging transitively via oxen-libquic. - Fix `onionreq` target not explicitly depending on oxen::logging - Remove oxen logging include from headers so that dependent code (e.g. an external cmake project depending on libsession-util) doesn't have to have it to include libsession headers. - set OXEN_LOGGING_SOURCE_ROOT to strip the source directory out of logged source file paths. --- external/CMakeLists.txt | 7 ++++++- include/session/network.hpp | 5 ++++- src/CMakeLists.txt | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 502767c8..1d78fa08 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -27,7 +27,7 @@ if(SUBMODULE_CHECK) check_submodule(ios-cmake) check_submodule(libsodium-internal) check_submodule(oxen-encoding) - check_submodule(oxen-libquic) + check_submodule(oxen-libquic external/oxen-logging) check_submodule(nlohmann-json) check_submodule(zstd) endif() @@ -108,6 +108,11 @@ system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) +if(NOT TARGET oxen::logging) + add_subdirectory(oxen-libquic/external/oxen-logging) +endif() +set(OXEN_LOGGING_SOURCE_ROOT "${OXEN_LOGGING_SOURCE_ROOT};${PROJECT_SOURCE_DIR}" CACHE INTERNAL "") + if(CMAKE_C_COMPILER_LAUNCHER) set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") endif() diff --git a/include/session/network.hpp b/include/session/network.hpp index 63c47755..bea34116 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include "network_service_node.hpp" @@ -8,6 +7,10 @@ #include "session/onionreq/key_types.hpp" #include "session/types.hpp" +namespace oxen::log { +class RingBufferSink; +} + namespace session::network { enum class ServiceNodeChangeType { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a3adc489..7b3d2aaa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -99,6 +99,7 @@ target_link_libraries(onionreq common quic PRIVATE + oxen::logging nlohmann_json::nlohmann_json libsodium::sodium-internal nettle::nettle From b0656090eac45723a55dc764d24c9ddb078cd61d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 15 Apr 2024 17:59:23 -0300 Subject: [PATCH 206/572] Remove duplicate Windows build This build is identical to the Static Windows x64 further down. --- .drone.jsonnet | 3 --- 1 file changed, 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index d863fe49..2c15bf83 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -347,9 +347,6 @@ local static_build(name, debian_build('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64', jobs=4), debian_build('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), - // Windows builds (x64) - windows_cross_pipeline('Windows', docker_base + 'debian-win32-cross'), - // Macos builds: mac_builder('macOS Intel (Release)'), mac_builder('macOS Arm64 (Release)', arch='arm64'), From 3358960bc80e22bf3915db1d75fdd5015dea0f5b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Apr 2024 12:09:23 +1000 Subject: [PATCH 207/572] Removed backported starts_with and ends_with --- include/session/util.hpp | 9 --------- src/network.cpp | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index a8303b79..b052cdf4 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -78,15 +78,6 @@ inline bool string_iequal(std::string_view s1, std::string_view s2) { }); } -// C++20 starts_/ends_with backport -inline constexpr bool starts_with(std::string_view str, std::string_view prefix) { - return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix; -} - -inline constexpr bool end_with(std::string_view str, std::string_view suffix) { - return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix; -} - // Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure void* sodium_buffer_allocate(size_t size); // Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. diff --git a/src/network.cpp b/src/network.cpp index b97309df..595f21ac 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -656,7 +656,7 @@ void Network::handle_errors( auto updated_path = *info.path; bool found_invalid_node = false; - if (response && starts_with(*response, node_not_found_prefix)) { + if (response && response->starts_with(node_not_found_prefix)) { std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { From d61746d878e69b89d398e728f9428df5f19cd7bf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 23 Apr 2024 17:52:25 +1000 Subject: [PATCH 208/572] Implemented the snode pool cache logic, PR comments The Network object now manages an internal service node pool cache as well as the onion request paths needed for sending requests. When calling `send_onion_request` it will check the snode pool (populating it if needed) and check for possible onion request paths (creating them if needed) before sending the request - these pool/path population approaches are blocking so the caller can trigger multiple requests and doesn't need to worry about multiple requests being triggered to populate the cache. The Network object also has logic to persist and load this cache from disk (when initialised with a `cache_path`) in order to improve launch times after retrieving an initial cache. Additionally the Network object exposes a couple of hooks so the client can register for updates to the onion request paths or the network connection status. --- include/session/network.h | 175 +- include/session/network.hpp | 270 +-- include/session/network_service_node.hpp | 12 + include/session/onionreq/builder.h | 12 - include/session/onionreq/builder.hpp | 25 +- src/CMakeLists.txt | 1 + src/network.cpp | 2007 +++++++++++++++------- src/network_service_node.cpp | 83 + src/onionreq/builder.cpp | 83 +- tests/test_network.cpp | 510 ------ 10 files changed, 1704 insertions(+), 1474 deletions(-) create mode 100644 src/network_service_node.cpp diff --git a/include/session/network.h b/include/session/network.h index 920c5cf4..8b2925db 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -11,31 +11,33 @@ extern "C" { #include "network_service_node.h" #include "onionreq/builder.h" +typedef enum CONNECTION_STATUS { + CONNECTION_STATUS_UNKNOWN = 0, + CONNECTION_STATUS_CONNECTING = 1, + CONNECTION_STATUS_CONNECTED = 2, + CONNECTION_STATUS_DISCONNECTED = 3, +} CONNECTION_STATUS; + typedef struct network_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; } network_object; -typedef enum SERVICE_NODE_CHANGE_TYPE { - SERVICE_NODE_CHANGE_TYPE_NONE = 0, - SERVICE_NODE_CHANGE_TYPE_INVALID_PATH = 1, - SERVICE_NODE_CHANGE_TYPE_REPLACE_SWARM = 2, - SERVICE_NODE_CHANGE_TYPE_UPDATE_PATH = 3, - SERVICE_NODE_CHANGE_TYPE_UPDATE_NODE = 4, -} SERVICE_NODE_CHANGE_TYPE; - -typedef struct network_service_node_changes { - SERVICE_NODE_CHANGE_TYPE type; - network_service_node* nodes; - size_t nodes_count; - uint8_t failure_count; - bool invalid; -} network_service_node_changes; +typedef struct network_server_destination { + const char* method; + const char* protocol; + const char* host; + const char* endpoint; + uint16_t port; + const char* x25519_pubkey; + const char** headers; + const char** header_values; + size_t headers_size; +} network_server_destination; typedef struct onion_request_path { const network_service_node* nodes; const size_t nodes_count; - uint8_t failure_count; } onion_request_path; /// API: network/network_init @@ -47,8 +49,11 @@ typedef struct onion_request_path { /// /// Inputs: /// - `network` -- [out] Pointer to the network object -/// - `ed25519_secretkey_bytes` -- [in] must be the 64-byte libsodium "secret key" value. This -/// field cannot be null. +/// - `cache_path` -- [in] Path where the snode cache files should be stored. Should be +/// NULL-terminated. +/// - `use_testnet` -- [in] Flag indicating whether the network should connect to testnet or +/// mainnet. +/// - `pre_build_paths` -- [in] Flag indicating whether the network should pre-build it's paths. /// - `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. @@ -57,8 +62,11 @@ typedef struct onion_request_path { /// - `bool` -- Returns true on success; returns false and write the exception message as a C-string /// into `error` (if not NULL) on failure. LIBSESSION_EXPORT bool network_init( - network_object** network, const unsigned char* ed25519_secretkey_bytes, char* error) - __attribute__((warn_unused_result)); + network_object** network, + const char* cache_path, + bool use_testnet, + bool pre_build_paths, + char* error) __attribute__((warn_unused_result)); /// API: network/network_free /// @@ -78,101 +86,48 @@ LIBSESSION_EXPORT void network_free(network_object* network); LIBSESSION_EXPORT void network_add_logger( network_object* network, void (*callback)(const char*, size_t)); -/// API: network/network_replace_key -/// -/// Replaces the secret key used to make network connections. Note: This will result in existing -/// path connections being removed and new ones created with the updated key on the next use. -/// -/// Inputs: -/// - `network` -- [in] Pointer to the network object -/// - `ed25519_seckey` -- [in] new ed25519 secret key to be used. -/// - `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. +/// API: network/network_clear_cache /// -/// Outputs: -/// - `bool` -- Returns true on success; returns false and write the exception message as a C-string -/// into `error` (if not NULL) on failure. -LIBSESSION_EXPORT bool network_replace_key( - network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error); +/// Clears the cached from memory and from disk (if a cache path was provided during +/// initialization). +LIBSESSION_EXPORT void network_clear_cache(network_object* network); -/// API: network/network_add_path +/// API: network/network_set_status_changed_callback /// -/// Adds a path to the list on the network object that is randomly selected from when making an -/// onion request. +/// Registers a callback to be called whenever the network connection status changes. /// /// Inputs: /// - `network` -- [in] Pointer to the network object -/// - `path` -- [in] the path of service nodes to be added as an option to the network. -/// - `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 on success; returns false and write the exception message as a C-string -/// into `error` (if not NULL) on failure. -LIBSESSION_EXPORT bool network_add_path( - network_object* network, const onion_request_path path, char* error); +/// - `callback` -- [in] callback to be called when the network connection status changes. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. +LIBSESSION_EXPORT void network_set_status_changed_callback( + network_object* network, void (*callback)(CONNECTION_STATUS status, void* ctx), void* ctx); -/// API: network/network_remove_path +/// API: network/network_set_paths_changed_callback /// -/// Removes a path from the list on the network object that is randomly selected from when making an -/// onion request. +/// Registers a callback to be called whenever the onion request paths are updated. /// /// Inputs: /// - `network` -- [in] Pointer to the network object -/// - `path` -- [in] the path of service nodes to be removed from the network. -/// - `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 on success; returns false and write the exception message as a C-string -/// into `error` (if not NULL) on failure. -LIBSESSION_EXPORT bool network_remove_path( - network_object* network, const network_service_node node, char* error); +/// - `callback` -- [in] callback to be called when the onion request paths are updated. NOTE: The +/// `paths` value will be freed immediately after the callback is triggered so the data needs to be +/// copied if it needs to live longer than this. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. +LIBSESSION_EXPORT void network_set_paths_changed_callback( + network_object* network, + void (*callback)(onion_request_path* paths, size_t paths_len, void* ctx), + void* ctx); -/// API: network/network_remove_all_paths -/// -/// Removes all paths from the list on the network object that are randomly selected from when -/// making an onion request. -/// -/// Inputs: -/// - `network` -- [in] Pointer to the network object -LIBSESSION_EXPORT void network_remove_all_paths(network_object* network); +LIBSESSION_EXPORT void network_get_swarm( + network_object* network, + const char* swarm_pubkey_hex, + void (*callback)(network_service_node*, size_t, void*), + void* ctx); -/// API: network/network_send_request -/// -/// Sends a request directly to the provided service node. -/// -/// Inputs: -/// - `network` -- [in] Pointer to the network object. -/// - `destination` -- [in] address information about the service node the request should be sent -/// to. -/// - `endpoint` -- [in] endpoint for the request. -/// - `body` -- [in] data to send to the specified endpoint. -/// - `body_size` -- [in] size of the `body`. -/// - `swarm` -- [in] current swarm information for the destination service node. Set to NULL if not -/// used. -/// - `swarm_count` -- [in] number of service nodes included in the `swarm`. -/// - `callback` -- [in] callback to be called with the result of the request. -/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. -LIBSESSION_EXPORT void network_send_request( +LIBSESSION_EXPORT void network_get_random_nodes( network_object* network, - const network_service_node destination, - const char* endpoint, - const unsigned char* body, - size_t body_size, - const network_service_node* swarm, - const size_t swarm_count, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - network_service_node_changes changes, - void*), + uint16_t count, + void (*callback)(network_service_node*, size_t, void*), void* ctx); /// API: network/network_send_onion_request_to_snode_destination @@ -188,16 +143,16 @@ LIBSESSION_EXPORT void network_send_request( /// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( network_object* network, - const onion_request_service_node_destination node, + const network_service_node node, const unsigned char* body, size_t body_size, + const char* swarm_pubkey_hex, void (*callback)( bool success, bool timeout, int16_t status_code, const char* response, size_t response_size, - network_service_node_changes changes, void*), void* ctx); @@ -229,18 +184,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( /// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( network_object* network, - const char* method, - const char* protocol, - const char* host, - const char* endpoint, - uint16_t port, - const char* x25519_pubkey, - const char** query_param_keys, - const char** query_param_values, - size_t query_params_size, - const char** headers, - const char** header_values, - size_t headers_size, + const network_server_destination server, const unsigned char* body, size_t body_size, void (*callback)( @@ -249,7 +193,6 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, - network_service_node_changes changes, void*), void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index bea34116..e0d9276b 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include "network_service_node.hpp" @@ -7,62 +9,82 @@ #include "session/onionreq/key_types.hpp" #include "session/types.hpp" -namespace oxen::log { -class RingBufferSink; -} - namespace session::network { -enum class ServiceNodeChangeType { - none = 0, - invalid_path = 1, - replace_swarm = 2, - update_path = 3, - update_node = 4, +enum class ConnectionStatus { + unknown = 0, + connecting = 1, + connected = 2, + disconnected = 3, }; -struct onion_path { +struct connection_info { + session::network::service_node node; std::shared_ptr conn; + std::shared_ptr stream; + + bool is_valid() const { return conn && stream && !stream->is_closing(); }; +}; + +struct onion_path { + connection_info conn_info; std::vector nodes; uint8_t failure_count; -}; -struct service_node_changes { - ServiceNodeChangeType type = ServiceNodeChangeType::none; - std::vector nodes = {}; - uint8_t path_failure_count = 0; - bool path_invalid = false; + bool operator==(const onion_path& other) const { + return nodes == other.nodes && failure_count == other.failure_count; + } }; struct request_info { - const service_node target; - const std::string endpoint; - const std::optional body; - const std::optional> swarm; - const std::optional path; - const bool is_retry; + service_node target; + std::string endpoint; + std::optional body; + std::optional swarm_pubkey; + onion_path path; + bool is_retry; }; using network_response_callback_t = std::function response, - service_node_changes changes)>; + bool success, bool timeout, int16_t status_code, std::optional response)>; class Network { private: + const bool use_testnet; + const bool should_cache_to_disk; + const std::string cache_path; + + // Disk thread state + std::mutex snode_cache_mutex; // This guards all the below: + std::condition_variable snode_cache_cv; + bool shut_down_disk_thread = false; + bool need_write = false; + bool need_pool_write = false; + bool need_swarm_write = false; + bool need_clear_cache = false; + + // Values persisted to disk + std::vector snode_pool; + std::chrono::system_clock::time_point last_snode_pool_update; + std::unordered_map> swarm_cache; + + ConnectionStatus status; oxen::quic::Network net; - std::shared_ptr creds; std::vector paths; + std::shared_ptr get_snode_pool_loop; + std::shared_ptr build_paths_loop; std::shared_ptr endpoint; - std::shared_ptr buffer; + spdlog::pattern_formatter formatter; public: - // Constructs a new network for the given credentials, all requests should be made via a single - // Network instance. - Network(const session::onionreq::ed25519_seckey ed25519_seckey); + std::function status_changed; + std::function> paths)> paths_changed; + + // Constructs a new network with the given cache path and a flag indicating whether it should + // use testnet or mainnet, all requests should be made via a single Network instance. + Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); + ~Network(); /// API: network/add_logger /// @@ -72,40 +94,37 @@ class Network { /// - `callback` -- [in] callback to be called when a new message should be logged. void add_logger(std::function callback); - /// API: network/replace_key - /// - /// Replaces the secret key used to make network connections. Note: This will result in existing - /// path connections being removed and new ones created with the updated key on the next use. + /// API: network/clear_cache /// - /// Inputs: - /// - `ed25519_seckey` -- [in] new ed25519 secret key to be used. - void replace_key(const session::onionreq::ed25519_seckey ed25519_seckey); + /// Clears the cached from memory and from disk (if a cache path was provided during + /// initialization). + void clear_cache(); - /// API: network/add_path + /// API: network/get_swarm /// - /// Adds a path to the list on the network object that is randomly selected from when making an - /// onion request. + /// Retrieves the swarm for the given pubkey. If there is already an entry in the cache for the + /// swarm then that will be returned, otherwise a network request will be made to retrieve the + /// swarm and save it to the cache. /// /// Inputs: - /// - `nodes` -- [in] nodes which make up the path to be added. - /// - `failure_count` -- [in] number of times the path has previously failed to complete a - /// request. - void add_path(std::vector nodes, uint8_t failure_count); + /// 'swarm_pubkey_hex' - [in] includes the prefix. + /// 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error + /// the callback will be called with an empty list). + void get_swarm( + std::string swarm_pubkey_hex, + std::function swarm)> callback); - /// API: network/remove_path + /// API: network/get_random_nodes /// - /// Removes a path from the list on the network object that is randomly selected from when - /// making an onion request. + /// Retrieves a number of random nodes from the snode pool. If the are no nodes in the pool a + /// new pool will be populated and the nodes will be retrieved from that. /// /// Inputs: - /// - `node` -- [in] first node in the path to be removed. - void remove_path(session::network::service_node node); - - /// API: network/remove_all_paths - /// - /// Removes all paths from the list on the network object that are randomly selected from when - /// making an onion request. - void remove_all_paths(); + /// 'count' - [in] the number of nodes to retrieve. + /// 'callback' - [in] callback to be called with the retrieved nodes (in the case of an error + /// the callback will be called with an empty list). + void get_random_nodes( + uint16_t count, std::function nodes)> callback); /// API: network/send_request /// @@ -113,77 +132,118 @@ class Network { /// /// Inputs: /// - `info` -- [in] wrapper around all of the information required to send a request. - /// - `handle_response` -- [in] callback to be called with the result of the request. - void send_request(const request_info info, network_response_callback_t handle_response); - - /// API: network/send_request - /// - /// Sends a request directly to the provided service node. - /// - /// Inputs: - /// - `target` -- [in] the address information for the service node to send the request to. - /// - `endpoint` -- [in] endpoint for the request. - /// - `body` -- [in] data to send to the specified endpoint. - /// - `swarm` -- [in] current swarm information for the destination service node. Set to NULL if - /// not used. + /// - `conn` -- [in] connection information used to send the request. /// - `handle_response` -- [in] callback to be called with the result of the request. void send_request( - const session::network::service_node target, - const std::string endpoint, - const std::optional body, - const std::optional> swarm, - network_response_callback_t handle_response); + request_info info, connection_info conn, network_response_callback_t handle_response); /// API: network/send_onion_request /// /// Sends a request via onion routing to the provided service node or server destination. /// /// Inputs: - /// - `path` -- [in] the path of service nodes that the request should be routed through. /// - `destination` -- [in] service node or server destination information. /// - `body` -- [in] data to send to the specified destination. /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used /// for internal purposes for cases which should retry automatically (like receiving a `421`) in /// order to prevent subsequent retries. /// - `handle_response` -- [in] callback to be called with the result of the request. - template void send_onion_request( - const Destination destination, - const std::optional body, - const bool is_retry, + onionreq::network_destination destination, + std::optional body, + bool is_retry, network_response_callback_t handle_response); + /// API: network/validate_response + /// + /// Processes a quic response to extract the status code and body or throw if it errored or + /// received a non-successful status code. + /// + /// Inputs: + /// - `resp` -- [in] the quic response. + /// - `is_bencoded` -- [in] flag indicating whether the response will be bencoded or JSON. + /// + /// Returns: + /// - `std::pair` -- the status code and response body (for a bencoded + /// response this is just the direct response body from quic as it simplifies consuming the + /// response elsewhere). + std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + /// API: network/handle_errors /// /// Processes a non-success response to automatically perform any standard operations based on - /// the errors returned from the service node network. + /// the errors returned from the service node network (ie. updating the service node cache, + /// dropping nodes and/or onion request paths). /// /// Inputs: - /// - `status_code` -- [in] the status code returned from the network. - /// - `response` -- [in, optional] response data returned from the network. /// - `info` -- [in] the information for the request that was made. - /// - `handle_response` -- [in] callback to be called with updated response information after - /// processing the error. + /// - `status_code` -- [in, optional] the status code returned from the network. + /// - `response` -- [in, optional] response data returned from the network. + /// - `handle_response` -- [in, optional] callback to be called with updated response + /// information after processing the error. void handle_errors( - const int16_t status_code, - const std::optional response, - const request_info info, - network_response_callback_t handle_response); + request_info info, + std::optional status_code, + std::optional response, + std::optional handle_response); private: - std::shared_ptr get_connection(const service_node target); - - /// API: network/get_btstream + /// API: network/update_status /// - /// Retrieves the `BTRequestStream` for the given target if there is an existing stream, - /// otherwise creates a new stream. + /// Internal function to update the connection status and trigger the `status_changed` hook if + /// provided, this method ignores invalid or unchanged status changes. /// /// Inputs: - /// - `target` -- [in] the service node we plan to send a request to. + /// 'updated_status' - [in] the updated connection status. + void update_status(ConnectionStatus updated_status); + + /// API: network/start_disk_write_thread + /// + /// Starts the disk write thread which monitors a number of private variables and persists the + /// snode pool and swarm caches to disk if a `cache_path` was provided during initialization. + void start_disk_write_thread(); + + /// API: network/load_cache_from_disk /// - /// Outputs: - /// - a shared pointer to the `BTRequestStream` for the target service node. - std::shared_ptr get_btstream(const service_node target); + /// Loads the snode pool and swarm caches from disk if a `cache_path` was provided and cached + /// data exists. + void load_cache_from_disk(); + + connection_info get_connection_info( + service_node target, + std::optional conn_established_cb); + + void with_snode_pool(std::function)> callback); + void with_path( + std::optional excluded_node, + std::function path)> callback); + void build_paths_if_needed( + std::optional excluded_node, + std::function updated_paths)> callback); + + void get_service_nodes_recursive( + std::optional limit, + std::vector nodes, + std::function nodes, std::optional error)> + callback); + void find_valid_guard_node_recursive( + std::vector unused_nodes, + std::function< + void(std::optional valid_guard_node, + std::vector unused_nodes)> callback); + + void get_version( + service_node node, + std::optional timeout, + std::function< + void(std::vector version, + connection_info info, + std::optional error)> callback); + void get_service_nodes( + std::optional limit, + service_node node, + std::function nodes, std::optional error)> + callback); /// API: network/process_snode_response /// @@ -196,9 +256,9 @@ class Network { /// - `handle_response` -- [in] callback to be called with updated response information after /// processing the error. void process_snode_response( - const session::onionreq::Builder builder, - const std::string response, - const request_info info, + session::onionreq::Builder builder, + std::string response, + request_info info, network_response_callback_t handle_response); /// API: network/process_server_response @@ -212,9 +272,9 @@ class Network { /// - `handle_response` -- [in] callback to be called with updated response information after /// processing the error. void process_server_response( - const session::onionreq::Builder builder, - const std::string response, - const request_info info, + session::onionreq::Builder builder, + std::string response, + request_info info, network_response_callback_t handle_response); }; diff --git a/include/session/network_service_node.hpp b/include/session/network_service_node.hpp index 6e149105..0beddf00 100644 --- a/include/session/network_service_node.hpp +++ b/include/session/network_service_node.hpp @@ -1,5 +1,8 @@ #pragma once +#include + +#include #include #include @@ -29,6 +32,13 @@ struct service_node { failure_count{failure_count}, invalid{invalid} {} + service_node(nlohmann::json json); + service_node(std::string_view serialised); + service_node(oxenc::bt_dict_consumer bencoded); + + std::string serialise() const; + std::string pretty_description() const; + bool operator==(const service_node& other) const { return ip == other.ip && quic_port == other.quic_port && x25519_pubkey == other.x25519_pubkey && ed25519_pubkey == other.ed25519_pubkey && @@ -36,4 +46,6 @@ struct service_node { } }; +std::array split_ipv4(std::string_view ip); + } // namespace session::network diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 8718bf59..316d0707 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -22,18 +22,6 @@ typedef struct onion_request_builder_object { ENCRYPT_TYPE enc_type; } onion_request_builder_object; -typedef struct onion_request_service_node_destination { - uint8_t ip[4]; - uint16_t quic_port; - char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. - char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. - - uint8_t failure_count; - bool invalid; - const network_service_node* swarm; - const size_t swarm_count; -} onion_request_service_node_destination; - /// API: groups/onion_request_builder_init /// /// Constructs an onion request builder and sets a pointer to it in `builder`. diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 24c603be..6de08100 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -10,7 +10,7 @@ namespace session::onionreq { struct SnodeDestination { session::network::service_node node; - std::optional> swarm; + std::optional swarm_pubkey; }; struct ServerDestination { @@ -18,31 +18,29 @@ struct ServerDestination { std::string host; std::string endpoint; session::onionreq::x25519_pubkey x25519_pubkey; - std::string method; std::optional port; std::optional>> headers; - std::optional>> query_params; + std::string method; ServerDestination( std::string protocol, std::string host, std::string endpoint, session::onionreq::x25519_pubkey x25519_pubkey, - std::string method = "GET", - std::optional port = std::nullopt, - std::optional>> headers = std::nullopt, - std::optional>> query_params = - std::nullopt) : + std::optional port, + std::optional>> headers, + std::string method = "GET") : protocol{std::move(protocol)}, host{std::move(host)}, endpoint{std::move(endpoint)}, x25519_pubkey{std::move(x25519_pubkey)}, - method{std::move(method)}, port{std::move(port)}, headers{std::move(headers)}, - query_params{std::move(query_params)} {} + method{std::move(method)} {} }; +using network_destination = std::variant; + enum class EncryptType { aes_gcm, xchacha20, @@ -71,9 +69,9 @@ class Builder { void set_enc_type(EncryptType enc_type_) { enc_type = enc_type_; } - template - void set_destination(Destination destination); - + void set_destination(network_destination destination); + std::optional node_for_destination( + network_destination destination); void add_hop(std::pair keys) { hops_.push_back(keys); } ustring generate_payload(std::optional body) const; @@ -89,7 +87,6 @@ class Builder { // Proxied request values std::optional host_ = std::nullopt; - std::optional target_ = std::nullopt; std::optional endpoint_ = std::nullopt; std::optional protocol_ = std::nullopt; std::optional method_ = std::nullopt; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b3d2aaa..288a9078 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -91,6 +91,7 @@ add_libsession_util_library(onionreq onionreq/parser.cpp onionreq/response_parser.cpp network.cpp + network_service_node.cpp ) target_link_libraries(onionreq diff --git a/src/network.cpp b/src/network.cpp index 595f21ac..f6cf46a2 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -3,17 +3,21 @@ #include #include #include +#include +#include +#include #include #include #include -#include #include #include -#include +#include #include #include +#include +#include "session/ed25519.hpp" #include "session/export.h" #include "session/network.h" #include "session/network_service_node.h" @@ -25,8 +29,8 @@ #include "session/util.hpp" using namespace session; -using namespace oxen::quic; using namespace session::onionreq; +using namespace session::network; using namespace std::literals; using namespace oxen::log::literals; @@ -36,9 +40,25 @@ namespace { inline auto log_cat = oxen::log::Cat("network"); - class Timeout : public std::exception {}; + class status_code_exception : public std::runtime_error { + public: + int16_t status_code; + + status_code_exception(int16_t status_code, std::string message) : + std::runtime_error(message), status_code{status_code} {} + }; + + // The amount of time the snode cache can be used before it needs to be refreshed + const std::chrono::seconds snode_cache_expiration_duration = 2h; + + // The smallest size the snode pool can get to before we need to fetch more. + const uint16_t min_snode_pool_count = 12; - const std::chrono::seconds default_timeout = 5s; + // The number of paths we want to maintain. + const uint8_t target_path_count = 2; + + // The number of snodes (including the guard snode) in a path. + const uint8_t path_size = 3; // The number of times a path can fail before it's replaced. const uint16_t path_failure_threshold = 3; @@ -46,249 +66,1105 @@ namespace { // The number of times a snode can fail before it's replaced. const uint16_t snode_failure_threshold = 3; + // File names + const auto file_testnet = "/testnet"s; + const auto file_snode_pool = "/snode_pool"s; + const auto file_snode_pool_updated = "/snode_pool_updated"s; + const auto swarm_dir = "/swarm"s; + constexpr auto node_not_found_prefix = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; - std::array split_ipv4(std::string_view ip) { - std::array quad; - auto nums = split(ip, "."); - if (nums.size() != 4) - throw "Invalid IPv4 address"; - for (int i = 0; i < 4; i++) { - auto end = nums[i].data() + nums[i].size(); - if (auto [p, ec] = std::from_chars(nums[i].data(), end, quad[i]); - ec != std::errc{} || p != end) - throw "Invalid malformed IPv4 address"; - } + const std::vector seed_nodes_testnet{ + service_node{"144.76.164.202|35400|" + "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45|" + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"sv}}; + const std::vector seed_nodes_mainnet{ + service_node{"144.76.164.202|20200|" + "be83fe1221fdd85e4d9d2b62e2a34ba82eaf73da45700185d25aff4575ec6018|" + "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef"sv}, + service_node{"88.99.102.229|20201|" + "05c8c236cf6c4013b8ca930a343fdc62c413ba038a16bb12e75632e0179d404a|" + "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce"sv}, + service_node{"195.16.73.17|20202|" + "22ced8efd4e5faf15531e9b9244b2c1de299342892b97d19268c4db69ab6350f|" + "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f"sv}, + service_node{"104.194.11.120|20203|" + "330ad0d67b58f39a6f46fbeaf5c3622860dfa584e9d787f70c3702031712767a|" + "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b"sv}, + service_node{"104.194.8.115|20204|" + "929c5fc60efa1834a2d4a77a4a33387c1c3d5afc2b192c2ba0e040b29388b216|" + "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0"sv}}; + + /// rng type that uses llarp::randint(), which is cryptographically secure + struct CSRNG { + using result_type = uint64_t; + + static constexpr uint64_t min() { return std::numeric_limits::min(); }; + + static constexpr uint64_t max() { return std::numeric_limits::max(); }; + + uint64_t operator()() { + uint64_t i; + randombytes((uint8_t*)&i, sizeof(i)); + return i; + }; + }; + + /// Converts a string such as "1.2.3" to a vector of ints {1,2,3}. Throws if something + /// in/around + /// the .'s isn't parseable as an integer. + std::vector parse_version(std::string_view vers, bool trim_trailing_zero = true) { + auto v_s = session::split(vers, "."); + std::vector result; + for (const auto& piece : v_s) + if (!oxen::quic::parse_int(piece, result.emplace_back())) + throw std::invalid_argument{"Invalid version"}; + + // Remove any trailing `0` values (but ensure we at least end up with a "0" version) + if (trim_trailing_zero) + while (result.size() > 1 && result.back() == 0) + result.pop_back(); + + return result; + } - return quad; + std::optional node_for_destination( + network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return dest->node; + + return std::nullopt; + } + + std::optional swarm_pubkey_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return dest->swarm_pubkey; + + return std::nullopt; } } // namespace -Network::Network(const session::onionreq::ed25519_seckey ed25519_seckey) { - creds = GNUTLSCreds::make_from_ed_seckey(std::string(ed25519_seckey.view())); - endpoint = net.endpoint(Address{"0.0.0.0", 0}, opt::outbound_alpns{{uALPN}}); +// MARK: Initialization + +Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : + use_testnet{use_testnet}, + should_cache_to_disk{cache_path}, + cache_path{cache_path.value_or("")} { + get_snode_pool_loop = std::make_shared(); + build_paths_loop = std::make_shared(); + endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); + + // Load the cache from disk and start the disk write thread + if (should_cache_to_disk) { + load_cache_from_disk(); + std::thread disk_write_thread(&Network::start_disk_write_thread, this); + disk_write_thread.detach(); + } + + // Kick off a separate thread to build paths (may as well kick this off early) + if (pre_build_paths) { + std::thread build_paths_thread( + &Network::build_paths_if_needed, + this, + std::nullopt, + [](std::optional>) {}); + build_paths_thread.detach(); + } } -void Network::add_logger(std::function callback) { - buffer = std::make_shared(100UL, callback); - oxen::log::add_sink(buffer); +Network::~Network() { + { + std::lock_guard lock{snode_cache_mutex}; + shut_down_disk_thread = true; + } + snode_cache_cv.notify_one(); } -void Network::replace_key(const session::onionreq::ed25519_seckey ed25519_seckey) { - creds = GNUTLSCreds::make_from_ed_seckey(std::string(ed25519_seckey.view())); +// MARK: Cache Management + +void Network::load_cache_from_disk() { + // If the cache is for the wrong network then delete everything + auto cache_is_for_testnet = std::filesystem::exists(cache_path + file_testnet); + if ((!use_testnet && cache_is_for_testnet) || (use_testnet && !cache_is_for_testnet)) + std::filesystem::remove_all(cache_path); + + // Create the cache directory if needed + std::filesystem::create_directories(cache_path); + std::filesystem::create_directories(cache_path + swarm_dir); + + // If we are using testnet then create a file to indicate that + if (use_testnet) + std::ofstream{cache_path + file_testnet}; + + // Load the last time the snode pool was updated + // + // Note: We aren't just reading the write time of the file because Apple consider + // accessing file timestamps a method that can be used to track the user (and we + // want to avoid being flagged as using such) + if (std::filesystem::exists(cache_path + file_snode_pool_updated)) { + std::ifstream file{cache_path + file_snode_pool_updated}; + std::time_t timestamp; + file >> timestamp; + last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); + } - // Since the key is getting replaced we need to remove any connections from the paths - net.call([this]() mutable { - for (auto& path : paths) - path.conn.reset(); - }); -} + // Load the snode pool + if (std::filesystem::exists(cache_path + file_snode_pool)) { + std::ifstream file{cache_path + file_snode_pool}; + std::vector loaded_pool; + std::string line; -void Network::add_path(std::vector nodes, uint8_t failure_count) { - if (nodes.empty()) - throw std::invalid_argument{"No nodes in the path"}; + while (std::getline(file, line)) { + try { + loaded_pool.push_back(std::string_view(line)); + } catch (...) { + oxen::log::warning(log_cat, "Skipping invalid entry in snode pool cache."); + } + } - auto existing_path = net.call_get([this, node = nodes.front()]() -> std::optional { - auto target_path = std::find_if(paths.begin(), paths.end(), [&node](const auto& path) { - return !path.nodes.empty() && node == path.nodes.front(); - }); + snode_pool = loaded_pool; + } - if (target_path == paths.end()) - return std::nullopt; + // Load the swarm cache + auto swarm_path = (cache_path + swarm_dir); + std::unordered_map> loaded_cache; - return *target_path; - }); + for (auto& entry : std::filesystem::directory_iterator(swarm_path)) { + std::ifstream file{entry.path()}; + std::vector nodes; + std::string line; - if (existing_path) - throw std::invalid_argument{"Cannot have multiple paths with the same starting node"}; + while (std::getline(file, line)) { + try { + nodes.push_back(std::string_view(line)); + } catch (...) { + oxen::log::warning(log_cat, "Skipping invalid entry in snode pool cache."); + } + } - auto c = get_connection(nodes.front()); - net.call([this, c, nodes, failure_count]() mutable { - paths.emplace_back(onion_path{std::move(c), std::move(nodes), failure_count}); - }); + loaded_cache[entry.path().filename()] = nodes; + } + + swarm_cache = loaded_cache; + + oxen::log::info( + log_cat, + "Loaded cache of {} snodes, {} swarms.", + snode_pool.size(), + swarm_cache.size()); +} +void Network::start_disk_write_thread() { + std::unique_lock lock{snode_cache_mutex}; + while (true) { + snode_cache_cv.wait(lock, [this] { return need_write || shut_down_disk_thread; }); + + if (need_write) { + // Make local copies so that we can release the lock and not + // worry about other threads wanting to change things: + auto snode_pool_write = snode_pool; + auto last_pool_update_write = last_snode_pool_update; + auto swarm_cache_write = swarm_cache; + + lock.unlock(); + { + // Create the cache directories if needed + std::filesystem::create_directories(cache_path); + std::filesystem::create_directories(cache_path + swarm_dir); + + // Save the snode pool to disk + if (need_pool_write) { + auto pool_path = cache_path + file_snode_pool; + std::filesystem::remove(pool_path + "_new"); + std::ofstream file{pool_path + "_new"}; + for (auto& snode : snode_pool_write) + file << snode.serialise() << '\n'; + + std::filesystem::remove(pool_path); + std::filesystem::rename(pool_path + "_new", pool_path); + + // Write the last update timestamp to disk + std::filesystem::remove(cache_path + file_snode_pool_updated); + std::ofstream timestamp_file{cache_path + file_snode_pool_updated}; + timestamp_file << std::chrono::system_clock::to_time_t(last_pool_update_write); + oxen::log::debug(log_cat, "Finished writing snode pool cache to disk."); + } + + // Write the swarm cache to disk + if (need_swarm_write) { + for (auto& [key, swarm] : swarm_cache_write) { + auto swarm_path = cache_path + swarm_dir + "/" + key; + std::filesystem::remove(swarm_path + "_new"); + std::ofstream swarm_file{swarm_path + "_new"}; + for (auto& snode : swarm) + swarm_file << snode.serialise() << '\n'; + + std::filesystem::remove(cache_path + swarm_dir + "/" + key); + std::filesystem::rename(swarm_path + "_new", swarm_path); + } + oxen::log::debug(log_cat, "Finished writing swarm cache to disk."); + } + + need_pool_write = false; + need_swarm_write = false; + need_write = false; + } + lock.lock(); + } + if (need_clear_cache) { + snode_pool = {}; + last_snode_pool_update = {}; + swarm_cache = {}; + + lock.unlock(); + { std::filesystem::remove_all(cache_path); } + lock.lock(); + need_clear_cache = false; + } + if (shut_down_disk_thread) + return; + } } -void Network::remove_path(session::network::service_node node) { - auto it = std::find_if(paths.begin(), paths.end(), [&node](const auto& path) { - return path.nodes[0] == node; +void Network::clear_cache() { + net.call([this]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + need_clear_cache = true; + } + snode_cache_cv.notify_one(); }); +} - if (it != paths.end()) - paths.erase(it); +// MARK: Logging + +void Network::add_logger(std::function callback) { + auto sink = std::make_shared( + [this, cb = std::move(callback)](const spdlog::details::log_msg& msg) { + spdlog::memory_buf_t buf; + formatter.format(msg, buf); + cb(to_string(buf)); + }); + oxen::log::add_sink(sink); } -void Network::remove_all_paths() { - paths.clear(); +// MARK: Connection + +void Network::update_status(ConnectionStatus updated_status) { + // Ignore updates which don't change the status + if (status == updated_status) + return; + + // If we are already 'connected' then ignore 'connecting' status changes (if we drop one path + // and build another in the background this can happen) + if (status == ConnectionStatus::connected && updated_status == ConnectionStatus::connecting) + return; + + // Store the updated status + status = updated_status; + + if (!status_changed) + return; + + status_changed(updated_status); } -std::shared_ptr Network::get_connection( - const service_node target) { +connection_info Network::get_connection_info( + service_node target, + std::optional conn_established_cb) { auto remote_ip = "{}"_format(fmt::join(target.ip, ".")); - auto remote = RemoteAddress{target.ed25519_pubkey.view(), remote_ip, target.quic_port}; + auto remote = + oxen::quic::RemoteAddress{target.ed25519_pubkey.view(), remote_ip, target.quic_port}; + auto connection_key_pair = ed25519::ed25519_key_pair(); + auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( + std::string(connection_key_pair.second.begin(), connection_key_pair.second.end())); - return endpoint->connect( + auto c = endpoint->connect( remote, creds, oxen::quic::opt::keep_alive{10s}, - [this, target](connection_interface& conn, uint64_t) { + conn_established_cb, + [this, target](oxen::quic::connection_interface& conn, uint64_t) { + // When the connection is closed, update the path and connection status auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); - if (target_path != paths.end() && target_path->conn && - conn.reference_id() == target_path->conn->reference_id()) - target_path->conn.reset(); + if (target_path != paths.end() && target_path->conn_info.conn && + conn.reference_id() == target_path->conn_info.conn->reference_id()) { + target_path->conn_info.conn.reset(); + target_path->conn_info.stream.reset(); + handle_errors( + {target, "", std::nullopt, std::nullopt, *target_path, false}, + std::nullopt, + std::nullopt, + std::nullopt); + } }); -} -std::shared_ptr Network::get_btstream(const service_node target) { - auto has_target_path = net.call_get([this, &target]() -> bool { - auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { - return !path.nodes.empty() && target == path.nodes.front(); - }); + return {target, c, c->open_stream()}; +} - return target_path != paths.end(); - }); +// MARK: Snode Pool and Onion Path - // If we are targeting one of the paths then wait for `default_timeout` to give it a chance to - // create an active connection - std::chrono::milliseconds wait_time_ms = 0ms; - while (has_target_path && wait_time_ms < default_timeout) { - auto result = net.call_get( - [this, &target]() -> std::pair, bool> { - auto target_path = - std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { - return !path.nodes.empty() && target == path.nodes.front(); - }); +void Network::with_snode_pool(std::function)> callback) { + get_snode_pool_loop->call([this, cb = std::move(callback)]() mutable { + auto current_pool_info = net.call_get( + [this]() -> std::pair< + std::vector, + std::chrono::system_clock::time_point> { + return {snode_pool, last_snode_pool_update}; + }); - if (target_path != paths.end() && target_path->conn && - target_path->conn->remote_key() != to_usv(target.ed25519_pubkey.view())) { - if (auto str = - target_path->conn->maybe_stream(0)) - return {str, true}; + // Check if the cache is too old or if the updated timestamp is invalid + auto cache_duration = std::chrono::duration_cast( + std::chrono::system_clock::now() - current_pool_info.second); + auto cache_has_expired = + (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); + + // If the cache has enough snodes and it hasn't expired then return it + if (current_pool_info.first.size() >= min_snode_pool_count && !cache_has_expired) + return cb(current_pool_info.first); + + // Update the network status + net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + + // Define the response handler to avoid code duplication + auto handle_nodes_response = [](std::promise>& prom) { + return [&prom](std::vector nodes, std::optional error) { + try { + if (nodes.empty()) + throw std::runtime_error{error.value_or("No nodes received.")}; + prom.set_value(nodes); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }; + }; - return {nullptr, true}; + try { + CSRNG rng; + std::vector target_pool; + + // If we don't have enough nodes in the current cached pool then we need to fetch from + // the seed nodes + if (current_pool_info.first.size() < min_snode_pool_count) { + oxen::log::info(log_cat, "Fetching from seed nodes."); + target_pool = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); + + // Just in case, make sure the seed nodes are have values + if (target_pool.empty()) + throw std::runtime_error{"Insufficient seed nodes."}; + + std::shuffle(target_pool.begin(), target_pool.end(), rng); + std::promise> prom; + + get_service_nodes(256, target_pool.front(), handle_nodes_response(prom)); + + // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool + // so we don't double up on requests + auto nodes = prom.get_future().get(); + + // Update the cache + net.call([this, nodes]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + snode_pool = nodes; + last_snode_pool_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; } + snode_cache_cv.notify_one(); + }); + + oxen::log::info(log_cat, "Updated snode pool from seed node."); + return cb(nodes); + } + + // Pick ~9 random snodes from the current cache to fetch nodes from (we want to + // fetch from 3 snodes and retry up to 3 times if needed) + target_pool = current_pool_info.first; + std::shuffle(target_pool.begin(), target_pool.end(), rng); + size_t num_retries = std::min(target_pool.size() / 3, static_cast(3)); + + oxen::log::info(log_cat, "Fetching from random expired cache nodes."); + std::vector first_nodes( + target_pool.begin(), target_pool.begin() + num_retries); + std::vector second_nodes( + target_pool.begin() + num_retries, target_pool.begin() + (num_retries * 2)); + std::vector third_nodes( + target_pool.begin() + (num_retries * 2), + target_pool.begin() + (num_retries * 3)); + std::promise> prom1; + std::promise> prom2; + std::promise> prom3; + + // Kick off 3 concurrent requests + get_service_nodes_recursive(std::nullopt, first_nodes, handle_nodes_response(prom1)); + get_service_nodes_recursive(std::nullopt, second_nodes, handle_nodes_response(prom2)); + get_service_nodes_recursive(std::nullopt, third_nodes, handle_nodes_response(prom3)); + + // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool + // so we don't double up on requests + auto first_result_nodes = prom1.get_future().get(); + auto second_result_nodes = prom2.get_future().get(); + auto third_result_nodes = prom3.get_future().get(); + + auto compare_nodes = [](const auto& a, const auto& b) { + if (a.ip == b.ip) { + return a.quic_port < b.quic_port; + } + return a.ip < b.ip; + }; + + // Sort the vectors (so make it easier to find the + // intersection) + std::stable_sort(first_result_nodes.begin(), first_result_nodes.end(), compare_nodes); + std::stable_sort(second_result_nodes.begin(), second_result_nodes.end(), compare_nodes); + std::stable_sort(third_result_nodes.begin(), third_result_nodes.end(), compare_nodes); + + // Get the intersection of the vectors + std::vector first_second_intersection; + std::vector intersection; + + std::set_intersection( + first_result_nodes.begin(), + first_result_nodes.end(), + second_result_nodes.begin(), + second_result_nodes.end(), + std::back_inserter(first_second_intersection), + [](const auto& a, const auto& b) { return a == b; }); + std::set_intersection( + first_second_intersection.begin(), + first_second_intersection.end(), + third_result_nodes.begin(), + third_result_nodes.end(), + std::back_inserter(intersection), + [](const auto& a, const auto& b) { return a == b; }); + + // Since we sorted it we now need to shuffle it again + std::shuffle(intersection.begin(), intersection.end(), rng); + + // Update the cache to be the first 256 nodes from + // the intersection + auto size = std::min(256, static_cast(intersection.size())); + std::vector updated_pool( + intersection.begin(), intersection.begin() + size); + net.call([this, updated_pool]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + snode_pool = updated_pool; + last_snode_pool_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + }); + + oxen::log::info(log_cat, "Updated snode pool."); + cb(updated_pool); + } catch (const std::exception& e) { + oxen::log::info(log_cat, "Failed to get snode pool: {}", e.what()); + cb({}); + } + }); +} + +void Network::with_path( + std::optional excluded_node, + std::function path)> callback) { + auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + + // Retrieve a random path that doesn't contain the excluded node + auto select_valid_path = [](std::optional excluded_node, + std::vector paths) -> std::optional { + if (paths.empty()) + return std::nullopt; - return {nullptr, false}; + std::vector possible_paths; + std::copy_if( + paths.begin(), + paths.end(), + std::back_inserter(possible_paths), + [&excluded_node](const auto& path) { + return !path.nodes.empty() && + (!excluded_node || + std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == + path.nodes.end()); }); - // If we were able to get a valid connection then return it - if (result.first) - return result.first; + if (possible_paths.empty()) + return std::nullopt; - // Otherwise if there was no connection at all then return nullptr immediately so a new - // connection gets created (this can happen if the `conn` times out or fails initially so we - // don't want to bother looping in that case) - if (!result.second) - break; + CSRNG rng; + std::shuffle(possible_paths.begin(), possible_paths.end(), rng); + + return possible_paths.front(); + }; + + // If we found a path then return it (also build additional paths in the background if needed) + if (auto target_path = select_valid_path(excluded_node, current_paths)) { + // Build additional paths in the background if we don't have enough + if (current_paths.size() < target_path_count) { + std::thread build_paths_thread( + &Network::build_paths_if_needed, + this, + std::nullopt, + [](std::optional>) {}); + build_paths_thread.detach(); + } + + // If the stream had been closed then try to open a new one + if (!target_path->conn_info.is_valid()) + target_path->conn_info = get_connection_info( + target_path->nodes[0], [this](oxen::quic::connection_interface&) { + // If the connection is re-established update the network status back to + // connected + update_status(ConnectionStatus::connected); + }); - std::this_thread::sleep_for(100ms); - wait_time_ms += 100ms; + return callback(*target_path); } - // We weren't able to get an existing connection so we need to create a new one - auto c = get_connection(target); - net.call([this, c, &target]() mutable { - auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { - return !path.nodes.empty() && target == path.nodes.front(); - }); + // If we don't have any paths then build them + build_paths_if_needed( + std::nullopt, + [excluded_node, select_path = std::move(select_valid_path), cb = std::move(callback)]( + std::vector updated_paths) { + cb(select_path(excluded_node, updated_paths)); + }); +} - if (target_path == paths.end()) - return; +void Network::build_paths_if_needed( + std::optional excluded_node, + std::function updated_paths)> callback) { + with_snode_pool([this, excluded_node, cb = std::move(callback)]( + std::vector pool) { + if (pool.empty()) + return cb({}); + + build_paths_loop->call([this, excluded_node, pool, cb = std::move(cb)]() mutable { + auto current_paths = + net.call_get([this]() -> std::vector { return paths; }); + // No need to do anything if we already have enough paths + if (current_paths.size() >= target_path_count) + return cb(current_paths); + + // Update the network status + net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + + // Get the possible guard nodes + oxen::log::info(log_cat, "Building paths."); + std::vector nodes_to_exclude; + + if (excluded_node) + nodes_to_exclude.push_back(*excluded_node); + + for (auto& path : paths) + nodes_to_exclude.insert( + nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); + std::vector possible_guard_nodes; + + if (nodes_to_exclude.empty()) + possible_guard_nodes = pool; + else + std::copy_if( + pool.begin(), + pool.end(), + std::back_inserter(possible_guard_nodes), + [&nodes_to_exclude](const auto& node) { + return std::find( + nodes_to_exclude.begin(), + nodes_to_exclude.end(), + node) == nodes_to_exclude.end(); + }); - target_path->conn = std::move(c); - }); - std::shared_ptr str = - c->open_stream(); + if (possible_guard_nodes.empty()) { + oxen::log::info( + log_cat, "Unable to build paths due to lack of possible guard nodes."); + return cb({}); + } - return str; -} + // Now that we have a list of possible guard nodes we need to build the paths, first off + // we need to find valid guard nodes for the paths + CSRNG rng; + std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); -void Network::send_request(const request_info info, network_response_callback_t handle_response) { - try { - std::promise prom; - bstring_view payload = {}; + // Split the possible nodes list into a list of lists (one list could run out before the + // other but in most cases this should work fine) + size_t required_paths = (target_path_count - current_paths.size()); + size_t chunk_size = (possible_guard_nodes.size() / required_paths); + std::vector> nodes_to_test; + auto start = 0; - if (info.body) - payload = bstring_view{ - reinterpret_cast(info.body->data()), info.body->size()}; + for (size_t i = 0; i < required_paths; ++i) { + auto end = std::min(start + chunk_size, possible_guard_nodes.size()); - get_btstream(info.target)->command(info.endpoint, payload, [&prom](message resp) { + if (i == required_paths - 1) + end = possible_guard_nodes.size(); + + nodes_to_test.emplace_back( + possible_guard_nodes.begin() + start, possible_guard_nodes.begin() + end); + start = end; + } + + // Start testing guard nodes based on the number of paths we want to build + std::vector>>> + promises(required_paths); + + for (size_t i = 0; i < required_paths; ++i) { + find_valid_guard_node_recursive( + nodes_to_test[i], + [&prom = promises[i]]( + std::optional valid_guard_node, + std::vector unused_nodes) { + try { + if (!valid_guard_node) + std::runtime_error{"Failed to find valid guard node."}; + prom.set_value({*valid_guard_node, unused_nodes}); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }); + } + + // Combine the results (we want to block the `build_paths_loop` until we have retrieved + // the valid guard nodes so we don't double up on requests try { - if (resp.timed_out) - throw Timeout{}; + std::vector valid_nodes; + std::vector unused_nodes; + + for (auto& prom : promises) { + auto result = prom.get_future().get(); + valid_nodes.emplace_back(result.first); + unused_nodes.insert( + unused_nodes.begin(), result.second.begin(), result.second.end()); + } - std::string body = resp.body_str(); - if (resp.is_error() && !body.empty()) - throw std::runtime_error{"Failed to fetch response with error: " + body}; - else if (resp.is_error()) - throw std::runtime_error{"Failed to fetch response"}; + // Make sure we ended up getting enough valid nodes + auto have_enough_guard_nodes = + (current_paths.size() + valid_nodes.size() >= target_path_count); + auto have_enough_unused_nodes = + (unused_nodes.size() >= ((path_size - 1) * target_path_count)); - prom.set_value(body); - } catch (...) { - prom.set_exception(std::current_exception()); + if (!have_enough_guard_nodes || !have_enough_unused_nodes) + throw std::runtime_error{"Not enough remaining nodes."}; + + // Build the paths + auto updated_paths = current_paths; + + for (auto& info : valid_nodes) { + std::vector path{info.node}; + + for (auto i = 0; i < path_size - 1; i++) { + auto node = unused_nodes.back(); + unused_nodes.pop_back(); + path.push_back(node); + } + + updated_paths.emplace_back(onion_path{std::move(info), path, 0}); + + // Log that a path was built + std::vector node_descriptions; + std::transform( + path.begin(), + path.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.pretty_description(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + oxen::log::info( + log_cat, "Built new onion request path: [{}]", path_description); + } + + // Paths were successfully built, update the connection status + update_status(ConnectionStatus::connected); + + // Store the updated paths and update the connection status + std::vector> raw_paths; + for (auto& path : updated_paths) + raw_paths.emplace_back(path.nodes); + + net.call([this, updated_paths, raw_paths]() mutable { + paths = updated_paths; + + if (paths_changed) + paths_changed(raw_paths); + }); + + // Trigger the callback with the updated paths + cb(updated_paths); + } catch (const std::exception& e) { + oxen::log::info(log_cat, "Unable to build paths due to error: {}", e.what()); + cb({}); } }); + }); +} - // Default to a 200 success if the response is empty but didn't timeout or error - std::string response = prom.get_future().get(); - int16_t status_code = 200; - std::string response_data; +// MARK: Multi-request logic + +void Network::get_service_nodes_recursive( + std::optional limit, + std::vector target_nodes, + std::function nodes, std::optional error)> + callback) { + if (target_nodes.empty()) + return callback({}, "No nodes to fetch from provided."); + + auto target_node = target_nodes.front(); + get_service_nodes( + limit, + target_node, + [this, limit, target_nodes, cb = std::move(callback)]( + std::vector nodes, std::optional error) { + // If we got nodes then stop looping and return them + if (!nodes.empty()) + return cb(nodes, error); + + // Loop if we didn't get any nodes + std::vector remaining_nodes( + target_nodes.begin() + 1, target_nodes.end()); + get_service_nodes_recursive(limit, remaining_nodes, cb); + }); +} - try { - nlohmann::json response_json = nlohmann::json::parse(response); +void Network::find_valid_guard_node_recursive( + std::vector unused_nodes, + std::function< + void(std::optional valid_guard_node, + std::vector unused_nodes)> callback) { + if (unused_nodes.empty()) + return callback(std::nullopt, {}); + + auto target_node = unused_nodes.front(); + oxen::log::info(log_cat, "Testing guard snode: {}", target_node.pretty_description()); + + get_version( + target_node, + 3s, + [this, target_node, unused_nodes, cb = std::move(callback)]( + std::vector version, + connection_info info, + std::optional error) { + std::vector remaining_nodes( + unused_nodes.begin() + 1, unused_nodes.end()); + + // Log the error and loop after a slight delay (don't want to drain the pool + // too quickly if the network goes down) + if (error) { + oxen::log::info( + log_cat, + "Testing {} failed with error: {}", + target_node.pretty_description(), + *error); + std::thread retry_thread([this, remaining_nodes, cb = std::move(cb)] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + find_valid_guard_node_recursive(remaining_nodes, cb); + }); + retry_thread.detach(); + return; + } - if (response_json.is_array() && response_json.size() == 2) { - status_code = response_json[0].get(); - response_data = response_json[1].dump(); - } else - response_data = response; - } catch (...) { - response_data = response; - } + // Ensure the node meets the minimum version requirements after a slight + // delay (don't want to drain the pool if the network goes down) + std::vector min_version = parse_version("2.0.7"); + if (version < min_version) { + oxen::log::info( + log_cat, + "Testing {} failed with error: Outdated node version ({})", + target_node.pretty_description(), + fmt::join(version, ".")); + std::thread retry_thread([this, remaining_nodes, cb = std::move(cb)] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + find_valid_guard_node_recursive(remaining_nodes, cb); + }); + retry_thread.detach(); + return; + } - // If we have a status code that is not in the 2xx range, return the error - if (status_code < 200 || status_code > 299) - return handle_errors(status_code, response_data, info, handle_response); + oxen::log::info(log_cat, "Guard snode {} valid.", target_node.pretty_description()); + cb(info, remaining_nodes); + }); +} - handle_response(true, false, status_code, response_data, service_node_changes{}); - } catch (const Timeout&) { - handle_response(false, true, -1, "Request timed out", service_node_changes{}); - } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), service_node_changes{}); - } +// MARK: Pre-Defined Requests + +void Network::get_service_nodes( + std::optional limit, + service_node node, + std::function nodes, std::optional error)> + callback) { + auto info = get_connection_info(node, std::nullopt); + + if (!info.is_valid()) + return callback({}, "Network is unreachable."); + + nlohmann::json params{ + {"active_only", true}, + {"fields", + {{"public_ip", true}, + {"pubkey_ed25519", true}, + {"pubkey_x25519", true}, + {"storage_lmq_port", true}}}}; + + if (limit) + params["limit"] = *limit; + + oxenc::bt_dict_producer payload; + payload.append("endpoint", "get_service_nodes"); + payload.append("params", params.dump()); + + info.stream->command( + "oxend_request", + payload.view(), + [this, cb = std::move(callback)](oxen::quic::message resp) { + try { + auto [status_code, body] = validate_response(resp, true); + + oxenc::bt_list_consumer result_bencode{body}; + result_bencode.skip_value(); // Skip the status code (already validated) + auto response_dict = result_bencode.consume_dict_consumer(); + response_dict.skip_until("result"); + + auto result_dict = response_dict.consume_dict_consumer(); + result_dict.skip_until("service_node_states"); + + // Process the node list + std::vector result; + auto node = result_dict.consume_list_consumer(); + + while (!node.is_finished()) + result.emplace_back(node.consume_dict_consumer()); + + // Output the result + cb(result, std::nullopt); + } catch (const std::exception& e) { + cb({}, e.what()); + } + }); } -void Network::send_request( - const session::network::service_node target, - const std::string endpoint, - const std::optional body, - const std::optional> swarm, - network_response_callback_t handle_response) { - send_request({target, endpoint, body, swarm, std::nullopt, false}, handle_response); +void Network::get_version( + service_node node, + std::optional timeout, + std::function version, connection_info info, std::optional error)> + callback) { + auto info = get_connection_info(node, std::nullopt); + + if (!info.is_valid()) + return callback({}, info, "Network is unreachable."); + + oxenc::bt_dict_producer payload; + info.stream->command( + "info", + payload.view(), + timeout, + [this, info, cb = std::move(callback)](oxen::quic::message resp) { + try { + auto [status_code, body] = validate_response(resp, true); + + oxenc::bt_list_consumer result_bencode{body}; + result_bencode.skip_value(); // Skip the status code (already validated) + auto response_dict = result_bencode.consume_dict_consumer(); + response_dict.skip_until("result"); + + std::vector version; + response_dict.skip_until("version"); + auto version_list = response_dict.consume_list_consumer(); + + while (!version_list.is_finished()) + version.emplace_back(version_list.consume_integer()); + + cb(version, info, std::nullopt); + } catch (const std::exception& e) { + return cb({}, info, e.what()); + } + }); +} + +void Network::get_swarm( + std::string swarm_pubkey_hex, + std::function swarm)> callback) { + auto cached_swarm = + net.call_get([this, swarm_pubkey_hex]() -> std::optional> { + if (!swarm_cache.contains(swarm_pubkey_hex)) + return std::nullopt; + return swarm_cache[swarm_pubkey_hex]; + }); + + // If we have a cached swarm then return it + if (cached_swarm) + return callback(*cached_swarm); + + // Pick a random node from the snode pool to fetch the swarm from + with_snode_pool([this, swarm_pubkey_hex, cb = std::move(callback)]( + std::vector pool) { + if (pool.empty()) + return cb({}); + + auto updated_pool = pool; + CSRNG rng; + std::shuffle(updated_pool.begin(), updated_pool.end(), rng); + auto node = updated_pool.front(); + + nlohmann::json params{{"pubkey", swarm_pubkey_hex}}; + nlohmann::json payload{ + {"method", "get_swarm"}, + {"params", params}, + }; + + send_onion_request( + SnodeDestination{node, swarm_pubkey_hex}, + ustring{oxen::quic::to_usv(payload.dump())}, + false, + [this, swarm_pubkey_hex, cb = std::move(cb)]( + bool success, bool timeout, int16_t, std::optional response) { + if (!success || timeout || !response) + return cb({}); + + std::vector swarm; + + try { + nlohmann::json response_json = nlohmann::json::parse(*response); + + if (!response_json.contains("snodes") || + !response_json["snodes"].is_array()) + throw std::runtime_error{"JSON missing swarm field."}; + + for (auto& snode : response_json["snodes"]) + swarm.emplace_back(snode); + } catch (...) { + return cb({}); + } + + // Update the cache + net.call([this, swarm_pubkey_hex, swarm]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[swarm_pubkey_hex] = swarm; + need_swarm_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + }); + + cb(swarm); + }); + }); } -template -std::optional> swarm_for_destination( - const Destination) { - return std::nullopt; +void Network::get_random_nodes( + uint16_t count, std::function nodes)> callback) { + with_snode_pool([count, cb = std::move(callback)](std::vector pool) { + if (pool.size() < count) + return cb({}); + + auto random_pool = pool; + CSRNG rng; + std::shuffle(random_pool.begin(), random_pool.end(), rng); + + std::vector result(random_pool.begin(), random_pool.begin() + count); + cb(result); + }); +} + +// MARK: Request Handling + +void Network::send_request( + request_info info, connection_info conn_info, network_response_callback_t handle_response) { + if (!conn_info.is_valid()) + return handle_response(false, false, -1, "Network is unreachable."); + + oxen::quic::bstring_view payload{}; + + if (info.body) + payload = oxen::quic::bstring_view{ + reinterpret_cast(info.body->data()), info.body->size()}; + + conn_info.stream->command( + info.endpoint, + payload, + [this, info, cb = std::move(handle_response)](oxen::quic::message resp) { + try { + auto [status_code, body] = validate_response(resp, false); + cb(true, false, status_code, body); + } catch (const status_code_exception& e) { + handle_errors(info, e.status_code, e.what(), cb); + } catch (const std::exception& e) { + cb(false, resp.timed_out, -1, e.what()); + } + }); } -template <> -std::optional> swarm_for_destination( - const SnodeDestination destination) { - return destination.swarm; +void Network::send_onion_request( + network_destination destination, + std::optional body, + bool is_retry, + network_response_callback_t handle_response) { + with_path( + node_for_destination(destination), + [this, destination, body, is_retry, cb = std::move(handle_response)]( + std::optional path) { + if (!path) + return cb(false, false, -1, "No valid onion paths."); + + try { + // Construct the onion request + auto builder = Builder(); + builder.set_destination(destination); + + for (auto& node : path->nodes) + builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); + + auto payload = builder.generate_payload(body); + auto onion_req_payload = builder.build(payload); + + request_info info{ + path->nodes[0], + "onion_req", + onion_req_payload, + swarm_pubkey_for_destination(destination), + *path, + is_retry}; + + send_request( + info, + path->conn_info, + [this, + builder = std::move(builder), + info, + destination = std::move(destination), + cb = std::move(cb)]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + if (!success || timeout || + !ResponseParser::response_long_enough( + builder.enc_type, response->size())) + return handle_errors(info, status_code, response, cb); + + if (std::holds_alternative(destination)) + process_snode_response(builder, *response, info, cb); + else if (std::holds_alternative(destination)) + process_server_response(builder, *response, info, cb); + }); + } catch (const std::exception& e) { + cb(false, false, -1, e.what()); + } + }); } +// MARK: Response Handling + // The SnodeDestination runs via V3 onion requests void Network::process_snode_response( - const Builder builder, - const std::string response, - const request_info info, + Builder builder, + std::string response, + request_info info, network_response_callback_t handle_response) { try { std::string base64_iv_and_ciphertext; @@ -332,22 +1208,22 @@ void Network::process_snode_response( // If we got a non 2xx status code, return the error if (status_code < 200 || status_code > 299) - return handle_errors(status_code, body, info, handle_response); + return handle_errors(info, status_code, body, handle_response); - handle_response(true, false, status_code, body, service_node_changes{}); + handle_response(true, false, status_code, body); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), service_node_changes{}); + handle_response(false, false, -1, e.what()); } } // The ServerDestination runs via V4 onion requests void Network::process_server_response( - const Builder builder, - const std::string response, - const request_info info, + Builder builder, + std::string response, + request_info info, network_response_callback_t handle_response) { try { - ustring response_data = {to_unsigned(response.data()), response.size()}; + ustring response_data{to_unsigned(response.data()), response.size()}; auto parser = ResponseParser(builder); auto result = parser.decrypt(response_data); @@ -369,168 +1245,133 @@ void Network::process_server_response( // If we have a status code that is not in the 2xx range, return the error if (status_code < 200 || status_code > 299) { if (result_bencode.is_finished()) - return handle_errors(status_code, std::nullopt, info, handle_response); + return handle_errors(info, status_code, std::nullopt, handle_response); return handle_errors( - status_code, result_bencode.consume_string(), info, handle_response); + info, status_code, result_bencode.consume_string(), handle_response); } // If there is no body just return the success status if (result_bencode.is_finished()) - return handle_response(true, false, status_code, std::nullopt, service_node_changes{}); + return handle_response(true, false, status_code, std::nullopt); // Otherwise return the result - handle_response( - true, false, status_code, result_bencode.consume_string(), service_node_changes{}); + handle_response(true, false, status_code, result_bencode.consume_string()); } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), service_node_changes{}); + handle_response(false, false, -1, e.what()); } } -template -std::vector valid_paths_for_destination( - const std::vector paths, const Destination /*destination*/) { - return paths; -} +// MARK: Error Handling -template <> -std::vector valid_paths_for_destination( - const std::vector paths, const SnodeDestination destination) { - std::vector valid_paths = paths; - valid_paths.erase( - std::remove_if( - valid_paths.begin(), - valid_paths.end(), - [destination](const onion_path& path) { - return std::any_of( - path.nodes.begin(), - path.nodes.end(), - [&destination](const service_node& node) { - return node == destination.node; - }); - }), - valid_paths.end()); - return valid_paths; -} +std::pair Network::validate_response( + oxen::quic::message resp, bool is_bencoded) { + std::string body = resp.body_str(); -template -void Network::send_onion_request( - const Destination destination, - const std::optional body, - const bool is_retry, - network_response_callback_t handle_response) { - // Select a random path - auto paths = net.call_get([this]() -> std::vector { return this->paths; }); - auto valid_paths = valid_paths_for_destination(paths, destination); - onion_path path; + if (resp.timed_out) + throw std::runtime_error{"Timed out"}; + if (resp.is_error()) + throw std::runtime_error{body.empty() ? "Unknown error" : body}; - if (valid_paths.empty()) { - handle_response( - false, - false, - -1, - (paths.empty() ? "No onion paths" : "No valid onion paths"), - service_node_changes{ServiceNodeChangeType::invalid_path}); - return; - } + if (is_bencoded) { + // Process the bencoded response + oxenc::bt_list_consumer result_bencode{body}; + + if (result_bencode.is_finished() || !result_bencode.is_integer()) + throw std::runtime_error{"Invalid bencoded response"}; + + // If we have a status code that is not in the 2xx range, return the error + auto status_code = result_bencode.consume_integer(); - if (valid_paths.size() == 1) - path = valid_paths.front(); - else { - std::random_device rd; - std::uniform_int_distribution dist(0, valid_paths.size() - 1); - uint32_t random_index = dist(rd); - path = valid_paths[random_index]; + if (status_code < 200 || status_code > 299) { + if (result_bencode.is_finished() || !result_bencode.is_string()) + throw status_code_exception{ + status_code, + "Request failed with status code: " + std::to_string(status_code)}; + + throw status_code_exception{status_code, result_bencode.consume_string()}; + } + + // Can't convert the data to a string so just return the response body itself + return {status_code, body}; } + // Default to a 200 success if the response is empty but didn't timeout or error + int16_t status_code = 200; + std::string response_string; + try { - // Construct the onion request - auto builder = Builder(); - builder.set_destination(destination); - - for (const auto& node : path.nodes) - builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); - - auto payload = builder.generate_payload(body); - auto onion_req_payload = builder.build(payload); - - request_info info = { - path.nodes[0], - "onion_req", - onion_req_payload, - swarm_for_destination(destination), - path, - is_retry}; - - send_request( - info, - [this, - builder = std::move(builder), - info, - destination = std::move(destination), - callback = std::move(handle_response)]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes) { - if (!success || timeout || - !ResponseParser::response_long_enough(builder.enc_type, response->size())) - return handle_errors(status_code, response, info, callback); - - if constexpr (std::is_same_v) - process_snode_response(builder, *response, info, callback); - else if constexpr (std::is_same_v) - process_server_response(builder, *response, info, callback); - else - callback(false, false, -1, "Invalid destination.", service_node_changes{}); - }); - } catch (const std::exception& e) { - handle_response(false, false, -1, e.what(), service_node_changes{}); + nlohmann::json response_json = nlohmann::json::parse(body); + + if (response_json.is_array() && response_json.size() == 2) { + status_code = response_json[0].get(); + response_string = response_json[1].dump(); + } else + response_string = body; + } catch (...) { + response_string = body; } + + if (status_code < 200 || status_code > 299) + throw status_code_exception{status_code, response_string}; + + return {status_code, response_string}; } void Network::handle_errors( - const int16_t status_code, - const std::optional response, - const request_info info, - network_response_callback_t handle_response) { + request_info info, + std::optional status_code_, + std::optional response, + std::optional handle_response) { + auto status_code = status_code_.value_or(-1); + switch (status_code) { // A 404 or a 400 is likely due to a bad/missing SOGS or file so // shouldn't mark a path or snode as invalid case 400: case 404: - return handle_response(false, false, status_code, response, service_node_changes{}); + if (handle_response) + return (*handle_response)(false, false, status_code, response); + return; // The user's clock is out of sync with the service node network (a // snode will return 406, but V4 onion requests returns a 425) case 406: case 425: - return handle_response(false, false, status_code, response, service_node_changes{}); + if (handle_response) + return (*handle_response)(false, false, status_code, response); + return; // The snode is reporting that it isn't associated with the given public key anymore. If - // this is the first 421 then we want to try another node in the swarm (just in case it was - // reported incorrectly). If this is the second occurrence of the 421 then the client needs - // to update the swarm (if the response contains updated swarm data), or increment the path - // failure count. + // this is the first 421 then we want to try another node in the swarm (just in case it + // was reported incorrectly). If this is the second occurrence of the 421 then the + // client needs to update the swarm (if the response contains updated swarm data), or + // increment the path failure count. case 421: try { - // If there is no response data or no swarm informaiton was provided then we should - // just replace the swarm - if (!info.swarm) + // If there is no response handler or no swarm information was provided then we + // should just replace the swarm + if (!handle_response || !info.swarm_pubkey) + throw std::invalid_argument{"Unable to handle redirect."}; + + auto cached_swarm = net.call_get( + [this, swarm_pubkey = *info.swarm_pubkey]() -> std::vector { + return swarm_cache[swarm_pubkey]; + }); + + if (cached_swarm.empty()) throw std::invalid_argument{"Unable to handle redirect."}; - // If this was the first 421 then we want to retry using another node in the swarm - // to get confirmation that we should switch to a different swarm + // If this was the first 421 then we want to retry using another node in the + // swarm to get confirmation that we should switch to a different swarm if (!info.is_retry) { - std::random_device rd; - std::mt19937 g(rd()); - std::vector swarm_copy = *info.swarm; - std::shuffle(swarm_copy.begin(), swarm_copy.end(), g); + CSRNG rng; + std::vector swarm_copy = cached_swarm; + std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); std::optional random_node; - for (const auto& node : swarm_copy) { + for (auto& node : swarm_copy) { if (node == info.target) continue; @@ -541,21 +1382,11 @@ void Network::handle_errors( if (!random_node) throw std::invalid_argument{"No other nodes in the swarm."}; - if (info.path) - return send_onion_request( - SnodeDestination{*random_node, *info.swarm}, - info.body, - true, - handle_response); - - return send_request( - {*random_node, - info.endpoint, - info.body, - info.swarm, - std::nullopt, - true}, - handle_response); + return send_onion_request( + SnodeDestination{*random_node, info.swarm_pubkey}, + info.body, + true, + (*handle_response)); } if (!response) @@ -569,153 +1400,140 @@ void Network::handle_errors( std::vector swarm; - for (auto snode : snodes) { - swarm.emplace_back( - split_ipv4(snode["ip"].get()), - snode["port_omq"].get(), - x25519_pubkey::from_hex(snode["pubkey_x25519"].get()), - ed25519_pubkey::from_hex(snode["pubkey_ed25519"].get()), - 0, - false); - } + for (auto snode : snodes) + swarm.emplace_back(snode); if (swarm.empty()) throw std::invalid_argument{"No snodes in the response."}; - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ServiceNodeChangeType::replace_swarm, swarm}); - } catch (...) { - // If we don't have a path then this is a direct request so we can only update the - // failure count for the target node - if (!info.path) { - auto updated_node = info.target; - updated_node.failure_count += 1; - - if (updated_node.failure_count >= snode_failure_threshold) - updated_node.invalid = true; - - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_node, {updated_node}}); - } - - auto updated_path = *info.path; - updated_path.failure_count += 1; - - // If the path has failed too many times we want to drop the guard snode (marking it - // as invalid) and increment the failure count of each node in the path - if (updated_path.failure_count >= path_failure_threshold) { - updated_path.nodes[0].invalid = true; - - for (auto& it : updated_path.nodes) { - it.failure_count += 1; - - if (it.failure_count >= snode_failure_threshold) - it.invalid = true; + // Update the cache + net.call([this, swarm_pubkey = *info.swarm_pubkey, swarm]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[swarm_pubkey] = swarm; + need_swarm_write = true; + need_write = true; } - } + snode_cache_cv.notify_one(); + }); - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_path, - updated_path.nodes, - updated_path.failure_count, - (updated_path.failure_count >= path_failure_threshold)}); + return (*handle_response)(false, false, status_code, response); + } catch (...) { } - default: - // If we don't have a path then this is a direct request so we can only update the - // failure count for the target node - if (!info.path) { - auto updated_node = info.target; - updated_node.failure_count += 1; - - if (updated_node.failure_count >= snode_failure_threshold) - updated_node.invalid = true; + // If we weren't able to retry or redirect the swarm then handle this like any other + // error + break; - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ServiceNodeChangeType::update_node, {updated_node}}); - } + default: break; + } - auto updated_path = *info.path; - bool found_invalid_node = false; + // Check if we got an error specifying the specific node that failed + auto updated_path = info.path; + bool found_invalid_node = false; - if (response && response->starts_with(node_not_found_prefix)) { - std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; + if (response && response->starts_with(node_not_found_prefix)) { + std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; - if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { - session::onionreq::ed25519_pubkey edpk = - session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); + if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { + session::onionreq::ed25519_pubkey edpk = + session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); - auto snode_it = std::find_if( - updated_path.nodes.begin(), - updated_path.nodes.end(), - [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); + auto snode_it = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); - // Increment the failure count for the snode - if (snode_it != updated_path.nodes.end()) { - snode_it->failure_count += 1; - found_invalid_node = true; + // Increment the failure count for the snode + if (snode_it != updated_path.nodes.end()) { + snode_it->failure_count += 1; + found_invalid_node = true; - if (snode_it->failure_count >= snode_failure_threshold) - snode_it->invalid = true; - } - } + if (snode_it->failure_count >= snode_failure_threshold) + snode_it->invalid = true; } + } + } - // If we didn't find the specific node that was invalid then increment the path failure - // count - if (!found_invalid_node) { - // Increment the path failure count - updated_path.failure_count += 1; + // If we didn't find the specific node or the paths connection was closed then increment the + // path failure count + if (!found_invalid_node || !updated_path.conn_info.is_valid()) { + // Increment the path failure count + updated_path.failure_count += 1; - // If the path has failed too many times we want to drop the guard snode (marking it - // as invalid) and increment the failure count of each node in the path - if (updated_path.failure_count >= path_failure_threshold) { - updated_path.nodes[0].invalid = true; + // If the path has failed too many times we want to drop the guard snode + // (marking it as invalid) and increment the failure count of each node in the + // path + if (updated_path.failure_count >= path_failure_threshold) { + updated_path.nodes[0].invalid = true; - for (auto& it : updated_path.nodes) { - it.failure_count += 1; + for (auto& it : updated_path.nodes) { + it.failure_count += 1; - if (it.failure_count >= snode_failure_threshold) - it.invalid = true; - } - } + if (it.failure_count >= snode_failure_threshold) + it.invalid = true; } - - return handle_response( - false, - false, - status_code, - response, - service_node_changes{ - ServiceNodeChangeType::update_path, - updated_path.nodes, - updated_path.failure_count, - (updated_path.failure_count >= path_failure_threshold)}); + } } + + // Update the cache + net.call([this, + swarm_pubkey = info.swarm_pubkey, + old_path = info.path, + updated_path]() mutable { + // Drop the path if it's guard node is invalid + if (updated_path.nodes[0].invalid) { + oxen::log::info(log_cat, "Dropping path."); + paths.erase(std::remove(paths.begin(), paths.end(), old_path), paths.end()); + } else + std::replace(paths.begin(), paths.end(), old_path, updated_path); + + // Update the network status if we've removed all paths + if (paths.empty()) + update_status(ConnectionStatus::disconnected); + + { + std::lock_guard lock{snode_cache_mutex}; + + for (size_t i = 0; i < updated_path.nodes.size(); ++i) + if (updated_path.nodes[i].invalid) { + snode_pool.erase( + std::remove(snode_pool.begin(), snode_pool.end(), old_path.nodes[i]), + snode_pool.end()); + + if (swarm_pubkey) + if (swarm_cache.contains(*swarm_pubkey)) { + auto updated_swarm = swarm_cache[*swarm_pubkey]; + updated_swarm.erase( + std::remove( + updated_swarm.begin(), + updated_swarm.end(), + old_path.nodes[i]), + updated_swarm.end()); + swarm_cache[*swarm_pubkey] = updated_swarm; + } + } else + std::replace( + snode_pool.begin(), + snode_pool.end(), + old_path.nodes[i], + updated_path.nodes[i]); + + need_pool_write = true; + need_swarm_write = (swarm_pubkey && swarm_cache.contains(*swarm_pubkey)); + need_write = true; + } + snode_cache_cv.notify_one(); + }); + + if (handle_response) + (*handle_response)(false, false, status_code, response); } std::vector convert_service_nodes( - const std::vector nodes) { + std::vector nodes) { std::vector converted_nodes; - for (const auto& node : nodes) { + for (auto& node : nodes) { network_service_node converted_node; std::memcpy(converted_node.ip, node.ip.data(), sizeof(converted_node.ip)); strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); @@ -733,6 +1551,8 @@ std::vector convert_service_nodes( } // namespace session::network +// MARK: C API + namespace { inline session::network::Network& unbox(network_object* network_) { @@ -758,10 +1578,18 @@ extern "C" { using namespace session::network; LIBSESSION_C_API bool network_init( - network_object** network, const unsigned char* ed25519_secretkey_bytes, char* error) { + network_object** network, + const char* cache_path_, + bool use_testnet, + bool pre_build_paths, + char* error) { try { + std::optional cache_path; + if (cache_path_) + cache_path = cache_path_; + auto n = std::make_unique( - ed25519_seckey::from_bytes({ed25519_secretkey_bytes, 64})); + cache_path, use_testnet, pre_build_paths); auto n_object = std::make_unique(); n_object->internals = n.release(); @@ -780,157 +1608,92 @@ LIBSESSION_C_API void network_add_logger( network_object* network, void (*callback)(const char*, size_t)) { assert(callback); unbox(network).add_logger( - [callback](const std::string& msg) { callback(msg.c_str(), msg.size()); }); + [cb = std::move(callback)](const std::string& msg) { cb(msg.c_str(), msg.size()); }); } -LIBSESSION_C_API bool network_replace_key( - network_object* network, const unsigned char* ed25519_secretkey_bytes, char* error) { - try { - unbox(network).replace_key(ed25519_seckey::from_bytes({ed25519_secretkey_bytes, 64})); - return true; - } catch (const std::exception& e) { - return set_error(error, e); - } +LIBSESSION_C_API void network_clear_cache(network_object* network) { + unbox(network).clear_cache(); } -LIBSESSION_C_API bool network_add_path( - network_object* network, const onion_request_path path, char* error) { - try { - std::vector nodes; - for (size_t i = 0; i < path.nodes_count; i++) { - std::array ip; - std::memcpy(ip.data(), path.nodes[i].ip, ip.size()); - nodes.emplace_back( - ip, - path.nodes[i].quic_port, - x25519_pubkey::from_hex({path.nodes[i].x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({path.nodes[i].ed25519_pubkey_hex, 64}), - path.nodes[i].failure_count, - false); - } - - unbox(network).add_path(nodes, path.failure_count); - return true; - } catch (const std::exception& e) { - return set_error(error, e); - } +LIBSESSION_C_API void network_set_status_changed_callback( + network_object* network, void (*callback)(CONNECTION_STATUS status, void* ctx), void* ctx) { + if (!callback) + unbox(network).status_changed = nullptr; + else + unbox(network).status_changed = [cb = std::move(callback), ctx](ConnectionStatus status) { + cb(static_cast(status), ctx); + }; } -LIBSESSION_C_API bool network_remove_path( - network_object* network, const network_service_node node, char* error) { - try { - std::array ip; - std::memcpy(ip.data(), node.ip, ip.size()); - unbox(network).remove_path(session::network::service_node{ - ip, - node.quic_port, - x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), - node.failure_count, - false}); - return true; - } catch (const std::exception& e) { - return set_error(error, e); - } -} +LIBSESSION_C_API void network_set_paths_changed_callback( + network_object* network, + void (*callback)(onion_request_path* paths, size_t paths_len, void* ctx), + void* ctx) { + if (!callback) + unbox(network).paths_changed = nullptr; + else + unbox(network).paths_changed = [cb = std::move(callback), + ctx](std::vector> paths) { + std::vector c_paths; + c_paths.reserve(paths.size()); + + for (auto& nodes : paths) { + auto c_nodes = session::network::convert_service_nodes(nodes); + + // Allocate memory that persists outside the loop + network_service_node* c_nodes_array = new network_service_node[c_nodes.size()]; + std::copy(c_nodes.begin(), c_nodes.end(), c_nodes_array); + + onion_request_path c_path{c_nodes_array, c_nodes.size()}; + c_paths.emplace_back(c_path); + } -LIBSESSION_C_API void network_remove_all_paths(network_object* network) { - unbox(network).remove_all_paths(); + cb(c_paths.data(), c_paths.size(), ctx); + + // Free the allocated memory after cb is called + for (auto& c_path : c_paths) + delete[] c_path.nodes; + }; } -LIBSESSION_C_API void network_send_request( +LIBSESSION_C_API void network_get_swarm( network_object* network, - const network_service_node destination, - const char* endpoint, - const unsigned char* body_, - size_t body_size, - const network_service_node* swarm_, - const size_t swarm_count, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - network_service_node_changes changes, - void*), + const char* swarm_pubkey_hex, + void (*callback)(network_service_node*, size_t, void*), void* ctx) { - assert(endpoint && callback); - - std::optional body; - if (body_size > 0) - body = {body_, body_size}; - - std::optional> swarm; - - if (swarm_count > 0) { - swarm = std::vector{}; - swarm->reserve(swarm_count); - - for (size_t i = 0; i < swarm_count; i++) { - std::array ip; - std::memcpy(ip.data(), swarm_[i].ip, ip.size()); - swarm->emplace_back( - ip, - swarm_[i].quic_port, - x25519_pubkey::from_hex({swarm_[i].x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({swarm_[i].ed25519_pubkey_hex, 64}), - swarm_[i].failure_count, - false); - } - } + assert(swarm_pubkey_hex && callback); + unbox(network).get_swarm( + swarm_pubkey_hex, [cb = std::move(callback), ctx](std::vector nodes) { + auto c_nodes = session::network::convert_service_nodes(nodes); + cb(c_nodes.data(), c_nodes.size(), ctx); + }); +} - std::array ip; - std::memcpy(ip.data(), destination.ip, ip.size()); - - unbox(network).send_request( - session::network::service_node{ - ip, - destination.quic_port, - x25519_pubkey::from_hex({destination.x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({destination.ed25519_pubkey_hex, 64}), - destination.failure_count, - false}, - endpoint, - body, - swarm, - [callback, ctx]( - bool success, - bool timeout, - int status_code, - std::optional response, - service_node_changes changes) { - auto c_nodes = session::network::convert_service_nodes(changes.nodes); - auto c_changes = network_service_node_changes{ - static_cast(changes.type), - c_nodes.data(), - c_nodes.size(), - changes.path_failure_count, - changes.path_invalid}; - - callback( - success, - timeout, - status_code, - response->data(), - response->size(), - c_changes, - ctx); +LIBSESSION_C_API void network_get_random_nodes( + network_object* network, + uint16_t count, + void (*callback)(network_service_node*, size_t, void*), + void* ctx) { + assert(callback); + unbox(network).get_random_nodes( + count, [cb = std::move(callback), ctx](std::vector nodes) { + auto c_nodes = session::network::convert_service_nodes(nodes); + cb(c_nodes.data(), c_nodes.size(), ctx); }); } LIBSESSION_C_API void network_send_onion_request_to_snode_destination( network_object* network, - const onion_request_service_node_destination node, + const network_service_node node, const unsigned char* body_, size_t body_size, + const char* swarm_pubkey_hex, void (*callback)( bool success, bool timeout, int16_t status_code, const char* response, size_t response_size, - network_service_node_changes changes, void*), void* ctx) { assert(callback); @@ -940,24 +1703,9 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( if (body_size > 0) body = {body_, body_size}; - std::optional> swarm; - - if (node.swarm_count > 0) { - swarm = std::vector{}; - swarm->reserve(node.swarm_count); - - for (size_t i = 0; i < node.swarm_count; i++) { - std::array ip; - std::memcpy(ip.data(), node.swarm[i].ip, ip.size()); - swarm->emplace_back( - ip, - node.swarm[i].quic_port, - x25519_pubkey::from_hex({node.swarm[i].x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({node.swarm[i].ed25519_pubkey_hex, 64}), - node.swarm[i].failure_count, - false); - } - } + std::optional swarm_pubkey; + if (swarm_pubkey_hex) + swarm_pubkey = std::string(swarm_pubkey_hex); std::array ip; std::memcpy(ip.data(), node.ip, ip.size()); @@ -970,58 +1718,25 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), node.failure_count, false}, - swarm}, + swarm_pubkey}, body, false, [callback, ctx]( bool success, bool timeout, int status_code, - std::optional response, - service_node_changes changes) { - auto c_nodes = session::network::convert_service_nodes(changes.nodes); - auto c_changes = network_service_node_changes{ - static_cast(changes.type), - c_nodes.data(), - c_nodes.size(), - changes.path_failure_count, - changes.path_invalid}; - + std::optional response) { callback( - success, - timeout, - status_code, - response->data(), - response->size(), - c_changes, - ctx); + success, timeout, status_code, response->data(), response->size(), ctx); }); } catch (const std::exception& e) { - callback( - false, - false, - -1, - e.what(), - std::strlen(e.what()), - network_service_node_changes{}, - ctx); + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } } LIBSESSION_C_API void network_send_onion_request_to_server_destination( network_object* network, - const char* method, - const char* protocol, - const char* host, - const char* endpoint, - uint16_t port, - const char* x25519_pubkey, - const char** query_param_keys, - const char** query_param_values, - size_t query_params_size, - const char** headers_, - const char** header_values, - size_t headers_size, + const network_server_destination server, const unsigned char* body_, size_t body_size, void (*callback)( @@ -1030,26 +1745,17 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( int16_t status_code, const char* response, size_t response_size, - network_service_node_changes changes, void*), void* ctx) { - assert(method && protocol && host && endpoint && x25519_pubkey && callback); + assert(callback); try { std::optional>> headers; - if (headers_size > 0) { + if (server.headers_size > 0) { headers = std::vector>{}; - for (size_t i = 0; i < headers_size; i++) - headers->emplace_back(headers_[i], header_values[i]); - } - - std::optional>> query_params; - if (query_params_size > 0) { - query_params = std::vector>{}; - - for (size_t i = 0; i < query_params_size; i++) - query_params->emplace_back(query_param_keys[i], query_param_values[i]); + for (size_t i = 0; i < server.headers_size; i++) + headers->emplace_back(server.headers[i], server.header_values[i]); } std::optional body; @@ -1058,48 +1764,25 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( unbox(network).send_onion_request( ServerDestination{ - protocol, - host, - endpoint, - x25519_pubkey::from_hex({x25519_pubkey, 64}), - method, - port, + server.protocol, + server.host, + server.endpoint, + x25519_pubkey::from_hex({server.x25519_pubkey, 64}), + server.port, headers, - query_params}, + server.method}, body, false, [callback, ctx]( bool success, bool timeout, int status_code, - std::optional response, - service_node_changes changes) { - auto c_nodes = session::network::convert_service_nodes(changes.nodes); - auto c_changes = network_service_node_changes{ - static_cast(changes.type), - c_nodes.data(), - c_nodes.size(), - changes.path_failure_count, - changes.path_invalid}; - + std::optional response) { callback( - success, - timeout, - status_code, - response->data(), - response->size(), - c_changes, - ctx); + success, timeout, status_code, response->data(), response->size(), ctx); }); } catch (const std::exception& e) { - callback( - false, - false, - -1, - e.what(), - std::strlen(e.what()), - network_service_node_changes{}, - ctx); + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } } diff --git a/src/network_service_node.cpp b/src/network_service_node.cpp new file mode 100644 index 00000000..c268751a --- /dev/null +++ b/src/network_service_node.cpp @@ -0,0 +1,83 @@ +#include "session/network_service_node.hpp" + +#include + +#include +#include +#include + +#include "session/onionreq/key_types.hpp" + +using namespace session; +using namespace session::onionreq; + +namespace session::network { + +service_node::service_node(nlohmann::json json) { + ip = split_ipv4(json["ip"].get()); + quic_port = json["port_omq"].get(); + x25519_pubkey = x25519_pubkey::from_hex(json["pubkey_x25519"].get()); + ed25519_pubkey = ed25519_pubkey::from_hex(json["pubkey_ed25519"].get()); +} + +service_node::service_node(std::string_view serialised) { + auto parts = split(serialised, "|"); + if (parts.size() < 4) + throw std::invalid_argument{ + "Invalid service node serialisation: " + std::to_string(parts.size()) + ", " + + std::string(serialised)}; + + ip = split_ipv4(parts[0]); + quic_port = std::stoi(std::string{parts[1]}); + x25519_pubkey = x25519_pubkey::from_hex(parts[2]); + ed25519_pubkey = ed25519_pubkey::from_hex(parts[3]); + invalid = false; // If a node is invalid we would have removed it from the pool + + // If we have a failure count then parse it + if (parts.size() >= 5) + failure_count = std::stoi(std::string{parts[4]}); + else + failure_count = 0; +} + +service_node::service_node(oxenc::bt_dict_consumer bencoded) { + ed25519_pubkey = ed25519_pubkey::from_hex(bencoded.consume_string()); + x25519_pubkey = x25519_pubkey::from_hex(bencoded.consume_string()); + ip = split_ipv4(bencoded.consume_string()); + quic_port = bencoded.consume_integer(); +} + +std::string service_node::serialise() const { + return fmt::format( + "{}.{}.{}.{}|{}|{}|{}|{}|{}", + ip[0], + ip[1], + ip[2], + ip[3], + quic_port, + x25519_pubkey.hex(), + ed25519_pubkey.hex(), + failure_count, + invalid ? "1" : "0"); +} + +std::string service_node::pretty_description() const { + return fmt::format("{}.{}.{}.{}:{}", ip[0], ip[1], ip[2], ip[3], quic_port); +}; + +std::array split_ipv4(std::string_view ip) { + std::array quad; + auto nums = split(ip, "."); + if (nums.size() != 4) + throw "Invalid IPv4 address"; + for (int i = 0; i < 4; i++) { + auto end = nums[i].data() + nums[i].size(); + if (auto [p, ec] = std::from_chars(nums[i].data(), end, quad[i]); + ec != std::errc{} || p != end) + throw "Invalid malformed IPv4 address"; + } + + return quad; +} + +} // namespace session::network \ No newline at end of file diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 4dfccd44..8e15153f 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,9 @@ #include "session/util.hpp" #include "session/xed25519.hpp" +using namespace std::literals; +using session::ustring_view; + namespace session::onionreq { namespace { @@ -44,46 +48,34 @@ EncryptType parse_enc_type(std::string_view enc_type) { throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; } -template -void Builder::set_destination(Destination /*destination*/) { - throw std::runtime_error{"Invalid destination."}; -} - -template <> -void Builder::set_destination(SnodeDestination destination) { +void Builder::set_destination(network_destination destination) { destination_x25519_public_key.reset(); ed25519_public_key_.reset(); - destination_x25519_public_key.emplace(destination.node.x25519_pubkey); - ed25519_public_key_.emplace(destination.node.ed25519_pubkey); -} - -template <> -void Builder::set_destination(ServerDestination destination) { - destination_x25519_public_key.reset(); - - host_.emplace(destination.host); - target_.emplace("/oxen/v4/lsrpc"); // All servers support V4 onion requests - endpoint_.emplace(destination.endpoint); - protocol_.emplace(destination.protocol); - method_.emplace(destination.method); - if (destination.port) - port_.emplace(*destination.port); + if (auto* dest = std::get_if(&destination)) { + destination_x25519_public_key.emplace(dest->node.x25519_pubkey); + ed25519_public_key_.emplace(dest->node.ed25519_pubkey); + } else if (auto* dest = std::get_if(&destination)) { + host_.emplace(dest->host); + endpoint_.emplace(dest->endpoint); + protocol_.emplace(dest->protocol); + method_.emplace(dest->method); - if (destination.headers) - headers_.emplace(*destination.headers); + if (dest->port) + port_.emplace(*dest->port); - if (destination.query_params) - headers_.emplace(*destination.query_params); + if (dest->headers) + headers_.emplace(*dest->headers); - destination_x25519_public_key.emplace(destination.x25519_pubkey); + destination_x25519_public_key.emplace(dest->x25519_pubkey); + } else + throw std::invalid_argument{"Invalid destination type."}; } ustring Builder::generate_payload(std::optional body) const { // If we don't have the data required for a server request, then assume it's targeting a // service node and, therefore, the `body` is the payload - if (!host_ || !target_ || !endpoint_ || !protocol_ || !method_ || - !destination_x25519_public_key) + if (!host_ || !endpoint_ || !protocol_ || !method_ || !destination_x25519_public_key) return body.value_or(ustring{}); // Otherwise generate the payload for a server request @@ -99,32 +91,14 @@ ustring Builder::generate_payload(std::optional body) const { if (body && !headers_json.contains("Content-Type")) headers_json["Content-Type"] = "application/json"; - // Need to ensure the endpoint has a leading forward slash so add it if it's missing - auto endpoint = *endpoint_; - - if (!endpoint.empty() && endpoint.front() != '/') - endpoint = '/' + endpoint; - - // If we have query parameters, add them to the endpoint to be included in the payload - if (query_params_ && !query_params_->empty()) { - std::string query_string = ""; - - for (auto& query : *query_params_) - query_string += query.first + "=" + query.second + "&"; - - // Drop the extra '&' from the end - endpoint += "?" + query_string.substr(0, query_string.size() - 1); - } - // Structure the request information nlohmann::json request_info{ - {"method", *method_}, {"endpoint", endpoint}, {"headers", headers_json}}; - auto request_info_dump = request_info.dump(); - std::vector payload{request_info_dump}; + {"method", *method_}, {"endpoint", *endpoint_}, {"headers", headers_json}}; + std::vector payload{request_info.dump()}; // If we were given a body, add it to the payload if (body.has_value()) - payload.emplace_back(std::string(from_unsigned(body->data()), body->size())); + payload.emplace_back(from_unsigned_sv(*body)); auto result = oxenc::bt_serialize(payload); return {to_unsigned(result.data()), result.size()}; @@ -181,10 +155,10 @@ ustring Builder::build(ustring payload) { // The data we send to the destination differs depending on whether the destination is a // server or a service node - if (host_ && target_ && protocol_ && destination_x25519_public_key) { + if (host_ && protocol_ && destination_x25519_public_key) { final_route = { {"host", *host_}, - {"target", *target_}, + {"target", "/oxen/v4/lsrpc"}, // All servers support V4 onion requests {"method", "POST"}, {"protocol", *protocol_}, {"port", port_.value_or(*protocol_ == "https" ? 443 : 80)}, @@ -214,7 +188,7 @@ ustring Builder::build(ustring payload) { throw std::runtime_error{"Destination not set: No destination ed25519 public key"}; throw std::runtime_error{ "Destination not set: " + host_.value_or("N/A") + ", " + - target_.value_or("N/A") + ", " + protocol_.value_or("N/A")}; + protocol_.value_or("N/A")}; } // Save these because we need them again to decrypt the final response: @@ -331,10 +305,9 @@ LIBSESSION_C_API void onion_request_builder_set_server_destination( host, endpoint, session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), - method, port, std::nullopt, - std::nullopt}); + method}); } LIBSESSION_C_API void onion_request_builder_add_hop( diff --git a/tests/test_network.cpp b/tests/test_network.cpp index b689ee50..4e30b2ba 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -17,516 +17,6 @@ struct Result { bool timeout; int16_t status_code; std::optional response; - service_node_changes changes; }; } // namespace -TEST_CASE("Network error handling", "[network]") { - auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - auto ed_sk = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - auto x_pk = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; - auto x_pk2 = "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes; - auto target = service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk), - ed25519_pubkey::from_bytes(ed_pk), - 0, - false}; - auto mock_request = - request_info{target, "test", std::nullopt, std::nullopt, std::nullopt, false}; - Result result; - auto network = Network(ed25519_seckey::from_bytes(ed_sk)); - - // Check the handling of the codes which make no changes - auto codes_with_no_changes = {400, 404, 406, 425}; - - for (auto code : codes_with_no_changes) { - network.handle_errors( - code, - std::nullopt, - mock_request, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == code); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::none); - } - - // Check general error handling with no provided path (first failure) - network.handle_errors( - 500, - std::nullopt, - mock_request, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::update_node); - REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].ip == target.ip); - CHECK(result.changes.nodes[0].quic_port == target.quic_port); - CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); - CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); - CHECK(result.changes.nodes[0].failure_count == 1); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.path_failure_count == 0); - - // Check general error handling with no provided path (too many failures) - auto mock_request2 = request_info{ - service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk), - ed25519_pubkey::from_bytes(ed_pk), - 9, - false}, - "test", - std::nullopt, - std::nullopt, - std::nullopt, - false}; - network.handle_errors( - 500, - std::nullopt, - mock_request2, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::update_node); - REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].failure_count == 10); - CHECK(result.changes.nodes[0].invalid); - CHECK(result.changes.path_failure_count == 0); - - // Check general error handling with a path but no response (first failure) - auto path = onion_path{nullptr, {target}, 0}; - auto mock_request3 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - network.handle_errors( - 500, - std::nullopt, - mock_request3, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].failure_count == 0); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.path_failure_count == 1); - - // Check general error handling with a path but no response (too many path failures) - path = onion_path{ - nullptr, - {target, - service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk), - ed25519_pubkey::from_bytes(ed_pk), - 0, - false}}, - 9}; - auto mock_request4 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - network.handle_errors( - 500, - std::nullopt, - mock_request4, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 2); - CHECK(result.changes.nodes[0].failure_count == 1); - CHECK(result.changes.nodes[0].invalid); - CHECK(result.changes.nodes[1].failure_count == 1); - CHECK_FALSE(result.changes.nodes[1].invalid); - CHECK(result.changes.path_failure_count == 10); - - // Check general error handling with a path but no response (too many path & node failures) - path = onion_path{ - nullptr, - {target, - service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk), - ed25519_pubkey::from_bytes(ed_pk), - 9, - false}}, - 9}; - auto mock_request5 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - network.handle_errors( - 500, - std::nullopt, - mock_request5, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK_FALSE(result.response.has_value()); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 2); - CHECK(result.changes.nodes[0].failure_count == 1); - CHECK(result.changes.nodes[0].invalid); - CHECK(result.changes.nodes[1].failure_count == 10); - CHECK(result.changes.nodes[1].invalid); - CHECK(result.changes.path_failure_count == 10); - - // Check general error handling with a path and a random response (first failure) - path = onion_path{nullptr, {target}, 0}; - auto mock_request6 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - auto response = std::string{"Test"}; - network.handle_errors( - 500, - response, - mock_request6, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK(result.response == response); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].failure_count == 0); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.path_failure_count == 1); - - // Check general error handling with a path and specific node failure (first failure) - path = onion_path{ - nullptr, - {target, - service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk2), - ed25519_pubkey::from_bytes(ed_pk2), - 0, - false}}, - 0}; - auto mock_request7 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - network.handle_errors( - 500, - response, - mock_request7, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK(result.response == response); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 2); - CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); - CHECK(result.changes.nodes[0].failure_count == 0); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); - CHECK(result.changes.nodes[1].failure_count == 1); - CHECK_FALSE(result.changes.nodes[1].invalid); - CHECK(result.changes.path_failure_count == 0); - - // Check general error handling with a path and specific node failure (too many failures) - path = onion_path{ - nullptr, - {target, - service_node{ - {0, 0, 0, 0}, - 0, - x25519_pubkey::from_bytes(x_pk2), - ed25519_pubkey::from_bytes(ed_pk2), - 9, - false}}, - 0}; - auto mock_request8 = request_info{target, "test", std::nullopt, std::nullopt, path, false}; - response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - network.handle_errors( - 500, - response, - mock_request8, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK(result.response == response); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - REQUIRE(result.changes.nodes.size() == 2); - CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); - CHECK(result.changes.nodes[0].failure_count == 0); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.nodes[1].ed25519_pubkey == ed25519_pubkey::from_bytes(ed_pk2)); - CHECK(result.changes.nodes[1].failure_count == 10); - CHECK(result.changes.nodes[1].invalid); - CHECK(result.changes.path_failure_count == 0); - - // Check a 421 with no swarm data throws (no good way to handle this case) - network.handle_errors( - 421, - std::nullopt, - mock_request, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(result.changes.type == ServiceNodeChangeType::update_node); - - // Check the retry request of a 421 with no response data throws (no good way to handle this - // case) - auto mock_request9 = request_info{ - target, "test", std::nullopt, std::vector{target}, path, true}; - network.handle_errors( - 421, - std::nullopt, - mock_request9, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - - // Check the retry request of a 421 with non-swarm response data throws (no good way to handle - // this case) - network.handle_errors( - 421, - "Test", - mock_request9, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(result.changes.type == ServiceNodeChangeType::update_path); - - // Check the retry request of a 421 instructs to replace the swarm - auto snodes = nlohmann::json::array(); - snodes.push_back( - {{"ip", "1.1.1.1"}, - {"port_omq", 1}, - {"pubkey_x25519", x25519_pubkey::from_bytes(x_pk).hex()}, - {"pubkey_ed25519", x25519_pubkey::from_bytes(ed_pk).hex()}}); - nlohmann::json swarm_json{{"snodes", snodes}}; - response = swarm_json.dump(); - network.handle_errors( - 421, - response, - mock_request9, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result = {success, timeout, status_code, response, changes}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(result.changes.type == ServiceNodeChangeType::replace_swarm); - REQUIRE(result.changes.nodes.size() == 1); - CHECK(result.changes.nodes[0].ip == std::array{{1, 1, 1, 1}}); - CHECK(result.changes.nodes[0].quic_port == 1); - CHECK(result.changes.nodes[0].x25519_pubkey == target.x25519_pubkey); - CHECK(result.changes.nodes[0].ed25519_pubkey == target.ed25519_pubkey); - CHECK(result.changes.nodes[0].failure_count == 0); - CHECK_FALSE(result.changes.nodes[0].invalid); - CHECK(result.changes.path_failure_count == 0); -} - -TEST_CASE("Network direct request", "[send_request][network]") { - auto ed_sk = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - auto test_service_node = service_node{ - {144, 76, 164, 202}, - 35400, - x25519_pubkey::from_bytes( - "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"_hexbytes), - ed25519_pubkey::from_bytes( - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes), - 0, - false}; - auto network = Network(ed25519_seckey::from_bytes(ed_sk)); - std::promise result_promise; - - network.send_request( - test_service_node, - "info", - std::nullopt, - std::nullopt, - [&result_promise]( - bool success, - bool timeout, - int16_t status_code, - std::optional response, - service_node_changes changes) { - result_promise.set_value({success, timeout, status_code, response, changes}); - }); - - // Wait for the result to be set - auto result = result_promise.get_future().get(); - - CHECK(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 200); - CHECK(result.changes.type == ServiceNodeChangeType::none); - REQUIRE(result.response.has_value()); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); - - auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); - CHECK(response.contains("t")); - CHECK(response.contains("version")); -} - -TEST_CASE("Network direct request C API", "[network_send_request][network]") { - auto ed_sk = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - network_object* network; - network_init(&network, ed_sk.data(), nullptr); - std::array target_ip = {144, 76, 164, 202}; - auto test_service_node = network_service_node{}; - test_service_node.quic_port = 35400; - std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); - std::strcpy( - test_service_node.x25519_pubkey_hex, - "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45"); - std::strcpy( - test_service_node.ed25519_pubkey_hex, - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); - - network_send_request( - network, - test_service_node, - "info", - nullptr, - 0, - nullptr, - 0, - [](bool success, - bool timeout, - int16_t status_code, - const char* c_response, - size_t response_size, - network_service_node_changes changes, - void* ctx) { - CHECK(success); - CHECK_FALSE(timeout); - CHECK(status_code == 200); - CHECK(changes.type == SERVICE_NODE_CHANGE_TYPE_NONE); - REQUIRE(response_size != 0); - - auto response_str = std::string(c_response, response_size); - REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); - - auto response = nlohmann::json::parse(response_str); - CHECK(response.contains("hf")); - CHECK(response.contains("t")); - CHECK(response.contains("version")); - network_free(static_cast(ctx)); - }, - network); -} From e3bfd89619c5baa3a58941bf36ed95349148cf12 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Apr 2024 17:56:36 +1000 Subject: [PATCH 209/572] Resolved remaining PR comments and minor tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the service_node to inherit from RemoteAddress • Added the ability to provide a custom timeout when making an onion request • Standardised the log::level enums • Finished function docs --- include/session/config/base.h | 14 +-- include/session/config/base.hpp | 8 +- include/session/log_level.h | 20 ++++ include/session/network.h | 36 +++++- include/session/network.hpp | 120 ++++++++++++++++--- include/session/network_service_node.hpp | 72 ++++++++---- src/CMakeLists.txt | 1 + src/config/base.cpp | 23 ++-- src/network.cpp | 143 ++++++++++++----------- src/network_service_node.cpp | 73 +----------- src/onionreq/builder.cpp | 11 +- tests/test_config_userprofile.cpp | 12 +- 12 files changed, 324 insertions(+), 209 deletions(-) create mode 100644 include/session/log_level.h diff --git a/include/session/config/base.h b/include/session/config/base.h index 8707d903..27741edd 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -10,6 +10,7 @@ extern "C" { #include "../config.h" #include "../export.h" +#include "../log_level.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. @@ -42,13 +43,6 @@ typedef struct config_object { /// - `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 @@ -58,7 +52,7 @@ typedef enum config_log_level { /// /// The logging function must have signature: /// -/// void log(config_log_level lvl, const char* msg, void* ctx); +/// void log(LOG_LEVEL lvl, const char* msg, void* ctx); /// /// Can be called with callback set to NULL to clear an existing logger. /// @@ -68,7 +62,7 @@ typedef enum config_log_level { /// ```cpp /// VOID config_set_logger( /// [in, out] config_object* conf, -/// [in] void(*)(config_log_level, const char*, void*) callback, +/// [in] void(*)(LOG_LEVEL, const char*, void*) callback, /// [in] void* ctx /// ); /// ``` @@ -78,7 +72,7 @@ typedef enum config_log_level { /// - `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); + config_object* conf, void (*callback)(LOG_LEVEL, const char*, void*), void* ctx); /// API: base/config_storage_namespace /// diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index a7e5486f..2cb06e64 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -27,9 +28,6 @@ template static constexpr bool is_dict_value = is_dict_subtype || is_one_of; -// Levels for the logging callback -enum class LogLevel { debug = 0, info, warning, error }; - /// Our current config state enum class ConfigState : int { /// Clean means the config is confirmed stored on the server and we haven't changed anything. @@ -188,7 +186,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) { + void log(oxen::log::Level lvl, std::string msg) { if (logger) logger(lvl, std::move(msg)); } @@ -830,7 +828,7 @@ class ConfigBase : public ConfigSig { const DictFieldRoot data{*this}; // If set then we log things by calling this callback - std::function logger; + std::function logger; /// API: base/ConfigBase::storage_namespace /// diff --git a/include/session/log_level.h b/include/session/log_level.h new file mode 100644 index 00000000..7849b2fc --- /dev/null +++ b/include/session/log_level.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Note: These values must match the values in spdlog::level::level_enum +typedef enum LOG_LEVEL { + LOG_LEVEL_TRACE = 0, + LOG_LEVEL_DEBUG = 1, + LOG_LEVEL_INFO = 2, + LOG_LEVEL_WARN = 3, + LOG_LEVEL_ERROR = 4, + LOG_LEVEL_CRITICAL = 5, + LOG_LEVEL_OFF = 6, +} LOG_LEVEL; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/network.h b/include/session/network.h index 8b2925db..5af176e2 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -8,6 +8,7 @@ extern "C" { #include #include "export.h" +#include "log_level.h" #include "network_service_node.h" #include "onionreq/builder.h" @@ -84,7 +85,9 @@ LIBSESSION_EXPORT void network_free(network_object* network); /// - `network` -- [in] Pointer to the network object /// - `callback` -- [in] callback to be called when a new message should be logged. LIBSESSION_EXPORT void network_add_logger( - network_object* network, void (*callback)(const char*, size_t)); + network_object* network, + void (*callback)( + LOG_LEVEL lvl, const char* name, size_t namelen, const char* msg, size_t msglen)); /// API: network/network_clear_cache /// @@ -118,12 +121,35 @@ LIBSESSION_EXPORT void network_set_paths_changed_callback( void (*callback)(onion_request_path* paths, size_t paths_len, void* ctx), void* ctx); +/// API: network/network_get_swarm +/// +/// Retrieves the swarm for the given pubkey. If there is already an entry in the cache for the +/// swarm then that will be returned, otherwise a network request will be made to retrieve the +/// swarm and save it to the cache. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - 'swarm_pubkey_hex' - [in] includes the prefix. +/// - 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error +/// the callback will be called with an empty list). +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_get_swarm( network_object* network, const char* swarm_pubkey_hex, - void (*callback)(network_service_node*, size_t, void*), + void (*callback)(network_service_node* nodes, size_t nodes_len, void*), void* ctx); +/// API: network/network_get_random_nodes +/// +/// Retrieves a number of random nodes from the snode pool. If the are no nodes in the pool a +/// new pool will be populated and the nodes will be retrieved from that. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - 'count' - [in] the number of nodes to retrieve. +/// - 'callback' - [in] callback to be called with the retrieved nodes (in the case of an error +/// the callback will be called with an empty list). +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_get_random_nodes( network_object* network, uint16_t count, @@ -139,6 +165,7 @@ LIBSESSION_EXPORT void network_get_random_nodes( /// - `node` -- [in] address information about the service node the request should be sent to. /// - `body` -- [in] data to send to the specified node. /// - `body_size` -- [in] size of the `body`. +/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( @@ -147,6 +174,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( const unsigned char* body, size_t body_size, const char* swarm_pubkey_hex, + int64_t timeout_ms, void (*callback)( bool success, bool timeout, @@ -180,13 +208,15 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( /// - `headers_size` -- [in] The number of headers provided. /// - `body` -- [in] data to send to the specified endpoint. /// - `body_size` -- [in] size of the `body`. +/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. /// - `callback` -- [in] callback to be called with the result of the request. -/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. +/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( network_object* network, const network_server_destination server, const unsigned char* body, size_t body_size, + int64_t timeout_ms, void (*callback)( bool success, bool timeout, diff --git a/include/session/network.hpp b/include/session/network.hpp index e0d9276b..8de149d4 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -2,6 +2,7 @@ #include +#include #include #include "network_service_node.hpp" @@ -42,6 +43,7 @@ struct request_info { std::optional body; std::optional swarm_pubkey; onion_path path; + std::chrono::milliseconds timeout; bool is_retry; }; @@ -78,7 +80,10 @@ class Network { spdlog::pattern_formatter formatter; public: + // Hook to be notified whenever the network connection status changes. std::function status_changed; + + // Hook to be notified whenever the onion request paths are updated. std::function> paths)> paths_changed; // Constructs a new network with the given cache path and a flag indicating whether it should @@ -92,7 +97,9 @@ class Network { /// /// Inputs: /// - `callback` -- [in] callback to be called when a new message should be logged. - void add_logger(std::function callback); + void add_logger(std::function< + void(oxen::log::Level lvl, const std::string& name, const std::string& msg)> + callback); /// API: network/clear_cache /// @@ -107,8 +114,8 @@ class Network { /// swarm and save it to the cache. /// /// Inputs: - /// 'swarm_pubkey_hex' - [in] includes the prefix. - /// 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error + /// - 'swarm_pubkey_hex' - [in] includes the prefix. + /// - 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error /// the callback will be called with an empty list). void get_swarm( std::string swarm_pubkey_hex, @@ -120,8 +127,8 @@ class Network { /// new pool will be populated and the nodes will be retrieved from that. /// /// Inputs: - /// 'count' - [in] the number of nodes to retrieve. - /// 'callback' - [in] callback to be called with the retrieved nodes (in the case of an error + /// - 'count' - [in] the number of nodes to retrieve. + /// - 'callback' - [in] callback to be called with the retrieved nodes (in the case of an error /// the callback will be called with an empty list). void get_random_nodes( uint16_t count, std::function nodes)> callback); @@ -144,6 +151,7 @@ class Network { /// Inputs: /// - `destination` -- [in] service node or server destination information. /// - `body` -- [in] data to send to the specified destination. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used /// for internal purposes for cases which should retry automatically (like receiving a `421`) in /// order to prevent subsequent retries. @@ -151,6 +159,7 @@ class Network { void send_onion_request( onionreq::network_destination destination, std::optional body, + std::chrono::milliseconds timeout, bool is_retry, network_response_callback_t handle_response); @@ -194,7 +203,7 @@ class Network { /// provided, this method ignores invalid or unchanged status changes. /// /// Inputs: - /// 'updated_status' - [in] the updated connection status. + /// - 'updated_status' - [in] the updated connection status. void update_status(ConnectionStatus updated_status); /// API: network/start_disk_write_thread @@ -209,29 +218,117 @@ class Network { /// data exists. void load_cache_from_disk(); + /// API: network/get_connection_info + /// + /// Creates a connection and opens a stream to the target service node. + /// + /// Inputs: + /// - `target` -- [in] the target service node to connect to. + /// - `conn_established_cb` -- [in, optional] a callback to be passed when creating the + /// connection that should be triggered when the connection is established. connection_info get_connection_info( service_node target, std::optional conn_established_cb); - void with_snode_pool(std::function)> callback); + /// API: network/with_snode_pool + /// + /// Retrieves the service node pool from the cache. If the cache is empty it will first be + /// populated from the network. + /// + /// Inputs: + /// - `callback` -- [in] callback to be triggered once we have the service node pool. NOTE: If + /// we are unable to retrieve the service node pool the callback will be triggered with an empty + /// list. + void with_snode_pool(std::function pool)> callback); + + /// API: network/with_path + /// + /// Retrieves a valid onion request path to perform a request on. If there aren't currently any + /// paths then new paths will be constructed by opening and testing connections to random + /// service nodes in the snode pool. + /// + /// Inputs: + /// - `excluded_node` -- [in, optional] node which should not be included in the path. + /// - `callback` -- [in] callback to be triggered once we have a valid path, NULL if we are + /// unable to find a valid path. void with_path( std::optional excluded_node, std::function path)> callback); + + /// API: network/build_paths_if_needed + /// + /// Builds onion request paths if needed by opening and testing connections to random service + /// nodes in the snode pool. If we already have enough paths the callback will be triggered + /// with the current paths. + /// + /// Inputs: + /// - `excluded_node` -- [in, optional] node which should not be included in the paths. + /// - `callback` -- [in] callback to be triggered once we have enough paths. NOTE: If we are + /// unable to create the paths the callback will be triggered with an empty list. void build_paths_if_needed( std::optional excluded_node, std::function updated_paths)> callback); + /// API: network/get_service_nodes_recursive + /// + /// A recursive function that will attempt to retrieve service nodes from a given node until it + /// successfully retrieves nodes or the list is drained. + /// + /// Inputs: + /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's + /// drained. + /// - `limit` -- [in, optional] the number of service nodes to retrieve. + /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If we drain the + /// `target_nodes` and haven't gotten a successful response an empty list will be returned along + /// with an error string. void get_service_nodes_recursive( + std::vector target_nodes, std::optional limit, - std::vector nodes, std::function nodes, std::optional error)> callback); + + /// API: network/find_valid_guard_node_recursive + /// + /// A recursive function that sends a request to provided nodes until a successful response is + /// received or the list is drained. + /// + /// Inputs: + /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's + /// drained. + /// - `callback` -- [in] callback to be triggered once we make a successful request. NOTE: If + /// we drain the `target_nodes` and haven't gotten a successful response a NULL + /// `connection_info` and empty will be provided. void find_valid_guard_node_recursive( - std::vector unused_nodes, + std::vector target_nodes, std::function< void(std::optional valid_guard_node, std::vector unused_nodes)> callback); + /// API: network/get_service_nodes + /// + /// Retrieves all or a random subset of service nodes from the given node. + /// + /// Inputs: + /// - `node` -- [in] node to retrieve the service nodes from. + /// - `limit` -- [in, optional] the number of service nodes to retrieve. + /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If an error + /// occurs an empty list and an error will be returned. + void get_service_nodes( + service_node node, + std::optional limit, + std::function nodes, std::optional error)> + callback); + + /// API: network/get_version + /// + /// Retrieves the version information for a given service node. + /// + /// Inputs: + /// - `node` -- [in] node to retrieve the version from. + /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the + /// `quic::DEFAULT_TIMEOUT` will be used. + /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an + /// error occurs an empty list and an error will be returned. void get_version( service_node node, std::optional timeout, @@ -239,11 +336,6 @@ class Network { void(std::vector version, connection_info info, std::optional error)> callback); - void get_service_nodes( - std::optional limit, - service_node node, - std::function nodes, std::optional error)> - callback); /// API: network/process_snode_response /// diff --git a/include/session/network_service_node.hpp b/include/session/network_service_node.hpp index 0beddf00..467356cf 100644 --- a/include/session/network_service_node.hpp +++ b/include/session/network_service_node.hpp @@ -3,49 +3,75 @@ #include #include +#include #include #include #include "onionreq/key_types.hpp" +#include "util.hpp" namespace session::network { -struct service_node { - std::array ip; - uint16_t quic_port; +namespace { + std::vector split_snode(std::string_view str) { + auto parts = split(str, "|"); + if (parts.size() < 4) + throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); + + return parts; + } +} // namespace + +struct service_node : public oxen::quic::RemoteAddress { + public: session::onionreq::x25519_pubkey x25519_pubkey; - session::onionreq::ed25519_pubkey ed25519_pubkey; uint8_t failure_count; bool invalid; service_node( - std::array ip, - uint16_t quic_port, - session::onionreq::x25519_pubkey x25519_pubkey, - session::onionreq::ed25519_pubkey ed25519_pubkey, - uint8_t failure_count, - bool invalid) : - ip{ip}, - quic_port{quic_port}, - x25519_pubkey{std::move(x25519_pubkey)}, - ed25519_pubkey{std::move(ed25519_pubkey)}, + std::string_view ed25519_pubkey_hex, + std::string_view x25519_pubkey_hex, + std::string ip, + uint16_t port, + uint8_t failure_count = 0, + bool invalid = false) : + oxen::quic::RemoteAddress{oxenc::from_hex(ed25519_pubkey_hex), ip, port}, + x25519_pubkey{session::onionreq::x25519_pubkey::from_hex(x25519_pubkey_hex)}, failure_count{failure_count}, invalid{invalid} {} - service_node(nlohmann::json json); - service_node(std::string_view serialised); - service_node(oxenc::bt_dict_consumer bencoded); + service_node(nlohmann::json json) : + service_node( + json["pubkey_ed25519"].get(), + json["pubkey_x25519"].get(), + json["ip"].get(), + json["port_omq"].get()){}; + + service_node(oxenc::bt_dict_consumer bencoded) : + service_node( + bencoded.consume_string(), // pubkey_ed25519 + bencoded.consume_string(), // pubkey_x25519 + bencoded.consume_string(), // public_ip + bencoded.consume_integer()){}; // storage_lmq_port + + service_node(std::string_view serialised) : service_node(split_snode(serialised)){}; std::string serialise() const; - std::string pretty_description() const; bool operator==(const service_node& other) const { - return ip == other.ip && quic_port == other.quic_port && - x25519_pubkey == other.x25519_pubkey && ed25519_pubkey == other.ed25519_pubkey && - failure_count == other.failure_count && invalid == other.invalid; + return oxen::quic::RemoteAddress::operator==(other) && + x25519_pubkey == other.x25519_pubkey && failure_count == other.failure_count && + invalid == other.invalid; } -}; -std::array split_ipv4(std::string_view ip); + private: + service_node(std::vector parts) : + service_node( + parts[3], // ed25519_pubkey + parts[2], // x25519_pubkey + std::string(parts[0]), // ip + std::stoi(std::string{parts[1]}), // port + (parts.size() >= 5 ? std::stoi(std::string{parts[4]}) : 0)){}; // failure_count +}; } // namespace session::network diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 288a9078..0e91dc9f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,6 +82,7 @@ target_link_libraries(config PRIVATE libsodium::sodium-internal libzstd::static + oxen::logging ) add_libsession_util_library(onionreq diff --git a/src/config/base.cpp b/src/config/base.cpp index ae44ffcc..28d1923c 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -132,15 +133,15 @@ std::vector ConfigBase::_merge( plaintexts.emplace_back(hash, decrypt(conf, key(i), encryption_domain())); decrypted = true; } catch (const decrypt_error&) { - log(LogLevel::debug, + log(oxen::log::Level::debug, "Failed to decrypt message " + std::to_string(ci) + " using key " + std::to_string(i)); } } if (!decrypted) - log(LogLevel::warning, "Failed to decrypt message " + std::to_string(ci)); + log(oxen::log::Level::warn, "Failed to decrypt message " + std::to_string(ci)); } - log(LogLevel::debug, + log(oxen::log::Level::debug, "successfully decrypted " + std::to_string(plaintexts.size()) + " of " + std::to_string(configs.size()) + " incoming messages"); @@ -151,13 +152,13 @@ std::vector ConfigBase::_merge( plain.resize(plain.size() - p); } if (plain.empty()) { - log(LogLevel::error, "Invalid config message: contains no data"); + log(oxen::log::Level::err, "Invalid config message: contains no data"); continue; } // TODO FIXME (see above) if (plain[0] == 'm') { - log(LogLevel::warning, "multi-part messages not yet supported!"); + log(oxen::log::Level::warn, "multi-part messages not yet supported!"); continue; } @@ -168,13 +169,13 @@ std::vector ConfigBase::_merge( decompressed && !decompressed->empty()) plain = std::move(*decompressed); else { - log(LogLevel::warning, "Invalid config message: decompression failed"); + log(oxen::log::Level::warn, "Invalid config message: decompression failed"); continue; } } if (plain[0] != 'd') - log(LogLevel::error, + log(oxen::log::Level::err, "invalid/unsupported config message with type " + (plain[0] >= 0x20 && plain[0] <= 0x7e ? "'" + std::string{from_unsigned_sv(plain.substr(0, 1))} + "'" @@ -193,7 +194,7 @@ std::vector ConfigBase::_merge( _config->signer, config_lags(), [&](size_t i, const config_error& e) { - log(LogLevel::warning, e.what()); + log(oxen::log::Level::warn, e.what()); assert(i > 0); // i == 0 means we can't deserialize our own serialization bad_confs.insert(i); }); @@ -758,12 +759,12 @@ LIBSESSION_EXPORT void config_clear_sig_keys(config_object* conf) { } LIBSESSION_EXPORT void config_set_logger( - config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx) { + config_object* conf, void (*callback)(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); + unbox(conf)->logger = [callback, ctx](oxen::log::Level lvl, std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); }; } diff --git a/src/network.cpp b/src/network.cpp index f6cf46a2..ff7a0f03 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -339,12 +339,14 @@ void Network::clear_cache() { // MARK: Logging -void Network::add_logger(std::function callback) { +void Network::add_logger( + std::function + callback) { auto sink = std::make_shared( [this, cb = std::move(callback)](const spdlog::details::log_msg& msg) { spdlog::memory_buf_t buf; formatter.format(msg, buf); - cb(to_string(buf)); + cb(msg.level, to_string(msg.logger_name), to_string(buf)); }); oxen::log::add_sink(sink); } @@ -373,15 +375,12 @@ void Network::update_status(ConnectionStatus updated_status) { connection_info Network::get_connection_info( service_node target, std::optional conn_established_cb) { - auto remote_ip = "{}"_format(fmt::join(target.ip, ".")); - auto remote = - oxen::quic::RemoteAddress{target.ed25519_pubkey.view(), remote_ip, target.quic_port}; auto connection_key_pair = ed25519::ed25519_key_pair(); auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( std::string(connection_key_pair.second.begin(), connection_key_pair.second.end())); auto c = endpoint->connect( - remote, + target, creds, oxen::quic::opt::keep_alive{10s}, conn_established_cb, @@ -397,7 +396,7 @@ connection_info Network::get_connection_info( target_path->conn_info.conn.reset(); target_path->conn_info.stream.reset(); handle_errors( - {target, "", std::nullopt, std::nullopt, *target_path, false}, + {target, "", std::nullopt, std::nullopt, *target_path, 0ms, false}, std::nullopt, std::nullopt, std::nullopt); @@ -409,7 +408,7 @@ connection_info Network::get_connection_info( // MARK: Snode Pool and Onion Path -void Network::with_snode_pool(std::function)> callback) { +void Network::with_snode_pool(std::function pool)> callback) { get_snode_pool_loop->call([this, cb = std::move(callback)]() mutable { auto current_pool_info = net.call_get( [this]() -> std::pair< @@ -461,7 +460,7 @@ void Network::with_snode_pool(std::function)> cal std::shuffle(target_pool.begin(), target_pool.end(), rng); std::promise> prom; - get_service_nodes(256, target_pool.front(), handle_nodes_response(prom)); + get_service_nodes(target_pool.front(), 256, handle_nodes_response(prom)); // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool // so we don't double up on requests @@ -502,9 +501,9 @@ void Network::with_snode_pool(std::function)> cal std::promise> prom3; // Kick off 3 concurrent requests - get_service_nodes_recursive(std::nullopt, first_nodes, handle_nodes_response(prom1)); - get_service_nodes_recursive(std::nullopt, second_nodes, handle_nodes_response(prom2)); - get_service_nodes_recursive(std::nullopt, third_nodes, handle_nodes_response(prom3)); + get_service_nodes_recursive(first_nodes, std::nullopt, handle_nodes_response(prom1)); + get_service_nodes_recursive(second_nodes, std::nullopt, handle_nodes_response(prom2)); + get_service_nodes_recursive(third_nodes, std::nullopt, handle_nodes_response(prom3)); // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool // so we don't double up on requests @@ -513,10 +512,10 @@ void Network::with_snode_pool(std::function)> cal auto third_result_nodes = prom3.get_future().get(); auto compare_nodes = [](const auto& a, const auto& b) { - if (a.ip == b.ip) { - return a.quic_port < b.quic_port; + if (a.host() == b.host()) { + return a.port() < b.port(); } - return a.ip < b.ip; + return a.host() < b.host(); }; // Sort the vectors (so make it easier to find the @@ -648,6 +647,7 @@ void Network::build_paths_if_needed( build_paths_loop->call([this, excluded_node, pool, cb = std::move(cb)]() mutable { auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + // No need to do anything if we already have enough paths if (current_paths.size() >= target_path_count) return cb(current_paths); @@ -658,6 +658,7 @@ void Network::build_paths_if_needed( // Get the possible guard nodes oxen::log::info(log_cat, "Building paths."); std::vector nodes_to_exclude; + std::vector possible_guard_nodes; if (excluded_node) nodes_to_exclude.push_back(*excluded_node); @@ -665,7 +666,6 @@ void Network::build_paths_if_needed( for (auto& path : paths) nodes_to_exclude.insert( nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); - std::vector possible_guard_nodes; if (nodes_to_exclude.empty()) possible_guard_nodes = pool; @@ -772,7 +772,7 @@ void Network::build_paths_if_needed( path.begin(), path.end(), std::back_inserter(node_descriptions), - [](service_node& node) { return node.pretty_description(); }); + [](service_node& node) { return node.to_string(); }); auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); oxen::log::info( log_cat, "Built new onion request path: [{}]", path_description); @@ -806,8 +806,8 @@ void Network::build_paths_if_needed( // MARK: Multi-request logic void Network::get_service_nodes_recursive( - std::optional limit, std::vector target_nodes, + std::optional limit, std::function nodes, std::optional error)> callback) { if (target_nodes.empty()) @@ -815,8 +815,8 @@ void Network::get_service_nodes_recursive( auto target_node = target_nodes.front(); get_service_nodes( - limit, target_node, + limit, [this, limit, target_nodes, cb = std::move(callback)]( std::vector nodes, std::optional error) { // If we got nodes then stop looping and return them @@ -826,74 +826,66 @@ void Network::get_service_nodes_recursive( // Loop if we didn't get any nodes std::vector remaining_nodes( target_nodes.begin() + 1, target_nodes.end()); - get_service_nodes_recursive(limit, remaining_nodes, cb); + get_service_nodes_recursive(remaining_nodes, limit, cb); }); } void Network::find_valid_guard_node_recursive( - std::vector unused_nodes, + std::vector target_nodes, std::function< void(std::optional valid_guard_node, std::vector unused_nodes)> callback) { - if (unused_nodes.empty()) + if (target_nodes.empty()) return callback(std::nullopt, {}); - auto target_node = unused_nodes.front(); - oxen::log::info(log_cat, "Testing guard snode: {}", target_node.pretty_description()); + auto target_node = target_nodes.front(); + oxen::log::info(log_cat, "Testing guard snode: {}", target_node.to_string()); get_version( target_node, 3s, - [this, target_node, unused_nodes, cb = std::move(callback)]( + [this, target_node, target_nodes, cb = std::move(callback)]( std::vector version, connection_info info, std::optional error) { std::vector remaining_nodes( - unused_nodes.begin() + 1, unused_nodes.end()); + target_nodes.begin() + 1, target_nodes.end()); - // Log the error and loop after a slight delay (don't want to drain the pool - // too quickly if the network goes down) - if (error) { + try { + if (error) + throw std::runtime_error{*error}; + + // Ensure the node meets the minimum version requirements after a slight + // delay (don't want to drain the pool if the network goes down) + std::vector min_version = parse_version("2.0.7"); + if (version < min_version) + throw std::runtime_error{ + "Outdated node version ({})"_format(fmt::join(version, "."))}; + + oxen::log::info(log_cat, "Guard snode {} valid.", target_node.to_string()); + cb(info, remaining_nodes); + } catch (const std::exception& e) { + // Log the error and loop after a slight delay (don't want to drain the pool + // too quickly if the network goes down) oxen::log::info( log_cat, "Testing {} failed with error: {}", - target_node.pretty_description(), - *error); - std::thread retry_thread([this, remaining_nodes, cb = std::move(cb)] { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - find_valid_guard_node_recursive(remaining_nodes, cb); - }); - retry_thread.detach(); - return; - } - - // Ensure the node meets the minimum version requirements after a slight - // delay (don't want to drain the pool if the network goes down) - std::vector min_version = parse_version("2.0.7"); - if (version < min_version) { - oxen::log::info( - log_cat, - "Testing {} failed with error: Outdated node version ({})", - target_node.pretty_description(), - fmt::join(version, ".")); + target_node.to_string(), + e.what()); std::thread retry_thread([this, remaining_nodes, cb = std::move(cb)] { std::this_thread::sleep_for(std::chrono::milliseconds(100)); find_valid_guard_node_recursive(remaining_nodes, cb); }); retry_thread.detach(); - return; } - - oxen::log::info(log_cat, "Guard snode {} valid.", target_node.pretty_description()); - cb(info, remaining_nodes); }); } // MARK: Pre-Defined Requests void Network::get_service_nodes( - std::optional limit, service_node node, + std::optional limit, std::function nodes, std::optional error)> callback) { auto info = get_connection_info(node, std::nullopt); @@ -1019,6 +1011,7 @@ void Network::get_swarm( send_onion_request( SnodeDestination{node, swarm_pubkey_hex}, ustring{oxen::quic::to_usv(payload.dump())}, + oxen::quic::DEFAULT_TIMEOUT, false, [this, swarm_pubkey_hex, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { @@ -1087,6 +1080,7 @@ void Network::send_request( conn_info.stream->command( info.endpoint, payload, + info.timeout, [this, info, cb = std::move(handle_response)](oxen::quic::message resp) { try { auto [status_code, body] = validate_response(resp, false); @@ -1102,11 +1096,12 @@ void Network::send_request( void Network::send_onion_request( network_destination destination, std::optional body, + std::chrono::milliseconds timeout, bool is_retry, network_response_callback_t handle_response) { with_path( node_for_destination(destination), - [this, destination, body, is_retry, cb = std::move(handle_response)]( + [this, destination, body, timeout, is_retry, cb = std::move(handle_response)]( std::optional path) { if (!path) return cb(false, false, -1, "No valid onion paths."); @@ -1117,7 +1112,9 @@ void Network::send_onion_request( builder.set_destination(destination); for (auto& node : path->nodes) - builder.add_hop({node.ed25519_pubkey, node.x25519_pubkey}); + builder.add_hop( + {ed25519_pubkey::from_bytes(node.view_remote_key()), + node.x25519_pubkey}); auto payload = builder.generate_payload(body); auto onion_req_payload = builder.build(payload); @@ -1128,6 +1125,7 @@ void Network::send_onion_request( onion_req_payload, swarm_pubkey_for_destination(destination), *path, + timeout, is_retry}; send_request( @@ -1385,6 +1383,7 @@ void Network::handle_errors( return send_onion_request( SnodeDestination{*random_node, info.swarm_pubkey}, info.body, + info.timeout, true, (*handle_response)); } @@ -1438,11 +1437,12 @@ void Network::handle_errors( if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { session::onionreq::ed25519_pubkey edpk = session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); + auto edpk_view = to_unsigned_sv(edpk.view()); auto snode_it = std::find_if( updated_path.nodes.begin(), updated_path.nodes.end(), - [&edpk](const auto& node) { return node.ed25519_pubkey == edpk; }); + [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); // Increment the failure count for the snode if (snode_it != updated_path.nodes.end()) { @@ -1534,13 +1534,14 @@ std::vector convert_service_nodes( std::vector nodes) { std::vector converted_nodes; for (auto& node : nodes) { + auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); network_service_node converted_node; - std::memcpy(converted_node.ip, node.ip.data(), sizeof(converted_node.ip)); + std::memcpy(converted_node.ip, node.host().data(), sizeof(converted_node.ip)); strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); - strncpy(converted_node.ed25519_pubkey_hex, node.ed25519_pubkey.hex().c_str(), 64); + strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); converted_node.x25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination - converted_node.quic_port = node.quic_port; + converted_node.quic_port = node.port(); converted_node.failure_count = node.failure_count; converted_node.invalid = node.invalid; converted_nodes.push_back(converted_node); @@ -1605,10 +1606,15 @@ LIBSESSION_C_API void network_free(network_object* network) { } LIBSESSION_C_API void network_add_logger( - network_object* network, void (*callback)(const char*, size_t)) { + network_object* network, + void (*callback)( + LOG_LEVEL lvl, const char* name, size_t namelen, const char* msg, size_t msglen)) { assert(callback); unbox(network).add_logger( - [cb = std::move(callback)](const std::string& msg) { cb(msg.c_str(), msg.size()); }); + [cb = std::move(callback)]( + oxen::log::Level lvl, const std::string& name, const std::string& msg) { + cb(static_cast(lvl), name.c_str(), name.size(), msg.c_str(), msg.size()); + }); } LIBSESSION_C_API void network_clear_cache(network_object* network) { @@ -1659,7 +1665,7 @@ LIBSESSION_C_API void network_set_paths_changed_callback( LIBSESSION_C_API void network_get_swarm( network_object* network, const char* swarm_pubkey_hex, - void (*callback)(network_service_node*, size_t, void*), + void (*callback)(network_service_node* nodes, size_t nodes_len, void*), void* ctx) { assert(swarm_pubkey_hex && callback); unbox(network).get_swarm( @@ -1688,6 +1694,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( const unsigned char* body_, size_t body_size, const char* swarm_pubkey_hex, + int64_t timeout_ms, void (*callback)( bool success, bool timeout, @@ -1712,14 +1719,14 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( unbox(network).send_onion_request( SnodeDestination{ - {ip, + {{node.ed25519_pubkey_hex, 64}, + {node.x25519_pubkey_hex, 64}, + "{}"_format(fmt::join(ip, ".")), node.quic_port, - x25519_pubkey::from_hex({node.x25519_pubkey_hex, 64}), - ed25519_pubkey::from_hex({node.ed25519_pubkey_hex, 64}), - node.failure_count, - false}, + node.failure_count}, swarm_pubkey}, body, + std::chrono::milliseconds{timeout_ms}, false, [callback, ctx]( bool success, @@ -1739,6 +1746,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( const network_server_destination server, const unsigned char* body_, size_t body_size, + int64_t timeout_ms, void (*callback)( bool success, bool timeout, @@ -1772,6 +1780,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers, server.method}, body, + std::chrono::milliseconds{timeout_ms}, false, [callback, ctx]( bool success, diff --git a/src/network_service_node.cpp b/src/network_service_node.cpp index c268751a..6012b3be 100644 --- a/src/network_service_node.cpp +++ b/src/network_service_node.cpp @@ -1,83 +1,22 @@ #include "session/network_service_node.hpp" -#include - #include -#include -#include #include "session/onionreq/key_types.hpp" -using namespace session; -using namespace session::onionreq; - namespace session::network { -service_node::service_node(nlohmann::json json) { - ip = split_ipv4(json["ip"].get()); - quic_port = json["port_omq"].get(); - x25519_pubkey = x25519_pubkey::from_hex(json["pubkey_x25519"].get()); - ed25519_pubkey = ed25519_pubkey::from_hex(json["pubkey_ed25519"].get()); -} - -service_node::service_node(std::string_view serialised) { - auto parts = split(serialised, "|"); - if (parts.size() < 4) - throw std::invalid_argument{ - "Invalid service node serialisation: " + std::to_string(parts.size()) + ", " + - std::string(serialised)}; - - ip = split_ipv4(parts[0]); - quic_port = std::stoi(std::string{parts[1]}); - x25519_pubkey = x25519_pubkey::from_hex(parts[2]); - ed25519_pubkey = ed25519_pubkey::from_hex(parts[3]); - invalid = false; // If a node is invalid we would have removed it from the pool - - // If we have a failure count then parse it - if (parts.size() >= 5) - failure_count = std::stoi(std::string{parts[4]}); - else - failure_count = 0; -} - -service_node::service_node(oxenc::bt_dict_consumer bencoded) { - ed25519_pubkey = ed25519_pubkey::from_hex(bencoded.consume_string()); - x25519_pubkey = x25519_pubkey::from_hex(bencoded.consume_string()); - ip = split_ipv4(bencoded.consume_string()); - quic_port = bencoded.consume_integer(); -} - std::string service_node::serialise() const { + auto ed25519_pubkey_hex = oxenc::to_hex(view_remote_key()); + return fmt::format( - "{}.{}.{}.{}|{}|{}|{}|{}|{}", - ip[0], - ip[1], - ip[2], - ip[3], - quic_port, + "{}|{}|{}|{}|{}|{}", + host(), + port(), x25519_pubkey.hex(), - ed25519_pubkey.hex(), + ed25519_pubkey_hex, failure_count, invalid ? "1" : "0"); } -std::string service_node::pretty_description() const { - return fmt::format("{}.{}.{}.{}:{}", ip[0], ip[1], ip[2], ip[3], quic_port); -}; - -std::array split_ipv4(std::string_view ip) { - std::array quad; - auto nums = split(ip, "."); - if (nums.size() != 4) - throw "Invalid IPv4 address"; - for (int i = 0; i < 4; i++) { - auto end = nums[i].data() + nums[i].size(); - if (auto [p, ec] = std::from_chars(nums[i].data(), end, quad[i]); - ec != std::errc{} || p != end) - throw "Invalid malformed IPv4 address"; - } - - return quad; -} - } // namespace session::network \ No newline at end of file diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 8e15153f..a8edfda3 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "session/export.h" #include "session/onionreq/builder.h" @@ -26,6 +27,7 @@ #include "session/xed25519.hpp" using namespace std::literals; +using namespace oxen::log::literals; using session::ustring_view; namespace session::onionreq { @@ -54,7 +56,7 @@ void Builder::set_destination(network_destination destination) { if (auto* dest = std::get_if(&destination)) { destination_x25519_public_key.emplace(dest->node.x25519_pubkey); - ed25519_public_key_.emplace(dest->node.ed25519_pubkey); + ed25519_public_key_.emplace(ed25519_pubkey::from_bytes(dest->node.view_remote_key())); } else if (auto* dest = std::get_if(&destination)) { host_.emplace(dest->host); endpoint_.emplace(dest->endpoint); @@ -280,11 +282,12 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( std::array target_ip; std::memcpy(target_ip.data(), ip, target_ip.size()); + auto node = session::network::service_node{ - target_ip, + {ed25519_pubkey, 64}, + {x25519_pubkey, 64}, + "{}"_format(fmt::join(target_ip, ".")), quic_port, - session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64}), - session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), failure_count, false}; unbox(builder).set_destination(session::onionreq::SnodeDestination{node, std::nullopt}); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 34ca80db..f5c34633 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -12,11 +12,13 @@ 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" - : "debug") +void log_msg(LOG_LEVEL lvl, const char* msg, void*) { + INFO((lvl == LOG_LEVEL_CRITICAL ? "CRITICAL" + : lvl == LOG_LEVEL_ERROR ? "ERROR" + : lvl == LOG_LEVEL_WARN ? "Warning" + : lvl == LOG_LEVEL_INFO ? "Info" + : lvl == LOG_LEVEL_DEBUG ? "debug" + : "trace") << ": " << msg); } From cc2d8fc8e89af3f7f74f34f61f6a293809c48a88 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Apr 2024 17:08:44 +1000 Subject: [PATCH 210/572] Used 'RemoteAddress' instead of a custom service_node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Replaced custom `service_node` with straight `oxen::quic::RemoteAddress` use • Moved the failure count into a separate unsorted_map • Got the tests working again --- include/session/network.h | 9 +- include/session/network.hpp | 65 ++++- include/session/network_service_node.h | 23 -- include/session/network_service_node.hpp | 77 ------ include/session/onionreq/builder.h | 18 +- include/session/onionreq/builder.hpp | 12 +- src/CMakeLists.txt | 1 - src/network.cpp | 330 ++++++++++++++++------- src/network_service_node.cpp | 22 -- src/onionreq/builder.cpp | 43 +-- tests/test_network.cpp | 315 ++++++++++++++++++++++ 11 files changed, 654 insertions(+), 261 deletions(-) delete mode 100644 include/session/network_service_node.h delete mode 100644 include/session/network_service_node.hpp delete mode 100644 src/network_service_node.cpp diff --git a/include/session/network.h b/include/session/network.h index 5af176e2..b87eb63f 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -9,7 +9,6 @@ extern "C" { #include "export.h" #include "log_level.h" -#include "network_service_node.h" #include "onionreq/builder.h" typedef enum CONNECTION_STATUS { @@ -24,6 +23,12 @@ typedef struct network_object { void* internals; } network_object; +typedef struct network_service_node { + uint8_t ip[4]; + uint16_t quic_port; + char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. +} network_service_node; + typedef struct network_server_destination { const char* method; const char* protocol; @@ -129,7 +134,7 @@ LIBSESSION_EXPORT void network_set_paths_changed_callback( /// /// Inputs: /// - `network` -- [in] Pointer to the network object -/// - 'swarm_pubkey_hex' - [in] includes the prefix. +/// - 'swarm_pubkey_hex' - [in] x25519 pubkey for the swarm in hex (64 characters). /// - 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error /// the callback will be called with an empty list). /// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. diff --git a/include/session/network.hpp b/include/session/network.hpp index 8de149d4..80835fb2 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -5,13 +5,16 @@ #include #include -#include "network_service_node.hpp" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/types.hpp" namespace session::network { +using service_node = oxen::quic::RemoteAddress; +using network_response_callback_t = std::function response)>; + enum class ConnectionStatus { unknown = 0, connecting = 1, @@ -20,7 +23,7 @@ enum class ConnectionStatus { }; struct connection_info { - session::network::service_node node; + service_node node; std::shared_ptr conn; std::shared_ptr stream; @@ -29,7 +32,7 @@ struct connection_info { struct onion_path { connection_info conn_info; - std::vector nodes; + std::vector nodes; uint8_t failure_count; bool operator==(const onion_path& other) const { @@ -41,15 +44,12 @@ struct request_info { service_node target; std::string endpoint; std::optional body; - std::optional swarm_pubkey; + std::optional swarm_pubkey; onion_path path; std::chrono::milliseconds timeout; bool is_retry; }; -using network_response_callback_t = std::function response)>; - class Network { private: const bool use_testnet; @@ -67,6 +67,7 @@ class Network { // Values persisted to disk std::vector snode_pool; + std::unordered_map snode_failure_counts; std::chrono::system_clock::time_point last_snode_pool_update; std::unordered_map> swarm_cache; @@ -114,13 +115,22 @@ class Network { /// swarm and save it to the cache. /// /// Inputs: - /// - 'swarm_pubkey_hex' - [in] includes the prefix. + /// - 'swarm_pubkey' - [in] public key for the swarm. /// - 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error /// the callback will be called with an empty list). void get_swarm( - std::string swarm_pubkey_hex, + session::onionreq::x25519_pubkey swarm_pubkey, std::function swarm)> callback); + /// API: network/set_swarm + /// + /// Update the nodes to be used for a swarm. This function should never be called directly. + /// + /// Inputs: + /// - 'swarm_pubkey' - [in] public key for the swarm. + /// - `swarm` -- [in] nodes for the swarm. + void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm); + /// API: network/get_random_nodes /// /// Retrieves a number of random nodes from the snode pool. If the are no nodes in the pool a @@ -151,6 +161,8 @@ class Network { /// Inputs: /// - `destination` -- [in] service node or server destination information. /// - `body` -- [in] data to send to the specified destination. + /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. + /// Should be NULL if the request is not associated with a swarm. /// - `timeout` -- [in] timeout in milliseconds to use for the request. /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used /// for internal purposes for cases which should retry automatically (like receiving a `421`) in @@ -159,6 +171,7 @@ class Network { void send_onion_request( onionreq::network_destination destination, std::optional body, + std::optional swarm_pubkey, std::chrono::milliseconds timeout, bool is_retry, network_response_callback_t handle_response); @@ -196,6 +209,40 @@ class Network { std::optional response, std::optional handle_response); + /// API: network/get_failure_count + /// + /// Retrieves the current failure count for a given node, returns 0 if it is not present. + /// + /// Inputs: + /// - `node` -- [in] the node to get the failure count for. + uint8_t get_failure_count(service_node node); + + /// API: network/set_failure_count + /// + /// Updated the failure count for a given node. + /// + /// Inputs: + /// - `node` -- [in] the node to get the failure count for. + /// - `failure_count` -- [in] the failure count to set the node to. + void set_failure_count(service_node node, uint8_t failure_count); + + /// API: network/set_paths + /// + /// Update the paths to be used for sending onion requests. This function should never be + /// called directly. + /// + /// Inputs: + /// - `paths` -- [in] paths to use. + void set_paths(std::vector paths); + + /// API: network/get_failure_count + /// + /// Retrieves the current failure count for a given path, returns 0 if it is not present. + /// + /// Inputs: + /// - `path` -- [in] the path to get the failure count for. + uint8_t get_failure_count(onion_path path); + private: /// API: network/update_status /// diff --git a/include/session/network_service_node.h b/include/session/network_service_node.h deleted file mode 100644 index 02020890..00000000 --- a/include/session/network_service_node.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -typedef struct network_service_node { - uint8_t ip[4]; - uint16_t quic_port; - char x25519_pubkey_hex[65]; // The 64-byte x25519 pubkey in hex + null terminator. - char ed25519_pubkey_hex[65]; // The 64-byte ed25519 pubkey in hex + null terminator. - - uint8_t failure_count; - bool invalid; -} network_service_node; - -#ifdef __cplusplus -} -#endif diff --git a/include/session/network_service_node.hpp b/include/session/network_service_node.hpp deleted file mode 100644 index 467356cf..00000000 --- a/include/session/network_service_node.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "onionreq/key_types.hpp" -#include "util.hpp" - -namespace session::network { - -namespace { - std::vector split_snode(std::string_view str) { - auto parts = split(str, "|"); - if (parts.size() < 4) - throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); - - return parts; - } -} // namespace - -struct service_node : public oxen::quic::RemoteAddress { - public: - session::onionreq::x25519_pubkey x25519_pubkey; - uint8_t failure_count; - bool invalid; - - service_node( - std::string_view ed25519_pubkey_hex, - std::string_view x25519_pubkey_hex, - std::string ip, - uint16_t port, - uint8_t failure_count = 0, - bool invalid = false) : - oxen::quic::RemoteAddress{oxenc::from_hex(ed25519_pubkey_hex), ip, port}, - x25519_pubkey{session::onionreq::x25519_pubkey::from_hex(x25519_pubkey_hex)}, - failure_count{failure_count}, - invalid{invalid} {} - - service_node(nlohmann::json json) : - service_node( - json["pubkey_ed25519"].get(), - json["pubkey_x25519"].get(), - json["ip"].get(), - json["port_omq"].get()){}; - - service_node(oxenc::bt_dict_consumer bencoded) : - service_node( - bencoded.consume_string(), // pubkey_ed25519 - bencoded.consume_string(), // pubkey_x25519 - bencoded.consume_string(), // public_ip - bencoded.consume_integer()){}; // storage_lmq_port - - service_node(std::string_view serialised) : service_node(split_snode(serialised)){}; - - std::string serialise() const; - - bool operator==(const service_node& other) const { - return oxen::quic::RemoteAddress::operator==(other) && - x25519_pubkey == other.x25519_pubkey && failure_count == other.failure_count && - invalid == other.invalid; - } - - private: - service_node(std::vector parts) : - service_node( - parts[3], // ed25519_pubkey - parts[2], // x25519_pubkey - std::string(parts[0]), // ip - std::stoi(std::string{parts[1]}), // port - (parts.size() >= 5 ? std::stoi(std::string{parts[4]}) : 0)){}; // failure_count -}; - -} // namespace session::network diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 316d0707..cf011495 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -8,7 +8,6 @@ extern "C" { #include #include "../export.h" -#include "../network_service_node.h" typedef enum ENCRYPT_TYPE { ENCRYPT_TYPE_AES_GCM = 0, @@ -53,15 +52,11 @@ LIBSESSION_EXPORT void onion_request_builder_set_enc_type( /// - `ip` -- [in] The IP address for the snode destination /// - `quic_port` -- [in] The Quic port request for the snode destination /// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination -/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination -/// - `failure_count` -- [in] The number of times requests to this service node have failed LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, const uint8_t ip[4], const uint16_t quic_port, - const char* x25519_pubkey, - const char* ed25519_pubkey, - const uint8_t failure_count); + const char* ed25519_pubkey); /// API: onion_request_builder_set_server_destination /// @@ -85,6 +80,17 @@ LIBSESSION_EXPORT void onion_request_builder_set_server_destination( uint16_t port, const char* x25519_pubkey); +/// API: onion_request_builder_set_destination_pubkey +/// +/// Wrapper around session::onionreq::Builder::set_destination_pubkey. +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `x25519_pubkey` -- [in] The x25519 public key for server (Hex string of exactly 64 +/// characters). +LIBSESSION_EXPORT void onion_request_builder_set_destination_pubkey( + onion_request_builder_object* builder, const char* x25519_pubkey); + /// API: onion_request_builder_add_hop /// /// Wrapper around session::onionreq::Builder::add_hop. ed25519_pubkey and diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 6de08100..af72f5ae 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -1,18 +1,13 @@ #pragma once +#include #include #include -#include "../network_service_node.hpp" #include "key_types.hpp" namespace session::onionreq { -struct SnodeDestination { - session::network::service_node node; - std::optional swarm_pubkey; -}; - struct ServerDestination { std::string protocol; std::string host; @@ -39,7 +34,7 @@ struct ServerDestination { method{std::move(method)} {} }; -using network_destination = std::variant; +using network_destination = std::variant; enum class EncryptType { aes_gcm, @@ -70,8 +65,7 @@ class Builder { void set_enc_type(EncryptType enc_type_) { enc_type = enc_type_; } void set_destination(network_destination destination); - std::optional node_for_destination( - network_destination destination); + void set_destination_pubkey(session::onionreq::x25519_pubkey x25519_pubkey); void add_hop(std::pair keys) { hops_.push_back(keys); } ustring generate_payload(std::optional body) const; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0e91dc9f..07542b91 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -92,7 +92,6 @@ add_libsession_util_library(onionreq onionreq/parser.cpp onionreq/response_parser.cpp network.cpp - network_service_node.cpp ) target_link_libraries(onionreq diff --git a/src/network.cpp b/src/network.cpp index ff7a0f03..22bfe0c3 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -20,8 +20,6 @@ #include "session/ed25519.hpp" #include "session/export.h" #include "session/network.h" -#include "session/network_service_node.h" -#include "session/network_service_node.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" @@ -51,6 +49,9 @@ namespace { // The amount of time the snode cache can be used before it needs to be refreshed const std::chrono::seconds snode_cache_expiration_duration = 2h; + // The amount of time a swarm cache can be used before it needs to be refreshed + const std::chrono::seconds swarm_cache_expiration_duration = (24h * 7); + // The smallest size the snode pool can get to before we need to fetch more. const uint16_t min_snode_pool_count = 12; @@ -76,26 +77,47 @@ namespace { constexpr auto ALPN = "oxenstorage"sv; const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; + service_node node_from_json(nlohmann::json json) { + return {oxenc::from_hex(json["pubkey_ed25519"].get()), + json["ip"].get(), + json["port_omq"].get()}; + } + + std::pair node_from_disk(std::string_view str) { + auto parts = split(str, "|"); + if (parts.size() != 4) + throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); + + return { + { + oxenc::from_hex(parts[2]), // ed25519_pubkey + std::string(parts[0]), // ip + static_cast(std::stoul(std::string{parts[1]})), // port + }, + std::stoul(std::string{parts[3]}) // failure_count + }; + } + const std::vector seed_nodes_testnet{ - service_node{"144.76.164.202|35400|" - "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45|" - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"sv}}; + node_from_disk("144.76.164.202|35400|" + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9|0"sv) + .first}; const std::vector seed_nodes_mainnet{ - service_node{"144.76.164.202|20200|" - "be83fe1221fdd85e4d9d2b62e2a34ba82eaf73da45700185d25aff4575ec6018|" - "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef"sv}, - service_node{"88.99.102.229|20201|" - "05c8c236cf6c4013b8ca930a343fdc62c413ba038a16bb12e75632e0179d404a|" - "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce"sv}, - service_node{"195.16.73.17|20202|" - "22ced8efd4e5faf15531e9b9244b2c1de299342892b97d19268c4db69ab6350f|" - "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f"sv}, - service_node{"104.194.11.120|20203|" - "330ad0d67b58f39a6f46fbeaf5c3622860dfa584e9d787f70c3702031712767a|" - "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b"sv}, - service_node{"104.194.8.115|20204|" - "929c5fc60efa1834a2d4a77a4a33387c1c3d5afc2b192c2ba0e040b29388b216|" - "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0"sv}}; + node_from_disk("144.76.164.202|20200|" + "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef|0"sv) + .first, + node_from_disk("88.99.102.229|20201|" + "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce|0"sv) + .first, + node_from_disk("195.16.73.17|20202|" + "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f|0"sv) + .first, + node_from_disk("104.194.11.120|20203|" + "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b|0"sv) + .first, + node_from_disk("104.194.8.115|20204|" + "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0|0"sv) + .first}; /// rng type that uses llarp::randint(), which is cryptographically secure struct CSRNG { @@ -130,20 +152,43 @@ namespace { return result; } - std::optional node_for_destination( - network_destination destination) { - if (auto* dest = std::get_if(&destination)) - return dest->node; + std::string node_to_disk( + service_node node, std::unordered_map failure_counts) { + auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); - return std::nullopt; + return fmt::format( + "{}|{}|{}|{}", + node.host(), + node.port(), + ed25519_pubkey_hex, + failure_counts.try_emplace(node.to_string(), 0).first->second); + } + + session::onionreq::x25519_pubkey compute_xpk(ustring_view ed25519_pk) { + std::array xpk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk.data())) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to X25519; " + "is the pubkey valid?"}; + return session::onionreq::x25519_pubkey::from_bytes({xpk.data(), 32}); } - std::optional swarm_pubkey_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) - return dest->swarm_pubkey; + std::optional node_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return *dest; return std::nullopt; } + + session::onionreq::x25519_pubkey pubkey_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return compute_xpk(dest->view_remote_key()); + + if (auto* dest = std::get_if(&destination)) + return dest->x25519_pubkey; + + throw std::runtime_error{"Invalid destination."}; + } } // namespace // MARK: Initialization @@ -214,47 +259,85 @@ void Network::load_cache_from_disk() { if (std::filesystem::exists(cache_path + file_snode_pool)) { std::ifstream file{cache_path + file_snode_pool}; std::vector loaded_pool; + std::unordered_map loaded_failure_count; std::string line; while (std::getline(file, line)) { try { - loaded_pool.push_back(std::string_view(line)); + auto [node, failure_count] = node_from_disk(line); + loaded_pool.push_back(node); + loaded_failure_count[node.to_string()] = failure_count; } catch (...) { oxen::log::warning(log_cat, "Skipping invalid entry in snode pool cache."); } } snode_pool = loaded_pool; + snode_failure_counts = loaded_failure_count; } // Load the swarm cache auto swarm_path = (cache_path + swarm_dir); + auto time_now = std::chrono::system_clock::now(); std::unordered_map> loaded_cache; + std::vector caches_to_remove; for (auto& entry : std::filesystem::directory_iterator(swarm_path)) { + // If the pubkey was valid then process the content std::ifstream file{entry.path()}; std::vector nodes; std::string line; + bool checked_swarm_expiration = false; + std::chrono::seconds swarm_lifetime = 0s; while (std::getline(file, line)) { try { - nodes.push_back(std::string_view(line)); + // If we haven't checked if the swarm cache has expired then do so, removing + // any expired/invalid caches + if (!checked_swarm_expiration && line.find('|') != std::string::npos) { + auto swarm_last_updated = + std::chrono::system_clock::from_time_t(std::stoi(line)); + swarm_lifetime = std::chrono::duration_cast( + time_now - swarm_last_updated); + checked_swarm_expiration = true; + + if (swarm_lifetime < swarm_cache_expiration_duration) + throw std::runtime_error{"Expired swarm cache."}; + } + + // Otherwise try to parse as a node + nodes.push_back(node_from_disk(line).first); } catch (...) { - oxen::log::warning(log_cat, "Skipping invalid entry in snode pool cache."); + oxen::log::warning(log_cat, "Skipping invalid or expired entry in swarm cache."); + + // The cache is invalid, we should remove it + if (!checked_swarm_expiration) { + caches_to_remove.emplace_back(entry.path()); + break; + } } } - loaded_cache[entry.path().filename()] = nodes; + // If we got nodes the add it to the cache, otherwise we want to remove it + if (!nodes.empty()) + loaded_cache[entry.path().filename()] = nodes; + else + caches_to_remove.emplace_back(entry.path()); } swarm_cache = loaded_cache; + // Remove any expired cache files + for (auto& cache_path : caches_to_remove) + std::filesystem::remove_all(cache_path); + oxen::log::info( log_cat, "Loaded cache of {} snodes, {} swarms.", snode_pool.size(), swarm_cache.size()); } + void Network::start_disk_write_thread() { std::unique_lock lock{snode_cache_mutex}; while (true) { @@ -264,6 +347,7 @@ void Network::start_disk_write_thread() { // Make local copies so that we can release the lock and not // worry about other threads wanting to change things: auto snode_pool_write = snode_pool; + auto snode_failure_counts_write = snode_failure_counts; auto last_pool_update_write = last_snode_pool_update; auto swarm_cache_write = swarm_cache; @@ -279,7 +363,7 @@ void Network::start_disk_write_thread() { std::filesystem::remove(pool_path + "_new"); std::ofstream file{pool_path + "_new"}; for (auto& snode : snode_pool_write) - file << snode.serialise() << '\n'; + file << node_to_disk(snode, snode_failure_counts_write) << '\n'; std::filesystem::remove(pool_path); std::filesystem::rename(pool_path + "_new", pool_path); @@ -293,12 +377,19 @@ void Network::start_disk_write_thread() { // Write the swarm cache to disk if (need_swarm_write) { + auto time_now = std::chrono::system_clock::now(); + for (auto& [key, swarm] : swarm_cache_write) { auto swarm_path = cache_path + swarm_dir + "/" + key; std::filesystem::remove(swarm_path + "_new"); std::ofstream swarm_file{swarm_path + "_new"}; + + // Write the timestamp to the file + swarm_file << std::chrono::system_clock::to_time_t(time_now) << '\n'; + + // Write the nodes to the file for (auto& snode : swarm) - swarm_file << snode.serialise() << '\n'; + swarm_file << node_to_disk(snode, snode_failure_counts_write) << '\n'; std::filesystem::remove(cache_path + swarm_dir + "/" + key); std::filesystem::rename(swarm_path + "_new", swarm_path); @@ -896,10 +987,7 @@ void Network::get_service_nodes( nlohmann::json params{ {"active_only", true}, {"fields", - {{"public_ip", true}, - {"pubkey_ed25519", true}, - {"pubkey_x25519", true}, - {"storage_lmq_port", true}}}}; + {{"public_ip", true}, {"pubkey_ed25519", true}, {"storage_lmq_port", true}}}}; if (limit) params["limit"] = *limit; @@ -927,8 +1015,13 @@ void Network::get_service_nodes( std::vector result; auto node = result_dict.consume_list_consumer(); - while (!node.is_finished()) - result.emplace_back(node.consume_dict_consumer()); + while (!node.is_finished()) { + auto node_consumer = node.consume_dict_consumer(); + result.emplace_back( + oxenc::from_hex(node_consumer.consume_string()), // pubkey_ed25519 + node_consumer.consume_string(), // public_ip + node_consumer.consume_integer()); // storage_lmq_port + } // Output the result cb(result, std::nullopt); @@ -978,13 +1071,13 @@ void Network::get_version( } void Network::get_swarm( - std::string swarm_pubkey_hex, + session::onionreq::x25519_pubkey swarm_pubkey, std::function swarm)> callback) { auto cached_swarm = - net.call_get([this, swarm_pubkey_hex]() -> std::optional> { - if (!swarm_cache.contains(swarm_pubkey_hex)) + net.call_get([this, swarm_pubkey]() -> std::optional> { + if (!swarm_cache.contains(swarm_pubkey.hex())) return std::nullopt; - return swarm_cache[swarm_pubkey_hex]; + return swarm_cache[swarm_pubkey.hex()]; }); // If we have a cached swarm then return it @@ -992,8 +1085,7 @@ void Network::get_swarm( return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from - with_snode_pool([this, swarm_pubkey_hex, cb = std::move(callback)]( - std::vector pool) { + with_snode_pool([this, swarm_pubkey, cb = std::move(callback)](std::vector pool) { if (pool.empty()) return cb({}); @@ -1002,18 +1094,19 @@ void Network::get_swarm( std::shuffle(updated_pool.begin(), updated_pool.end(), rng); auto node = updated_pool.front(); - nlohmann::json params{{"pubkey", swarm_pubkey_hex}}; + nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; nlohmann::json payload{ {"method", "get_swarm"}, {"params", params}, }; send_onion_request( - SnodeDestination{node, swarm_pubkey_hex}, + node, ustring{oxen::quic::to_usv(payload.dump())}, + swarm_pubkey, oxen::quic::DEFAULT_TIMEOUT, false, - [this, swarm_pubkey_hex, cb = std::move(cb)]( + [this, swarm_pubkey, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { if (!success || timeout || !response) return cb({}); @@ -1028,16 +1121,16 @@ void Network::get_swarm( throw std::runtime_error{"JSON missing swarm field."}; for (auto& snode : response_json["snodes"]) - swarm.emplace_back(snode); + swarm.emplace_back(node_from_json(snode)); } catch (...) { return cb({}); } // Update the cache - net.call([this, swarm_pubkey_hex, swarm]() mutable { + net.call([this, swarm_pubkey, swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey_hex] = swarm; + swarm_cache[swarm_pubkey.hex()] = swarm; need_swarm_write = true; need_write = true; } @@ -1049,6 +1142,19 @@ void Network::get_swarm( }); } +void Network::set_swarm( + session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { + net.call([this, swarm_pubkey, swarm]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[swarm_pubkey.hex()] = swarm; + need_swarm_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + }); +} + void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { with_snode_pool([count, cb = std::move(callback)](std::vector pool) { @@ -1096,13 +1202,19 @@ void Network::send_request( void Network::send_onion_request( network_destination destination, std::optional body, + std::optional swarm_pubkey, std::chrono::milliseconds timeout, bool is_retry, network_response_callback_t handle_response) { with_path( node_for_destination(destination), - [this, destination, body, timeout, is_retry, cb = std::move(handle_response)]( - std::optional path) { + [this, + destination, + body, + swarm_pubkey, + timeout, + is_retry, + cb = std::move(handle_response)](std::optional path) { if (!path) return cb(false, false, -1, "No valid onion paths."); @@ -1110,11 +1222,12 @@ void Network::send_onion_request( // Construct the onion request auto builder = Builder(); builder.set_destination(destination); + builder.set_destination_pubkey(pubkey_for_destination(destination)); for (auto& node : path->nodes) builder.add_hop( {ed25519_pubkey::from_bytes(node.view_remote_key()), - node.x25519_pubkey}); + compute_xpk(node.view_remote_key())}); auto payload = builder.generate_payload(body); auto onion_req_payload = builder.build(payload); @@ -1123,7 +1236,7 @@ void Network::send_onion_request( path->nodes[0], "onion_req", onion_req_payload, - swarm_pubkey_for_destination(destination), + swarm_pubkey, *path, timeout, is_retry}; @@ -1145,7 +1258,7 @@ void Network::send_onion_request( builder.enc_type, response->size())) return handle_errors(info, status_code, response, cb); - if (std::holds_alternative(destination)) + if (std::holds_alternative(destination)) process_snode_response(builder, *response, info, cb); else if (std::holds_alternative(destination)) process_server_response(builder, *response, info, cb); @@ -1354,7 +1467,7 @@ void Network::handle_errors( auto cached_swarm = net.call_get( [this, swarm_pubkey = *info.swarm_pubkey]() -> std::vector { - return swarm_cache[swarm_pubkey]; + return swarm_cache[swarm_pubkey.hex()]; }); if (cached_swarm.empty()) @@ -1381,8 +1494,9 @@ void Network::handle_errors( throw std::invalid_argument{"No other nodes in the swarm."}; return send_onion_request( - SnodeDestination{*random_node, info.swarm_pubkey}, + *random_node, info.body, + info.swarm_pubkey, info.timeout, true, (*handle_response)); @@ -1400,7 +1514,7 @@ void Network::handle_errors( std::vector swarm; for (auto snode : snodes) - swarm.emplace_back(snode); + swarm.emplace_back(node_from_json(snode)); if (swarm.empty()) throw std::invalid_argument{"No snodes in the response."}; @@ -1409,7 +1523,7 @@ void Network::handle_errors( net.call([this, swarm_pubkey = *info.swarm_pubkey, swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey] = swarm; + swarm_cache[swarm_pubkey.hex()] = swarm; need_swarm_write = true; need_write = true; } @@ -1428,6 +1542,8 @@ void Network::handle_errors( } // Check if we got an error specifying the specific node that failed + auto updated_failure_counts = net.call_get( + [this]() -> std::unordered_map { return snode_failure_counts; }); auto updated_path = info.path; bool found_invalid_node = false; @@ -1444,13 +1560,13 @@ void Network::handle_errors( updated_path.nodes.end(), [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); - // Increment the failure count for the snode + // If we found an invalid node then store it to increment the failure count if (snode_it != updated_path.nodes.end()) { - snode_it->failure_count += 1; found_invalid_node = true; - if (snode_it->failure_count >= snode_failure_threshold) - snode_it->invalid = true; + auto failure_count = + updated_failure_counts.try_emplace(snode_it->to_string(), 0).first->second; + updated_failure_counts[snode_it->to_string()] = failure_count + 1; } } } @@ -1458,21 +1574,20 @@ void Network::handle_errors( // If we didn't find the specific node or the paths connection was closed then increment the // path failure count if (!found_invalid_node || !updated_path.conn_info.is_valid()) { - // Increment the path failure count updated_path.failure_count += 1; // If the path has failed too many times we want to drop the guard snode // (marking it as invalid) and increment the failure count of each node in the // path if (updated_path.failure_count >= path_failure_threshold) { - updated_path.nodes[0].invalid = true; - for (auto& it : updated_path.nodes) { - it.failure_count += 1; - - if (it.failure_count >= snode_failure_threshold) - it.invalid = true; + auto failure_count = + updated_failure_counts.try_emplace(it.to_string(), 0).first->second; + updated_failure_counts[it.to_string()] = failure_count + 1; } + + // Set the failure count of the guard node to match the threshold so we drop it + updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; } } @@ -1480,9 +1595,10 @@ void Network::handle_errors( net.call([this, swarm_pubkey = info.swarm_pubkey, old_path = info.path, + updated_failure_counts, updated_path]() mutable { - // Drop the path if it's guard node is invalid - if (updated_path.nodes[0].invalid) { + // Drop the path if invalid + if (updated_path.failure_count >= path_failure_threshold) { oxen::log::info(log_cat, "Dropping path."); paths.erase(std::remove(paths.begin(), paths.end(), old_path), paths.end()); } else @@ -1496,21 +1612,22 @@ void Network::handle_errors( std::lock_guard lock{snode_cache_mutex}; for (size_t i = 0; i < updated_path.nodes.size(); ++i) - if (updated_path.nodes[i].invalid) { + if (updated_failure_counts.try_emplace(updated_path.nodes[i].to_string(), 0) + .first->second >= snode_failure_threshold) { snode_pool.erase( std::remove(snode_pool.begin(), snode_pool.end(), old_path.nodes[i]), snode_pool.end()); if (swarm_pubkey) - if (swarm_cache.contains(*swarm_pubkey)) { - auto updated_swarm = swarm_cache[*swarm_pubkey]; + if (swarm_cache.contains(swarm_pubkey->hex())) { + auto updated_swarm = swarm_cache[swarm_pubkey->hex()]; updated_swarm.erase( std::remove( updated_swarm.begin(), updated_swarm.end(), old_path.nodes[i]), updated_swarm.end()); - swarm_cache[*swarm_pubkey] = updated_swarm; + swarm_cache[swarm_pubkey->hex()] = updated_swarm; } } else std::replace( @@ -1519,8 +1636,9 @@ void Network::handle_errors( old_path.nodes[i], updated_path.nodes[i]); + snode_failure_counts = updated_failure_counts; need_pool_write = true; - need_swarm_write = (swarm_pubkey && swarm_cache.contains(*swarm_pubkey)); + need_swarm_write = (swarm_pubkey && swarm_cache.contains(swarm_pubkey->hex())); need_write = true; } snode_cache_cv.notify_one(); @@ -1530,6 +1648,36 @@ void Network::handle_errors( (*handle_response)(false, false, status_code, response); } +uint8_t Network::get_failure_count(service_node node) { + return net.call_get([this, node]() -> uint8_t { + return snode_failure_counts.try_emplace(node.to_string(), 0).first->second; + }); +} + +void Network::set_failure_count(service_node node, uint8_t failure_count) { + net.call([this, node, failure_count]() mutable { + snode_failure_counts[node.to_string()] = failure_count; + }); +} + +void Network::set_paths(std::vector paths_) { + net.call([this, paths_]() mutable { paths = paths_; }); +} + +uint8_t Network::get_failure_count(onion_path path) { + auto current_paths = net.call_get([this, path]() -> std::vector { return paths; }); + + auto target_path = + std::find_if(current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { + return path_it.nodes[0] == path.nodes[0]; + }); + + if (target_path != current_paths.end()) + return target_path->failure_count; + + return 0; +} + std::vector convert_service_nodes( std::vector nodes) { std::vector converted_nodes; @@ -1537,13 +1685,9 @@ std::vector convert_service_nodes( auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); network_service_node converted_node; std::memcpy(converted_node.ip, node.host().data(), sizeof(converted_node.ip)); - strncpy(converted_node.x25519_pubkey_hex, node.x25519_pubkey.hex().c_str(), 64); strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); - converted_node.x25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.quic_port = node.port(); - converted_node.failure_count = node.failure_count; - converted_node.invalid = node.invalid; converted_nodes.push_back(converted_node); } @@ -1669,7 +1813,8 @@ LIBSESSION_C_API void network_get_swarm( void* ctx) { assert(swarm_pubkey_hex && callback); unbox(network).get_swarm( - swarm_pubkey_hex, [cb = std::move(callback), ctx](std::vector nodes) { + x25519_pubkey::from_hex({swarm_pubkey_hex, 64}), + [cb = std::move(callback), ctx](std::vector nodes) { auto c_nodes = session::network::convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); @@ -1710,22 +1855,20 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( if (body_size > 0) body = {body_, body_size}; - std::optional swarm_pubkey; + std::optional swarm_pubkey; if (swarm_pubkey_hex) - swarm_pubkey = std::string(swarm_pubkey_hex); + swarm_pubkey = x25519_pubkey::from_hex({swarm_pubkey_hex, 64}); std::array ip; std::memcpy(ip.data(), node.ip, ip.size()); unbox(network).send_onion_request( - SnodeDestination{ - {{node.ed25519_pubkey_hex, 64}, - {node.x25519_pubkey_hex, 64}, - "{}"_format(fmt::join(ip, ".")), - node.quic_port, - node.failure_count}, - swarm_pubkey}, + service_node{ + oxenc::from_hex({node.ed25519_pubkey_hex, 64}), + "{}"_format(fmt::join(ip, ".")), + node.quic_port}, body, + swarm_pubkey, std::chrono::milliseconds{timeout_ms}, false, [callback, ctx]( @@ -1780,6 +1923,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers, server.method}, body, + std::nullopt, std::chrono::milliseconds{timeout_ms}, false, [callback, ctx]( diff --git a/src/network_service_node.cpp b/src/network_service_node.cpp deleted file mode 100644 index 6012b3be..00000000 --- a/src/network_service_node.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "session/network_service_node.hpp" - -#include - -#include "session/onionreq/key_types.hpp" - -namespace session::network { - -std::string service_node::serialise() const { - auto ed25519_pubkey_hex = oxenc::to_hex(view_remote_key()); - - return fmt::format( - "{}|{}|{}|{}|{}|{}", - host(), - port(), - x25519_pubkey.hex(), - ed25519_pubkey_hex, - failure_count, - invalid ? "1" : "0"); -} - -} // namespace session::network \ No newline at end of file diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index a8edfda3..9aa6e61e 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -17,7 +17,9 @@ #include #include #include +#include #include +#include #include "session/export.h" #include "session/onionreq/builder.h" @@ -51,13 +53,11 @@ EncryptType parse_enc_type(std::string_view enc_type) { } void Builder::set_destination(network_destination destination) { - destination_x25519_public_key.reset(); ed25519_public_key_.reset(); - if (auto* dest = std::get_if(&destination)) { - destination_x25519_public_key.emplace(dest->node.x25519_pubkey); - ed25519_public_key_.emplace(ed25519_pubkey::from_bytes(dest->node.view_remote_key())); - } else if (auto* dest = std::get_if(&destination)) { + if (auto* dest = std::get_if(&destination)) + ed25519_public_key_.emplace(ed25519_pubkey::from_bytes(dest->view_remote_key())); + else if (auto* dest = std::get_if(&destination)) { host_.emplace(dest->host); endpoint_.emplace(dest->endpoint); protocol_.emplace(dest->protocol); @@ -68,12 +68,15 @@ void Builder::set_destination(network_destination destination) { if (dest->headers) headers_.emplace(*dest->headers); - - destination_x25519_public_key.emplace(dest->x25519_pubkey); } else throw std::invalid_argument{"Invalid destination type."}; } +void Builder::set_destination_pubkey(session::onionreq::x25519_pubkey x25519_pubkey) { + destination_x25519_public_key.reset(); + destination_x25519_public_key.emplace(x25519_pubkey); +} + ustring Builder::generate_payload(std::optional body) const { // If we don't have the data required for a server request, then assume it's targeting a // service node and, therefore, the `body` is the payload @@ -275,22 +278,16 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( onion_request_builder_object* builder, const uint8_t ip[4], const uint16_t quic_port, - const char* x25519_pubkey, - const char* ed25519_pubkey, - const uint8_t failure_count) { - assert(builder && ip && x25519_pubkey && ed25519_pubkey); + const char* ed25519_pubkey) { + assert(builder && ip && ed25519_pubkey); std::array target_ip; std::memcpy(target_ip.data(), ip, target_ip.size()); - auto node = session::network::service_node{ - {ed25519_pubkey, 64}, - {x25519_pubkey, 64}, + unbox(builder).set_destination(oxen::quic::RemoteAddress( + oxenc::from_hex({ed25519_pubkey, 64}), "{}"_format(fmt::join(target_ip, ".")), - quic_port, - failure_count, - false}; - unbox(builder).set_destination(session::onionreq::SnodeDestination{node, std::nullopt}); + quic_port)); } LIBSESSION_C_API void onion_request_builder_set_server_destination( @@ -301,7 +298,7 @@ LIBSESSION_C_API void onion_request_builder_set_server_destination( const char* method, uint16_t port, const char* x25519_pubkey) { - assert(builder && protocol && host && endpoint && x25519_pubkey); + assert(builder && protocol && host && endpoint && protocol && x25519_pubkey); unbox(builder).set_destination(session::onionreq::ServerDestination{ protocol, @@ -313,6 +310,14 @@ LIBSESSION_C_API void onion_request_builder_set_server_destination( method}); } +LIBSESSION_C_API void onion_request_builder_set_destination_pubkey( + onion_request_builder_object* builder, const char* x25519_pubkey) { + assert(builder && x25519_pubkey); + + unbox(builder).set_destination_pubkey( + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); +} + LIBSESSION_C_API void onion_request_builder_add_hop( onion_request_builder_object* builder, const char* ed25519_pubkey, diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 4e30b2ba..162ba421 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -20,3 +20,318 @@ struct Result { }; } // namespace +TEST_CASE("Network error handling", "[network]") { + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto x_pk_hex = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"; + auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; + auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; + auto path = onion_path{{{target}, nullptr, nullptr}, {target}, 0}; + auto mock_request = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + Result result; + auto network = Network(std::nullopt, true, false); + + // Check the handling of the codes which make no changes + auto codes_with_no_changes = {400, 404, 406, 425}; + + for (auto code : codes_with_no_changes) { + network.set_paths({path}); + network.set_failure_count(target, 0); + network.handle_errors( + mock_request, + code, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == code); + CHECK_FALSE(result.response.has_value()); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 0); + } + + // Check general error handling (first failure) + network.set_paths({path}); + network.set_failure_count(target, 0); + network.handle_errors( + mock_request, + 500, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 1); + + // Check general error handling with no response (too many path failures) + path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 9}; + auto mock_request2 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + network.set_paths({path}); + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.handle_errors( + mock_request2, + 500, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 500); + CHECK_FALSE(result.response.has_value()); + CHECK(network.get_failure_count(target) == 3); // Guard node should be set to failure threshold + CHECK(network.get_failure_count(target2) == 1); // Other nodes get their failure count incremented + CHECK(network.get_failure_count(path) == 0); // Path will have been dropped and reset + + // Check general error handling with a path and specific node failure (first failure) + path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; + auto mock_request3 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); + network.set_paths({path}); + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.handle_errors( + mock_request3, + 500, + response, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 500); + CHECK(result.response == response); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 1); + CHECK(network.get_failure_count(path) == 1); // Incremented because conn_info is invalid + + // Check general error handling with a path and specific node failure (too many failures) + auto mock_request4 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + network.set_paths({path}); + network.set_failure_count(target, 0); + network.set_failure_count(target2, 9); + network.handle_errors( + mock_request4, + 500, + response, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 500); + CHECK(result.response == response); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 10); + CHECK(network.get_failure_count(path) == 1); // Incremented because conn_info is invalid + + // Check a 421 with no swarm data throws (no good way to handle this case) + network.set_paths({path}); + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.handle_errors( + mock_request, + 421, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 421); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(path) == 0); + + // Check the retry request of a 421 with no response data is handled like any other error + auto mock_request5 = request_info{target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true}; + network.set_paths({path}); + network.set_failure_count(target, 0); + network.handle_errors( + mock_request5, + 421, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 421); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 1); + + // Check the retry request of a 421 with non-swarm response data is handled like any other error + network.handle_errors( + mock_request5, + 421, + "Test", + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 421); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 1); + + // Check the retry request of a 421 instructs to replace the swarm + auto snodes = nlohmann::json::array(); + snodes.push_back( + {{"ip", "1.1.1.1"}, + {"port_omq", 1}, + {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); + nlohmann::json swarm_json{{"snodes", snodes}}; + response = swarm_json.dump(); + network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target}); + network.set_paths({path}); + network.set_failure_count(target, 0); + network.handle_errors( + mock_request5, + 421, + response, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 421); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 0); + + network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm){ + REQUIRE(swarm.size() == 1); + CHECK(swarm.front().to_string() == "1.1.1.1:1"); + CHECK(oxenc::to_hex(swarm.front().view_remote_key()) == oxenc::to_hex(ed_pk)); + }); +} + +TEST_CASE("Network onion request", "[send_onion_request][network]") { + auto test_service_node = service_node{ + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + "144.76.164.202", + uint16_t{35400}}; + auto network = Network(std::nullopt, true, false); + std::promise result_promise; + + network.send_onion_request( + test_service_node, + ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, + std::nullopt, + oxen::quic::DEFAULT_TIMEOUT, + false, + [&result_promise]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result_promise.set_value({success, timeout, status_code, response}); + }); + + // Wait for the result to be set + auto result = result_promise.get_future().get(); + + CHECK(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 200); + REQUIRE(result.response.has_value()); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); +} + +TEST_CASE("Network direct request C API", "[network_send_request][network]") { + network_object* network; + network_init(&network, nullptr, true, false, nullptr); + std::array target_ip = {144, 76, 164, 202}; + auto test_service_node = network_service_node{}; + test_service_node.quic_port = 35400; + std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); + std::strcpy( + test_service_node.ed25519_pubkey_hex, + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); + auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; + + network_send_onion_request_to_snode_destination( + network, + test_service_node, + body.data(), + body.size(), + nullptr, + oxen::quic::DEFAULT_TIMEOUT.count(), + [](bool success, + bool timeout, + int16_t status_code, + const char* c_response, + size_t response_size, + void* ctx) { + CHECK(success); + CHECK_FALSE(timeout); + CHECK(status_code == 200); + REQUIRE(response_size != 0); + + auto response_str = std::string(c_response, response_size); + REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); + + auto response = nlohmann::json::parse(response_str); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); + network_free(static_cast(ctx)); + }, + network); +} From 587588e8cbfe177e6f702251409b24904701c38e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Apr 2024 17:46:21 +1000 Subject: [PATCH 211/572] Ran the formatter and fixed some CI build errors --- include/session/config.hpp | 1 - include/session/onionreq/builder.hpp | 1 + src/config.cpp | 12 ++++-------- src/config/base.cpp | 2 +- src/config/community.cpp | 1 - src/config/convo_info_volatile.cpp | 7 +++---- src/config/groups/keys.cpp | 2 +- src/network.cpp | 8 +++++--- tests/test_network.cpp | 10 ++++++---- 9 files changed, 21 insertions(+), 23 deletions(-) diff --git a/include/session/config.hpp b/include/session/config.hpp index 1c6cddb4..a9fbe1be 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -381,7 +381,6 @@ class MutableConfigMessage : public ConfigMessage { /// - throws on failure void verify_config_sig( oxenc::bt_dict_consumer dict, - ustring_view config_msg, const ConfigMessage::verify_callable& verifier, std::optional>* verified_signature = nullptr, bool trust_signature = false); diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index af72f5ae..e9c71a17 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "key_types.hpp" diff --git a/src/config.cpp b/src/config.cpp index 8073256b..66ae75eb 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -92,7 +92,6 @@ namespace { auto oldit = old.begin(), newit = new_.begin(); while (oldit != old.end() || newit != new_.end()) { - bool is_new = false; if (oldit == old.end() || (newit != new_.end() && newit->first < oldit->first)) { // newit is a new item; fall through to handle below @@ -117,7 +116,6 @@ namespace { if (o.index() != n.index()) { // The fundamental type (scalar, dict, set) changed, so we'll treat this as a // new value (which implicitly deletes a value of a wrong type when merging). - is_new = true; ++oldit; // fall through to handler below @@ -431,11 +429,9 @@ namespace { void verify_config_sig( oxenc::bt_dict_consumer dict, - ustring_view config_msg, const ConfigMessage::verify_callable& verifier, std::optional>* verified_signature, bool trust_signature) { - ustring_view to_verify, sig; if (dict.skip_until("~")) { dict.consume_signature([&](ustring_view to_verify, ustring_view sig) { if (sig.size() != 64) @@ -558,7 +554,7 @@ ConfigMessage::ConfigMessage( load_unknowns(unknown_, dict, "=", "~"); - verify_config_sig(dict, serialized, verifier, &verified_signature_, trust_signature); + verify_config_sig(dict, verifier, &verified_signature_, trust_signature); } catch (const oxenc::bt_deserialize_invalid& err) { throw config_parse_error{"Failed to parse config file: "s + err.what()}; } @@ -592,12 +588,12 @@ ConfigMessage::ConfigMessage( // prune out redundant messages (i.e. messages already included in another message's diff, and // duplicates) - for (int i = 0; i < configs.size(); i++) { + for (size_t i = 0; i < configs.size(); i++) { auto& [conf, redundant] = configs[i]; if (conf.seqno() > max_seqno) max_seqno = conf.seqno(); - for (int j = 0; !redundant && j < configs.size(); j++) { + for (size_t j = 0; !redundant && j < configs.size(); j++) { if (i == j) continue; const auto& conf2 = configs[j].first; @@ -619,7 +615,7 @@ ConfigMessage::ConfigMessage( if (curr_confs == 1) { // We have just one non-redundant config left after all that, so we become it directly as-is - for (int i = 0; i < configs.size(); i++) { + for (size_t i = 0; i < configs.size(); i++) { if (!configs[i].second) { *this = std::move(configs[i].first); unmerged_ = i; diff --git a/src/config/base.cpp b/src/config/base.cpp index 28d1923c..783ab327 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -205,7 +205,7 @@ std::vector ConfigBase::_merge( // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). int superconf = new_conf->unmerged_index(); // -1 if we had to merge - for (int i = 0; i < all_hashes.size(); i++) { + for (size_t i = 0; i < all_hashes.size(); i++) { if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty()) _old_hashes.emplace(all_hashes[i]); } diff --git a/src/config/community.cpp b/src/config/community.cpp index d6c2ed85..12cb85a9 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -277,6 +277,5 @@ LIBSESSION_C_API void community_make_full_url( auto full = session::config::community::full_url(base_url, room, session::ustring_view{pubkey, 32}); assert(full.size() <= COMMUNITY_FULL_URL_MAX_LENGTH); - size_t pos = 0; std::memcpy(full_url, full.data(), full.size() + 1); } diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 421a8666..52022e11 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -235,10 +235,9 @@ void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { } void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { - const int64_t cutoff = - std::chrono::duration_cast( - (std::chrono::system_clock::now() - PRUNE_HIGH).time_since_epoch()) - .count(); + const int64_t cutoff = std::chrono::duration_cast( + (std::chrono::system_clock::now() - prune).time_since_epoch()) + .count(); std::vector stale; for (auto it = begin_1to1(); it != end(); ++it) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3b034ab..075c4984 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1092,7 +1092,7 @@ bool Keys::load_key_message( } } - verify_config_sig(d, data, verifier_); + verify_config_sig(d, verifier_); // If this is our pending config or this has a later generation than our pending config then // drop our pending status. diff --git a/src/network.cpp b/src/network.cpp index 22bfe0c3..25b842c0 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -88,11 +88,13 @@ namespace { if (parts.size() != 4) throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); + uint16_t port = std::stoul(std::string{parts[1]}); + return { { - oxenc::from_hex(parts[2]), // ed25519_pubkey - std::string(parts[0]), // ip - static_cast(std::stoul(std::string{parts[1]})), // port + oxenc::from_hex(parts[2]), // ed25519_pubkey + std::string(parts[0]), // ip + port, // port }, std::stoul(std::string{parts[3]}) // failure_count }; diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 162ba421..a2412bfe 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -105,8 +105,9 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 3); // Guard node should be set to failure threshold - CHECK(network.get_failure_count(target2) == 1); // Other nodes get their failure count incremented - CHECK(network.get_failure_count(path) == 0); // Path will have been dropped and reset + CHECK(network.get_failure_count(target2) == + 1); // Other nodes get their failure count incremented + CHECK(network.get_failure_count(path) == 0); // Path will have been dropped and reset // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; @@ -183,7 +184,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(path) == 0); // Check the retry request of a 421 with no response data is handled like any other error - auto mock_request5 = request_info{target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true}; + auto mock_request5 = request_info{ + target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true}; network.set_paths({path}); network.set_failure_count(target, 0); network.handle_errors( @@ -250,7 +252,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(path) == 0); - network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm){ + network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm) { REQUIRE(swarm.size() == 1); CHECK(swarm.front().to_string() == "1.1.1.1:1"); CHECK(oxenc::to_hex(swarm.front().view_remote_key()) == oxenc::to_hex(ed_pk)); From aee7ea693eeac1748339e19d57301307e9ed07b6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Apr 2024 17:57:51 +1000 Subject: [PATCH 212/572] Fixed more CI warnings --- src/config/base.cpp | 2 +- src/config/groups/keys.cpp | 4 ---- src/config/protos.cpp | 1 - src/network.cpp | 3 ++- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 783ab327..0bbad4c6 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -204,7 +204,7 @@ std::vector ConfigBase::_merge( // might be our current config, or might be one single one of the new incoming messages). // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). - int superconf = new_conf->unmerged_index(); // -1 if we had to merge + size_t superconf = new_conf->unmerged_index(); // -1 if we had to merge for (size_t i = 0; i < all_hashes.size(); i++) { if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty()) _old_hashes.emplace(all_hashes[i]); diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 075c4984..e5095559 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -598,7 +598,6 @@ ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, boo auto X = session_id_pk(session_id); auto& c = _sign_sk; - auto& C = *_sign_pk; auto k = subaccount_blind_factor(X); @@ -636,9 +635,6 @@ ustring Keys::swarm_subaccount_token(std::string_view session_id, bool write, bo // Similar to the above, but we only care about getting flags || kT auto X = session_id_pk(session_id); - auto& c = _sign_sk; - auto& C = *_sign_pk; - auto k = subaccount_blind_factor(X); // T = |S| diff --git a/src/config/protos.cpp b/src/config/protos.cpp index affcc83c..ef1bcbdc 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -138,7 +138,6 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp if (!req.ParseFromArray(data.data(), data.size())) throw std::runtime_error{"Failed to parse WebSocketMessage"}; - const auto& msg_type = req.type(); if (req.type() != WebSocketProtos::WebSocketMessage_Type_REQUEST) throw std::runtime_error{"Error: received invalid WebSocketRequest"}; diff --git a/src/network.cpp b/src/network.cpp index 25b842c0..30e9a4b5 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -89,6 +89,7 @@ namespace { throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); uint16_t port = std::stoul(std::string{parts[1]}); + uint8_t failure_count = std::stoul(std::string{parts[3]}); return { { @@ -96,7 +97,7 @@ namespace { std::string(parts[0]), // ip port, // port }, - std::stoul(std::string{parts[3]}) // failure_count + failure_count // failure_count }; } From f76bc9e1dce5a185c3e2a0a9c22c447ca95e3fc1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 08:55:20 +1000 Subject: [PATCH 213/572] More tweaks to resolve CI errors (warnings) --- src/config/base.cpp | 2 +- src/config/internal.cpp | 2 +- src/config/user_groups.cpp | 2 +- src/network.cpp | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 0bbad4c6..4a95341c 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -233,7 +233,7 @@ std::vector ConfigBase::_merge( } else { _config = std::move(new_conf); assert(((old_seqno == 0 && mine.empty()) || _config->unmerged_index() >= 1) && - _config->unmerged_index() < all_hashes.size()); + static_cast(_config->unmerged_index()) < all_hashes.size()); set_state(ConfigState::Clean); _curr_hash = all_hashes[_config->unmerged_index()]; } diff --git a/src/config/internal.cpp b/src/config/internal.cpp index c5b4421c..9ad61eba 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -211,7 +211,7 @@ std::optional zstd_decompress(ustring_view data, size_t max_size) { ZSTD_initDStream(zds); ZSTD_inBuffer input{/*.src=*/data.data(), /*.size=*/data.size(), /*.pos=*/0}; std::array out_buf; - ZSTD_outBuffer output{/*.dst=*/out_buf.data(), /*.size=*/out_buf.size()}; + ZSTD_outBuffer output{/*.dst=*/out_buf.data(), /*.size=*/out_buf.size(), /*.pos=*/0}; ustring decompressed; diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 9b08aa0c..576dd3bf 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -287,7 +287,7 @@ std::optional UserGroups::get_community( og.load(*info_dict); if (!pubkey.empty()) og.set_pubkey(pubkey); - return std::move(og); + return og; } return std::nullopt; } diff --git a/src/network.cpp b/src/network.cpp index 30e9a4b5..0aec0bdd 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -292,6 +292,7 @@ void Network::load_cache_from_disk() { std::string line; bool checked_swarm_expiration = false; std::chrono::seconds swarm_lifetime = 0s; + std::string path = entry.path(); while (std::getline(file, line)) { try { @@ -315,7 +316,7 @@ void Network::load_cache_from_disk() { // The cache is invalid, we should remove it if (!checked_swarm_expiration) { - caches_to_remove.emplace_back(entry.path()); + caches_to_remove.emplace_back(path); break; } } @@ -323,9 +324,9 @@ void Network::load_cache_from_disk() { // If we got nodes the add it to the cache, otherwise we want to remove it if (!nodes.empty()) - loaded_cache[entry.path().filename()] = nodes; + loaded_cache[std::string(entry.path().filename())] = nodes; else - caches_to_remove.emplace_back(entry.path()); + caches_to_remove.emplace_back(path); } swarm_cache = loaded_cache; From 5dbf1ac22a677123f89a1ebe2c3bb09afab0b742 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 09:18:55 +1000 Subject: [PATCH 214/572] Further tweaks for CI errors --- include/session/config/user_groups.hpp | 2 +- proto/CMakeLists.txt | 4 ++++ src/config/base.cpp | 5 +++-- src/network.cpp | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index d35855dd..4bf50448 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -235,7 +235,7 @@ struct community_info : base_group_info, community { void load(const dict& info_dict); friend class UserGroups; - friend class comm_iterator_helper; + friend struct comm_iterator_helper; }; using any_group_info = std::variant; diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 18386102..d291bda7 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -59,6 +59,10 @@ set_target_properties( OUTPUT_NAME session-protos SOVERSION ${LIBSESSION_LIBVERSION}) +# -Wunused-parameter triggers in the protobuf dependency +message(STATUS "Disabling -Werror for proto unused-parameter") +target_compile_options(protos PUBLIC -Wno-error=unused-parameter) + libsession_static_bundle(protos) add_library(libsession::protos ALIAS protos) diff --git a/src/config/base.cpp b/src/config/base.cpp index 4a95341c..bd60ce83 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -697,8 +697,9 @@ LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* 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(static_cast(std::count_if(keys.begin(), keys.end(), [](const auto& k) { + return k.size() == 32; + })) == keys.size()); assert(len); *len = keys.size(); if (keys.empty()) diff --git a/src/network.cpp b/src/network.cpp index 0aec0bdd..09d3dcbf 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1902,7 +1902,8 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( size_t response_size, void*), void* ctx) { - assert(callback); + assert(server.method && server.protocol && server.host && server.endpoint && + server.x25519_pubkey && callback); try { std::optional>> headers; From efb526314826bf8c67f72218cf9df02a6f968ef2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 09:45:52 +1000 Subject: [PATCH 215/572] Additional tweaks for CI warnings --- src/network.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 09d3dcbf..61f2b4dc 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -292,7 +292,8 @@ void Network::load_cache_from_disk() { std::string line; bool checked_swarm_expiration = false; std::chrono::seconds swarm_lifetime = 0s; - std::string path = entry.path(); + auto path = entry.path().string(); + auto filename = entry.path().filename().string(); while (std::getline(file, line)) { try { @@ -324,7 +325,7 @@ void Network::load_cache_from_disk() { // If we got nodes the add it to the cache, otherwise we want to remove it if (!nodes.empty()) - loaded_cache[std::string(entry.path().filename())] = nodes; + loaded_cache[filename] = nodes; else caches_to_remove.emplace_back(path); } @@ -1936,8 +1937,16 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( bool timeout, int status_code, std::optional response) { - callback( - success, timeout, status_code, response->data(), response->size(), ctx); + if (response) + callback( + success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); + else + callback(success, timeout, status_code, nullptr, 0, ctx); }); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); From 6d54d8d343e074a8d6e1f2fd99f9e6a1ce0fcc62 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 09:52:40 +1000 Subject: [PATCH 216/572] Missed the 'snode destination' onion request call in the previous commit --- src/network.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 61f2b4dc..4b1ac8ad 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1881,8 +1881,16 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( bool timeout, int status_code, std::optional response) { - callback( - success, timeout, status_code, response->data(), response->size(), ctx); + if (response) + callback( + success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); + else + callback(success, timeout, status_code, nullptr, 0, ctx); }); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); From fdc2a1f855c0cc712e5e5d7b54b91d3e919c5640 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 10:57:30 +1000 Subject: [PATCH 217/572] Pointing at the latest dev libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index f7a02381..826c6db1 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit f7a02381cc1e977614f7edda39f643e012a930d0 +Subproject commit 826c6db13ce52e2ce718a49b1c57b6e5f78e2246 From 0ae1c77a642e239ac4dbf3f33f8e258f50c2617d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 11:05:42 +1000 Subject: [PATCH 218/572] Reverted to a previous `dev` commit --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 826c6db1..6dd9d16e 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 826c6db13ce52e2ce718a49b1c57b6e5f78e2246 +Subproject commit 6dd9d16ef030b2634fd79f438d7b3f2bbe6cb41f From 6c86cc0d374bf0b3372512c31ab6d0d5727be643 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Apr 2024 16:55:12 +1000 Subject: [PATCH 219/572] Added a function to retrieve the current old hashes without pushing --- include/session/config/base.h | 27 +++++++++++++++++++++++++++ include/session/config/base.hpp | 14 +++++++++++++- src/config/base.cpp | 15 +++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/include/session/config/base.h b/include/session/config/base.h index 27741edd..66e098e7 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -289,6 +289,33 @@ LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* #endif ; +/// API: base/config_old_hashes +/// +/// Obtains the known old hashes. Note that this will be empty if there are no old hashes or +/// the config is in a dirty state (in which case these should be retrieved via the `push` +/// function). Calling this function, or the `push` function, will clear the stored old_hashes. +/// +/// The returned pointer belongs to the caller and must be freed via `free()` when done with it. +/// +/// Declaration: +/// ```cpp +/// CONFIG_STRING_LIST* config_old_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_old_hashes(config_object* conf) +#ifdef __GNUC__ + __attribute__((warn_unused_result)) +#endif + ; + /// API: base/config_get_keys /// /// Obtains the current group decryption keys. diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 2cb06e64..e26cb419 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -159,7 +159,7 @@ class ConfigBase : public ConfigSig { std::string _curr_hash; // Contains obsolete known message hashes that are obsoleted by the most recent merge or push; - // these are returned (and cleared) when `push` is called. + // these are returned (and cleared) when `push` or `old_hashes` are called. std::unordered_set _old_hashes; protected: @@ -994,6 +994,18 @@ class ConfigBase : public ConfigSig { /// - `std::vector` -- Returns current config hashes std::vector current_hashes() const; + /// API: base/ConfigBase::old_hashes + /// + /// The old config hash(es); this can be empty if there are no old hashes or if the config is in + /// a dirty state (in which case these should be retrieved via the `push` function). Calling + /// this function or the `push` function will clear the stored old_hashes. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::vector` -- Returns old config hashes + std::vector old_hashes(); + /// API: base/ConfigBase::needs_push /// /// Returns true if this object contains updated data that has not yet been confirmed stored on diff --git a/src/config/base.cpp b/src/config/base.cpp index bd60ce83..32c50066 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -259,6 +259,17 @@ std::vector ConfigBase::current_hashes() const { return hashes; } +std::vector ConfigBase::old_hashes() { + std::vector hashes; + if (!is_dirty()) { + for (auto& old : _old_hashes) + hashes.push_back(std::move(old)); + _old_hashes.clear(); + } + + return hashes; +} + bool ConfigBase::needs_push() const { return !is_clean(); } @@ -695,6 +706,10 @@ LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* return make_string_list(unbox(conf)->current_hashes()); } +LIBSESSION_EXPORT config_string_list* config_old_hashes(config_object* conf) { + return make_string_list(unbox(conf)->old_hashes()); +} + LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size_t* len) { const auto keys = unbox(conf)->get_keys(); assert(static_cast(std::count_if(keys.begin(), keys.end(), [](const auto& k) { From aae9ab6f6eb47710c2fc9b4b8c5aebc4373ed9d5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 30 Apr 2024 15:17:45 -0300 Subject: [PATCH 220/572] Use oxen-encoding via libquic Avoids a duplicate submodule, but also avoids worrying about conflicting versions between the local one and the one in libquic. --- .gitmodules | 3 --- external/CMakeLists.txt | 14 ++++++++------ external/oxen-encoding | 1 - external/oxen-libquic | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) delete mode 160000 external/oxen-encoding diff --git a/.gitmodules b/.gitmodules index 536aad48..ae5089e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "external/oxen-encoding"] - path = external/oxen-encoding - url = https://github.com/oxen-io/oxen-encoding.git [submodule "external/libsodium-internal"] path = external/libsodium-internal url = https://github.com/jagerman/libsodium-internal.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 1d78fa08..df6b2f84 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -26,8 +26,7 @@ if(SUBMODULE_CHECK) message(STATUS "Checking submodules") check_submodule(ios-cmake) check_submodule(libsodium-internal) - check_submodule(oxen-encoding) - check_submodule(oxen-libquic external/oxen-logging) + check_submodule(oxen-libquic external/oxen-logging external/oxen-encoding) check_submodule(nlohmann-json) check_submodule(zstd) endif() @@ -101,13 +100,16 @@ if(CMAKE_CROSSCOMPILING) endif() endif() -set(OXENC_BUILD_TESTS OFF CACHE BOOL "") -set(OXENC_BUILD_DOCS OFF CACHE BOOL "") -system_or_submodule(OXENC oxenc liboxenc>=1.0.10 oxen-encoding) - set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) +if(NOT TARGET oxenc::oxenc) + # The oxenc target will already exist if we load libquic above via submodule + set(OXENC_BUILD_TESTS OFF CACHE BOOL "") + set(OXENC_BUILD_DOCS OFF CACHE BOOL "") + system_or_submodule(OXENC oxenc liboxenc>=1.1.0 external/oxen-libquic/oxen-encoding) +endif() + if(NOT TARGET oxen::logging) add_subdirectory(oxen-libquic/external/oxen-logging) endif() diff --git a/external/oxen-encoding b/external/oxen-encoding deleted file mode 160000 index 9dd7fb39..00000000 --- a/external/oxen-encoding +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9dd7fb39d49666c20f62c6b63399a711fe3ac7f7 diff --git a/external/oxen-libquic b/external/oxen-libquic index 6dd9d16e..826c6db1 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 6dd9d16ef030b2634fd79f438d7b3f2bbe6cb41f +Subproject commit 826c6db13ce52e2ce718a49b1c57b6e5f78e2246 From 67eb146ab9161a5f4aa84aa8e10e77e293aeec40 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 30 Apr 2024 15:31:10 -0300 Subject: [PATCH 221/572] oxenc 1.1.0 compatibility --- tests/catch2_bt_format.hpp | 2 +- tests/test_configdata.cpp | 16 +++++++++------- tests/test_multi_encrypt.cpp | 16 ++++++++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/catch2_bt_format.hpp b/tests/catch2_bt_format.hpp index fa87bae1..ef6942c2 100644 --- a/tests/catch2_bt_format.hpp +++ b/tests/catch2_bt_format.hpp @@ -24,7 +24,7 @@ struct StringMaker { inline std::string StringMaker::convert(const oxenc::bt_value& value) { return var::visit( [](const auto& x) { - return StringMaker>{}.convert(x); + return StringMaker>{}.convert(x); }, static_cast(value)); } diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 0a2b1eed..8acdd35c 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -53,7 +53,7 @@ TEST_CASE("config data dict encoding", "[config][data][dict]") { d["D"] = config::dict{{"x", 1}, {"y", 2}}; d["d"] = config::dict{{"e", config::dict{{"f", config::dict{{"g", ""}}}}}}; - static_assert(oxenc::detail::is_bt_input_dict_container); + static_assert(oxenc::detail::bt_input_dict_container); CHECK(oxenc::bt_serialize(d) == "d1:B1:x1:Dd1:xi1e1:yi2ee1:ai23e1:cli-3ei4e1:11:2e1:dd1:ed1:fd1:g0:eeee"); @@ -202,12 +202,12 @@ TEST_CASE("config message serialization", "[config][serialization]") { "e")); // clang-format on - const auto hash0 = "d65738bba88b0f3455cef20fe09a7b4b10f25f9db82be24a6ce1bd06da197526"_hex; + const std::string hash0{"d65738bba88b0f3455cef20fe09a7b4b10f25f9db82be24a6ce1bd06da197526"_hex}; CHECK(view_hex(m.hash()) == oxenc::to_hex(hash0)); auto m1 = m.increment(); m1.data().erase("foo"); - const auto hash1 = "5b30b4abf4cba71db25dbc0d977cc25df1d0a8a87cad7f561cdec2b8caf65f5e"_hex; + const std::string hash1{"5b30b4abf4cba71db25dbc0d977cc25df1d0a8a87cad7f561cdec2b8caf65f5e"_hex}; CHECK(view_hex(m1.hash()) == oxenc::to_hex(hash1)); auto m2 = m1.increment(); @@ -220,7 +220,7 @@ TEST_CASE("config message serialization", "[config][serialization]") { s(d(m2.data()["bar"])[""]).erase("b"); s(d(m2.data()["bar"])[""]).insert(42); // already present - const auto hash2 = "027552203cf669070d3ecbeecfa65c65497d59aa4da490e0f68f8131ce081320"_hex; + const std::string hash2{"027552203cf669070d3ecbeecfa65c65497d59aa4da490e0f68f8131ce081320"_hex}; CHECK(view_hex(m2.hash()) == oxenc::to_hex(hash2)); // clang-format off @@ -266,16 +266,18 @@ TEST_CASE("config message serialization", "[config][serialization]") { "3:foo" "0:" "e" "e")); + // clang-format on auto m5 = m2.increment().increment().increment(); - const auto hash3 = "b83871ea06587f9254cdf2b2af8daff19bd7fb550fb90d5f8f9f546464c08bc5"_hex; - const auto hash4 = "c30e2cfa7ec93c64a1ab6420c9bccfb63da8e4c2940ed6509ffb64f3f0131860"_hex; - const auto hash5 = "3234eb7da8cf4b79b9eec2a144247279d10f6f118184f82429a42c5996bea60c"_hex; + const std::string hash3{"b83871ea06587f9254cdf2b2af8daff19bd7fb550fb90d5f8f9f546464c08bc5"_hex}, + hash4{"c30e2cfa7ec93c64a1ab6420c9bccfb63da8e4c2940ed6509ffb64f3f0131860"_hex}, + hash5{"3234eb7da8cf4b79b9eec2a144247279d10f6f118184f82429a42c5996bea60c"_hex}; CHECK(view_hex(m2.increment().hash()) == oxenc::to_hex(hash3)); CHECK(view_hex(m2.increment().increment().hash()) == oxenc::to_hex(hash4)); CHECK(view_hex(m5.hash()) == oxenc::to_hex(hash5)); + // clang-format off CHECK(printable(m5.serialize()) == printable( "d" "1:#" "i15e" diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 64e72683..0c679622 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -292,12 +293,15 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim "test suite", nonce); - CHECK(printable(encrypted) == - printable( - "d1:#24:" + "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hex + "1:el" + - "21:" + "e64937e5ea201b84f4e88a976dad900d91caaf6a17"_hex + - "21:" + "bcb642c49c6da03f70cdaab2ed6666721318afd631"_hex + - "21:" + "1ecee2215d226817edfdb097f05037eb799309103a"_hex + "ee")); + CHECK(printable(encrypted) == printable(fmt::format( + "d" + "1:#24:{}" + "1:el21:{}21:{}21:{}e" + "e", + "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hex, + "e64937e5ea201b84f4e88a976dad900d91caaf6a17"_hex, + "bcb642c49c6da03f70cdaab2ed6666721318afd631"_hex, + "1ecee2215d226817edfdb097f05037eb799309103a"_hex))); m1 = session::decrypt_for_multiple_simple( encrypted, From dad43115da8806a8d81de18dbe5fc62ff783962e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 1 May 2024 17:22:24 +1000 Subject: [PATCH 222/572] Tweaked 'with_path' behaviour, added ability to reset/create endpoint --- include/session/network.h | 5 ++ include/session/network.hpp | 10 +++ src/network.cpp | 126 +++++++++++++++++++++++++----------- 3 files changed, 105 insertions(+), 36 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index b87eb63f..1334f1f6 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -94,6 +94,11 @@ LIBSESSION_EXPORT void network_add_logger( void (*callback)( LOG_LEVEL lvl, const char* name, size_t namelen, const char* msg, size_t msglen)); +/// API: network/network_close_connections +/// +/// Closes any currently active connections. +LIBSESSION_EXPORT void network_close_connections(network_object* network); + /// API: network/network_clear_cache /// /// Clears the cached from memory and from disk (if a cache path was provided during diff --git a/include/session/network.hpp b/include/session/network.hpp index 80835fb2..bd5f800e 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -102,6 +102,11 @@ class Network { void(oxen::log::Level lvl, const std::string& name, const std::string& msg)> callback); + /// API: network/close_connections + /// + /// Closes any currently active connections. + void close_connections(); + /// API: network/clear_cache /// /// Clears the cached from memory and from disk (if a cache path was provided during @@ -265,6 +270,11 @@ class Network { /// data exists. void load_cache_from_disk(); + /// API: network/get_endpoint + /// + /// Retrieves or creates a new endpoint pointer. + std::shared_ptr get_endpoint(); + /// API: network/get_connection_info /// /// Creates a connection and opens a stream to the target service node. diff --git a/src/network.cpp b/src/network.cpp index 4b1ac8ad..0a7562de 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -202,7 +202,6 @@ Network::Network(std::optional cache_path, bool use_testnet, bool p cache_path{cache_path.value_or("")} { get_snode_pool_loop = std::make_shared(); build_paths_loop = std::make_shared(); - endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { @@ -423,6 +422,12 @@ void Network::start_disk_write_thread() { } } +void Network::close_connections() { + net.call([this]() mutable { + endpoint.reset(); + }); +} + void Network::clear_cache() { net.call([this]() mutable { { @@ -468,14 +473,27 @@ void Network::update_status(ConnectionStatus updated_status) { status_changed(updated_status); } +std::shared_ptr Network::get_endpoint() { + auto current_endpoint = net.call_get([this] { return endpoint; }); + + if (current_endpoint) + return current_endpoint; + + auto new_endpoint = net.call_get([this]() mutable { + endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); + return endpoint; + }); + + return new_endpoint; +} + connection_info Network::get_connection_info( service_node target, std::optional conn_established_cb) { auto connection_key_pair = ed25519::ed25519_key_pair(); - auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( - std::string(connection_key_pair.second.begin(), connection_key_pair.second.end())); + auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); - auto c = endpoint->connect( + auto c = get_endpoint()->connect( target, creds, oxen::quic::opt::keep_alive{10s}, @@ -670,8 +688,6 @@ void Network::with_snode_pool(std::function pool) void Network::with_path( std::optional excluded_node, std::function path)> callback) { - auto current_paths = net.call_get([this]() -> std::vector { return paths; }); - // Retrieve a random path that doesn't contain the excluded node auto select_valid_path = [](std::optional excluded_node, std::vector paths) -> std::optional { @@ -699,37 +715,71 @@ void Network::with_path( return possible_paths.front(); }; - // If we found a path then return it (also build additional paths in the background if needed) - if (auto target_path = select_valid_path(excluded_node, current_paths)) { - // Build additional paths in the background if we don't have enough - if (current_paths.size() < target_path_count) { - std::thread build_paths_thread( - &Network::build_paths_if_needed, - this, - std::nullopt, - [](std::optional>) {}); - build_paths_thread.detach(); - } - - // If the stream had been closed then try to open a new one - if (!target_path->conn_info.is_valid()) - target_path->conn_info = get_connection_info( - target_path->nodes[0], [this](oxen::quic::connection_interface&) { + std::pair, uint8_t> path_info; + auto [target_path, paths_count] = path_info; + auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + paths_count = current_paths.size(); + target_path = select_valid_path(excluded_node, current_paths); + + // If we found a path but it's connection wasn't valid then we should try to reconnect and block the path building loop + if (target_path && !target_path->conn_info.is_valid()) { + path_info = build_paths_loop->call_get([this, excluded_node, select_valid_path]() mutable -> std::pair, uint8_t> { + // Since this may have been blocked by another thread we should start by trying to get a new target path + auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + + // If we found a path then return it (also build additional paths in the background if needed) + if (auto target_path = select_valid_path(excluded_node, current_paths)) { + // If the stream had been closed then try to open a new stream + if (!target_path->conn_info.is_valid()) { + auto info = get_connection_info(target_path->nodes[0], [this](oxen::quic::connection_interface&) { // If the connection is re-established update the network status back to // connected update_status(ConnectionStatus::connected); }); - return callback(*target_path); + if (!info.is_valid()) + return {std::nullopt, current_paths.size()}; + + auto updated_path = onion_path{std::move(info), std::move(target_path->nodes), 0}; + + // No need to call the 'paths_changed' callback as the paths haven't actually changed, just + // their connection info + auto paths_count = net.call_get([this, target_path, updated_path]() mutable -> uint8_t { + paths.erase(std::remove(paths.begin(), paths.end(), target_path), paths.end()); + paths.emplace_back(updated_path); + return paths.size(); + }); + + return {updated_path, paths_count}; + } + + return {target_path, current_paths.size()}; + } + + return {std::nullopt, current_paths.size()}; + }); } - // If we don't have any paths then build them - build_paths_if_needed( - std::nullopt, - [excluded_node, select_path = std::move(select_valid_path), cb = std::move(callback)]( - std::vector updated_paths) { - cb(select_path(excluded_node, updated_paths)); - }); + // If we didn't get a target path then we have to build paths + if (!target_path) + return build_paths_if_needed( + std::nullopt, + [excluded_node, select_path = std::move(select_valid_path), cb = std::move(callback)]( + std::vector updated_paths) { + cb(select_path(excluded_node, updated_paths)); + }); + + // Build additional paths in the background if we don't have enough + if (paths_count < target_path_count) { + std::thread build_paths_thread( + &Network::build_paths_if_needed, + this, + std::nullopt, + [](std::optional>) {}); + build_paths_thread.detach(); + } + + callback(target_path); } void Network::build_paths_if_needed( @@ -1766,6 +1816,10 @@ LIBSESSION_C_API void network_add_logger( }); } +LIBSESSION_C_API void network_close_connections(network_object* network) { + unbox(network).close_connections(); +} + LIBSESSION_C_API void network_clear_cache(network_object* network) { unbox(network).clear_cache(); } @@ -1876,13 +1930,13 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( swarm_pubkey, std::chrono::milliseconds{timeout_ms}, false, - [callback, ctx]( + [cb = std::move(callback), ctx]( bool success, bool timeout, int status_code, std::optional response) { if (response) - callback( + cb( success, timeout, status_code, @@ -1890,7 +1944,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( (*response).size(), ctx); else - callback(success, timeout, status_code, nullptr, 0, ctx); + cb(success, timeout, status_code, nullptr, 0, ctx); }); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); @@ -1940,13 +1994,13 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( std::nullopt, std::chrono::milliseconds{timeout_ms}, false, - [callback, ctx]( + [cb = std::move(callback), ctx]( bool success, bool timeout, int status_code, std::optional response) { if (response) - callback( + cb( success, timeout, status_code, @@ -1954,7 +2008,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( (*response).size(), ctx); else - callback(success, timeout, status_code, nullptr, 0, ctx); + cb(success, timeout, status_code, nullptr, 0, ctx); }); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); From c7c68fb6b344d431f6a5b7652eab0fd8f7be8286 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 11:27:44 +1000 Subject: [PATCH 223/572] Fixed the get_endpoint and paths changed callback memory --- include/session/network.h | 6 +++--- src/network.cpp | 38 +++++++++++++++++--------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index 1334f1f6..e0272cdc 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -120,11 +120,11 @@ LIBSESSION_EXPORT void network_set_status_changed_callback( /// /// Registers a callback to be called whenever the onion request paths are updated. /// +/// The pointer provided to the callback belongs to the caller and must be freed via `free()` when done with it. +/// /// Inputs: /// - `network` -- [in] Pointer to the network object -/// - `callback` -- [in] callback to be called when the onion request paths are updated. NOTE: The -/// `paths` value will be freed immediately after the callback is triggered so the data needs to be -/// copied if it needs to live longer than this. +/// - `callback` -- [in] callback to be called when the onion request paths are updated. /// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. LIBSESSION_EXPORT void network_set_paths_changed_callback( network_object* network, diff --git a/src/network.cpp b/src/network.cpp index 0a7562de..082bb2fe 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -425,6 +425,7 @@ void Network::start_disk_write_thread() { void Network::close_connections() { net.call([this]() mutable { endpoint.reset(); + update_status(ConnectionStatus::disconnected); }); } @@ -474,17 +475,12 @@ void Network::update_status(ConnectionStatus updated_status) { } std::shared_ptr Network::get_endpoint() { - auto current_endpoint = net.call_get([this] { return endpoint; }); - - if (current_endpoint) - return current_endpoint; + return net.call_get([this] mutable { + if (!endpoint) + endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); - auto new_endpoint = net.call_get([this]() mutable { - endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); return endpoint; }); - - return new_endpoint; } connection_info Network::get_connection_info( @@ -1843,25 +1839,25 @@ LIBSESSION_C_API void network_set_paths_changed_callback( else unbox(network).paths_changed = [cb = std::move(callback), ctx](std::vector> paths) { - std::vector c_paths; - c_paths.reserve(paths.size()); + size_t paths_mem_size = 0; + for (auto& nodes : paths) + paths_mem_size += sizeof(onion_request_path) + (sizeof(network_service_node) * nodes.size()); - for (auto& nodes : paths) { - auto c_nodes = session::network::convert_service_nodes(nodes); + // Allocate the memory for the onion_request_paths* array + auto* c_paths_array = static_cast(std::malloc(paths_mem_size)); + auto* current_pos = c_paths_array; + for (size_t i = 0; i < paths.size(); ++i) { + auto c_nodes = session::network::convert_service_nodes(paths[i]); // Allocate memory that persists outside the loop - network_service_node* c_nodes_array = new network_service_node[c_nodes.size()]; + size_t node_array_size = sizeof(network_service_node) * c_nodes.size(); + auto* c_nodes_array = static_cast(std::malloc(node_array_size)); std::copy(c_nodes.begin(), c_nodes.end(), c_nodes_array); - - onion_request_path c_path{c_nodes_array, c_nodes.size()}; - c_paths.emplace_back(c_path); + new (c_paths_array + i) onion_request_path{c_nodes_array, c_nodes.size()}; + current_pos += sizeof(onion_request_path) + node_array_size; } - cb(c_paths.data(), c_paths.size(), ctx); - - // Free the allocated memory after cb is called - for (auto& c_path : c_paths) - delete[] c_path.nodes; + cb(c_paths_array, paths.size(), ctx); }; } From 6dc24d295801542703fdfc82d86a978181637baa Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 11:31:23 +1000 Subject: [PATCH 224/572] Ran the formatter --- include/session/network.h | 3 +- src/network.cpp | 125 ++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index e0272cdc..1dec2164 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -120,7 +120,8 @@ LIBSESSION_EXPORT void network_set_status_changed_callback( /// /// Registers a callback to be called whenever the onion request paths are updated. /// -/// The pointer provided to the callback belongs to the caller and must be freed via `free()` when done with it. +/// The pointer provided to the callback belongs to the caller and must be freed via `free()` when +/// done with it. /// /// Inputs: /// - `network` -- [in] Pointer to the network object diff --git a/src/network.cpp b/src/network.cpp index 082bb2fe..b14b743c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -477,7 +477,8 @@ void Network::update_status(ConnectionStatus updated_status) { std::shared_ptr Network::get_endpoint() { return net.call_get([this] mutable { if (!endpoint) - endpoint = net.endpoint(oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); + endpoint = net.endpoint( + oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); return endpoint; }); @@ -487,7 +488,8 @@ connection_info Network::get_connection_info( service_node target, std::optional conn_established_cb) { auto connection_key_pair = ed25519::ed25519_key_pair(); - auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); + auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( + from_unsigned_sv(connection_key_pair.second)); auto c = get_endpoint()->connect( target, @@ -717,51 +719,66 @@ void Network::with_path( paths_count = current_paths.size(); target_path = select_valid_path(excluded_node, current_paths); - // If we found a path but it's connection wasn't valid then we should try to reconnect and block the path building loop + // If we found a path but it's connection wasn't valid then we should try to reconnect and block + // the path building loop if (target_path && !target_path->conn_info.is_valid()) { - path_info = build_paths_loop->call_get([this, excluded_node, select_valid_path]() mutable -> std::pair, uint8_t> { - // Since this may have been blocked by another thread we should start by trying to get a new target path - auto current_paths = net.call_get([this]() -> std::vector { return paths; }); - - // If we found a path then return it (also build additional paths in the background if needed) - if (auto target_path = select_valid_path(excluded_node, current_paths)) { - // If the stream had been closed then try to open a new stream - if (!target_path->conn_info.is_valid()) { - auto info = get_connection_info(target_path->nodes[0], [this](oxen::quic::connection_interface&) { - // If the connection is re-established update the network status back to - // connected - update_status(ConnectionStatus::connected); - }); - - if (!info.is_valid()) - return {std::nullopt, current_paths.size()}; - - auto updated_path = onion_path{std::move(info), std::move(target_path->nodes), 0}; - - // No need to call the 'paths_changed' callback as the paths haven't actually changed, just - // their connection info - auto paths_count = net.call_get([this, target_path, updated_path]() mutable -> uint8_t { - paths.erase(std::remove(paths.begin(), paths.end(), target_path), paths.end()); - paths.emplace_back(updated_path); - return paths.size(); - }); - - return {updated_path, paths_count}; - } + path_info = build_paths_loop->call_get( + [this, + excluded_node, + select_valid_path]() mutable -> std::pair, uint8_t> { + // Since this may have been blocked by another thread we should start by trying + // to get a new target path + auto current_paths = + net.call_get([this]() -> std::vector { return paths; }); + + // If we found a path then return it (also build additional paths in the + // background if needed) + if (auto target_path = select_valid_path(excluded_node, current_paths)) { + // If the stream had been closed then try to open a new stream + if (!target_path->conn_info.is_valid()) { + auto info = get_connection_info( + target_path->nodes[0], + [this](oxen::quic::connection_interface&) { + // If the connection is re-established update the network + // status back to connected + update_status(ConnectionStatus::connected); + }); + + if (!info.is_valid()) + return {std::nullopt, current_paths.size()}; + + auto updated_path = + onion_path{std::move(info), std::move(target_path->nodes), 0}; + + // No need to call the 'paths_changed' callback as the paths haven't + // actually changed, just their connection info + auto paths_count = net.call_get( + [this, target_path, updated_path]() mutable -> uint8_t { + paths.erase( + std::remove( + paths.begin(), paths.end(), target_path), + paths.end()); + paths.emplace_back(updated_path); + return paths.size(); + }); + + return {updated_path, paths_count}; + } - return {target_path, current_paths.size()}; - } + return {target_path, current_paths.size()}; + } - return {std::nullopt, current_paths.size()}; - }); + return {std::nullopt, current_paths.size()}; + }); } // If we didn't get a target path then we have to build paths if (!target_path) return build_paths_if_needed( std::nullopt, - [excluded_node, select_path = std::move(select_valid_path), cb = std::move(callback)]( - std::vector updated_paths) { + [excluded_node, + select_path = std::move(select_valid_path), + cb = std::move(callback)](std::vector updated_paths) { cb(select_path(excluded_node, updated_paths)); }); @@ -1841,7 +1858,8 @@ LIBSESSION_C_API void network_set_paths_changed_callback( ctx](std::vector> paths) { size_t paths_mem_size = 0; for (auto& nodes : paths) - paths_mem_size += sizeof(onion_request_path) + (sizeof(network_service_node) * nodes.size()); + paths_mem_size += + sizeof(onion_request_path) + (sizeof(network_service_node) * nodes.size()); // Allocate the memory for the onion_request_paths* array auto* c_paths_array = static_cast(std::malloc(paths_mem_size)); @@ -1851,7 +1869,8 @@ LIBSESSION_C_API void network_set_paths_changed_callback( // Allocate memory that persists outside the loop size_t node_array_size = sizeof(network_service_node) * c_nodes.size(); - auto* c_nodes_array = static_cast(std::malloc(node_array_size)); + auto* c_nodes_array = + static_cast(std::malloc(node_array_size)); std::copy(c_nodes.begin(), c_nodes.end(), c_nodes_array); new (c_paths_array + i) onion_request_path{c_nodes_array, c_nodes.size()}; current_pos += sizeof(onion_request_path) + node_array_size; @@ -1932,13 +1951,12 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( int status_code, std::optional response) { if (response) - cb( - success, - timeout, - status_code, - (*response).c_str(), - (*response).size(), - ctx); + cb(success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); }); @@ -1996,13 +2014,12 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( int status_code, std::optional response) { if (response) - cb( - success, - timeout, - status_code, - (*response).c_str(), - (*response).size(), - ctx); + cb(success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); }); From e49e379fe2f40256d7b57aaca4f211c905096c29 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 11:53:17 +1000 Subject: [PATCH 225/572] Fixed a build error --- src/network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index b14b743c..c658fecf 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -475,7 +475,7 @@ void Network::update_status(ConnectionStatus updated_status) { } std::shared_ptr Network::get_endpoint() { - return net.call_get([this] mutable { + return net.call_get([this]() mutable { if (!endpoint) endpoint = net.endpoint( oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); From 0bacafad4e9d37df040736926fdc969e4a028ff2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 13:37:44 +1000 Subject: [PATCH 226/572] Fixed more issues found by the CI --- src/network.cpp | 2 -- tests/test_network.cpp | 14 +++++++++----- utils/android.sh | 2 -- utils/static-bundle.sh | 2 -- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index c658fecf..a047fb95 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1863,7 +1863,6 @@ LIBSESSION_C_API void network_set_paths_changed_callback( // Allocate the memory for the onion_request_paths* array auto* c_paths_array = static_cast(std::malloc(paths_mem_size)); - auto* current_pos = c_paths_array; for (size_t i = 0; i < paths.size(); ++i) { auto c_nodes = session::network::convert_service_nodes(paths[i]); @@ -1873,7 +1872,6 @@ LIBSESSION_C_API void network_set_paths_changed_callback( static_cast(std::malloc(node_array_size)); std::copy(c_nodes.begin(), c_nodes.end(), c_nodes_array); new (c_paths_array + i) onion_request_path{c_nodes_array, c_nodes.size()}; - current_pos += sizeof(onion_request_path) + node_array_size; } cb(c_paths_array, paths.size(), ctx); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index a2412bfe..cd3ba479 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -288,12 +288,16 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); - auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); - CHECK(response.contains("t")); - CHECK(response.contains("version")); + try { + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); + } catch(...) { + CHECK(*result.response == "{JSON}"); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + } } TEST_CASE("Network direct request C API", "[network_send_request][network]") { diff --git a/utils/android.sh b/utils/android.sh index 444c4e04..b6d67e39 100755 --- a/utils/android.sh +++ b/utils/android.sh @@ -48,8 +48,6 @@ pkg="${archive%%.tar.xz}" mkdir -p "$pkg"/include cp -rv ../include/session "$pkg"/include/ -mkdir -p "$pkg"/include/oxenc -cp -v ../external/oxen-encoding/oxenc/*.h x86_64/external/oxen-encoding/oxenc/version.h "$pkg"/include/oxenc/ for abi in "${abis[@]}"; do mkdir -p "$pkg"/lib/$abi diff --git a/utils/static-bundle.sh b/utils/static-bundle.sh index 7b8ea49d..1b6ad0a9 100755 --- a/utils/static-bundle.sh +++ b/utils/static-bundle.sh @@ -72,8 +72,6 @@ fi mkdir -p "$pkg"/{lib,include} cp -v libsession-util.a "$pkg"/lib cp -rv "$projdir"/include/session "$pkg"/include -mkdir -p "$pkg"/include/oxenc -cp -v "$projdir"/external/oxen-encoding/oxenc/*.h external/oxen-encoding/oxenc/version.h "$pkg"/include/oxenc/ if [ -z "$zip" ]; then tar cvJf "$archive" "$pkg" From 31bf2ba276c3daaabefad58e0002ec549d8fddb5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 14:02:22 +1000 Subject: [PATCH 227/572] Ran the linter, bubble up path building errors --- include/session/network.hpp | 7 +++++-- src/network.cpp | 32 ++++++++++++++++++-------------- tests/test_network.cpp | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index bd5f800e..fd2b20f6 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -310,7 +310,8 @@ class Network { /// unable to find a valid path. void with_path( std::optional excluded_node, - std::function path)> callback); + std::function path, std::optional error)> + callback); /// API: network/build_paths_if_needed /// @@ -324,7 +325,9 @@ class Network { /// unable to create the paths the callback will be triggered with an empty list. void build_paths_if_needed( std::optional excluded_node, - std::function updated_paths)> callback); + std::function< + void(std::vector updated_paths, std::optional error)> + callback); /// API: network/get_service_nodes_recursive /// diff --git a/src/network.cpp b/src/network.cpp index a047fb95..2d0cb1c5 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -216,7 +216,7 @@ Network::Network(std::optional cache_path, bool use_testnet, bool p &Network::build_paths_if_needed, this, std::nullopt, - [](std::optional>) {}); + [](std::optional>, std::optional) {}); build_paths_thread.detach(); } } @@ -685,7 +685,8 @@ void Network::with_snode_pool(std::function pool) void Network::with_path( std::optional excluded_node, - std::function path)> callback) { + std::function path, std::optional error)> + callback) { // Retrieve a random path that doesn't contain the excluded node auto select_valid_path = [](std::optional excluded_node, std::vector paths) -> std::optional { @@ -778,8 +779,9 @@ void Network::with_path( std::nullopt, [excluded_node, select_path = std::move(select_valid_path), - cb = std::move(callback)](std::vector updated_paths) { - cb(select_path(excluded_node, updated_paths)); + cb = std::move(callback)]( + std::vector updated_paths, std::optional error) { + cb(select_path(excluded_node, updated_paths), error); }); // Build additional paths in the background if we don't have enough @@ -788,20 +790,21 @@ void Network::with_path( &Network::build_paths_if_needed, this, std::nullopt, - [](std::optional>) {}); + [](std::optional>, std::optional) {}); build_paths_thread.detach(); } - callback(target_path); + callback(target_path, std::nullopt); } void Network::build_paths_if_needed( std::optional excluded_node, - std::function updated_paths)> callback) { + std::function updated_paths, std::optional error)> + callback) { with_snode_pool([this, excluded_node, cb = std::move(callback)]( std::vector pool) { if (pool.empty()) - return cb({}); + return cb({}, "No snode pool."); build_paths_loop->call([this, excluded_node, pool, cb = std::move(cb)]() mutable { auto current_paths = @@ -809,7 +812,7 @@ void Network::build_paths_if_needed( // No need to do anything if we already have enough paths if (current_paths.size() >= target_path_count) - return cb(current_paths); + return cb(current_paths, std::nullopt); // Update the network status net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); @@ -843,7 +846,7 @@ void Network::build_paths_if_needed( if (possible_guard_nodes.empty()) { oxen::log::info( log_cat, "Unable to build paths due to lack of possible guard nodes."); - return cb({}); + return cb({}, "Unable to build paths due to lack of possible guard nodes."); } // Now that we have a list of possible guard nodes we need to build the paths, first off @@ -953,10 +956,10 @@ void Network::build_paths_if_needed( }); // Trigger the callback with the updated paths - cb(updated_paths); + cb(updated_paths, std::nullopt); } catch (const std::exception& e) { oxen::log::info(log_cat, "Unable to build paths due to error: {}", e.what()); - cb({}); + cb({}, e.what()); } }); }); @@ -1282,9 +1285,10 @@ void Network::send_onion_request( swarm_pubkey, timeout, is_retry, - cb = std::move(handle_response)](std::optional path) { + cb = std::move(handle_response)]( + std::optional path, std::optional error) { if (!path) - return cb(false, false, -1, "No valid onion paths."); + return cb(false, false, -1, error.value_or("No valid onion paths.")); try { // Construct the onion request diff --git a/tests/test_network.cpp b/tests/test_network.cpp index cd3ba479..0cab0b55 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -294,7 +294,7 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { CHECK(response.contains("hf")); CHECK(response.contains("t")); CHECK(response.contains("version")); - } catch(...) { + } catch (...) { CHECK(*result.response == "{JSON}"); REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); } From 4494ebe57360b092191a88b9cb53209740a8cf8a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 14:08:48 +1000 Subject: [PATCH 228/572] Bubble up snode pool errors --- include/session/network.hpp | 4 +++- src/network.cpp | 22 +++++++++++++--------- tests/test_network.cpp | 3 +-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index fd2b20f6..31937170 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -296,7 +296,9 @@ class Network { /// - `callback` -- [in] callback to be triggered once we have the service node pool. NOTE: If /// we are unable to retrieve the service node pool the callback will be triggered with an empty /// list. - void with_snode_pool(std::function pool)> callback); + void with_snode_pool( + std::function pool, std::optional error)> + callback); /// API: network/with_path /// diff --git a/src/network.cpp b/src/network.cpp index 2d0cb1c5..7c961a71 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -520,7 +520,9 @@ connection_info Network::get_connection_info( // MARK: Snode Pool and Onion Path -void Network::with_snode_pool(std::function pool)> callback) { +void Network::with_snode_pool( + std::function pool, std::optional error)> + callback) { get_snode_pool_loop->call([this, cb = std::move(callback)]() mutable { auto current_pool_info = net.call_get( [this]() -> std::pair< @@ -537,7 +539,7 @@ void Network::with_snode_pool(std::function pool) // If the cache has enough snodes and it hasn't expired then return it if (current_pool_info.first.size() >= min_snode_pool_count && !cache_has_expired) - return cb(current_pool_info.first); + return cb(current_pool_info.first, std::nullopt); // Update the network status net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); @@ -591,7 +593,7 @@ void Network::with_snode_pool(std::function pool) }); oxen::log::info(log_cat, "Updated snode pool from seed node."); - return cb(nodes); + return cb(nodes, std::nullopt); } // Pick ~9 random snodes from the current cache to fetch nodes from (we want to @@ -675,10 +677,10 @@ void Network::with_snode_pool(std::function pool) }); oxen::log::info(log_cat, "Updated snode pool."); - cb(updated_pool); + cb(updated_pool, std::nullopt); } catch (const std::exception& e) { oxen::log::info(log_cat, "Failed to get snode pool: {}", e.what()); - cb({}); + cb({}, e.what()); } }); } @@ -802,9 +804,9 @@ void Network::build_paths_if_needed( std::function updated_paths, std::optional error)> callback) { with_snode_pool([this, excluded_node, cb = std::move(callback)]( - std::vector pool) { + std::vector pool, std::optional error) { if (pool.empty()) - return cb({}, "No snode pool."); + return cb({}, error.value_or("No snode pool.")); build_paths_loop->call([this, excluded_node, pool, cb = std::move(cb)]() mutable { auto current_paths = @@ -1156,7 +1158,8 @@ void Network::get_swarm( return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from - with_snode_pool([this, swarm_pubkey, cb = std::move(callback)](std::vector pool) { + with_snode_pool([this, swarm_pubkey, cb = std::move(callback)]( + std::vector pool, std::optional /*error*/) { if (pool.empty()) return cb({}); @@ -1228,7 +1231,8 @@ void Network::set_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { - with_snode_pool([count, cb = std::move(callback)](std::vector pool) { + with_snode_pool([count, cb = std::move(callback)]( + std::vector pool, std::optional /*error*/) { if (pool.size() < count) return cb({}); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 0cab0b55..1c5199c8 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -295,8 +295,7 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { CHECK(response.contains("t")); CHECK(response.contains("version")); } catch (...) { - CHECK(*result.response == "{JSON}"); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + REQUIRE(*result.response == "{VALID JSON}"); } } From 56e75ac289ac325ac26940f94a685ff80e68ca91 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 2 May 2024 14:39:48 +1000 Subject: [PATCH 229/572] Tweaks to try and fix the CI test failures --- src/network.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 7c961a71..7bea326a 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -192,6 +192,21 @@ namespace { throw std::runtime_error{"Invalid destination."}; } + + std::string consume_string(oxenc::bt_dict_consumer dict, std::string_view key) { + if (!dict.skip_until(key)) + throw std::invalid_argument{ + "Unable to find entry in dict for key '" + std::string(key) + "'"}; + return dict.consume_string(); + } + + template + auto consume_integer(oxenc::bt_dict_consumer dict, std::string_view key) { + if (!dict.skip_until(key)) + throw std::invalid_argument{ + "Unable to find entry in dict for key '" + std::string(key) + "'"}; + return dict.next_integer().second; + } } // namespace // MARK: Initialization @@ -1091,9 +1106,9 @@ void Network::get_service_nodes( while (!node.is_finished()) { auto node_consumer = node.consume_dict_consumer(); result.emplace_back( - oxenc::from_hex(node_consumer.consume_string()), // pubkey_ed25519 - node_consumer.consume_string(), // public_ip - node_consumer.consume_integer()); // storage_lmq_port + oxenc::from_hex(consume_string(node_consumer, "pubkey_ed25519")), + consume_string(node_consumer, "public_ip"), + consume_integer(node_consumer, "storage_lmq_port")); } // Output the result From 0b6400dcb5ad112763d47036dfb2e6e1ede79f3c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 7 May 2024 14:07:19 -0300 Subject: [PATCH 230/572] remove comment for removed argument --- include/session/config.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/session/config.hpp b/include/session/config.hpp index a9fbe1be..150d81dd 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -364,8 +364,6 @@ class MutableConfigMessage : public ConfigMessage { /// - `dict` -- a `bt_dict_consumer` positioned at or before the "~" key where the signature is /// expected. (If the bt_dict_consumer has already consumed the "~" key then this call will fail /// as if the signature was missing). -/// - `config_msg` -- the full config message; this must be a view of the same data in memory that -/// `dict` is parsing (i.e. it cannot be a copy). /// - `verifier` -- a callback to invoke to verify the signature of the message. If the callback is /// empty then the signature will be ignored (it is neither required nor verified). /// - `verified_signature` is a pointer to a std::optional array of signature data; if this is From e6c823b6b4565f0bf16fce4e3e404bf1c7342d46 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 8 May 2024 07:54:24 +1000 Subject: [PATCH 231/572] Update src/config/contacts.cpp Co-authored-by: Jason Rhinelander --- src/config/contacts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index ec3fd940..004b2136 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -41,7 +41,7 @@ contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { void contact_info::set_name(std::string n) { if (n.size() > MAX_NAME_LENGTH) - n = n.substr(0, MAX_NAME_LENGTH); + n.resize(MAX_NAME_LENGTH); name = std::move(n); } From c75357dc288f6a48f51350d567831cf008a26628 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 7 May 2024 19:33:55 -0300 Subject: [PATCH 232/572] Replace callback logger; de-header oxen-logging and spdlog The spdlog callback_sink_mt is kind of gross to use: you get passed a reference to an internal struct, and the sink's formatter gets completely ignored (requiring you to maintain your *own* formatter rather than the one that is right there). This updates to a new oxen-logging commit that adds a `formatted_callback_sink` that provides a nicer interface. Other related changes here: - Remove oxen-logging and spdlog use from headers; if they are in the headers then they become obligatory dev packages for libsession-util itself (i.e. code trying to compile against libsession-util has to have oxen-logging and spdlog available, rather than have those remain private implementation details of libsession-util itself). - The above required re-introducing a LogLevel that (somewhat) duplicates oxen::log::Level, but I tried to make it somewhat transparent and auto-converting to be easy to use. (To do all that required making it a struct rather than an enum, but it works somewhat similarly to an enum class). - Move the Network::add_logger out of Network to be a free function (not in the session/logging.hpp header) as the logging system is process-wide and isn't really specific to Network (we'll almost certainly expand it into other parts of libsession-util before too long). This also means you can set up a logger *before* creating a network, to get early-Network log statements as well. --- external/oxen-libquic | 2 +- include/session/config/base.hpp | 7 ++-- include/session/log_level.h | 2 +- include/session/logging.h | 35 +++++++++++++++++++ include/session/logging.hpp | 59 +++++++++++++++++++++++++++++++++ include/session/network.h | 12 ------- include/session/network.hpp | 14 -------- src/CMakeLists.txt | 22 ++++++++---- src/config/base.cpp | 4 +-- src/logging.cpp | 42 +++++++++++++++++++++++ src/network.cpp | 29 +--------------- 11 files changed, 160 insertions(+), 68 deletions(-) create mode 100644 include/session/logging.h create mode 100644 include/session/logging.hpp create mode 100644 src/logging.cpp diff --git a/external/oxen-libquic b/external/oxen-libquic index 826c6db1..d1af022a 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 826c6db13ce52e2ce718a49b1c57b6e5f78e2246 +Subproject commit d1af022a1a866cd4ba365241fc61379a7b0cf09c diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index e26cb419..17b18161 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -13,6 +12,7 @@ #include "base.h" #include "namespaces.hpp" +#include "../logging.hpp" namespace session::config { @@ -186,7 +186,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(oxen::log::Level lvl, std::string msg) { + void log(LogLevel lvl, std::string msg) { if (logger) logger(lvl, std::move(msg)); } @@ -828,7 +828,8 @@ class ConfigBase : public ConfigSig { const DictFieldRoot data{*this}; // If set then we log things by calling this callback - std::function logger; + // TODO: replace this in favour of logging via oxen::logging instead + std::function logger; /// API: base/ConfigBase::storage_namespace /// diff --git a/include/session/log_level.h b/include/session/log_level.h index 7849b2fc..bc941424 100644 --- a/include/session/log_level.h +++ b/include/session/log_level.h @@ -17,4 +17,4 @@ typedef enum LOG_LEVEL { #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/include/session/logging.h b/include/session/logging.h new file mode 100644 index 00000000..507c59a7 --- /dev/null +++ b/include/session/logging.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" +#include "log_level.h" + +/// API: network/network_add_logger_simple +/// +/// Adds a logger to the network object. The callback is invoked with just the log message. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `callback` -- [in] callback to be called when a new message should be logged. +LIBSESSION_EXPORT void network_add_logger_simple(void (*callback)(const char* msg, size_t msglen)); + +/// API: network/network_add_logger_full +/// +/// Adds a logger to the network object. The callback is invoked with the log message, the +/// category name of the log message, and the level of the message. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object +/// - `callback` -- [in] callback to be called when a new message should be logged. + +LIBSESSION_EXPORT void network_add_logger_full(void (*callback)( + const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/logging.hpp b/include/session/logging.hpp new file mode 100644 index 00000000..b5fc0698 --- /dev/null +++ b/include/session/logging.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include "log_level.h" + +// forward declaration +namespace spdlog::level { +enum level_enum : int; +} + +namespace session { + +// This is working roughly like an enum class, but with some useful conversions and comparisons +// defined. We allow implicit conversion to this from a spdlog level_enum, and explicit conversion +// *to* a level_enum, as well as comparison operators (so that, for example, LogLevel::warn >= +// LogLevel::info). +struct LogLevel { + int level; + + LogLevel(spdlog::level::level_enum lvl); + explicit constexpr LogLevel(int lvl) : level{lvl} {} + + explicit operator spdlog::level::level_enum() const; + + static const LogLevel trace; + static const LogLevel debug; + static const LogLevel info; + static const LogLevel warn; + static const LogLevel error; + static const LogLevel critical; + + auto operator<=>(const LogLevel& other) const { return level <=> other.level; } +}; + +inline const LogLevel LogLevel::trace{LOG_LEVEL_TRACE}; +inline const LogLevel LogLevel::debug{LOG_LEVEL_DEBUG}; +inline const LogLevel LogLevel::info{LOG_LEVEL_INFO}; +inline const LogLevel LogLevel::warn{LOG_LEVEL_WARN}; +inline const LogLevel LogLevel::error{LOG_LEVEL_ERROR}; +inline const LogLevel LogLevel::critical{LOG_LEVEL_CRITICAL}; + +/// API: add_logger +/// +/// Adds a logger callback for oxen-logging log messages (such as from the network object). +/// +/// Inputs: +/// - `callback` -- [in] callback to be called when a new message should be logged. This +/// callback must be callable as one of: +/// +/// callback(std::string_view msg) +/// callback(std::string_view msg, std::string_view log_cat, LogLevel level) +/// +void add_logger(std::function cb); +void add_logger( + std::function cb); + +} // namespace session diff --git a/include/session/network.h b/include/session/network.h index 1dec2164..585bf98d 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -82,18 +82,6 @@ LIBSESSION_EXPORT bool network_init( /// - `network` -- [in] Pointer to network_object object LIBSESSION_EXPORT void network_free(network_object* network); -/// API: network/network_add_logger -/// -/// Adds a logger to the network object. -/// -/// Inputs: -/// - `network` -- [in] Pointer to the network object -/// - `callback` -- [in] callback to be called when a new message should be logged. -LIBSESSION_EXPORT void network_add_logger( - network_object* network, - void (*callback)( - LOG_LEVEL lvl, const char* name, size_t namelen, const char* msg, size_t msglen)); - /// API: network/network_close_connections /// /// Closes any currently active connections. diff --git a/include/session/network.hpp b/include/session/network.hpp index 31937170..f7f200ab 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,8 +1,5 @@ #pragma once -#include - -#include #include #include "session/onionreq/builder.hpp" @@ -78,7 +75,6 @@ class Network { std::shared_ptr build_paths_loop; std::shared_ptr endpoint; - spdlog::pattern_formatter formatter; public: // Hook to be notified whenever the network connection status changes. @@ -92,16 +88,6 @@ class Network { Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); ~Network(); - /// API: network/add_logger - /// - /// Adds a logger to the network object. - /// - /// Inputs: - /// - `callback` -- [in] callback to be called when a new message should be logged. - void add_logger(std::function< - void(oxen::log::Level lvl, const std::string& name, const std::string& msg)> - callback); - /// API: network/close_connections /// /// Closes any currently active connections. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 07542b91..3d58f5da 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,8 @@ add_library(common INTERFACE) target_link_libraries(common INTERFACE - oxenc::oxenc) + oxenc::oxenc + oxen::logging) target_include_directories(common INTERFACE ../include) if(WARNINGS_AS_ERRORS) @@ -34,6 +35,12 @@ macro(add_libsession_util_library name) endmacro() + +add_libsession_util_library(util + logging.cpp + util.cpp +) + add_libsession_util_library(crypto blinding.cpp curve25519.cpp @@ -42,7 +49,6 @@ add_libsession_util_library(crypto multi_encrypt.cpp random.cpp session_encrypt.cpp - util.cpp xed25519.cpp ) @@ -67,9 +73,15 @@ add_libsession_util_library(config -target_link_libraries(crypto +target_link_libraries(util PUBLIC common + oxen::logging +) + +target_link_libraries(crypto + PUBLIC + util PRIVATE libsodium::sodium-internal ) @@ -77,12 +89,10 @@ target_link_libraries(crypto target_link_libraries(config PUBLIC crypto - common libsession::protos PRIVATE libsodium::sodium-internal libzstd::static - oxen::logging ) add_libsession_util_library(onionreq @@ -97,10 +107,8 @@ add_libsession_util_library(onionreq target_link_libraries(onionreq PUBLIC crypto - common quic PRIVATE - oxen::logging nlohmann_json::nlohmann_json libsodium::sodium-internal nettle::nettle diff --git a/src/config/base.cpp b/src/config/base.cpp index 32c50066..44923f7b 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -779,8 +779,8 @@ LIBSESSION_EXPORT void config_set_logger( if (!callback) unbox(conf)->logger = nullptr; else - unbox(conf)->logger = [callback, ctx](oxen::log::Level lvl, std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + unbox(conf)->logger = [callback, ctx](LogLevel lvl, std::string msg) { + callback(static_cast(lvl.level), msg.c_str(), ctx); }; } diff --git a/src/logging.cpp b/src/logging.cpp new file mode 100644 index 00000000..ad9341f4 --- /dev/null +++ b/src/logging.cpp @@ -0,0 +1,42 @@ +#include "session/logging.hpp" + +#include +#include +#include +#include + +#include "oxen/log/level.hpp" +#include "session/export.h" + +namespace session { + +LogLevel::LogLevel(spdlog::level::level_enum lvl) : level{static_cast(lvl)} {} + +void add_logger(std::function cb) { + oxen::log::add_sink(std::make_shared(std::move(cb))); +} +void add_logger( + std::function cb) { + oxen::log::add_sink(std::make_shared(std::move(cb))); +} + +} // namespace session + +LIBSESSION_EXPORT void network_add_logger_simple(void (*callback)(const char* msg, size_t msglen)) { + assert(callback); + session::add_logger([cb = callback](std::string_view msg) { cb(msg.data(), msg.size()); }); +} + +LIBSESSION_EXPORT void network_add_logger_full(void (*callback)( + const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)) { + assert(callback); + session::add_logger( + [cb = callback]( + std::string_view msg, std::string_view category, session::LogLevel level) { + cb(msg.data(), + msg.size(), + category.data(), + category.size(), + static_cast(level.level)); + }); +} diff --git a/src/network.cpp b/src/network.cpp index 7bea326a..73cbc490 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -454,20 +453,6 @@ void Network::clear_cache() { }); } -// MARK: Logging - -void Network::add_logger( - std::function - callback) { - auto sink = std::make_shared( - [this, cb = std::move(callback)](const spdlog::details::log_msg& msg) { - spdlog::memory_buf_t buf; - formatter.format(msg, buf); - cb(msg.level, to_string(msg.logger_name), to_string(buf)); - }); - oxen::log::add_sink(sink); -} - // MARK: Connection void Network::update_status(ConnectionStatus updated_status) { @@ -1840,18 +1825,6 @@ LIBSESSION_C_API void network_free(network_object* network) { delete network; } -LIBSESSION_C_API void network_add_logger( - network_object* network, - void (*callback)( - LOG_LEVEL lvl, const char* name, size_t namelen, const char* msg, size_t msglen)) { - assert(callback); - unbox(network).add_logger( - [cb = std::move(callback)]( - oxen::log::Level lvl, const std::string& name, const std::string& msg) { - cb(static_cast(lvl), name.c_str(), name.size(), msg.c_str(), msg.size()); - }); -} - LIBSESSION_C_API void network_close_connections(network_object* network) { unbox(network).close_connections(); } @@ -2049,4 +2022,4 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( } } -} // extern "C" \ No newline at end of file +} // extern "C" From e3a824e7a6ae0e68d15f07adf274c9ee2bf9c177 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 7 May 2024 20:13:49 -0300 Subject: [PATCH 233/572] Mildly simplify hash() implementation --- src/hash.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/hash.cpp b/src/hash.cpp index fd16949f..0b87599c 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -11,27 +11,25 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; - if (key && static_cast(*key).size() > crypto_generichash_blake2b_BYTES_MAX) + if (key && key->size() > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; auto result_code = 0; - unsigned char result[size]; - - if (key) - result_code = crypto_generichash_blake2b( - result, - size, - msg.data(), - msg.size(), - static_cast(*key).data(), - static_cast(*key).size()); - else - result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); + ustring result; + result.resize(size); + + result_code = crypto_generichash_blake2b( + result.data(), + size, + msg.data(), + msg.size(), + key ? key->data() : nullptr, + key ? key->size() : 0); if (result_code != 0) throw std::runtime_error{"Hash generation failed"}; - return {result, size}; + return result; } } // namespace session::hash From c0cd3ee1014348d63d6900dba5d61025c77f558f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 8 May 2024 13:15:49 +1000 Subject: [PATCH 234/572] Renamed the C API for the logger functions, fixed build errors --- include/session/logging.h | 17 +++++++---------- src/CMakeLists.txt | 2 +- src/logging.cpp | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/include/session/logging.h b/include/session/logging.h index 507c59a7..31a009e9 100644 --- a/include/session/logging.h +++ b/include/session/logging.h @@ -4,30 +4,27 @@ extern "C" { #endif -#include +#include #include "export.h" #include "log_level.h" -/// API: network/network_add_logger_simple +/// API: session/session_add_logger_simple /// -/// Adds a logger to the network object. The callback is invoked with just the log message. +/// Registers a callback that is invoked when a message is logged. This callback is invoked with just the log message. /// /// Inputs: -/// - `network` -- [in] Pointer to the network object /// - `callback` -- [in] callback to be called when a new message should be logged. -LIBSESSION_EXPORT void network_add_logger_simple(void (*callback)(const char* msg, size_t msglen)); +LIBSESSION_EXPORT void session_add_logger_simple(void (*callback)(const char* msg, size_t msglen)); -/// API: network/network_add_logger_full +/// API: session/session_add_logger_full /// -/// Adds a logger to the network object. The callback is invoked with the log message, the +/// Registers a callback that is invoked when a message is logged. The callback is invoked with the log message, the /// category name of the log message, and the level of the message. /// /// Inputs: -/// - `network` -- [in] Pointer to the network object /// - `callback` -- [in] callback to be called when a new message should be logged. - -LIBSESSION_EXPORT void network_add_logger_full(void (*callback)( +LIBSESSION_EXPORT void session_add_logger_full(void (*callback)( const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)); #ifdef __cplusplus diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3d58f5da..7a29ec41 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,7 +38,6 @@ endmacro() add_libsession_util_library(util logging.cpp - util.cpp ) add_libsession_util_library(crypto @@ -49,6 +48,7 @@ add_libsession_util_library(crypto multi_encrypt.cpp random.cpp session_encrypt.cpp + util.cpp xed25519.cpp ) diff --git a/src/logging.cpp b/src/logging.cpp index ad9341f4..85e6ed00 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -22,12 +22,12 @@ void add_logger( } // namespace session -LIBSESSION_EXPORT void network_add_logger_simple(void (*callback)(const char* msg, size_t msglen)) { +LIBSESSION_C_API void session_add_logger_simple(void (*callback)(const char* msg, size_t msglen)) { assert(callback); session::add_logger([cb = callback](std::string_view msg) { cb(msg.data(), msg.size()); }); } -LIBSESSION_EXPORT void network_add_logger_full(void (*callback)( +LIBSESSION_C_API void session_add_logger_full(void (*callback)( const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)) { assert(callback); session::add_logger( From 6c4ea3cb32d308d727c04fbd4e0206de9b5bd07a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 8 May 2024 13:15:56 +1000 Subject: [PATCH 235/572] Fixed an issue where the network status wouldn't get updated in some cases --- src/network.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/network.cpp b/src/network.cpp index 73cbc490..1a020de9 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -796,6 +796,9 @@ void Network::with_path( build_paths_thread.detach(); } + // We have a valid path, update the status in case we had flagged it as disconnected for + // some reason + update_status(ConnectionStatus::connected); callback(target_path, std::nullopt); } From 46361347139a84435089391f2d96229ae2aaf1b5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 18:10:23 -0300 Subject: [PATCH 236/572] logging: add level controls; add tests Wrapping these functions allow applications to control the log levels without needing oxen::logging headers to be available. --- .clang-format | 2 +- include/session/logging.h | 38 ++++++++++++++++++++++++--- include/session/logging.hpp | 44 +++++++++++++++++++++++++++++++- src/logging.cpp | 51 ++++++++++++++++++++++++++++++++++--- tests/CMakeLists.txt | 1 + 5 files changed, 126 insertions(+), 10 deletions(-) diff --git a/.clang-format b/.clang-format index 525f5600..5846e195 100644 --- a/.clang-format +++ b/.clang-format @@ -28,7 +28,7 @@ SpacesInAngles: 'false' SpacesInContainerLiterals: 'false' SpacesInParentheses: 'false' SpacesInSquareBrackets: 'false' -Standard: c++17 +Standard: c++20 UseTab: Never SortIncludes: true ColumnLimit: 100 diff --git a/include/session/logging.h b/include/session/logging.h index 31a009e9..9698c59f 100644 --- a/include/session/logging.h +++ b/include/session/logging.h @@ -11,7 +11,8 @@ extern "C" { /// API: session/session_add_logger_simple /// -/// Registers a callback that is invoked when a message is logged. This callback is invoked with just the log message. +/// Registers a callback that is invoked when a message is logged. This callback is invoked with +/// just the log message. /// /// Inputs: /// - `callback` -- [in] callback to be called when a new message should be logged. @@ -19,13 +20,42 @@ LIBSESSION_EXPORT void session_add_logger_simple(void (*callback)(const char* ms /// API: session/session_add_logger_full /// -/// Registers a callback that is invoked when a message is logged. The callback is invoked with the log message, the -/// category name of the log message, and the level of the message. +/// Registers a callback that is invoked when a message is logged. The callback is invoked with the +/// log message, the category name of the log message, and the level of the message. /// /// Inputs: /// - `callback` -- [in] callback to be called when a new message should be logged. LIBSESSION_EXPORT void session_add_logger_full(void (*callback)( - const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)); + const char* msg, size_t msglen, const char* cat, size_t cat_len, LOG_LEVEL level)); + +/// API: session/session_logger_reset_level +/// +/// Resets the log level of all existing category loggers, and sets a new default for any created +/// after this call. If this has not been called, the default log level of category loggers is +/// info. +LIBSESSION_EXPORT void session_logger_reset_level(LOG_LEVEL level); + +/// API: session/session_logger_set_level_default +/// +/// Sets the log level of new category loggers initialized after this call, but does not change the +/// log level of already-initialized category loggers. +LIBSESSION_EXPORT void session_logger_set_level_default(LOG_LEVEL level); + +/// API: session/session_logger_get_level_default +/// +/// Gets the default log level of new loggers (since the last reset_level or set_level_default +/// call). +LIBSESSION_EXPORT LOG_LEVEL session_logger_get_level_default(); + +/// API: session/session_logger_set_level +/// +/// Set the log level of a specific logger category +LIBSESSION_EXPORT void session_logger_set_level(const char* cat_name, LOG_LEVEL level); + +/// API: session/session_logger_get_level +/// +/// Gets the log level of a specific logger category +LIBSESSION_EXPORT LOG_LEVEL session_logger_get_level(const char* cat_name); #ifdef __cplusplus } diff --git a/include/session/logging.hpp b/include/session/logging.hpp index b5fc0698..5f1ef8f0 100644 --- a/include/session/logging.hpp +++ b/include/session/logging.hpp @@ -22,7 +22,10 @@ struct LogLevel { LogLevel(spdlog::level::level_enum lvl); explicit constexpr LogLevel(int lvl) : level{lvl} {} - explicit operator spdlog::level::level_enum() const; + // Returns the log level as an spdlog enum (which is also a oxen::log::Level). + spdlog::level::level_enum spdlog_level() const; + + std::string_view to_string() const; static const LogLevel trace; static const LogLevel debug; @@ -56,4 +59,43 @@ void add_logger(std::function cb); void add_logger( std::function cb); +/// API: session/logger_reset_level +/// +/// Resets the log level of all existing category loggers, and sets a new default for any created +/// after this call. If this has not been called, the default log level of category loggers is +/// info. +/// +/// This function is simply a wrapper around oxen::log::reset_level +void logger_reset_level(LogLevel level); + +/// API: session/logger_set_level_default +/// +/// Sets the log level of new category loggers initialized after this call, but does not change the +/// log level of already-initialized category loggers. +/// +/// This function is simply a wrapper around oxen::log::set_level_default +void logger_set_level_default(LogLevel level); + +/// API: session/logger_get_level_default +/// +/// Gets the default log level of new loggers (since the last reset_level or set_level_default +/// call). +/// +/// This function is simply a wrapper around oxen::log::get_level_default +LogLevel logger_get_level_default(); + +/// API: session/logger_set_level +/// +/// Set the log level of a specific logger category +/// +/// This function is simply a wrapper around oxen::log::set_level +void logger_set_level(std::string cat_name, LogLevel level); + +/// API: session/logger_get_level +/// +/// Gets the log level of a specific logger category +/// +/// This function is simply a wrapper around oxen::log::get_level +LogLevel logger_get_level(std::string cat_name); + } // namespace session diff --git a/src/logging.cpp b/src/logging.cpp index 85e6ed00..091c09ea 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -1,23 +1,50 @@ #include "session/logging.hpp" +#include + #include #include #include -#include #include "oxen/log/level.hpp" #include "session/export.h" namespace session { +namespace log = oxen::log; + LogLevel::LogLevel(spdlog::level::level_enum lvl) : level{static_cast(lvl)} {} +spdlog::level::level_enum LogLevel::spdlog_level() const { + return static_cast(level); +} + +std::string_view LogLevel::to_string() const { + return log::to_string(spdlog_level()); +} + void add_logger(std::function cb) { - oxen::log::add_sink(std::make_shared(std::move(cb))); + log::add_sink(std::make_shared(std::move(cb))); } void add_logger( std::function cb) { - oxen::log::add_sink(std::make_shared(std::move(cb))); + log::add_sink(std::make_shared(std::move(cb))); +} + +void logger_reset_level(LogLevel level) { + log::reset_level(level.spdlog_level()); +} +void logger_set_level_default(LogLevel level) { + log::set_level_default(level.spdlog_level()); +} +LogLevel logger_get_level_default() { + return log::get_level_default(); +} +void logger_set_level(std::string cat_name, LogLevel level) { + log::set_level(std::move(cat_name), level.spdlog_level()); +} +LogLevel logger_get_level(std::string cat_name) { + return log::get_level(std::move(cat_name)); } } // namespace session @@ -28,7 +55,7 @@ LIBSESSION_C_API void session_add_logger_simple(void (*callback)(const char* msg } LIBSESSION_C_API void session_add_logger_full(void (*callback)( - const char* msg, size_t msglen, const char* cat, size_t can_len, LOG_LEVEL level)) { + const char* msg, size_t msglen, const char* cat, size_t cat_len, LOG_LEVEL level)) { assert(callback); session::add_logger( [cb = callback]( @@ -40,3 +67,19 @@ LIBSESSION_C_API void session_add_logger_full(void (*callback)( static_cast(level.level)); }); } + +LIBSESSION_C_API void session_logger_reset_level(LOG_LEVEL level) { + oxen::log::reset_level(static_cast(level)); +} +LIBSESSION_C_API void session_logger_set_level_default(LOG_LEVEL level) { + oxen::log::set_level_default(static_cast(level)); +} +LIBSESSION_C_API LOG_LEVEL session_logger_get_level_default() { + return static_cast(oxen::log::get_level_default()); +} +LIBSESSION_C_API void session_logger_set_level(const char* cat_name, LOG_LEVEL level) { + oxen::log::set_level(cat_name, static_cast(level)); +} +LIBSESSION_C_API LOG_LEVEL session_logger_get_level(const char* cat_name) { + return static_cast(oxen::log::get_level(cat_name)); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8b1a3f21..46e9cb4d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(testAll test_group_info.cpp test_group_members.cpp test_hash.cpp + test_logging.cpp test_multi_encrypt.cpp test_network.cpp test_onionreq.cpp From a97e510ae0303249a714989854ae3581b197c216 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 18:33:15 -0300 Subject: [PATCH 237/572] C network API doc tweaks & typedef - Remove some removed fields from the API comment - Use a typedef for the snode/server req callback function (since it is the same for both calls), and document its fields. --- include/session/network.h | 60 ++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index 585bf98d..3ce4d88d 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -155,6 +155,27 @@ LIBSESSION_EXPORT void network_get_random_nodes( void (*callback)(network_service_node*, size_t, void*), void* ctx); +/// API: network/network_onion_response_callback_t +/// +/// Function pointer typedef for the callback function pointer given to +/// network_send_onion_request_to_snode_destination and +/// network_send_onion_request_to_server_destination. +/// +/// Fields: +/// - `success` -- true if the request was successful, false if it failed. +/// - `timeout` -- true if the request failed because of a timeout +/// - `status_code` -- the HTTP numeric status code of the request, e.g. 200 for OK +/// - `response` -- pointer to the beginning of the response body +/// - `response_size` -- length of the response body +/// - `ctx` -- the context pointer passed to the function that initiated the request. +typedef void (*network_onion_response_callback_t)( + bool success, + bool timeout, + int16_t status_code, + const char* response, + size_t response_size, + void* ctx); + /// API: network/network_send_onion_request_to_snode_destination /// /// Sends a request via onion routing to the provided service node. @@ -166,7 +187,8 @@ LIBSESSION_EXPORT void network_get_random_nodes( /// - `body_size` -- [in] size of the `body`. /// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. /// - `callback` -- [in] callback to be called with the result of the request. -/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. +/// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set to +/// NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( network_object* network, const network_service_node node, @@ -174,13 +196,7 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( size_t body_size, const char* swarm_pubkey_hex, int64_t timeout_ms, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - void*), + network_onion_response_callback_t callback, void* ctx); /// API: network/network_send_onion_request_to_server_destination @@ -189,40 +205,20 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( /// /// Inputs: /// - `network` -- [in] Pointer to the network object. -/// - `method` -- [in] the HTTP method to use for performing the request on the server. -/// - `protocol` -- [in] the protocol to use for performing the request on the server. -/// - `host` -- [in] the server host. -/// - `endpoint` -- [in] the endpoint to call on the server. -/// - `port` -- [in] the port to send the request to on the server. -/// - `x25519_pubkey` -- [in] the x25519 pubkey of the server. -/// - `query_param_keys` -- [in] array of keys for any query params to send to the server, must be -/// the same size as `query_param_values`. Set to NULL if unused. -/// - `query_param_values` -- [in] array of values for any query params to send to the server, must -/// be the same size as `query_param_keys`. Set to NULL if unused. -/// - `query_params_size` -- [in] The number of query params provided. -/// - `headers` -- [in] array of keys for any headers to send to the server, must be the same size -/// as `header_values`. Set to NULL if unused. -/// - `header_values` -- [in] array of values for any headers to send to the server, must be the -/// same size as `headers`. Set to NULL if unused. -/// - `headers_size` -- [in] The number of headers provided. +/// - `server` -- [in] struct containing information about the server the request should be sent to. /// - `body` -- [in] data to send to the specified endpoint. /// - `body_size` -- [in] size of the `body`. /// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. /// - `callback` -- [in] callback to be called with the result of the request. -/// - `ctx` -- [in, optional] Pointer to an optional context. Set to NULL if unused. +/// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set +/// to NULL if unused. LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( network_object* network, const network_server_destination server, const unsigned char* body, size_t body_size, int64_t timeout_ms, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - void*), + network_onion_response_callback_t callback, void* ctx); #ifdef __cplusplus From 5f24da229fb22bdfc010f5cc63fcc57a693a7020 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 18:39:14 -0300 Subject: [PATCH 238/572] Remove unneeded specific values We don't care about the specific numeric values underlying this enum, so don't need to give them. (It'll effectively be the same under the hood, but by not giving them we signal that we just want different values, but don't care what they are underneath, just that they are distinct). --- include/session/network.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index f7f200ab..87c3ef12 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -13,10 +13,10 @@ using network_response_callback_t = std::function response)>; enum class ConnectionStatus { - unknown = 0, - connecting = 1, - connected = 2, - disconnected = 3, + unknown, + connecting, + connected, + disconnected, }; struct connection_info { From 64e996d962c49b14a182cf25a4c429b093167f69 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 18:47:53 -0300 Subject: [PATCH 239/572] Re-join disk writing thread during destruction If we don't wait for the disk thread to exit then we could end up destroying the Network instance, but the disk writing thread also depends on that instance and so we could end up destroying its referenced `this` object out from under it. This tweaks it to store the std::thread in Network rather than detaching it so that we can re-join it (which waits for it to finish) before we complete the Network destruction. This also renames the disk thread function slight to `disk_write_thread_loop` (from `start_disk_write_thread`) because the function doesn't actually *start* the thread: it is just the infinite loop body that the thread runs forever. --- include/session/network.hpp | 11 +++++++---- src/network.cpp | 7 ++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 87c3ef12..763e3712 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -68,6 +68,8 @@ class Network { std::chrono::system_clock::time_point last_snode_pool_update; std::unordered_map> swarm_cache; + std::thread disk_write_thread; + ConnectionStatus status; oxen::quic::Network net; std::vector paths; @@ -244,11 +246,12 @@ class Network { /// - 'updated_status' - [in] the updated connection status. void update_status(ConnectionStatus updated_status); - /// API: network/start_disk_write_thread + /// API: network/disk_write_thread_loop /// - /// Starts the disk write thread which monitors a number of private variables and persists the - /// snode pool and swarm caches to disk if a `cache_path` was provided during initialization. - void start_disk_write_thread(); + /// Body of the disk writer which runs until signalled to stop. This is intended to run in its + /// own thread. The thread monitors a number of private variables and persists the snode pool + /// and swarm caches to disk if a `cache_path` was provided during initialization. + void disk_write_thread_loop(); /// API: network/load_cache_from_disk /// diff --git a/src/network.cpp b/src/network.cpp index 1a020de9..afa60f7b 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -220,8 +220,7 @@ Network::Network(std::optional cache_path, bool use_testnet, bool p // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { load_cache_from_disk(); - std::thread disk_write_thread(&Network::start_disk_write_thread, this); - disk_write_thread.detach(); + disk_write_thread = std::thread{&Network::disk_write_thread_loop, this}; } // Kick off a separate thread to build paths (may as well kick this off early) @@ -241,6 +240,8 @@ Network::~Network() { shut_down_disk_thread = true; } snode_cache_cv.notify_one(); + if (disk_write_thread.joinable()) + disk_write_thread.join(); } // MARK: Cache Management @@ -356,7 +357,7 @@ void Network::load_cache_from_disk() { swarm_cache.size()); } -void Network::start_disk_write_thread() { +void Network::disk_write_thread_loop() { std::unique_lock lock{snode_cache_mutex}; while (true) { snode_cache_cv.wait(lock, [this] { return need_write || shut_down_disk_thread; }); From 99dab9969ba3d337351a66f5d960daaa1ff580fd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 19:05:04 -0300 Subject: [PATCH 240/572] `#include` cleanups - prefer `` to `"session/blah.hpp"` in include/ headers (that will get installed on the system) for referencing "full" header names, and use `"blah.hpp"` for relative header includes paths. - remove some unused headers clang-tidy was warning about --- include/session/config/groups/info.h | 1 - include/session/config/groups/keys.hpp | 2 -- include/session/config/profile_pic.hpp | 2 +- include/session/config/protos.hpp | 2 +- include/session/network.hpp | 6 +++--- include/session/onionreq/builder.h | 3 ++- include/session/onionreq/parser.hpp | 7 ++++--- 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 32da3ae9..24651457 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -6,7 +6,6 @@ extern "C" { #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; diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index f7dd59ff..54054816 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -1,13 +1,11 @@ #pragma once #include -#include #include #include "../../config.hpp" #include "../base.hpp" #include "../namespaces.hpp" -#include "../profile_pic.hpp" #include "members.hpp" namespace session::config::groups { diff --git a/include/session/config/profile_pic.hpp b/include/session/config/profile_pic.hpp index 59601a14..605276be 100644 --- a/include/session/config/profile_pic.hpp +++ b/include/session/config/profile_pic.hpp @@ -2,7 +2,7 @@ #include -#include "session/types.hpp" +#include namespace session::config { diff --git a/include/session/config/protos.hpp b/include/session/config/protos.hpp index d9eec29b..c05182d4 100644 --- a/include/session/config/protos.hpp +++ b/include/session/config/protos.hpp @@ -1,7 +1,7 @@ #pragma once #include "namespaces.hpp" -#include "session/util.hpp" +#include namespace session::config::protos { diff --git a/include/session/network.hpp b/include/session/network.hpp index 763e3712..e901d471 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -2,9 +2,9 @@ #include -#include "session/onionreq/builder.hpp" -#include "session/onionreq/key_types.hpp" -#include "session/types.hpp" +#include "onionreq/builder.hpp" +#include "onionreq/key_types.hpp" +#include "types.hpp" namespace session::network { diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index cf011495..40fb003f 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -6,6 +6,7 @@ extern "C" { #include #include +#include #include "../export.h" @@ -159,4 +160,4 @@ LIBSESSION_EXPORT bool onion_request_builder_build( #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 8d2d290e..d1e7208b 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -1,7 +1,8 @@ -#include +#pragma once -#include "session/onionreq/hop_encryption.hpp" -#include "session/types.hpp" +#include + +#include "hop_encryption.hpp" namespace session::onionreq { From b41b469287b20551ee5c11f19261ee46cc3712dd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 19:07:45 -0300 Subject: [PATCH 241/572] Minor API doc wording tweaks --- include/session/network.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index e901d471..7ba7d315 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -330,8 +330,8 @@ class Network { /// drained. /// - `limit` -- [in, optional] the number of service nodes to retrieve. /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If we drain the - /// `target_nodes` and haven't gotten a successful response an empty list will be returned along - /// with an error string. + /// `target_nodes` and haven't gotten a successful response then the callback will be invoked + /// with an empty vector and an error string. void get_service_nodes_recursive( std::vector target_nodes, std::optional limit, @@ -347,8 +347,8 @@ class Network { /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's /// drained. /// - `callback` -- [in] callback to be triggered once we make a successful request. NOTE: If - /// we drain the `target_nodes` and haven't gotten a successful response a NULL - /// `connection_info` and empty will be provided. + /// we drain the `target_nodes` and haven't gotten a successful response then the callback will + /// be invoked with a std::nullopt `valid_guard_node` and `unused_nodes`. void find_valid_guard_node_recursive( std::vector target_nodes, std::function< @@ -363,7 +363,7 @@ class Network { /// - `node` -- [in] node to retrieve the service nodes from. /// - `limit` -- [in, optional] the number of service nodes to retrieve. /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If an error - /// occurs an empty list and an error will be returned. + /// occurs an empty list and an error will be provided. void get_service_nodes( service_node node, std::optional limit, @@ -379,7 +379,7 @@ class Network { /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the /// `quic::DEFAULT_TIMEOUT` will be used. /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an - /// error occurs an empty list and an error will be returned. + /// error occurs an empty list and an error will be provided. void get_version( service_node node, std::optional timeout, From 55785833c0418f35021461abd16a0b87927ae2a4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 19:24:18 -0300 Subject: [PATCH 242/572] Replace crappy config logger with oxen-logging --- include/session/config/base.h | 32 ----------------- include/session/config/base.hpp | 12 +------ include/session/config/profile_pic.hpp | 3 +- include/session/config/protos.hpp | 3 +- src/config/base.cpp | 49 ++++++++++++-------------- src/network.cpp | 48 ++++++++++++------------- src/onionreq/builder.cpp | 5 ++- tests/test_config_userprofile.cpp | 13 ------- 8 files changed, 51 insertions(+), 114 deletions(-) diff --git a/include/session/config/base.h b/include/session/config/base.h index 66e098e7..1bd6cc86 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -10,7 +10,6 @@ extern "C" { #include "../config.h" #include "../export.h" -#include "../log_level.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. @@ -43,37 +42,6 @@ typedef struct config_object { /// - `conf` -- [in] Pointer to config_object object LIBSESSION_EXPORT void config_free(config_object* conf); -/// 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(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(*)(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)(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. diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 17b18161..f25bbc3f 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -10,9 +10,9 @@ #include #include +#include "../logging.hpp" #include "base.h" #include "namespaces.hpp" -#include "../logging.hpp" namespace session::config { @@ -185,12 +185,6 @@ class ConfigBase : public ConfigSig { // deleted at the next push. 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)); - } - // 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. MutableConfigMessage& dirty(); @@ -827,10 +821,6 @@ class ConfigBase : public ConfigSig { // Proxy class providing read and write access to the contained config data. const DictFieldRoot data{*this}; - // If set then we log things by calling this callback - // TODO: replace this in favour of logging via oxen::logging instead - std::function logger; - /// API: base/ConfigBase::storage_namespace /// /// Accesses the storage namespace where this config type is to be stored/loaded from. See diff --git a/include/session/config/profile_pic.hpp b/include/session/config/profile_pic.hpp index 605276be..93259c95 100644 --- a/include/session/config/profile_pic.hpp +++ b/include/session/config/profile_pic.hpp @@ -1,8 +1,7 @@ #pragma once -#include - #include +#include namespace session::config { diff --git a/include/session/config/protos.hpp b/include/session/config/protos.hpp index c05182d4..f7d1371d 100644 --- a/include/session/config/protos.hpp +++ b/include/session/config/protos.hpp @@ -1,8 +1,9 @@ #pragma once -#include "namespaces.hpp" #include +#include "namespaces.hpp" + namespace session::config::protos { /// API: config/protos::wrap_config diff --git a/src/config/base.cpp b/src/config/base.cpp index 44923f7b..e398d4a2 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -21,9 +22,14 @@ #include "session/util.hpp" using namespace std::literals; +using namespace oxen::log::literals; namespace session::config { +namespace log = oxen::log; + +auto cat = log::Cat("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"}; @@ -133,17 +139,17 @@ std::vector ConfigBase::_merge( plaintexts.emplace_back(hash, decrypt(conf, key(i), encryption_domain())); decrypted = true; } catch (const decrypt_error&) { - log(oxen::log::Level::debug, - "Failed to decrypt message " + std::to_string(ci) + " using key " + - std::to_string(i)); + log::debug(cat, "Failed to decrypt message {} using key {}", ci, i); } } if (!decrypted) - log(oxen::log::Level::warn, "Failed to decrypt message " + std::to_string(ci)); + log::warning(cat, "Failed to decrypt message {}", ci); } - log(oxen::log::Level::debug, - "successfully decrypted " + std::to_string(plaintexts.size()) + " of " + - std::to_string(configs.size()) + " incoming messages"); + log::debug( + cat, + "successfully decrypted {} of {} incoming messages", + plaintexts.size(), + configs.size()); for (auto& [hash, plain] : plaintexts) { // Remove prefix padding: @@ -152,13 +158,13 @@ std::vector ConfigBase::_merge( plain.resize(plain.size() - p); } if (plain.empty()) { - log(oxen::log::Level::err, "Invalid config message: contains no data"); + log::error(cat, "Invalid config message: contains no data"); continue; } // TODO FIXME (see above) if (plain[0] == 'm') { - log(oxen::log::Level::warn, "multi-part messages not yet supported!"); + log::warning(cat, "multi-part messages not yet supported!"); continue; } @@ -169,17 +175,18 @@ std::vector ConfigBase::_merge( decompressed && !decompressed->empty()) plain = std::move(*decompressed); else { - log(oxen::log::Level::warn, "Invalid config message: decompression failed"); + log::warning(cat, "Invalid config message: decompression failed"); continue; } } if (plain[0] != 'd') - log(oxen::log::Level::err, - "invalid/unsupported config message with type " + - (plain[0] >= 0x20 && plain[0] <= 0x7e - ? "'" + std::string{from_unsigned_sv(plain.substr(0, 1))} + "'" - : "0x" + oxenc::to_hex(plain.begin(), plain.begin() + 1))); + log::error( + cat, + "invalid/unsupported config message with type {}", + (plain[0] >= 0x20 && plain[0] <= 0x7e + ? "'{}'"_format(static_cast(plain[0])) + : "0x{:02x}"_format(plain[0]))); all_hashes.emplace_back(hash); all_confs.emplace_back(plain); @@ -194,7 +201,7 @@ std::vector ConfigBase::_merge( _config->signer, config_lags(), [&](size_t i, const config_error& e) { - log(oxen::log::Level::warn, e.what()); + log::warning(cat, "{}", e.what()); assert(i > 0); // i == 0 means we can't deserialize our own serialization bad_confs.insert(i); }); @@ -774,14 +781,4 @@ 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)(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(lvl.level), msg.c_str(), ctx); - }; -} - } // extern "C" diff --git a/src/network.cpp b/src/network.cpp index afa60f7b..f1892ed7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -33,9 +33,11 @@ using namespace oxen::log::literals; namespace session::network { +namespace log = oxen::log; + namespace { - inline auto log_cat = oxen::log::Cat("network"); + inline auto cat = log::Cat("network"); class status_code_exception : public std::runtime_error { public: @@ -285,7 +287,7 @@ void Network::load_cache_from_disk() { loaded_pool.push_back(node); loaded_failure_count[node.to_string()] = failure_count; } catch (...) { - oxen::log::warning(log_cat, "Skipping invalid entry in snode pool cache."); + log::warning(cat, "Skipping invalid entry in snode pool cache."); } } @@ -327,7 +329,7 @@ void Network::load_cache_from_disk() { // Otherwise try to parse as a node nodes.push_back(node_from_disk(line).first); } catch (...) { - oxen::log::warning(log_cat, "Skipping invalid or expired entry in swarm cache."); + log::warning(cat, "Skipping invalid or expired entry in swarm cache."); // The cache is invalid, we should remove it if (!checked_swarm_expiration) { @@ -350,11 +352,7 @@ void Network::load_cache_from_disk() { for (auto& cache_path : caches_to_remove) std::filesystem::remove_all(cache_path); - oxen::log::info( - log_cat, - "Loaded cache of {} snodes, {} swarms.", - snode_pool.size(), - swarm_cache.size()); + log::info(cat, "Loaded cache of {} snodes, {} swarms.", snode_pool.size(), swarm_cache.size()); } void Network::disk_write_thread_loop() { @@ -391,7 +389,7 @@ void Network::disk_write_thread_loop() { std::filesystem::remove(cache_path + file_snode_pool_updated); std::ofstream timestamp_file{cache_path + file_snode_pool_updated}; timestamp_file << std::chrono::system_clock::to_time_t(last_pool_update_write); - oxen::log::debug(log_cat, "Finished writing snode pool cache to disk."); + log::debug(cat, "Finished writing snode pool cache to disk."); } // Write the swarm cache to disk @@ -413,7 +411,7 @@ void Network::disk_write_thread_loop() { std::filesystem::remove(cache_path + swarm_dir + "/" + key); std::filesystem::rename(swarm_path + "_new", swarm_path); } - oxen::log::debug(log_cat, "Finished writing swarm cache to disk."); + log::debug(cat, "Finished writing swarm cache to disk."); } need_pool_write = false; @@ -565,7 +563,7 @@ void Network::with_snode_pool( // If we don't have enough nodes in the current cached pool then we need to fetch from // the seed nodes if (current_pool_info.first.size() < min_snode_pool_count) { - oxen::log::info(log_cat, "Fetching from seed nodes."); + log::info(cat, "Fetching from seed nodes."); target_pool = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); // Just in case, make sure the seed nodes are have values @@ -593,7 +591,7 @@ void Network::with_snode_pool( snode_cache_cv.notify_one(); }); - oxen::log::info(log_cat, "Updated snode pool from seed node."); + log::info(cat, "Updated snode pool from seed node."); return cb(nodes, std::nullopt); } @@ -603,7 +601,7 @@ void Network::with_snode_pool( std::shuffle(target_pool.begin(), target_pool.end(), rng); size_t num_retries = std::min(target_pool.size() / 3, static_cast(3)); - oxen::log::info(log_cat, "Fetching from random expired cache nodes."); + log::info(cat, "Fetching from random expired cache nodes."); std::vector first_nodes( target_pool.begin(), target_pool.begin() + num_retries); std::vector second_nodes( @@ -677,10 +675,10 @@ void Network::with_snode_pool( snode_cache_cv.notify_one(); }); - oxen::log::info(log_cat, "Updated snode pool."); + log::info(cat, "Updated snode pool."); cb(updated_pool, std::nullopt); } catch (const std::exception& e) { - oxen::log::info(log_cat, "Failed to get snode pool: {}", e.what()); + log::info(cat, "Failed to get snode pool: {}", e.what()); cb({}, e.what()); } }); @@ -824,7 +822,7 @@ void Network::build_paths_if_needed( net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); // Get the possible guard nodes - oxen::log::info(log_cat, "Building paths."); + log::info(cat, "Building paths."); std::vector nodes_to_exclude; std::vector possible_guard_nodes; @@ -850,8 +848,7 @@ void Network::build_paths_if_needed( }); if (possible_guard_nodes.empty()) { - oxen::log::info( - log_cat, "Unable to build paths due to lack of possible guard nodes."); + log::info(cat, "Unable to build paths due to lack of possible guard nodes."); return cb({}, "Unable to build paths due to lack of possible guard nodes."); } @@ -942,8 +939,7 @@ void Network::build_paths_if_needed( std::back_inserter(node_descriptions), [](service_node& node) { return node.to_string(); }); auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - oxen::log::info( - log_cat, "Built new onion request path: [{}]", path_description); + log::info(cat, "Built new onion request path: [{}]", path_description); } // Paths were successfully built, update the connection status @@ -964,7 +960,7 @@ void Network::build_paths_if_needed( // Trigger the callback with the updated paths cb(updated_paths, std::nullopt); } catch (const std::exception& e) { - oxen::log::info(log_cat, "Unable to build paths due to error: {}", e.what()); + log::info(cat, "Unable to build paths due to error: {}", e.what()); cb({}, e.what()); } }); @@ -1007,7 +1003,7 @@ void Network::find_valid_guard_node_recursive( return callback(std::nullopt, {}); auto target_node = target_nodes.front(); - oxen::log::info(log_cat, "Testing guard snode: {}", target_node.to_string()); + log::info(cat, "Testing guard snode: {}", target_node.to_string()); get_version( target_node, @@ -1030,13 +1026,13 @@ void Network::find_valid_guard_node_recursive( throw std::runtime_error{ "Outdated node version ({})"_format(fmt::join(version, "."))}; - oxen::log::info(log_cat, "Guard snode {} valid.", target_node.to_string()); + log::info(cat, "Guard snode {} valid.", target_node.to_string()); cb(info, remaining_nodes); } catch (const std::exception& e) { // Log the error and loop after a slight delay (don't want to drain the pool // too quickly if the network goes down) - oxen::log::info( - log_cat, + log::info( + cat, "Testing {} failed with error: {}", target_node.to_string(), e.what()); @@ -1679,7 +1675,7 @@ void Network::handle_errors( updated_path]() mutable { // Drop the path if invalid if (updated_path.failure_count >= path_failure_threshold) { - oxen::log::info(log_cat, "Dropping path."); + log::info(cat, "Dropping path."); paths.erase(std::remove(paths.begin(), paths.end(), old_path), paths.end()); } else std::replace(paths.begin(), paths.end(), old_path, updated_path); diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 9aa6e61e..4a403b1f 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -1,5 +1,6 @@ #include "session/onionreq/builder.hpp" +#include #include #include #include @@ -14,10 +15,8 @@ #include #include -#include #include #include -#include #include #include @@ -360,4 +359,4 @@ LIBSESSION_C_API bool onion_request_builder_build( return false; } } -} \ No newline at end of file +} diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index f5c34633..5774ab57 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -12,16 +12,6 @@ using namespace std::literals; using namespace oxenc::literals; -void log_msg(LOG_LEVEL lvl, const char* msg, void*) { - INFO((lvl == LOG_LEVEL_CRITICAL ? "CRITICAL" - : lvl == LOG_LEVEL_ERROR ? "ERROR" - : lvl == LOG_LEVEL_WARN ? "Warning" - : lvl == LOG_LEVEL_INFO ? "Info" - : lvl == LOG_LEVEL_DEBUG ? "debug" - : "trace") - << ": " << msg); -} - TEST_CASE("user profile C API", "[config][user_profile][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; @@ -44,8 +34,6 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { rc = user_profile_init(&conf, ed_sk.data(), NULL, 0, err); REQUIRE(rc == 0); - config_set_logger(conf, log_msg, NULL); - // We don't need to push anything, since this is an empty config CHECK_FALSE(config_needs_push(conf)); // And we haven't changed anything so don't need to dump to db @@ -208,7 +196,6 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // 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)); // Now imagine we just pulled down the encrypted string from the swarm; we merge it into conf2: From 8d3adffee598b8b44d22f93b2bf1454d74991b4f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 19:47:01 -0300 Subject: [PATCH 243/572] Change unmerged_index() to optional Having it as an `int` with -1 meaning merged causes trouble with various other code using it needing to compare it with size_t values. Since it really is an index, an optional seems more appropriate and avoids all the issues. --- include/session/config.hpp | 12 ++++++------ src/config.cpp | 2 +- src/config/base.cpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/session/config.hpp b/include/session/config.hpp index 150d81dd..eaf418e5 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -95,10 +95,10 @@ class ConfigMessage { std::optional> verified_signature_; // This will be set during construction from configs based on the merge result: - // -1 means we had to merge one or more configs together into a new merged config - // >= 0 indicates the index of the config we used if we did not merge (i.e. there was only one + // nullopt means we had to merge one or more configs together into a new merged config + // If set to a value then the value is the index of the config we used (i.e. there was only one // config, or there were multiple but one of them referenced all the others). - int unmerged_ = -1; + std::optional unmerged_; public: constexpr static int DEFAULT_DIFF_LAGS = 5; @@ -203,13 +203,13 @@ class ConfigMessage { /// After loading multiple config files this flag indicates whether or not we had to produce a /// new, merged configuration message (true) or did not need to merge (false). (For config /// messages that were not loaded from serialized data this is always true). - bool merged() const { return unmerged_ == -1; } + bool merged() const { return !unmerged_; } /// After loading multiple config files this field contains the index of the single config we /// used if we didn't need to merge (that is: there was only one config or one config that /// superceded all the others). If we had to merge (or this wasn't loaded from serialized - /// data), this will return -1. - int unmerged_index() const { return unmerged_; } + /// data), this will return std::nullopt. + const std::optional& unmerged_index() const { return unmerged_; } /// Read-only access to the optional verified signature if this message contained a valid, /// verified signature when it was parsed. Returns nullopt otherwise (e.g. not loaded from diff --git a/src/config.cpp b/src/config.cpp index 66ae75eb..2972d9fb 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -643,7 +643,7 @@ ConfigMessage::ConfigMessage( return; } - unmerged_ = -1; + unmerged_ = std::nullopt; // Clear any redundant messages. (we do it *here* rather than above because, in the // single-good-config case, above, we need the index of the good config for `unmerged_`). diff --git a/src/config/base.cpp b/src/config/base.cpp index e398d4a2..0eb47b5b 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -211,7 +211,7 @@ std::vector ConfigBase::_merge( // might be our current config, or might be one single one of the new incoming messages). // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). - size_t superconf = new_conf->unmerged_index(); // -1 if we had to merge + std::optional superconf = new_conf->unmerged_index(); // nullopt if we had to merge for (size_t i = 0; i < all_hashes.size(); i++) { if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty()) _old_hashes.emplace(all_hashes[i]); @@ -240,9 +240,9 @@ std::vector ConfigBase::_merge( } else { _config = std::move(new_conf); assert(((old_seqno == 0 && mine.empty()) || _config->unmerged_index() >= 1) && - static_cast(_config->unmerged_index()) < all_hashes.size()); + _config->unmerged_index() < all_hashes.size()); set_state(ConfigState::Clean); - _curr_hash = all_hashes[_config->unmerged_index()]; + _curr_hash = all_hashes[*_config->unmerged_index()]; } } else { // the merging affect nothing (if it had seqno would have been incremented), so don't From afa6d7578ac238994f6fb33514d95160136a2981 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 21:31:56 -0300 Subject: [PATCH 244/572] Fix flakey oxen-logging source dir stripping oxen-logging had a cmake cache variable, but it doesn't really work properly (especially when multiple projects are trying to set it). This updates to a newer oxen-logging that replaces the cache variable with a function to add the source dir reliably. --- external/CMakeLists.txt | 2 +- external/oxen-libquic | 2 +- tests/CMakeLists.txt | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index df6b2f84..90de09e5 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -113,7 +113,7 @@ endif() if(NOT TARGET oxen::logging) add_subdirectory(oxen-libquic/external/oxen-logging) endif() -set(OXEN_LOGGING_SOURCE_ROOT "${OXEN_LOGGING_SOURCE_ROOT};${PROJECT_SOURCE_DIR}" CACHE INTERNAL "") +oxen_logging_add_source_dir("${PROJECT_SOURCE_DIR}") if(CMAKE_C_COMPILER_LAUNCHER) set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") diff --git a/external/oxen-libquic b/external/oxen-libquic index d1af022a..d969b876 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit d1af022a1a866cd4ba365241fc61379a7b0cf09c +Subproject commit d969b8760916998933f328dadfa69ef359c92055 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46e9cb4d..1fd82e5f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,7 @@ target_link_libraries(testAll PRIVATE libsession::onionreq libsodium::sodium-internal nlohmann_json::nlohmann_json + oxen::logging Catch2Wrapper) if(NOT TARGET check) From 94393237a01ef2d46091711beb04de9c7cc8ede6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 21:40:45 -0300 Subject: [PATCH 245/572] Add fix for test case premature timeout `oxen::quic::DEFAULT_TIMEOUT` is a std::chrono::seconds, so its `.count()` is a seconds value, but the C API expects milliseconds here. --- tests/test_network.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 1c5199c8..c867d016 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -317,7 +317,7 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { body.data(), body.size(), nullptr, - oxen::quic::DEFAULT_TIMEOUT.count(), + std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), [](bool success, bool timeout, int16_t status_code, @@ -330,6 +330,7 @@ TEST_CASE("Network direct request C API", "[network_send_request][network]") { REQUIRE(response_size != 0); auto response_str = std::string(c_response, response_size); + INFO("response_str is: " << response_str); REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); auto response = nlohmann::json::parse(response_str); From a8012aa80a92b14ed9da935e4255ce7080a5853a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 21:44:35 -0300 Subject: [PATCH 246/572] Use libquic's (relatively new) string_view-accepting opt::alpns --- src/network.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index f1892ed7..1cfe5fd0 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -76,7 +76,6 @@ namespace { constexpr auto node_not_found_prefix = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; - const ustring uALPN{reinterpret_cast(ALPN.data()), ALPN.size()}; service_node node_from_json(nlohmann::json json) { return {oxenc::from_hex(json["pubkey_ed25519"].get()), @@ -477,7 +476,7 @@ std::shared_ptr Network::get_endpoint() { return net.call_get([this]() mutable { if (!endpoint) endpoint = net.endpoint( - oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{{uALPN}}); + oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{ALPN}); return endpoint; }); From c8f0c837e74e59f091280f5f714550d8dbfb063f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 23:14:15 -0300 Subject: [PATCH 247/572] More input validity checks - oxenc::from_hex doesn't validate input (and requires that the input be checked with oxenc::is_hex if not already known to be hex). - Switch to quic::parse_int for string-to-integer parsing as it's a big nicer than stoi in various input weird edge cases. --- src/network.cpp | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 1cfe5fd0..afbee42c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -25,16 +25,13 @@ #include "session/onionreq/response_parser.hpp" #include "session/util.hpp" -using namespace session; +using namespace oxen; using namespace session::onionreq; -using namespace session::network; using namespace std::literals; using namespace oxen::log::literals; namespace session::network { -namespace log = oxen::log; - namespace { inline auto cat = log::Cat("network"); @@ -78,7 +75,11 @@ namespace { constexpr auto ALPN = "oxenstorage"sv; service_node node_from_json(nlohmann::json json) { - return {oxenc::from_hex(json["pubkey_ed25519"].get()), + auto pk_ed = json["pubkey_ed25519"].get(); + if (pk_ed.size() != 64 || !oxenc::is_hex(pk_ed)) + throw std::invalid_argument{ + "Invalid service node json: pubkey_ed25519 is not a valid, hex pubkey"}; + return {oxenc::from_hex(pk_ed), json["ip"].get(), json["port_omq"].get()}; } @@ -86,10 +87,18 @@ namespace { std::pair node_from_disk(std::string_view str) { auto parts = split(str, "|"); if (parts.size() != 4) - throw std::invalid_argument("Invalid service node serialisation: " + std::string(str)); + throw std::invalid_argument("Invalid service node serialisation: {}"_format(str)); + if (parts[2].size() != 64 || !oxenc::is_hex(parts[2])) + throw std::invalid_argument{ + "Invalid service node serialisation: pubkey is not hex or has wrong size"}; + + uint16_t port; + if (!quic::parse_int(parts[1], port)) + throw std::invalid_argument{"Invalid service node serialization: invalid port"}; - uint16_t port = std::stoul(std::string{parts[1]}); - uint8_t failure_count = std::stoul(std::string{parts[3]}); + uint8_t failure_count; + if (!quic::parse_int(parts[3], failure_count)) + throw std::invalid_argument{"Invalid service node serialization: invalid port"}; return { { From 0a70ba14db4ef7fe233400d987bdce4e5ec546ea Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 23:17:51 -0300 Subject: [PATCH 248/572] Drop `oxen::` prefix from various things This file has a `using namespace oxen` now and dropping it reduces verbosity (especially because most things in the oxen:: namespace are subnamespaces already, like `log::` and `quic::`, so it doesn't really pollute by doing so). --- src/network.cpp | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index afbee42c..ce95bc47 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -153,7 +153,7 @@ namespace { auto v_s = session::split(vers, "."); std::vector result; for (const auto& piece : v_s) - if (!oxen::quic::parse_int(piece, result.emplace_back())) + if (!quic::parse_int(piece, result.emplace_back())) throw std::invalid_argument{"Invalid version"}; // Remove any trailing `0` values (but ensure we at least end up with a "0" version) @@ -186,14 +186,14 @@ namespace { } std::optional node_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) + if (auto* dest = std::get_if(&destination)) return *dest; return std::nullopt; } session::onionreq::x25519_pubkey pubkey_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) + if (auto* dest = std::get_if(&destination)) return compute_xpk(dest->view_remote_key()); if (auto* dest = std::get_if(&destination)) @@ -224,8 +224,8 @@ Network::Network(std::optional cache_path, bool use_testnet, bool p use_testnet{use_testnet}, should_cache_to_disk{cache_path}, cache_path{cache_path.value_or("")} { - get_snode_pool_loop = std::make_shared(); - build_paths_loop = std::make_shared(); + get_snode_pool_loop = std::make_shared(); + build_paths_loop = std::make_shared(); // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { @@ -481,11 +481,10 @@ void Network::update_status(ConnectionStatus updated_status) { status_changed(updated_status); } -std::shared_ptr Network::get_endpoint() { +std::shared_ptr Network::get_endpoint() { return net.call_get([this]() mutable { if (!endpoint) - endpoint = net.endpoint( - oxen::quic::Address{"0.0.0.0", 0}, oxen::quic::opt::alpns{ALPN}); + endpoint = net.endpoint(quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); return endpoint; }); @@ -493,17 +492,17 @@ std::shared_ptr Network::get_endpoint() { connection_info Network::get_connection_info( service_node target, - std::optional conn_established_cb) { + std::optional conn_established_cb) { auto connection_key_pair = ed25519::ed25519_key_pair(); - auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( - from_unsigned_sv(connection_key_pair.second)); + auto creds = + quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); auto c = get_endpoint()->connect( target, creds, - oxen::quic::opt::keep_alive{10s}, + quic::opt::keep_alive{10s}, conn_established_cb, - [this, target](oxen::quic::connection_interface& conn, uint64_t) { + [this, target](quic::connection_interface& conn, uint64_t) { // When the connection is closed, update the path and connection status auto target_path = std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { @@ -522,7 +521,7 @@ connection_info Network::get_connection_info( } }); - return {target, c, c->open_stream()}; + return {target, c, c->open_stream()}; } // MARK: Snode Pool and Onion Path @@ -747,8 +746,7 @@ void Network::with_path( // If the stream had been closed then try to open a new stream if (!target_path->conn_info.is_valid()) { auto info = get_connection_info( - target_path->nodes[0], - [this](oxen::quic::connection_interface&) { + target_path->nodes[0], [this](quic::connection_interface&) { // If the connection is re-established update the network // status back to connected update_status(ConnectionStatus::connected); @@ -1078,9 +1076,7 @@ void Network::get_service_nodes( payload.append("params", params.dump()); info.stream->command( - "oxend_request", - payload.view(), - [this, cb = std::move(callback)](oxen::quic::message resp) { + "oxend_request", payload.view(), [this, cb = std::move(callback)](quic::message resp) { try { auto [status_code, body] = validate_response(resp, true); @@ -1128,7 +1124,7 @@ void Network::get_version( "info", payload.view(), timeout, - [this, info, cb = std::move(callback)](oxen::quic::message resp) { + [this, info, cb = std::move(callback)](quic::message resp) { try { auto [status_code, body] = validate_response(resp, true); @@ -1184,9 +1180,9 @@ void Network::get_swarm( send_onion_request( node, - ustring{oxen::quic::to_usv(payload.dump())}, + ustring{quic::to_usv(payload.dump())}, swarm_pubkey, - oxen::quic::DEFAULT_TIMEOUT, + quic::DEFAULT_TIMEOUT, false, [this, swarm_pubkey, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { @@ -1260,17 +1256,17 @@ void Network::send_request( if (!conn_info.is_valid()) return handle_response(false, false, -1, "Network is unreachable."); - oxen::quic::bstring_view payload{}; + quic::bstring_view payload{}; if (info.body) - payload = oxen::quic::bstring_view{ + payload = quic::bstring_view{ reinterpret_cast(info.body->data()), info.body->size()}; conn_info.stream->command( info.endpoint, payload, info.timeout, - [this, info, cb = std::move(handle_response)](oxen::quic::message resp) { + [this, info, cb = std::move(handle_response)](quic::message resp) { try { auto [status_code, body] = validate_response(resp, false); cb(true, false, status_code, body); @@ -1459,8 +1455,7 @@ void Network::process_server_response( // MARK: Error Handling -std::pair Network::validate_response( - oxen::quic::message resp, bool is_bencoded) { +std::pair Network::validate_response(quic::message resp, bool is_bencoded) { std::string body = resp.body_str(); if (resp.timed_out) From 5492941b1a9688135d0128c8cd1c23162ea52e1f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 23:19:04 -0300 Subject: [PATCH 249/572] Remove custom comparison for Address compare Address (and RemoteAddress) already have a built-in `<` operator that does pretty much what this custom comparator does. (Although I found a weirdness in it that sorts the port in an unexpected way -- https://github.com/oxen-io/oxen-libquic/pull/128 fixes that. That fix isn't strictly necessary here, though, because here we just need unique ordering, but the weird ordering in current libquic is still unique). --- src/network.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index ce95bc47..c5b3c5f3 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -631,18 +631,11 @@ void Network::with_snode_pool( auto second_result_nodes = prom2.get_future().get(); auto third_result_nodes = prom3.get_future().get(); - auto compare_nodes = [](const auto& a, const auto& b) { - if (a.host() == b.host()) { - return a.port() < b.port(); - } - return a.host() < b.host(); - }; - // Sort the vectors (so make it easier to find the // intersection) - std::stable_sort(first_result_nodes.begin(), first_result_nodes.end(), compare_nodes); - std::stable_sort(second_result_nodes.begin(), second_result_nodes.end(), compare_nodes); - std::stable_sort(third_result_nodes.begin(), third_result_nodes.end(), compare_nodes); + std::stable_sort(first_result_nodes.begin(), first_result_nodes.end()); + std::stable_sort(second_result_nodes.begin(), second_result_nodes.end()); + std::stable_sort(third_result_nodes.begin(), third_result_nodes.end()); // Get the intersection of the vectors std::vector first_second_intersection; From 091b58f134bb48377a9cf93b905b6235d54d6581 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 9 May 2024 12:46:28 +1000 Subject: [PATCH 250/572] PR comments and logger testing --- external/oxen-libquic | 2 +- include/session/onionreq/builder.hpp | 4 ++-- src/network.cpp | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index d969b876..6106356c 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit d969b8760916998933f328dadfa69ef359c92055 +Subproject commit 6106356c31be4def16c0a56a17ecc290bb7060e2 diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index e9c71a17..1eb97f95 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -23,8 +23,8 @@ struct ServerDestination { std::string host, std::string endpoint, session::onionreq::x25519_pubkey x25519_pubkey, - std::optional port, - std::optional>> headers, + std::optional port = std::nullopt, + std::optional>> headers = std::nullopt, std::string method = "GET") : protocol{std::move(protocol)}, host{std::move(host)}, diff --git a/src/network.cpp b/src/network.cpp index c5b3c5f3..e666f6f0 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -224,6 +224,7 @@ Network::Network(std::optional cache_path, bool use_testnet, bool p use_testnet{use_testnet}, should_cache_to_disk{cache_path}, cache_path{cache_path.value_or("")} { + log::info(cat, "Test info log - Create network"); get_snode_pool_loop = std::make_shared(); build_paths_loop = std::make_shared(); @@ -1915,7 +1916,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( assert(callback); try { - std::optional body; + std::optional body; if (body_size > 0) body = {body_, body_size}; @@ -1981,7 +1982,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers->emplace_back(server.headers[i], server.header_values[i]); } - std::optional body; + std::optional body; if (body_size > 0) body = {body_, body_size}; From 4e4b3998f261c1c200af6f43eb1fe5c9882add13 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 23:25:59 -0300 Subject: [PATCH 251/572] Unbreak compilation --- src/network.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index e666f6f0..a41f9b47 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1793,6 +1793,7 @@ inline bool set_error(char* error, const std::exception& e) { extern "C" { +using namespace session; using namespace session::network; LIBSESSION_C_API bool network_init( @@ -1916,7 +1917,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( assert(callback); try { - std::optional body; + std::optional body; if (body_size > 0) body = {body_, body_size}; @@ -1982,7 +1983,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( headers->emplace_back(server.headers[i], server.header_values[i]); } - std::optional body; + std::optional body; if (body_size > 0) body = {body_, body_size}; From 9d001e904ee2560eeaeae2ef2fc4ca76b0b68f19 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 May 2024 23:49:13 -0300 Subject: [PATCH 252/572] Add missing test file --- tests/test_logging.cpp | 95 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_logging.cpp diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp new file mode 100644 index 00000000..b870de35 --- /dev/null +++ b/tests/test_logging.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace session; +using namespace oxen; +using namespace oxen::log::literals; + +std::regex timestamp_re{R"(\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[\+[\d.hms]+\])"}; +// Clears timestamps out of a log statement for testing reproducibility +std::string fixup_log(std::string_view log) { + std::string fixed; + std::regex_replace( + std::back_inserter(fixed), + log.begin(), + log.end(), + timestamp_re, + "[] []", + std::regex_constants::format_first_only); + return fixed; +} + +std::vector simple_logs; +std::vector full_logs; // "cat|level|msg" + +TEST_CASE("Logging callbacks", "[logging]") { + oxen::log::clear_sinks(); + simple_logs.clear(); + full_logs.clear(); + session::logger_reset_level(LogLevel::info); + + SECTION("C++ lambdas") { + session::add_logger([&](std::string_view msg) { simple_logs.emplace_back(msg); }); + session::add_logger([&](auto msg, auto cat, auto level) { + full_logs.push_back("{}|{}|{}"_format(cat, level.to_string(), msg)); + }); + } + SECTION("C function pointers") { + session_add_logger_simple( + [](const char* msg, size_t msglen) { simple_logs.emplace_back(msg, msglen); }); + session_add_logger_full([](const char* msg, + size_t msglen, + const char* cat, + size_t cat_len, + LOG_LEVEL level) { + full_logs.push_back("{}|{}|{}"_format( + std::string{cat, cat_len}, + oxen::log::to_string(static_cast(level)), + std::string{msg, msglen})); + }); + } + + log::critical(log::Cat("test.a"), "abc {}", 21 * 2); + int line0 = __LINE__ - 1; + log::info(log::Cat("test.b"), "hi"); + int line1 = __LINE__ - 1; + + oxen::log::clear_sinks(); + + REQUIRE(simple_logs.size() == 2); + REQUIRE(full_logs.size() == 2); + CHECK(fixup_log(simple_logs[0]) == + "[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( + line0)); + CHECK(fixup_log(simple_logs[1]) == + "[] [] [test.b:info|tests/test_logging.cpp:{}] hi\n"_format(line1)); + CHECK(fixup_log(full_logs[0]) == + "test.a|critical|[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( + line0)); + CHECK(fixup_log(full_logs[1]) == + "test.b|info|[] [] [test.b:info|tests/test_logging.cpp:{}] hi\n"_format( + line1)); +} + +TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { + oxen::log::clear_sinks(); + simple_logs.clear(); + session::logger_set_level("quic", LogLevel::debug); + + session::add_logger([&](std::string_view msg) { simple_logs.emplace_back(msg); }); + + { quic::Network net; } + + oxen::log::clear_sinks(); + + CHECK(simple_logs.size() >= 2); + // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); + CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); +} From 6744a7de2fdfcca739d7cda75f96a16d6e455914 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 9 May 2024 14:41:33 +1000 Subject: [PATCH 253/572] Added a function to manually trigger a log for debugging purposes --- include/session/logging.h | 5 +++++ include/session/logging.hpp | 5 +++++ src/logging.cpp | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/include/session/logging.h b/include/session/logging.h index 9698c59f..8c1d2c44 100644 --- a/include/session/logging.h +++ b/include/session/logging.h @@ -57,6 +57,11 @@ LIBSESSION_EXPORT void session_logger_set_level(const char* cat_name, LOG_LEVEL /// Gets the log level of a specific logger category LIBSESSION_EXPORT LOG_LEVEL session_logger_get_level(const char* cat_name); +/// API: session/session_manual_log +/// +/// Logs the provided value via oxen::log, can be used to test that the loggers are working correctly +LIBSESSION_EXPORT void session_manual_log(const char* msg); + #ifdef __cplusplus } #endif diff --git a/include/session/logging.hpp b/include/session/logging.hpp index 5f1ef8f0..12bda3ff 100644 --- a/include/session/logging.hpp +++ b/include/session/logging.hpp @@ -98,4 +98,9 @@ void logger_set_level(std::string cat_name, LogLevel level); /// This function is simply a wrapper around oxen::log::get_level LogLevel logger_get_level(std::string cat_name); +/// API: session/manual_log +/// +/// Logs the provided value via oxen::log, can be used to test that the loggers are working correctly +void manual_log(std::string_view msg); + } // namespace session diff --git a/src/logging.cpp b/src/logging.cpp index 091c09ea..69046ac3 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -31,6 +31,10 @@ void add_logger( log::add_sink(std::make_shared(std::move(cb))); } +void manual_log(std::string_view msg) { + log::critical(oxen::log::Cat("manual"), "{}", msg); +} + void logger_reset_level(LogLevel level) { log::reset_level(level.spdlog_level()); } @@ -49,16 +53,18 @@ LogLevel logger_get_level(std::string cat_name) { } // namespace session +extern "C" { + LIBSESSION_C_API void session_add_logger_simple(void (*callback)(const char* msg, size_t msglen)) { assert(callback); - session::add_logger([cb = callback](std::string_view msg) { cb(msg.data(), msg.size()); }); + session::add_logger([cb = std::move(callback)](std::string_view msg) { cb(msg.data(), msg.size()); }); } LIBSESSION_C_API void session_add_logger_full(void (*callback)( const char* msg, size_t msglen, const char* cat, size_t cat_len, LOG_LEVEL level)) { assert(callback); session::add_logger( - [cb = callback]( + [cb = std::move(callback)]( std::string_view msg, std::string_view category, session::LogLevel level) { cb(msg.data(), msg.size(), @@ -83,3 +89,9 @@ LIBSESSION_C_API void session_logger_set_level(const char* cat_name, LOG_LEVEL l LIBSESSION_C_API LOG_LEVEL session_logger_get_level(const char* cat_name) { return static_cast(oxen::log::get_level(cat_name)); } + +LIBSESSION_C_API void session_manual_log(const char* msg) { + session::manual_log(msg); +} + +} // extern "C" \ No newline at end of file From 269126e115867fae42fe427085d9374b1112d9e8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 9 May 2024 19:16:48 -0300 Subject: [PATCH 254/572] Split out sodium utils into a dedicated header It felt a bit too specific for a general "util" header. Additionally this allows moving utils.cpp into the general util library that everything depends on, but puts the sodium stuff into the crypto library. --- include/session/blinding.hpp | 2 +- include/session/config/base.hpp | 1 + include/session/multi_encrypt.hpp | 2 +- include/session/sodium_array.hpp | 231 ++++++++++++++++++++++++++++ include/session/util.hpp | 242 ------------------------------ src/CMakeLists.txt | 3 +- src/ed25519.cpp | 2 +- src/session_encrypt.cpp | 2 +- src/sodium_array.cpp | 23 +++ src/util.cpp | 17 --- 10 files changed, 261 insertions(+), 264 deletions(-) create mode 100644 include/session/sodium_array.hpp create mode 100644 src/sodium_array.cpp diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index c6e41cf5..092dfee3 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -3,7 +3,7 @@ #include #include -#include "util.hpp" +#include "sodium_array.hpp" namespace session { diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index f25bbc3f..881cc37e 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -11,6 +11,7 @@ #include #include "../logging.hpp" +#include "../sodium_array.hpp" #include "base.h" #include "namespaces.hpp" diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 7d73dfed..2231f9b5 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -9,7 +9,7 @@ #include #include "types.hpp" -#include "util.hpp" +#include "sodium_array.hpp" // Helper functions for implementing multiply encrypted messages by creating separate copies of the // message for each message recipient. This is used most prominently in group key update messages diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp new file mode 100644 index 00000000..589040b7 --- /dev/null +++ b/include/session/sodium_array.hpp @@ -0,0 +1,231 @@ +#pragma once + +#include "util.hpp" + +namespace session { + +// Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure +void* sodium_buffer_allocate(size_t size); +// Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. +void sodium_buffer_deallocate(void* p); +// Calls sodium_memzero to zero a buffer +void sodium_zero_buffer(void* ptr, size_t size); + +// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but +// more secure for sensitive data). +template +struct sodium_ptr { + private: + T* x; + + public: + sodium_ptr() : x{nullptr} {} + sodium_ptr(std::nullptr_t) : sodium_ptr{} {} + ~sodium_ptr() { reset(x); } + + // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` + // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly + // to destruct and deallocate the existing object. + template + T& emplace(Args&&... args) { + if (x) + reset(); + x = static_cast(sodium_buffer_allocate(sizeof(T))); + new (x) T(std::forward(args)...); + return *x; + } + + void reset() { + if (x) { + x->~T(); + sodium_buffer_deallocate(x); + x = nullptr; + } + } + void operator=(std::nullptr_t) { reset(); } + + T& operator*() { return *x; } + const T& operator*() const { return *x; } + + T* operator->() { return x; } + const T* operator->() const { return x; } + + explicit operator bool() const { return x != nullptr; } +}; + +// Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only +// be used with trivially destructible types. +template >> +struct sodium_cleared : T { + using T::T; + + ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } +}; + +template +using cleared_array = sodium_cleared>; + +using cleared_uc32 = cleared_array<32>; +using cleared_uc64 = cleared_array<64>; + +// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation +// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes +// extra precautions, intended for storing sensitive values. +template +struct sodium_array { + private: + T* buf; + size_t len; + + public: + // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). + sodium_array() : buf{nullptr}, len{0} {} + + // Constructs an array with a given size, default-constructing the individual elements. + template >> + explicit sodium_array(size_t length) : + buf{length == 0 ? nullptr + : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, + len{0} { + + if (length > 0) { + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else if constexpr (std::is_nothrow_default_constructible_v) { + for (; len < length; len++) + new (buf[len]) T(); + } else { + try { + for (; len < length; len++) + new (buf[len]) T(); + } catch (...) { + reset(); + throw; + } + } + } + } + + ~sodium_array() { reset(); } + + // Moveable: ownership is transferred to the new object and the old object becomes empty. + sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { + other.buf = nullptr; + other.len = 0; + } + sodium_array& operator=(sodium_array&& other) { + sodium_buffer_deallocate(buf); + buf = other.buf; + len = other.len; + other.buf = nullptr; + other.len = 0; + } + + // Non-copyable + sodium_array(const sodium_array&) = delete; + sodium_array& operator=(const sodium_array&) = delete; + + // Destroys the held array; after destroying elements the allocated space is overwritten with + // 0s before being deallocated. + void reset() { + if (buf) { + if constexpr (!std::is_trivially_destructible_v) + while (len > 0) + buf[--len].~T(); + + sodium_buffer_deallocate(buf); + } + buf = nullptr; + len = 0; + } + + // Calls reset() to destroy the current value (if any) and then allocates a new + // default-constructed one of the given size. + template >> + void reset(size_t length) { + reset(); + if (length > 0) { + buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else { + for (; len < length; len++) + new (buf[len]) T(); + } + } + } + + // Loads the array from a pointer and size; this first resets a value (if present), allocates a + // new array of the given size, the copies the given value(s) into the new buffer. T must be + // copyable. This is *not* safe to use if `buf` points into the currently allocated data. + template >> + void load(const T* data, size_t length) { + reset(length); + if (length == 0) + return; + + if constexpr (std::is_trivially_copyable_v) + std::memcpy(buf, data, sizeof(T) * length); + else + for (; len < length; len++) + new (buf[len]) T(data[len]); + } + + const T& operator[](size_t i) const { + assert(i < len); + return buf[i]; + } + T& operator[](size_t i) { + assert(i < len); + return buf[i]; + } + + T* data() { return buf; } + const T* data() const { return buf; } + + size_t size() const { return len; } + bool empty() const { return len == 0; } + explicit operator bool() const { return !empty(); } + + T* begin() { return buf; } + const T* begin() const { return buf; } + T* end() { return buf + len; } + const T* end() const { return buf + len; } + + using difference_type = ptrdiff_t; + using value_type = T; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::random_access_iterator_tag; +}; + +// sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with +// sensitive data. It is as a result slower and has more overhead than a standard allocator and +// intended for use with a container (such as std::vector) when storing keys. +template +struct sodium_allocator { + using value_type = T; + + [[nodiscard]] static T* allocate(std::size_t n) { + return static_cast(sodium_buffer_allocate(n * sizeof(T))); + } + + static void deallocate(T* p, std::size_t) { sodium_buffer_deallocate(p); } + + template + bool operator==(const sodium_allocator&) const noexcept { + return true; + } + template + bool operator!=(const sodium_allocator&) const noexcept { + return false; + } +}; + +/// Vector that uses sodium's secure (but heavy) memory allocations +template +using sodium_vector = std::vector>; + +} diff --git a/include/session/util.hpp b/include/session/util.hpp index b052cdf4..d6209201 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -78,251 +78,9 @@ inline bool string_iequal(std::string_view s1, std::string_view s2) { }); } -// Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure -void* sodium_buffer_allocate(size_t size); -// Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. -void sodium_buffer_deallocate(void* p); -// Calls sodium_memzero to zero a buffer -void sodium_zero_buffer(void* ptr, size_t size); - -// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but -// more secure for sensitive data). -template -struct sodium_ptr { - private: - T* x; - - public: - sodium_ptr() : x{nullptr} {} - sodium_ptr(std::nullptr_t) : sodium_ptr{} {} - ~sodium_ptr() { reset(x); } - - // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` - // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly - // to destruct and deallocate the existing object. - template - T& emplace(Args&&... args) { - if (x) - reset(); - x = static_cast(sodium_buffer_allocate(sizeof(T))); - new (x) T(std::forward(args)...); - return *x; - } - - void reset() { - if (x) { - x->~T(); - sodium_buffer_deallocate(x); - x = nullptr; - } - } - void operator=(std::nullptr_t) { reset(); } - - T& operator*() { return *x; } - const T& operator*() const { return *x; } - - T* operator->() { return x; } - const T* operator->() const { return x; } - - explicit operator bool() const { return x != nullptr; } -}; - -// Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only -// be used with trivially destructible types. -template >> -struct sodium_cleared : T { - using T::T; - - ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } -}; - -template -using cleared_array = sodium_cleared>; - using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; - -// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation -// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes -// extra precautions, intended for storing sensitive values. -template -struct sodium_array { - private: - T* buf; - size_t len; - - public: - // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). - sodium_array() : buf{nullptr}, len{0} {} - - // Constructs an array with a given size, default-constructing the individual elements. - template >> - explicit sodium_array(size_t length) : - buf{length == 0 ? nullptr - : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, - len{0} { - - if (length > 0) { - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else if constexpr (std::is_nothrow_default_constructible_v) { - for (; len < length; len++) - new (buf[len]) T(); - } else { - try { - for (; len < length; len++) - new (buf[len]) T(); - } catch (...) { - reset(); - throw; - } - } - } - } - - ~sodium_array() { reset(); } - - // Moveable: ownership is transferred to the new object and the old object becomes empty. - sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { - other.buf = nullptr; - other.len = 0; - } - sodium_array& operator=(sodium_array&& other) { - sodium_buffer_deallocate(buf); - buf = other.buf; - len = other.len; - other.buf = nullptr; - other.len = 0; - } - - // Non-copyable - sodium_array(const sodium_array&) = delete; - sodium_array& operator=(const sodium_array&) = delete; - - // Destroys the held array; after destroying elements the allocated space is overwritten with - // 0s before being deallocated. - void reset() { - if (buf) { - if constexpr (!std::is_trivially_destructible_v) - while (len > 0) - buf[--len].~T(); - - sodium_buffer_deallocate(buf); - } - buf = nullptr; - len = 0; - } - - // Calls reset() to destroy the current value (if any) and then allocates a new - // default-constructed one of the given size. - template >> - void reset(size_t length) { - reset(); - if (length > 0) { - buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else { - for (; len < length; len++) - new (buf[len]) T(); - } - } - } - - // Loads the array from a pointer and size; this first resets a value (if present), allocates a - // new array of the given size, the copies the given value(s) into the new buffer. T must be - // copyable. This is *not* safe to use if `buf` points into the currently allocated data. - template >> - void load(const T* data, size_t length) { - reset(length); - if (length == 0) - return; - - if constexpr (std::is_trivially_copyable_v) - std::memcpy(buf, data, sizeof(T) * length); - else - for (; len < length; len++) - new (buf[len]) T(data[len]); - } - - const T& operator[](size_t i) const { - assert(i < len); - return buf[i]; - } - T& operator[](size_t i) { - assert(i < len); - return buf[i]; - } - - T* data() { return buf; } - const T* data() const { return buf; } - - size_t size() const { return len; } - bool empty() const { return len == 0; } - explicit operator bool() const { return !empty(); } - - T* begin() { return buf; } - const T* begin() const { return buf; } - T* end() { return buf + len; } - const T* end() const { return buf + len; } - - using difference_type = ptrdiff_t; - using value_type = T; - using pointer = value_type*; - using reference = value_type&; - using iterator_category = std::random_access_iterator_tag; -}; - -// sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with -// sensitive data. It is as a result slower and has more overhead than a standard allocator and -// intended for use with a container (such as std::vector) when storing keys. -template -struct sodium_allocator { - using value_type = T; - - [[nodiscard]] static T* allocate(std::size_t n) { - return static_cast(sodium_buffer_allocate(n * sizeof(T))); - } - - static void deallocate(T* p, std::size_t) { sodium_buffer_deallocate(p); } - - template - bool operator==(const sodium_allocator&) const noexcept { - return true; - } - template - bool operator!=(const sodium_allocator&) const noexcept { - return false; - } -}; - -/// Vector that uses sodium's secure (but heavy) memory allocations -template -using sodium_vector = std::vector>; - -template -using string_view_char_type = std::conditional_t< - std::is_convertible_v, - char, - std::conditional_t< - std::is_convertible_v>, - unsigned char, - std::conditional_t< - std::is_convertible_v>, - std::byte, - void>>>; - -template -constexpr bool is_char_array = false; -template -inline constexpr bool is_char_array> = - std::is_same_v || std::is_same_v || - std::is_same_v; /// Takes a container of string-like binary values and returns a vector of ustring_views viewing /// those values. This can be used on a container of any type with a `.data()` and a `.size()` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a29ec41..0711e398 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,7 @@ endmacro() add_libsession_util_library(util logging.cpp + util.cpp ) add_libsession_util_library(crypto @@ -48,7 +49,7 @@ add_libsession_util_library(crypto multi_encrypt.cpp random.cpp session_encrypt.cpp - util.cpp + sodium_array.cpp xed25519.cpp ) diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 87297b09..a4483a2f 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -6,7 +6,7 @@ #include #include "session/export.h" -#include "session/util.hpp" +#include "session/sodium_array.hpp" namespace session::ed25519 { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 88bc4e7a..2b7b91b3 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -17,7 +17,7 @@ #include #include "session/blinding.hpp" -#include "session/util.hpp" +#include "session/sodium_array.hpp" using namespace std::literals; diff --git a/src/sodium_array.cpp b/src/sodium_array.cpp new file mode 100644 index 00000000..b0b09dde --- /dev/null +++ b/src/sodium_array.cpp @@ -0,0 +1,23 @@ +#include + +#include + +namespace session { + +void* sodium_buffer_allocate(size_t length) { + if (auto* p = sodium_malloc(length)) + return p; + throw std::bad_alloc{}; +} + +void sodium_buffer_deallocate(void* p) { + if (p) + sodium_free(p); +} + +void sodium_zero_buffer(void* ptr, size_t size) { + if (ptr) + sodium_memzero(ptr, size); +} + +} diff --git a/src/util.cpp b/src/util.cpp index 4a786b9a..79448099 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,23 +1,6 @@ -#include - #include namespace session { -void* sodium_buffer_allocate(size_t length) { - if (auto* p = sodium_malloc(length)) - return p; - throw std::bad_alloc{}; -} - -void sodium_buffer_deallocate(void* p) { - if (p) - sodium_free(p); -} - -void sodium_zero_buffer(void* ptr, size_t size) { - if (ptr) - sodium_memzero(ptr, size); -} std::vector split(std::string_view str, const std::string_view delim, bool trim) { std::vector results; From 1784e0d6380525e62ba9350b96bb8ede489ee648 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 9 May 2024 19:24:27 -0300 Subject: [PATCH 255/572] Add file helpers; push more std::filesystem::path - std::ifstream/ofstream are ancient interfaces that have exceptions enabled via a runtime configuration call (and off by default), and so we wouldn't be catching read/write failures. This abstracts it with a new file.hpp header of helper functions to help opening files, and help reading/writing files with single string contents. - Added a fs namespace alias for the overly verbose std::filesystem. - Pushed fs::path usage deeper so that we get fs::path objects earlier and use them for joining paths and whatnot, rather than joining strings and then converting to fs::path. - Fixes various little issues that are likely to cause issues later with Windows (most notably where strings and fs::paths interact, which is painful in Windows because the native fs::path object is backed by wstrings on Windows). - Removed some redundant path operations (such as: no need to remove before a rename: rename overwrites the target; also no need to remove a file before overwriting it as we can just do that with the open flags). - Added a convert_sv that makes it easier to flip between arbitrary string view types (in this case: between u8strings and strings, for fs::path interoperability). - Make sure last_snode_pool_update gets initialized to 0 (which will be unix epoch). - Fixed a bug in reading cache files that was (probably?) failing to re-read them properly because of the != npos check (which should have been == npos, but is just dropped here because we don't really need it at all). - Replaced stoi with quic::parse_int for its better sanity (i.e. only succeeding if it consumes the entire string). - Changed some constants to constexpr. --- include/session/file.hpp | 28 ++++++ include/session/logging.h | 3 +- include/session/logging.hpp | 3 +- include/session/multi_encrypt.hpp | 2 +- include/session/network.hpp | 8 +- include/session/sodium_array.hpp | 2 +- include/session/util.hpp | 14 +++ src/CMakeLists.txt | 1 + src/file.cpp | 36 ++++++++ src/logging.cpp | 3 +- src/network.cpp | 138 +++++++++++++++++------------- src/sodium_array.cpp | 2 +- 12 files changed, 171 insertions(+), 69 deletions(-) create mode 100644 include/session/file.hpp create mode 100644 src/file.cpp diff --git a/include/session/file.hpp b/include/session/file.hpp new file mode 100644 index 00000000..4a7f99f1 --- /dev/null +++ b/include/session/file.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include +#include + +// Utility functions for working with files + +namespace session { + +namespace fs = std::filesystem; + +/// Opens a file for writing of binary data, setting up the returned ofstream with exceptions +/// enabled for any failures. This also throws if the file cannot be opened. If the file already +/// exists it will be truncated. +std::ofstream open_for_writing(const fs::path& filename); + +/// Opens a file for reading of binary data, setting up the returned ifstream with exceptions +/// enabled for any failures. This also throws if the file cannot be opened. +std::ifstream open_for_reading(const fs::path& filename); + +/// Reads a (binary) file from disk into the string `contents`. +std::string read_whole_file(const fs::path& filename); + +/// Dumps (binary) string contents to disk. The file is overwritten if it already exists. +void write_whole_file(const fs::path& filename, std::string_view contents); + +} // namespace session diff --git a/include/session/logging.h b/include/session/logging.h index 8c1d2c44..d027a32c 100644 --- a/include/session/logging.h +++ b/include/session/logging.h @@ -59,7 +59,8 @@ LIBSESSION_EXPORT LOG_LEVEL session_logger_get_level(const char* cat_name); /// API: session/session_manual_log /// -/// Logs the provided value via oxen::log, can be used to test that the loggers are working correctly +/// Logs the provided value via oxen::log, can be used to test that the loggers are working +/// correctly LIBSESSION_EXPORT void session_manual_log(const char* msg); #ifdef __cplusplus diff --git a/include/session/logging.hpp b/include/session/logging.hpp index 12bda3ff..14c2a5de 100644 --- a/include/session/logging.hpp +++ b/include/session/logging.hpp @@ -100,7 +100,8 @@ LogLevel logger_get_level(std::string cat_name); /// API: session/manual_log /// -/// Logs the provided value via oxen::log, can be used to test that the loggers are working correctly +/// Logs the provided value via oxen::log, can be used to test that the loggers are working +/// correctly void manual_log(std::string_view msg); } // namespace session diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 2231f9b5..533f055e 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -8,8 +8,8 @@ #include #include -#include "types.hpp" #include "sodium_array.hpp" +#include "types.hpp" // Helper functions for implementing multiply encrypted messages by creating separate copies of the // message for each message recipient. This is used most prominently in group key update messages diff --git a/include/session/network.hpp b/include/session/network.hpp index 7ba7d315..af7826a9 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -8,6 +8,8 @@ namespace session::network { +namespace fs = std::filesystem; + using service_node = oxen::quic::RemoteAddress; using network_response_callback_t = std::function response)>; @@ -51,7 +53,7 @@ class Network { private: const bool use_testnet; const bool should_cache_to_disk; - const std::string cache_path; + const fs::path cache_path; // Disk thread state std::mutex snode_cache_mutex; // This guards all the below: @@ -65,7 +67,7 @@ class Network { // Values persisted to disk std::vector snode_pool; std::unordered_map snode_failure_counts; - std::chrono::system_clock::time_point last_snode_pool_update; + std::chrono::system_clock::time_point last_snode_pool_update{}; std::unordered_map> swarm_cache; std::thread disk_write_thread; @@ -87,7 +89,7 @@ class Network { // Constructs a new network with the given cache path and a flag indicating whether it should // use testnet or mainnet, all requests should be made via a single Network instance. - Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); + Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); ~Network(); /// API: network/close_connections diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp index 589040b7..a0a52de0 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -228,4 +228,4 @@ struct sodium_allocator { template using sodium_vector = std::vector>; -} +} // namespace session diff --git a/include/session/util.hpp b/include/session/util.hpp index d6209201..06b92fe4 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -40,6 +42,18 @@ inline const char* from_unsigned(const unsigned char* x) { inline char* from_unsigned(unsigned char* x) { return reinterpret_cast(x); } +// Helper to switch from basic_string_view to basic_string_view. Both CFrom and CTo +// must be primitive, one-byte types. +template +inline std::basic_string_view convert_sv(std::basic_string_view from) { + return {reinterpret_cast(from.data()), from.size()}; +} +// Same as above, but with a const basic_string& argument (to allow deduction of CFrom when +// using a basic_string). +template +inline std::basic_string_view convert_sv(const std::basic_string& from) { + return {reinterpret_cast(from.data()), from.size()}; +} // Helper function to switch between basic_string_view and ustring_view inline ustring_view to_unsigned_sv(std::string_view v) { return {to_unsigned(v.data()), v.size()}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0711e398..768ae074 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ endmacro() add_libsession_util_library(util + file.cpp logging.cpp util.cpp ) diff --git a/src/file.cpp b/src/file.cpp new file mode 100644 index 00000000..6f64cc7b --- /dev/null +++ b/src/file.cpp @@ -0,0 +1,36 @@ +#include +#include + +namespace session { + +std::ofstream open_for_writing(const fs::path& filename) { + std::ofstream out; + out.exceptions(std::ios_base::failbit | std::ios_base::badbit); + out.open(filename, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); + return out; +} + +std::ifstream open_for_reading(const fs::path& filename) { + std::ifstream in; + in.exceptions(std::ios_base::failbit | std::ios_base::badbit); + in.open(filename, std::ios::binary | std::ios::in); + return in; +} + +std::string read_whole_file(const fs::path& filename) { + auto in = open_for_reading(filename); + std::string contents; + in.seekg(0, std::ios::end); + auto size = in.tellg(); + in.seekg(0, std::ios::beg); + contents.resize(size); + in.read(contents.data(), size); + return contents; +} + +void write_whole_file(const fs::path& filename, std::string_view contents) { + auto out = open_for_writing(filename); + out.write(contents.data(), static_cast(contents.size())); +} + +} // namespace session diff --git a/src/logging.cpp b/src/logging.cpp index 69046ac3..cff49f5b 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -57,7 +57,8 @@ extern "C" { LIBSESSION_C_API void session_add_logger_simple(void (*callback)(const char* msg, size_t msglen)) { assert(callback); - session::add_logger([cb = std::move(callback)](std::string_view msg) { cb(msg.data(), msg.size()); }); + session::add_logger( + [cb = std::move(callback)](std::string_view msg) { cb(msg.data(), msg.size()); }); } LIBSESSION_C_API void session_add_logger_full(void (*callback)( diff --git a/src/network.cpp b/src/network.cpp index a41f9b47..58095d10 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -18,6 +18,7 @@ #include "session/ed25519.hpp" #include "session/export.h" +#include "session/file.hpp" #include "session/network.h" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" @@ -45,31 +46,30 @@ namespace { }; // The amount of time the snode cache can be used before it needs to be refreshed - const std::chrono::seconds snode_cache_expiration_duration = 2h; + constexpr auto snode_cache_expiration_duration = 2h; // The amount of time a swarm cache can be used before it needs to be refreshed - const std::chrono::seconds swarm_cache_expiration_duration = (24h * 7); + constexpr auto swarm_cache_expiration_duration = (24h * 7); // The smallest size the snode pool can get to before we need to fetch more. - const uint16_t min_snode_pool_count = 12; + constexpr uint16_t min_snode_pool_count = 12; // The number of paths we want to maintain. - const uint8_t target_path_count = 2; + constexpr uint8_t target_path_count = 2; // The number of snodes (including the guard snode) in a path. - const uint8_t path_size = 3; + constexpr uint8_t path_size = 3; // The number of times a path can fail before it's replaced. - const uint16_t path_failure_threshold = 3; + constexpr uint16_t path_failure_threshold = 3; // The number of times a snode can fail before it's replaced. - const uint16_t snode_failure_threshold = 3; + constexpr uint16_t snode_failure_threshold = 3; // File names - const auto file_testnet = "/testnet"s; - const auto file_snode_pool = "/snode_pool"s; - const auto file_snode_pool_updated = "/snode_pool_updated"s; - const auto swarm_dir = "/swarm"s; + const fs::path file_testnet{u8"testnet"}, file_snode_pool{u8"snode_pool"}, + file_snode_pool_updated{u8"snode_pool_updated"}, swarm_dir{u8"swarm"}, + default_cache_path{u8"."}; constexpr auto node_not_found_prefix = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; @@ -220,10 +220,10 @@ namespace { // MARK: Initialization -Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : +Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : use_testnet{use_testnet}, should_cache_to_disk{cache_path}, - cache_path{cache_path.value_or("")} { + cache_path{cache_path.value_or(default_cache_path)} { log::info(cat, "Test info log - Create network"); get_snode_pool_loop = std::make_shared(); build_paths_loop = std::make_shared(); @@ -259,33 +259,45 @@ Network::~Network() { void Network::load_cache_from_disk() { // If the cache is for the wrong network then delete everything - auto cache_is_for_testnet = std::filesystem::exists(cache_path + file_testnet); - if ((!use_testnet && cache_is_for_testnet) || (use_testnet && !cache_is_for_testnet)) - std::filesystem::remove_all(cache_path); + auto testnet_stub = cache_path / file_testnet; + bool cache_is_for_testnet = fs::exists(testnet_stub); + if (use_testnet != cache_is_for_testnet) + fs::remove_all(cache_path); - // Create the cache directory if needed - std::filesystem::create_directories(cache_path); - std::filesystem::create_directories(cache_path + swarm_dir); + // Create the cache directory (and swarm_dir, inside it) if needed + auto swarm_path = cache_path / swarm_dir; + fs::create_directories(swarm_path); // If we are using testnet then create a file to indicate that if (use_testnet) - std::ofstream{cache_path + file_testnet}; + write_whole_file(testnet_stub, ""); // Load the last time the snode pool was updated // // Note: We aren't just reading the write time of the file because Apple consider // accessing file timestamps a method that can be used to track the user (and we // want to avoid being flagged as using such) - if (std::filesystem::exists(cache_path + file_snode_pool_updated)) { - std::ifstream file{cache_path + file_snode_pool_updated}; - std::time_t timestamp; - file >> timestamp; - last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); + auto last_updated_path = cache_path / file_snode_pool_updated; + if (fs::exists(last_updated_path)) { + try { + auto timestamp_str = read_whole_file(last_updated_path); + while (timestamp_str.ends_with('\n')) + timestamp_str.pop_back(); + + std::time_t timestamp; + if (!quic::parse_int(timestamp_str, timestamp)) + throw std::runtime_error{"invalid file data: expected timestamp first line"}; + + last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); + } catch (const std::exception& e) { + log::error(cat, "Ignoring invalid last update timestamp file: {}", e.what()); + } } // Load the snode pool - if (std::filesystem::exists(cache_path + file_snode_pool)) { - std::ifstream file{cache_path + file_snode_pool}; + auto pool_path = cache_path / file_snode_pool; + if (fs::exists(pool_path)) { + auto file = open_for_reading(pool_path); std::vector loaded_pool; std::unordered_map loaded_failure_count; std::string line; @@ -305,28 +317,30 @@ void Network::load_cache_from_disk() { } // Load the swarm cache - auto swarm_path = (cache_path + swarm_dir); auto time_now = std::chrono::system_clock::now(); std::unordered_map> loaded_cache; - std::vector caches_to_remove; + std::vector caches_to_remove; - for (auto& entry : std::filesystem::directory_iterator(swarm_path)) { + for (auto& entry : fs::directory_iterator(swarm_path)) { // If the pubkey was valid then process the content - std::ifstream file{entry.path()}; + auto file = open_for_reading(entry.path()); std::vector nodes; std::string line; bool checked_swarm_expiration = false; std::chrono::seconds swarm_lifetime = 0s; - auto path = entry.path().string(); - auto filename = entry.path().filename().string(); + const auto& path = entry.path(); + std::string filename{convert_sv(path.filename().u8string())}; while (std::getline(file, line)) { try { // If we haven't checked if the swarm cache has expired then do so, removing // any expired/invalid caches - if (!checked_swarm_expiration && line.find('|') != std::string::npos) { - auto swarm_last_updated = - std::chrono::system_clock::from_time_t(std::stoi(line)); + if (!checked_swarm_expiration) { + std::time_t timestamp; + if (!quic::parse_int(line, timestamp)) + throw std::runtime_error{ + "invalid file data: expected timestamp first line"}; + auto swarm_last_updated = std::chrono::system_clock::from_time_t(timestamp); swarm_lifetime = std::chrono::duration_cast( time_now - swarm_last_updated); checked_swarm_expiration = true; @@ -337,8 +351,9 @@ void Network::load_cache_from_disk() { // Otherwise try to parse as a node nodes.push_back(node_from_disk(line).first); - } catch (...) { - log::warning(cat, "Skipping invalid or expired entry in swarm cache."); + + } catch (const std::exception& e) { + log::warning(cat, "Skipping invalid or expired entry in swarm cache: {}", e.what()); // The cache is invalid, we should remove it if (!checked_swarm_expiration) { @@ -350,7 +365,7 @@ void Network::load_cache_from_disk() { // If we got nodes the add it to the cache, otherwise we want to remove it if (!nodes.empty()) - loaded_cache[filename] = nodes; + loaded_cache[filename] = std::move(nodes); else caches_to_remove.emplace_back(path); } @@ -359,7 +374,7 @@ void Network::load_cache_from_disk() { // Remove any expired cache files for (auto& cache_path : caches_to_remove) - std::filesystem::remove_all(cache_path); + fs::remove_all(cache_path); log::info(cat, "Loaded cache of {} snodes, {} swarms.", snode_pool.size(), swarm_cache.size()); } @@ -380,24 +395,28 @@ void Network::disk_write_thread_loop() { lock.unlock(); { // Create the cache directories if needed - std::filesystem::create_directories(cache_path); - std::filesystem::create_directories(cache_path + swarm_dir); + auto swarm_base = cache_path / swarm_dir; + fs::create_directories(swarm_base); // Save the snode pool to disk if (need_pool_write) { - auto pool_path = cache_path + file_snode_pool; - std::filesystem::remove(pool_path + "_new"); - std::ofstream file{pool_path + "_new"}; - for (auto& snode : snode_pool_write) - file << node_to_disk(snode, snode_failure_counts_write) << '\n'; + auto pool_path = cache_path / file_snode_pool; + auto pool_tmp = pool_path; + pool_tmp += u8"_new"; + + { + auto file = open_for_writing(pool_tmp); + for (auto& snode : snode_pool_write) + file << node_to_disk(snode, snode_failure_counts_write) << '\n'; + } - std::filesystem::remove(pool_path); - std::filesystem::rename(pool_path + "_new", pool_path); + fs::rename(pool_tmp, pool_path); // Write the last update timestamp to disk - std::filesystem::remove(cache_path + file_snode_pool_updated); - std::ofstream timestamp_file{cache_path + file_snode_pool_updated}; - timestamp_file << std::chrono::system_clock::to_time_t(last_pool_update_write); + write_whole_file( + cache_path / file_snode_pool_updated, + "{}"_format( + std::chrono::system_clock::to_time_t(last_pool_update_write))); log::debug(cat, "Finished writing snode pool cache to disk."); } @@ -406,9 +425,10 @@ void Network::disk_write_thread_loop() { auto time_now = std::chrono::system_clock::now(); for (auto& [key, swarm] : swarm_cache_write) { - auto swarm_path = cache_path + swarm_dir + "/" + key; - std::filesystem::remove(swarm_path + "_new"); - std::ofstream swarm_file{swarm_path + "_new"}; + auto swarm_path = swarm_base / key; + auto swarm_tmp = swarm_path; + swarm_tmp += u8"_new"; + auto swarm_file = open_for_writing(swarm_tmp); // Write the timestamp to the file swarm_file << std::chrono::system_clock::to_time_t(time_now) << '\n'; @@ -417,8 +437,7 @@ void Network::disk_write_thread_loop() { for (auto& snode : swarm) swarm_file << node_to_disk(snode, snode_failure_counts_write) << '\n'; - std::filesystem::remove(cache_path + swarm_dir + "/" + key); - std::filesystem::rename(swarm_path + "_new", swarm_path); + fs::rename(swarm_tmp, swarm_path); } log::debug(cat, "Finished writing swarm cache to disk."); } @@ -435,7 +454,7 @@ void Network::disk_write_thread_loop() { swarm_cache = {}; lock.unlock(); - { std::filesystem::remove_all(cache_path); } + fs::remove_all(cache_path); lock.lock(); need_clear_cache = false; } @@ -1253,8 +1272,7 @@ void Network::send_request( quic::bstring_view payload{}; if (info.body) - payload = quic::bstring_view{ - reinterpret_cast(info.body->data()), info.body->size()}; + payload = convert_sv(*info.body); conn_info.stream->command( info.endpoint, diff --git a/src/sodium_array.cpp b/src/sodium_array.cpp index b0b09dde..3465836a 100644 --- a/src/sodium_array.cpp +++ b/src/sodium_array.cpp @@ -20,4 +20,4 @@ void sodium_zero_buffer(void* ptr, size_t size) { sodium_memzero(ptr, size); } -} +} // namespace session From 610965b417709e77c7f3b832dae6fce75088c198 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 May 2024 13:12:19 +1000 Subject: [PATCH 256/572] Build fix --- src/logging.cpp | 2 +- src/sodium_array.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logging.cpp b/src/logging.cpp index cff49f5b..28a457b6 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -32,7 +32,7 @@ void add_logger( } void manual_log(std::string_view msg) { - log::critical(oxen::log::Cat("manual"), "{}", msg); + log::info(oxen::log::Cat("manual"), "{}", msg); } void logger_reset_level(LogLevel level) { diff --git a/src/sodium_array.cpp b/src/sodium_array.cpp index 3465836a..14ce50eb 100644 --- a/src/sodium_array.cpp +++ b/src/sodium_array.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace session { From ac50ef42b4654f482e6ada4caabdf163b0efaf20 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 May 2024 14:41:39 +1000 Subject: [PATCH 257/572] Fixed an issue when merging the current config into itself This change fixes an issue where when the user would merge a config message which is identical to the current config - there was existing logic to handle this case (no merge would occur and the existing config gets used) but it only applied to the `all_confs` vector and not the `all_hashes` vector. The result of this discrepancy would mean that the current hash could be added to the `_old_hashes` set, resulting in the current config messages getting deleted from the swarm. This change avoids adding the hash to `_old_hashes` if it's the same as the current config hash. --- src/config/base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 0eb47b5b..4f12bc05 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -212,8 +212,12 @@ std::vector ConfigBase::_merge( // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). std::optional superconf = new_conf->unmerged_index(); // nullopt if we had to merge + std::string_view superconf_hash = superconf && *superconf < all_hashes.size() + ? all_hashes[*superconf] + : ""; + for (size_t i = 0; i < all_hashes.size(); i++) { - if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty()) + if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty() && superconf_hash != all_hashes[i]) _old_hashes.emplace(all_hashes[i]); } From 469a72cf24565f8c6e2d6d68076a3386b6342ba7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 May 2024 16:33:58 +1000 Subject: [PATCH 258/572] Ran formatter, catch exceptions thrown when loading cache, disable 'open_for_reading' exceptions --- src/config/base.cpp | 8 +- src/file.cpp | 5 +- src/network.cpp | 210 +++++++++++++++++++++++--------------------- 3 files changed, 118 insertions(+), 105 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 4f12bc05..702b8e14 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -212,12 +212,12 @@ std::vector ConfigBase::_merge( // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). std::optional superconf = new_conf->unmerged_index(); // nullopt if we had to merge - std::string_view superconf_hash = superconf && *superconf < all_hashes.size() - ? all_hashes[*superconf] - : ""; + std::string_view superconf_hash = + superconf && *superconf < all_hashes.size() ? all_hashes[*superconf] : ""; for (size_t i = 0; i < all_hashes.size(); i++) { - if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty() && superconf_hash != all_hashes[i]) + if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty() && + superconf_hash != all_hashes[i]) _old_hashes.emplace(all_hashes[i]); } diff --git a/src/file.cpp b/src/file.cpp index 6f64cc7b..46275fca 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -7,13 +7,16 @@ std::ofstream open_for_writing(const fs::path& filename) { std::ofstream out; out.exceptions(std::ios_base::failbit | std::ios_base::badbit); out.open(filename, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); + if (!out.is_open()) + throw std::runtime_error{"Failed to open file for writing: " + std::string(filename)}; return out; } std::ifstream open_for_reading(const fs::path& filename) { std::ifstream in; - in.exceptions(std::ios_base::failbit | std::ios_base::badbit); in.open(filename, std::ios::binary | std::ios::in); + if (!in.is_open()) + throw std::runtime_error{"Failed to open file for reading: " + std::string(filename)}; return in; } diff --git a/src/network.cpp b/src/network.cpp index 58095d10..899734d7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -258,125 +258,135 @@ Network::~Network() { // MARK: Cache Management void Network::load_cache_from_disk() { - // If the cache is for the wrong network then delete everything - auto testnet_stub = cache_path / file_testnet; - bool cache_is_for_testnet = fs::exists(testnet_stub); - if (use_testnet != cache_is_for_testnet) - fs::remove_all(cache_path); + try { + // If the cache is for the wrong network then delete everything + auto testnet_stub = cache_path / file_testnet; + bool cache_is_for_testnet = fs::exists(testnet_stub); + if (use_testnet != cache_is_for_testnet) + fs::remove_all(cache_path); - // Create the cache directory (and swarm_dir, inside it) if needed - auto swarm_path = cache_path / swarm_dir; - fs::create_directories(swarm_path); - - // If we are using testnet then create a file to indicate that - if (use_testnet) - write_whole_file(testnet_stub, ""); - - // Load the last time the snode pool was updated - // - // Note: We aren't just reading the write time of the file because Apple consider - // accessing file timestamps a method that can be used to track the user (and we - // want to avoid being flagged as using such) - auto last_updated_path = cache_path / file_snode_pool_updated; - if (fs::exists(last_updated_path)) { - try { - auto timestamp_str = read_whole_file(last_updated_path); - while (timestamp_str.ends_with('\n')) - timestamp_str.pop_back(); + // Create the cache directory (and swarm_dir, inside it) if needed + auto swarm_path = cache_path / swarm_dir; + fs::create_directories(swarm_path); + + // If we are using testnet then create a file to indicate that + if (use_testnet) + write_whole_file(testnet_stub, ""); + + // Load the last time the snode pool was updated + // + // Note: We aren't just reading the write time of the file because Apple consider + // accessing file timestamps a method that can be used to track the user (and we + // want to avoid being flagged as using such) + auto last_updated_path = cache_path / file_snode_pool_updated; + if (fs::exists(last_updated_path)) { + try { + auto timestamp_str = read_whole_file(last_updated_path); + while (timestamp_str.ends_with('\n')) + timestamp_str.pop_back(); - std::time_t timestamp; - if (!quic::parse_int(timestamp_str, timestamp)) - throw std::runtime_error{"invalid file data: expected timestamp first line"}; + std::time_t timestamp; + if (!quic::parse_int(timestamp_str, timestamp)) + throw std::runtime_error{"invalid file data: expected timestamp first line"}; - last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); - } catch (const std::exception& e) { - log::error(cat, "Ignoring invalid last update timestamp file: {}", e.what()); + last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); + } catch (const std::exception& e) { + log::error(cat, "Ignoring invalid last update timestamp file: {}", e.what()); + } } - } - // Load the snode pool - auto pool_path = cache_path / file_snode_pool; - if (fs::exists(pool_path)) { - auto file = open_for_reading(pool_path); - std::vector loaded_pool; - std::unordered_map loaded_failure_count; - std::string line; + // Load the snode pool + auto pool_path = cache_path / file_snode_pool; + if (fs::exists(pool_path)) { + auto file = open_for_reading(pool_path); + std::vector loaded_pool; + std::unordered_map loaded_failure_count; + std::string line; - while (std::getline(file, line)) { - try { - auto [node, failure_count] = node_from_disk(line); - loaded_pool.push_back(node); - loaded_failure_count[node.to_string()] = failure_count; - } catch (...) { - log::warning(cat, "Skipping invalid entry in snode pool cache."); + while (std::getline(file, line)) { + try { + auto [node, failure_count] = node_from_disk(line); + loaded_pool.push_back(node); + loaded_failure_count[node.to_string()] = failure_count; + } catch (...) { + log::warning(cat, "Skipping invalid entry in snode pool cache."); + } } - } - snode_pool = loaded_pool; - snode_failure_counts = loaded_failure_count; - } + snode_pool = loaded_pool; + snode_failure_counts = loaded_failure_count; + } - // Load the swarm cache - auto time_now = std::chrono::system_clock::now(); - std::unordered_map> loaded_cache; - std::vector caches_to_remove; - - for (auto& entry : fs::directory_iterator(swarm_path)) { - // If the pubkey was valid then process the content - auto file = open_for_reading(entry.path()); - std::vector nodes; - std::string line; - bool checked_swarm_expiration = false; - std::chrono::seconds swarm_lifetime = 0s; - const auto& path = entry.path(); - std::string filename{convert_sv(path.filename().u8string())}; - - while (std::getline(file, line)) { - try { - // If we haven't checked if the swarm cache has expired then do so, removing - // any expired/invalid caches - if (!checked_swarm_expiration) { - std::time_t timestamp; - if (!quic::parse_int(line, timestamp)) - throw std::runtime_error{ - "invalid file data: expected timestamp first line"}; - auto swarm_last_updated = std::chrono::system_clock::from_time_t(timestamp); - swarm_lifetime = std::chrono::duration_cast( - time_now - swarm_last_updated); - checked_swarm_expiration = true; - - if (swarm_lifetime < swarm_cache_expiration_duration) - throw std::runtime_error{"Expired swarm cache."}; - } + // Load the swarm cache + auto time_now = std::chrono::system_clock::now(); + std::unordered_map> loaded_cache; + std::vector caches_to_remove; + + for (auto& entry : fs::directory_iterator(swarm_path)) { + // If the pubkey was valid then process the content + auto file = open_for_reading(entry.path()); + std::vector nodes; + std::string line; + bool checked_swarm_expiration = false; + std::chrono::seconds swarm_lifetime = 0s; + const auto& path = entry.path(); + std::string filename{convert_sv(path.filename().u8string())}; + + while (std::getline(file, line)) { + try { + // If we haven't checked if the swarm cache has expired then do so, removing + // any expired/invalid caches + if (!checked_swarm_expiration) { + std::time_t timestamp; + if (!quic::parse_int(line, timestamp)) + throw std::runtime_error{ + "invalid file data: expected timestamp first line"}; + auto swarm_last_updated = std::chrono::system_clock::from_time_t(timestamp); + swarm_lifetime = std::chrono::duration_cast( + time_now - swarm_last_updated); + checked_swarm_expiration = true; + + if (swarm_lifetime < swarm_cache_expiration_duration) + throw std::runtime_error{"Expired swarm cache."}; + } - // Otherwise try to parse as a node - nodes.push_back(node_from_disk(line).first); + // Otherwise try to parse as a node + nodes.push_back(node_from_disk(line).first); - } catch (const std::exception& e) { - log::warning(cat, "Skipping invalid or expired entry in swarm cache: {}", e.what()); + } catch (const std::exception& e) { + log::warning( + cat, "Skipping invalid or expired entry in swarm cache: {}", e.what()); - // The cache is invalid, we should remove it - if (!checked_swarm_expiration) { - caches_to_remove.emplace_back(path); - break; + // The cache is invalid, we should remove it + if (!checked_swarm_expiration) { + caches_to_remove.emplace_back(path); + break; + } } } + + // If we got nodes the add it to the cache, otherwise we want to remove it + if (!nodes.empty()) + loaded_cache[filename] = std::move(nodes); + else + caches_to_remove.emplace_back(path); } - // If we got nodes the add it to the cache, otherwise we want to remove it - if (!nodes.empty()) - loaded_cache[filename] = std::move(nodes); - else - caches_to_remove.emplace_back(path); - } + swarm_cache = loaded_cache; - swarm_cache = loaded_cache; + // Remove any expired cache files + for (auto& cache_path : caches_to_remove) + fs::remove_all(cache_path); - // Remove any expired cache files - for (auto& cache_path : caches_to_remove) + log::info( + cat, + "Loaded cache of {} snodes, {} swarms.", + snode_pool.size(), + swarm_cache.size()); + } catch (const std::exception& e) { + log::error(cat, "Failed to load cache, will rebuild ({}).", e.what()); fs::remove_all(cache_path); - - log::info(cat, "Loaded cache of {} snodes, {} swarms.", snode_pool.size(), swarm_cache.size()); + } } void Network::disk_write_thread_loop() { From 60b3021d816d8cc45d770d32d98076808e252c57 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 May 2024 17:49:07 +1000 Subject: [PATCH 259/572] Combined the snode_pool and build_paths loopers --- include/session/network.hpp | 13 +- src/network.cpp | 404 ++++++++++++++++++------------------ 2 files changed, 209 insertions(+), 208 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index af7826a9..80c8d75e 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -75,7 +75,6 @@ class Network { ConnectionStatus status; oxen::quic::Network net; std::vector paths; - std::shared_ptr get_snode_pool_loop; std::shared_ptr build_paths_loop; std::shared_ptr endpoint; @@ -306,6 +305,18 @@ class Network { std::function path, std::optional error)> callback); + /// API: network/find_possible_path + /// + /// Picks a random path from the provided paths excluding the provided node if one is available. + /// + /// Inputs: + /// - `excluded_node` -- [in, optional] node which should not be included in the paths. + /// + /// Outputs: + /// - The possible path, if found, and the number of paths provided. + std::pair, uint8_t> find_possible_path( + std::optional excluded_node, std::vector paths); + /// API: network/build_paths_if_needed /// /// Builds onion request paths if needed by opening and testing connections to random service diff --git a/src/network.cpp b/src/network.cpp index 899734d7..aeed98e5 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -559,7 +559,7 @@ connection_info Network::get_connection_info( void Network::with_snode_pool( std::function pool, std::optional error)> callback) { - get_snode_pool_loop->call([this, cb = std::move(callback)]() mutable { + build_paths_loop->call([this, cb = std::move(callback)]() mutable { auto current_pool_info = net.call_get( [this]() -> std::pair< std::vector, @@ -718,88 +718,50 @@ void Network::with_path( std::optional excluded_node, std::function path, std::optional error)> callback) { - // Retrieve a random path that doesn't contain the excluded node - auto select_valid_path = [](std::optional excluded_node, - std::vector paths) -> std::optional { - if (paths.empty()) - return std::nullopt; - - std::vector possible_paths; - std::copy_if( - paths.begin(), - paths.end(), - std::back_inserter(possible_paths), - [&excluded_node](const auto& path) { - return !path.nodes.empty() && - (!excluded_node || - std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == - path.nodes.end()); - }); - - if (possible_paths.empty()) - return std::nullopt; - - CSRNG rng; - std::shuffle(possible_paths.begin(), possible_paths.end(), rng); - - return possible_paths.front(); - }; - - std::pair, uint8_t> path_info; - auto [target_path, paths_count] = path_info; auto current_paths = net.call_get([this]() -> std::vector { return paths; }); - paths_count = current_paths.size(); - target_path = select_valid_path(excluded_node, current_paths); + std::pair, uint8_t> path_info = + find_possible_path(excluded_node, current_paths); + auto& [target_path, paths_count] = path_info; - // If we found a path but it's connection wasn't valid then we should try to reconnect and block - // the path building loop + // The path doesn't have a valid connection so we should try to reconnect (we will end + // up updating the `paths` value so should do this in a blocking way) if (target_path && !target_path->conn_info.is_valid()) { path_info = build_paths_loop->call_get( [this, - excluded_node, - select_valid_path]() mutable -> std::pair, uint8_t> { - // Since this may have been blocked by another thread we should start by trying - // to get a new target path + path = *target_path]() mutable -> std::pair, uint8_t> { + // Since this may have been blocked by another thread we should start by + // making sure the target path is still one of the current paths auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + auto target_path_it = + std::find(current_paths.begin(), current_paths.end(), path); + + // If we didn't find the path then don't bother continuing + if (target_path_it == current_paths.end()) + return {std::nullopt, current_paths.size()}; + + auto info = + get_connection_info(path.nodes[0], [this](quic::connection_interface&) { + // If the connection is re-established update the network + // status back to connected + update_status(ConnectionStatus::connected); + }); - // If we found a path then return it (also build additional paths in the - // background if needed) - if (auto target_path = select_valid_path(excluded_node, current_paths)) { - // If the stream had been closed then try to open a new stream - if (!target_path->conn_info.is_valid()) { - auto info = get_connection_info( - target_path->nodes[0], [this](quic::connection_interface&) { - // If the connection is re-established update the network - // status back to connected - update_status(ConnectionStatus::connected); - }); - - if (!info.is_valid()) - return {std::nullopt, current_paths.size()}; - - auto updated_path = - onion_path{std::move(info), std::move(target_path->nodes), 0}; - - // No need to call the 'paths_changed' callback as the paths haven't - // actually changed, just their connection info - auto paths_count = net.call_get( - [this, target_path, updated_path]() mutable -> uint8_t { - paths.erase( - std::remove( - paths.begin(), paths.end(), target_path), - paths.end()); - paths.emplace_back(updated_path); - return paths.size(); - }); - - return {updated_path, paths_count}; - } - - return {target_path, current_paths.size()}; - } + if (!info.is_valid()) + return {std::nullopt, current_paths.size()}; + + // No need to call the 'paths_changed' callback as the paths haven't + // actually changed, just their connection info + auto updated_path = onion_path{std::move(info), std::move(path.nodes), 0}; + auto paths_count = + net.call_get([this, path, updated_path]() mutable -> uint8_t { + paths.erase( + std::remove(paths.begin(), paths.end(), path), paths.end()); + paths.emplace_back(updated_path); + return paths.size(); + }); - return {std::nullopt, current_paths.size()}; + return {updated_path, paths_count}; }); } @@ -807,11 +769,15 @@ void Network::with_path( if (!target_path) return build_paths_if_needed( std::nullopt, - [excluded_node, - select_path = std::move(select_valid_path), - cb = std::move(callback)]( + [this, excluded_node, cb = std::move(callback)]( std::vector updated_paths, std::optional error) { - cb(select_path(excluded_node, updated_paths), error); + if (error) + return cb(std::nullopt, *error); + auto [target_path, paths_count] = find_possible_path(excluded_node, updated_paths); + + if (!target_path) + return cb(std::nullopt, "Unable to find valid path."); + cb(*target_path, std::nullopt); }); // Build additional paths in the background if we don't have enough @@ -830,6 +796,32 @@ void Network::with_path( callback(target_path, std::nullopt); } +std::pair, uint8_t> Network::find_possible_path( + std::optional excluded_node, std::vector paths) { + if (paths.empty()) + return {std::nullopt, paths.size()}; + + std::vector possible_paths; + std::copy_if( + paths.begin(), + paths.end(), + std::back_inserter(possible_paths), + [&excluded_node](const auto& path) { + return !path.nodes.empty() && + (!excluded_node || + std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == + path.nodes.end()); + }); + + if (possible_paths.empty()) + return {std::nullopt, paths.size()}; + + CSRNG rng; + std::shuffle(possible_paths.begin(), possible_paths.end(), rng); + + return {possible_paths.front(), paths.size()}; +}; + void Network::build_paths_if_needed( std::optional excluded_node, std::function updated_paths, std::optional error)> @@ -839,160 +831,158 @@ void Network::build_paths_if_needed( if (pool.empty()) return cb({}, error.value_or("No snode pool.")); - build_paths_loop->call([this, excluded_node, pool, cb = std::move(cb)]() mutable { - auto current_paths = - net.call_get([this]() -> std::vector { return paths; }); + auto current_paths = + net.call_get([this]() -> std::vector { return paths; }); - // No need to do anything if we already have enough paths - if (current_paths.size() >= target_path_count) - return cb(current_paths, std::nullopt); + // No need to do anything if we already have enough paths + if (current_paths.size() >= target_path_count) + return cb(current_paths, std::nullopt); - // Update the network status - net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); - - // Get the possible guard nodes - log::info(cat, "Building paths."); - std::vector nodes_to_exclude; - std::vector possible_guard_nodes; + // Update the network status + net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); - if (excluded_node) - nodes_to_exclude.push_back(*excluded_node); + // Get the possible guard nodes + log::info(cat, "Building paths."); + std::vector nodes_to_exclude; + std::vector possible_guard_nodes; - for (auto& path : paths) - nodes_to_exclude.insert( - nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); + if (excluded_node) + nodes_to_exclude.push_back(*excluded_node); - if (nodes_to_exclude.empty()) - possible_guard_nodes = pool; - else - std::copy_if( - pool.begin(), - pool.end(), - std::back_inserter(possible_guard_nodes), - [&nodes_to_exclude](const auto& node) { - return std::find( - nodes_to_exclude.begin(), - nodes_to_exclude.end(), - node) == nodes_to_exclude.end(); - }); + for (auto& path : paths) + nodes_to_exclude.insert( + nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); - if (possible_guard_nodes.empty()) { - log::info(cat, "Unable to build paths due to lack of possible guard nodes."); - return cb({}, "Unable to build paths due to lack of possible guard nodes."); - } + if (nodes_to_exclude.empty()) + possible_guard_nodes = pool; + else + std::copy_if( + pool.begin(), + pool.end(), + std::back_inserter(possible_guard_nodes), + [&nodes_to_exclude](const auto& node) { + return std::find( + nodes_to_exclude.begin(), + nodes_to_exclude.end(), + node) == nodes_to_exclude.end(); + }); - // Now that we have a list of possible guard nodes we need to build the paths, first off - // we need to find valid guard nodes for the paths - CSRNG rng; - std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); + if (possible_guard_nodes.empty()) { + log::info(cat, "Unable to build paths due to lack of possible guard nodes."); + return cb({}, "Unable to build paths due to lack of possible guard nodes."); + } - // Split the possible nodes list into a list of lists (one list could run out before the - // other but in most cases this should work fine) - size_t required_paths = (target_path_count - current_paths.size()); - size_t chunk_size = (possible_guard_nodes.size() / required_paths); - std::vector> nodes_to_test; - auto start = 0; + // Now that we have a list of possible guard nodes we need to build the paths, first off + // we need to find valid guard nodes for the paths + CSRNG rng; + std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); - for (size_t i = 0; i < required_paths; ++i) { - auto end = std::min(start + chunk_size, possible_guard_nodes.size()); + // Split the possible nodes list into a list of lists (one list could run out before the + // other but in most cases this should work fine) + size_t required_paths = (target_path_count - current_paths.size()); + size_t chunk_size = (possible_guard_nodes.size() / required_paths); + std::vector> nodes_to_test; + auto start = 0; - if (i == required_paths - 1) - end = possible_guard_nodes.size(); + for (size_t i = 0; i < required_paths; ++i) { + auto end = std::min(start + chunk_size, possible_guard_nodes.size()); - nodes_to_test.emplace_back( - possible_guard_nodes.begin() + start, possible_guard_nodes.begin() + end); - start = end; - } + if (i == required_paths - 1) + end = possible_guard_nodes.size(); - // Start testing guard nodes based on the number of paths we want to build - std::vector>>> - promises(required_paths); - - for (size_t i = 0; i < required_paths; ++i) { - find_valid_guard_node_recursive( - nodes_to_test[i], - [&prom = promises[i]]( - std::optional valid_guard_node, - std::vector unused_nodes) { - try { - if (!valid_guard_node) - std::runtime_error{"Failed to find valid guard node."}; - prom.set_value({*valid_guard_node, unused_nodes}); - } catch (...) { - prom.set_exception(std::current_exception()); - } - }); - } + nodes_to_test.emplace_back( + possible_guard_nodes.begin() + start, possible_guard_nodes.begin() + end); + start = end; + } - // Combine the results (we want to block the `build_paths_loop` until we have retrieved - // the valid guard nodes so we don't double up on requests - try { - std::vector valid_nodes; - std::vector unused_nodes; - - for (auto& prom : promises) { - auto result = prom.get_future().get(); - valid_nodes.emplace_back(result.first); - unused_nodes.insert( - unused_nodes.begin(), result.second.begin(), result.second.end()); - } + // Start testing guard nodes based on the number of paths we want to build + std::vector>>> + promises(required_paths); + + for (size_t i = 0; i < required_paths; ++i) { + find_valid_guard_node_recursive( + nodes_to_test[i], + [&prom = promises[i]]( + std::optional valid_guard_node, + std::vector unused_nodes) { + try { + if (!valid_guard_node) + std::runtime_error{"Failed to find valid guard node."}; + prom.set_value({*valid_guard_node, unused_nodes}); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }); + } - // Make sure we ended up getting enough valid nodes - auto have_enough_guard_nodes = - (current_paths.size() + valid_nodes.size() >= target_path_count); - auto have_enough_unused_nodes = - (unused_nodes.size() >= ((path_size - 1) * target_path_count)); + // Combine the results (we want to block the `build_paths_loop` until we have retrieved + // the valid guard nodes so we don't double up on requests + try { + std::vector valid_nodes; + std::vector unused_nodes; + + for (auto& prom : promises) { + auto result = prom.get_future().get(); + valid_nodes.emplace_back(result.first); + unused_nodes.insert( + unused_nodes.begin(), result.second.begin(), result.second.end()); + } - if (!have_enough_guard_nodes || !have_enough_unused_nodes) - throw std::runtime_error{"Not enough remaining nodes."}; + // Make sure we ended up getting enough valid nodes + auto have_enough_guard_nodes = + (current_paths.size() + valid_nodes.size() >= target_path_count); + auto have_enough_unused_nodes = + (unused_nodes.size() >= ((path_size - 1) * target_path_count)); - // Build the paths - auto updated_paths = current_paths; + if (!have_enough_guard_nodes || !have_enough_unused_nodes) + throw std::runtime_error{"Not enough remaining nodes."}; - for (auto& info : valid_nodes) { - std::vector path{info.node}; + // Build the paths + auto updated_paths = current_paths; - for (auto i = 0; i < path_size - 1; i++) { - auto node = unused_nodes.back(); - unused_nodes.pop_back(); - path.push_back(node); - } + for (auto& info : valid_nodes) { + std::vector path{info.node}; - updated_paths.emplace_back(onion_path{std::move(info), path, 0}); - - // Log that a path was built - std::vector node_descriptions; - std::transform( - path.begin(), - path.end(), - std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info(cat, "Built new onion request path: [{}]", path_description); + for (auto i = 0; i < path_size - 1; i++) { + auto node = unused_nodes.back(); + unused_nodes.pop_back(); + path.push_back(node); } - // Paths were successfully built, update the connection status - update_status(ConnectionStatus::connected); + updated_paths.emplace_back(onion_path{std::move(info), path, 0}); + + // Log that a path was built + std::vector node_descriptions; + std::transform( + path.begin(), + path.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + log::info(cat, "Built new onion request path: [{}]", path_description); + } - // Store the updated paths and update the connection status - std::vector> raw_paths; - for (auto& path : updated_paths) - raw_paths.emplace_back(path.nodes); + // Paths were successfully built, update the connection status + update_status(ConnectionStatus::connected); - net.call([this, updated_paths, raw_paths]() mutable { - paths = updated_paths; + // Store the updated paths and update the connection status + std::vector> raw_paths; + for (auto& path : updated_paths) + raw_paths.emplace_back(path.nodes); - if (paths_changed) - paths_changed(raw_paths); - }); + net.call([this, updated_paths, raw_paths]() mutable { + paths = updated_paths; - // Trigger the callback with the updated paths - cb(updated_paths, std::nullopt); - } catch (const std::exception& e) { - log::info(cat, "Unable to build paths due to error: {}", e.what()); - cb({}, e.what()); - } - }); + if (paths_changed) + paths_changed(raw_paths); + }); + + // Trigger the callback with the updated paths + cb(updated_paths, std::nullopt); + } catch (const std::exception& e) { + log::info(cat, "Unable to build paths due to error: {}", e.what()); + cb({}, e.what()); + } }); } From f4bca74a5fd46619ce96a931d1373104c8429db7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 10 May 2024 20:16:52 +1000 Subject: [PATCH 260/572] Combined the path building and snode pool logic --- include/session/network.hpp | 47 +-- src/network.cpp | 792 +++++++++++++++++++----------------- 2 files changed, 436 insertions(+), 403 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 80c8d75e..cefa7a72 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -75,7 +75,7 @@ class Network { ConnectionStatus status; oxen::quic::Network net; std::vector paths; - std::shared_ptr build_paths_loop; + std::shared_ptr paths_and_pool_loop; std::shared_ptr endpoint; @@ -277,18 +277,21 @@ class Network { service_node target, std::optional conn_established_cb); - /// API: network/with_snode_pool + /// API: network/with_paths_and_pool /// - /// Retrieves the service node pool from the cache. If the cache is empty it will first be - /// populated from the network. + /// Retrieves the current onion request paths and service node pool from the cache. If the + /// cache is empty it will first be populated from the network. /// /// Inputs: - /// - `callback` -- [in] callback to be triggered once we have the service node pool. NOTE: If - /// we are unable to retrieve the service node pool the callback will be triggered with an empty - /// list. - void with_snode_pool( - std::function pool, std::optional error)> - callback); + /// - `callback` -- [in] callback to be triggered once we have built the paths and service node + /// pool. NOTE: If we are unable to build the paths or retrieve the service node pool the + /// callback will be triggered with empty lists and an error. + void with_paths_and_pool( + std::optional excluded_node, + std::function< + void(std::vector updated_paths, + std::vector pool, + std::optional error)> callback); /// API: network/with_path /// @@ -314,24 +317,22 @@ class Network { /// /// Outputs: /// - The possible path, if found, and the number of paths provided. - std::pair, uint8_t> find_possible_path( - std::optional excluded_node, std::vector paths); + std::pair validate_paths_and_pool( + std::vector paths, + std::vector pool, + std::chrono::system_clock::time_point last_pool_update); - /// API: network/build_paths_if_needed + /// API: network/find_possible_path /// - /// Builds onion request paths if needed by opening and testing connections to random service - /// nodes in the snode pool. If we already have enough paths the callback will be triggered - /// with the current paths. + /// Picks a random path from the provided paths excluding the provided node if one is available. /// /// Inputs: /// - `excluded_node` -- [in, optional] node which should not be included in the paths. - /// - `callback` -- [in] callback to be triggered once we have enough paths. NOTE: If we are - /// unable to create the paths the callback will be triggered with an empty list. - void build_paths_if_needed( - std::optional excluded_node, - std::function< - void(std::vector updated_paths, std::optional error)> - callback); + /// + /// Outputs: + /// - The possible path, if found, and the number of paths provided. + std::pair, uint8_t> find_possible_path( + std::optional excluded_node, std::vector paths); /// API: network/get_service_nodes_recursive /// diff --git a/src/network.cpp b/src/network.cpp index aeed98e5..00c21a53 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -237,10 +237,11 @@ Network::Network(std::optional cache_path, bool use_testnet, bool pre_ // Kick off a separate thread to build paths (may as well kick this off early) if (pre_build_paths) { std::thread build_paths_thread( - &Network::build_paths_if_needed, + &Network::with_paths_and_pool, this, std::nullopt, - [](std::optional>, std::optional) {}); + [](std::vector, std::vector, std::optional) { + }); build_paths_thread.detach(); } } @@ -556,162 +557,341 @@ connection_info Network::get_connection_info( // MARK: Snode Pool and Onion Path -void Network::with_snode_pool( - std::function pool, std::optional error)> - callback) { - build_paths_loop->call([this, cb = std::move(callback)]() mutable { - auto current_pool_info = net.call_get( - [this]() -> std::pair< - std::vector, - std::chrono::system_clock::time_point> { - return {snode_pool, last_snode_pool_update}; - }); +using paths_and_pool_result = + std::tuple, std::vector, std::optional>; +using paths_and_pool_info = std::tuple< + std::vector, + std::vector, + std::chrono::system_clock::time_point>; - // Check if the cache is too old or if the updated timestamp is invalid - auto cache_duration = std::chrono::duration_cast( - std::chrono::system_clock::now() - current_pool_info.second); - auto cache_has_expired = - (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); +void Network::with_paths_and_pool( + std::optional excluded_node, + std::function< + void(std::vector updated_paths, + std::vector pool, + std::optional error)> callback) { + auto [current_paths, pool, last_pool_update] = net.call_get([this]() -> paths_and_pool_info { + return {paths, snode_pool, last_snode_pool_update}; + }); - // If the cache has enough snodes and it hasn't expired then return it - if (current_pool_info.first.size() >= min_snode_pool_count && !cache_has_expired) - return cb(current_pool_info.first, std::nullopt); + // Check if the current data is valid, and if so just return it + auto [paths_valid, pool_valid] = validate_paths_and_pool(current_paths, pool, last_pool_update); - // Update the network status - net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + if (paths_valid && pool_valid) + return callback(current_paths, pool, std::nullopt); - // Define the response handler to avoid code duplication - auto handle_nodes_response = [](std::promise>& prom) { - return [&prom](std::vector nodes, std::optional error) { - try { - if (nodes.empty()) - throw std::runtime_error{error.value_or("No nodes received.")}; - prom.set_value(nodes); - } catch (...) { - prom.set_exception(std::current_exception()); + auto [updated_paths, updated_pool, error] = + paths_and_pool_loop->call_get([this, excluded_node]() mutable -> paths_and_pool_result { + auto [current_paths, pool, last_pool_update] = + net.call_get([this]() -> paths_and_pool_info { + return {paths, snode_pool, last_snode_pool_update}; + }); + + // Check if the current data is valid, and if so just return it + auto [paths_valid, pool_valid] = + validate_paths_and_pool(current_paths, pool, last_pool_update); + + if (paths_valid && pool_valid) + return {current_paths, pool, std::nullopt}; + + // Update the network status + net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + + // If the pool isn't valid then we should update it + CSRNG rng; + std::vector pool_result = pool; + std::vector paths_result = current_paths; + + // Populate the snode pool if needed + if (!pool_valid) { + // Define the response handler to avoid code duplication + auto handle_nodes_response = [](std::promise>& prom) { + return [&prom](std::vector nodes, + std::optional error) { + try { + if (nodes.empty()) + throw std::runtime_error{error.value_or("No nodes received.")}; + prom.set_value(nodes); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }; + }; + + try { + // If we don't have enough nodes in the current cached pool then we need to + // fetch from the seed nodes + if (pool_result.size() < min_snode_pool_count) { + log::info(cat, "Fetching from seed nodes."); + pool_result = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); + + // Just in case, make sure the seed nodes are have values + if (pool_result.empty()) + throw std::runtime_error{"Insufficient seed nodes."}; + + std::shuffle(pool_result.begin(), pool_result.end(), rng); + std::promise> prom; + + get_service_nodes( + pool_result.front(), 256, handle_nodes_response(prom)); + + // We want to block the `get_snode_pool_loop` until we have retrieved + // the snode pool so we don't double up on requests + pool_result = prom.get_future().get(); + log::info(cat, "Retrieved snode pool from seed node."); + } else { + // Pick ~9 random snodes from the current cache to fetch nodes from (we + // want to fetch from 3 snodes and retry up to 3 times if needed) + std::shuffle(pool_result.begin(), pool_result.end(), rng); + size_t num_retries = + std::min(pool_result.size() / 3, static_cast(3)); + + log::info(cat, "Fetching from random expired cache nodes."); + std::vector first_nodes( + pool_result.begin(), pool_result.begin() + num_retries); + std::vector second_nodes( + pool_result.begin() + num_retries, + pool_result.begin() + (num_retries * 2)); + std::vector third_nodes( + pool_result.begin() + (num_retries * 2), + pool_result.begin() + (num_retries * 3)); + std::promise> prom1; + std::promise> prom2; + std::promise> prom3; + + // Kick off 3 concurrent requests + get_service_nodes_recursive( + first_nodes, std::nullopt, handle_nodes_response(prom1)); + get_service_nodes_recursive( + second_nodes, std::nullopt, handle_nodes_response(prom2)); + get_service_nodes_recursive( + third_nodes, std::nullopt, handle_nodes_response(prom3)); + + // We want to block the `get_snode_pool_loop` until we have retrieved + // the snode pool so we don't double up on requests + auto first_result_nodes = prom1.get_future().get(); + auto second_result_nodes = prom2.get_future().get(); + auto third_result_nodes = prom3.get_future().get(); + + // Sort the vectors (so make it easier to find the + // intersection) + std::stable_sort(first_result_nodes.begin(), first_result_nodes.end()); + std::stable_sort( + second_result_nodes.begin(), second_result_nodes.end()); + std::stable_sort(third_result_nodes.begin(), third_result_nodes.end()); + + // Get the intersection of the vectors + std::vector first_second_intersection; + std::vector intersection; + + std::set_intersection( + first_result_nodes.begin(), + first_result_nodes.end(), + second_result_nodes.begin(), + second_result_nodes.end(), + std::back_inserter(first_second_intersection), + [](const auto& a, const auto& b) { return a == b; }); + std::set_intersection( + first_second_intersection.begin(), + first_second_intersection.end(), + third_result_nodes.begin(), + third_result_nodes.end(), + std::back_inserter(intersection), + [](const auto& a, const auto& b) { return a == b; }); + + // Since we sorted it we now need to shuffle it again + std::shuffle(intersection.begin(), intersection.end(), rng); + + // Update the cache to be the first 256 nodes from + // the intersection + auto size = std::min(256, static_cast(intersection.size())); + pool_result = std::vector( + intersection.begin(), intersection.begin() + size); + log::info(cat, "Retrieved snode pool."); + } + } catch (const std::exception& e) { + log::info(cat, "Failed to get snode pool: {}", e.what()); + return {{}, {}, e.what()}; + } } - }; - }; - try { - CSRNG rng; - std::vector target_pool; + // Build new paths if needed + if (!paths_valid) { + try { + // Get the possible guard nodes + log::info(cat, "Building paths."); + std::vector nodes_to_exclude; + std::vector possible_guard_nodes; + + if (excluded_node) + nodes_to_exclude.push_back(*excluded_node); + + for (auto& path : paths_result) + nodes_to_exclude.insert( + nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); + + if (nodes_to_exclude.empty()) + possible_guard_nodes = pool_result; + else + std::copy_if( + pool_result.begin(), + pool_result.end(), + std::back_inserter(possible_guard_nodes), + [&nodes_to_exclude](const auto& node) { + return std::find( + nodes_to_exclude.begin(), + nodes_to_exclude.end(), + node) == nodes_to_exclude.end(); + }); + + if (possible_guard_nodes.empty()) + throw std::runtime_error{ + "Unable to build paths due to lack of possible guard nodes."}; - // If we don't have enough nodes in the current cached pool then we need to fetch from - // the seed nodes - if (current_pool_info.first.size() < min_snode_pool_count) { - log::info(cat, "Fetching from seed nodes."); - target_pool = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); + // Now that we have a list of possible guard nodes we need to build the + // paths, first off we need to find valid guard nodes for the paths + std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); - // Just in case, make sure the seed nodes are have values - if (target_pool.empty()) - throw std::runtime_error{"Insufficient seed nodes."}; + // Split the possible nodes list into a list of lists (one list could run + // out before the other but in most cases this should work fine) + size_t required_paths = (target_path_count - current_paths.size()); + size_t chunk_size = (possible_guard_nodes.size() / required_paths); + std::vector> nodes_to_test; + auto start = 0; - std::shuffle(target_pool.begin(), target_pool.end(), rng); - std::promise> prom; + for (size_t i = 0; i < required_paths; ++i) { + auto end = std::min(start + chunk_size, possible_guard_nodes.size()); - get_service_nodes(target_pool.front(), 256, handle_nodes_response(prom)); + if (i == required_paths - 1) + end = possible_guard_nodes.size(); - // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool - // so we don't double up on requests - auto nodes = prom.get_future().get(); + nodes_to_test.emplace_back( + possible_guard_nodes.begin() + start, + possible_guard_nodes.begin() + end); + start = end; + } - // Update the cache - net.call([this, nodes]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - snode_pool = nodes; - last_snode_pool_update = std::chrono::system_clock::now(); - need_pool_write = true; - need_write = true; + // Start testing guard nodes based on the number of paths we want to build + std::vector< + std::promise>>> + promises(required_paths); + + for (size_t i = 0; i < required_paths; ++i) { + find_valid_guard_node_recursive( + nodes_to_test[i], + [&prom = promises[i]]( + std::optional valid_guard_node, + std::vector unused_nodes) { + try { + if (!valid_guard_node) + std::runtime_error{ + "Failed to find valid guard node."}; + prom.set_value({*valid_guard_node, unused_nodes}); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }); + } + + // Combine the results (we want to block the `paths_and_pool_loop` until we + // have retrieved the valid guard nodes so we don't double up on requests + std::vector valid_nodes; + std::vector unused_nodes; + + for (auto& prom : promises) { + auto result = prom.get_future().get(); + valid_nodes.emplace_back(result.first); + unused_nodes.insert( + unused_nodes.begin(), + result.second.begin(), + result.second.end()); + } + + // Make sure we ended up getting enough valid nodes + auto have_enough_guard_nodes = + (current_paths.size() + valid_nodes.size() >= target_path_count); + auto have_enough_unused_nodes = + (unused_nodes.size() >= ((path_size - 1) * target_path_count)); + + if (!have_enough_guard_nodes || !have_enough_unused_nodes) + throw std::runtime_error{"Not enough remaining nodes."}; + + // Build the new paths + for (auto& info : valid_nodes) { + std::vector path{info.node}; + + for (auto i = 0; i < path_size - 1; i++) { + auto node = unused_nodes.back(); + unused_nodes.pop_back(); + path.push_back(node); + } + + paths_result.emplace_back(onion_path{std::move(info), path, 0}); + + // Log that a path was built + std::vector node_descriptions; + std::transform( + path.begin(), + path.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + log::info(cat, "Built new onion request path: [{}]", path_description); + } + } catch (const std::exception& e) { + log::info(cat, "Unable to build paths due to error: {}", e.what()); + return {{}, {}, e.what()}; + } + } + + // Store to instance variables + net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { + if (!paths_valid) { + paths = paths_result; + + // Call the paths_changed callback if provided + if (paths_changed) { + std::vector> raw_paths; + for (auto& path : paths_result) + raw_paths.emplace_back(path.nodes); + + paths_changed(raw_paths); + } + } + + // Only update the disk cache if the snode pool was updated + if (!pool_valid) { + { + std::lock_guard lock{snode_cache_mutex}; + snode_pool = pool_result; + last_snode_pool_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); } - snode_cache_cv.notify_one(); }); - log::info(cat, "Updated snode pool from seed node."); - return cb(nodes, std::nullopt); - } + // Paths were successfully built, update the connection status + update_status(ConnectionStatus::connected); - // Pick ~9 random snodes from the current cache to fetch nodes from (we want to - // fetch from 3 snodes and retry up to 3 times if needed) - target_pool = current_pool_info.first; - std::shuffle(target_pool.begin(), target_pool.end(), rng); - size_t num_retries = std::min(target_pool.size() / 3, static_cast(3)); - - log::info(cat, "Fetching from random expired cache nodes."); - std::vector first_nodes( - target_pool.begin(), target_pool.begin() + num_retries); - std::vector second_nodes( - target_pool.begin() + num_retries, target_pool.begin() + (num_retries * 2)); - std::vector third_nodes( - target_pool.begin() + (num_retries * 2), - target_pool.begin() + (num_retries * 3)); - std::promise> prom1; - std::promise> prom2; - std::promise> prom3; - - // Kick off 3 concurrent requests - get_service_nodes_recursive(first_nodes, std::nullopt, handle_nodes_response(prom1)); - get_service_nodes_recursive(second_nodes, std::nullopt, handle_nodes_response(prom2)); - get_service_nodes_recursive(third_nodes, std::nullopt, handle_nodes_response(prom3)); - - // We want to block the `get_snode_pool_loop` until we have retrieved the snode pool - // so we don't double up on requests - auto first_result_nodes = prom1.get_future().get(); - auto second_result_nodes = prom2.get_future().get(); - auto third_result_nodes = prom3.get_future().get(); - - // Sort the vectors (so make it easier to find the - // intersection) - std::stable_sort(first_result_nodes.begin(), first_result_nodes.end()); - std::stable_sort(second_result_nodes.begin(), second_result_nodes.end()); - std::stable_sort(third_result_nodes.begin(), third_result_nodes.end()); - - // Get the intersection of the vectors - std::vector first_second_intersection; - std::vector intersection; - - std::set_intersection( - first_result_nodes.begin(), - first_result_nodes.end(), - second_result_nodes.begin(), - second_result_nodes.end(), - std::back_inserter(first_second_intersection), - [](const auto& a, const auto& b) { return a == b; }); - std::set_intersection( - first_second_intersection.begin(), - first_second_intersection.end(), - third_result_nodes.begin(), - third_result_nodes.end(), - std::back_inserter(intersection), - [](const auto& a, const auto& b) { return a == b; }); - - // Since we sorted it we now need to shuffle it again - std::shuffle(intersection.begin(), intersection.end(), rng); - - // Update the cache to be the first 256 nodes from - // the intersection - auto size = std::min(256, static_cast(intersection.size())); - std::vector updated_pool( - intersection.begin(), intersection.begin() + size); - net.call([this, updated_pool]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - snode_pool = updated_pool; - last_snode_pool_update = std::chrono::system_clock::now(); - need_pool_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); + return {paths_result, pool_result, std::nullopt}; }); - log::info(cat, "Updated snode pool."); - cb(updated_pool, std::nullopt); - } catch (const std::exception& e) { - log::info(cat, "Failed to get snode pool: {}", e.what()); - cb({}, e.what()); - } - }); + return callback(updated_paths, updated_pool, error); +} + +std::pair Network::validate_paths_and_pool( + std::vector paths, + std::vector pool, + std::chrono::system_clock::time_point last_pool_update) { + auto cache_duration = std::chrono::duration_cast( + std::chrono::system_clock::now() - last_pool_update); + auto cache_has_expired = + (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); + + return {(paths.size() >= target_path_count), + (pool.size() >= min_snode_pool_count && !cache_has_expired)}; } void Network::with_path( @@ -726,7 +906,7 @@ void Network::with_path( // The path doesn't have a valid connection so we should try to reconnect (we will end // up updating the `paths` value so should do this in a blocking way) if (target_path && !target_path->conn_info.is_valid()) { - path_info = build_paths_loop->call_get( + path_info = paths_and_pool_loop->call_get( [this, path = *target_path]() mutable -> std::pair, uint8_t> { // Since this may have been blocked by another thread we should start by @@ -767,13 +947,16 @@ void Network::with_path( // If we didn't get a target path then we have to build paths if (!target_path) - return build_paths_if_needed( - std::nullopt, + return with_paths_and_pool( + excluded_node, [this, excluded_node, cb = std::move(callback)]( - std::vector updated_paths, std::optional error) { + std::vector updated_paths, + std::vector, + std::optional error) { if (error) return cb(std::nullopt, *error); - auto [target_path, paths_count] = find_possible_path(excluded_node, updated_paths); + auto [target_path, paths_count] = + find_possible_path(excluded_node, updated_paths); if (!target_path) return cb(std::nullopt, "Unable to find valid path."); @@ -782,12 +965,14 @@ void Network::with_path( // Build additional paths in the background if we don't have enough if (paths_count < target_path_count) { - std::thread build_paths_thread( - &Network::build_paths_if_needed, + std::thread build_additional_paths_thread( + &Network::with_paths_and_pool, this, std::nullopt, - [](std::optional>, std::optional) {}); - build_paths_thread.detach(); + [](std::optional>, + std::vector, + std::optional) {}); + build_additional_paths_thread.detach(); } // We have a valid path, update the status in case we had flagged it as disconnected for @@ -822,170 +1007,6 @@ std::pair, uint8_t> Network::find_possible_path( return {possible_paths.front(), paths.size()}; }; -void Network::build_paths_if_needed( - std::optional excluded_node, - std::function updated_paths, std::optional error)> - callback) { - with_snode_pool([this, excluded_node, cb = std::move(callback)]( - std::vector pool, std::optional error) { - if (pool.empty()) - return cb({}, error.value_or("No snode pool.")); - - auto current_paths = - net.call_get([this]() -> std::vector { return paths; }); - - // No need to do anything if we already have enough paths - if (current_paths.size() >= target_path_count) - return cb(current_paths, std::nullopt); - - // Update the network status - net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); - - // Get the possible guard nodes - log::info(cat, "Building paths."); - std::vector nodes_to_exclude; - std::vector possible_guard_nodes; - - if (excluded_node) - nodes_to_exclude.push_back(*excluded_node); - - for (auto& path : paths) - nodes_to_exclude.insert( - nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); - - if (nodes_to_exclude.empty()) - possible_guard_nodes = pool; - else - std::copy_if( - pool.begin(), - pool.end(), - std::back_inserter(possible_guard_nodes), - [&nodes_to_exclude](const auto& node) { - return std::find( - nodes_to_exclude.begin(), - nodes_to_exclude.end(), - node) == nodes_to_exclude.end(); - }); - - if (possible_guard_nodes.empty()) { - log::info(cat, "Unable to build paths due to lack of possible guard nodes."); - return cb({}, "Unable to build paths due to lack of possible guard nodes."); - } - - // Now that we have a list of possible guard nodes we need to build the paths, first off - // we need to find valid guard nodes for the paths - CSRNG rng; - std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); - - // Split the possible nodes list into a list of lists (one list could run out before the - // other but in most cases this should work fine) - size_t required_paths = (target_path_count - current_paths.size()); - size_t chunk_size = (possible_guard_nodes.size() / required_paths); - std::vector> nodes_to_test; - auto start = 0; - - for (size_t i = 0; i < required_paths; ++i) { - auto end = std::min(start + chunk_size, possible_guard_nodes.size()); - - if (i == required_paths - 1) - end = possible_guard_nodes.size(); - - nodes_to_test.emplace_back( - possible_guard_nodes.begin() + start, possible_guard_nodes.begin() + end); - start = end; - } - - // Start testing guard nodes based on the number of paths we want to build - std::vector>>> - promises(required_paths); - - for (size_t i = 0; i < required_paths; ++i) { - find_valid_guard_node_recursive( - nodes_to_test[i], - [&prom = promises[i]]( - std::optional valid_guard_node, - std::vector unused_nodes) { - try { - if (!valid_guard_node) - std::runtime_error{"Failed to find valid guard node."}; - prom.set_value({*valid_guard_node, unused_nodes}); - } catch (...) { - prom.set_exception(std::current_exception()); - } - }); - } - - // Combine the results (we want to block the `build_paths_loop` until we have retrieved - // the valid guard nodes so we don't double up on requests - try { - std::vector valid_nodes; - std::vector unused_nodes; - - for (auto& prom : promises) { - auto result = prom.get_future().get(); - valid_nodes.emplace_back(result.first); - unused_nodes.insert( - unused_nodes.begin(), result.second.begin(), result.second.end()); - } - - // Make sure we ended up getting enough valid nodes - auto have_enough_guard_nodes = - (current_paths.size() + valid_nodes.size() >= target_path_count); - auto have_enough_unused_nodes = - (unused_nodes.size() >= ((path_size - 1) * target_path_count)); - - if (!have_enough_guard_nodes || !have_enough_unused_nodes) - throw std::runtime_error{"Not enough remaining nodes."}; - - // Build the paths - auto updated_paths = current_paths; - - for (auto& info : valid_nodes) { - std::vector path{info.node}; - - for (auto i = 0; i < path_size - 1; i++) { - auto node = unused_nodes.back(); - unused_nodes.pop_back(); - path.push_back(node); - } - - updated_paths.emplace_back(onion_path{std::move(info), path, 0}); - - // Log that a path was built - std::vector node_descriptions; - std::transform( - path.begin(), - path.end(), - std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info(cat, "Built new onion request path: [{}]", path_description); - } - - // Paths were successfully built, update the connection status - update_status(ConnectionStatus::connected); - - // Store the updated paths and update the connection status - std::vector> raw_paths; - for (auto& path : updated_paths) - raw_paths.emplace_back(path.nodes); - - net.call([this, updated_paths, raw_paths]() mutable { - paths = updated_paths; - - if (paths_changed) - paths_changed(raw_paths); - }); - - // Trigger the callback with the updated paths - cb(updated_paths, std::nullopt); - } catch (const std::exception& e) { - log::info(cat, "Unable to build paths due to error: {}", e.what()); - cb({}, e.what()); - } - }); -} - // MARK: Multi-request logic void Network::get_service_nodes_recursive( @@ -1175,62 +1196,69 @@ void Network::get_swarm( return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from - with_snode_pool([this, swarm_pubkey, cb = std::move(callback)]( - std::vector pool, std::optional /*error*/) { - if (pool.empty()) - return cb({}); - - auto updated_pool = pool; - CSRNG rng; - std::shuffle(updated_pool.begin(), updated_pool.end(), rng); - auto node = updated_pool.front(); - - nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; - nlohmann::json payload{ - {"method", "get_swarm"}, - {"params", params}, - }; - - send_onion_request( - node, - ustring{quic::to_usv(payload.dump())}, - swarm_pubkey, - quic::DEFAULT_TIMEOUT, - false, - [this, swarm_pubkey, cb = std::move(cb)]( - bool success, bool timeout, int16_t, std::optional response) { - if (!success || timeout || !response) - return cb({}); - - std::vector swarm; - - try { - nlohmann::json response_json = nlohmann::json::parse(*response); - - if (!response_json.contains("snodes") || - !response_json["snodes"].is_array()) - throw std::runtime_error{"JSON missing swarm field."}; - - for (auto& snode : response_json["snodes"]) - swarm.emplace_back(node_from_json(snode)); - } catch (...) { - return cb({}); - } - - // Update the cache - net.call([this, swarm_pubkey, swarm]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey.hex()] = swarm; - need_swarm_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - }); + with_paths_and_pool( + std::nullopt, + [this, swarm_pubkey, cb = std::move(callback)]( + std::vector, + std::vector pool, + std::optional) { + if (pool.empty()) + return cb({}); + + auto updated_pool = pool; + CSRNG rng; + std::shuffle(updated_pool.begin(), updated_pool.end(), rng); + auto node = updated_pool.front(); + + nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; + nlohmann::json payload{ + {"method", "get_swarm"}, + {"params", params}, + }; + + send_onion_request( + node, + ustring{quic::to_usv(payload.dump())}, + swarm_pubkey, + quic::DEFAULT_TIMEOUT, + false, + [this, swarm_pubkey, cb = std::move(cb)]( + bool success, + bool timeout, + int16_t, + std::optional response) { + if (!success || timeout || !response) + return cb({}); + + std::vector swarm; + + try { + nlohmann::json response_json = nlohmann::json::parse(*response); + + if (!response_json.contains("snodes") || + !response_json["snodes"].is_array()) + throw std::runtime_error{"JSON missing swarm field."}; + + for (auto& snode : response_json["snodes"]) + swarm.emplace_back(node_from_json(snode)); + } catch (...) { + return cb({}); + } + + // Update the cache + net.call([this, swarm_pubkey, swarm]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[swarm_pubkey.hex()] = swarm; + need_swarm_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + }); - cb(swarm); - }); - }); + cb(swarm); + }); + }); } void Network::set_swarm( @@ -1248,18 +1276,22 @@ void Network::set_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { - with_snode_pool([count, cb = std::move(callback)]( - std::vector pool, std::optional /*error*/) { - if (pool.size() < count) - return cb({}); - - auto random_pool = pool; - CSRNG rng; - std::shuffle(random_pool.begin(), random_pool.end(), rng); - - std::vector result(random_pool.begin(), random_pool.begin() + count); - cb(result); - }); + with_paths_and_pool( + std::nullopt, + [count, cb = std::move(callback)]( + std::vector, + std::vector pool, + std::optional) { + if (pool.size() < count) + return cb({}); + + auto random_pool = pool; + CSRNG rng; + std::shuffle(random_pool.begin(), random_pool.end(), rng); + + std::vector result(random_pool.begin(), random_pool.begin() + count); + cb(result); + }); } // MARK: Request Handling From 163a2feecc8c7e2804c60b89fd5eb5187764219e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 May 2024 17:48:14 -0300 Subject: [PATCH 261/572] Set badbits but not failbit after opening getline sets failbit if it reads nothing (e.g. at eof) so we really don't want failbit exceptions to fire, *except* for the open call which, for some inexplicable reason, sets failbit rather than badbit if it fails to open the file. We do, however, always want `badbit` which indicates some serious error (e.g. I/O error, full disk, etc.) on both if/ofstreams. --- src/file.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/file.cpp b/src/file.cpp index 46275fca..8f0ee656 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -7,16 +7,15 @@ std::ofstream open_for_writing(const fs::path& filename) { std::ofstream out; out.exceptions(std::ios_base::failbit | std::ios_base::badbit); out.open(filename, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); - if (!out.is_open()) - throw std::runtime_error{"Failed to open file for writing: " + std::string(filename)}; + out.exceptions(std::ios_base::badbit); return out; } std::ifstream open_for_reading(const fs::path& filename) { std::ifstream in; + in.exceptions(std::ios_base::failbit | std::ios_base::badbit); in.open(filename, std::ios::binary | std::ios::in); - if (!in.is_open()) - throw std::runtime_error{"Failed to open file for reading: " + std::string(filename)}; + in.exceptions(std::ios_base::badbit); return in; } From 6e89b4b3cb7c4c2623a71caa2e60d8c394d7d608 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 May 2024 20:17:47 -0300 Subject: [PATCH 262/572] Bump to latest libquic & oxen-logging --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 6106356c..f4b75ec0 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 6106356c31be4def16c0a56a17ecc290bb7060e2 +Subproject commit f4b75ec0f5ae985ffd6b7df567f1b23959b56b5c From 0dea82b2105cd70bc5aaf4c7eb92ca0387a5f20e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 May 2024 20:20:58 -0300 Subject: [PATCH 263/572] Add hack to hide broken log path with xcode 15 --- external/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 90de09e5..85cd9053 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -113,8 +113,18 @@ endif() if(NOT TARGET oxen::logging) add_subdirectory(oxen-libquic/external/oxen-logging) endif() + oxen_logging_add_source_dir("${PROJECT_SOURCE_DIR}") +# Apple xcode 15 has a completely broken std::source_location; we can't fix it, but at least we can +# hack up the source locations to hide the path that it uses (which is the useless path to +# oxen/log.hpp where the info/critical/etc. bodies are). +if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL AppleClang AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16) + message(WARNING "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} is broken: filenames in logging statements will not display properly") + oxen_logging_add_source_dir("${CMAKE_CURRENT_SOURCE_DIR}/oxen-libquic/external/oxen-logging/include/oxen") +endif() + + if(CMAKE_C_COMPILER_LAUNCHER) set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") endif() From b36c727f1e1f6be87718ce2203332ae48f2d89a5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 May 2024 20:21:56 -0300 Subject: [PATCH 264/572] Fix Loop variable initialization --- src/network.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 00c21a53..845d52b4 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -225,8 +225,7 @@ Network::Network(std::optional cache_path, bool use_testnet, bool pre_ should_cache_to_disk{cache_path}, cache_path{cache_path.value_or(default_cache_path)} { log::info(cat, "Test info log - Create network"); - get_snode_pool_loop = std::make_shared(); - build_paths_loop = std::make_shared(); + paths_and_pool_loop = std::make_shared(); // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { From ebb618846f62968076aeb035b8aeffc435665153 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 09:52:39 +1000 Subject: [PATCH 265/572] Updated the unit tests for Apple builds (file path differs) --- tests/test_logging.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index b870de35..47cd9882 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -64,6 +64,16 @@ TEST_CASE("Logging callbacks", "[logging]") { REQUIRE(simple_logs.size() == 2); REQUIRE(full_logs.size() == 2); + +#if defined(__APPLE__) && defined(__clang__) + CHECK(fixup_log(simple_logs[0]) == + "[] [] [test.a:critical|log.hpp:177] abc 42\n"); + CHECK(fixup_log(simple_logs[1]) == "[] [] [test.b:info|log.hpp:98] hi\n"); + CHECK(fixup_log(full_logs[0]) == + "test.a|critical|[] [] [test.a:critical|log.hpp:177] abc 42\n"); + CHECK(fixup_log(full_logs[1]) == + "test.b|info|[] [] [test.b:info|log.hpp:98] hi\n"); +#elif CHECK(fixup_log(simple_logs[0]) == "[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( line0)); @@ -75,6 +85,7 @@ TEST_CASE("Logging callbacks", "[logging]") { CHECK(fixup_log(full_logs[1]) == "test.b|info|[] [] [test.b:info|tests/test_logging.cpp:{}] hi\n"_format( line1)); +#endif } TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { From 6695f83c962242ccdbc993b2b00f44a0b14976c0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 09:56:10 +1000 Subject: [PATCH 266/572] Silly autocomplete mistake --- tests/test_logging.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 47cd9882..84840aa9 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -73,7 +73,7 @@ TEST_CASE("Logging callbacks", "[logging]") { "test.a|critical|[] [] [test.a:critical|log.hpp:177] abc 42\n"); CHECK(fixup_log(full_logs[1]) == "test.b|info|[] [] [test.b:info|log.hpp:98] hi\n"); -#elif +#else CHECK(fixup_log(simple_logs[0]) == "[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( line0)); From a335598692572588dbc7b6c3f7f2621a3bb1d04c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 10:34:52 +1000 Subject: [PATCH 267/572] Testing debian test failure 1 --- src/network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index 845d52b4..b3ecc65f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -876,7 +876,7 @@ void Network::with_paths_and_pool( return {paths_result, pool_result, std::nullopt}; }); - +throw std::runtime_error{"CI TEST - throw after got pool and paths"}; return callback(updated_paths, updated_pool, error); } From 0c30c54913ab8a484c94908b2a976c3ef7b5cdf7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 10:40:11 +1000 Subject: [PATCH 268/572] Testing debian test failure 2 --- src/network.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index b3ecc65f..b7ffe4c6 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -713,7 +713,7 @@ void Network::with_paths_and_pool( return {{}, {}, e.what()}; } } - +throw std::runtime_error{"CI TEST - throw after got pool"}; // Build new paths if needed if (!paths_valid) { try { @@ -876,7 +876,7 @@ void Network::with_paths_and_pool( return {paths_result, pool_result, std::nullopt}; }); -throw std::runtime_error{"CI TEST - throw after got pool and paths"}; + return callback(updated_paths, updated_pool, error); } From 3eeb56d6684f6c0eccb44c54305046633437b6b9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 10:49:31 +1000 Subject: [PATCH 269/572] Testing debian test failure 3 --- src/network.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index b7ffe4c6..ad9f3d59 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -713,7 +713,7 @@ void Network::with_paths_and_pool( return {{}, {}, e.what()}; } } -throw std::runtime_error{"CI TEST - throw after got pool"}; + // Build new paths if needed if (!paths_valid) { try { @@ -774,7 +774,7 @@ throw std::runtime_error{"CI TEST - throw after got pool"}; std::vector< std::promise>>> promises(required_paths); - + throw std::runtime_error{"CI TEST - throw before testing guard nodes"}; for (size_t i = 0; i < required_paths; ++i) { find_valid_guard_node_recursive( nodes_to_test[i], From a92f8001f5c43422d04bdc4f0963171921808f39 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 10:58:41 +1000 Subject: [PATCH 270/572] Testing debian test failure 4 --- src/network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index ad9f3d59..4b1a297e 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -805,7 +805,7 @@ void Network::with_paths_and_pool( result.second.begin(), result.second.end()); } - + throw std::runtime_error{"CI TEST - throw after testing guard nodes"}; // Make sure we ended up getting enough valid nodes auto have_enough_guard_nodes = (current_paths.size() + valid_nodes.size() >= target_path_count); From 9bf1ff6b883dfbaf1efdab0399c4a89c023b61ad Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:05:38 +1000 Subject: [PATCH 271/572] Testing debian test failure 5 --- src/network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index 4b1a297e..4d644c6d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -774,7 +774,7 @@ void Network::with_paths_and_pool( std::vector< std::promise>>> promises(required_paths); - throw std::runtime_error{"CI TEST - throw before testing guard nodes"}; + for (size_t i = 0; i < required_paths; ++i) { find_valid_guard_node_recursive( nodes_to_test[i], From 6992d1fa717e67787926b59fa878824d741d05f8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:11:39 +1000 Subject: [PATCH 272/572] Testing debian test failure 6 --- src/network.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 4d644c6d..1767f9c7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -805,7 +805,7 @@ void Network::with_paths_and_pool( result.second.begin(), result.second.end()); } - throw std::runtime_error{"CI TEST - throw after testing guard nodes"}; + // Make sure we ended up getting enough valid nodes auto have_enough_guard_nodes = (current_paths.size() + valid_nodes.size() >= target_path_count); @@ -842,7 +842,7 @@ void Network::with_paths_and_pool( return {{}, {}, e.what()}; } } - +throw std::runtime_error{"CI TEST - throw after building paths"}; // Store to instance variables net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { if (!paths_valid) { From d0832946bd6c4a64986fbff63dd26df8d085d5c5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:19:41 +1000 Subject: [PATCH 273/572] Testing debian test failure 7 --- src/network.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 1767f9c7..c0219096 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -842,7 +842,7 @@ void Network::with_paths_and_pool( return {{}, {}, e.what()}; } } -throw std::runtime_error{"CI TEST - throw after building paths"}; + // Store to instance variables net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { if (!paths_valid) { @@ -857,7 +857,7 @@ throw std::runtime_error{"CI TEST - throw after building paths"}; paths_changed(raw_paths); } } - +throw std::runtime_error{"CI TEST - throw after updating paths"}; // Only update the disk cache if the snode pool was updated if (!pool_valid) { { From 49a76c77ddcb71ef5479ebf598799888a25b194c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:27:59 +1000 Subject: [PATCH 274/572] Testing debian test failure 8 --- src/network.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index c0219096..92a6ec29 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -845,6 +845,7 @@ void Network::with_paths_and_pool( // Store to instance variables net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { +throw std::runtime_error{"CI TEST - throw in net.call"}; if (!paths_valid) { paths = paths_result; @@ -857,7 +858,7 @@ void Network::with_paths_and_pool( paths_changed(raw_paths); } } -throw std::runtime_error{"CI TEST - throw after updating paths"}; + // Only update the disk cache if the snode pool was updated if (!pool_valid) { { From 5605edc3f350c6d5709fb088b8bbe6bbbea95814 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:38:33 +1000 Subject: [PATCH 275/572] Ensured network status changes run in the network looper --- src/network.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 92a6ec29..d82ac111 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -870,10 +870,10 @@ throw std::runtime_error{"CI TEST - throw in net.call"}; } snode_cache_cv.notify_one(); } - }); - // Paths were successfully built, update the connection status - update_status(ConnectionStatus::connected); + // Paths were successfully built, update the connection status + update_status(ConnectionStatus::connected); + }); return {paths_result, pool_result, std::nullopt}; }); @@ -977,7 +977,7 @@ void Network::with_path( // We have a valid path, update the status in case we had flagged it as disconnected for // some reason - update_status(ConnectionStatus::connected); + net.call([this]() mutable { update_status(ConnectionStatus::connected); }); callback(target_path, std::nullopt); } From 9961c01a78bd1bf1afb97abb43fc465b878530cf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:38:44 +1000 Subject: [PATCH 276/572] Testing debian test failure 9 --- src/network.cpp | 51 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index d82ac111..c46c2195 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -844,32 +844,31 @@ void Network::with_paths_and_pool( } // Store to instance variables - net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { -throw std::runtime_error{"CI TEST - throw in net.call"}; - if (!paths_valid) { - paths = paths_result; - - // Call the paths_changed callback if provided - if (paths_changed) { - std::vector> raw_paths; - for (auto& path : paths_result) - raw_paths.emplace_back(path.nodes); - - paths_changed(raw_paths); - } - } - - // Only update the disk cache if the snode pool was updated - if (!pool_valid) { - { - std::lock_guard lock{snode_cache_mutex}; - snode_pool = pool_result; - last_snode_pool_update = std::chrono::system_clock::now(); - need_pool_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - } + net.call([this/*, pool_result, paths_result, pool_valid, paths_valid*/]() mutable { + // if (!paths_valid) { + // paths = paths_result; + + // // Call the paths_changed callback if provided + // if (paths_changed) { + // std::vector> raw_paths; + // for (auto& path : paths_result) + // raw_paths.emplace_back(path.nodes); + + // paths_changed(raw_paths); + // } + // } + + // // Only update the disk cache if the snode pool was updated + // if (!pool_valid) { + // { + // std::lock_guard lock{snode_cache_mutex}; + // snode_pool = pool_result; + // last_snode_pool_update = std::chrono::system_clock::now(); + // need_pool_write = true; + // need_write = true; + // } + // snode_cache_cv.notify_one(); + // } // Paths were successfully built, update the connection status update_status(ConnectionStatus::connected); From 845134d23f146395c7807b6d7e377b398889bd80 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 11:50:15 +1000 Subject: [PATCH 277/572] Testing debian test failure 10 --- src/network.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index c46c2195..f23c4f12 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -844,9 +844,9 @@ void Network::with_paths_and_pool( } // Store to instance variables - net.call([this/*, pool_result, paths_result, pool_valid, paths_valid*/]() mutable { + net.call([this, /*pool_result, */paths_result/*, pool_valid, paths_valid*/]() mutable { // if (!paths_valid) { - // paths = paths_result; + paths = paths_result; // // Call the paths_changed callback if provided // if (paths_changed) { From 39ecc58cbc5acd4de8d13a8309d6f9a4b98334c2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 12:06:15 +1000 Subject: [PATCH 278/572] Testing debian test failure 11 --- src/network.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index f23c4f12..10ec455c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -824,7 +824,8 @@ void Network::with_paths_and_pool( unused_nodes.pop_back(); path.push_back(node); } - + info.conn.reset(); + info.stream.reset(); paths_result.emplace_back(onion_path{std::move(info), path, 0}); // Log that a path was built From f92caaacfea66c80b664c86e19ffc9c8f7c12241 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 13:08:58 +1000 Subject: [PATCH 279/572] Testing debian test failure 12 (Try std::moving `info` everywhere) --- src/network.cpp | 57 +++++++++++++++++++++--------------------- tests/test_logging.cpp | 2 +- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 10ec455c..cb7c4924 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -785,7 +785,7 @@ void Network::with_paths_and_pool( if (!valid_guard_node) std::runtime_error{ "Failed to find valid guard node."}; - prom.set_value({*valid_guard_node, unused_nodes}); + prom.set_value({std::move(*valid_guard_node), unused_nodes}); } catch (...) { prom.set_exception(std::current_exception()); } @@ -824,8 +824,7 @@ void Network::with_paths_and_pool( unused_nodes.pop_back(); path.push_back(node); } - info.conn.reset(); - info.stream.reset(); + paths_result.emplace_back(onion_path{std::move(info), path, 0}); // Log that a path was built @@ -845,31 +844,31 @@ void Network::with_paths_and_pool( } // Store to instance variables - net.call([this, /*pool_result, */paths_result/*, pool_valid, paths_valid*/]() mutable { - // if (!paths_valid) { + net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { + if (!paths_valid) { paths = paths_result; - // // Call the paths_changed callback if provided - // if (paths_changed) { - // std::vector> raw_paths; - // for (auto& path : paths_result) - // raw_paths.emplace_back(path.nodes); - - // paths_changed(raw_paths); - // } - // } - - // // Only update the disk cache if the snode pool was updated - // if (!pool_valid) { - // { - // std::lock_guard lock{snode_cache_mutex}; - // snode_pool = pool_result; - // last_snode_pool_update = std::chrono::system_clock::now(); - // need_pool_write = true; - // need_write = true; - // } - // snode_cache_cv.notify_one(); - // } + // Call the paths_changed callback if provided + if (paths_changed) { + std::vector> raw_paths; + for (auto& path : paths_result) + raw_paths.emplace_back(path.nodes); + + paths_changed(raw_paths); + } + } + + // Only update the disk cache if the snode pool was updated + if (!pool_valid) { + { + std::lock_guard lock{snode_cache_mutex}; + snode_pool = pool_result; + last_snode_pool_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + } // Paths were successfully built, update the connection status update_status(ConnectionStatus::connected); @@ -1067,7 +1066,7 @@ void Network::find_valid_guard_node_recursive( "Outdated node version ({})"_format(fmt::join(version, "."))}; log::info(cat, "Guard snode {} valid.", target_node.to_string()); - cb(info, remaining_nodes); + cb(std::move(info), remaining_nodes); } catch (const std::exception& e) { // Log the error and loop after a slight delay (don't want to drain the pool // too quickly if the network goes down) @@ -1151,14 +1150,14 @@ void Network::get_version( auto info = get_connection_info(node, std::nullopt); if (!info.is_valid()) - return callback({}, info, "Network is unreachable."); + return callback({}, std::move(info), "Network is unreachable."); oxenc::bt_dict_producer payload; info.stream->command( "info", payload.view(), timeout, - [this, info, cb = std::move(callback)](quic::message resp) { + [this, info = std::move(info), cb = std::move(callback)](quic::message resp) { try { auto [status_code, body] = validate_response(resp, true); diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 84840aa9..26702e6f 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -100,7 +100,7 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); } From 052c593b66282f1d2a12752ee339a4d660ce8ceb Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 13:35:06 +1000 Subject: [PATCH 280/572] Reverted the previous change, added check to fix logging test --- src/network.cpp | 8 ++++---- tests/test_logging.cpp | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index cb7c4924..025d22e8 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -785,7 +785,7 @@ void Network::with_paths_and_pool( if (!valid_guard_node) std::runtime_error{ "Failed to find valid guard node."}; - prom.set_value({std::move(*valid_guard_node), unused_nodes}); + prom.set_value({*valid_guard_node, unused_nodes}); } catch (...) { prom.set_exception(std::current_exception()); } @@ -1066,7 +1066,7 @@ void Network::find_valid_guard_node_recursive( "Outdated node version ({})"_format(fmt::join(version, "."))}; log::info(cat, "Guard snode {} valid.", target_node.to_string()); - cb(std::move(info), remaining_nodes); + cb(info, remaining_nodes); } catch (const std::exception& e) { // Log the error and loop after a slight delay (don't want to drain the pool // too quickly if the network goes down) @@ -1150,14 +1150,14 @@ void Network::get_version( auto info = get_connection_info(node, std::nullopt); if (!info.is_valid()) - return callback({}, std::move(info), "Network is unreachable."); + return callback({}, info, "Network is unreachable."); oxenc::bt_dict_producer payload; info.stream->command( "info", payload.view(), timeout, - [this, info = std::move(info), cb = std::move(callback)](quic::message resp) { + [this, info, cb = std::move(callback)](quic::message resp) { try { auto [status_code, body] = validate_response(resp, true); diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 26702e6f..699a55b0 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -100,7 +100,11 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); +#ifndef NDEBUG + CHECK(simple_logs.front().find("Started libevent") != std::string::npos); +#else CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); +#endif CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); } From d252eefc995a91c850f61663165b4817864ee24a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 13:50:40 +1000 Subject: [PATCH 281/572] Added a preprocessor macro to identify the release build for tests --- tests/CMakeLists.txt | 4 ++++ tests/test_logging.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1fd82e5f..7dab0e4a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,9 @@ add_subdirectory(Catch2) +if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_definitions(-DRELEASE_BUILD) +endif() + add_executable(testAll test_blinding.cpp test_bt_merge.cpp diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 699a55b0..28559429 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -101,7 +101,7 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { CHECK(simple_logs.size() >= 2); // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); -#ifndef NDEBUG +#ifdef RELEASE_BUILD CHECK(simple_logs.front().find("Started libevent") != std::string::npos); #else CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); From ad21e73a5d17001faff3b30dec7b133ae6c350c0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 13 May 2024 15:22:59 +1000 Subject: [PATCH 282/572] Another tweak to logging test --- tests/test_logging.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 28559429..b7d8b7ed 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -101,7 +101,7 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { CHECK(simple_logs.size() >= 2); // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); -#ifdef RELEASE_BUILD +#if defined(__APPLE__) && defined(__clang__) && defined(RELEASE_BUILD) CHECK(simple_logs.front().find("Started libevent") != std::string::npos); #else CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); From d113e77c7df369eaa0fcc5dbeb3e1e249efeade9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 22 May 2024 16:48:57 +1000 Subject: [PATCH 283/572] Made a number of changes based on iOS crash logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Take ownership of the cache_path provided during init • Wrapped the snode cache writing in a try/catch and log any exceptions which occur • Updated the logs for dropping a path so it's clearer which path is being dropped --- include/session/network.hpp | 3 + src/config/base.cpp | 6 +- src/network.cpp | 251 +++++++++++++++++++++++------------- tests/test_network.cpp | 71 +++++++++- 4 files changed, 236 insertions(+), 95 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index cefa7a72..a03130ad 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -46,6 +46,7 @@ struct request_info { std::optional swarm_pubkey; onion_path path; std::chrono::milliseconds timeout; + bool node_destination; bool is_retry; }; @@ -193,12 +194,14 @@ class Network { /// /// Inputs: /// - `info` -- [in] the information for the request that was made. + /// - `timeout` -- [in, optional] flag indicating whether the request timed out. /// - `status_code` -- [in, optional] the status code returned from the network. /// - `response` -- [in, optional] response data returned from the network. /// - `handle_response` -- [in, optional] callback to be called with updated response /// information after processing the error. void handle_errors( request_info info, + bool timeout, std::optional status_code, std::optional response, std::optional handle_response); diff --git a/src/config/base.cpp b/src/config/base.cpp index 702b8e14..fe136236 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -143,7 +143,11 @@ std::vector ConfigBase::_merge( } } if (!decrypted) - log::warning(cat, "Failed to decrypt message {}", ci); + log::warning( + cat, + "Failed to decrypt message {} for namespace {}", + ci, + static_cast(storage_namespace())); } log::debug( cat, diff --git a/src/network.cpp b/src/network.cpp index 025d22e8..ca6dbd15 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -223,8 +223,7 @@ namespace { Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : use_testnet{use_testnet}, should_cache_to_disk{cache_path}, - cache_path{cache_path.value_or(default_cache_path)} { - log::info(cat, "Test info log - Create network"); + cache_path{std::move(cache_path.value_or(default_cache_path))} { paths_and_pool_loop = std::make_shared(); // Load the cache from disk and start the disk write thread @@ -384,7 +383,7 @@ void Network::load_cache_from_disk() { snode_pool.size(), swarm_cache.size()); } catch (const std::exception& e) { - log::error(cat, "Failed to load cache, will rebuild ({}).", e.what()); + log::error(cat, "Failed to load snode cache, will rebuild ({}).", e.what()); fs::remove_all(cache_path); } } @@ -404,57 +403,62 @@ void Network::disk_write_thread_loop() { lock.unlock(); { - // Create the cache directories if needed - auto swarm_base = cache_path / swarm_dir; - fs::create_directories(swarm_base); + try { + // Create the cache directories if needed + auto swarm_base = cache_path / swarm_dir; + fs::create_directories(swarm_base); - // Save the snode pool to disk - if (need_pool_write) { - auto pool_path = cache_path / file_snode_pool; - auto pool_tmp = pool_path; - pool_tmp += u8"_new"; + // Save the snode pool to disk + if (need_pool_write) { + auto pool_path = cache_path / file_snode_pool; + auto pool_tmp = pool_path; + pool_tmp += u8"_new"; - { - auto file = open_for_writing(pool_tmp); - for (auto& snode : snode_pool_write) - file << node_to_disk(snode, snode_failure_counts_write) << '\n'; - } + { + auto file = open_for_writing(pool_tmp); + for (auto& snode : snode_pool_write) + file << node_to_disk(snode, snode_failure_counts_write) << '\n'; + } - fs::rename(pool_tmp, pool_path); + fs::rename(pool_tmp, pool_path); - // Write the last update timestamp to disk - write_whole_file( - cache_path / file_snode_pool_updated, - "{}"_format( - std::chrono::system_clock::to_time_t(last_pool_update_write))); - log::debug(cat, "Finished writing snode pool cache to disk."); - } + // Write the last update timestamp to disk + write_whole_file( + cache_path / file_snode_pool_updated, + "{}"_format(std::chrono::system_clock::to_time_t( + last_pool_update_write))); + log::debug(cat, "Finished writing snode pool cache to disk."); + } - // Write the swarm cache to disk - if (need_swarm_write) { - auto time_now = std::chrono::system_clock::now(); + // Write the swarm cache to disk + if (need_swarm_write) { + auto time_now = std::chrono::system_clock::now(); - for (auto& [key, swarm] : swarm_cache_write) { - auto swarm_path = swarm_base / key; - auto swarm_tmp = swarm_path; - swarm_tmp += u8"_new"; - auto swarm_file = open_for_writing(swarm_tmp); + for (auto& [key, swarm] : swarm_cache_write) { + auto swarm_path = swarm_base / key; + auto swarm_tmp = swarm_path; + swarm_tmp += u8"_new"; + auto swarm_file = open_for_writing(swarm_tmp); - // Write the timestamp to the file - swarm_file << std::chrono::system_clock::to_time_t(time_now) << '\n'; + // Write the timestamp to the file + swarm_file << std::chrono::system_clock::to_time_t(time_now) << '\n'; - // Write the nodes to the file - for (auto& snode : swarm) - swarm_file << node_to_disk(snode, snode_failure_counts_write) << '\n'; + // Write the nodes to the file + for (auto& snode : swarm) + swarm_file << node_to_disk(snode, snode_failure_counts_write) + << '\n'; - fs::rename(swarm_tmp, swarm_path); + fs::rename(swarm_tmp, swarm_path); + } + log::debug(cat, "Finished writing swarm cache to disk."); } - log::debug(cat, "Finished writing swarm cache to disk."); - } - need_pool_write = false; - need_swarm_write = false; - need_write = false; + need_pool_write = false; + need_swarm_write = false; + need_write = false; + } catch (const std::exception& e) { + log::error(cat, "Failed to write snode cache: {}", e.what()); + } } lock.lock(); } @@ -545,6 +549,7 @@ connection_info Network::get_connection_info( target_path->conn_info.stream.reset(); handle_errors( {target, "", std::nullopt, std::nullopt, *target_path, 0ms, false}, + false, std::nullopt, std::nullopt, std::nullopt); @@ -604,18 +609,20 @@ void Network::with_paths_and_pool( // Populate the snode pool if needed if (!pool_valid) { // Define the response handler to avoid code duplication - auto handle_nodes_response = [](std::promise>& prom) { - return [&prom](std::vector nodes, - std::optional error) { - try { - if (nodes.empty()) - throw std::runtime_error{error.value_or("No nodes received.")}; - prom.set_value(nodes); - } catch (...) { - prom.set_exception(std::current_exception()); - } - }; - }; + auto handle_nodes_response = + [](std::promise>&& prom) { + return [&prom](std::vector nodes, + std::optional error) { + try { + if (nodes.empty()) + throw std::runtime_error{ + error.value_or("No nodes received.")}; + prom.set_value(nodes); + } catch (...) { + prom.set_exception(std::current_exception()); + } + }; + }; try { // If we don't have enough nodes in the current cached pool then we need to @@ -630,13 +637,16 @@ void Network::with_paths_and_pool( std::shuffle(pool_result.begin(), pool_result.end(), rng); std::promise> prom; + std::future> prom_future = prom.get_future(); get_service_nodes( - pool_result.front(), 256, handle_nodes_response(prom)); + pool_result.front(), + 256, + handle_nodes_response(std::move(prom))); // We want to block the `get_snode_pool_loop` until we have retrieved // the snode pool so we don't double up on requests - pool_result = prom.get_future().get(); + pool_result = prom_future.get(); log::info(cat, "Retrieved snode pool from seed node."); } else { // Pick ~9 random snodes from the current cache to fetch nodes from (we @@ -646,55 +656,60 @@ void Network::with_paths_and_pool( std::min(pool_result.size() / 3, static_cast(3)); log::info(cat, "Fetching from random expired cache nodes."); - std::vector first_nodes( + std::vector nodes1( pool_result.begin(), pool_result.begin() + num_retries); - std::vector second_nodes( + std::vector nodes2( pool_result.begin() + num_retries, pool_result.begin() + (num_retries * 2)); - std::vector third_nodes( + std::vector nodes3( pool_result.begin() + (num_retries * 2), pool_result.begin() + (num_retries * 3)); std::promise> prom1; std::promise> prom2; std::promise> prom3; + std::future> prom_future1 = + prom1.get_future(); + std::future> prom_future2 = + prom2.get_future(); + std::future> prom_future3 = + prom3.get_future(); // Kick off 3 concurrent requests get_service_nodes_recursive( - first_nodes, std::nullopt, handle_nodes_response(prom1)); + nodes1, std::nullopt, handle_nodes_response(std::move(prom1))); get_service_nodes_recursive( - second_nodes, std::nullopt, handle_nodes_response(prom2)); + nodes2, std::nullopt, handle_nodes_response(std::move(prom2))); get_service_nodes_recursive( - third_nodes, std::nullopt, handle_nodes_response(prom3)); + nodes3, std::nullopt, handle_nodes_response(std::move(prom3))); // We want to block the `get_snode_pool_loop` until we have retrieved // the snode pool so we don't double up on requests - auto first_result_nodes = prom1.get_future().get(); - auto second_result_nodes = prom2.get_future().get(); - auto third_result_nodes = prom3.get_future().get(); + auto result_nodes1 = prom_future1.get(); + auto result_nodes2 = prom_future2.get(); + auto result_nodes3 = prom_future3.get(); // Sort the vectors (so make it easier to find the // intersection) - std::stable_sort(first_result_nodes.begin(), first_result_nodes.end()); - std::stable_sort( - second_result_nodes.begin(), second_result_nodes.end()); - std::stable_sort(third_result_nodes.begin(), third_result_nodes.end()); + std::stable_sort(result_nodes1.begin(), result_nodes1.end()); + std::stable_sort(result_nodes2.begin(), result_nodes2.end()); + std::stable_sort(result_nodes3.begin(), result_nodes3.end()); // Get the intersection of the vectors - std::vector first_second_intersection; + std::vector intersection1_2; std::vector intersection; std::set_intersection( - first_result_nodes.begin(), - first_result_nodes.end(), - second_result_nodes.begin(), - second_result_nodes.end(), - std::back_inserter(first_second_intersection), + result_nodes1.begin(), + result_nodes1.end(), + result_nodes2.begin(), + result_nodes2.end(), + std::back_inserter(intersection1_2), [](const auto& a, const auto& b) { return a == b; }); std::set_intersection( - first_second_intersection.begin(), - first_second_intersection.end(), - third_result_nodes.begin(), - third_result_nodes.end(), + intersection1_2.begin(), + intersection1_2.end(), + result_nodes3.begin(), + result_nodes3.end(), std::back_inserter(intersection), [](const auto& a, const auto& b) { return a == b; }); @@ -1314,9 +1329,9 @@ void Network::send_request( auto [status_code, body] = validate_response(resp, false); cb(true, false, status_code, body); } catch (const status_code_exception& e) { - handle_errors(info, e.status_code, e.what(), cb); + handle_errors(info, false, e.status_code, e.what(), cb); } catch (const std::exception& e) { - cb(false, resp.timed_out, -1, e.what()); + handle_errors(info, resp.timed_out, -1, e.what(), cb); } }); } @@ -1362,6 +1377,7 @@ void Network::send_onion_request( swarm_pubkey, *path, timeout, + node_for_destination(destination).has_value(), is_retry}; send_request( @@ -1376,11 +1392,18 @@ void Network::send_onion_request( bool timeout, int16_t status_code, std::optional response) { - if (!success || timeout || - !ResponseParser::response_long_enough( + // If the request was reported as a failure or a timeout then we + // will have already handled the errors so just trigger the callback + if (!success || timeout) + return cb(success, timeout, status_code, response); + + // Ensure the response is long enough to be processed, if not then + // handle it as an error + if (!ResponseParser::response_long_enough( builder.enc_type, response->size())) - return handle_errors(info, status_code, response, cb); + return handle_errors(info, timeout, status_code, response, cb); + // Otherwise, process the onion request response if (std::holds_alternative(destination)) process_snode_response(builder, *response, info, cb); else if (std::holds_alternative(destination)) @@ -1442,7 +1465,7 @@ void Network::process_snode_response( // If we got a non 2xx status code, return the error if (status_code < 200 || status_code > 299) - return handle_errors(info, status_code, body, handle_response); + return handle_errors(info, false, status_code, body, handle_response); handle_response(true, false, status_code, body); } catch (const std::exception& e) { @@ -1479,10 +1502,10 @@ void Network::process_server_response( // If we have a status code that is not in the 2xx range, return the error if (status_code < 200 || status_code > 299) { if (result_bencode.is_finished()) - return handle_errors(info, status_code, std::nullopt, handle_response); + return handle_errors(info, false, status_code, std::nullopt, handle_response); return handle_errors( - info, status_code, result_bencode.consume_string(), handle_response); + info, false, status_code, result_bencode.consume_string(), handle_response); } // If there is no body just return the success status @@ -1553,11 +1576,46 @@ std::pair Network::validate_response(quic::message resp, void Network::handle_errors( request_info info, + bool timeout, std::optional status_code_, std::optional response, std::optional handle_response) { auto status_code = status_code_.value_or(-1); + // If we are making a proxied request to a server then it's possible that the server destination + // failed rather than nodes along the path, to avoid needlessly dropping paths we want to try to + // detect some of the more common cases and just handle the response without updating the + // path/snode state + if (!info.node_destination) { + // A tmeout could be caused because the destination is unreachable rather than the the path + // (eg. if a user has an old SOGS which is no longer running on their device they will get a + // timeout) + if (timeout) { + log::warning(cat, "Detected server timeout, finishing early."); + if (handle_response) + return (*handle_response)(false, true, status_code, response); + return; + } + + // A number of server errors can return HTML data but no status code, this indicates that + // we managed to connect to the destination so try to intercept some of these cases + if (status_code == -1 && response) { + const std::unordered_map> response_map = { + {"500 Internal Server Error", {500, false}}, + {"504 Gateway Timeout", {504, true}}, + }; + + for (const auto& [prefix, result] : response_map) { + if (response->starts_with(prefix)) { + log::warning(cat, "Detected {}, finishing early.", prefix); + if (handle_response) + return (*handle_response)(false, result.second, result.first, response); + return; + } + } + } + } + switch (status_code) { // A 404 or a 400 is likely due to a bad/missing SOGS or file so // shouldn't mark a path or snode as invalid @@ -1721,8 +1779,23 @@ void Network::handle_errors( updated_path]() mutable { // Drop the path if invalid if (updated_path.failure_count >= path_failure_threshold) { - log::info(cat, "Dropping path."); + auto old_paths_size = paths.size(); + old_path.conn_info.conn.reset(); + old_path.conn_info.stream.reset(); paths.erase(std::remove(paths.begin(), paths.end(), old_path), paths.end()); + + std::vector node_descriptions; + std::transform( + old_path.nodes.begin(), + old_path.nodes.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + + if (paths.size() != old_paths_size) + log::info(cat, "Dropping path: [{}]", path_description); + else + log::info(cat, "Path already dropped: [{}]", path_description); } else std::replace(paths.begin(), paths.end(), old_path, updated_path); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c867d016..607dae58 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -30,7 +30,8 @@ TEST_CASE("Network error handling", "[network]") { auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; auto path = onion_path{{{target}, nullptr, nullptr}, {target}, 0}; - auto mock_request = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + auto mock_request = + request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; Result result; auto network = Network(std::nullopt, true, false); @@ -42,6 +43,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target, 0); network.handle_errors( mock_request, + false, code, std::nullopt, [&result]( @@ -65,6 +67,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target, 0); network.handle_errors( mock_request, + false, 500, std::nullopt, [&result]( @@ -84,12 +87,14 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with no response (too many path failures) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 9}; - auto mock_request2 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + auto mock_request2 = + request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; network.set_paths({path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.handle_errors( mock_request2, + false, 500, std::nullopt, [&result]( @@ -111,13 +116,15 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; - auto mock_request3 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + auto mock_request3 = + request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.set_paths({path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.handle_errors( mock_request3, + false, 500, response, [&result]( @@ -137,12 +144,14 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(path) == 1); // Incremented because conn_info is invalid // Check general error handling with a path and specific node failure (too many failures) - auto mock_request4 = request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, false}; + auto mock_request4 = + request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; network.set_paths({path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); network.handle_errors( mock_request4, + false, 500, response, [&result]( @@ -167,6 +176,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target2, 0); network.handle_errors( mock_request, + false, 421, std::nullopt, [&result]( @@ -185,11 +195,12 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with no response data is handled like any other error auto mock_request5 = request_info{ - target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true}; + target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true, true}; network.set_paths({path}); network.set_failure_count(target, 0); network.handle_errors( mock_request5, + false, 421, std::nullopt, [&result]( @@ -208,6 +219,7 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with non-swarm response data is handled like any other error network.handle_errors( mock_request5, + false, 421, "Test", [&result]( @@ -236,6 +248,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target, 0); network.handle_errors( mock_request5, + false, 421, response, [&result]( @@ -257,6 +270,54 @@ TEST_CASE("Network error handling", "[network]") { CHECK(swarm.front().to_string() == "1.1.1.1:1"); CHECK(oxenc::to_hex(swarm.front().view_remote_key()) == oxenc::to_hex(ed_pk)); }); + + // Check a timeout with a sever destination doesn't impact the failure counts + auto mock_request6 = request_info{ + target, + "test", + std::nullopt, + x25519_pubkey::from_hex(x_pk_hex), + path, + 0ms, + false, + false}; + network.handle_errors( + mock_request6, + true, + std::nullopt, + "Test", + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK(result.timeout); + CHECK(result.status_code == -1); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 0); + + // Check a server response starting with '500 Internal Server Error' is reported as a `500` + // error and doesn't affect the failure count + network.handle_errors( + mock_request6, + false, + std::nullopt, + "500 Internal Server Error", + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 500); + CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(path) == 0); } TEST_CASE("Network onion request", "[send_onion_request][network]") { From b66e54b25805a3edbf5c09fafa2c486b18766383 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 23 May 2024 12:03:20 +1000 Subject: [PATCH 284/572] Removed a couple of debug logs --- src/network.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index ca6dbd15..f2d0927c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1591,7 +1591,6 @@ void Network::handle_errors( // (eg. if a user has an old SOGS which is no longer running on their device they will get a // timeout) if (timeout) { - log::warning(cat, "Detected server timeout, finishing early."); if (handle_response) return (*handle_response)(false, true, status_code, response); return; @@ -1607,7 +1606,6 @@ void Network::handle_errors( for (const auto& [prefix, result] : response_map) { if (response->starts_with(prefix)) { - log::warning(cat, "Detected {}, finishing early.", prefix); if (handle_response) return (*handle_response)(false, result.second, result.first, response); return; From d0b03ecf7d17e365edb077d888e926c88ef5d593 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 31 May 2024 18:06:56 +1000 Subject: [PATCH 285/572] Made a few changes to fix some race conditions in network recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed a crash due to not actually throwing an exception when running out of guard nodes • Updated the 'get_connection_info' function to block until the connection is either established or closed This change is due to an issue that was occurring where calling `get_connection_info` while offline when "fixing" an invalid connection would return a "valid" connection which would immediately close but still be considered valid, resulting in the client being unable to make any requests • Updated the path building logic to exclude invalid paths If we failed to recover a connection then we would trigger path building, but it would still see two active paths - by excluding any paths which are reporting an invalid connection we now properly recover in this case --- include/session/network.hpp | 35 ++++++--- src/network.cpp | 146 +++++++++++++++++++++++++++--------- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index a03130ad..e43d0783 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -270,15 +270,15 @@ class Network { /// API: network/get_connection_info /// - /// Creates a connection and opens a stream to the target service node. + /// Creates a connection for the target node and waits for the connection to be established (or + /// closed in case it fails to establish) before returning the connection. /// /// Inputs: /// - `target` -- [in] the target service node to connect to. - /// - `conn_established_cb` -- [in, optional] a callback to be passed when creating the - /// connection that should be triggered when the connection is established. - connection_info get_connection_info( - service_node target, - std::optional conn_established_cb); + /// + /// Returns: + /// - `connection_info` -- The connection info for the target service node. + connection_info get_connection_info(service_node target); /// API: network/with_paths_and_pool /// @@ -311,16 +311,29 @@ class Network { std::function path, std::optional error)> callback); - /// API: network/find_possible_path + /// API: network/valid_paths /// - /// Picks a random path from the provided paths excluding the provided node if one is available. + /// Filters the provided paths down to a list of valid paths. /// /// Inputs: - /// - `excluded_node` -- [in, optional] node which should not be included in the paths. + /// - `paths` -- [in] paths to validate. /// /// Outputs: - /// - The possible path, if found, and the number of paths provided. - std::pair validate_paths_and_pool( + /// - A list of valid paths. + std::vector valid_paths(std::vector paths); + + /// API: network/validate_paths_and_pool_sizes + /// + /// Returns a pair of bools indicating whether the provided paths and pool are valid. + /// + /// Inputs: + /// - `paths` -- [in] paths to validate size for. + /// - `pool` -- [in] pool to validate size for. + /// - `last_pool_update` -- [in] timestamp for when the pool was last updated. + /// + /// Outputs: + /// - A pair of flags indicating whether the provided paths and pool have the correct sizes. + std::pair validate_paths_and_pool_sizes( std::vector paths, std::vector pool, std::chrono::system_clock::time_point last_pool_update); diff --git a/src/network.cpp b/src/network.cpp index f2d0927c..a1dfaec7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -480,7 +480,14 @@ void Network::disk_write_thread_loop() { void Network::close_connections() { net.call([this]() mutable { endpoint.reset(); + + for (auto& path : paths) { + path.conn_info.conn.reset(); + path.conn_info.stream.reset(); + } + update_status(ConnectionStatus::disconnected); + log::info(cat, "Closed all connections."); }); } @@ -524,9 +531,12 @@ std::shared_ptr Network::get_endpoint() { }); } -connection_info Network::get_connection_info( - service_node target, - std::optional conn_established_cb) { +connection_info Network::get_connection_info(service_node target) { + std::once_flag cb_called; + std::mutex mutex; + std::condition_variable cv; + bool connection_established = false; + bool done = false; auto connection_key_pair = ed25519::ed25519_key_pair(); auto creds = quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); @@ -535,15 +545,38 @@ connection_info Network::get_connection_info( target, creds, quic::opt::keep_alive{10s}, - conn_established_cb, - [this, target](quic::connection_interface& conn, uint64_t) { + [&mutex, &cv, &connection_established, &done, &cb_called]( + quic::connection_interface& conn) { + std::call_once(cb_called, [&]() { + { + std::lock_guard lock(mutex); + connection_established = true; + done = true; + } + cv.notify_one(); + }); + }, + [this, target, &mutex, &cv, &done, &cb_called]( + quic::connection_interface& conn, uint64_t) { + // Trigger the callback first before updating the paths in case this was triggered + // when try to establish a connection + std::call_once(cb_called, [&]() { + { + std::lock_guard lock(mutex); + done = true; + } + cv.notify_one(); + }); + // When the connection is closed, update the path and connection status - auto target_path = - std::find_if(paths.begin(), paths.end(), [&target](const auto& path) { + auto current_paths = + net.call_get([this]() -> std::vector { return paths; }); + auto target_path = std::find_if( + current_paths.begin(), current_paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); }); - if (target_path != paths.end() && target_path->conn_info.conn && + if (target_path != current_paths.end() && target_path->conn_info.conn && conn.reference_id() == target_path->conn_info.conn->reference_id()) { target_path->conn_info.conn.reset(); target_path->conn_info.stream.reset(); @@ -556,6 +589,12 @@ connection_info Network::get_connection_info( } }); + std::unique_lock lock(mutex); + cv.wait(lock, [&done] { return done; }); + + if (!connection_established) + return {target, nullptr, nullptr}; + return {target, c, c->open_stream()}; } @@ -579,10 +618,12 @@ void Network::with_paths_and_pool( }); // Check if the current data is valid, and if so just return it - auto [paths_valid, pool_valid] = validate_paths_and_pool(current_paths, pool, last_pool_update); + auto current_valid_paths = valid_paths(current_paths); + auto [paths_valid, pool_valid] = + validate_paths_and_pool_sizes(current_valid_paths, pool, last_pool_update); if (paths_valid && pool_valid) - return callback(current_paths, pool, std::nullopt); + return callback(current_valid_paths, pool, std::nullopt); auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get([this, excluded_node]() mutable -> paths_and_pool_result { @@ -592,11 +633,12 @@ void Network::with_paths_and_pool( }); // Check if the current data is valid, and if so just return it + auto current_valid_paths = valid_paths(current_paths); auto [paths_valid, pool_valid] = - validate_paths_and_pool(current_paths, pool, last_pool_update); + validate_paths_and_pool_sizes(current_valid_paths, pool, last_pool_update); if (paths_valid && pool_valid) - return {current_paths, pool, std::nullopt}; + return {current_valid_paths, pool, std::nullopt}; // Update the network status net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); @@ -604,7 +646,7 @@ void Network::with_paths_and_pool( // If the pool isn't valid then we should update it CSRNG rng; std::vector pool_result = pool; - std::vector paths_result = current_paths; + std::vector paths_result = current_valid_paths; // Populate the snode pool if needed if (!pool_valid) { @@ -768,7 +810,7 @@ void Network::with_paths_and_pool( // Split the possible nodes list into a list of lists (one list could run // out before the other but in most cases this should work fine) - size_t required_paths = (target_path_count - current_paths.size()); + size_t required_paths = (target_path_count - current_valid_paths.size()); size_t chunk_size = (possible_guard_nodes.size() / required_paths); std::vector> nodes_to_test; auto start = 0; @@ -787,22 +829,30 @@ void Network::with_paths_and_pool( // Start testing guard nodes based on the number of paths we want to build std::vector< - std::promise>>> - promises(required_paths); + std::future>>> + futures; + futures.reserve(required_paths); for (size_t i = 0; i < required_paths; ++i) { + std::promise>> + guard_node_prom; + futures.emplace_back(guard_node_prom.get_future()); + + auto prom = std::make_shared>>>( + std::move(guard_node_prom)); + find_valid_guard_node_recursive( nodes_to_test[i], - [&prom = promises[i]]( - std::optional valid_guard_node, - std::vector unused_nodes) { + [prom](std::optional valid_guard_node, + std::vector unused_nodes) { try { if (!valid_guard_node) - std::runtime_error{ + throw std::runtime_error{ "Failed to find valid guard node."}; - prom.set_value({*valid_guard_node, unused_nodes}); + prom->set_value({*valid_guard_node, unused_nodes}); } catch (...) { - prom.set_exception(std::current_exception()); + prom->set_exception(std::current_exception()); } }); } @@ -812,8 +862,8 @@ void Network::with_paths_and_pool( std::vector valid_nodes; std::vector unused_nodes; - for (auto& prom : promises) { - auto result = prom.get_future().get(); + for (auto& fut : futures) { + auto result = fut.get(); valid_nodes.emplace_back(result.first); unused_nodes.insert( unused_nodes.begin(), @@ -823,7 +873,8 @@ void Network::with_paths_and_pool( // Make sure we ended up getting enough valid nodes auto have_enough_guard_nodes = - (current_paths.size() + valid_nodes.size() >= target_path_count); + (current_valid_paths.size() + valid_nodes.size() >= + target_path_count); auto have_enough_unused_nodes = (unused_nodes.size() >= ((path_size - 1) * target_path_count)); @@ -895,7 +946,18 @@ void Network::with_paths_and_pool( return callback(updated_paths, updated_pool, error); } -std::pair Network::validate_paths_and_pool( +std::vector Network::valid_paths(std::vector paths) { + auto valid_paths = paths; + auto valid_paths_end = + std::remove_if(valid_paths.begin(), valid_paths.end(), [](onion_path path) { + return !path.conn_info.is_valid(); + }); + valid_paths.erase(valid_paths_end, valid_paths.end()); + + return valid_paths; +} + +std::pair Network::validate_paths_and_pool_sizes( std::vector paths, std::vector pool, std::chrono::system_clock::time_point last_pool_update) { @@ -934,16 +996,20 @@ void Network::with_path( if (target_path_it == current_paths.end()) return {std::nullopt, current_paths.size()}; - auto info = - get_connection_info(path.nodes[0], [this](quic::connection_interface&) { - // If the connection is re-established update the network - // status back to connected - update_status(ConnectionStatus::connected); - }); + // Try to retrieve a valid connection for the guard node + auto info = get_connection_info(path.nodes[0]); + // It's possible that the connection was created successfully, and reported as + // valid, but isn't actually valid (eg. it was shutdown immediately due to the + // network being unreachable) so to avoid this we wait for either the connection + // to be established or the connection to fail before continuing if (!info.is_valid()) return {std::nullopt, current_paths.size()}; + // If the connection info is valid update the connection status back to + // connected + update_status(ConnectionStatus::connected); + // No need to call the 'paths_changed' callback as the paths haven't // actually changed, just their connection info auto updated_path = onion_path{std::move(info), std::move(path.nodes), 0}; @@ -969,11 +1035,13 @@ void Network::with_path( std::optional error) { if (error) return cb(std::nullopt, *error); + auto [target_path, paths_count] = find_possible_path(excluded_node, updated_paths); if (!target_path) return cb(std::nullopt, "Unable to find valid path."); + cb(*target_path, std::nullopt); }); @@ -1106,7 +1174,7 @@ void Network::get_service_nodes( std::optional limit, std::function nodes, std::optional error)> callback) { - auto info = get_connection_info(node, std::nullopt); + auto info = get_connection_info(node); if (!info.is_valid()) return callback({}, "Network is unreachable."); @@ -1162,7 +1230,7 @@ void Network::get_version( std::function version, connection_info info, std::optional error)> callback) { - auto info = get_connection_info(node, std::nullopt); + auto info = get_connection_info(node); if (!info.is_valid()) return callback({}, info, "Network is unreachable."); @@ -1587,7 +1655,7 @@ void Network::handle_errors( // detect some of the more common cases and just handle the response without updating the // path/snode state if (!info.node_destination) { - // A tmeout could be caused because the destination is unreachable rather than the the path + // A timeout could be caused because the destination is unreachable rather than the the path // (eg. if a user has an old SOGS which is no longer running on their device they will get a // timeout) if (timeout) { @@ -1792,8 +1860,12 @@ void Network::handle_errors( if (paths.size() != old_paths_size) log::info(cat, "Dropping path: [{}]", path_description); - else + else { + // If the path was already dropped then the snode pool would have already been + // updated so no need to continue log::info(cat, "Path already dropped: [{}]", path_description); + return; + } } else std::replace(paths.begin(), paths.end(), old_path, updated_path); @@ -1858,7 +1930,7 @@ void Network::set_paths(std::vector paths_) { } uint8_t Network::get_failure_count(onion_path path) { - auto current_paths = net.call_get([this, path]() -> std::vector { return paths; }); + auto current_paths = net.call_get([this]() -> std::vector { return paths; }); auto target_path = std::find_if(current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { From 8b4477a72229a15c66ed86f154cb317a9e82a963 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 1 Jun 2024 11:30:10 +1000 Subject: [PATCH 286/572] Wrapped a few throwing functions in try/catches in the C API --- src/config/base.cpp | 140 ++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index fe136236..e52684bc 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -651,13 +651,19 @@ LIBSESSION_EXPORT config_string_list* config_merge( 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)); + try { + 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)); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + return nullptr; } LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { @@ -665,39 +671,45 @@ LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { } 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; - } + try { + 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; + return ret; + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + return nullptr; } LIBSESSION_EXPORT void config_confirm_pushed( @@ -706,11 +718,16 @@ LIBSESSION_EXPORT void config_confirm_pushed( } 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()); + try { + 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()); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } } LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { @@ -745,10 +762,21 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size } LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}); + try { + unbox(conf)->add_key({key, 32}); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + } LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}, /*high_priority=*/false); + try { + unbox(conf)->add_key({key, 32}, /*high_priority=*/false); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } } LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { return unbox(conf)->clear_keys(); @@ -760,7 +788,11 @@ 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}); + try { + return unbox(conf)->has_key({key, 32}); + } catch (...) { + return false; + } } LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i) { return unbox(conf)->key(i).data(); @@ -771,11 +803,21 @@ LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf } LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret) { - unbox(conf)->set_sig_keys({secret, 64}); + try { + unbox(conf)->set_sig_keys({secret, 64}); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } } LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - unbox(conf)->set_sig_pubkey({pubkey, 32}); + try { + unbox(conf)->set_sig_pubkey({pubkey, 32}); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } } LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { From 62bd727efc266ab30897465d8562693c410be7c3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sun, 2 Jun 2024 09:45:08 +1000 Subject: [PATCH 287/572] Updated CI script to latest changes --- .drone.jsonnet | 91 +++++++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 5b574798..f6d6e47a 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,5 +1,3 @@ -local docker_base = 'registry.oxen.rocks/lokinet-ci-'; - local submodule_commands = [ 'git fetch --tags', 'git submodule update --init --recursive --depth=1 --jobs=4', @@ -15,17 +13,27 @@ local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; local default_deps_nocxx = ['nlohmann-json3-dev']; local default_deps = ['g++'] + default_deps_nocxx; -local docker_base = 'registry.oxen.rocks/lokinet-ci-'; +local default_test_deps = []; + +local docker_base = 'registry.oxen.rocks/'; + +local debian_backports(distro, pkgs) = [ + 'echo "deb http://deb.debian.org/debian ' + distro + '-backports main" >/etc/apt/sources.list.d/' + distro + '-backports.list', + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' install -y ' + std.join(' ', std.map(function(x) x + '/' + distro + '-backports', pkgs)), +]; // Do something on a debian-like system local debian_pipeline(name, image, arch='amd64', deps=default_deps, - oxen_repo=false, + oxen_repo=true, kitware_repo=''/* ubuntu codename, if wanted */, allow_fail=false, + cmake_pkg='cmake', build=['echo "Error: drone build argument not set"', 'exit 1'], + extra_setup=[], extra_steps=[]) = { kind: 'pipeline', @@ -59,9 +67,9 @@ local debian_pipeline(name, 'echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ ' + kitware_repo + ' main" >/etc/apt/sources.list.d/kitware.list', 'eatmydata ' + apt_get_quiet + ' update', ] else [] - ) + [ + ) + extra_setup + [ 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y cmake make git ccache ca-certificates ' + std.join(' ', deps), + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + cmake_pkg + ' make git ccache ca-certificates ' + std.join(' ', deps), ] + build, }, ] + extra_steps, @@ -72,14 +80,17 @@ local debian_build(name, image, arch='amd64', deps=default_deps, + test_deps=default_test_deps, build_type='Release', lto=false, werror=true, cmake_extra='', + shared_libs=true, jobs=6, tests=true, - oxen_repo=false, + oxen_repo=true, kitware_repo=''/* ubuntu codename, if wanted */, + extra_setup=[], allow_fail=false) = debian_pipeline( name, @@ -94,7 +105,7 @@ local debian_build(name, 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE=' + build_type + ' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + - '-DBUILD_SHARED_LIBS=ON ' + + (if shared_libs then '-DBUILD_SHARED_LIBS=ON ' else '') + '-DUSE_LTO=' + (if lto then 'ON ' else 'OFF ') + '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') + cmake_extra, @@ -106,10 +117,23 @@ local debian_build(name, image: image, pull: 'always', [if allow_fail then 'failure']: 'ignore', - commands: [ - 'cd build', - './tests/testAll --colour-mode ansi -d yes', - ], + commands: + [apt_get_quiet + ' install -y eatmydata'] + ( + if std.length(test_deps) > 0 then ( + if oxen_repo then [ + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y lsb-release', + 'cp utils/deb.oxen.io.gpg /etc/apt/trusted.gpg.d', + 'echo deb http://deb.oxen.io $$(lsb_release -sc) main >/etc/apt/sources.list.d/oxen.list', + ] else [] + ) + [ + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', test_deps), + ] else [] + ) + [ + 'cd build', + './tests/testAll --colour-mode ansi -d yes', + ], }] else []) ); // windows cross compile on debian @@ -160,9 +184,10 @@ local windows_cross_pipeline(name, ); local clang(version) = debian_build( - 'Debian sid/clang-' + version + ' (amd64)', + 'Debian sid/clang-' + version, docker_base + 'debian-sid-clang', deps=['clang-' + version] + default_deps_nocxx, + shared_libs=false, cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' ' ); @@ -182,6 +207,7 @@ local full_llvm(version) = debian_build( // Macos build local mac_pipeline(name, + arch='amd64', allow_fail=false, build=['echo "Error: drone build argument not set"', 'exit 1'], extra_steps=[]) @@ -189,7 +215,7 @@ local mac_pipeline(name, kind: 'pipeline', type: 'exec', name: name, - platform: { os: 'darwin', arch: 'amd64' }, + platform: { os: 'darwin', arch: arch }, steps: [ { name: 'submodules', commands: submodule_commands }, { @@ -205,6 +231,7 @@ local mac_pipeline(name, ] + extra_steps, }; local mac_builder(name, + arch='amd64', build_type='Release', werror=true, lto=false, @@ -212,7 +239,7 @@ local mac_builder(name, jobs=6, tests=true, allow_fail=false) - = mac_pipeline(name, allow_fail=allow_fail, build=[ + = mac_pipeline(name, arch=arch, allow_fail=allow_fail, build=[ 'mkdir build', 'cd build', 'cmake .. -DCMAKE_CXX_FLAGS=-fcolor-diagnostics -DCMAKE_BUILD_TYPE=' + build_type + ' ' + @@ -249,6 +276,7 @@ local static_build(name, image, arch=arch, deps=deps, + oxen_repo=oxen_repo, build=[ 'export JOBS=' + jobs, './utils/static-bundle.sh build ' + archive_name + ' -DSTATIC_LIBSTD=ON ' + cmake_extra, @@ -299,35 +327,30 @@ local static_build(name, }, // Various debian builds - debian_build('Debian sid (amd64)', docker_base + 'debian-sid'), - debian_build('Debian sid/Debug (amd64)', docker_base + 'debian-sid', build_type='Debug'), + debian_build('Debian sid', docker_base + 'debian-sid'), + debian_build('Debian sid/Debug', docker_base + 'debian-sid', build_type='Debug'), + debian_build('Debian testing', docker_base + 'debian-testing'), clang(16), full_llvm(16), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), - debian_build('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'), - debian_build('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'), - debian_build('Ubuntu bionic (amd64)', - docker_base + 'ubuntu-bionic', - deps=['g++-8'], - kitware_repo='bionic', - cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8'), + debian_build('Debian 11', docker_base + 'debian-bullseye', extra_setup=debian_backports('bullseye', ['cmake'])), + debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), + debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), // ARM builds (ARM64 and armhf) debian_build('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64', jobs=4), debian_build('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), - // Windows builds (x64) - windows_cross_pipeline('Windows (amd64)', docker_base + 'debian-win32-cross-wine'), - // Macos builds: - mac_builder('macOS (Release)'), - mac_builder('macOS (Debug)', build_type='Debug'), + mac_builder('macOS Intel (Release)'), + mac_builder('macOS Arm64 (Release)', arch='arm64'), + mac_builder('macOS Arm64 (Debug)', arch='arm64', build_type='Debug'), // Static lib builds - static_build('Static Linux amd64', docker_base + 'debian-stable', 'libsession-util-linux-amd64-TAG.tar.xz'), - static_build('Static Linux i386', docker_base + 'debian-stable', 'libsession-util-linux-i386-TAG.tar.xz'), - static_build('Static Linux arm64', docker_base + 'debian-stable', 'libsession-util-linux-arm64-TAG.tar.xz', arch='arm64'), - static_build('Static Linux armhf', docker_base + 'debian-stable/arm32v7', 'libsession-util-linux-armhf-TAG.tar.xz', arch='arm64'), + static_build('Static Linux/amd64', docker_base + 'debian-stable', 'libsession-util-linux-amd64-TAG.tar.xz'), + static_build('Static Linux/i386', docker_base + 'debian-stable', 'libsession-util-linux-i386-TAG.tar.xz'), + static_build('Static Linux/arm64', docker_base + 'debian-stable', 'libsession-util-linux-arm64-TAG.tar.xz', arch='arm64'), + static_build('Static Linux/armhf', docker_base + 'debian-stable/arm32v7', 'libsession-util-linux-armhf-TAG.tar.xz', arch='arm64'), static_build('Static Windows x64', docker_base + 'debian-win32-cross', 'libsession-util-windows-x64-TAG.zip', @@ -355,7 +378,7 @@ local static_build(name, 'cd build-macos && ../utils/ci/drone-static-upload.sh', ]), - mac_pipeline('Static iOS', build=[ + mac_pipeline('Static iOS', arch='arm64', build=[ 'export JOBS=6', './utils/ios.sh libsession-util-ios-TAG', 'cd build-ios && ../utils/ci/drone-static-upload.sh', From 1e670f022859b39789fac6eaa5cb0b1c2a51186e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sun, 2 Jun 2024 09:53:32 +1000 Subject: [PATCH 288/572] Added the gnutls dep to the CI script --- .drone.jsonnet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index f6d6e47a..57b6732b 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,7 +10,11 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local default_deps_nocxx = ['nlohmann-json3-dev']; +local libngtcp2_deps = ['libgnutls28-dev']; + +local default_deps_nocxx = [ + 'nlohmann-json3-dev' +] + libngtcp2_deps; local default_deps = ['g++'] + default_deps_nocxx; local default_test_deps = []; From a3911473c3c827d8c7891d7abebc0dc02de994b4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sun, 2 Jun 2024 10:01:08 +1000 Subject: [PATCH 289/572] Fixing CI errors --- src/network.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index a1dfaec7..f6818bd3 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -223,7 +223,7 @@ namespace { Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : use_testnet{use_testnet}, should_cache_to_disk{cache_path}, - cache_path{std::move(cache_path.value_or(default_cache_path))} { + cache_path{cache_path.value_or(default_cache_path)} { paths_and_pool_loop = std::make_shared(); // Load the cache from disk and start the disk write thread @@ -545,8 +545,7 @@ connection_info Network::get_connection_info(service_node target) { target, creds, quic::opt::keep_alive{10s}, - [&mutex, &cv, &connection_established, &done, &cb_called]( - quic::connection_interface& conn) { + [&mutex, &cv, &connection_established, &done, &cb_called](quic::connection_interface&) { std::call_once(cb_called, [&]() { { std::lock_guard lock(mutex); @@ -581,7 +580,7 @@ connection_info Network::get_connection_info(service_node target) { target_path->conn_info.conn.reset(); target_path->conn_info.stream.reset(); handle_errors( - {target, "", std::nullopt, std::nullopt, *target_path, 0ms, false}, + {target, "", std::nullopt, std::nullopt, *target_path, 0ms, false, false}, false, std::nullopt, std::nullopt, From 3ee3a2a071de5719aa9ac5933d02c7e817a59e14 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sun, 2 Jun 2024 10:06:51 +1000 Subject: [PATCH 290/572] Added some missing Ci script tweaks, ran formatter --- .drone.jsonnet | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 57b6732b..b05e7668 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -13,11 +13,12 @@ local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; local libngtcp2_deps = ['libgnutls28-dev']; local default_deps_nocxx = [ - 'nlohmann-json3-dev' + 'nlohmann-json3-dev', ] + libngtcp2_deps; + local default_deps = ['g++'] + default_deps_nocxx; -local default_test_deps = []; +local default_test_deps = libngtcp2_deps; local docker_base = 'registry.oxen.rocks/'; @@ -115,6 +116,7 @@ local debian_build(name, cmake_extra, 'make VERBOSE=1 -j' + jobs, ], + extra_setup=extra_setup, extra_steps=(if tests then [{ name: 'tests', @@ -191,15 +193,15 @@ local clang(version) = debian_build( 'Debian sid/clang-' + version, docker_base + 'debian-sid-clang', deps=['clang-' + version] + default_deps_nocxx, - shared_libs=false, cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' ' ); local full_llvm(version) = debian_build( - 'Debian sid/llvm-' + version + ' (amd64)', + 'Debian sid/llvm-' + version, docker_base + 'debian-sid-clang', deps=['clang-' + version, ' lld-' + version, ' libc++-' + version + '-dev', 'libc++abi-' + version + '-dev'] + default_deps_nocxx, + shared_libs=false, cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' -DCMAKE_CXX_FLAGS="-stdlib=libc++ -fcolor-diagnostics" ' + From ee31759fda18be9062bfd928f95f4e639f9283d0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 3 Jun 2024 08:54:39 +1000 Subject: [PATCH 291/572] Bumped nlohmann-json to latest version --- external/nlohmann-json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/nlohmann-json b/external/nlohmann-json index bc889afb..9cca280a 160000 --- a/external/nlohmann-json +++ b/external/nlohmann-json @@ -1 +1 @@ -Subproject commit bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d +Subproject commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 From a7340157358f2a62c05cb46bc981044db977458c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 3 Jun 2024 12:45:44 +1000 Subject: [PATCH 292/572] Updated the C API config error handling logic --- include/session/config/base.hpp | 56 +++++++++---- src/config/base.cpp | 53 ++++-------- src/config/contacts.cpp | 23 ++---- src/config/convo_info_volatile.cpp | 88 +++++++------------- src/config/groups/info.cpp | 25 +++--- src/config/groups/keys.cpp | 125 +++++++++++++++-------------- src/config/groups/members.cpp | 24 +++--- src/config/internal.hpp | 8 -- src/config/user_groups.cpp | 67 ++++++---------- src/config/user_profile.cpp | 17 ++-- tests/test_group_keys.cpp | 10 ++- 11 files changed, 210 insertions(+), 286 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 881cc37e..14de937b 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1270,24 +1270,46 @@ 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; +template +void copy_c_str(char (&dest)[N], std::string_view src) { + if (src.size() >= N) + src.remove_suffix(src.size() - N - 1); + std::memcpy(dest, src.data(), src.size()); + dest[src.size()] = 0; } -// 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); +// Wraps a labmda and, if an exception is thrown, 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. +// +// No return value: accepts void and pointer returns; pointer returns will become nullptr on error +template +decltype(auto) wrap_exceptions(config_object* conf, Call&& f) { + using Ret = std::invoke_result_t; + + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + if constexpr (std::is_pointer_v) + return static_cast(nullptr); + else static_assert(std::is_void_v, "Don't know how to return an error value!"); +} + +// Same as above but accepts callbacks with value returns on errors: returns `f()` on success, +// `error_return` on exception +template +Ret wrap_exceptions(config_object* conf, Call&& f, Ret error_return) { + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + return error_return; +} } // namespace session::config diff --git a/src/config/base.cpp b/src/config/base.cpp index e52684bc..0ff179aa 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -651,7 +651,7 @@ LIBSESSION_EXPORT config_string_list* config_merge( const unsigned char** configs, const size_t* lengths, size_t count) { - try { + return wrap_exceptions(conf, [&]{ auto& config = *unbox(conf); std::vector> confs; confs.reserve(count); @@ -659,11 +659,7 @@ LIBSESSION_EXPORT config_string_list* config_merge( confs.emplace_back(msg_hashes[i], ustring_view{configs[i], lengths[i]}); return make_string_list(config.merge(confs)); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return nullptr; + }); } LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { @@ -671,7 +667,7 @@ LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { } LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { - try { + return wrap_exceptions(conf, [&]{ auto& config = *unbox(conf); auto [seqno, data, obs] = config.push(); @@ -705,11 +701,7 @@ LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { } return ret; - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return nullptr; + }); } LIBSESSION_EXPORT void config_confirm_pushed( @@ -718,16 +710,13 @@ LIBSESSION_EXPORT void config_confirm_pushed( } LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen) { - try { + wrap_exceptions(conf, [&]{ 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()); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } + }); } LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { @@ -762,21 +751,15 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size } LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - try { + wrap_exceptions(conf, [&]{ unbox(conf)->add_key({key, 32}); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - + }); } + LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - try { + wrap_exceptions(conf, [&]{ unbox(conf)->add_key({key, 32}, /*high_priority=*/false); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } + }); } LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { return unbox(conf)->clear_keys(); @@ -803,21 +786,15 @@ LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf } LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret) { - try { + wrap_exceptions(conf, [&]{ unbox(conf)->set_sig_keys({secret, 64}); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } + }); } LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - try { + wrap_exceptions(conf, [&]{ unbox(conf)->set_sig_pubkey({pubkey, 32}); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } + }); } LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index fa61e1ed..95bab843 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -174,17 +174,13 @@ std::optional Contacts::get(std::string_view pubkey_hex) const { LIBSESSION_C_API bool contacts_get( config_object* conf, contacts_contact* contact, const char* session_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ 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; + return false; + }, false); } contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { @@ -196,15 +192,10 @@ contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { LIBSESSION_C_API bool contacts_get_or_construct( config_object* conf, contacts_contact* contact, const char* session_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ 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; - } + }, false); } void Contacts::set(const contact_info& contact) { @@ -246,7 +237,9 @@ void Contacts::set(const contact_info& contact) { } LIBSESSION_C_API void contacts_set(config_object* conf, const contacts_contact* contact) { - unbox(conf)->set(contact_info{*contact}); + wrap_exceptions(conf, [&]{ + unbox(conf)->set(contact_info{*contact}); + }); } void Contacts::set_name(std::string_view session_id, std::string name) { diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 52022e11..f3c9a241 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -496,30 +496,21 @@ int convo_info_volatile_init( 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; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->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; - } - return false; + return false; + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_1to1( config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf)->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; - } + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_community( @@ -527,17 +518,13 @@ LIBSESSION_C_API bool convo_info_volatile_get_community( convo_info_volatile_community* og, const char* base_url, const char* room) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->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; - } - return false; + return false; + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( config_object* conf, @@ -545,78 +532,57 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( const char* base_url, const char* room, unsigned const char* pubkey) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf) ->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; - } + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_group( config_object* conf, convo_info_volatile_group* convo, const char* id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->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; - } - return false; + return false; + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( config_object* conf, convo_info_volatile_group* convo, const char* id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf)->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; - } + }, false); } LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->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; - } - return false; + return false; + }, 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) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf)->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; - } + }, false); } 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}); + wrap_exceptions(conf, [&]{ + unbox(conf)->set(convo::one_to_one{*convo}); + }); } LIBSESSION_C_API void convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo) { @@ -624,11 +590,15 @@ LIBSESSION_C_API void convo_info_volatile_set_community( } LIBSESSION_C_API void convo_info_volatile_set_group( config_object* conf, const convo_info_volatile_group* convo) { - unbox(conf)->set(convo::group{*convo}); + wrap_exceptions(conf, [&]{ + unbox(conf)->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}); + wrap_exceptions(conf, [&]{ + unbox(conf)->set(convo::legacy_group{*convo}); + }); } LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 88b3a1eb..9002be1f 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -163,12 +163,10 @@ LIBSESSION_C_API const char* groups_info_get_name(const config_object* conf) { /// Outputs: /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_C_API int groups_info_set_name(config_object* conf, const char* name) { - try { + return wrap_exceptions(conf, [&]{ unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - return 0; + return 0; + }, static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_description @@ -201,12 +199,10 @@ LIBSESSION_C_API const char* groups_info_get_description(const config_object* co /// Outputs: /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_C_API int groups_info_set_description(config_object* conf, const char* description) { - try { + return wrap_exceptions(conf, [&]{ unbox(conf)->set_description(description); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - return 0; + return 0; + }, static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_pic @@ -248,13 +244,10 @@ LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic p if (!url.empty()) key = {pic.key, 32}; - try { + return wrap_exceptions(conf, [&]{ unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - - return 0; + return 0; + }, static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_expiry_timer diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index e5095559..a2cc5c44 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1378,12 +1378,38 @@ const groups::Keys& unbox(const config_group_keys* conf) { 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; +// Wraps a labmda and, if an exception is thrown, 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. +// +// No return value: accepts void and pointer returns; pointer returns will become nullptr on error +template +decltype(auto) wrap_exceptions(config_group_keys* conf, Call&& f) { + using Ret = std::invoke_result_t; + + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + if constexpr (std::is_pointer_v) + return nullptr; + else static_assert(std::is_void_v, "Don't know how to return an error value!"); +} + +// Same as above but accepts callbacks with value returns on errors: returns `f()` on success, +// `error_return` on exception +template +Ret wrap_exceptions(config_group_keys* conf, Call&& f, Ret error_return) { + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + return error_return; } } // namespace @@ -1450,16 +1476,13 @@ LIBSESSION_C_API bool groups_keys_load_admin_key( const unsigned char* secret, config_object* info, config_object* members) { - try { + return wrap_exceptions(conf, [&]{ 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; - } - return true; + return true; + }, false); } LIBSESSION_C_API bool groups_keys_rekey( @@ -1471,15 +1494,13 @@ LIBSESSION_C_API bool groups_keys_rekey( assert(info && members && out && outlen); auto& keys = unbox(conf); ustring_view to_push; - try { + + return wrap_exceptions(conf, [&]{ to_push = keys.rekey(*unbox(info), *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; - } - *out = to_push.data(); - *outlen = to_push.size(); - return true; + *out = to_push.data(); + *outlen = to_push.size(); + return true; + }, false); } LIBSESSION_C_API bool groups_keys_pending_config( @@ -1502,18 +1523,15 @@ LIBSESSION_C_API bool groups_keys_load_message( config_object* info, config_object* members) { assert(data && info && members); - try { + return wrap_exceptions(conf, [&]{ unbox(conf).load_key_message( msg_hash, ustring_view{data, datalen}, timestamp_ms, *unbox(info), *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; - } - return true; + return true; + }, false); } LIBSESSION_C_API config_string_list* groups_keys_current_hashes(const config_group_keys* conf) { @@ -1566,7 +1584,7 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( size_t* plaintext_len) { assert(ciphertext_in && plaintext_out && plaintext_len); - try { + return wrap_exceptions(conf, [&]{ auto [sid, plaintext] = unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); std::memcpy(session_id, sid.c_str(), sid.size() + 1); @@ -1574,10 +1592,7 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); *plaintext_len = plaintext.size(); return true; - } catch (const std::exception& e) { - set_error(conf, e.what()); - } - return false; + }, false); } LIBSESSION_C_API bool groups_keys_key_supplement( @@ -1591,16 +1606,14 @@ LIBSESSION_C_API bool groups_keys_key_supplement( std::vector session_ids; for (size_t i = 0; i < sids_len; i++) session_ids.emplace_back(sids[i]); - try { + + return wrap_exceptions(conf, [&]{ 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; - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; - } + }, false); } LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { @@ -1614,15 +1627,12 @@ LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( bool del, unsigned char* sign_value) { assert(sign_value); - try { + return wrap_exceptions(conf, [&]{ auto val = unbox(conf).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; - } + }, false); } LIBSESSION_C_API bool groups_keys_swarm_make_subaccount( @@ -1652,10 +1662,14 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value) { - return groups::Keys::swarm_verify_subaccount( - group_id, - ustring_view{session_ed25519_secretkey, 64}, - ustring_view{signing_value, 100}); + try { + return groups::Keys::swarm_verify_subaccount( + group_id, + ustring_view{session_ed25519_secretkey, 64}, + ustring_view{signing_value, 100}); + } catch (...) { + return false; + } } LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( @@ -1668,7 +1682,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( char* subaccount_sig, char* signature) { assert(msg && signing_value && subaccount && subaccount_sig && signature); - try { + return wrap_exceptions(conf, [&]{ auto auth = unbox(conf).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); assert(auth.subaccount.size() == 48); @@ -1678,10 +1692,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( std::memcpy(subaccount_sig, auth.subaccount_sig.c_str(), auth.subaccount_sig.size() + 1); 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; - } + }, false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( @@ -1694,7 +1705,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( unsigned char* subaccount_sig, unsigned char* signature) { assert(msg && signing_value && subaccount && subaccount_sig && signature); - try { + return wrap_exceptions(conf, [&]{ auto auth = unbox(conf).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}, true); assert(auth.subaccount.size() == 36); @@ -1704,10 +1715,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 64); std::memcpy(signature, auth.signature.data(), 64); return true; - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; - } + }, false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( @@ -1716,15 +1724,12 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( bool write, bool del, unsigned char* token) { - try { + return wrap_exceptions(conf, [&]{ auto tok = unbox(conf).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; - } + }, false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_token( diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..5ee7aec3 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -191,34 +191,28 @@ LIBSESSION_C_API int groups_members_init( LIBSESSION_C_API bool groups_members_get( config_object* conf, config_group_member* member, const char* session_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->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; - } - return false; + return false; + }, false); } LIBSESSION_C_API bool groups_members_get_or_construct( config_object* conf, config_group_member* member, const char* session_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf)->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; - return false; - } + }, false); } LIBSESSION_C_API void groups_members_set(config_object* conf, const config_group_member* member) { - unbox(conf)->set(groups::member{*member}); + return wrap_exceptions(conf, [&]{ + unbox(conf)->set(groups::member{*member}); + }); + } LIBSESSION_C_API bool groups_members_erase(config_object* conf, const char* session_id) { diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 8872ea4c..c338cbc3 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -74,14 +74,6 @@ template 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) - src.remove_suffix(src.size() - N - 1); - std::memcpy(dest, src.data(), src.size()); - dest[src.size()] = 0; -} - // Copies a container of std::strings into a self-contained malloc'ed config_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()`). diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 576dd3bf..eb3e5875 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -623,17 +623,13 @@ int user_groups_init( LIBSESSION_C_API bool user_groups_get_community( config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto c = unbox(conf)->get_community(base_url, room)) { c->into(*comm); return true; } - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return false; + return false; + }, false); } LIBSESSION_C_API bool user_groups_get_or_construct_community( config_object* conf, @@ -641,41 +637,29 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( const char* base_url, const char* room, unsigned const char* pubkey) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf) ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) .into(*comm); return true; - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; - } + }, false); } LIBSESSION_C_API bool user_groups_get_group( config_object* conf, ugroups_group_info* group, const char* group_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ if (auto g = unbox(conf)->get_group(group_id)) { g->into(*group); return true; } - } catch (const std::exception& e) { - set_error(conf, e.what()); - } - return false; + return false; + }, false); } LIBSESSION_C_API bool user_groups_get_or_construct_group( config_object* conf, ugroups_group_info* group, const char* group_id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ unbox(conf)->get_or_construct_group(group_id).into(*group); return true; - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; - } + }, false); } LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { @@ -687,34 +671,25 @@ LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_legacy_group( config_object* conf, const char* id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ auto group = std::make_unique(); group->_internal = nullptr; if (auto c = unbox(conf)->get_legacy_group(id)) { std::move(c)->into(*group); return group.release(); } - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return nullptr; + return static_cast(nullptr); + }); } LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( config_object* conf, const char* id) { - try { - conf->last_error = nullptr; + return wrap_exceptions(conf, [&]{ auto group = std::make_unique(); group->_internal = nullptr; unbox(conf)->get_or_construct_legacy_group(id).into(*group); return group.release(); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return nullptr; - } + }); } LIBSESSION_C_API void user_groups_set_community( @@ -722,15 +697,21 @@ LIBSESSION_C_API void user_groups_set_community( unbox(conf)->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}); + wrap_exceptions(conf, [&]{ + unbox(conf)->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}); + wrap_exceptions(conf, [&]{ + unbox(conf)->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)}); + wrap_exceptions(conf, [&]{ + unbox(conf)->set(legacy_group_info{std::move(*group)}); + }); } LIBSESSION_C_API bool user_groups_erase_community( diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 1c5a3df2..5f024a83 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -42,12 +42,10 @@ 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 { + return wrap_exceptions(conf, [&]{ unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - return 0; + return 0; + }, static_cast(SESSION_ERR_BAD_VALUE)); } profile_pic UserProfile::get_profile_pic() const { @@ -84,13 +82,10 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic if (!url.empty()) key = {pic.key, 32}; - try { + return wrap_exceptions(conf, [&]{ unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - - return 0; + return 0; + }, static_cast(SESSION_ERR_BAD_VALUE)); } void UserProfile::set_nts_priority(int priority) { diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 52418640..ddd393b0 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -721,10 +721,12 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { m.info, m.members)); [[maybe_unused]] 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)); + hashes = config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1); + REQUIRE(m.info->last_error == "Cannot merge configs without any decryption keys"sv); + m.info->last_error = nullptr; + hashes = config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1); + REQUIRE(m.members->last_error == "Cannot merge configs without any decryption keys"sv); + m.members->last_error = nullptr; REQUIRE(groups_members_size(m.members) == 0); } From b20b0ffce08da126edbad7693a4411170a00b0ce Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 3 Jun 2024 17:11:28 +1000 Subject: [PATCH 293/572] Updated get_connection_info vars to be shared_ptrs to prevent bg crash --- src/network.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index f6818bd3..ad24061d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -532,11 +532,11 @@ std::shared_ptr Network::get_endpoint() { } connection_info Network::get_connection_info(service_node target) { - std::once_flag cb_called; - std::mutex mutex; - std::condition_variable cv; - bool connection_established = false; - bool done = false; + auto cb_called = std::make_shared(); + auto mutex = std::make_shared(); + auto cv = std::make_shared(); + auto connection_established = std::make_shared(false); + auto done = std::make_shared(false); auto connection_key_pair = ed25519::ed25519_key_pair(); auto creds = quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); @@ -545,26 +545,26 @@ connection_info Network::get_connection_info(service_node target) { target, creds, quic::opt::keep_alive{10s}, - [&mutex, &cv, &connection_established, &done, &cb_called](quic::connection_interface&) { - std::call_once(cb_called, [&]() { + [mutex, cv, connection_established, done, cb_called](quic::connection_interface&) { + std::call_once(*cb_called, [&]() { { - std::lock_guard lock(mutex); - connection_established = true; - done = true; + std::lock_guard lock(*mutex); + *connection_established = true; + *done = true; } - cv.notify_one(); + cv->notify_one(); }); }, - [this, target, &mutex, &cv, &done, &cb_called]( + [this, target, mutex, cv, done, cb_called]( quic::connection_interface& conn, uint64_t) { // Trigger the callback first before updating the paths in case this was triggered // when try to establish a connection - std::call_once(cb_called, [&]() { + std::call_once(*cb_called, [&]() { { - std::lock_guard lock(mutex); - done = true; + std::lock_guard lock(*mutex); + *done = true; } - cv.notify_one(); + cv->notify_one(); }); // When the connection is closed, update the path and connection status @@ -588,10 +588,10 @@ connection_info Network::get_connection_info(service_node target) { } }); - std::unique_lock lock(mutex); - cv.wait(lock, [&done] { return done; }); + std::unique_lock lock(*mutex); + cv->wait(lock, [&done] { return *done; }); - if (!connection_established) + if (!*connection_established) return {target, nullptr, nullptr}; return {target, c, c->open_stream()}; From 70a39f6e37e6eb42b812e515a4ebce70877b1b01 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 5 Jun 2024 10:57:51 +1000 Subject: [PATCH 294/572] Updated to the latest libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index f4b75ec0..88aba0cd 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit f4b75ec0f5ae985ffd6b7df567f1b23959b56b5c +Subproject commit 88aba0cd7b2b49a30b2b6037b0076ae1fdd25d97 From 66193912627619b85c17d391820c3feac689164f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 6 Jun 2024 14:14:26 +1000 Subject: [PATCH 295/572] chore: fetch protobuf as git submodule --- .gitmodules | 3 +++ external/CMakeLists.txt | 13 +++++++++++++ external/protobuf | 1 + proto/CMakeLists.txt | 34 ++-------------------------------- 4 files changed, 19 insertions(+), 32 deletions(-) create mode 160000 external/protobuf diff --git a/.gitmodules b/.gitmodules index e8d2e5df..3b5ab0d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "external/nlohmann-json"] path = external/nlohmann-json url = https://github.com/nlohmann/json.git +[submodule "external/protobuf"] + path = external/protobuf + url = https://github.com/protocolbuffers/protobuf.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a99ec4d5..7344e118 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -29,9 +29,22 @@ if(SUBMODULE_CHECK) check_submodule(oxen-encoding) check_submodule(nlohmann-json) check_submodule(zstd) + check_submodule(protobuf) endif() endif() +set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) +set(protobuf_INSTALL ON CACHE BOOL "" FORCE) +set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) +set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") +set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) +add_subdirectory(protobuf) + if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) diff --git a/external/protobuf b/external/protobuf new file mode 160000 index 00000000..f0dc78d7 --- /dev/null +++ b/external/protobuf @@ -0,0 +1 @@ +Subproject commit f0dc78d7e6e331b8c6bb2d5283e06aa26883ca7c diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 18386102..a99ded2f 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -15,39 +15,9 @@ if (BUILD_SHARED_LIBS AND NOT BUILD_STATIC_DEPS) endif() endif() -if(NOT TARGET protobuf::libprotobuf-lite) +check_target(protobuf::libprotobuf-lite) +libsession_static_bundle(protobuf::libprotobuf-lite) -# System protobuf not found, or we are building our own deps: - include(FetchContent) - - FetchContent_Declare( - protobuf - GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git - GIT_TAG v3.21.12 # apparently this must be a tag (not hash) for git_shallow to work? - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE - ) - - set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) - set(protobuf_INSTALL ON CACHE BOOL "" FORCE) - set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) - set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) - set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) - set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) - set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") - set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) - - message(STATUS "Pulling protobuf repository...") - - FetchContent_MakeAvailable(protobuf) - - check_target(protobuf::libprotobuf-lite) - - libsession_static_bundle(protobuf::libprotobuf-lite) - -endif() add_library(protos SessionProtos.pb.cc From 702302626d401d6954e67b60b28805c785be7a7c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Jun 2024 15:31:56 +1000 Subject: [PATCH 296/572] Added separate paths for uploading/downloading files, fixed a few bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added separate upload & download paths so they don't hold up standard requests • Added a separate failure threshold for timeouts (legacy PN server was resulting in timeouts that would cause paths to rebuild too frequently) • Tweaked the snode pool cache logs so we don't warn for expired swarm caches (was spammy) • Make the parse_url function more generic and moved to util • Fixed an issue where the "node not found" error wasn't detected correctly due to a discrepancy to the error string --- include/session/config/base.hpp | 7 +- include/session/network.h | 66 ++ include/session/network.hpp | 203 ++++-- include/session/util.hpp | 7 + src/config/base.cpp | 22 +- src/config/community.cpp | 70 +- src/config/contacts.cpp | 32 +- src/config/convo_info_volatile.cpp | 129 ++-- src/config/groups/info.cpp | 33 +- src/config/groups/keys.cpp | 185 ++--- src/config/groups/members.cpp | 34 +- src/config/user_groups.cpp | 77 ++- src/config/user_profile.cpp | 22 +- src/network.cpp | 781 ++++++++++++++++------ src/onionreq/builder.cpp | 8 +- src/onionreq/response_parser.cpp | 17 +- src/util.cpp | 61 ++ tests/test_config_convo_info_volatile.cpp | 2 +- tests/test_configdata.cpp | 2 +- tests/test_network.cpp | 160 ++++- 20 files changed, 1337 insertions(+), 581 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 14de937b..d9a14b2d 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1278,8 +1278,8 @@ void copy_c_str(char (&dest)[N], std::string_view src) { dest[src.size()] = 0; } -// Wraps a labmda and, if an exception is thrown, 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. +// Wraps a labmda and, if an exception is thrown, 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. // // No return value: accepts void and pointer returns; pointer returns will become nullptr on error template @@ -1295,7 +1295,8 @@ decltype(auto) wrap_exceptions(config_object* conf, Call&& f) { } if constexpr (std::is_pointer_v) return static_cast(nullptr); - else static_assert(std::is_void_v, "Don't know how to return an error value!"); + else + static_assert(std::is_void_v, "Don't know how to return an error value!"); } // Same as above but accepts callbacks with value returns on errors: returns `f()` on success, diff --git a/include/session/network.h b/include/session/network.h index 3ce4d88d..1d7d1523 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -18,6 +18,12 @@ typedef enum CONNECTION_STATUS { CONNECTION_STATUS_DISCONNECTED = 3, } CONNECTION_STATUS; +typedef enum CLIENT_PLATFORM { + CLIENT_PLATFORM_ANDROID = 0, + CLIENT_PLATFORM_DESKTOP = 1, + CLIENT_PLATFORM_IOS = 2, +} CLIENT_PLATFORM; + typedef struct network_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; @@ -221,6 +227,66 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( network_onion_response_callback_t callback, void* ctx); +/// API: network/network_upload_to_server +/// +/// Uploads a file to a server. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `server` -- [in] struct containing information about the server the request should be sent to. +/// - `data` -- [in] data to upload to the file server. +/// - `data_len` -- [in] size of the `data`. +/// - `file_name` -- [in, optional] name of the file being uploaded. MUST be null terminated. +/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set +/// to NULL if unused. +LIBSESSION_EXPORT void network_upload_to_server( + network_object* network, + const network_server_destination server, + const unsigned char* data, + size_t data_len, + const char* file_name, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx); + +/// API: network/network_download_from_server +/// +/// Downloads a file from a server. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `server` -- [in] struct containing information about file to be downloaded. +/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set +/// to NULL if unused. +LIBSESSION_EXPORT void network_download_from_server( + network_object* network, + const network_server_destination server, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx); + +/// API: network/network_get_client_version +/// +/// Retrieves the version information for the given platform. +/// +/// Inputs: +/// - `network` -- [in] Pointer to the network object. +/// - `platform` -- [in] the platform to retrieve the client version for. +/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `callback` -- [in] callback to be called with the result of the request. +/// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set +/// to NULL if unused. +LIBSESSION_EXPORT void network_get_client_version( + network_object* network, + CLIENT_PLATFORM platform, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx); + #ifdef __cplusplus } #endif diff --git a/include/session/network.hpp b/include/session/network.hpp index e43d0783..da06c126 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -21,6 +21,18 @@ enum class ConnectionStatus { disconnected, }; +enum class PathType { + standard, + upload, + download, +}; + +enum class Platform { + android, + desktop, + ios, +}; + struct connection_info { service_node node; std::shared_ptr conn; @@ -33,9 +45,11 @@ struct onion_path { connection_info conn_info; std::vector nodes; uint8_t failure_count; + uint8_t timeout_count; bool operator==(const onion_path& other) const { - return nodes == other.nodes && failure_count == other.failure_count; + return nodes == other.nodes && failure_count == other.failure_count && + timeout_count == other.timeout_count; } }; @@ -45,6 +59,7 @@ struct request_info { std::optional body; std::optional swarm_pubkey; onion_path path; + PathType path_type; std::chrono::milliseconds timeout; bool node_destination; bool is_retry; @@ -75,12 +90,16 @@ class Network { ConnectionStatus status; oxen::quic::Network net; - std::vector paths; + std::vector standard_paths; + std::vector upload_paths; + std::vector download_paths; std::shared_ptr paths_and_pool_loop; std::shared_ptr endpoint; public: + friend class TestNetwork; + // Hook to be notified whenever the network connection status changes. std::function status_changed; @@ -168,79 +187,83 @@ class Network { std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - bool is_retry, network_response_callback_t handle_response); - /// API: network/validate_response + /// API: network/upload_file_to_server /// - /// Processes a quic response to extract the status code and body or throw if it errored or - /// received a non-successful status code. + /// Uploads a file to a given server destination. /// /// Inputs: - /// - `resp` -- [in] the quic response. - /// - `is_bencoded` -- [in] flag indicating whether the response will be bencoded or JSON. - /// - /// Returns: - /// - `std::pair` -- the status code and response body (for a bencoded - /// response this is just the direct response body from quic as it simplifies consuming the - /// response elsewhere). - std::pair validate_response(oxen::quic::message resp, bool is_bencoded); - - /// API: network/handle_errors - /// - /// Processes a non-success response to automatically perform any standard operations based on - /// the errors returned from the service node network (ie. updating the service node cache, - /// dropping nodes and/or onion request paths). - /// - /// Inputs: - /// - `info` -- [in] the information for the request that was made. - /// - `timeout` -- [in, optional] flag indicating whether the request timed out. - /// - `status_code` -- [in, optional] the status code returned from the network. - /// - `response` -- [in, optional] response data returned from the network. - /// - `handle_response` -- [in, optional] callback to be called with updated response - /// information after processing the error. - void handle_errors( - request_info info, - bool timeout, - std::optional status_code, - std::optional response, - std::optional handle_response); + /// - 'data' - [in] the data to be uploaded to a server. + /// - `server` -- [in] the server destination to upload the file to. + /// - `file_name` -- [in, optional] optional name to use for the file. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void upload_file_to_server( + ustring data, + onionreq::ServerDestination server, + std::optional file_name, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response); - /// API: network/get_failure_count + /// API: network/download_file /// - /// Retrieves the current failure count for a given node, returns 0 if it is not present. + /// Download a file from a given server destination. /// /// Inputs: - /// - `node` -- [in] the node to get the failure count for. - uint8_t get_failure_count(service_node node); + /// - `server` -- [in] the server destination to download the file from. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void download_file( + onionreq::ServerDestination server, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response); - /// API: network/set_failure_count + /// API: network/download_file /// - /// Updated the failure count for a given node. + /// Convenience function to download a file from a given url and x25519 pubkey combination. + /// Calls through to the above `download_file` function after constructing a server destination + /// from the provided values. /// /// Inputs: - /// - `node` -- [in] the node to get the failure count for. - /// - `failure_count` -- [in] the failure count to set the node to. - void set_failure_count(service_node node, uint8_t failure_count); + /// - `download_url` -- [in] the url to download the file from. + /// - `x25519_pubkey` -- [in] the server destination to download the file from. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void download_file( + std::string_view download_url, + onionreq::x25519_pubkey x25519_pubkey, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response); - /// API: network/set_paths + /// API: network/get_client_version /// - /// Update the paths to be used for sending onion requests. This function should never be - /// called directly. + /// Retrieves the version information for the given platform. /// /// Inputs: - /// - `paths` -- [in] paths to use. - void set_paths(std::vector paths); + /// - `platform` -- [in] the platform to retrieve the client version for. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void get_client_version( + Platform platform, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response); - /// API: network/get_failure_count + private: + /// API: network/paths_for_type /// - /// Retrieves the current failure count for a given path, returns 0 if it is not present. + /// Internal function to retrieve the onion paths for a given request type /// /// Inputs: - /// - `path` -- [in] the path to get the failure count for. - uint8_t get_failure_count(onion_path path); + /// - 'path_type' - [in] the type of paths to retrieve. + std::vector paths_for_type(PathType type) const { + switch (type) { + case PathType::standard: return standard_paths; + case PathType::upload: return upload_paths; + case PathType::download: return download_paths; + } + }; - private: /// API: network/update_status /// /// Internal function to update the connection status and trigger the `status_changed` hook if @@ -274,11 +297,12 @@ class Network { /// closed in case it fails to establish) before returning the connection. /// /// Inputs: + /// - 'path_type' - [in] the type of paths to retrieve. /// - `target` -- [in] the target service node to connect to. /// /// Returns: /// - `connection_info` -- The connection info for the target service node. - connection_info get_connection_info(service_node target); + connection_info get_connection_info(PathType path_type, service_node target); /// API: network/with_paths_and_pool /// @@ -286,10 +310,13 @@ class Network { /// cache is empty it will first be populated from the network. /// /// Inputs: + /// - `path_type` -- [in] the type of path the request should be sent along. + /// - `excluded_node` -- [in, optional] node which should not be included in the path. /// - `callback` -- [in] callback to be triggered once we have built the paths and service node /// pool. NOTE: If we are unable to build the paths or retrieve the service node pool the /// callback will be triggered with empty lists and an error. void with_paths_and_pool( + PathType path_type, std::optional excluded_node, std::function< void(std::vector updated_paths, @@ -303,10 +330,12 @@ class Network { /// service nodes in the snode pool. /// /// Inputs: + /// - `path_type` -- [in] the type of path the request should be sent along. /// - `excluded_node` -- [in, optional] node which should not be included in the path. /// - `callback` -- [in] callback to be triggered once we have a valid path, NULL if we are /// unable to find a valid path. void with_path( + PathType path_type, std::optional excluded_node, std::function path, std::optional error)> callback); @@ -327,6 +356,7 @@ class Network { /// Returns a pair of bools indicating whether the provided paths and pool are valid. /// /// Inputs: + /// - `path_type` -- [in] the type of path to validate the size for. /// - `paths` -- [in] paths to validate size for. /// - `pool` -- [in] pool to validate size for. /// - `last_pool_update` -- [in] timestamp for when the pool was last updated. @@ -334,16 +364,33 @@ class Network { /// Outputs: /// - A pair of flags indicating whether the provided paths and pool have the correct sizes. std::pair validate_paths_and_pool_sizes( + PathType path_type, std::vector paths, std::vector pool, std::chrono::system_clock::time_point last_pool_update); + /// API: network/validate_response + /// + /// Processes a quic response to extract the status code and body or throw if it errored or + /// received a non-successful status code. + /// + /// Inputs: + /// - `resp` -- [in] the quic response. + /// - `is_bencoded` -- [in] flag indicating whether the response will be bencoded or JSON. + /// + /// Returns: + /// - `std::pair` -- the status code and response body (for a bencoded + /// response this is just the direct response body from quic as it simplifies consuming the + /// response elsewhere). + std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + /// API: network/find_possible_path /// /// Picks a random path from the provided paths excluding the provided node if one is available. /// /// Inputs: /// - `excluded_node` -- [in, optional] node which should not be included in the paths. + /// - `paths` -- [in] paths to select from. /// /// Outputs: /// - The possible path, if found, and the number of paths provided. @@ -374,12 +421,14 @@ class Network { /// received or the list is drained. /// /// Inputs: + /// - `path_type` -- [in] the type of path to validate the size for. /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's /// drained. /// - `callback` -- [in] callback to be triggered once we make a successful request. NOTE: If /// we drain the `target_nodes` and haven't gotten a successful response then the callback will /// be invoked with a std::nullopt `valid_guard_node` and `unused_nodes`. void find_valid_guard_node_recursive( + PathType path_type, std::vector target_nodes, std::function< void(std::optional valid_guard_node, @@ -405,12 +454,14 @@ class Network { /// Retrieves the version information for a given service node. /// /// Inputs: + /// - 'type' - [in] the type of paths to send the request across. /// - `node` -- [in] node to retrieve the version from. /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the /// `quic::DEFAULT_TIMEOUT` will be used. /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an /// error occurs an empty list and an error will be provided. void get_version( + PathType path_type, service_node node, std::optional timeout, std::function< @@ -418,6 +469,30 @@ class Network { connection_info info, std::optional error)> callback); + /// API: network/send_onion_request + /// + /// Sends a request via onion routing to the provided service node or server destination. + /// + /// Inputs: + /// - 'type' - [in] the type of paths to send the request across. + /// - `destination` -- [in] service node or server destination information. + /// - `body` -- [in] data to send to the specified destination. + /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. + /// Should be NULL if the request is not associated with a swarm. + /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used + /// for internal purposes for cases which should retry automatically (like receiving a `421`) in + /// order to prevent subsequent retries. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void send_onion_request( + PathType type, + onionreq::network_destination destination, + std::optional body, + std::optional swarm_pubkey, + std::chrono::milliseconds timeout, + bool is_retry, + network_response_callback_t handle_response); + /// API: network/process_snode_response /// /// Processes the response from an onion request sent to a service node destination. @@ -449,6 +524,26 @@ class Network { std::string response, request_info info, network_response_callback_t handle_response); + + /// API: network/handle_errors + /// + /// Processes a non-success response to automatically perform any standard operations based on + /// the errors returned from the service node network (ie. updating the service node cache, + /// dropping nodes and/or onion request paths). + /// + /// Inputs: + /// - `info` -- [in] the information for the request that was made. + /// - `timeout` -- [in, optional] flag indicating whether the request timed out. + /// - `status_code` -- [in, optional] the status code returned from the network. + /// - `response` -- [in, optional] response data returned from the network. + /// - `handle_response` -- [in, optional] callback to be called with updated response + /// information after processing the error. + void handle_errors( + request_info info, + bool timeout, + std::optional status_code, + std::optional response, + std::optional handle_response); }; } // namespace session::network diff --git a/include/session/util.hpp b/include/session/util.hpp index 06b92fe4..00d0293b 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -148,4 +148,11 @@ std::vector to_view_vector(const Container& c) { std::vector split( std::string_view str, std::string_view delim, bool trim = false); +/// Returns protocol, host, port, path. Port can be empty; throws on unparseable values. protocol +/// and host get normalized to lower-case. Port will be null if not present in the URL, or if set +/// to the default for the protocol. Path can be empty (a single optional `/` after the domain will +/// be ignored). +std::tuple, std::optional> parse_url( + std::string_view url); + } // namespace session diff --git a/src/config/base.cpp b/src/config/base.cpp index 0ff179aa..83f056c0 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -651,7 +651,7 @@ LIBSESSION_EXPORT config_string_list* config_merge( const unsigned char** configs, const size_t* lengths, size_t count) { - return wrap_exceptions(conf, [&]{ + return wrap_exceptions(conf, [&] { auto& config = *unbox(conf); std::vector> confs; confs.reserve(count); @@ -667,7 +667,7 @@ LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { } LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { - return wrap_exceptions(conf, [&]{ + return wrap_exceptions(conf, [&] { auto& config = *unbox(conf); auto [seqno, data, obs] = config.push(); @@ -710,7 +710,7 @@ LIBSESSION_EXPORT void config_confirm_pushed( } LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen) { - wrap_exceptions(conf, [&]{ + wrap_exceptions(conf, [&] { assert(out && outlen); auto data = unbox(conf)->dump(); *outlen = data.size(); @@ -751,15 +751,11 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size } LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - wrap_exceptions(conf, [&]{ - unbox(conf)->add_key({key, 32}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->add_key({key, 32}); }); } LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - wrap_exceptions(conf, [&]{ - unbox(conf)->add_key({key, 32}, /*high_priority=*/false); - }); + wrap_exceptions(conf, [&] { unbox(conf)->add_key({key, 32}, /*high_priority=*/false); }); } LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { return unbox(conf)->clear_keys(); @@ -786,15 +782,11 @@ LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf } LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set_sig_keys({secret, 64}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set_sig_keys({secret, 64}); }); } LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set_sig_pubkey({pubkey, 32}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set_sig_pubkey({pubkey, 32}); }); } LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { diff --git a/src/config/community.cpp b/src/config/community.cpp index 12cb85a9..78894a80 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -90,67 +90,6 @@ std::string community::full_url( return url; } -// returns protocol, host, port. Port can be empty; throws on unparseable values. protocol and -// host get normalized to lower-case. Port will be 0 if not present in the URL, or if set to -// the default for the protocol. The URL must not include a path (though a single optional `/` -// after the domain is accepted and ignored). -std::tuple parse_url(std::string_view url) { - std::tuple result{}; - auto& [proto, host, port] = result; - if (auto pos = url.find("://"); pos != std::string::npos) { - auto proto_name = url.substr(0, pos); - url.remove_prefix(proto_name.size() + 3); - if (string_iequal(proto_name, "http")) - proto = "http://"; - else if (string_iequal(proto_name, "https")) - proto = "https://"; - } - if (proto.empty()) - throw std::invalid_argument{"Invalid community URL: invalid/missing protocol://"}; - - bool next_allow_dot = false; - bool has_dot = false; - while (!url.empty()) { - auto c = url.front(); - if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-') { - host += c; - next_allow_dot = true; - } else if (c >= 'A' && c <= 'Z') { - host += c + ('a' - 'A'); - next_allow_dot = true; - } else if (next_allow_dot && c == '.') { - host += '.'; - has_dot = true; - next_allow_dot = false; - } else { - break; - } - url.remove_prefix(1); - } - if (host.size() < 4 || !has_dot || host.back() == '.') - throw std::invalid_argument{"Invalid community URL: invalid hostname"}; - - if (!url.empty() && url.front() == ':') { - url.remove_prefix(1); - if (auto [p, ec] = std::from_chars(url.data(), url.data() + url.size(), port); - ec == std::errc{}) - url.remove_prefix(p - url.data()); - else - throw std::invalid_argument{"Invalid community URL: invalid port"}; - if ((port == 80 && proto == "http://") || (port == 443 && proto == "https://")) - port = 0; - } - - if (!url.empty() && url.front() == '/') - url.remove_prefix(1); - - // We don't (currently) allow a /path in a community URL - if (!url.empty()) - throw std::invalid_argument{"Invalid community URL: found unexpected trailing value"}; - - return result; -} - void community::canonicalize_url(std::string& url) { if (auto new_url = canonical_url(url); new_url != url) url = std::move(new_url); @@ -170,14 +109,17 @@ void community::canonicalize_room(std::string& room) { } std::string community::canonical_url(std::string_view url) { - const auto& [proto, host, port] = parse_url(url); + const auto& [proto, host, port, path] = parse_url(url); std::string result; result += proto; result += host; - if (port != 0) { + if (port) { result += ':'; - result += std::to_string(port); + result += std::to_string(*port); } + // We don't (currently) allow a /path in a community URL + if (path) + throw std::invalid_argument{"Invalid community URL: found unexpected trailing value"}; if (result.size() > BASE_URL_MAX_LENGTH) throw std::invalid_argument{"Invalid community URL: base URL is too long"}; return result; diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 24298f04..193ec063 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -176,13 +176,16 @@ std::optional Contacts::get(std::string_view pubkey_hex) const { LIBSESSION_C_API bool contacts_get( config_object* conf, contacts_contact* contact, const char* session_id) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get(session_id)) { - c->into(*contact); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get(session_id)) { + c->into(*contact); + return true; + } + return false; + }, + false); } contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { @@ -194,10 +197,13 @@ contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { LIBSESSION_C_API bool contacts_get_or_construct( config_object* conf, contacts_contact* contact, const char* session_id) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct(session_id).into(*contact); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct(session_id).into(*contact); + return true; + }, + false); } void Contacts::set(const contact_info& contact) { @@ -239,9 +245,7 @@ void Contacts::set(const contact_info& contact) { } LIBSESSION_C_API void contacts_set(config_object* conf, const contacts_contact* contact) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(contact_info{*contact}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set(contact_info{*contact}); }); } void Contacts::set_name(std::string_view session_id, std::string name) { diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index f3c9a241..80b8447a 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -496,21 +496,27 @@ int convo_info_volatile_init( LIBSESSION_C_API bool convo_info_volatile_get_1to1( config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get_1to1(session_id)) { - c->into(*convo); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_1to1(session_id)) { + c->into(*convo); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_1to1( config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct_1to1(session_id).into(*convo); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct_1to1(session_id).into(*convo); + return true; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_community( @@ -518,13 +524,16 @@ LIBSESSION_C_API bool convo_info_volatile_get_community( convo_info_volatile_community* og, const char* base_url, const char* room) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get_community(base_url, room)) { - c->into(*og); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_community(base_url, room)) { + c->into(*og); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( config_object* conf, @@ -532,57 +541,70 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( const char* base_url, const char* room, unsigned const char* pubkey) { - return wrap_exceptions(conf, [&]{ - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) - .into(*convo); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf) + ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + .into(*convo); + return true; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_group( config_object* conf, convo_info_volatile_group* convo, const char* id) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get_group(id)) { - c->into(*convo); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_group(id)) { + c->into(*convo); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( config_object* conf, convo_info_volatile_group* convo, const char* id) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct_group(id).into(*convo); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct_group(id).into(*convo); + return true; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get_legacy_group(id)) { - c->into(*convo); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_legacy_group(id)) { + c->into(*convo); + return true; + } + return false; + }, + 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) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct_legacy_group(id).into(*convo); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct_legacy_group(id).into(*convo); + return true; + }, + false); } LIBSESSION_C_API void convo_info_volatile_set_1to1( config_object* conf, const convo_info_volatile_1to1* convo) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(convo::one_to_one{*convo}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set(convo::one_to_one{*convo}); }); } LIBSESSION_C_API void convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo) { @@ -590,15 +612,12 @@ LIBSESSION_C_API void convo_info_volatile_set_community( } LIBSESSION_C_API void convo_info_volatile_set_group( config_object* conf, const convo_info_volatile_group* convo) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(convo::group{*convo}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set(convo::group{*convo}); }); } LIBSESSION_C_API void convo_info_volatile_set_legacy_group( config_object* conf, const convo_info_volatile_legacy_group* convo) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(convo::legacy_group{*convo}); - }); + wrap_exceptions( + conf, [&] { unbox(conf)->set(convo::legacy_group{*convo}); }); } LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 5524dabe..1601ebcc 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -163,10 +163,13 @@ LIBSESSION_C_API const char* groups_info_get_name(const config_object* conf) { /// Outputs: /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_C_API int groups_info_set_name(config_object* conf, const char* name) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->set_name(name); - return 0; - }, static_cast(SESSION_ERR_BAD_VALUE)); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_name(name); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_description @@ -199,10 +202,13 @@ LIBSESSION_C_API const char* groups_info_get_description(const config_object* co /// 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 wrap_exceptions(conf, [&]{ - unbox(conf)->set_description(description); - return 0; - }, static_cast(SESSION_ERR_BAD_VALUE)); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_description(description); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_pic @@ -244,10 +250,13 @@ LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic p if (!url.empty()) key = {pic.key, 32}; - return wrap_exceptions(conf, [&]{ - unbox(conf)->set_profile_pic(url, key); - return 0; - }, static_cast(SESSION_ERR_BAD_VALUE)); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_profile_pic(url, key); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); } /// API: groups_info/groups_info_get_expiry_timer diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a9126f56..3c6a811c 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1376,8 +1376,8 @@ const groups::Keys& unbox(const config_group_keys* conf) { return *static_cast(conf->internals); } -// Wraps a labmda and, if an exception is thrown, 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. +// Wraps a labmda and, if an exception is thrown, 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. // // No return value: accepts void and pointer returns; pointer returns will become nullptr on error template @@ -1393,7 +1393,8 @@ decltype(auto) wrap_exceptions(config_group_keys* conf, Call&& f) { } if constexpr (std::is_pointer_v) return nullptr; - else static_assert(std::is_void_v, "Don't know how to return an error value!"); + else + static_assert(std::is_void_v, "Don't know how to return an error value!"); } // Same as above but accepts callbacks with value returns on errors: returns `f()` on success, @@ -1474,13 +1475,16 @@ LIBSESSION_C_API bool groups_keys_load_admin_key( const unsigned char* secret, config_object* info, config_object* members) { - return wrap_exceptions(conf, [&]{ - unbox(conf).load_admin_key( - ustring_view{secret, 32}, - *unbox(info), - *unbox(members)); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf).load_admin_key( + ustring_view{secret, 32}, + *unbox(info), + *unbox(members)); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_rekey( @@ -1493,14 +1497,17 @@ LIBSESSION_C_API bool groups_keys_rekey( auto& keys = unbox(conf); ustring_view to_push; - return wrap_exceptions(conf, [&]{ - to_push = keys.rekey(*unbox(info), *unbox(members)); - if (out && outlen) { - *out = to_push.data(); - *outlen = to_push.size(); - } - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + to_push = keys.rekey(*unbox(info), *unbox(members)); + if (out && outlen) { + *out = to_push.data(); + *outlen = to_push.size(); + } + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_pending_config( @@ -1523,15 +1530,18 @@ LIBSESSION_C_API bool groups_keys_load_message( config_object* info, config_object* members) { assert(data && info && members); - return wrap_exceptions(conf, [&]{ - unbox(conf).load_key_message( - msg_hash, - ustring_view{data, datalen}, - timestamp_ms, - *unbox(info), - *unbox(members)); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf).load_key_message( + msg_hash, + ustring_view{data, datalen}, + timestamp_ms, + *unbox(info), + *unbox(members)); + return true; + }, + false); } LIBSESSION_C_API config_string_list* groups_keys_current_hashes(const config_group_keys* conf) { @@ -1584,15 +1594,18 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( size_t* plaintext_len) { assert(ciphertext_in && plaintext_out && plaintext_len); - return wrap_exceptions(conf, [&]{ - 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(); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + 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(); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_key_supplement( @@ -1607,13 +1620,16 @@ LIBSESSION_C_API bool groups_keys_key_supplement( for (size_t i = 0; i < sids_len; i++) session_ids.emplace_back(sids[i]); - return wrap_exceptions(conf, [&]{ - 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; - }, false); + return wrap_exceptions( + conf, + [&] { + 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; + }, + false); } LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { @@ -1627,12 +1643,15 @@ LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( bool del, unsigned char* sign_value) { assert(sign_value); - return wrap_exceptions(conf, [&]{ - auto val = unbox(conf).swarm_make_subaccount(session_id, write, del); - assert(val.size() == 100); - std::memcpy(sign_value, val.data(), val.size()); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + auto val = unbox(conf).swarm_make_subaccount(session_id, write, del); + assert(val.size() == 100); + std::memcpy(sign_value, val.data(), val.size()); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_swarm_make_subaccount( @@ -1682,17 +1701,23 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( char* subaccount_sig, char* signature) { assert(msg && signing_value && subaccount && subaccount_sig && signature); - return wrap_exceptions(conf, [&]{ - auto auth = unbox(conf).swarm_subaccount_sign( - ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); - assert(auth.subaccount.size() == 48); - assert(auth.subaccount_sig.size() == 88); - assert(auth.signature.size() == 88); - std::memcpy(subaccount, auth.subaccount.c_str(), auth.subaccount.size() + 1); - std::memcpy(subaccount_sig, auth.subaccount_sig.c_str(), auth.subaccount_sig.size() + 1); - std::memcpy(signature, auth.signature.c_str(), auth.signature.size() + 1); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + auto auth = unbox(conf).swarm_subaccount_sign( + ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); + assert(auth.subaccount.size() == 48); + assert(auth.subaccount_sig.size() == 88); + assert(auth.signature.size() == 88); + std::memcpy(subaccount, auth.subaccount.c_str(), auth.subaccount.size() + 1); + std::memcpy( + subaccount_sig, + auth.subaccount_sig.c_str(), + auth.subaccount_sig.size() + 1); + std::memcpy(signature, auth.signature.c_str(), auth.signature.size() + 1); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( @@ -1705,17 +1730,20 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( unsigned char* subaccount_sig, unsigned char* signature) { assert(msg && signing_value && subaccount && subaccount_sig && signature); - return wrap_exceptions(conf, [&]{ - auto auth = unbox(conf).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); - assert(auth.signature.size() == 64); - std::memcpy(subaccount, auth.subaccount.data(), 36); - std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 64); - std::memcpy(signature, auth.signature.data(), 64); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + auto auth = unbox(conf).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); + assert(auth.signature.size() == 64); + std::memcpy(subaccount, auth.subaccount.data(), 36); + std::memcpy(subaccount_sig, auth.subaccount_sig.data(), 64); + std::memcpy(signature, auth.signature.data(), 64); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( @@ -1724,12 +1752,15 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( bool write, bool del, unsigned char* token) { - return wrap_exceptions(conf, [&]{ - auto tok = unbox(conf).swarm_subaccount_token(session_id, write, del); - assert(tok.size() == 36); - std::memcpy(token, tok.data(), 36); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + auto tok = unbox(conf).swarm_subaccount_token(session_id, write, del); + assert(tok.size() == 36); + std::memcpy(token, tok.data(), 36); + return true; + }, + false); } LIBSESSION_C_API bool groups_keys_swarm_subaccount_token( diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 5ee7aec3..bce1ad05 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -191,28 +191,32 @@ LIBSESSION_C_API int groups_members_init( LIBSESSION_C_API bool groups_members_get( config_object* conf, config_group_member* member, const char* session_id) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get(session_id)) { - c->into(*member); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get(session_id)) { + c->into(*member); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool groups_members_get_or_construct( config_object* conf, config_group_member* member, const char* session_id) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct(session_id).into(*member); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct(session_id).into(*member); + return true; + }, + false); } LIBSESSION_C_API void groups_members_set(config_object* conf, const config_group_member* member) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->set(groups::member{*member}); - }); - + return wrap_exceptions( + conf, [&] { unbox(conf)->set(groups::member{*member}); }); } LIBSESSION_C_API bool groups_members_erase(config_object* conf, const char* session_id) { diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index eb3e5875..3c5044b1 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -623,13 +623,16 @@ int user_groups_init( LIBSESSION_C_API bool user_groups_get_community( config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) { - return wrap_exceptions(conf, [&]{ - if (auto c = unbox(conf)->get_community(base_url, room)) { - c->into(*comm); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_community(base_url, room)) { + c->into(*comm); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool user_groups_get_or_construct_community( config_object* conf, @@ -637,29 +640,38 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( const char* base_url, const char* room, unsigned const char* pubkey) { - return wrap_exceptions(conf, [&]{ - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) - .into(*comm); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf) + ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + .into(*comm); + return true; + }, + false); } LIBSESSION_C_API bool user_groups_get_group( config_object* conf, ugroups_group_info* group, const char* group_id) { - return wrap_exceptions(conf, [&]{ - if (auto g = unbox(conf)->get_group(group_id)) { - g->into(*group); - return true; - } - return false; - }, false); + return wrap_exceptions( + conf, + [&] { + if (auto g = unbox(conf)->get_group(group_id)) { + g->into(*group); + return true; + } + return false; + }, + false); } LIBSESSION_C_API bool user_groups_get_or_construct_group( config_object* conf, ugroups_group_info* group, const char* group_id) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->get_or_construct_group(group_id).into(*group); - return true; - }, false); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct_group(group_id).into(*group); + return true; + }, + false); } LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { @@ -671,7 +683,7 @@ LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_legacy_group( config_object* conf, const char* id) { - return wrap_exceptions(conf, [&]{ + return wrap_exceptions(conf, [&] { auto group = std::make_unique(); group->_internal = nullptr; if (auto c = unbox(conf)->get_legacy_group(id)) { @@ -684,7 +696,7 @@ LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_legacy_group( LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( config_object* conf, const char* id) { - return wrap_exceptions(conf, [&]{ + return wrap_exceptions(conf, [&] { auto group = std::make_unique(); group->_internal = nullptr; unbox(conf)->get_or_construct_legacy_group(id).into(*group); @@ -697,21 +709,16 @@ LIBSESSION_C_API void user_groups_set_community( unbox(conf)->set(community_info{*comm}); } LIBSESSION_C_API void user_groups_set_group(config_object* conf, const ugroups_group_info* group) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(group_info{*group}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set(group_info{*group}); }); } LIBSESSION_C_API void user_groups_set_legacy_group( config_object* conf, const ugroups_legacy_group_info* group) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(legacy_group_info{*group}); - }); + wrap_exceptions(conf, [&] { unbox(conf)->set(legacy_group_info{*group}); }); } LIBSESSION_C_API void user_groups_set_free_legacy_group( config_object* conf, ugroups_legacy_group_info* group) { - wrap_exceptions(conf, [&]{ - unbox(conf)->set(legacy_group_info{std::move(*group)}); - }); + wrap_exceptions( + conf, [&] { unbox(conf)->set(legacy_group_info{std::move(*group)}); }); } LIBSESSION_C_API bool user_groups_erase_community( diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 29a522af..4a6145a2 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -45,10 +45,13 @@ 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) { - return wrap_exceptions(conf, [&]{ - unbox(conf)->set_name(name); - return 0; - }, static_cast(SESSION_ERR_BAD_VALUE)); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_name(name); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); } profile_pic UserProfile::get_profile_pic() const { @@ -85,10 +88,13 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic if (!url.empty()) key = {pic.key, 32}; - return wrap_exceptions(conf, [&]{ - unbox(conf)->set_profile_pic(url, key); - return 0; - }, static_cast(SESSION_ERR_BAD_VALUE)); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_profile_pic(url, key); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); } void UserProfile::set_nts_priority(int priority) { diff --git a/src/network.cpp b/src/network.cpp index ad24061d..cad89b48 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -37,6 +37,10 @@ namespace { inline auto cat = log::Cat("network"); + class load_cache_exception : public std::runtime_error { + public: + load_cache_exception(std::string message) : std::runtime_error(message) {} + }; class status_code_exception : public std::runtime_error { public: int16_t status_code; @@ -54,15 +58,15 @@ namespace { // The smallest size the snode pool can get to before we need to fetch more. constexpr uint16_t min_snode_pool_count = 12; - // The number of paths we want to maintain. - constexpr uint8_t target_path_count = 2; - // The number of snodes (including the guard snode) in a path. constexpr uint8_t path_size = 3; // The number of times a path can fail before it's replaced. constexpr uint16_t path_failure_threshold = 3; + // The number of times a path can timeout before it's replaced. + constexpr uint16_t path_timeout_threshold = 10; + // The number of times a snode can fail before it's replaced. constexpr uint16_t snode_failure_threshold = 3; @@ -71,9 +75,27 @@ namespace { file_snode_pool_updated{u8"snode_pool_updated"}, swarm_dir{u8"swarm"}, default_cache_path{u8"."}; - constexpr auto node_not_found_prefix = "Next node not found: "sv; + constexpr auto node_not_found_prefix = "502 Bad Gateway\n\nNext node not found: "sv; + constexpr auto node_not_found_prefix_no_status = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; + std::string path_type_name(PathType path_type) { + switch (path_type) { + case PathType::standard: return "standard"; + case PathType::upload: return "upload"; + case PathType::download: return "download"; + } + } + + // The number of paths we want to maintain. + uint8_t target_path_count(PathType path_type) { + switch (path_type) { + case PathType::standard: return 2; + case PathType::upload: return 1; + case PathType::download: return 1; + } + } + service_node node_from_json(nlohmann::json json) { auto pk_ed = json["pubkey_ed25519"].get(); if (pk_ed.size() != 64 || !oxenc::is_hex(pk_ed)) @@ -130,6 +152,9 @@ namespace { node_from_disk("104.194.8.115|20204|" "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0|0"sv) .first}; + constexpr auto file_server = "filev2.getsession.org"sv; + constexpr auto file_server_pubkey = + "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"sv; /// rng type that uses llarp::randint(), which is cryptographically secure struct CSRNG { @@ -237,6 +262,7 @@ Network::Network(std::optional cache_path, bool use_testnet, bool pre_ std::thread build_paths_thread( &Network::with_paths_and_pool, this, + PathType::standard, std::nullopt, [](std::vector, std::vector, std::optional) { }); @@ -346,15 +372,18 @@ void Network::load_cache_from_disk() { checked_swarm_expiration = true; if (swarm_lifetime < swarm_cache_expiration_duration) - throw std::runtime_error{"Expired swarm cache."}; + throw load_cache_exception{"Expired swarm cache."}; } // Otherwise try to parse as a node nodes.push_back(node_from_disk(line).first); } catch (const std::exception& e) { - log::warning( - cat, "Skipping invalid or expired entry in swarm cache: {}", e.what()); + // Don't bother logging for expired entries (we include the count separately at + // the end) + if (dynamic_cast(&e) == nullptr) { + log::warning(cat, "Skipping invalid entry in swarm cache: {}", e.what()); + } // The cache is invalid, we should remove it if (!checked_swarm_expiration) { @@ -379,9 +408,10 @@ void Network::load_cache_from_disk() { log::info( cat, - "Loaded cache of {} snodes, {} swarms.", + "Loaded cache of {} snodes, {} swarms ({} expired swarms).", snode_pool.size(), - swarm_cache.size()); + swarm_cache.size(), + caches_to_remove.size()); } catch (const std::exception& e) { log::error(cat, "Failed to load snode cache, will rebuild ({}).", e.what()); fs::remove_all(cache_path); @@ -481,9 +511,11 @@ void Network::close_connections() { net.call([this]() mutable { endpoint.reset(); - for (auto& path : paths) { - path.conn_info.conn.reset(); - path.conn_info.stream.reset(); + for (auto& paths : {&standard_paths, &upload_paths, &download_paths}) { + for (auto& path : *paths) { + path.conn_info.conn.reset(); + path.conn_info.stream.reset(); + } } update_status(ConnectionStatus::disconnected); @@ -531,7 +563,7 @@ std::shared_ptr Network::get_endpoint() { }); } -connection_info Network::get_connection_info(service_node target) { +connection_info Network::get_connection_info(PathType path_type, service_node target) { auto cb_called = std::make_shared(); auto mutex = std::make_shared(); auto cv = std::make_shared(); @@ -546,30 +578,34 @@ connection_info Network::get_connection_info(service_node target) { creds, quic::opt::keep_alive{10s}, [mutex, cv, connection_established, done, cb_called](quic::connection_interface&) { - std::call_once(*cb_called, [&]() { - { - std::lock_guard lock(*mutex); - *connection_established = true; - *done = true; - } - cv->notify_one(); - }); + if (cb_called) + std::call_once(*cb_called, [&]() { + { + std::lock_guard lock(*mutex); + *connection_established = true; + *done = true; + } + cv->notify_one(); + }); }, - [this, target, mutex, cv, done, cb_called]( + [this, path_type, target, mutex, cv, done, cb_called]( quic::connection_interface& conn, uint64_t) { // Trigger the callback first before updating the paths in case this was triggered // when try to establish a connection - std::call_once(*cb_called, [&]() { - { - std::lock_guard lock(*mutex); - *done = true; - } - cv->notify_one(); - }); + if (cb_called) { + std::call_once(*cb_called, [&]() { + { + std::lock_guard lock(*mutex); + *done = true; + } + cv->notify_one(); + }); + } // When the connection is closed, update the path and connection status - auto current_paths = - net.call_get([this]() -> std::vector { return paths; }); + auto current_paths = net.call_get([this, path_type]() -> std::vector { + return paths_for_type(path_type); + }); auto target_path = std::find_if( current_paths.begin(), current_paths.end(), [&target](const auto& path) { return !path.nodes.empty() && target == path.nodes.front(); @@ -580,7 +616,15 @@ connection_info Network::get_connection_info(service_node target) { target_path->conn_info.conn.reset(); target_path->conn_info.stream.reset(); handle_errors( - {target, "", std::nullopt, std::nullopt, *target_path, 0ms, false, false}, + {target, + "", + std::nullopt, + std::nullopt, + *target_path, + path_type, + 0ms, + false, + false}, false, std::nullopt, std::nullopt, @@ -607,40 +651,43 @@ using paths_and_pool_info = std::tuple< std::chrono::system_clock::time_point>; void Network::with_paths_and_pool( + PathType path_type, std::optional excluded_node, std::function< void(std::vector updated_paths, std::vector pool, std::optional error)> callback) { - auto [current_paths, pool, last_pool_update] = net.call_get([this]() -> paths_and_pool_info { - return {paths, snode_pool, last_snode_pool_update}; - }); + auto [current_paths, pool, last_pool_update] = + net.call_get([this, path_type]() -> paths_and_pool_info { + return {paths_for_type(path_type), snode_pool, last_snode_pool_update}; + }); // Check if the current data is valid, and if so just return it auto current_valid_paths = valid_paths(current_paths); auto [paths_valid, pool_valid] = - validate_paths_and_pool_sizes(current_valid_paths, pool, last_pool_update); + validate_paths_and_pool_sizes(path_type, current_valid_paths, pool, last_pool_update); if (paths_valid && pool_valid) return callback(current_valid_paths, pool, std::nullopt); - auto [updated_paths, updated_pool, error] = - paths_and_pool_loop->call_get([this, excluded_node]() mutable -> paths_and_pool_result { + auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get( + [this, path_type, excluded_node]() mutable -> paths_and_pool_result { auto [current_paths, pool, last_pool_update] = - net.call_get([this]() -> paths_and_pool_info { - return {paths, snode_pool, last_snode_pool_update}; + net.call_get([this, path_type]() -> paths_and_pool_info { + return {paths_for_type(path_type), snode_pool, last_snode_pool_update}; }); // Check if the current data is valid, and if so just return it auto current_valid_paths = valid_paths(current_paths); - auto [paths_valid, pool_valid] = - validate_paths_and_pool_sizes(current_valid_paths, pool, last_pool_update); + auto [paths_valid, pool_valid] = validate_paths_and_pool_sizes( + path_type, current_valid_paths, pool, last_pool_update); if (paths_valid && pool_valid) return {current_valid_paths, pool, std::nullopt}; // Update the network status - net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + if (path_type == PathType::standard) + net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); // If the pool isn't valid then we should update it CSRNG rng; @@ -774,7 +821,7 @@ void Network::with_paths_and_pool( if (!paths_valid) { try { // Get the possible guard nodes - log::info(cat, "Building paths."); + log::info(cat, "Building paths for {}.", path_type_name(path_type)); std::vector nodes_to_exclude; std::vector possible_guard_nodes; @@ -809,7 +856,8 @@ void Network::with_paths_and_pool( // Split the possible nodes list into a list of lists (one list could run // out before the other but in most cases this should work fine) - size_t required_paths = (target_path_count - current_valid_paths.size()); + size_t required_paths = + (target_path_count(path_type) - current_valid_paths.size()); size_t chunk_size = (possible_guard_nodes.size() / required_paths); std::vector> nodes_to_test; auto start = 0; @@ -842,6 +890,7 @@ void Network::with_paths_and_pool( std::move(guard_node_prom)); find_valid_guard_node_recursive( + path_type, nodes_to_test[i], [prom](std::optional valid_guard_node, std::vector unused_nodes) { @@ -873,9 +922,10 @@ void Network::with_paths_and_pool( // Make sure we ended up getting enough valid nodes auto have_enough_guard_nodes = (current_valid_paths.size() + valid_nodes.size() >= - target_path_count); + target_path_count(path_type)); auto have_enough_unused_nodes = - (unused_nodes.size() >= ((path_size - 1) * target_path_count)); + (unused_nodes.size() >= + ((path_size - 1) * target_path_count(path_type))); if (!have_enough_guard_nodes || !have_enough_unused_nodes) throw std::runtime_error{"Not enough remaining nodes."}; @@ -890,7 +940,7 @@ void Network::with_paths_and_pool( path.push_back(node); } - paths_result.emplace_back(onion_path{std::move(info), path, 0}); + paths_result.emplace_back(onion_path{std::move(info), path, 0, 0}); // Log that a path was built std::vector node_descriptions; @@ -900,21 +950,38 @@ void Network::with_paths_and_pool( std::back_inserter(node_descriptions), [](service_node& node) { return node.to_string(); }); auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info(cat, "Built new onion request path: [{}]", path_description); + log::info( + cat, + "Built new onion request path for {}: [{}]", + path_type_name(path_type), + path_description); } } catch (const std::exception& e) { - log::info(cat, "Unable to build paths due to error: {}", e.what()); + log::info( + cat, + "Unable to build paths for {} due to error: {}", + path_type_name(path_type), + e.what()); return {{}, {}, e.what()}; } } // Store to instance variables - net.call([this, pool_result, paths_result, pool_valid, paths_valid]() mutable { + net.call([this, + path_type, + pool_result, + paths_result, + pool_valid, + paths_valid]() mutable { if (!paths_valid) { - paths = paths_result; + switch (path_type) { + case PathType::standard: standard_paths = paths_result; break; + case PathType::upload: upload_paths = paths_result; break; + case PathType::download: download_paths = paths_result; break; + } // Call the paths_changed callback if provided - if (paths_changed) { + if (path_type == PathType::standard && paths_changed) { std::vector> raw_paths; for (auto& path : paths_result) raw_paths.emplace_back(path.nodes); @@ -935,8 +1002,9 @@ void Network::with_paths_and_pool( snode_cache_cv.notify_one(); } - // Paths were successfully built, update the connection status - update_status(ConnectionStatus::connected); + // Standard paths were successfully built, update the connection status + if (path_type == PathType::standard) + update_status(ConnectionStatus::connected); }); return {paths_result, pool_result, std::nullopt}; @@ -957,6 +1025,7 @@ std::vector Network::valid_paths(std::vector paths) { } std::pair Network::validate_paths_and_pool_sizes( + PathType path_type, std::vector paths, std::vector pool, std::chrono::system_clock::time_point last_pool_update) { @@ -965,15 +1034,17 @@ std::pair Network::validate_paths_and_pool_sizes( auto cache_has_expired = (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); - return {(paths.size() >= target_path_count), + return {(paths.size() >= target_path_count(path_type)), (pool.size() >= min_snode_pool_count && !cache_has_expired)}; } void Network::with_path( + PathType path_type, std::optional excluded_node, std::function path, std::optional error)> callback) { - auto current_paths = net.call_get([this]() -> std::vector { return paths; }); + auto current_paths = net.call_get( + [this, path_type]() -> std::vector { return paths_for_type(path_type); }); std::pair, uint8_t> path_info = find_possible_path(excluded_node, current_paths); auto& [target_path, paths_count] = path_info; @@ -983,11 +1054,14 @@ void Network::with_path( if (target_path && !target_path->conn_info.is_valid()) { path_info = paths_and_pool_loop->call_get( [this, + path_type, path = *target_path]() mutable -> std::pair, uint8_t> { // Since this may have been blocked by another thread we should start by // making sure the target path is still one of the current paths auto current_paths = - net.call_get([this]() -> std::vector { return paths; }); + net.call_get([this, path_type]() -> std::vector { + return paths_for_type(path_type); + }); auto target_path_it = std::find(current_paths.begin(), current_paths.end(), path); @@ -996,7 +1070,11 @@ void Network::with_path( return {std::nullopt, current_paths.size()}; // Try to retrieve a valid connection for the guard node - auto info = get_connection_info(path.nodes[0]); + log::info( + cat, + "Connection to {} path no longer valid, attempting reconnection.", + path_type_name(path_type)); + auto info = get_connection_info(path_type, path.nodes[0]); // It's possible that the connection was created successfully, and reported as // valid, but isn't actually valid (eg. it was shutdown immediately due to the @@ -1005,19 +1083,41 @@ void Network::with_path( if (!info.is_valid()) return {std::nullopt, current_paths.size()}; - // If the connection info is valid update the connection status back to - // connected - update_status(ConnectionStatus::connected); + // If the connection info is valid and it's a standard path then update the + // connection status back to connected + if (path_type == PathType::standard) + update_status(ConnectionStatus::connected); // No need to call the 'paths_changed' callback as the paths haven't // actually changed, just their connection info - auto updated_path = onion_path{std::move(info), std::move(path.nodes), 0}; - auto paths_count = - net.call_get([this, path, updated_path]() mutable -> uint8_t { - paths.erase( - std::remove(paths.begin(), paths.end(), path), paths.end()); - paths.emplace_back(updated_path); - return paths.size(); + auto updated_path = onion_path{std::move(info), std::move(path.nodes), 0, 0}; + auto paths_count = net.call_get( + [this, path_type, path, updated_path]() mutable -> uint8_t { + switch (path_type) { + case PathType::standard: + std::replace( + standard_paths.begin(), + standard_paths.end(), + path, + updated_path); + return standard_paths.size(); + + case PathType::upload: + std::replace( + upload_paths.begin(), + upload_paths.end(), + path, + updated_path); + return upload_paths.size(); + + case PathType::download: + std::replace( + download_paths.begin(), + download_paths.end(), + path, + updated_path); + return download_paths.size(); + } }); return {updated_path, paths_count}; @@ -1027,6 +1127,7 @@ void Network::with_path( // If we didn't get a target path then we have to build paths if (!target_path) return with_paths_and_pool( + path_type, excluded_node, [this, excluded_node, cb = std::move(callback)]( std::vector updated_paths, @@ -1045,10 +1146,11 @@ void Network::with_path( }); // Build additional paths in the background if we don't have enough - if (paths_count < target_path_count) { + if (paths_count < target_path_count(path_type)) { std::thread build_additional_paths_thread( &Network::with_paths_and_pool, this, + path_type, std::nullopt, [](std::optional>, std::vector, @@ -1056,9 +1158,11 @@ void Network::with_path( build_additional_paths_thread.detach(); } - // We have a valid path, update the status in case we had flagged it as disconnected for - // some reason - net.call([this]() mutable { update_status(ConnectionStatus::connected); }); + // We have a valid path for the standard path type then update the status in case we had + // flagged it as disconnected for some reason + if (path_type == PathType::standard) + net.call([this]() mutable { update_status(ConnectionStatus::connected); }); + callback(target_path, std::nullopt); } @@ -1068,16 +1172,20 @@ std::pair, uint8_t> Network::find_possible_path( return {std::nullopt, paths.size()}; std::vector possible_paths; - std::copy_if( - paths.begin(), - paths.end(), - std::back_inserter(possible_paths), - [&excluded_node](const auto& path) { - return !path.nodes.empty() && - (!excluded_node || - std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == - path.nodes.end()); - }); + + if (!excluded_node) + possible_paths = paths; + else + std::copy_if( + paths.begin(), + paths.end(), + std::back_inserter(possible_paths), + [&excluded_node](const auto& path) { + return !path.nodes.empty() && + (!excluded_node || + std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == + path.nodes.end()); + }); if (possible_paths.empty()) return {std::nullopt, paths.size()}; @@ -1116,6 +1224,7 @@ void Network::get_service_nodes_recursive( } void Network::find_valid_guard_node_recursive( + PathType path_type, std::vector target_nodes, std::function< void(std::optional valid_guard_node, @@ -1127,9 +1236,10 @@ void Network::find_valid_guard_node_recursive( log::info(cat, "Testing guard snode: {}", target_node.to_string()); get_version( + path_type, target_node, 3s, - [this, target_node, target_nodes, cb = std::move(callback)]( + [this, path_type, target_node, target_nodes, cb = std::move(callback)]( std::vector version, connection_info info, std::optional error) { @@ -1157,10 +1267,11 @@ void Network::find_valid_guard_node_recursive( "Testing {} failed with error: {}", target_node.to_string(), e.what()); - std::thread retry_thread([this, remaining_nodes, cb = std::move(cb)] { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - find_valid_guard_node_recursive(remaining_nodes, cb); - }); + std::thread retry_thread( + [this, path_type, remaining_nodes, cb = std::move(cb)] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + find_valid_guard_node_recursive(path_type, remaining_nodes, cb); + }); retry_thread.detach(); } }); @@ -1173,7 +1284,7 @@ void Network::get_service_nodes( std::optional limit, std::function nodes, std::optional error)> callback) { - auto info = get_connection_info(node); + auto info = get_connection_info(PathType::standard, node); if (!info.is_valid()) return callback({}, "Network is unreachable."); @@ -1224,12 +1335,13 @@ void Network::get_service_nodes( } void Network::get_version( + PathType path_type, service_node node, std::optional timeout, std::function version, connection_info info, std::optional error)> callback) { - auto info = get_connection_info(node); + auto info = get_connection_info(path_type, node); if (!info.is_valid()) return callback({}, info, "Network is unreachable."); @@ -1278,6 +1390,7 @@ void Network::get_swarm( // Pick a random node from the snode pool to fetch the swarm from with_paths_and_pool( + PathType::standard, std::nullopt, [this, swarm_pubkey, cb = std::move(callback)]( std::vector, @@ -1298,6 +1411,7 @@ void Network::get_swarm( }; send_onion_request( + PathType::standard, node, ustring{quic::to_usv(payload.dump())}, swarm_pubkey, @@ -1358,6 +1472,7 @@ void Network::set_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { with_paths_and_pool( + PathType::standard, std::nullopt, [count, cb = std::move(callback)]( std::vector, @@ -1404,6 +1519,7 @@ void Network::send_request( } void Network::send_onion_request( + PathType path_type, network_destination destination, std::optional body, std::optional swarm_pubkey, @@ -1411,9 +1527,11 @@ void Network::send_onion_request( bool is_retry, network_response_callback_t handle_response) { with_path( + path_type, node_for_destination(destination), [this, - destination, + path_type, + destination = std::move(destination), body, swarm_pubkey, timeout, @@ -1443,6 +1561,7 @@ void Network::send_onion_request( onion_req_payload, swarm_pubkey, *path, + path_type, timeout, node_for_destination(destination).has_value(), is_retry}; @@ -1482,6 +1601,111 @@ void Network::send_onion_request( }); } +void Network::send_onion_request( + network_destination destination, + std::optional body, + std::optional swarm_pubkey, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response) { + send_onion_request( + PathType::standard, destination, body, swarm_pubkey, timeout, false, handle_response); +} + +void Network::upload_file_to_server( + ustring data, + onionreq::ServerDestination server, + std::optional file_name, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response) { + std::vector> headers; + std::unordered_set existing_keys; + + if (server.headers) + for (auto& [key, value] : *server.headers) { + headers.emplace_back(key, value); + existing_keys.insert(key); + } + + // Add the required headers if they weren't provided + if (existing_keys.find("Content-Disposition") == existing_keys.end()) + headers.emplace_back( + "Content-Disposition", + (file_name ? "attachment; filename=\"{}\""_format(*file_name) : "attachment")); + + if (existing_keys.find("Content-Type") == existing_keys.end()) + headers.emplace_back("Content-Type", "application/octet-stream"); + + send_onion_request( + PathType::upload, + ServerDestination{ + server.protocol, + server.host, + server.endpoint, + server.x25519_pubkey, + server.port, + headers, + server.method}, + data, + std::nullopt, + timeout, + false, + handle_response); +} + +void Network::download_file( + std::string_view download_url, + session::onionreq::x25519_pubkey x25519_pubkey, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response) { + const auto& [proto, host, port, path] = parse_url(download_url); + + if (!path) + throw std::invalid_argument{"Invalid URL provided: Missing path"}; + + download_file( + ServerDestination{proto, host, *path, x25519_pubkey, port, std::nullopt, "GET"}, + timeout, + handle_response); +} + +void Network::download_file( + onionreq::ServerDestination server, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response) { + send_onion_request( + PathType::download, + server, + std::nullopt, + std::nullopt, + timeout, + false, + handle_response); +} + +void Network::get_client_version( + Platform platform, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response) { + std::string endpoint; + + switch (platform) { + case Platform::android: endpoint = "/session_version?platform=android"; break; + case Platform::desktop: endpoint = "/session_version?platform=desktop"; break; + case Platform::ios: endpoint = "/session_version?platform=ios"; break; + } + + auto pubkey = x25519_pubkey::from_hex(file_server_pubkey); + send_onion_request( + PathType::standard, + ServerDestination{ + "http", std::string(file_server), endpoint, pubkey, 80, std::nullopt, "GET"}, + std::nullopt, + pubkey, + timeout, + false, + handle_response); +} + // MARK: Response Handling // The SnodeDestination runs via V3 onion requests @@ -1643,44 +1867,41 @@ std::pair Network::validate_response(quic::message resp, void Network::handle_errors( request_info info, - bool timeout, + bool timeout_, std::optional status_code_, std::optional response, std::optional handle_response) { + bool timeout = timeout_; auto status_code = status_code_.value_or(-1); - // If we are making a proxied request to a server then it's possible that the server destination - // failed rather than nodes along the path, to avoid needlessly dropping paths we want to try to - // detect some of the more common cases and just handle the response without updating the - // path/snode state - if (!info.node_destination) { - // A timeout could be caused because the destination is unreachable rather than the the path - // (eg. if a user has an old SOGS which is no longer running on their device they will get a - // timeout) - if (timeout) { - if (handle_response) - return (*handle_response)(false, true, status_code, response); - return; - } + // A number of server errors can return HTML data but no status code, we want to extract those + // cases so they can be handled properly below + if (status_code == -1 && response) { + const std::unordered_map> response_map = { + {"500 Internal Server Error", {500, false}}, + {"502 Bad Gateway", {502, false}}, + {"503 Service Unavailable", {502, false}}, + {"504 Gateway Timeout", {504, true}}, + }; - // A number of server errors can return HTML data but no status code, this indicates that - // we managed to connect to the destination so try to intercept some of these cases - if (status_code == -1 && response) { - const std::unordered_map> response_map = { - {"500 Internal Server Error", {500, false}}, - {"504 Gateway Timeout", {504, true}}, - }; - - for (const auto& [prefix, result] : response_map) { - if (response->starts_with(prefix)) { - if (handle_response) - return (*handle_response)(false, result.second, result.first, response); - return; - } + for (const auto& [prefix, result] : response_map) { + if (response->starts_with(prefix)) { + status_code = result.first; + timeout = (timeout || result.second); } } } + // A timeout could be caused because the destination is unreachable rather than the the path + // (eg. if a user has an old SOGS which is no longer running on their device they will get a + // timeout) so if we timed out while sending a proxied request we assume something is wrong on + // the server side and don't update the path/snode state + if (!info.node_destination && timeout) { + if (handle_response) + return (*handle_response)(false, true, status_code, response); + return; + } + switch (status_code) { // A 404 or a 400 is likely due to a bad/missing SOGS or file so // shouldn't mark a path or snode as invalid @@ -1739,6 +1960,7 @@ void Network::handle_errors( throw std::invalid_argument{"No other nodes in the swarm."}; return send_onion_request( + info.path_type, *random_node, info.body, info.swarm_pubkey, @@ -1783,6 +2005,18 @@ void Network::handle_errors( // error break; + case 500: + case 504: + // If we are making a proxied request to a server then assume 500 errors are occurring + // on the server rather than in the service node network and don't update the path/snode + // state + if (!info.node_destination) { + if (handle_response) + return (*handle_response)(false, timeout, status_code, response); + return; + } + break; + default: break; } @@ -1792,12 +2026,20 @@ void Network::handle_errors( auto updated_path = info.path; bool found_invalid_node = false; - if (response && response->starts_with(node_not_found_prefix)) { - std::string_view ed25519PublicKey{response->data() + node_not_found_prefix.size()}; + if (response) { + std::optional ed25519PublicKey; - if (ed25519PublicKey.size() == 64 && oxenc::is_hex(ed25519PublicKey)) { + // Check if the response has one of the 'node_not_found' prefixes + if (response->starts_with(node_not_found_prefix)) + ed25519PublicKey = {response->data() + node_not_found_prefix.size()}; + else if (response->starts_with(node_not_found_prefix_no_status)) + ed25519PublicKey = {response->data() + node_not_found_prefix_no_status.size()}; + + // If we found a result then try to extract the pubkey and process it + if (ed25519PublicKey && ed25519PublicKey->size() == 64 && + oxenc::is_hex(*ed25519PublicKey)) { session::onionreq::ed25519_pubkey edpk = - session::onionreq::ed25519_pubkey::from_hex(ed25519PublicKey); + session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); auto edpk_view = to_unsigned_sv(edpk.view()); auto snode_it = std::find_if( @@ -1819,12 +2061,16 @@ void Network::handle_errors( // If we didn't find the specific node or the paths connection was closed then increment the // path failure count if (!found_invalid_node || !updated_path.conn_info.is_valid()) { - updated_path.failure_count += 1; + if (timeout) + updated_path.timeout_count += 1; + else + updated_path.failure_count += 1; - // If the path has failed too many times we want to drop the guard snode - // (marking it as invalid) and increment the failure count of each node in the - // path - if (updated_path.failure_count >= path_failure_threshold) { + // If the path has failed or timed out too many times we want to drop the guard + // snode (marking it as invalid) and increment the failure count of each node in + // the path) + if (updated_path.failure_count >= path_failure_threshold || + updated_path.timeout_count >= path_timeout_threshold) { for (auto& it : updated_path.nodes) { auto failure_count = updated_failure_counts.try_emplace(it.to_string(), 0).first->second; @@ -1836,18 +2082,51 @@ void Network::handle_errors( } } - // Update the cache + // Update the cache (want to wait until this has been completed incase) + std::condition_variable cv; + std::mutex mtx; + bool done = false; + net.call([this, + path_type = info.path_type, swarm_pubkey = info.swarm_pubkey, old_path = info.path, updated_failure_counts, - updated_path]() mutable { + updated_path, + &cv, + &mtx, + &done]() mutable { // Drop the path if invalid - if (updated_path.failure_count >= path_failure_threshold) { - auto old_paths_size = paths.size(); + if (updated_path.failure_count >= path_failure_threshold || + updated_path.timeout_count >= path_timeout_threshold) { + auto old_paths_size = paths_for_type(path_type).size(); + + // Close the connection immediately (just in case there are other requests happening) + if (old_path.conn_info.conn) + old_path.conn_info.conn->close_connection(); + old_path.conn_info.conn.reset(); old_path.conn_info.stream.reset(); - paths.erase(std::remove(paths.begin(), paths.end(), old_path), paths.end()); + + switch (path_type) { + case PathType::standard: + standard_paths.erase( + std::remove(standard_paths.begin(), standard_paths.end(), old_path), + standard_paths.end()); + break; + + case PathType::upload: + upload_paths.erase( + std::remove(upload_paths.begin(), upload_paths.end(), old_path), + upload_paths.end()); + break; + + case PathType::download: + download_paths.erase( + std::remove(download_paths.begin(), download_paths.end(), old_path), + download_paths.end()); + break; + } std::vector node_descriptions; std::transform( @@ -1856,22 +2135,58 @@ void Network::handle_errors( std::back_inserter(node_descriptions), [](service_node& node) { return node.to_string(); }); auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - - if (paths.size() != old_paths_size) - log::info(cat, "Dropping path: [{}]", path_description); + auto new_paths_size = paths_for_type(path_type).size(); + + if (new_paths_size != old_paths_size) + log::info( + cat, + "Dropping path for {}: [{}]", + path_type_name(path_type), + path_description); else { // If the path was already dropped then the snode pool would have already been // updated so no need to continue - log::info(cat, "Path already dropped: [{}]", path_description); + log::info( + cat, + "Path already dropped for {}: [{}]", + path_type_name(path_type), + path_description); return; } - } else - std::replace(paths.begin(), paths.end(), old_path, updated_path); + } else { + switch (path_type) { + case PathType::standard: + std::replace( + standard_paths.begin(), standard_paths.end(), old_path, updated_path); + break; + + case PathType::upload: + std::replace(upload_paths.begin(), upload_paths.end(), old_path, updated_path); + break; - // Update the network status if we've removed all paths - if (paths.empty()) + case PathType::download: + std::replace( + download_paths.begin(), download_paths.end(), old_path, updated_path); + break; + } + } + + // Update the snode failure counts + snode_failure_counts = updated_failure_counts; + + // Update the network status if we've removed all standard paths + if (standard_paths.empty()) update_status(ConnectionStatus::disconnected); + // Since we've finished updating the path and failure count states we can stop blocking + // the caller (no need to wait for the snode cache to update) + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); + + // Update the snode cache { std::lock_guard lock{snode_cache_mutex}; @@ -1900,7 +2215,6 @@ void Network::handle_errors( old_path.nodes[i], updated_path.nodes[i]); - snode_failure_counts = updated_failure_counts; need_pool_write = true; need_swarm_write = (swarm_pubkey && swarm_cache.contains(swarm_pubkey->hex())); need_write = true; @@ -1908,40 +2222,16 @@ void Network::handle_errors( snode_cache_cv.notify_one(); }); + // Wait for the failure states to complete updating before triggering the callback + { + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return done; }); + } + if (handle_response) (*handle_response)(false, false, status_code, response); } -uint8_t Network::get_failure_count(service_node node) { - return net.call_get([this, node]() -> uint8_t { - return snode_failure_counts.try_emplace(node.to_string(), 0).first->second; - }); -} - -void Network::set_failure_count(service_node node, uint8_t failure_count) { - net.call([this, node, failure_count]() mutable { - snode_failure_counts[node.to_string()] = failure_count; - }); -} - -void Network::set_paths(std::vector paths_) { - net.call([this, paths_]() mutable { paths = paths_; }); -} - -uint8_t Network::get_failure_count(onion_path path) { - auto current_paths = net.call_get([this]() -> std::vector { return paths; }); - - auto target_path = - std::find_if(current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { - return path_it.nodes[0] == path.nodes[0]; - }); - - if (target_path != current_paths.end()) - return target_path->failure_count; - - return 0; -} - std::vector convert_service_nodes( std::vector nodes) { std::vector converted_nodes; @@ -2097,13 +2387,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( size_t body_size, const char* swarm_pubkey_hex, int64_t timeout_ms, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - void*), + network_onion_response_callback_t callback, void* ctx) { assert(callback); @@ -2127,7 +2411,6 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( body, swarm_pubkey, std::chrono::milliseconds{timeout_ms}, - false, [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2154,13 +2437,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( const unsigned char* body_, size_t body_size, int64_t timeout_ms, - void (*callback)( - bool success, - bool timeout, - int16_t status_code, - const char* response, - size_t response_size, - void*), + network_onion_response_callback_t callback, void* ctx) { assert(server.method && server.protocol && server.host && server.endpoint && server.x25519_pubkey && callback); @@ -2190,7 +2467,135 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( body, std::nullopt, std::chrono::milliseconds{timeout_ms}, - false, + [cb = std::move(callback), ctx]( + bool success, + bool timeout, + int status_code, + std::optional response) { + if (response) + cb(success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); + else + cb(success, timeout, status_code, nullptr, 0, ctx); + }); + } catch (const std::exception& e) { + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + } +} + +LIBSESSION_C_API void network_upload_to_server( + network_object* network, + const network_server_destination server, + const unsigned char* data, + size_t data_len, + const char* file_name_, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx) { + assert(data && server.method && server.protocol && server.host && server.endpoint && + server.x25519_pubkey && callback); + + try { + std::optional>> headers; + if (server.headers_size > 0) { + headers = std::vector>{}; + + for (size_t i = 0; i < server.headers_size; i++) + headers->emplace_back(server.headers[i], server.header_values[i]); + } + + std::optional file_name; + if (file_name_) + file_name = file_name_; + + unbox(network).upload_file_to_server( + {data, data_len}, + ServerDestination{ + server.protocol, + server.host, + server.endpoint, + x25519_pubkey::from_hex({server.x25519_pubkey, 64}), + server.port, + headers, + server.method}, + file_name, + std::chrono::milliseconds{timeout_ms}, + [cb = std::move(callback), ctx]( + bool success, + bool timeout, + int status_code, + std::optional response) { + if (response) + cb(success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); + else + cb(success, timeout, status_code, nullptr, 0, ctx); + }); + } catch (const std::exception& e) { + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + } +} + +LIBSESSION_C_API void network_download_from_server( + network_object* network, + const network_server_destination server, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx) { + assert(server.method && server.protocol && server.host && server.endpoint && + server.x25519_pubkey && callback); + + try { + unbox(network).download_file( + ServerDestination{ + server.protocol, + server.host, + server.endpoint, + x25519_pubkey::from_hex({server.x25519_pubkey, 64}), + server.port, + std::nullopt, + server.method}, + std::chrono::milliseconds{timeout_ms}, + [cb = std::move(callback), ctx]( + bool success, + bool timeout, + int status_code, + std::optional response) { + if (response) + cb(success, + timeout, + status_code, + (*response).c_str(), + (*response).size(), + ctx); + else + cb(success, timeout, status_code, nullptr, 0, ctx); + }); + } catch (const std::exception& e) { + callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + } +} + +LIBSESSION_C_API void network_get_client_version( + network_object* network, + CLIENT_PLATFORM platform, + int64_t timeout_ms, + network_onion_response_callback_t callback, + void* ctx) { + assert(platform && callback); + + try { + unbox(network).get_client_version( + static_cast(platform), + std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, bool timeout, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 4a403b1f..0fde1f5b 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -59,9 +59,15 @@ void Builder::set_destination(network_destination destination) { else if (auto* dest = std::get_if(&destination)) { host_.emplace(dest->host); endpoint_.emplace(dest->endpoint); - protocol_.emplace(dest->protocol); method_.emplace(dest->method); + // Remove the '://' from the protocol if it was given + size_t pos = dest->protocol.find("://"); + if (pos != std::string::npos) + protocol_.emplace(dest->protocol.substr(0, pos)); + else + protocol_.emplace(dest->protocol); + if (dest->port) port_.emplace(*dest->port); diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index eda4aea7..a6104a0c 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -36,12 +36,17 @@ ustring ResponseParser::decrypt(ustring ciphertext) const { try { return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); } catch (const std::exception& e) { - if (enc_type_ == session::onionreq::EncryptType::xchacha20) - return d.decrypt( - session::onionreq::EncryptType::aes_gcm, - ciphertext, - destination_x25519_public_key_); - else + if (enc_type_ == session::onionreq::EncryptType::xchacha20) { + try { + return d.decrypt( + session::onionreq::EncryptType::aes_gcm, + ciphertext, + destination_x25519_public_key_); + } catch (...) { + throw std::runtime_error{ + "Decryption failed (XChaCha20-Poly1305, falling back to AES256-GCM)"}; + } + } else throw; } } diff --git a/src/util.cpp b/src/util.cpp index 79448099..13cd66cb 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -25,4 +25,65 @@ std::vector split(std::string_view str, const std::string_view return results; } +std::tuple, std::optional> parse_url( + std::string_view url) { + std::tuple, std::optional> + result{}; + auto& [proto, host, port, path] = result; + if (auto pos = url.find("://"); pos != std::string::npos) { + auto proto_name = url.substr(0, pos); + url.remove_prefix(proto_name.size() + 3); + if (string_iequal(proto_name, "http")) + proto = "http://"; + else if (string_iequal(proto_name, "https")) + proto = "https://"; + } + if (proto.empty()) + throw std::invalid_argument{"Invalid URL: invalid/missing protocol://"}; + + bool next_allow_dot = false; + bool has_dot = false; + while (!url.empty()) { + auto c = url.front(); + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-') { + host += c; + next_allow_dot = true; + } else if (c >= 'A' && c <= 'Z') { + host += c + ('a' - 'A'); + next_allow_dot = true; + } else if (next_allow_dot && c == '.') { + host += '.'; + has_dot = true; + next_allow_dot = false; + } else { + break; + } + url.remove_prefix(1); + } + if (host.size() < 4 || !has_dot || host.back() == '.') + throw std::invalid_argument{"Invalid URL: invalid hostname"}; + + if (!url.empty() && url.front() == ':') { + url.remove_prefix(1); + uint16_t target_port; + if (auto [p, ec] = std::from_chars(url.data(), url.data() + url.size(), target_port); + ec == std::errc{}) + url.remove_prefix(p - url.data()); + else + throw std::invalid_argument{"Invalid URL: invalid port"}; + if (!(target_port == 80 && proto == "http://") && !(target_port == 443 && proto == "https:/" + "/")) + port = target_port; + } + + if (url.size() > 1 && url.front() == '/') + path = url; + else if (!url.empty() && url.front() == '/') { + url.remove_prefix(1); + path = std::nullopt; + } + + return result; +} + } // namespace session diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 07be219b..bdac72e4 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -295,7 +295,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "bad-url", "room", "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); - CHECK(conf->last_error == "Invalid community URL: invalid/missing protocol://"sv); + CHECK(conf->last_error == "Invalid URL: invalid/missing protocol://"sv); CHECK_FALSE(convo_info_volatile_get_or_construct_community( conf, &og, diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 8acdd35c..433be62f 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -53,7 +53,7 @@ TEST_CASE("config data dict encoding", "[config][data][dict]") { d["D"] = config::dict{{"x", 1}, {"y", 2}}; d["d"] = config::dict{{"e", config::dict{{"f", config::dict{{"g", ""}}}}}}; - static_assert(oxenc::detail::bt_input_dict_container); + static_assert(oxenc::bt_input_dict_container); CHECK(oxenc::bt_serialize(d) == "d1:B1:x1:Dd1:xi1e1:yi2ee1:ai23e1:cli-3ei4e1:11:2e1:dd1:ed1:fd1:g0:eeee"); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 607dae58..35077578 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -20,6 +20,92 @@ struct Result { }; } // namespace +namespace session::network { +class TestNetwork { + public: + Network network; + + TestNetwork(std::optional cache_path, bool use_testnet, bool pre_build_paths) : + network(cache_path, use_testnet, pre_build_paths) {} + + void set_paths(PathType path_type, std::vector paths) { + switch (path_type) { + case PathType::standard: network.standard_paths = paths; break; + case PathType::upload: network.upload_paths = paths; break; + case PathType::download: network.download_paths = paths; break; + } + } + + void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { + network.set_swarm(swarm_pubkey, swarm); + } + + void get_swarm( + session::onionreq::x25519_pubkey swarm_pubkey, + std::function swarm)> callback) { + network.get_swarm(swarm_pubkey, callback); + } + + void set_failure_count(service_node node, uint8_t failure_count) { + network.snode_failure_counts[node.to_string()] = failure_count; + } + + uint8_t get_failure_count(service_node node) { + return network.snode_failure_counts.try_emplace(node.to_string(), 0).first->second; + } + + uint8_t get_failure_count(PathType path_type, onion_path path) { + auto current_paths = network.paths_for_type(path_type); + auto target_path = std::find_if( + current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { + return path_it.nodes[0] == path.nodes[0]; + }); + + if (target_path != current_paths.end()) + return target_path->failure_count; + + return 0; + } + + std::vector paths_for(PathType path_type) { + return network.paths_for_type(path_type); + } + + void handle_errors( + request_info info, + bool timeout, + std::optional status_code, + std::optional response, + std::optional handle_response) { + network.handle_errors(info, timeout, status_code, response, handle_response); + } +}; +} // namespace session::network + +TEST_CASE("Network Url Parsing", "[network][parse_url]") { + auto [proto1, host1, port1, path1] = parse_url("HTTPS://example.com/test"); + auto [proto2, host2, port2, path2] = parse_url("http://example2.com:1234/test/123456"); + auto [proto3, host3, port3, path3] = parse_url("https://example3.com"); + auto [proto4, host4, port4, path4] = parse_url("https://example4.com/test?value=test"); + + CHECK(proto1 == "https://"); + CHECK(proto2 == "http://"); + CHECK(proto3 == "https://"); + CHECK(proto4 == "https://"); + CHECK(host1 == "example.com"); + CHECK(host2 == "example2.com"); + CHECK(host3 == "example3.com"); + CHECK(host4 == "example4.com"); + CHECK(port1.value_or(9999) == 9999); + CHECK(port2.value_or(9999) == 1234); + CHECK(port3.value_or(9999) == 9999); + CHECK(port4.value_or(9999) == 9999); + CHECK(path1.value_or("NULL") == "/test"); + CHECK(path2.value_or("NULL") == "/test/123456"); + CHECK(path3.value_or("NULL") == "NULL"); + CHECK(path4.value_or("NULL") == "/test?value=test"); +} + TEST_CASE("Network error handling", "[network]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; @@ -30,16 +116,16 @@ TEST_CASE("Network error handling", "[network]") { auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; auto path = onion_path{{{target}, nullptr, nullptr}, {target}, 0}; - auto mock_request = - request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; + auto mock_request = request_info{ + target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; Result result; - auto network = Network(std::nullopt, true, false); + auto network = TestNetwork(std::nullopt, true, false); // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; for (auto code : codes_with_no_changes) { - network.set_paths({path}); + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.handle_errors( mock_request, @@ -59,11 +145,11 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == code); CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 0); } // Check general error handling (first failure) - network.set_paths({path}); + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.handle_errors( mock_request, @@ -77,19 +163,18 @@ TEST_CASE("Network error handling", "[network]") { std::optional response) { result = {success, timeout, status_code, response}; }); - CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 1); + CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check general error handling with no response (too many path failures) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 9}; - auto mock_request2 = - request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; - network.set_paths({path}); + auto mock_request2 = request_info{ + target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.handle_errors( @@ -111,15 +196,16 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 3); // Guard node should be set to failure threshold CHECK(network.get_failure_count(target2) == - 1); // Other nodes get their failure count incremented - CHECK(network.get_failure_count(path) == 0); // Path will have been dropped and reset + 1); // Other nodes get their failure count incremented + CHECK(network.get_failure_count(PathType::standard, path) == + 0); // Path will have been dropped and reset // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; - auto mock_request3 = - request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; + auto mock_request3 = request_info{ + target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - network.set_paths({path}); + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.handle_errors( @@ -141,12 +227,13 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.response == response); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 1); - CHECK(network.get_failure_count(path) == 1); // Incremented because conn_info is invalid + CHECK(network.get_failure_count(PathType::standard, path) == + 1); // Incremented because conn_info is invalid // Check general error handling with a path and specific node failure (too many failures) - auto mock_request4 = - request_info{target, "test", std::nullopt, std::nullopt, path, 0ms, true, false}; - network.set_paths({path}); + auto mock_request4 = request_info{ + target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); network.handle_errors( @@ -168,10 +255,11 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.response == response); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 10); - CHECK(network.get_failure_count(path) == 1); // Incremented because conn_info is invalid + CHECK(network.get_failure_count(PathType::standard, path) == + 1); // Incremented because conn_info is invalid // Check a 421 with no swarm data throws (no good way to handle this case) - network.set_paths({path}); + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.handle_errors( @@ -191,12 +279,20 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(path) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 0); // Check the retry request of a 421 with no response data is handled like any other error auto mock_request5 = request_info{ - target, "test", std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, 0ms, true, true}; - network.set_paths({path}); + target, + "test", + std::nullopt, + x25519_pubkey::from_hex(x_pk_hex), + path, + PathType::standard, + 0ms, + true, + true}; + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.handle_errors( mock_request5, @@ -214,7 +310,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 1); + CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 with non-swarm response data is handled like any other error network.handle_errors( @@ -233,7 +329,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 1); + CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 instructs to replace the swarm auto snodes = nlohmann::json::array(); @@ -244,7 +340,7 @@ TEST_CASE("Network error handling", "[network]") { nlohmann::json swarm_json{{"snodes", snodes}}; response = swarm_json.dump(); network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target}); - network.set_paths({path}); + network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.handle_errors( mock_request5, @@ -263,7 +359,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 0); network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm) { REQUIRE(swarm.size() == 1); @@ -278,6 +374,7 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, + PathType::standard, 0ms, false, false}; @@ -297,7 +394,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.timeout); CHECK(result.status_code == -1); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 0); // Check a server response starting with '500 Internal Server Error' is reported as a `500` // error and doesn't affect the failure count @@ -317,7 +414,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(path) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 0); } TEST_CASE("Network onion request", "[send_onion_request][network]") { @@ -333,7 +430,6 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, std::nullopt, oxen::quic::DEFAULT_TIMEOUT, - false, [&result_promise]( bool success, bool timeout, From 68d732acf1537bffe30166c710ba60267a5b77a7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jun 2024 17:54:29 +1000 Subject: [PATCH 297/572] Fixed bugs which recovering connections and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a 'suspended' flag which will automatically fail any requests which are started while it is 'true' (this is to prevent retries and other requests trying to rebuild the connections & paths in the background on iOS as they will result in invalid connections) • Added a convenience method to generate a base32 string to identify a specific request for debugging • Added logic to catch an onion request decryption failure and automatically retry the request • Fixed an issue where a failed path reporting a subsequent failure could result in a permanently blocked network looper • Fixed an issue where the `421` retry request was sending invalid data • Fixed an issue where a `421` from a batch request wouldn't get handled correctly --- include/session/network.h | 11 + include/session/network.hpp | 93 ++-- include/session/onionreq/response_parser.hpp | 3 + include/session/random.hpp | 11 + src/network.cpp | 532 +++++++++++++------ src/onionreq/response_parser.cpp | 3 +- src/random.cpp | 30 ++ tests/test_network.cpp | 56 +- 8 files changed, 547 insertions(+), 192 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index 1d7d1523..e14dd66e 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -88,6 +88,17 @@ LIBSESSION_EXPORT bool network_init( /// - `network` -- [in] Pointer to network_object object LIBSESSION_EXPORT void network_free(network_object* network); +/// API: network/network_suspend +/// +/// Suspends the network preventing any further requests from creating new connections and paths. +/// This function also calls the `close_connections` function. +LIBSESSION_EXPORT void network_suspend(network_object* network); + +/// API: network/network_resume +/// +/// Resumes the network allowing new requests to creating new connections and paths. +LIBSESSION_EXPORT void network_resume(network_object* network); + /// API: network/network_close_connections /// /// Closes any currently active connections. diff --git a/include/session/network.hpp b/include/session/network.hpp index da06c126..2e890963 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -48,21 +48,29 @@ struct onion_path { uint8_t timeout_count; bool operator==(const onion_path& other) const { - return nodes == other.nodes && failure_count == other.failure_count && - timeout_count == other.timeout_count; + // The `conn_info` and failure/timeout counts can be reset for a path in a number + // of situations so just use the nodes to determine if the paths match + return nodes == other.nodes; } }; struct request_info { + enum class RetryReason { + decryption_failure, + redirect, + }; + + std::string request_id; service_node target; std::string endpoint; std::optional body; + std::optional original_body; std::optional swarm_pubkey; onion_path path; PathType path_type; std::chrono::milliseconds timeout; bool node_destination; - bool is_retry; + std::optional retry_reason; }; class Network { @@ -88,6 +96,7 @@ class Network { std::thread disk_write_thread; + bool suspended = false; ConnectionStatus status; oxen::quic::Network net; std::vector standard_paths; @@ -111,6 +120,17 @@ class Network { Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); ~Network(); + /// API: network/suspend + /// + /// Suspends the network preventing any further requests from creating new connections and + /// paths. This function also calls the `close_connections` function. + void suspend(); + + /// API: network/resume + /// + /// Resumes the network allowing new requests to creating new connections and paths. + void resume(); + /// API: network/close_connections /// /// Closes any currently active connections. @@ -301,8 +321,10 @@ class Network { /// - `target` -- [in] the target service node to connect to. /// /// Returns: - /// - `connection_info` -- The connection info for the target service node. - connection_info get_connection_info(PathType path_type, service_node target); + /// - A pair of the connection info for the target service node and an optional error if the + /// connnection was unable to be established. + std::pair> get_connection_info( + PathType path_type, service_node target); /// API: network/with_paths_and_pool /// @@ -432,7 +454,8 @@ class Network { std::vector target_nodes, std::function< void(std::optional valid_guard_node, - std::vector unused_nodes)> callback); + std::vector unused_nodes, + std::optional)> callback); /// API: network/get_service_nodes /// @@ -480,9 +503,11 @@ class Network { /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. /// Should be NULL if the request is not associated with a swarm. /// - `timeout` -- [in] timeout in milliseconds to use for the request. - /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used - /// for internal purposes for cases which should retry automatically (like receiving a `421`) in - /// order to prevent subsequent retries. + /// - `existing_request_id` -- [in] the request id for the existing request when this request is + /// a retry. Mostly to simplify debugging. + /// - `retry_reason` -- [in] the reason we are retrying the request (if it's a retry). Generally + /// only used for internal purposes (like + // receiving a `421`) in order to prevent subsequent retries. /// - `handle_response` -- [in] callback to be called with the result of the request. void send_onion_request( PathType type, @@ -490,40 +515,35 @@ class Network { std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - bool is_retry, + std::optional existing_request_id, + std::optional retry_reason, network_response_callback_t handle_response); - /// API: network/process_snode_response + /// API: network/process_v3_onion_response /// - /// Processes the response from an onion request sent to a service node destination. + /// Processes a v3 onion request response. /// /// Inputs: /// - `builder` -- [in] the builder that was used to build the onion request. /// - `response` -- [in] the response data returned from the destination. - /// - `info` -- [in] the information for the request that was made. - /// - `handle_response` -- [in] callback to be called with updated response information after - /// processing the error. - void process_snode_response( - session::onionreq::Builder builder, - std::string response, - request_info info, - network_response_callback_t handle_response); + /// + /// Outputs: + /// - A pair containing the status code and body of the decrypted onion request response. + std::pair> process_v3_onion_response( + session::onionreq::Builder builder, std::string response); - /// API: network/process_server_response + /// API: network/process_v4_onion_response /// - /// Processes the response from an onion request sent to a server destination. + /// Processes a v4 onion request response. /// /// Inputs: /// - `builder` -- [in] the builder that was used to build the onion request. /// - `response` -- [in] the response data returned from the destination. - /// - `info` -- [in] the information for the request that was made. - /// - `handle_response` -- [in] callback to be called with updated response information after - /// processing the error. - void process_server_response( - session::onionreq::Builder builder, - std::string response, - request_info info, - network_response_callback_t handle_response); + /// + /// Outputs: + /// - A pair containing the status code and body of the decrypted onion request response. + std::pair> process_v4_onion_response( + session::onionreq::Builder builder, std::string response); /// API: network/handle_errors /// @@ -544,6 +564,19 @@ class Network { std::optional status_code, std::optional response, std::optional handle_response); + + /// API: network/handle_node_error + /// + /// Convenience method to increment the failure count for a given node and path (if a node + /// doesn't have an associated path then just create one with the single node). This just calls + /// into the 'handle_errors' function in a way that will trigger an update to the failure + /// counts. + /// + /// Inputs: + /// - `node` -- [in] the node to increment the failure count for. + /// - `path_type` -- [in] type of path the node (or provided path) belong to. + /// - `path` -- [in] path to increment the failure count for. + void handle_node_error(service_node node, PathType path_type, onion_path path); }; } // namespace session::network diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp index be6b3576..ade15e49 100644 --- a/include/session/onionreq/response_parser.hpp +++ b/include/session/onionreq/response_parser.hpp @@ -7,6 +7,9 @@ namespace session::onionreq { +constexpr auto decryption_failed_error = + "Decryption failed (both XChaCha20-Poly1305 and AES256-GCM)"sv; + class ResponseParser { public: /// Constructs a parser, parsing the given request sent to us. Throws if parsing or decryption diff --git a/include/session/random.hpp b/include/session/random.hpp index 72fc8b89..821049ba 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -15,4 +15,15 @@ namespace session::random { /// - random bytes of the specified length. ustring random(size_t size); +/// API: random/random_base32 +/// +/// Return a random base32 string with the given length. +/// +/// Inputs: +/// - `size` -- the number of characters to be generated. +/// +/// Outputs: +/// - random base32 string of the specified length. +std::string random_base32(size_t size); + } // namespace session::random diff --git a/src/network.cpp b/src/network.cpp index cad89b48..83732dea 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -24,6 +24,7 @@ #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/onionreq/response_parser.hpp" +#include "session/random.hpp" #include "session/util.hpp" using namespace oxen; @@ -507,6 +508,21 @@ void Network::disk_write_thread_loop() { } } +void Network::suspend() { + net.call([this]() mutable { + suspended = true; + close_connections(); + log::info(cat, "Suspended."); + }); +} + +void Network::resume() { + net.call([this]() mutable { + suspended = false; + log::info(cat, "Resumed."); + }); +} + void Network::close_connections() { net.call([this]() mutable { endpoint.reset(); @@ -563,7 +579,14 @@ std::shared_ptr Network::get_endpoint() { }); } -connection_info Network::get_connection_info(PathType path_type, service_node target) { +std::pair> Network::get_connection_info( + PathType path_type, service_node target) { + auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); + + // If the network is currently suspended then don't try to open a connection + if (currently_suspended) + return {{target, nullptr, nullptr}, "Network is suspended."}; + auto cb_called = std::make_shared(); auto mutex = std::make_shared(); auto cv = std::make_shared(); @@ -589,7 +612,7 @@ connection_info Network::get_connection_info(PathType path_type, service_node ta }); }, [this, path_type, target, mutex, cv, done, cb_called]( - quic::connection_interface& conn, uint64_t) { + quic::connection_interface& conn, uint64_t error_code) { // Trigger the callback first before updating the paths in case this was triggered // when try to establish a connection if (cb_called) { @@ -615,30 +638,22 @@ connection_info Network::get_connection_info(PathType path_type, service_node ta conn.reference_id() == target_path->conn_info.conn->reference_id()) { target_path->conn_info.conn.reset(); target_path->conn_info.stream.reset(); - handle_errors( - {target, - "", - std::nullopt, - std::nullopt, - *target_path, - path_type, - 0ms, - false, - false}, - false, - std::nullopt, - std::nullopt, - std::nullopt); - } + handle_node_error(target, path_type, *target_path); + } else if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) + // Depending on the state of the snode pool cache it's possible for certain + // errors to result in being permanently unable to establish a connection, to + // avoid this we handle those error codes and drop + handle_node_error( + target, path_type, {{target, nullptr, nullptr}, {target}, 0, 0}); }); std::unique_lock lock(*mutex); cv->wait(lock, [&done] { return *done; }); if (!*connection_established) - return {target, nullptr, nullptr}; + return {{target, nullptr, nullptr}, "Network is unreachable."}; - return {target, c, c->open_stream()}; + return {{target, c, c->open_stream()}, std::nullopt}; } // MARK: Snode Pool and Onion Path @@ -648,7 +663,8 @@ using paths_and_pool_result = using paths_and_pool_info = std::tuple< std::vector, std::vector, - std::chrono::system_clock::time_point>; + std::chrono::system_clock::time_point, + bool>; void Network::with_paths_and_pool( PathType path_type, @@ -657,11 +673,15 @@ void Network::with_paths_and_pool( void(std::vector updated_paths, std::vector pool, std::optional error)> callback) { - auto [current_paths, pool, last_pool_update] = + auto [current_paths, pool, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { - return {paths_for_type(path_type), snode_pool, last_snode_pool_update}; + return {paths_for_type(path_type), snode_pool, last_snode_pool_update, suspended}; }); + // If the network is currently suspended then fail immediately + if (currently_suspended) + return callback({}, {}, "Network is suspended"); + // Check if the current data is valid, and if so just return it auto current_valid_paths = valid_paths(current_paths); auto [paths_valid, pool_valid] = @@ -672,11 +692,18 @@ void Network::with_paths_and_pool( auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get( [this, path_type, excluded_node]() mutable -> paths_and_pool_result { - auto [current_paths, pool, last_pool_update] = + auto [current_paths, pool, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { - return {paths_for_type(path_type), snode_pool, last_snode_pool_update}; + return {paths_for_type(path_type), + snode_pool, + last_snode_pool_update, + suspended}; }); + // If the network is currently suspended then fail immediately + if (currently_suspended) + return {{}, {}, "Network is suspended"}; + // Check if the current data is valid, and if so just return it auto current_valid_paths = valid_paths(current_paths); auto [paths_valid, pool_valid] = validate_paths_and_pool_sizes( @@ -696,6 +723,8 @@ void Network::with_paths_and_pool( // Populate the snode pool if needed if (!pool_valid) { + log::info(cat, "Snode pool cache no longer valid, need to refetch."); + // Define the response handler to avoid code duplication auto handle_nodes_response = [](std::promise>&& prom) { @@ -893,11 +922,13 @@ void Network::with_paths_and_pool( path_type, nodes_to_test[i], [prom](std::optional valid_guard_node, - std::vector unused_nodes) { + std::vector unused_nodes, + std::optional error) { try { if (!valid_guard_node) throw std::runtime_error{ - "Failed to find valid guard node."}; + error.value_or("Failed to find valid guard " + "node.")}; prom->set_value({*valid_guard_node, unused_nodes}); } catch (...) { prom->set_exception(std::current_exception()); @@ -1043,8 +1074,15 @@ void Network::with_path( std::optional excluded_node, std::function path, std::optional error)> callback) { - auto current_paths = net.call_get( - [this, path_type]() -> std::vector { return paths_for_type(path_type); }); + auto [current_paths, currently_suspended] = + net.call_get([this, path_type]() -> std::pair, bool> { + return {paths_for_type(path_type), suspended}; + }); + + // If the network is currently suspended then fail immediately + if (currently_suspended) + return callback(std::nullopt, "Network is suspended"); + std::pair, uint8_t> path_info = find_possible_path(excluded_node, current_paths); auto& [target_path, paths_count] = path_info; @@ -1069,19 +1107,34 @@ void Network::with_path( if (target_path_it == current_paths.end()) return {std::nullopt, current_paths.size()}; + // It's possible that multiple requests were queued up waiting on the connection + // the be reestablished so check to see if the path is now valid and return it + // if it is + if (target_path_it->conn_info.is_valid()) + return {*target_path_it, current_paths.size()}; + // Try to retrieve a valid connection for the guard node log::info( cat, - "Connection to {} path no longer valid, attempting reconnection.", + "Connection to {} for {} path no longer valid, attempting " + "reconnection.", + target_path_it->nodes[0], path_type_name(path_type)); - auto info = get_connection_info(path_type, path.nodes[0]); + auto [info, error] = get_connection_info(path_type, path.nodes[0]); // It's possible that the connection was created successfully, and reported as // valid, but isn't actually valid (eg. it was shutdown immediately due to the // network being unreachable) so to avoid this we wait for either the connection // to be established or the connection to fail before continuing - if (!info.is_valid()) + if (!info.is_valid()) { + log::info( + cat, + "Reconnection to {} for {} path failed with error: {}.", + target_path_it->nodes[0], + path_type_name(path_type), + error.value_or("Unknown error.")); return {std::nullopt, current_paths.size()}; + } // If the connection info is valid and it's a standard path then update the // connection status back to connected @@ -1090,7 +1143,7 @@ void Network::with_path( // No need to call the 'paths_changed' callback as the paths haven't // actually changed, just their connection info - auto updated_path = onion_path{std::move(info), std::move(path.nodes), 0, 0}; + auto updated_path = onion_path{std::move(info), path.nodes, 0, 0}; auto paths_count = net.call_get( [this, path_type, path, updated_path]() mutable -> uint8_t { switch (path_type) { @@ -1228,9 +1281,15 @@ void Network::find_valid_guard_node_recursive( std::vector target_nodes, std::function< void(std::optional valid_guard_node, - std::vector unused_nodes)> callback) { + std::vector unused_nodes, + std::optional)> callback) { if (target_nodes.empty()) - return callback(std::nullopt, {}); + return callback(std::nullopt, {}, "Failed to find valid guard node."); + + // If the network is currently suspended then fail immediately + auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); + if (currently_suspended) + return callback(std::nullopt, {}, "Network is suspended"); auto target_node = target_nodes.front(); log::info(cat, "Testing guard snode: {}", target_node.to_string()); @@ -1258,7 +1317,7 @@ void Network::find_valid_guard_node_recursive( "Outdated node version ({})"_format(fmt::join(version, "."))}; log::info(cat, "Guard snode {} valid.", target_node.to_string()); - cb(info, remaining_nodes); + cb(info, remaining_nodes, std::nullopt); } catch (const std::exception& e) { // Log the error and loop after a slight delay (don't want to drain the pool // too quickly if the network goes down) @@ -1284,10 +1343,10 @@ void Network::get_service_nodes( std::optional limit, std::function nodes, std::optional error)> callback) { - auto info = get_connection_info(PathType::standard, node); + auto [info, error] = get_connection_info(PathType::standard, node); if (!info.is_valid()) - return callback({}, "Network is unreachable."); + return callback({}, error.value_or("Unknown error.")); nlohmann::json params{ {"active_only", true}, @@ -1341,10 +1400,10 @@ void Network::get_version( std::function version, connection_info info, std::optional error)> callback) { - auto info = get_connection_info(path_type, node); + auto [info, error] = get_connection_info(path_type, node); if (!info.is_valid()) - return callback({}, info, "Network is unreachable."); + return callback({}, info, error.value_or("Unknown error.")); oxenc::bt_dict_producer payload; info.stream->command( @@ -1389,6 +1448,8 @@ void Network::get_swarm( return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from + log::info(cat, "No cached swarm for {}, fetching from random node.", swarm_pubkey.hex()); + with_paths_and_pool( PathType::standard, std::nullopt, @@ -1416,7 +1477,8 @@ void Network::get_swarm( ustring{quic::to_usv(payload.dump())}, swarm_pubkey, quic::DEFAULT_TIMEOUT, - false, + std::nullopt, + std::nullopt, [this, swarm_pubkey, cb = std::move(cb)]( bool success, bool timeout, @@ -1524,7 +1586,8 @@ void Network::send_onion_request( std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - bool is_retry, + std::optional existing_request_id, + std::optional retry_reason, network_response_callback_t handle_response) { with_path( path_type, @@ -1535,7 +1598,8 @@ void Network::send_onion_request( body, swarm_pubkey, timeout, - is_retry, + existing_request_id, + retry_reason, cb = std::move(handle_response)]( std::optional path, std::optional error) { if (!path) @@ -1556,15 +1620,17 @@ void Network::send_onion_request( auto onion_req_payload = builder.build(payload); request_info info{ + existing_request_id.value_or(random::random_base32(4)), path->nodes[0], "onion_req", onion_req_payload, + body, swarm_pubkey, *path, path_type, timeout, node_for_destination(destination).has_value(), - is_retry}; + retry_reason}; send_request( info, @@ -1583,17 +1649,106 @@ void Network::send_onion_request( if (!success || timeout) return cb(success, timeout, status_code, response); - // Ensure the response is long enough to be processed, if not then - // handle it as an error - if (!ResponseParser::response_long_enough( - builder.enc_type, response->size())) - return handle_errors(info, timeout, status_code, response, cb); - - // Otherwise, process the onion request response - if (std::holds_alternative(destination)) - process_snode_response(builder, *response, info, cb); - else if (std::holds_alternative(destination)) - process_server_response(builder, *response, info, cb); + try { + // Ensure the response is long enough to be processed, if not + // then handle it as an error + if (!ResponseParser::response_long_enough( + builder.enc_type, response->size())) + throw status_code_exception{ + status_code, + "Response is too short to be an onion request " + "response: " + + *response}; + + // Otherwise, process the onion request response + std::pair> + processed_response; + + // The SnodeDestination runs via V3 onion requests and the + // ServerDestination runs via V4 + if (std::holds_alternative(destination)) + processed_response = + process_v3_onion_response(builder, *response); + else if (std::holds_alternative(destination)) + processed_response = + process_v4_onion_response(builder, *response); + + // If we got a non 2xx status code, return the error + auto& [processed_status_code, processed_body] = + processed_response; + if (processed_status_code < 200 || processed_status_code > 299) + throw status_code_exception{ + processed_status_code, + processed_body.value_or("Request returned " + "non-success status " + "code.")}; + + // Try process the body in case it was a batch request which + // failed + std::optional results; + if (processed_body) { + try { + auto processed_body_json = + nlohmann::json::parse(*processed_body); + + // If it wasn't a batch/sequence request then assume it + // was successful and return no error + if (processed_body_json.contains("results")) + results = processed_body_json["results"]; + } catch (...) { + } + } + + // If there was no 'results' array then it wasn't a batch + // request so we can stop here and return + if (!results) + return cb( + true, false, processed_status_code, processed_body); + + // Otherwise we want to check if all of the results have the + // same status code and, if so, handle that failure case + // (default the 'error_body' to the 'processed_body' in case we + // don't get an explicit error) + int16_t single_status_code = -1; + std::optional error_body = processed_body; + for (const auto& result : results->items()) { + if (result.value().contains("code") && + result.value()["code"].is_number() && + (single_status_code == -1 || + result.value()["code"].get() != + single_status_code)) + single_status_code = + result.value()["code"].get(); + else { + // Either there was no code, or the code was different + // from a former code in which case there wasn't an + // individual detectable error (ie. it needs specific + // handling) so return no error + single_status_code = 200; + break; + } + + if (result.value().contains("body") && + result.value()["body"].is_string()) + error_body = result.value()["body"].get(); + } + + // If all results contained the same error then handle it as a + // single error + if (single_status_code < 200 || single_status_code > 299) + throw status_code_exception{ + single_status_code, + error_body.value_or("Sub-request returned " + "non-success status code.")}; + + // Otherwise some requests succeeded and others failed so + // succeed with the processed data + return cb(true, false, processed_status_code, processed_body); + } catch (const status_code_exception& e) { + handle_errors(info, false, e.status_code, e.what(), cb); + } catch (const std::exception& e) { + handle_errors(info, false, -1, e.what(), cb); + } }); } catch (const std::exception& e) { cb(false, false, -1, e.what()); @@ -1608,7 +1763,14 @@ void Network::send_onion_request( std::chrono::milliseconds timeout, network_response_callback_t handle_response) { send_onion_request( - PathType::standard, destination, body, swarm_pubkey, timeout, false, handle_response); + PathType::standard, + destination, + body, + swarm_pubkey, + timeout, + std::nullopt, + std::nullopt, + handle_response); } void Network::upload_file_to_server( @@ -1648,7 +1810,8 @@ void Network::upload_file_to_server( data, std::nullopt, timeout, - false, + std::nullopt, + std::nullopt, handle_response); } @@ -1678,7 +1841,8 @@ void Network::download_file( std::nullopt, std::nullopt, timeout, - false, + std::nullopt, + std::nullopt, handle_response); } @@ -1702,112 +1866,81 @@ void Network::get_client_version( std::nullopt, pubkey, timeout, - false, + std::nullopt, + std::nullopt, handle_response); } // MARK: Response Handling -// The SnodeDestination runs via V3 onion requests -void Network::process_snode_response( - Builder builder, - std::string response, - request_info info, - network_response_callback_t handle_response) { +std::pair> Network::process_v3_onion_response( + Builder builder, std::string response) { + std::string base64_iv_and_ciphertext; try { - std::string base64_iv_and_ciphertext; + nlohmann::json response_json = nlohmann::json::parse(response); - try { - nlohmann::json response_json = nlohmann::json::parse(response); + if (!response_json.contains("result") || !response_json["result"].is_string()) + throw std::runtime_error{"JSON missing result field."}; - if (!response_json.contains("result") || !response_json["result"].is_string()) - throw std::runtime_error{"JSON missing result field."}; - - base64_iv_and_ciphertext = response_json["result"].get(); - } catch (...) { - base64_iv_and_ciphertext = response; - } - - if (!oxenc::is_base64(base64_iv_and_ciphertext)) - throw std::runtime_error{"Invalid base64 encoded IV and ciphertext."}; - - ustring iv_and_ciphertext; - oxenc::from_base64( - base64_iv_and_ciphertext.begin(), - base64_iv_and_ciphertext.end(), - std::back_inserter(iv_and_ciphertext)); - auto parser = ResponseParser(builder); - auto result = parser.decrypt(iv_and_ciphertext); - auto result_json = nlohmann::json::parse(result); - int16_t status_code; - std::string body; - - if (result_json.contains("status_code") && result_json["status_code"].is_number()) - status_code = result_json["status_code"].get(); - else if (result_json.contains("status") && result_json["status"].is_number()) - status_code = result_json["status"].get(); - else - throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; + base64_iv_and_ciphertext = response_json["result"].get(); + } catch (...) { + base64_iv_and_ciphertext = response; + } - if (result_json.contains("body") && result_json["body"].is_string()) - body = result_json["body"].get(); - else - body = result_json.dump(); + if (!oxenc::is_base64(base64_iv_and_ciphertext)) + throw std::runtime_error{"Invalid base64 encoded IV and ciphertext: " + response + "."}; + + ustring iv_and_ciphertext; + oxenc::from_base64( + base64_iv_and_ciphertext.begin(), + base64_iv_and_ciphertext.end(), + std::back_inserter(iv_and_ciphertext)); + auto parser = ResponseParser(builder); + auto result = parser.decrypt(iv_and_ciphertext); + auto result_json = nlohmann::json::parse(result); + int16_t status_code; + std::string body; + + if (result_json.contains("status_code") && result_json["status_code"].is_number()) + status_code = result_json["status_code"].get(); + else if (result_json.contains("status") && result_json["status"].is_number()) + status_code = result_json["status"].get(); + else + throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; - // If we got a non 2xx status code, return the error - if (status_code < 200 || status_code > 299) - return handle_errors(info, false, status_code, body, handle_response); + if (result_json.contains("body") && result_json["body"].is_string()) + body = result_json["body"].get(); + else + body = result_json.dump(); - handle_response(true, false, status_code, body); - } catch (const std::exception& e) { - handle_response(false, false, -1, e.what()); - } + return {status_code, body}; } -// The ServerDestination runs via V4 onion requests -void Network::process_server_response( - Builder builder, - std::string response, - request_info info, - network_response_callback_t handle_response) { - try { - ustring response_data{to_unsigned(response.data()), response.size()}; - auto parser = ResponseParser(builder); - auto result = parser.decrypt(response_data); +std::pair> Network::process_v4_onion_response( + Builder builder, std::string response) { + ustring response_data{to_unsigned(response.data()), response.size()}; + auto parser = ResponseParser(builder); + auto result = parser.decrypt(response_data); - // Process the bencoded response - oxenc::bt_list_consumer result_bencode{result}; + // Process the bencoded response + oxenc::bt_list_consumer result_bencode{result}; - if (result_bencode.is_finished() || !result_bencode.is_string()) - throw std::runtime_error{"Invalid bencoded response"}; + if (result_bencode.is_finished() || !result_bencode.is_string()) + throw std::runtime_error{"Invalid bencoded response"}; - auto response_info_string = result_bencode.consume_string(); - int16_t status_code; - nlohmann::json response_info_json = nlohmann::json::parse(response_info_string); - - if (response_info_json.contains("code") && response_info_json["code"].is_number()) - status_code = response_info_json["code"].get(); - else - throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; + auto response_info_string = result_bencode.consume_string(); + int16_t status_code; + nlohmann::json response_info_json = nlohmann::json::parse(response_info_string); - // If we have a status code that is not in the 2xx range, return the error - if (status_code < 200 || status_code > 299) { - if (result_bencode.is_finished()) - return handle_errors(info, false, status_code, std::nullopt, handle_response); - - return handle_errors( - info, false, status_code, result_bencode.consume_string(), handle_response); - } + if (response_info_json.contains("code") && response_info_json["code"].is_number()) + status_code = response_info_json["code"].get(); + else + throw std::runtime_error{"Invalid JSON response, missing required code field."}; - // If there is no body just return the success status - if (result_bencode.is_finished()) - return handle_response(true, false, status_code, std::nullopt); + if (result_bencode.is_finished()) + return {status_code, std::nullopt}; - // Otherwise return the result - handle_response(true, false, status_code, result_bencode.consume_string()); - } catch (const std::exception& e) { - handle_response(false, false, -1, e.what()); - } + return {status_code, result_bencode.consume_string()}; } // MARK: Error Handling @@ -1865,6 +1998,25 @@ std::pair Network::validate_response(quic::message resp, return {status_code, response_string}; } +void Network::handle_node_error(service_node node, PathType path_type, onion_path path) { + handle_errors( + {"Node Error", + node, + "", + std::nullopt, + std::nullopt, + std::nullopt, + path, + path_type, + 0ms, + false, + std::nullopt}, + false, + std::nullopt, + std::nullopt, + std::nullopt); +} + void Network::handle_errors( request_info info, bool timeout_, @@ -1874,6 +2026,30 @@ void Network::handle_errors( bool timeout = timeout_; auto status_code = status_code_.value_or(-1); + // There is an issue which can occur where we get invalid data back and are unable to decrypt + // it, if we do see this behaviour then we want to retry the request on the off chance it + // resolves itself + // + // When testing this case the retry always resulted in a 421 error, if that occurs we want to go + // through the standard 421 behaviour (which, in this case, would involve a 3rd retry against + // another node in the swarm to confirm the redirect) + if (!info.retry_reason && response && *response == session::onionreq::decryption_failed_error) { + log::info( + cat, + "Received decryption failure in request {} for {}, retrying.", + info.request_id, + path_type_name(info.path_type)); + return send_onion_request( + info.path_type, + info.target, + info.original_body, + info.swarm_pubkey, + info.timeout, + info.request_id, + request_info::RetryReason::decryption_failure, + (*handle_response)); + } + // A number of server errors can return HTML data but no status code, we want to extract those // cases so they can be handled properly below if (status_code == -1 && response) { @@ -1941,7 +2117,8 @@ void Network::handle_errors( // If this was the first 421 then we want to retry using another node in the // swarm to get confirmation that we should switch to a different swarm - if (!info.is_retry) { + if (!info.retry_reason || + info.retry_reason != request_info::RetryReason::redirect) { CSRNG rng; std::vector swarm_copy = cached_swarm; std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); @@ -1959,13 +2136,20 @@ void Network::handle_errors( if (!random_node) throw std::invalid_argument{"No other nodes in the swarm."}; + log::info( + cat, + "Received 421 error in request {} for {}, retrying once before " + "updating swarm.", + info.request_id, + path_type_name(info.path_type)); return send_onion_request( info.path_type, *random_node, - info.body, + info.original_body, info.swarm_pubkey, info.timeout, - true, + info.request_id, + request_info::RetryReason::redirect, (*handle_response)); } @@ -1986,6 +2170,12 @@ void Network::handle_errors( if (swarm.empty()) throw std::invalid_argument{"No snodes in the response."}; + log::info( + cat, + "Retry for request {} resulted in another 421 for {}, updating swarm.", + info.request_id, + path_type_name(info.path_type)); + // Update the cache net.call([this, swarm_pubkey = *info.swarm_pubkey, swarm]() mutable { { @@ -2079,6 +2269,14 @@ void Network::handle_errors( // Set the failure count of the guard node to match the threshold so we drop it updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; + } else if (updated_path.nodes.size() < path_size) { + // If the path doesn't have enough nodes then it's likely that this failure was + // triggered when trying to establish a new path and, as such, we should increase the + // failure count of the guard node since it is probably invalid + auto failure_count = + updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) + .first->second; + updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; } } @@ -2089,6 +2287,7 @@ void Network::handle_errors( net.call([this, path_type = info.path_type, + target_node = info.target, swarm_pubkey = info.swarm_pubkey, old_path = info.path, updated_failure_counts, @@ -2096,6 +2295,8 @@ void Network::handle_errors( &cv, &mtx, &done]() mutable { + auto already_handled_failure = false; + // Drop the path if invalid if (updated_path.failure_count >= path_failure_threshold || updated_path.timeout_count >= path_timeout_threshold) { @@ -2145,13 +2346,14 @@ void Network::handle_errors( path_description); else { // If the path was already dropped then the snode pool would have already been - // updated so no need to continue + // updated so update the `already_handled_failure` to avoid double-handle the + // failure + already_handled_failure = true; log::info( cat, "Path already dropped for {}: [{}]", path_type_name(path_type), path_description); - return; } } else { switch (path_type) { @@ -2171,12 +2373,16 @@ void Network::handle_errors( } } - // Update the snode failure counts - snode_failure_counts = updated_failure_counts; + // If we hadn't already handled the failure then update the failure counts and connection + // status + if (!already_handled_failure) { + // Update the snode failure counts + snode_failure_counts = updated_failure_counts; - // Update the network status if we've removed all standard paths - if (standard_paths.empty()) - update_status(ConnectionStatus::disconnected); + // Update the network status if we've removed all standard paths + if (standard_paths.empty()) + update_status(ConnectionStatus::disconnected); + } // Since we've finished updating the path and failure count states we can stop blocking // the caller (no need to wait for the snode cache to update) @@ -2186,6 +2392,10 @@ void Network::handle_errors( } cv.notify_one(); + // We've already handled the failure so don't update the cache + if (already_handled_failure) + return; + // Update the snode cache { std::lock_guard lock{snode_cache_mutex}; @@ -2215,6 +2425,12 @@ void Network::handle_errors( old_path.nodes[i], updated_path.nodes[i]); + // If the target node is invalid then remove it from the snode pool + if (updated_failure_counts[target_node.to_string()] >= snode_failure_threshold) + snode_pool.erase( + std::remove(snode_pool.begin(), snode_pool.end(), target_node), + snode_pool.end()); + need_pool_write = true; need_swarm_write = (swarm_pubkey && swarm_cache.contains(swarm_pubkey->hex())); need_write = true; @@ -2304,6 +2520,14 @@ LIBSESSION_C_API void network_free(network_object* network) { delete network; } +LIBSESSION_C_API void network_suspend(network_object* network) { + unbox(network).suspend(); +} + +LIBSESSION_C_API void network_resume(network_object* network) { + unbox(network).resume(); +} + LIBSESSION_C_API void network_close_connections(network_object* network) { unbox(network).close_connections(); } diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index a6104a0c..bbddcc6c 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -43,8 +43,7 @@ ustring ResponseParser::decrypt(ustring ciphertext) const { ciphertext, destination_x25519_public_key_); } catch (...) { - throw std::runtime_error{ - "Decryption failed (XChaCha20-Poly1305, falling back to AES256-GCM)"}; + throw std::runtime_error{std::string(decryption_failed_error)}; } } else throw; diff --git a/src/random.cpp b/src/random.cpp index 693dd489..996bf370 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -7,6 +7,23 @@ namespace session::random { +constexpr char base32_charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567"; + +/// rng type that uses llarp::randint(), which is cryptographically secure +struct CSRNG { + using result_type = uint64_t; + + static constexpr uint64_t min() { return std::numeric_limits::min(); }; + + static constexpr uint64_t max() { return std::numeric_limits::max(); }; + + uint64_t operator()() { + uint64_t i; + randombytes((uint8_t*)&i, sizeof(i)); + return i; + }; +}; + ustring random(size_t size) { ustring result; result.resize(size); @@ -15,6 +32,19 @@ ustring random(size_t size) { return result; } +std::string random_base32(size_t size) { + CSRNG rng; + std::string charset = base32_charset; + std::string result; + + for (size_t i = 0; i < size; ++i) { + std::shuffle(charset.begin(), charset.end(), rng); + result.push_back(charset[0]); + } + + return result; +} + } // namespace session::random extern "C" { diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 35077578..ebde3312 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -117,7 +117,17 @@ TEST_CASE("Network error handling", "[network]") { auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; auto path = onion_path{{{target}, nullptr, nullptr}, {target}, 0}; auto mock_request = request_info{ - target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + "AAAA", + target, + "test", + std::nullopt, + std::nullopt, + std::nullopt, + path, + PathType::standard, + 0ms, + true, + std::nullopt}; Result result; auto network = TestNetwork(std::nullopt, true, false); @@ -173,7 +183,17 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with no response (too many path failures) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 9}; auto mock_request2 = request_info{ - target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + "BBBB", + target, + "test", + std::nullopt, + std::nullopt, + std::nullopt, + path, + PathType::standard, + 0ms, + true, + std::nullopt}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); @@ -203,7 +223,17 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; auto mock_request3 = request_info{ - target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + "CCCC", + target, + "test", + std::nullopt, + std::nullopt, + std::nullopt, + path, + PathType::standard, + 0ms, + true, + std::nullopt}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); @@ -232,7 +262,17 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (too many failures) auto mock_request4 = request_info{ - target, "test", std::nullopt, std::nullopt, path, PathType::standard, 0ms, true, false}; + "DDDD", + target, + "test", + std::nullopt, + std::nullopt, + std::nullopt, + path, + PathType::standard, + 0ms, + true, + std::nullopt}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); @@ -283,15 +323,17 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with no response data is handled like any other error auto mock_request5 = request_info{ + "EEEE", target, "test", std::nullopt, + std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, PathType::standard, 0ms, true, - true}; + request_info::RetryReason::redirect}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.handle_errors( @@ -369,15 +411,17 @@ TEST_CASE("Network error handling", "[network]") { // Check a timeout with a sever destination doesn't impact the failure counts auto mock_request6 = request_info{ + "FFFF", target, "test", std::nullopt, + std::nullopt, x25519_pubkey::from_hex(x_pk_hex), path, PathType::standard, 0ms, false, - false}; + std::nullopt}; network.handle_errors( mock_request6, true, From 714230c0c8867ac8662603fd6debb5b4c4369ef1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jun 2024 18:02:30 +1000 Subject: [PATCH 298/572] Fixed the broken tests --- tests/test_network.cpp | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index ebde3312..0d673c75 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -115,7 +115,8 @@ TEST_CASE("Network error handling", "[network]") { auto x_pk_hex = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"; auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; - auto path = onion_path{{{target}, nullptr, nullptr}, {target}, 0}; + auto target3 = service_node{ed_pk2, "0.0.0.2", uint16_t{2}}; + auto path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", target, @@ -137,6 +138,8 @@ TEST_CASE("Network error handling", "[network]") { for (auto code : codes_with_no_changes) { network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request, false, @@ -155,12 +158,16 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == code); CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 0); } // Check general error handling (first failure) network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request, false, @@ -178,10 +185,12 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check general error handling with no response (too many path failures) - path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 9}; + path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 9}; auto mock_request2 = request_info{ "BBBB", target, @@ -197,6 +206,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request2, false, @@ -217,11 +227,13 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(target) == 3); // Guard node should be set to failure threshold CHECK(network.get_failure_count(target2) == 1); // Other nodes get their failure count incremented + CHECK(network.get_failure_count(target3) == + 1); // Other nodes get their failure count incremented CHECK(network.get_failure_count(PathType::standard, path) == 0); // Path will have been dropped and reset // Check general error handling with a path and specific node failure (first failure) - path = onion_path{{{target}, nullptr, nullptr}, {target, target2}, 0}; + path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request3 = request_info{ "CCCC", target, @@ -238,6 +250,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request3, false, @@ -257,6 +270,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.response == response); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 1); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Incremented because conn_info is invalid @@ -276,6 +290,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); + network.set_failure_count(target3, 0); network.handle_errors( mock_request4, false, @@ -295,6 +310,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.response == response); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 10); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Incremented because conn_info is invalid @@ -302,6 +318,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request, false, @@ -319,7 +336,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 0); + CHECK(network.get_failure_count(target3) == 0); + CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 with no response data is handled like any other error auto mock_request5 = request_info{ @@ -336,6 +354,8 @@ TEST_CASE("Network error handling", "[network]") { request_info::RetryReason::redirect}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request5, false, @@ -352,6 +372,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 with non-swarm response data is handled like any other error @@ -371,6 +393,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 instructs to replace the swarm @@ -384,6 +408,8 @@ TEST_CASE("Network error handling", "[network]") { network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( mock_request5, false, @@ -401,6 +427,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 0); network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm) { @@ -438,6 +466,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.timeout); CHECK(result.status_code == -1); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 0); // Check a server response starting with '500 Internal Server Error' is reported as a `500` @@ -458,6 +488,8 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(network.get_failure_count(target) == 0); + CHECK(network.get_failure_count(target2) == 0); + CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 0); } From d2190967323c88dc57a219bdf3aeb9985c5b66b0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Jun 2024 13:58:22 +1000 Subject: [PATCH 299/572] Looking to fix CI errors --- include/session/network.hpp | 1 + src/network.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/include/session/network.hpp b/include/session/network.hpp index 2e890963..eec26c50 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -282,6 +282,7 @@ class Network { case PathType::upload: return upload_paths; case PathType::download: return download_paths; } + return standard_paths; // Default }; /// API: network/update_status diff --git a/src/network.cpp b/src/network.cpp index 83732dea..196fee03 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -86,6 +86,7 @@ namespace { case PathType::upload: return "upload"; case PathType::download: return "download"; } + return "standard"; // Default } // The number of paths we want to maintain. @@ -95,6 +96,7 @@ namespace { case PathType::upload: return 1; case PathType::download: return 1; } + return 2; // Default } service_node node_from_json(nlohmann::json json) { From 2b29c06a1de53ff10129a35c74b4d3fda4eb102d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 13:55:31 +1000 Subject: [PATCH 300/572] Added code to handle unwrapping double-wrapped config messages --- src/config/base.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index ae44ffcc..4ba148e8 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -75,7 +75,23 @@ std::vector ConfigBase::merge( ustring_view{_keys.front().data(), _keys.front().size()}, c, storage_namespace()); - parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); + + // There was a release of one of the clients which resulted in double-wrapped + // config messages so we now need to try to double-unwrap in order to better + // support multi-device for users running those old versions + try { + auto unwrapped2 = protos::unwrap_config( + ustring_view{_keys.front().data(), _keys.front().size()}, + unwrapped, + storage_namespace()); + log::warning( + cat, + "Found double wraped message in namespace {}", + static_cast(storage_namespace())); + parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped2))); + } catch (...) { + parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); + } } catch (...) { parsed.emplace_back(h, c); } From a19cf09ba9fd50ef90a6a1c0671cd8f7f2983c8e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 15:40:29 +1000 Subject: [PATCH 301/572] Removed a warning log since oxen::log isn't included yet --- src/config/base.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 4ba148e8..2f22f7ca 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -84,10 +84,6 @@ std::vector ConfigBase::merge( ustring_view{_keys.front().data(), _keys.front().size()}, unwrapped, storage_namespace()); - log::warning( - cat, - "Found double wraped message in namespace {}", - static_cast(storage_namespace())); parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped2))); } catch (...) { parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); From 9a867d563266b875144285cd49b07b3aacf7206e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 18:09:44 +1000 Subject: [PATCH 302/572] A couple more bug fixes and added auth for versioning API call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a 'single_path_mode' to avoid generating multiple paths for iOS app extensions • Added a check to prevent a potential deadlock when getting connection info if the callback is called too quickly • Updated the 'get_client_version' endpoint to authenticate the request • Fixed an issue where a specific node failure wasn't resulting in the node being removed from it's path • Included pending changes from versioning & double unwrapping PRs --- include/session/blinding.h | 33 ++++++++ include/session/blinding.hpp | 20 +++++ include/session/network.h | 13 +-- include/session/network.hpp | 18 ++-- include/session/platform.h | 15 ++++ include/session/platform.hpp | 11 +++ src/blinding.cpp | 83 +++++++++++++++++++ src/config/base.cpp | 18 +++- src/network.cpp | 154 +++++++++++++++++++++++++++++------ tests/test_blinding.cpp | 20 +++++ tests/test_network.cpp | 18 ++-- 11 files changed, 361 insertions(+), 42 deletions(-) create mode 100644 include/session/platform.h create mode 100644 include/session/platform.hpp diff --git a/include/session/blinding.h b/include/session/blinding.h index 715ec8d9..c83b9f09 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -7,6 +7,7 @@ extern "C" { #include #include "export.h" +#include "platform.h" /// API: crypto/session_blind15_key_pair /// @@ -50,6 +51,24 @@ LIBSESSION_EXPORT bool session_blind25_key_pair( unsigned char* blinded_pk_out, /* 32 byte output buffer */ unsigned char* blinded_sk_out /* 32 byte output buffer */); +/// API: crypto/session_blind_version_key_pair +/// +/// This function attempts to generate a blind-version key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the user (64 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 64 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind_version_key_pair( + const unsigned char* ed25519_seckey, /* 64 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 64 byte output buffer */); + /// API: crypto/session_blind15_sign /// /// This function attempts to generate a signature for a message using a blind15 private key. @@ -94,6 +113,20 @@ LIBSESSION_EXPORT bool session_blind25_sign( size_t msg_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes). Returns the blinded public key, signature and +/// timestamp. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +LIBSESSION_EXPORT bool session_blind_version_sign( + const unsigned char* ed25519_seckey, /* 64 bytes */ + CLIENT_PLATFORM platform, + size_t timestamp, + unsigned char* blinded_sig_out /* 64 byte output buffer */); + /// API: crypto/session_blind25_sign /// /// This function attempts to generate a signature for a message using a blind25 private key. diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 092dfee3..d2d67d33 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -3,6 +3,7 @@ #include #include +#include "platform.hpp" #include "sodium_array.hpp" namespace session { @@ -138,6 +139,15 @@ std::pair blind15_key_pair( std::pair blind25_key_pair( ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); +/// Computes a version-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key and +/// private key (NOT a seed). +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind_version_key_pair(ustring_view ed25519_sk); + /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). /// @@ -158,6 +168,16 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key, +/// signature and timestamp. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp); + /// Takes in a standard session_id and returns a flag indicating whether it matches the given /// blinded_id for a given server_pk. /// diff --git a/include/session/network.h b/include/session/network.h index e14dd66e..bf29eaa9 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -10,6 +10,7 @@ extern "C" { #include "export.h" #include "log_level.h" #include "onionreq/builder.h" +#include "platform.h" typedef enum CONNECTION_STATUS { CONNECTION_STATUS_UNKNOWN = 0, @@ -18,12 +19,6 @@ typedef enum CONNECTION_STATUS { CONNECTION_STATUS_DISCONNECTED = 3, } CONNECTION_STATUS; -typedef enum CLIENT_PLATFORM { - CLIENT_PLATFORM_ANDROID = 0, - CLIENT_PLATFORM_DESKTOP = 1, - CLIENT_PLATFORM_IOS = 2, -} CLIENT_PLATFORM; - typedef struct network_object { // Internal opaque object pointer; calling code should leave this alone. void* internals; @@ -65,6 +60,9 @@ typedef struct onion_request_path { /// NULL-terminated. /// - `use_testnet` -- [in] Flag indicating whether the network should connect to testnet or /// mainnet. +/// - `single_path_mode` -- [in] Flag indicating whether the network should be in "single path mode" +/// (ie. use a single path for everything - this is useful for iOS App Extensions which perform a +/// single action and then close so we don't waste time building other paths). /// - `pre_build_paths` -- [in] Flag indicating whether the network should pre-build it's paths. /// - `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 @@ -77,6 +75,7 @@ LIBSESSION_EXPORT bool network_init( network_object** network, const char* cache_path, bool use_testnet, + bool single_path_mode, bool pre_build_paths, char* error) __attribute__((warn_unused_result)); @@ -287,6 +286,7 @@ LIBSESSION_EXPORT void network_download_from_server( /// Inputs: /// - `network` -- [in] Pointer to the network object. /// - `platform` -- [in] the platform to retrieve the client version for. +/// - `ed25519_secret` -- [in] the users ed25519 secret key (used for blinded auth - 64 bytes). /// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set @@ -294,6 +294,7 @@ LIBSESSION_EXPORT void network_download_from_server( LIBSESSION_EXPORT void network_get_client_version( network_object* network, CLIENT_PLATFORM platform, + const unsigned char* ed25519_secret, /* 64 bytes */ int64_t timeout_ms, network_onion_response_callback_t callback, void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index eec26c50..b0a023aa 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -4,6 +4,7 @@ #include "onionreq/builder.hpp" #include "onionreq/key_types.hpp" +#include "platform.hpp" #include "types.hpp" namespace session::network { @@ -27,12 +28,6 @@ enum class PathType { download, }; -enum class Platform { - android, - desktop, - ios, -}; - struct connection_info { service_node node; std::shared_ptr conn; @@ -77,6 +72,7 @@ class Network { private: const bool use_testnet; const bool should_cache_to_disk; + const bool single_path_mode; const fs::path cache_path; // Disk thread state @@ -117,7 +113,10 @@ class Network { // Constructs a new network with the given cache path and a flag indicating whether it should // use testnet or mainnet, all requests should be made via a single Network instance. - Network(std::optional cache_path, bool use_testnet, bool pre_build_paths); + Network(std::optional cache_path, + bool use_testnet, + bool single_path_mode, + bool pre_build_paths); ~Network(); /// API: network/suspend @@ -262,10 +261,12 @@ class Network { /// /// Inputs: /// - `platform` -- [in] the platform to retrieve the client version for. + /// - `seckey` -- [in] the users ed25519 secret key (to generated blinded auth). /// - `timeout` -- [in] timeout in milliseconds to use for the request. /// - `handle_response` -- [in] callback to be called with the result of the request. void get_client_version( Platform platform, + onionreq::ed25519_seckey seckey, std::chrono::milliseconds timeout, network_response_callback_t handle_response); @@ -277,6 +278,9 @@ class Network { /// Inputs: /// - 'path_type' - [in] the type of paths to retrieve. std::vector paths_for_type(PathType type) const { + if (single_path_mode) + return standard_paths; + switch (type) { case PathType::standard: return standard_paths; case PathType::upload: return upload_paths; diff --git a/include/session/platform.h b/include/session/platform.h new file mode 100644 index 00000000..3c89614b --- /dev/null +++ b/include/session/platform.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum CLIENT_PLATFORM { + CLIENT_PLATFORM_ANDROID = 0, + CLIENT_PLATFORM_DESKTOP = 1, + CLIENT_PLATFORM_IOS = 2, +} CLIENT_PLATFORM; + +#ifdef __cplusplus +} +#endif diff --git a/include/session/platform.hpp b/include/session/platform.hpp new file mode 100644 index 00000000..46463f74 --- /dev/null +++ b/include/session/platform.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace session { + +enum class Platform { + android, + desktop, + ios, +}; + +} // namespace session diff --git a/src/blinding.cpp b/src/blinding.cpp index a9892fe8..e1a39fbe 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -9,7 +9,10 @@ #include #include +#include "session/ed25519.hpp" #include "session/export.h" +#include "session/platform.h" +#include "session/platform.hpp" #include "session/xed25519.hpp" namespace session { @@ -298,6 +301,37 @@ std::pair blind25_key_pair( return result; } +static const auto version_blinding_hash_key_sig = to_unsigned_sv("VersionCheckKey_sig"sv); + +std::pair blind_version_key_pair(ustring_view ed25519_sk) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " + "value"}; + + uc32 seed; + crypto_generichash_blake2b( + seed.data(), + seed.size(), + ed25519_sk.data(), + 32, + version_blinding_hash_key_sig.data(), + version_blinding_hash_key_sig.size()); + + uc32 pk; + cleared_uc64 sk; + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data())) + throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; + + return {pk, sk}; +} + static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); @@ -427,6 +461,24 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } +ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp) { + auto [pk, sk] = blind_version_key_pair(ed25519_sk); + + // Signature should be on `TIMESTAMP || METHOD || PATH` + ustring buf; + buf.reserve(10 + 6 + 33); + buf += to_unsigned_sv(std::to_string(timestamp)); + buf += to_unsigned("GET"); + + switch (platform) { + case Platform::android: buf += to_unsigned("/session_version?platform=android"); break; + case Platform::desktop: buf += to_unsigned("/session_version?platform=desktop"); break; + case Platform::ios: buf += to_unsigned("/session_version?platform=ios"); break; + } + + return ed25519::sign({sk.data(), sk.size()}, buf); +} + bool session_id_matches_blinded_id( std::string_view session_id, std::string_view blinded_id, std::string_view server_pk) { if (session_id.size() != 66 || !oxenc::is_hex(session_id)) @@ -492,6 +544,21 @@ LIBSESSION_C_API bool session_blind25_key_pair( } } +LIBSESSION_C_API bool session_blind_version_key_pair( + const unsigned char* ed25519_seckey, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { + try { + auto result = session::blind_version_key_pair({ed25519_seckey, 64}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_blind15_sign( const unsigned char* ed25519_seckey, const unsigned char* server_pk, @@ -526,6 +593,22 @@ LIBSESSION_C_API bool session_blind25_sign( } } +LIBSESSION_C_API bool session_blind_version_sign( + const unsigned char* ed25519_seckey, + CLIENT_PLATFORM platform, + size_t timestamp, + unsigned char* blinded_sig_out) { + try { + auto result = session::blind_version_sign( + {ed25519_seckey, 64}, static_cast(platform), timestamp); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_id_matches_blinded_id( const char* session_id, const char* blinded_id, const char* server_pk) { try { diff --git a/src/config/base.cpp b/src/config/base.cpp index 83f056c0..862e9548 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -82,7 +82,23 @@ std::vector ConfigBase::merge( ustring_view{_keys.front().data(), _keys.front().size()}, c, storage_namespace()); - parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); + + // There was a release of one of the clients which resulted in double-wrapped + // config messages so we now need to try to double-unwrap in order to better + // support multi-device for users running those old versions + try { + auto unwrapped2 = protos::unwrap_config( + ustring_view{_keys.front().data(), _keys.front().size()}, + unwrapped, + storage_namespace()); + log::warning( + cat, + "Found double wraped message in namespace {}", + static_cast(storage_namespace())); + parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped2))); + } catch (...) { + parsed.emplace_back(h, keep_alive.emplace_back(std::move(unwrapped))); + } } catch (...) { parsed.emplace_back(h, c); } diff --git a/src/network.cpp b/src/network.cpp index 196fee03..0c85c133 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -16,6 +16,7 @@ #include #include +#include "session/blinding.hpp" #include "session/ed25519.hpp" #include "session/export.h" #include "session/file.hpp" @@ -80,7 +81,10 @@ namespace { constexpr auto node_not_found_prefix_no_status = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; - std::string path_type_name(PathType path_type) { + std::string path_type_name(PathType path_type, bool single_path_mode) { + if (single_path_mode) + "single_path"; + switch (path_type) { case PathType::standard: return "standard"; case PathType::upload: return "upload"; @@ -90,7 +94,10 @@ namespace { } // The number of paths we want to maintain. - uint8_t target_path_count(PathType path_type) { + uint8_t target_path_count(PathType path_type, bool single_path_mode) { + if (single_path_mode) + return 1; + switch (path_type) { case PathType::standard: return 2; case PathType::upload: return 1; @@ -248,9 +255,14 @@ namespace { // MARK: Initialization -Network::Network(std::optional cache_path, bool use_testnet, bool pre_build_paths) : +Network::Network( + std::optional cache_path, + bool use_testnet, + bool single_path_mode, + bool pre_build_paths) : use_testnet{use_testnet}, should_cache_to_disk{cache_path}, + single_path_mode{single_path_mode}, cache_path{cache_path.value_or(default_cache_path)} { paths_and_pool_loop = std::make_shared(); @@ -649,8 +661,10 @@ std::pair> Network::get_connection_i target, path_type, {{target, nullptr, nullptr}, {target}, 0, 0}); }); - std::unique_lock lock(*mutex); - cv->wait(lock, [&done] { return *done; }); + if (!*done) { + std::unique_lock lock(*mutex); + cv->wait(lock, [&done] { return *done; }); + } if (!*connection_established) return {{target, nullptr, nullptr}, "Network is unreachable."}; @@ -852,7 +866,10 @@ void Network::with_paths_and_pool( if (!paths_valid) { try { // Get the possible guard nodes - log::info(cat, "Building paths for {}.", path_type_name(path_type)); + log::info( + cat, + "Building paths for {}.", + path_type_name(path_type, single_path_mode)); std::vector nodes_to_exclude; std::vector possible_guard_nodes; @@ -888,7 +905,8 @@ void Network::with_paths_and_pool( // Split the possible nodes list into a list of lists (one list could run // out before the other but in most cases this should work fine) size_t required_paths = - (target_path_count(path_type) - current_valid_paths.size()); + (target_path_count(path_type, single_path_mode) - + current_valid_paths.size()); size_t chunk_size = (possible_guard_nodes.size() / required_paths); std::vector> nodes_to_test; auto start = 0; @@ -955,10 +973,11 @@ void Network::with_paths_and_pool( // Make sure we ended up getting enough valid nodes auto have_enough_guard_nodes = (current_valid_paths.size() + valid_nodes.size() >= - target_path_count(path_type)); + target_path_count(path_type, single_path_mode)); auto have_enough_unused_nodes = (unused_nodes.size() >= - ((path_size - 1) * target_path_count(path_type))); + ((path_size - 1) * + target_path_count(path_type, single_path_mode))); if (!have_enough_guard_nodes || !have_enough_unused_nodes) throw std::runtime_error{"Not enough remaining nodes."}; @@ -986,14 +1005,14 @@ void Network::with_paths_and_pool( log::info( cat, "Built new onion request path for {}: [{}]", - path_type_name(path_type), + path_type_name(path_type, single_path_mode), path_description); } } catch (const std::exception& e) { log::info( cat, "Unable to build paths for {} due to error: {}", - path_type_name(path_type), + path_type_name(path_type, single_path_mode), e.what()); return {{}, {}, e.what()}; } @@ -1067,7 +1086,7 @@ std::pair Network::validate_paths_and_pool_sizes( auto cache_has_expired = (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); - return {(paths.size() >= target_path_count(path_type)), + return {(paths.size() >= target_path_count(path_type, single_path_mode)), (pool.size() >= min_snode_pool_count && !cache_has_expired)}; } @@ -1121,7 +1140,7 @@ void Network::with_path( "Connection to {} for {} path no longer valid, attempting " "reconnection.", target_path_it->nodes[0], - path_type_name(path_type)); + path_type_name(path_type, single_path_mode)); auto [info, error] = get_connection_info(path_type, path.nodes[0]); // It's possible that the connection was created successfully, and reported as @@ -1133,11 +1152,18 @@ void Network::with_path( cat, "Reconnection to {} for {} path failed with error: {}.", target_path_it->nodes[0], - path_type_name(path_type), + path_type_name(path_type, single_path_mode), error.value_or("Unknown error.")); return {std::nullopt, current_paths.size()}; } + // Knowing that the reconnection succeeded is helpful for debugging + log::info( + cat, + "Reconnection to {} for {} path successful.", + target_path_it->nodes[0], + path_type_name(path_type, single_path_mode)); + // If the connection info is valid and it's a standard path then update the // connection status back to connected if (path_type == PathType::standard) @@ -1201,7 +1227,7 @@ void Network::with_path( }); // Build additional paths in the background if we don't have enough - if (paths_count < target_path_count(path_type)) { + if (paths_count < target_path_count(path_type, single_path_mode)) { std::thread build_additional_paths_thread( &Network::with_paths_and_pool, this, @@ -1850,6 +1876,7 @@ void Network::download_file( void Network::get_client_version( Platform platform, + onionreq::ed25519_seckey seckey, std::chrono::milliseconds timeout, network_response_callback_t handle_response) { std::string endpoint; @@ -1860,11 +1887,28 @@ void Network::get_client_version( case Platform::ios: endpoint = "/session_version?platform=ios"; break; } + // Generate the auth signature + auto blinded_keys = blind_version_key_pair(to_unsigned_sv(seckey.view())); + auto timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + auto signature = blind_version_sign(to_unsigned_sv(seckey.view()), platform, timestamp); auto pubkey = x25519_pubkey::from_hex(file_server_pubkey); + std::string blinded_pk_hex; + blinded_pk_hex.reserve(66); + blinded_pk_hex += "07"; + oxenc::to_hex( + blinded_keys.first.begin(), + blinded_keys.first.end(), + std::back_inserter(blinded_pk_hex)); + + auto headers = std::vector>{}; + headers.emplace_back("X-FS-Pubkey", blinded_pk_hex); + headers.emplace_back("X-FS-Timestamp", "{}"_format(timestamp)); + headers.emplace_back("X-FS-Signature", oxenc::to_base64(signature)); + send_onion_request( PathType::standard, ServerDestination{ - "http", std::string(file_server), endpoint, pubkey, 80, std::nullopt, "GET"}, + "http", std::string(file_server), endpoint, pubkey, 80, headers, "GET"}, std::nullopt, pubkey, timeout, @@ -2040,7 +2084,7 @@ void Network::handle_errors( cat, "Received decryption failure in request {} for {}, retrying.", info.request_id, - path_type_name(info.path_type)); + path_type_name(info.path_type, single_path_mode)); return send_onion_request( info.path_type, info.target, @@ -2058,7 +2102,7 @@ void Network::handle_errors( const std::unordered_map> response_map = { {"500 Internal Server Error", {500, false}}, {"502 Bad Gateway", {502, false}}, - {"503 Service Unavailable", {502, false}}, + {"503 Service Unavailable", {503, false}}, {"504 Gateway Timeout", {504, true}}, }; @@ -2143,7 +2187,7 @@ void Network::handle_errors( "Received 421 error in request {} for {}, retrying once before " "updating swarm.", info.request_id, - path_type_name(info.path_type)); + path_type_name(info.path_type, single_path_mode)); return send_onion_request( info.path_type, *random_node, @@ -2176,7 +2220,7 @@ void Network::handle_errors( cat, "Retry for request {} resulted in another 421 for {}, updating swarm.", info.request_id, - path_type_name(info.path_type)); + path_type_name(info.path_type, single_path_mode)); // Update the cache net.call([this, swarm_pubkey = *info.swarm_pubkey, swarm]() mutable { @@ -2246,6 +2290,67 @@ void Network::handle_errors( auto failure_count = updated_failure_counts.try_emplace(snode_it->to_string(), 0).first->second; updated_failure_counts[snode_it->to_string()] = failure_count + 1; + + // If the specific node has failed too many times then we should try to repair the + // existing path by replace the bad node with another one + if (updated_failure_counts[snode_it->to_string()] >= snode_failure_threshold) { + try { + // If the node that's gone bad is the guard node then we just have to drop + // the path + if (snode_it == updated_path.nodes.begin()) + throw std::runtime_error{"Cannot recover if guard node is bad"}; + + // Try to find an unused node to patch the path + auto [paths, unused_snodes] = net.call_get( + [this, path_type = info.path_type]() + -> std::pair< + std::vector, + std::vector> { + return {paths_for_type(path_type), snode_pool}; + }); + std::vector path_nodes; + + for (const auto& path : paths) + path_nodes.insert( + path_nodes.end(), path.nodes.begin(), path.nodes.end()); + + unused_snodes.erase( + std::remove_if( + unused_snodes.begin(), + unused_snodes.end(), + [&](const service_node& node) { + return std::find( + path_nodes.begin(), + path_nodes.end(), + node) != path_nodes.end(); + }), + unused_snodes.end()); + + if (unused_snodes.empty()) + throw std::runtime_error{"No remaining nodes"}; + + CSRNG rng; + std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); + + std::replace( + updated_path.nodes.begin(), + updated_path.nodes.end(), + *snode_it, + unused_snodes.front()); + log::info( + cat, + "Found bad node in path for {}, replacing node.", + path_type_name(info.path_type, single_path_mode)); + } catch (...) { + // There aren't enough unused nodes remaining so we need to drop the path + updated_path.failure_count = path_failure_threshold; + + log::info( + cat, + "Unable to replace bad node in path for {}.", + path_type_name(info.path_type, single_path_mode)); + } + } } } } @@ -2344,7 +2449,7 @@ void Network::handle_errors( log::info( cat, "Dropping path for {}: [{}]", - path_type_name(path_type), + path_type_name(path_type, single_path_mode), path_description); else { // If the path was already dropped then the snode pool would have already been @@ -2354,7 +2459,7 @@ void Network::handle_errors( log::info( cat, "Path already dropped for {}: [{}]", - path_type_name(path_type), + path_type_name(path_type, single_path_mode), path_description); } } else { @@ -2499,6 +2604,7 @@ LIBSESSION_C_API bool network_init( network_object** network, const char* cache_path_, bool use_testnet, + bool single_path_mode, bool pre_build_paths, char* error) { try { @@ -2507,7 +2613,7 @@ LIBSESSION_C_API bool network_init( cache_path = cache_path_; auto n = std::make_unique( - cache_path, use_testnet, pre_build_paths); + cache_path, use_testnet, single_path_mode, pre_build_paths); auto n_object = std::make_unique(); n_object->internals = n.release(); @@ -2813,6 +2919,7 @@ LIBSESSION_C_API void network_download_from_server( LIBSESSION_C_API void network_get_client_version( network_object* network, CLIENT_PLATFORM platform, + const unsigned char* ed25519_secret, int64_t timeout_ms, network_onion_response_callback_t callback, void* ctx) { @@ -2821,6 +2928,7 @@ LIBSESSION_C_API void network_get_client_version( try { unbox(network).get_client_version( static_cast(platform), + onionreq::ed25519_seckey::from_bytes({ed25519_secret, 64}), std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 0b347735..aa9bb89a 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -271,6 +271,26 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); } +TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { + REQUIRE(sodium_init() >= 0); + + auto [pubkey, seckey] = blind_version_key_pair(to_usv(seed1)); + CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == + "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); + CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == + "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f6988e8adb27e7b8ce776fcc25b" + "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); +} + +TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { + REQUIRE(sodium_init() >= 0); + + auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); + CHECK(oxenc::to_hex(signature.begin(), signature.end()) == + "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e920fa57daf4627c68f43fcbddb" + "2d465d5ea11def523f3befb2bbee39c769676305"); +} + TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { std::array server_pks = { "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 0d673c75..6c3ee422 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -25,8 +25,14 @@ class TestNetwork { public: Network network; - TestNetwork(std::optional cache_path, bool use_testnet, bool pre_build_paths) : - network(cache_path, use_testnet, pre_build_paths) {} + TestNetwork( + std::optional cache_path, + bool use_testnet, + bool single_path_mode, + bool pre_build_paths) : + network(cache_path, use_testnet, single_path_mode, pre_build_paths) {} + + void set_snode_pool(std::vector pool) { network.snode_pool = pool; } void set_paths(PathType path_type, std::vector paths) { switch (path_type) { @@ -116,6 +122,7 @@ TEST_CASE("Network error handling", "[network]") { auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; auto target3 = service_node{ed_pk2, "0.0.0.2", uint16_t{2}}; + auto target4 = service_node{ed_pk2, "0.0.0.3", uint16_t{3}}; auto path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", @@ -130,7 +137,7 @@ TEST_CASE("Network error handling", "[network]") { true, std::nullopt}; Result result; - auto network = TestNetwork(std::nullopt, true, false); + auto network = TestNetwork(std::nullopt, true, true, false); // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; @@ -287,6 +294,7 @@ TEST_CASE("Network error handling", "[network]") { 0ms, true, std::nullopt}; + network.set_snode_pool({target, target2, target3, target4}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); @@ -498,7 +506,7 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, "144.76.164.202", uint16_t{35400}}; - auto network = Network(std::nullopt, true, false); + auto network = Network(std::nullopt, true, true, false); std::promise result_promise; network.send_onion_request( @@ -534,7 +542,7 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { TEST_CASE("Network direct request C API", "[network_send_request][network]") { network_object* network; - network_init(&network, nullptr, true, false, nullptr); + network_init(&network, nullptr, true, true, false, nullptr); std::array target_ip = {144, 76, 164, 202}; auto test_service_node = network_service_node{}; test_service_node.quic_port = 35400; From fdfd9dbb698e4ce6e6e08dd1b85eec428ab2077e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 18 Jun 2024 14:39:25 +1000 Subject: [PATCH 303/572] Added a bunch of trace logs to help better debug network issues --- include/session/network.hpp | 15 ++- src/network.cpp | 235 ++++++++++++++++++++++++++++-------- 2 files changed, 197 insertions(+), 53 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index b0a023aa..4fd04c96 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -322,6 +322,7 @@ class Network { /// closed in case it fails to establish) before returning the connection. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - 'path_type' - [in] the type of paths to retrieve. /// - `target` -- [in] the target service node to connect to. /// @@ -329,7 +330,7 @@ class Network { /// - A pair of the connection info for the target service node and an optional error if the /// connnection was unable to be established. std::pair> get_connection_info( - PathType path_type, service_node target); + std::string request_id, PathType path_type, service_node target); /// API: network/with_paths_and_pool /// @@ -337,12 +338,14 @@ class Network { /// cache is empty it will first be populated from the network. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - `path_type` -- [in] the type of path the request should be sent along. /// - `excluded_node` -- [in, optional] node which should not be included in the path. /// - `callback` -- [in] callback to be triggered once we have built the paths and service node /// pool. NOTE: If we are unable to build the paths or retrieve the service node pool the /// callback will be triggered with empty lists and an error. void with_paths_and_pool( + std::string request_id, PathType path_type, std::optional excluded_node, std::function< @@ -357,11 +360,13 @@ class Network { /// service nodes in the snode pool. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - `path_type` -- [in] the type of path the request should be sent along. /// - `excluded_node` -- [in, optional] node which should not be included in the path. /// - `callback` -- [in] callback to be triggered once we have a valid path, NULL if we are /// unable to find a valid path. void with_path( + std::string request_id, PathType path_type, std::optional excluded_node, std::function path, std::optional error)> @@ -430,6 +435,7 @@ class Network { /// successfully retrieves nodes or the list is drained. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's /// drained. /// - `limit` -- [in, optional] the number of service nodes to retrieve. @@ -437,6 +443,7 @@ class Network { /// `target_nodes` and haven't gotten a successful response then the callback will be invoked /// with an empty vector and an error string. void get_service_nodes_recursive( + std::string request_id, std::vector target_nodes, std::optional limit, std::function nodes, std::optional error)> @@ -448,6 +455,7 @@ class Network { /// received or the list is drained. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - `path_type` -- [in] the type of path to validate the size for. /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's /// drained. @@ -455,6 +463,7 @@ class Network { /// we drain the `target_nodes` and haven't gotten a successful response then the callback will /// be invoked with a std::nullopt `valid_guard_node` and `unused_nodes`. void find_valid_guard_node_recursive( + std::string request_id, PathType path_type, std::vector target_nodes, std::function< @@ -467,11 +476,13 @@ class Network { /// Retrieves all or a random subset of service nodes from the given node. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - `node` -- [in] node to retrieve the service nodes from. /// - `limit` -- [in, optional] the number of service nodes to retrieve. /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If an error /// occurs an empty list and an error will be provided. void get_service_nodes( + std::string request_id, service_node node, std::optional limit, std::function nodes, std::optional error)> @@ -482,6 +493,7 @@ class Network { /// Retrieves the version information for a given service node. /// /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. /// - 'type' - [in] the type of paths to send the request across. /// - `node` -- [in] node to retrieve the version from. /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the @@ -489,6 +501,7 @@ class Network { /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an /// error occurs an empty list and an error will be provided. void get_version( + std::string request_id, PathType path_type, service_node node, std::optional timeout, diff --git a/src/network.cpp b/src/network.cpp index 0c85c133..ca1990d8 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -83,7 +83,7 @@ namespace { std::string path_type_name(PathType path_type, bool single_path_mode) { if (single_path_mode) - "single_path"; + return "single_path"; switch (path_type) { case PathType::standard: return "standard"; @@ -277,6 +277,7 @@ Network::Network( std::thread build_paths_thread( &Network::with_paths_and_pool, this, + "Constructor", PathType::standard, std::nullopt, [](std::vector, std::vector, std::optional) { @@ -594,7 +595,8 @@ std::shared_ptr Network::get_endpoint() { } std::pair> Network::get_connection_info( - PathType path_type, service_node target) { + std::string request_id, PathType path_type, service_node target) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); // If the network is currently suspended then don't try to open a connection @@ -614,7 +616,11 @@ std::pair> Network::get_connection_i target, creds, quic::opt::keep_alive{10s}, - [mutex, cv, connection_established, done, cb_called](quic::connection_interface&) { + [mutex, cv, connection_established, done, request_id, cb_called]( + quic::connection_interface&) { + log::trace( + cat, "{} connection established for {}.", __PRETTY_FUNCTION__, request_id); + if (cb_called) std::call_once(*cb_called, [&]() { { @@ -625,8 +631,10 @@ std::pair> Network::get_connection_i cv->notify_one(); }); }, - [this, path_type, target, mutex, cv, done, cb_called]( + [this, path_type, target, mutex, cv, done, request_id, cb_called]( quic::connection_interface& conn, uint64_t error_code) { + log::trace(cat, "{} connection closed for {}.", __PRETTY_FUNCTION__, request_id); + // Trigger the callback first before updating the paths in case this was triggered // when try to establish a connection if (cb_called) { @@ -683,12 +691,14 @@ using paths_and_pool_info = std::tuple< bool>; void Network::with_paths_and_pool( + std::string request_id, PathType path_type, std::optional excluded_node, std::function< void(std::vector updated_paths, std::vector pool, std::optional error)> callback) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); auto [current_paths, pool, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { return {paths_for_type(path_type), snode_pool, last_snode_pool_update, suspended}; @@ -703,11 +713,17 @@ void Network::with_paths_and_pool( auto [paths_valid, pool_valid] = validate_paths_and_pool_sizes(path_type, current_valid_paths, pool, last_pool_update); - if (paths_valid && pool_valid) + if (paths_valid && pool_valid) { + log::trace( + cat, + "{} returning valid cached paths and pool for {}.", + __PRETTY_FUNCTION__, + request_id); return callback(current_valid_paths, pool, std::nullopt); + } auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get( - [this, path_type, excluded_node]() mutable -> paths_and_pool_result { + [this, path_type, request_id, excluded_node]() mutable -> paths_and_pool_result { auto [current_paths, pool, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { return {paths_for_type(path_type), @@ -725,8 +741,14 @@ void Network::with_paths_and_pool( auto [paths_valid, pool_valid] = validate_paths_and_pool_sizes( path_type, current_valid_paths, pool, last_pool_update); - if (paths_valid && pool_valid) + if (paths_valid && pool_valid) { + log::trace( + cat, + "{} whithin loop cache has already been updated for {}.", + __PRETTY_FUNCTION__, + request_id); return {current_valid_paths, pool, std::nullopt}; + } // Update the network status if (path_type == PathType::standard) @@ -739,7 +761,10 @@ void Network::with_paths_and_pool( // Populate the snode pool if needed if (!pool_valid) { - log::info(cat, "Snode pool cache no longer valid, need to refetch."); + log::info( + cat, + "Snode pool cache no longer valid for {}, need to refetch.", + request_id); // Define the response handler to avoid code duplication auto handle_nodes_response = @@ -761,7 +786,7 @@ void Network::with_paths_and_pool( // If we don't have enough nodes in the current cached pool then we need to // fetch from the seed nodes if (pool_result.size() < min_snode_pool_count) { - log::info(cat, "Fetching from seed nodes."); + log::info(cat, "Fetching from seed nodes for {}.", request_id); pool_result = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); // Just in case, make sure the seed nodes are have values @@ -773,6 +798,7 @@ void Network::with_paths_and_pool( std::future> prom_future = prom.get_future(); get_service_nodes( + request_id, pool_result.front(), 256, handle_nodes_response(std::move(prom))); @@ -780,7 +806,8 @@ void Network::with_paths_and_pool( // We want to block the `get_snode_pool_loop` until we have retrieved // the snode pool so we don't double up on requests pool_result = prom_future.get(); - log::info(cat, "Retrieved snode pool from seed node."); + log::info( + cat, "Retrieved snode pool from seed node for {}.", request_id); } else { // Pick ~9 random snodes from the current cache to fetch nodes from (we // want to fetch from 3 snodes and retry up to 3 times if needed) @@ -788,7 +815,10 @@ void Network::with_paths_and_pool( size_t num_retries = std::min(pool_result.size() / 3, static_cast(3)); - log::info(cat, "Fetching from random expired cache nodes."); + log::info( + cat, + "Fetching from random expired cache nodes for {}.", + request_id); std::vector nodes1( pool_result.begin(), pool_result.begin() + num_retries); std::vector nodes2( @@ -809,11 +839,20 @@ void Network::with_paths_and_pool( // Kick off 3 concurrent requests get_service_nodes_recursive( - nodes1, std::nullopt, handle_nodes_response(std::move(prom1))); + "{}-1"_format(request_id), + nodes1, + std::nullopt, + handle_nodes_response(std::move(prom1))); get_service_nodes_recursive( - nodes2, std::nullopt, handle_nodes_response(std::move(prom2))); + "{}-2"_format(request_id), + nodes2, + std::nullopt, + handle_nodes_response(std::move(prom2))); get_service_nodes_recursive( - nodes3, std::nullopt, handle_nodes_response(std::move(prom3))); + "{}-3"_format(request_id), + nodes3, + std::nullopt, + handle_nodes_response(std::move(prom3))); // We want to block the `get_snode_pool_loop` until we have retrieved // the snode pool so we don't double up on requests @@ -854,10 +893,10 @@ void Network::with_paths_and_pool( auto size = std::min(256, static_cast(intersection.size())); pool_result = std::vector( intersection.begin(), intersection.begin() + size); - log::info(cat, "Retrieved snode pool."); + log::info(cat, "Retrieved snode pool for {}.", request_id); } } catch (const std::exception& e) { - log::info(cat, "Failed to get snode pool: {}", e.what()); + log::info(cat, "Failed to get snode pool for {}: {}", request_id, e.what()); return {{}, {}, e.what()}; } } @@ -868,8 +907,9 @@ void Network::with_paths_and_pool( // Get the possible guard nodes log::info( cat, - "Building paths for {}.", - path_type_name(path_type, single_path_mode)); + "Building paths of type {} for {}.", + path_type_name(path_type, single_path_mode), + request_id); std::vector nodes_to_exclude; std::vector possible_guard_nodes; @@ -939,6 +979,7 @@ void Network::with_paths_and_pool( std::move(guard_node_prom)); find_valid_guard_node_recursive( + request_id, path_type, nodes_to_test[i], [prom](std::optional valid_guard_node, @@ -1004,15 +1045,17 @@ void Network::with_paths_and_pool( auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); log::info( cat, - "Built new onion request path for {}: [{}]", + "Built new onion request path of type {} for {}: [{}]", path_type_name(path_type, single_path_mode), + request_id, path_description); } } catch (const std::exception& e) { log::info( cat, - "Unable to build paths for {} due to error: {}", + "Unable to build paths of type {} for {} due to error: {}", path_type_name(path_type, single_path_mode), + request_id, e.what()); return {{}, {}, e.what()}; } @@ -1091,10 +1134,12 @@ std::pair Network::validate_paths_and_pool_sizes( } void Network::with_path( + std::string request_id, PathType path_type, std::optional excluded_node, std::function path, std::optional error)> callback) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); auto [current_paths, currently_suspended] = net.call_get([this, path_type]() -> std::pair, bool> { return {paths_for_type(path_type), suspended}; @@ -1111,10 +1156,15 @@ void Network::with_path( // The path doesn't have a valid connection so we should try to reconnect (we will end // up updating the `paths` value so should do this in a blocking way) if (target_path && !target_path->conn_info.is_valid()) { + log::trace( + cat, + "{} found invalid connection for {}, will try to recover.", + __PRETTY_FUNCTION__, + request_id); + path_info = paths_and_pool_loop->call_get( - [this, - path_type, - path = *target_path]() mutable -> std::pair, uint8_t> { + [this, path_type, request_id, path = *target_path]() mutable + -> std::pair, uint8_t> { // Since this may have been blocked by another thread we should start by // making sure the target path is still one of the current paths auto current_paths = @@ -1125,23 +1175,37 @@ void Network::with_path( std::find(current_paths.begin(), current_paths.end(), path); // If we didn't find the path then don't bother continuing - if (target_path_it == current_paths.end()) + if (target_path_it == current_paths.end()) { + log::trace( + cat, + "{} path with invalid connection for {} no longer exists.", + __PRETTY_FUNCTION__, + request_id); return {std::nullopt, current_paths.size()}; + } // It's possible that multiple requests were queued up waiting on the connection // the be reestablished so check to see if the path is now valid and return it // if it is - if (target_path_it->conn_info.is_valid()) + if (target_path_it->conn_info.is_valid()) { + log::trace( + cat, + "{} connection to {} for {} has already been recovered.", + __PRETTY_FUNCTION__, + target_path_it->nodes[0], + request_id); return {*target_path_it, current_paths.size()}; + } // Try to retrieve a valid connection for the guard node log::info( cat, - "Connection to {} for {} path no longer valid, attempting " + "Connection to {} with type {} for {} path no longer valid, attempting " "reconnection.", target_path_it->nodes[0], - path_type_name(path_type, single_path_mode)); - auto [info, error] = get_connection_info(path_type, path.nodes[0]); + path_type_name(path_type, single_path_mode), + request_id); + auto [info, error] = get_connection_info(request_id, path_type, path.nodes[0]); // It's possible that the connection was created successfully, and reported as // valid, but isn't actually valid (eg. it was shutdown immediately due to the @@ -1150,9 +1214,11 @@ void Network::with_path( if (!info.is_valid()) { log::info( cat, - "Reconnection to {} for {} path failed with error: {}.", + "Reconnection to {} with type {} for {} path failed with error: " + "{}.", target_path_it->nodes[0], path_type_name(path_type, single_path_mode), + request_id, error.value_or("Unknown error.")); return {std::nullopt, current_paths.size()}; } @@ -1160,9 +1226,10 @@ void Network::with_path( // Knowing that the reconnection succeeded is helpful for debugging log::info( cat, - "Reconnection to {} for {} path successful.", + "Reconnection to {} with type {} for {} path successful.", target_path_it->nodes[0], - path_type_name(path_type, single_path_mode)); + path_type_name(path_type, single_path_mode), + request_id); // If the connection info is valid and it's a standard path then update the // connection status back to connected @@ -1206,8 +1273,10 @@ void Network::with_path( } // If we didn't get a target path then we have to build paths - if (!target_path) + if (!target_path) { + log::trace(cat, "{} no path found for {}.", __PRETTY_FUNCTION__, request_id); return with_paths_and_pool( + request_id, path_type, excluded_node, [this, excluded_node, cb = std::move(callback)]( @@ -1225,12 +1294,22 @@ void Network::with_path( cb(*target_path, std::nullopt); }); + } // Build additional paths in the background if we don't have enough if (paths_count < target_path_count(path_type, single_path_mode)) { + auto new_request_id = random::random_base32(4); + log::trace( + cat, + "{} found path, but we don't have the desired number so starting a background path " + "build from {} with new id: {}.", + __PRETTY_FUNCTION__, + request_id, + new_request_id); std::thread build_additional_paths_thread( &Network::with_paths_and_pool, this, + new_request_id, path_type, std::nullopt, [](std::optional>, @@ -1280,6 +1359,7 @@ std::pair, uint8_t> Network::find_possible_path( // MARK: Multi-request logic void Network::get_service_nodes_recursive( + std::string request_id, std::vector target_nodes, std::optional limit, std::function nodes, std::optional error)> @@ -1289,9 +1369,10 @@ void Network::get_service_nodes_recursive( auto target_node = target_nodes.front(); get_service_nodes( + request_id, target_node, limit, - [this, limit, target_nodes, cb = std::move(callback)]( + [this, limit, target_nodes, request_id, cb = std::move(callback)]( std::vector nodes, std::optional error) { // If we got nodes then stop looping and return them if (!nodes.empty()) @@ -1300,17 +1381,19 @@ void Network::get_service_nodes_recursive( // Loop if we didn't get any nodes std::vector remaining_nodes( target_nodes.begin() + 1, target_nodes.end()); - get_service_nodes_recursive(remaining_nodes, limit, cb); + get_service_nodes_recursive(request_id, remaining_nodes, limit, cb); }); } void Network::find_valid_guard_node_recursive( + std::string request_id, PathType path_type, std::vector target_nodes, std::function< void(std::optional valid_guard_node, std::vector unused_nodes, std::optional)> callback) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); if (target_nodes.empty()) return callback(std::nullopt, {}, "Failed to find valid guard node."); @@ -1320,16 +1403,18 @@ void Network::find_valid_guard_node_recursive( return callback(std::nullopt, {}, "Network is suspended"); auto target_node = target_nodes.front(); - log::info(cat, "Testing guard snode: {}", target_node.to_string()); + log::info(cat, "Testing guard snode: {} for {}", target_node.to_string(), request_id); get_version( + request_id, path_type, target_node, 3s, - [this, path_type, target_node, target_nodes, cb = std::move(callback)]( + [this, path_type, target_node, target_nodes, request_id, cb = std::move(callback)]( std::vector version, connection_info info, std::optional error) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); std::vector remaining_nodes( target_nodes.begin() + 1, target_nodes.end()); @@ -1344,20 +1429,26 @@ void Network::find_valid_guard_node_recursive( throw std::runtime_error{ "Outdated node version ({})"_format(fmt::join(version, "."))}; - log::info(cat, "Guard snode {} valid.", target_node.to_string()); + log::info( + cat, + "Guard snode {} valid for {}.", + target_node.to_string(), + request_id); cb(info, remaining_nodes, std::nullopt); } catch (const std::exception& e) { // Log the error and loop after a slight delay (don't want to drain the pool // too quickly if the network goes down) log::info( cat, - "Testing {} failed with error: {}", + "Testing {} for {} failed with error: {}", target_node.to_string(), + request_id, e.what()); std::thread retry_thread( - [this, path_type, remaining_nodes, cb = std::move(cb)] { + [this, path_type, remaining_nodes, request_id, cb = std::move(cb)] { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - find_valid_guard_node_recursive(path_type, remaining_nodes, cb); + find_valid_guard_node_recursive( + request_id, path_type, remaining_nodes, cb); }); retry_thread.detach(); } @@ -1367,11 +1458,13 @@ void Network::find_valid_guard_node_recursive( // MARK: Pre-Defined Requests void Network::get_service_nodes( + std::string request_id, service_node node, std::optional limit, std::function nodes, std::optional error)> callback) { - auto [info, error] = get_connection_info(PathType::standard, node); + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); + auto [info, error] = get_connection_info(request_id, PathType::standard, node); if (!info.is_valid()) return callback({}, error.value_or("Unknown error.")); @@ -1389,7 +1482,10 @@ void Network::get_service_nodes( payload.append("params", params.dump()); info.stream->command( - "oxend_request", payload.view(), [this, cb = std::move(callback)](quic::message resp) { + "oxend_request", + payload.view(), + [this, request_id, cb = std::move(callback)](quic::message resp) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); try { auto [status_code, body] = validate_response(resp, true); @@ -1422,13 +1518,15 @@ void Network::get_service_nodes( } void Network::get_version( + std::string request_id, PathType path_type, service_node node, std::optional timeout, std::function version, connection_info info, std::optional error)> callback) { - auto [info, error] = get_connection_info(path_type, node); + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); + auto [info, error] = get_connection_info(request_id, path_type, node); if (!info.is_valid()) return callback({}, info, error.value_or("Unknown error.")); @@ -1438,7 +1536,8 @@ void Network::get_version( "info", payload.view(), timeout, - [this, info, cb = std::move(callback)](quic::message resp) { + [this, info, request_id, cb = std::move(callback)](quic::message resp) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); try { auto [status_code, body] = validate_response(resp, true); @@ -1464,6 +1563,8 @@ void Network::get_version( void Network::get_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::function swarm)> callback) { + auto request_id = random::random_base32(4); + log::trace(cat, "{} called for {} as {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex(), request_id); auto cached_swarm = net.call_get([this, swarm_pubkey]() -> std::optional> { if (!swarm_cache.contains(swarm_pubkey.hex())) @@ -1476,12 +1577,17 @@ void Network::get_swarm( return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from - log::info(cat, "No cached swarm for {}, fetching from random node.", swarm_pubkey.hex()); + log::info( + cat, + "No cached swarm for {} as {}, fetching from random node.", + swarm_pubkey.hex(), + request_id); with_paths_and_pool( + request_id, PathType::standard, std::nullopt, - [this, swarm_pubkey, cb = std::move(callback)]( + [this, swarm_pubkey, request_id, cb = std::move(callback)]( std::vector, std::vector pool, std::optional) { @@ -1507,11 +1613,17 @@ void Network::get_swarm( quic::DEFAULT_TIMEOUT, std::nullopt, std::nullopt, - [this, swarm_pubkey, cb = std::move(cb)]( + [this, swarm_pubkey, request_id, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { + log::trace( + cat, + "{} got response for {} as {}.", + __PRETTY_FUNCTION__, + swarm_pubkey.hex(), + request_id); if (!success || timeout || !response) return cb({}); @@ -1561,13 +1673,17 @@ void Network::set_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { + auto request_id = random::random_base32(4); + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); with_paths_and_pool( + request_id, PathType::standard, std::nullopt, - [count, cb = std::move(callback)]( + [count, request_id, cb = std::move(callback)]( std::vector, std::vector pool, std::optional) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); if (pool.size() < count) return cb({}); @@ -1584,6 +1700,7 @@ void Network::get_random_nodes( void Network::send_request( request_info info, connection_info conn_info, network_response_callback_t handle_response) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, info.request_id); if (!conn_info.is_valid()) return handle_response(false, false, -1, "Network is unreachable."); @@ -1597,6 +1714,7 @@ void Network::send_request( payload, info.timeout, [this, info, cb = std::move(handle_response)](quic::message resp) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); try { auto [status_code, body] = validate_response(resp, false); cb(true, false, status_code, body); @@ -1617,7 +1735,10 @@ void Network::send_onion_request( std::optional existing_request_id, std::optional retry_reason, network_response_callback_t handle_response) { + auto request_id = existing_request_id.value_or(random::random_base32(4)); + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); with_path( + request_id, path_type, node_for_destination(destination), [this, @@ -1626,10 +1747,11 @@ void Network::send_onion_request( body, swarm_pubkey, timeout, - existing_request_id, + request_id, retry_reason, cb = std::move(handle_response)]( std::optional path, std::optional error) { + log::trace(cat, "{} got path for {}.", __PRETTY_FUNCTION__, request_id); if (!path) return cb(false, false, -1, error.value_or("No valid onion paths.")); @@ -1648,7 +1770,7 @@ void Network::send_onion_request( auto onion_req_payload = builder.build(payload); request_info info{ - existing_request_id.value_or(random::random_base32(4)), + request_id, path->nodes[0], "onion_req", onion_req_payload, @@ -1672,6 +1794,12 @@ void Network::send_onion_request( bool timeout, int16_t status_code, std::optional response) { + log::trace( + cat, + "{} got response for {}.", + __PRETTY_FUNCTION__, + info.request_id); + // If the request was reported as a failure or a timeout then we // will have already handled the errors so just trigger the callback if (!success || timeout) @@ -2393,6 +2521,7 @@ void Network::handle_errors( bool done = false; net.call([this, + request_id = info.request_id, path_type = info.path_type, target_node = info.target, swarm_pubkey = info.swarm_pubkey, @@ -2448,8 +2577,9 @@ void Network::handle_errors( if (new_paths_size != old_paths_size) log::info( cat, - "Dropping path for {}: [{}]", + "Dropping path of type {} for {}: [{}]", path_type_name(path_type, single_path_mode), + request_id, path_description); else { // If the path was already dropped then the snode pool would have already been @@ -2458,8 +2588,9 @@ void Network::handle_errors( already_handled_failure = true; log::info( cat, - "Path already dropped for {}: [{}]", + "Path of type: {} already dropped for {}: [{}]", path_type_name(path_type, single_path_mode), + request_id, path_description); } } else { From f0772222699640d67b4c413eda2b320bfab1dcd3 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 19 Jun 2024 13:48:25 +1000 Subject: [PATCH 304/572] fix: build for msvc --- include/session/config/groups/members.h | 16 +++++++++++++--- src/CMakeLists.txt | 12 +++++++++--- src/config/groups/keys.cpp | 2 +- src/hash.cpp | 2 +- src/session_encrypt.cpp | 2 +- tests/CMakeLists.txt | 12 +++++++++--- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index a4624b7d..5ecb2ebe 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -55,7 +55,11 @@ LIBSESSION_EXPORT int groups_members_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) +#ifdef __GNUC__ + __attribute__((warn_unused_result)) +#endif + ; /// API: groups/groups_members_get /// @@ -72,7 +76,10 @@ LIBSESSION_EXPORT int groups_members_init( /// - `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)); +#ifdef __GNUC__ + __attribute__((warn_unused_result)) +#endif + ; /// API: groups/groups_members_get_or_construct /// @@ -95,7 +102,10 @@ LIBSESSION_EXPORT bool groups_members_get( /// 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)); +#ifdef __GNUC__ + __attribute__((warn_unused_result)) +#endif + ; /// API: groups/groups_members_set /// diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c3f1158..b2ee4cfd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,16 +45,22 @@ if(NOT BUILD_STATIC_DEPS) endif() -add_libsession_util_library(crypto +SET(LIBSESSION_UTIL_SOURCE_FILES blinding.cpp curve25519.cpp ed25519.cpp - hash.cpp multi_encrypt.cpp random.cpp session_encrypt.cpp util.cpp - xed25519.cpp + xed25519.cpp ) + +if (NOT MSVC) + list(APPEND LIBSESSION_UTIL_SOURCE_FILES hash.cpp) +endif() + + +add_libsession_util_library(crypto ${LIBSESSION_UTIL_SOURCE_FILES} ) add_libsession_util_library(config diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 31743728..a5cbf596 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1286,7 +1286,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c // // Removing any null padding bytes from the end // - if (auto pos = plain.find_last_not_of('\0'); pos != std::string::npos) + if (auto pos = plain.find_last_not_of((unsigned char) 0); pos != std::string::npos) plain.resize(pos + 1); // diff --git a/src/hash.cpp b/src/hash.cpp index fd16949f..d2b3eec3 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -31,7 +31,7 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke if (result_code != 0) throw std::runtime_error{"Hash generation failed"}; - return {result, size}; + return to_unsigned(static_cast(result)); } } // namespace session::hash diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 88bc4e7a..87d043b0 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -584,7 +584,7 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; // Removing any null padding bytes from the end - if (auto pos = buf.find_last_not_of('\0'); pos != std::string::npos) + if (auto pos = buf.find_last_not_of((unsigned char)0); pos != std::string::npos) buf.resize(pos + 1); return buf; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a6a1d127..1298da92 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(Catch2) -add_executable(testAll + +set(SOURCE_FILES test_blinding.cpp test_bt_merge.cpp test_bugs.cpp @@ -16,14 +17,19 @@ add_executable(testAll test_group_keys.cpp test_group_info.cpp test_group_members.cpp - test_hash.cpp test_multi_encrypt.cpp test_onionreq.cpp test_proto.cpp test_random.cpp test_session_encrypt.cpp test_xed25519.cpp - ) +) + +if (NOT MSVC) + list(APPEND SOURCE_FILES test_hash.cpp) +endif() + +add_executable(testAll ${SOURCE_FILES}) target_link_libraries(testAll PRIVATE libsession::config From 926d05f7e0682661b30b073790f03440bbf2c207 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 19 Jun 2024 22:26:59 -0300 Subject: [PATCH 305/572] Clean up hash function - don't worry about checking the return value; blake2b hashing always returns 0. - Change VLA usage to a single ustring allocation that we write into directly (this is likely what MSVC was unhappy with). - De-duplicate the `crypto_generichash_blake2b` call (by using a ternary on the differing key arguments). - Removed some no-op `static_cast`s --- include/session/hash.hpp | 5 +++-- src/config/groups/keys.cpp | 2 +- src/hash.cpp | 31 ++++++++++++------------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 231a65aa..f9dce034 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -13,10 +13,11 @@ namespace session::hash { /// Inputs: /// - `size` -- length of the hash to be generated. /// - `msg` -- the message to generate a hash for. -/// - `key` -- an optional key to be used when generating the hash. +/// - `key` -- an optional key to be used when generating the hash. Can be omitted or an empty +/// string for an unkeyed hash. /// /// Outputs: /// - a `size` byte hash. -ustring hash(const size_t size, ustring_view msg, std::optional key); +ustring hash(const size_t size, ustring_view msg, std::optional key = std::nullopt); } // namespace session::hash diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a5cbf596..0fafaf87 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1286,7 +1286,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c // // Removing any null padding bytes from the end // - if (auto pos = plain.find_last_not_of((unsigned char) 0); pos != std::string::npos) + if (auto pos = plain.find_last_not_of(0); pos != std::string::npos) plain.resize(pos + 1); // diff --git a/src/hash.cpp b/src/hash.cpp index d2b3eec3..09968b57 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -11,27 +11,20 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; - if (key && static_cast(*key).size() > crypto_generichash_blake2b_BYTES_MAX) + if (key && key->size() > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; - auto result_code = 0; - unsigned char result[size]; - - if (key) - result_code = crypto_generichash_blake2b( - result, - size, - msg.data(), - msg.size(), - static_cast(*key).data(), - static_cast(*key).size()); - else - result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); - - if (result_code != 0) - throw std::runtime_error{"Hash generation failed"}; - - return to_unsigned(static_cast(result)); + ustring result; + result.resize(size); + crypto_generichash_blake2b( + result.data(), + size, + msg.data(), + msg.size(), + key ? key->data() : nullptr, + key ? key->size() : 0); + + return result; } } // namespace session::hash From b6b48417a91f2b1074126b82d76d88be42bcea61 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 20 Jun 2024 14:00:53 +1000 Subject: [PATCH 306/572] chore: replaced __attribute__((warn_unused_result)) with macro --- include/session/config/base.h | 11 ++---- include/session/config/contacts.h | 12 ++++--- include/session/config/convo_info_volatile.h | 36 ++++++++++++-------- include/session/config/groups/info.h | 2 +- include/session/config/groups/keys.h | 17 +++++---- include/session/config/groups/members.h | 22 ++++-------- include/session/config/user_groups.h | 10 +++--- include/session/config/user_profile.h | 6 +--- include/session/export.h | 6 ++++ 9 files changed, 58 insertions(+), 64 deletions(-) diff --git a/include/session/config/base.h b/include/session/config/base.h index 8707d903..dbb9801f 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -136,11 +136,7 @@ LIBSESSION_EXPORT config_string_list* config_merge( const char** msg_hashes, const unsigned char** configs, const size_t* lengths, - size_t count) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; + size_t count) LIBSESSION_WARN_UNUSED; /// API: base/config_needs_push /// @@ -290,10 +286,7 @@ LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf); /// 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 - ; + LIBSESSION_WARN_UNUSED; /// API: base/config_get_keys /// diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 7c6bc0e4..ee863074 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -74,7 +74,7 @@ LIBSESSION_EXPORT int contacts_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: contacts/contacts_get /// @@ -99,8 +99,9 @@ LIBSESSION_EXPORT int contacts_init( /// 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)); + config_object* conf, + contacts_contact* contact, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: contacts/contacts_get_or_construct /// @@ -130,8 +131,9 @@ LIBSESSION_EXPORT bool contacts_get( /// 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)); + config_object* conf, + contacts_contact* contact, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: contacts/contacts_set /// diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 6ddba3eb..eacecdb9 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -80,7 +80,7 @@ LIBSESSION_EXPORT int convo_info_volatile_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_1to1 /// @@ -107,8 +107,9 @@ LIBSESSION_EXPORT int convo_info_volatile_init( /// 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)); + config_object* conf, + convo_info_volatile_1to1* convo, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_or_construct_1to1 /// @@ -139,8 +140,9 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_1to1( /// 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)); + config_object* conf, + convo_info_volatile_1to1* convo, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_community /// @@ -173,7 +175,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_community( config_object* conf, convo_info_volatile_community* comm, const char* base_url, - const char* room) __attribute__((warn_unused_result)); + const char* room) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_or_construct_community /// @@ -218,7 +220,7 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( convo_info_volatile_community* convo, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_group /// @@ -245,8 +247,9 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( /// 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) - __attribute__((warn_unused_result)); + config_object* conf, + convo_info_volatile_group* convo, + const char* id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_or_construct_group /// @@ -277,8 +280,9 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_group( /// 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) - __attribute__((warn_unused_result)); + config_object* conf, + convo_info_volatile_group* convo, + const char* id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_legacy_group /// @@ -304,8 +308,9 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_group( /// 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)); + config_object* conf, + convo_info_volatile_legacy_group* convo, + const char* id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_get_or_construct_legacy_group /// @@ -336,8 +341,9 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_legacy_group( /// 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)); + config_object* conf, + convo_info_volatile_legacy_group* convo, + const char* id) LIBSESSION_WARN_UNUSED; /// API: convo_info_volatile/convo_info_volatile_set_1to1 /// diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 32da3ae9..04456e52 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -39,7 +39,7 @@ LIBSESSION_EXPORT int groups_info_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: groups_info/groups_info_get_name /// diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 1a1b034f..3a396cbc 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -72,7 +72,7 @@ LIBSESSION_EXPORT int groups_keys_init( config_object* group_members_conf, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_size /// @@ -176,7 +176,7 @@ LIBSESSION_EXPORT bool groups_keys_rekey( config_object* info, config_object* members, const unsigned char** out, - size_t* outlen) __attribute__((warn_unused_result)); + size_t* outlen) LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_pending_config /// @@ -194,8 +194,9 @@ LIBSESSION_EXPORT bool groups_keys_rekey( /// - `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)); + const config_group_keys* conf, + const unsigned char** out, + size_t* outlen) LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_load_message /// @@ -224,7 +225,7 @@ LIBSESSION_EXPORT bool groups_keys_load_message( size_t datalen, int64_t timestamp_ms, config_object* info, - config_object* members) __attribute__((warn_unused_result)); + config_object* members) LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_current_hashes /// @@ -254,8 +255,7 @@ LIBSESSION_EXPORT config_string_list* groups_keys_current_hashes(const config_gr /// Outputs: /// - `bool` -- `true` if `rekey()` needs to be called, `false` otherwise. LIBSESSION_EXPORT bool groups_keys_needs_rekey(const config_group_keys* conf) - __attribute__((warn_unused_result)); - + LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_needs_dump /// /// Checks whether a groups_keys_dump needs to be called to save state. This is analagous to @@ -268,8 +268,7 @@ LIBSESSION_EXPORT bool groups_keys_needs_rekey(const config_group_keys* conf) /// /// Outputs: /// - `bool` -- `true` if a dump is needed, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) LIBSESSION_WARN_UNUSED; /// API: groups/groups_keys_dump /// diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index 5ecb2ebe..ae5d9490 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -55,11 +55,7 @@ LIBSESSION_EXPORT int groups_members_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; + char* error) LIBSESSION_WARN_UNUSED; /// API: groups/groups_members_get /// @@ -75,11 +71,9 @@ LIBSESSION_EXPORT int groups_members_init( /// Output: /// - `bool` -- Returns true if member exists LIBSESSION_EXPORT bool groups_members_get( - config_object* conf, config_group_member* member, const char* session_id) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; + config_object* conf, + config_group_member* member, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: groups/groups_members_get_or_construct /// @@ -101,11 +95,9 @@ LIBSESSION_EXPORT bool groups_members_get( /// - `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) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; + config_object* conf, + config_group_member* member, + const char* session_id) LIBSESSION_WARN_UNUSED; /// API: groups/groups_members_set /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index b17bf2f7..919faa79 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -117,7 +117,7 @@ LIBSESSION_EXPORT int user_groups_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: user_groups/user_groups_get_group /// @@ -180,7 +180,7 @@ LIBSESSION_EXPORT bool user_groups_get_or_construct_group( /// - `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_WARN_UNUSED; /// API: user_groups/user_groups_get_or_construct_community /// @@ -222,7 +222,7 @@ LIBSESSION_EXPORT bool user_groups_get_or_construct_community( ugroups_community_info* comm, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey) LIBSESSION_WARN_UNUSED; /// API: user_groups/user_groups_get_legacy_group /// @@ -248,7 +248,7 @@ LIBSESSION_EXPORT bool user_groups_get_or_construct_community( /// 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)); + config_object* conf, const char* id) LIBSESSION_WARN_UNUSED; /// API: user_groups/user_groups_get_or_construct_legacy_group /// @@ -282,7 +282,7 @@ LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_legacy_group( /// 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)); + config_object* conf, const char* id) LIBSESSION_WARN_UNUSED; /// API: user_groups/ugroups_legacy_group_free /// diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 651f46af..87d2c0ce 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -45,11 +45,7 @@ LIBSESSION_EXPORT int user_profile_init( const unsigned char* ed25519_secretkey, const unsigned char* dump, size_t dumplen, - char* error) -#if defined(__GNUC__) || defined(__clang__) - __attribute__((warn_unused_result)) -#endif - ; + char* error) LIBSESSION_WARN_UNUSED; /// API: user_profile/user_profile_get_name /// diff --git a/include/session/export.h b/include/session/export.h index ab307f6f..892d68f2 100644 --- a/include/session/export.h +++ b/include/session/export.h @@ -6,3 +6,9 @@ #define LIBSESSION_EXPORT __attribute__((visibility("default"))) #endif #define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT + +#ifdef __GNUC__ +#define LIBSESSION_WARN_UNUSED __attribute__((warn_unused_result)) +#else +#define LIBSESSION_WARN_UNUSED +#endif \ No newline at end of file From 7080bffea4593f789fa05554de894c168f26106e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 20 Jun 2024 14:03:37 +1000 Subject: [PATCH 307/572] test: enable back hash.cpp and test_hash.cpp back for msvc builds --- src/CMakeLists.txt | 4 ++-- tests/CMakeLists.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b2ee4cfd..cbd5329d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,9 +55,9 @@ SET(LIBSESSION_UTIL_SOURCE_FILES util.cpp xed25519.cpp ) -if (NOT MSVC) +# if (NOT MSVC) list(APPEND LIBSESSION_UTIL_SOURCE_FILES hash.cpp) -endif() +# endif() add_libsession_util_library(crypto ${LIBSESSION_UTIL_SOURCE_FILES} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1298da92..78c988c3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,9 +25,9 @@ set(SOURCE_FILES test_xed25519.cpp ) -if (NOT MSVC) +# if (NOT MSVC) list(APPEND SOURCE_FILES test_hash.cpp) -endif() +# endif() add_executable(testAll ${SOURCE_FILES}) From 2dfc764b4734ceff175223e2ab47fc0e76d19f60 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 20 Jun 2024 14:21:10 +1000 Subject: [PATCH 308/572] fix: find_last_not_of(0) ambiguous for msvc --- src/config/groups/keys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 0fafaf87..bc1628ea 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1286,7 +1286,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c // // Removing any null padding bytes from the end // - if (auto pos = plain.find_last_not_of(0); pos != std::string::npos) + if (auto pos = plain.find_last_not_of((unsigned char)0); pos != std::string::npos) plain.resize(pos + 1); // From eba372aed799d1464fc914abceb9afaece3b7688 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 20 Jun 2024 14:25:57 +1000 Subject: [PATCH 309/572] chore: skip testing with libsession::onionreq when not building it --- src/CMakeLists.txt | 12 +++--------- tests/CMakeLists.txt | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cbd5329d..6c3f1158 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,22 +45,16 @@ if(NOT BUILD_STATIC_DEPS) endif() -SET(LIBSESSION_UTIL_SOURCE_FILES +add_libsession_util_library(crypto blinding.cpp curve25519.cpp ed25519.cpp + hash.cpp multi_encrypt.cpp random.cpp session_encrypt.cpp util.cpp - xed25519.cpp ) - -# if (NOT MSVC) - list(APPEND LIBSESSION_UTIL_SOURCE_FILES hash.cpp) -# endif() - - -add_libsession_util_library(crypto ${LIBSESSION_UTIL_SOURCE_FILES} + xed25519.cpp ) add_libsession_util_library(config diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 78c988c3..a4c9fa65 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,7 +1,6 @@ add_subdirectory(Catch2) - -set(SOURCE_FILES +add_executable(testAll test_blinding.cpp test_bt_merge.cpp test_bugs.cpp @@ -17,6 +16,7 @@ set(SOURCE_FILES test_group_keys.cpp test_group_info.cpp test_group_members.cpp + test_hash.cpp test_multi_encrypt.cpp test_onionreq.cpp test_proto.cpp @@ -25,17 +25,18 @@ set(SOURCE_FILES test_xed25519.cpp ) -# if (NOT MSVC) - list(APPEND SOURCE_FILES test_hash.cpp) -# endif() - -add_executable(testAll ${SOURCE_FILES}) -target_link_libraries(testAll PRIVATE +set(TEST_ALL_LIBS libsession::config - libsession::onionreq libsodium::sodium-internal - Catch2::Catch2WithMain) + Catch2::Catch2WithMain +) + +if (ENABLE_ONIONREQ) + list(APPEND TEST_ALL_LIBS libsession::onionreq) +endif() + +target_link_libraries(testAll PRIVATE ${TEST_ALL_LIBS}) add_custom_target(check COMMAND testAll) From 5a21127f119920fb341eea096cbe1bbf2729b7bb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 20 Jun 2024 10:36:29 -0300 Subject: [PATCH 310/572] Avoid extra variable in tests linkage --- tests/CMakeLists.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4c9fa65..bffd82f2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,18 +26,16 @@ add_executable(testAll ) -set(TEST_ALL_LIBS +target_link_libraries(testAll PRIVATE libsession::config libsodium::sodium-internal Catch2::Catch2WithMain ) if (ENABLE_ONIONREQ) - list(APPEND TEST_ALL_LIBS libsession::onionreq) + target_link_libraries(testAll PRIVATE libsession::onionreq) endif() -target_link_libraries(testAll PRIVATE ${TEST_ALL_LIBS}) - add_custom_target(check COMMAND testAll) add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) From c61333f824639872e22cf92e4fe208e8ca621715 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 24 Jun 2024 13:53:31 +1000 Subject: [PATCH 311/572] Added back the 'ENABLE_ONIONREQ' flag --- CMakeLists.txt | 3 +++ src/CMakeLists.txt | 39 ++++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cff8e6bf..30739ebb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,9 @@ option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd} option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) +# Provide this as an option for now because GMP and Desktop are sometimes unhappy with each other. +option(ENABLE_ONIONREQ "Build with onion request functionality" ON) + if(USE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 768ae074..f5f2c285 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,25 +97,26 @@ target_link_libraries(config libzstd::static ) -add_libsession_util_library(onionreq - onionreq/builder.cpp - onionreq/hop_encryption.cpp - onionreq/key_types.cpp - onionreq/parser.cpp - onionreq/response_parser.cpp - network.cpp -) - -target_link_libraries(onionreq - PUBLIC - crypto - quic - PRIVATE - nlohmann_json::nlohmann_json - libsodium::sodium-internal - nettle::nettle -) - +if(ENABLE_ONIONREQ) + add_libsession_util_library(onionreq + onionreq/builder.cpp + onionreq/hop_encryption.cpp + onionreq/key_types.cpp + onionreq/parser.cpp + onionreq/response_parser.cpp + network.cpp + ) + + target_link_libraries(onionreq + PUBLIC + crypto + quic + PRIVATE + nlohmann_json::nlohmann_json + libsodium::sodium-internal + nettle::nettle + ) +endif() if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") # GCC 11 has an overzealous (and false) stringop-overread warning, but only when LTO is off. From c9a98dbb2e2ef45aa85075576247d61355269e1c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 24 Jun 2024 15:04:15 +1000 Subject: [PATCH 312/572] Fixed an issue where bad nodes might not get dropped in some cases --- include/session/network.hpp | 14 +++++++ src/network.cpp | 83 +++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 4fd04c96..849b37b8 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -289,6 +289,20 @@ class Network { return standard_paths; // Default }; + /// API: network/all_path_nodes + /// + /// Internal function to retrieve all of the nodes current used in paths + std::vector all_path_nodes() const { + std::vector result; + + for (auto& paths : {&standard_paths, &upload_paths, &download_paths}) + for (auto& path : *paths) + for (auto& node : path.nodes) + result.emplace_back(node); + + return result; + }; + /// API: network/update_status /// /// Internal function to update the connection status and trigger the `status_changed` hook if diff --git a/src/network.cpp b/src/network.cpp index ca1990d8..d1775253 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2062,7 +2062,7 @@ std::pair> Network::process_v3_onion_respons } if (!oxenc::is_base64(base64_iv_and_ciphertext)) - throw std::runtime_error{"Invalid base64 encoded IV and ciphertext: " + response + "."}; + throw std::runtime_error{"Invalid base64 encoded IV and ciphertext."}; ustring iv_and_ciphertext; oxenc::from_base64( @@ -2385,6 +2385,7 @@ void Network::handle_errors( } // Check if we got an error specifying the specific node that failed + std::vector nodes_to_drop; auto updated_failure_counts = net.call_get( [this]() -> std::unordered_map { return snode_failure_counts; }); auto updated_path = info.path; @@ -2421,7 +2422,9 @@ void Network::handle_errors( // If the specific node has failed too many times then we should try to repair the // existing path by replace the bad node with another one - if (updated_failure_counts[snode_it->to_string()] >= snode_failure_threshold) { + if (failure_count + 1 >= snode_failure_threshold) { + nodes_to_drop.emplace_back(*snode_it); + try { // If the node that's gone bad is the guard node then we just have to drop // the path @@ -2429,18 +2432,12 @@ void Network::handle_errors( throw std::runtime_error{"Cannot recover if guard node is bad"}; // Try to find an unused node to patch the path - auto [paths, unused_snodes] = net.call_get( - [this, path_type = info.path_type]() - -> std::pair< - std::vector, - std::vector> { - return {paths_for_type(path_type), snode_pool}; + auto [path_nodes, unused_snodes] = net.call_get( + [this]() -> std::pair< + std::vector, + std::vector> { + return {all_path_nodes(), snode_pool}; }); - std::vector path_nodes; - - for (const auto& path : paths) - path_nodes.insert( - path_nodes.end(), path.nodes.begin(), path.nodes.end()); unused_snodes.erase( std::remove_if( @@ -2500,10 +2497,14 @@ void Network::handle_errors( auto failure_count = updated_failure_counts.try_emplace(it.to_string(), 0).first->second; updated_failure_counts[it.to_string()] = failure_count + 1; + + if (failure_count + 1 >= snode_failure_threshold) + nodes_to_drop.emplace_back(it); } // Set the failure count of the guard node to match the threshold so we drop it updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; + nodes_to_drop.emplace_back(updated_path.nodes[0]); } else if (updated_path.nodes.size() < path_size) { // If the path doesn't have enough nodes then it's likely that this failure was // triggered when trying to establish a new path and, as such, we should increase the @@ -2512,9 +2513,16 @@ void Network::handle_errors( updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) .first->second; updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; + + if (failure_count + 1 >= snode_failure_threshold) + nodes_to_drop.emplace_back(updated_path.nodes[0]); } } + // If the target node has become invalid then add it to the list for removal + if (updated_failure_counts[info.target.to_string()] >= snode_failure_threshold) + nodes_to_drop.emplace_back(info.target); + // Update the cache (want to wait until this has been completed incase) std::condition_variable cv; std::mutex mtx; @@ -2528,6 +2536,7 @@ void Network::handle_errors( old_path = info.path, updated_failure_counts, updated_path, + nodes_to_drop, &cv, &mtx, &done]() mutable { @@ -2638,36 +2647,28 @@ void Network::handle_errors( { std::lock_guard lock{snode_cache_mutex}; + // Update the snode pool with the updated node failure counts for (size_t i = 0; i < updated_path.nodes.size(); ++i) - if (updated_failure_counts.try_emplace(updated_path.nodes[i].to_string(), 0) - .first->second >= snode_failure_threshold) { - snode_pool.erase( - std::remove(snode_pool.begin(), snode_pool.end(), old_path.nodes[i]), - snode_pool.end()); - - if (swarm_pubkey) - if (swarm_cache.contains(swarm_pubkey->hex())) { - auto updated_swarm = swarm_cache[swarm_pubkey->hex()]; - updated_swarm.erase( - std::remove( - updated_swarm.begin(), - updated_swarm.end(), - old_path.nodes[i]), - updated_swarm.end()); - swarm_cache[swarm_pubkey->hex()] = updated_swarm; - } - } else - std::replace( - snode_pool.begin(), - snode_pool.end(), - old_path.nodes[i], - updated_path.nodes[i]); - - // If the target node is invalid then remove it from the snode pool - if (updated_failure_counts[target_node.to_string()] >= snode_failure_threshold) + std::replace( + snode_pool.begin(), + snode_pool.end(), + old_path.nodes[i], + updated_path.nodes[i]); + + // Drop any nodes which have been added to the list to drop + for (auto& node : nodes_to_drop) { snode_pool.erase( - std::remove(snode_pool.begin(), snode_pool.end(), target_node), - snode_pool.end()); + std::remove(snode_pool.begin(), snode_pool.end(), node), snode_pool.end()); + + if (swarm_pubkey) + if (swarm_cache.contains(swarm_pubkey->hex())) { + auto updated_swarm = swarm_cache[swarm_pubkey->hex()]; + updated_swarm.erase( + std::remove(updated_swarm.begin(), updated_swarm.end(), node), + updated_swarm.end()); + swarm_cache[swarm_pubkey->hex()] = updated_swarm; + } + } need_pool_write = true; need_swarm_write = (swarm_pubkey && swarm_cache.contains(swarm_pubkey->hex())); From 3ad5920794d6c84d7ebd44936a23627f574fdc73 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 28 Jun 2024 18:02:41 +1000 Subject: [PATCH 313/572] Removed the path "recovery" logic (just rebuild instead) --- include/session/network.hpp | 3 +- src/network.cpp | 168 +++++++++--------------------------- tests/test_network.cpp | 77 ++++++----------- 3 files changed, 68 insertions(+), 180 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 849b37b8..59b1d79b 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -61,7 +61,6 @@ struct request_info { std::optional body; std::optional original_body; std::optional swarm_pubkey; - onion_path path; PathType path_type; std::chrono::milliseconds timeout; bool node_destination; @@ -585,6 +584,7 @@ class Network { /// /// Inputs: /// - `info` -- [in] the information for the request that was made. + /// - `path` -- [in] the onion path the request was sent along. /// - `timeout` -- [in, optional] flag indicating whether the request timed out. /// - `status_code` -- [in, optional] the status code returned from the network. /// - `response` -- [in, optional] response data returned from the network. @@ -592,6 +592,7 @@ class Network { /// information after processing the error. void handle_errors( request_info info, + onion_path path, bool timeout, std::optional status_code, std::optional response, diff --git a/src/network.cpp b/src/network.cpp index d1775253..f4498c1f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -540,8 +540,10 @@ void Network::resume() { void Network::close_connections() { net.call([this]() mutable { + // Explicitly reset the endpoint to close all connections endpoint.reset(); + // Explicitly reset the connection and stream (just in case) for (auto& paths : {&standard_paths, &upload_paths, &download_paths}) { for (auto& path : *paths) { path.conn_info.conn.reset(); @@ -549,6 +551,11 @@ void Network::close_connections() { } } + // Clear the paths (rebuild them on reconnection) + standard_paths.clear(); + upload_paths.clear(); + download_paths.clear(); + update_status(ConnectionStatus::disconnected); log::info(cat, "Closed all connections."); }); @@ -1149,127 +1156,20 @@ void Network::with_path( if (currently_suspended) return callback(std::nullopt, "Network is suspended"); + auto current_valid_paths = valid_paths(current_paths); std::pair, uint8_t> path_info = - find_possible_path(excluded_node, current_paths); + find_possible_path(excluded_node, current_valid_paths); auto& [target_path, paths_count] = path_info; - // The path doesn't have a valid connection so we should try to reconnect (we will end - // up updating the `paths` value so should do this in a blocking way) - if (target_path && !target_path->conn_info.is_valid()) { - log::trace( - cat, - "{} found invalid connection for {}, will try to recover.", - __PRETTY_FUNCTION__, - request_id); - - path_info = paths_and_pool_loop->call_get( - [this, path_type, request_id, path = *target_path]() mutable - -> std::pair, uint8_t> { - // Since this may have been blocked by another thread we should start by - // making sure the target path is still one of the current paths - auto current_paths = - net.call_get([this, path_type]() -> std::vector { - return paths_for_type(path_type); - }); - auto target_path_it = - std::find(current_paths.begin(), current_paths.end(), path); - - // If we didn't find the path then don't bother continuing - if (target_path_it == current_paths.end()) { - log::trace( - cat, - "{} path with invalid connection for {} no longer exists.", - __PRETTY_FUNCTION__, - request_id); - return {std::nullopt, current_paths.size()}; - } - - // It's possible that multiple requests were queued up waiting on the connection - // the be reestablished so check to see if the path is now valid and return it - // if it is - if (target_path_it->conn_info.is_valid()) { - log::trace( - cat, - "{} connection to {} for {} has already been recovered.", - __PRETTY_FUNCTION__, - target_path_it->nodes[0], - request_id); - return {*target_path_it, current_paths.size()}; - } - - // Try to retrieve a valid connection for the guard node - log::info( - cat, - "Connection to {} with type {} for {} path no longer valid, attempting " - "reconnection.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), - request_id); - auto [info, error] = get_connection_info(request_id, path_type, path.nodes[0]); - - // It's possible that the connection was created successfully, and reported as - // valid, but isn't actually valid (eg. it was shutdown immediately due to the - // network being unreachable) so to avoid this we wait for either the connection - // to be established or the connection to fail before continuing - if (!info.is_valid()) { - log::info( - cat, - "Reconnection to {} with type {} for {} path failed with error: " - "{}.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), - request_id, - error.value_or("Unknown error.")); - return {std::nullopt, current_paths.size()}; - } - - // Knowing that the reconnection succeeded is helpful for debugging - log::info( - cat, - "Reconnection to {} with type {} for {} path successful.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), - request_id); - - // If the connection info is valid and it's a standard path then update the - // connection status back to connected - if (path_type == PathType::standard) - update_status(ConnectionStatus::connected); - - // No need to call the 'paths_changed' callback as the paths haven't - // actually changed, just their connection info - auto updated_path = onion_path{std::move(info), path.nodes, 0, 0}; - auto paths_count = net.call_get( - [this, path_type, path, updated_path]() mutable -> uint8_t { - switch (path_type) { - case PathType::standard: - std::replace( - standard_paths.begin(), - standard_paths.end(), - path, - updated_path); - return standard_paths.size(); - - case PathType::upload: - std::replace( - upload_paths.begin(), - upload_paths.end(), - path, - updated_path); - return upload_paths.size(); - - case PathType::download: - std::replace( - download_paths.begin(), - download_paths.end(), - path, - updated_path); - return download_paths.size(); - } - }); - - return {updated_path, paths_count}; - }); + // If we somehow have invalid paths then remove them from the cache + if (current_paths.size() != current_valid_paths.size()) { + net.call([this, path_type, current_valid_paths]() mutable { + switch (path_type) { + case PathType::standard: standard_paths = current_valid_paths; break; + case PathType::upload: upload_paths = current_valid_paths; break; + case PathType::download: download_paths = current_valid_paths; break; + } + }); } // If we didn't get a target path then we have to build paths @@ -1713,15 +1613,28 @@ void Network::send_request( info.endpoint, payload, info.timeout, - [this, info, cb = std::move(handle_response)](quic::message resp) { + [this, info, target = info.target, cb = std::move(handle_response)]( + quic::message resp) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); try { auto [status_code, body] = validate_response(resp, false); cb(true, false, status_code, body); } catch (const status_code_exception& e) { - handle_errors(info, false, e.status_code, e.what(), cb); + handle_errors( + info, + {{target, nullptr, nullptr}, {target}, 0, 0}, + false, + e.status_code, + e.what(), + cb); } catch (const std::exception& e) { - handle_errors(info, resp.timed_out, -1, e.what(), cb); + handle_errors( + info, + {{target, nullptr, nullptr}, {target}, 0, 0}, + resp.timed_out, + -1, + e.what(), + cb); } }); } @@ -1776,7 +1689,6 @@ void Network::send_onion_request( onion_req_payload, body, swarm_pubkey, - *path, path_type, timeout, node_for_destination(destination).has_value(), @@ -1788,6 +1700,7 @@ void Network::send_onion_request( [this, builder = std::move(builder), info, + path = *path, destination = std::move(destination), cb = std::move(cb)]( bool success, @@ -1901,9 +1814,9 @@ void Network::send_onion_request( // succeed with the processed data return cb(true, false, processed_status_code, processed_body); } catch (const status_code_exception& e) { - handle_errors(info, false, e.status_code, e.what(), cb); + handle_errors(info, path, false, e.status_code, e.what(), cb); } catch (const std::exception& e) { - handle_errors(info, false, -1, e.what(), cb); + handle_errors(info, path, false, -1, e.what(), cb); } }); } catch (const std::exception& e) { @@ -2180,11 +2093,11 @@ void Network::handle_node_error(service_node node, PathType path_type, onion_pat std::nullopt, std::nullopt, std::nullopt, - path, path_type, 0ms, false, std::nullopt}, + path, false, std::nullopt, std::nullopt, @@ -2193,6 +2106,7 @@ void Network::handle_node_error(service_node node, PathType path_type, onion_pat void Network::handle_errors( request_info info, + onion_path path, bool timeout_, std::optional status_code_, std::optional response, @@ -2388,7 +2302,7 @@ void Network::handle_errors( std::vector nodes_to_drop; auto updated_failure_counts = net.call_get( [this]() -> std::unordered_map { return snode_failure_counts; }); - auto updated_path = info.path; + auto updated_path = path; bool found_invalid_node = false; if (response) { @@ -2533,7 +2447,7 @@ void Network::handle_errors( path_type = info.path_type, target_node = info.target, swarm_pubkey = info.swarm_pubkey, - old_path = info.path, + old_path = path, updated_failure_counts, updated_path, nodes_to_drop, diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 6c3ee422..c3c46be2 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -79,11 +79,12 @@ class TestNetwork { void handle_errors( request_info info, + onion_path path, bool timeout, std::optional status_code, std::optional response, std::optional handle_response) { - network.handle_errors(info, timeout, status_code, response, handle_response); + network.handle_errors(info, path, timeout, status_code, response, handle_response); } }; } // namespace session::network @@ -131,7 +132,6 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt, std::nullopt, std::nullopt, - path, PathType::standard, 0ms, true, @@ -149,6 +149,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, + path, false, code, std::nullopt, @@ -177,6 +178,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, + path, false, 500, std::nullopt, @@ -198,24 +200,13 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with no response (too many path failures) path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 9}; - auto mock_request2 = request_info{ - "BBBB", - target, - "test", - std::nullopt, - std::nullopt, - std::nullopt, - path, - PathType::standard, - 0ms, - true, - std::nullopt}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.set_failure_count(target3, 0); network.handle_errors( - mock_request2, + mock_request, + path, false, 500, std::nullopt, @@ -241,25 +232,14 @@ TEST_CASE("Network error handling", "[network]") { // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; - auto mock_request3 = request_info{ - "CCCC", - target, - "test", - std::nullopt, - std::nullopt, - std::nullopt, - path, - PathType::standard, - 0ms, - true, - std::nullopt}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.set_failure_count(target3, 0); network.handle_errors( - mock_request3, + mock_request, + path, false, 500, response, @@ -282,25 +262,14 @@ TEST_CASE("Network error handling", "[network]") { 1); // Incremented because conn_info is invalid // Check general error handling with a path and specific node failure (too many failures) - auto mock_request4 = request_info{ - "DDDD", - target, - "test", - std::nullopt, - std::nullopt, - std::nullopt, - path, - PathType::standard, - 0ms, - true, - std::nullopt}; network.set_snode_pool({target, target2, target3, target4}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); network.set_failure_count(target3, 0); network.handle_errors( - mock_request4, + mock_request, + path, false, 500, response, @@ -329,6 +298,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, + path, false, 421, std::nullopt, @@ -348,14 +318,13 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 with no response data is handled like any other error - auto mock_request5 = request_info{ - "EEEE", + auto mock_request2 = request_info{ + "BBBB", target, "test", std::nullopt, std::nullopt, x25519_pubkey::from_hex(x_pk_hex), - path, PathType::standard, 0ms, true, @@ -365,7 +334,8 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target2, 0); network.set_failure_count(target3, 0); network.handle_errors( - mock_request5, + mock_request2, + path, false, 421, std::nullopt, @@ -386,7 +356,8 @@ TEST_CASE("Network error handling", "[network]") { // Check the retry request of a 421 with non-swarm response data is handled like any other error network.handle_errors( - mock_request5, + mock_request2, + path, false, 421, "Test", @@ -419,7 +390,8 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target2, 0); network.set_failure_count(target3, 0); network.handle_errors( - mock_request5, + mock_request2, + path, false, 421, response, @@ -446,20 +418,20 @@ TEST_CASE("Network error handling", "[network]") { }); // Check a timeout with a sever destination doesn't impact the failure counts - auto mock_request6 = request_info{ - "FFFF", + auto mock_request3 = request_info{ + "CCCC", target, "test", std::nullopt, std::nullopt, x25519_pubkey::from_hex(x_pk_hex), - path, PathType::standard, 0ms, false, std::nullopt}; network.handle_errors( - mock_request6, + mock_request3, + path, true, std::nullopt, "Test", @@ -481,7 +453,8 @@ TEST_CASE("Network error handling", "[network]") { // Check a server response starting with '500 Internal Server Error' is reported as a `500` // error and doesn't affect the failure count network.handle_errors( - mock_request6, + mock_request3, + path, false, std::nullopt, "500 Internal Server Error", From dfcd3ae009dd68b8ca9f32a8b1fe5da46b793c99 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 1 Jul 2024 13:00:37 +1000 Subject: [PATCH 314/572] Reverted the removal of the path "recovery" logic --- src/network.cpp | 134 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index f4498c1f..df67cc93 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -551,11 +551,6 @@ void Network::close_connections() { } } - // Clear the paths (rebuild them on reconnection) - standard_paths.clear(); - upload_paths.clear(); - download_paths.clear(); - update_status(ConnectionStatus::disconnected); log::info(cat, "Closed all connections."); }); @@ -1156,20 +1151,127 @@ void Network::with_path( if (currently_suspended) return callback(std::nullopt, "Network is suspended"); - auto current_valid_paths = valid_paths(current_paths); std::pair, uint8_t> path_info = - find_possible_path(excluded_node, current_valid_paths); + find_possible_path(excluded_node, current_paths); auto& [target_path, paths_count] = path_info; - // If we somehow have invalid paths then remove them from the cache - if (current_paths.size() != current_valid_paths.size()) { - net.call([this, path_type, current_valid_paths]() mutable { - switch (path_type) { - case PathType::standard: standard_paths = current_valid_paths; break; - case PathType::upload: upload_paths = current_valid_paths; break; - case PathType::download: download_paths = current_valid_paths; break; - } - }); + // The path doesn't have a valid connection so we should try to reconnect (we will end + // up updating the `paths` value so should do this in a blocking way) + if (target_path && !target_path->conn_info.is_valid()) { + log::trace( + cat, + "{} found invalid connection for {}, will try to recover.", + __PRETTY_FUNCTION__, + request_id); + + path_info = paths_and_pool_loop->call_get( + [this, path_type, request_id, path = *target_path]() mutable + -> std::pair, uint8_t> { + // Since this may have been blocked by another thread we should start by + // making sure the target path is still one of the current paths + auto current_paths = + net.call_get([this, path_type]() -> std::vector { + return paths_for_type(path_type); + }); + auto target_path_it = + std::find(current_paths.begin(), current_paths.end(), path); + + // If we didn't find the path then don't bother continuing + if (target_path_it == current_paths.end()) { + log::trace( + cat, + "{} path with invalid connection for {} no longer exists.", + __PRETTY_FUNCTION__, + request_id); + return {std::nullopt, current_paths.size()}; + } + + // It's possible that multiple requests were queued up waiting on the connection + // the be reestablished so check to see if the path is now valid and return it + // if it is + if (target_path_it->conn_info.is_valid()) { + log::trace( + cat, + "{} connection to {} for {} has already been recovered.", + __PRETTY_FUNCTION__, + target_path_it->nodes[0], + request_id); + return {*target_path_it, current_paths.size()}; + } + + // Try to retrieve a valid connection for the guard node + log::info( + cat, + "Connection to {} with type {} for {} path no longer valid, attempting " + "reconnection.", + target_path_it->nodes[0], + path_type_name(path_type, single_path_mode), + request_id); + auto [info, error] = get_connection_info(request_id, path_type, path.nodes[0]); + + // It's possible that the connection was created successfully, and reported as + // valid, but isn't actually valid (eg. it was shutdown immediately due to the + // network being unreachable) so to avoid this we wait for either the connection + // to be established or the connection to fail before continuing + if (!info.is_valid()) { + log::info( + cat, + "Reconnection to {} with type {} for {} path failed with error: " + "{}.", + target_path_it->nodes[0], + path_type_name(path_type, single_path_mode), + request_id, + error.value_or("Unknown error.")); + return {std::nullopt, current_paths.size()}; + } + + // Knowing that the reconnection succeeded is helpful for debugging + log::info( + cat, + "Reconnection to {} with type {} for {} path successful.", + target_path_it->nodes[0], + path_type_name(path_type, single_path_mode), + request_id); + + // If the connection info is valid and it's a standard path then update the + // connection status back to connected + if (path_type == PathType::standard) + update_status(ConnectionStatus::connected); + + // No need to call the 'paths_changed' callback as the paths haven't + // actually changed, just their connection info + auto updated_path = onion_path{std::move(info), path.nodes, 0, 0}; + auto paths_count = net.call_get( + [this, path_type, path, updated_path]() mutable -> uint8_t { + switch (path_type) { + case PathType::standard: + std::replace( + standard_paths.begin(), + standard_paths.end(), + path, + updated_path); + return standard_paths.size(); + + case PathType::upload: + std::replace( + upload_paths.begin(), + upload_paths.end(), + path, + updated_path); + return upload_paths.size(); + + case PathType::download: + std::replace( + download_paths.begin(), + download_paths.end(), + path, + updated_path); + return download_paths.size(); + } + }); + + return {updated_path, paths_count}; + }); } // If we didn't get a target path then we have to build paths From 18e7fb8563666e09e2562df7467847ae01ea830e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 28 Jun 2024 09:56:14 +1000 Subject: [PATCH 315/572] fix build --- .gitmodules | 2 ++ CMakeLists.txt | 20 +++++++++++--------- external/CMakeLists.txt | 6 ++++-- external/oxen-libquic | 2 +- include/session/util.hpp | 1 + proto/CMakeLists.txt | 6 +++++- src/random.cpp | 2 ++ src/util.cpp | 1 + 8 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8b29c9c6..63005ac3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,5 @@ [submodule "external/protobuf"] path = external/protobuf url = https://github.com/protocolbuffers/protobuf.git +[submodule "external/oxen-libquic/"] + url = https://github.com/oxen-io/oxen-libquic/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 30739ebb..b491704b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,15 +118,17 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(external) -if(NOT TARGET nettle::nettle) - if(BUILD_STATIC_DEPS) - message(FATAL_ERROR "Internal error: nettle::nettle target (expected via libquic BUILD_STATIC_DEPS) not found") - else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(NETTLE REQUIRED IMPORTED_TARGET nettle) - add_library(nettle INTERFACE) - target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) - add_library(nettle::nettle ALIAS nettle) +if(ENABLE_ONIONREQ) + if(NOT TARGET nettle::nettle) + if(BUILD_STATIC_DEPS) + message(FATAL_ERROR "Internal error: nettle::nettle target (expected via libquic BUILD_STATIC_DEPS) not found") + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(NETTLE REQUIRED IMPORTED_TARGET nettle) + add_library(nettle INTERFACE) + target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) + add_library(nettle::nettle ALIAS nettle) + endif() endif() endif() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index e1b34545..194db24b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -114,13 +114,15 @@ if(CMAKE_CROSSCOMPILING) endif() set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") -system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) +if(ENABLE_ONIONREQ) + system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) +endif() if(NOT TARGET oxenc::oxenc) # The oxenc target will already exist if we load libquic above via submodule set(OXENC_BUILD_TESTS OFF CACHE BOOL "") set(OXENC_BUILD_DOCS OFF CACHE BOOL "") - system_or_submodule(OXENC oxenc liboxenc>=1.1.0 external/oxen-libquic/oxen-encoding) + system_or_submodule(OXENC oxenc liboxenc>=1.1.0 oxen-libquic/external/oxen-encoding) endif() if(NOT TARGET oxen::logging) diff --git a/external/oxen-libquic b/external/oxen-libquic index 88aba0cd..ef94de7d 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 88aba0cd7b2b49a30b2b6037b0076ae1fdd25d97 +Subproject commit ef94de7d9b01b95ccb2684ec44243347ae3c022b diff --git a/include/session/util.hpp b/include/session/util.hpp index 00d0293b..76da4061 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "types.hpp" diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index db216826..7c682a80 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -31,7 +31,11 @@ set_target_properties( # -Wunused-parameter triggers in the protobuf dependency message(STATUS "Disabling -Werror for proto unused-parameter") -target_compile_options(protos PUBLIC -Wno-error=unused-parameter) + +if (MSVC) +else() + target_compile_options(protos PUBLIC -Wno-error=unused-parameter) +endif() libsession_static_bundle(protos) diff --git a/src/random.cpp b/src/random.cpp index 996bf370..4a85a448 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -2,6 +2,8 @@ #include +#include + #include "session/export.h" #include "session/util.hpp" diff --git a/src/util.cpp b/src/util.cpp index 13cd66cb..7669d0e1 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,3 +1,4 @@ +#include #include namespace session { From cffdbde7061b935397c69ab816910228ddf5cb12 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 4 Jul 2024 14:35:29 +1000 Subject: [PATCH 316/572] fix: dirty fix TO BE REMOVED to make needs_dump true when need to dump --- include/session/config/base.hpp | 11 ++++++++++- src/config/base.cpp | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index d9a14b2d..7c3bd5de 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -181,6 +181,8 @@ class ConfigBase : public ConfigSig { // calling set_state, which sets to to true implicitly). bool _needs_dump = false; + ustring last_dumped = to_unsigned(""); + // Sets the current state; this also sets _needs_dump to true. If transitioning to a dirty // state and we know our current message hash, that hash gets added to `old_hashes_` to be // deleted at the next push. @@ -1097,7 +1099,14 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if something has changed since last call to dump - virtual bool needs_dump() const { return _needs_dump; } + virtual bool needs_dump() const { + if(_needs_dump) { + return _needs_dump; + } + auto current_dump = this->make_dump(); + auto dump_did_change = this->last_dumped != current_dump; + return dump_did_change; + } /// API: base/ConfigBase::add_key /// diff --git a/src/config/base.cpp b/src/config/base.cpp index 862e9548..2518751a 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -368,6 +368,7 @@ ustring ConfigBase::dump() { auto d = make_dump(); _needs_dump = false; + this->last_dumped = d; return d; } @@ -397,9 +398,10 @@ ConfigBase::ConfigBase( if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; - if (dump) + if (dump) { init_from_dump(from_unsigned_sv(*dump)); - else + this->last_dumped = ustring{*dump}; + } else _config = std::make_unique(); init_sig_keys(ed25519_pubkey, ed25519_secretkey); From 5c0aa37a80e29239b77f15453a5a1afba483c33a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Jul 2024 08:56:08 +1000 Subject: [PATCH 317/572] Added 'set_{x}_truncated' functions so clients have non-throwing versions --- include/session/config/contacts.hpp | 11 +++++++++++ include/session/config/groups/info.hpp | 21 ++++++++++++++++++++- include/session/config/groups/members.hpp | 12 ++++++++++++ include/session/config/user_profile.hpp | 8 ++++++++ src/config/contacts.cpp | 11 +++++++++++ src/config/groups/info.cpp | 12 ++++++++++++ src/config/groups/members.cpp | 6 ++++++ src/config/user_profile.cpp | 5 +++++ tests/test_config_contacts.cpp | 4 ++++ tests/test_group_info.cpp | 4 ++++ tests/test_group_members.cpp | 5 +++++ 11 files changed, 98 insertions(+), 1 deletion(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 7ac4b237..062c2e75 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -90,6 +90,7 @@ struct contact_info { /// - `name` -- Name to assign to the contact void set_name(std::string name); void set_nickname(std::string nickname); + void set_nickname_truncated(std::string nickname); private: friend class Contacts; @@ -206,6 +207,16 @@ class Contacts : public ConfigBase { /// - `nickname` -- string of the contacts nickname void set_nickname(std::string_view session_id, std::string nickname); + /// API: contacts/contacts::set_nickname_truncated + /// + /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates the value when it's too long. (If setting multiple fields at once you + /// should use `set()` instead). + /// + /// Inputs: + /// - `session_id` -- hex string of the session id + /// - `nickname` -- string of the contacts nickname + void set_nickname_truncated(std::string_view session_id, std::string nickname); + /// API: contacts/contacts::set_profile_pic /// /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 4010bf35..7650dc18 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -104,12 +104,22 @@ class Info final : public ConfigBase { /// /// Sets the group name; if given an empty string then the name is removed. /// - /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes it will be truncated. + /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes an error will be thrown. /// /// Inputs: /// - `new_name` -- The name to be put into the group Info void set_name(std::string_view new_name); + /// API: groups/Info::set_name_truncated + /// + /// Sets the group name; if given an empty string then the name is removed. + /// + /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes it will be truncated. + /// + /// Inputs: + /// - `new_name` -- The name to be put into the group Info + void set_name_truncated(std::string new_name); + /// API: groups/Info::get_description /// /// Returns the group description, or std::nullopt if there is no group description set. @@ -132,6 +142,15 @@ class Info final : public ConfigBase { /// - `new_desc` -- The new description to be put into the group Info void set_description(std::string_view new_desc); + /// API: groups/Info::set_description_truncated + /// + /// Sets the optional group description; if given an empty string then an existing description + /// is removed. The same as `set_description` but if the name is too long it'll be truncated. + /// + /// Inputs: + /// - `new_desc` -- The new description to be put into the group Info + void set_description_truncated(std::string new_desc); + /// API: groups/Info::get_profile_pic /// /// Gets the group's current profile pic URL and decryption key. The returned object will diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..f797202c 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -260,6 +260,18 @@ struct member { /// - `name` -- Name to assign to the contact void set_name(std::string name); + /// API: groups/member::set_name_truncated + /// + /// Sets a name; this is exactly the same as assigning to .name directly, except that we truncate + /// if the given name is longer than MAX_NAME_LENGTH. + /// + /// Note that you can set a longer name directly into the `.name` member, but it will be + /// truncated when serializing the record. + /// + /// Inputs: + /// - `name` -- Name to assign to the contact + void set_name_truncated(std::string name); + private: friend class Members; void load(const dict& info_dict); diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index d99f19e9..f5483e62 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -87,6 +87,14 @@ class UserProfile final : public ConfigBase { /// - `new_name` -- The name to be put into the user profile void set_name(std::string_view new_name); + /// API: user_profile/UserProfile::set_name_truncated + /// + /// Sets the user profile name; if given an empty string then the name is removed. Same as the `set_name` function but truncates the name if it's too long. + /// + /// Inputs: + /// - `new_name` -- The name to be put into the user profile + void set_name_truncated(std::string new_name); + /// API: user_profile/UserProfile::get_profile_pic /// /// Gets the user's current profile pic URL and decryption key. The returned object will diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index eed87390..b307c037 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -51,6 +51,12 @@ void contact_info::set_nickname(std::string n) { nickname = std::move(n); } +void contact_info::set_nickname_truncated(std::string n) { + if (n.size() > MAX_NAME_LENGTH) + n.resize(MAX_NAME_LENGTH); + set_nickname(n); +} + Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); @@ -261,6 +267,11 @@ void Contacts::set_nickname(std::string_view session_id, std::string nickname) { c.set_nickname(std::move(nickname)); set(c); } +void Contacts::set_nickname_truncated(std::string_view session_id, std::string nickname) { + auto c = get_or_construct(session_id); + c.set_nickname_truncated(std::move(nickname)); + set(c); +} void Contacts::set_profile_pic(std::string_view session_id, profile_pic pic) { auto c = get_or_construct(session_id); c.profile_picture = std::move(pic); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index b7c0d2c1..3c0478fa 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -35,6 +35,12 @@ void Info::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } +void Info::set_name_truncated(std::string new_name) { + if (new_name.size() > NAME_MAX_LENGTH) + new_name.resize(NAME_MAX_LENGTH); + set_name(new_name); +} + std::optional Info::get_description() const { if (auto* s = data["o"].string(); s && !s->empty()) return *s; @@ -47,6 +53,12 @@ void Info::set_description(std::string_view new_desc) { set_nonempty_str(data["o"], new_desc); } +void Info::set_description_truncated(std::string new_desc) { + if (new_desc.size() > DESCRIPTION_MAX_LENGTH) + new_desc.resize(DESCRIPTION_MAX_LENGTH); + set_description(new_desc); +} + profile_pic Info::get_profile_pic() const { profile_pic pic{}; if (auto* url = data["p"].string(); url && !url->empty()) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..d4919250 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -173,6 +173,12 @@ void member::set_name(std::string n) { name = std::move(n); } +void member::set_name_truncated(std::string n) { + if (n.size() > MAX_NAME_LENGTH) + n.resize(MAX_NAME_LENGTH); + set_name(n); +} + } // namespace session::config::groups using namespace session; diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index baa3219c..ebd5fc3b 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -44,6 +44,11 @@ void UserProfile::set_name(std::string_view new_name) { throw std::invalid_argument{"Invalid profile name: exceeds maximum length"}; set_nonempty_str(data["n"], new_name); } +void UserProfile::set_name_truncated(std::string new_name) { + if (new_name.size() > contact_info::MAX_NAME_LENGTH) + new_name.resize(contact_info::MAX_NAME_LENGTH); + set_name(new_name); +} LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { try { unbox(conf)->set_name(name); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 17883d75..5ec0b3b7 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -223,6 +223,10 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); + + CHECK_THROWS(c.set_nickname("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 1896b47f..0ea57f69 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -131,6 +131,10 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo2.is_destroyed()); + + CHECK_THROWS(ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(ginfo1.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(ginfo1.get_name() == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 747b2adb..97bf6205 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -264,4 +264,9 @@ TEST_CASE("Group Members", "[config][groups][members]") { } CHECK(i == 66); } + + auto m = gmem1.get_or_construct(sids[0]); + CHECK_THROWS(m.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(m.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(m.name == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } From cb8d6b9c6ba2791d70eb7d2076d2057727edb67e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Jul 2024 10:54:29 +1000 Subject: [PATCH 318/572] Ran the formatter --- include/session/config/contacts.hpp | 5 +++-- include/session/config/groups/members.hpp | 4 ++-- include/session/config/user_profile.hpp | 3 ++- tests/test_config_contacts.cpp | 12 +++++++++--- tests/test_group_info.cpp | 12 +++++++++--- tests/test_group_members.cpp | 12 +++++++++--- 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 062c2e75..08a7d792 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -209,8 +209,9 @@ class Contacts : public ConfigBase { /// API: contacts/contacts::set_nickname_truncated /// - /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates the value when it's too long. (If setting multiple fields at once you - /// should use `set()` instead). + /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates + /// the value when it's too long. (If setting multiple fields at once you should use `set()` + /// instead). /// /// Inputs: /// - `session_id` -- hex string of the session id diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index f797202c..9e2a22cc 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -262,8 +262,8 @@ struct member { /// API: groups/member::set_name_truncated /// - /// Sets a name; this is exactly the same as assigning to .name directly, except that we truncate - /// if the given name is longer than MAX_NAME_LENGTH. + /// Sets a name; this is exactly the same as assigning to .name directly, except that we + /// truncate if the given name is longer than MAX_NAME_LENGTH. /// /// Note that you can set a longer name directly into the `.name` member, but it will be /// truncated when serializing the record. diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index f5483e62..8f5db6b7 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -89,7 +89,8 @@ class UserProfile final : public ConfigBase { /// API: user_profile/UserProfile::set_name_truncated /// - /// Sets the user profile name; if given an empty string then the name is removed. Same as the `set_name` function but truncates the name if it's too long. + /// Sets the user profile name; if given an empty string then the name is removed. Same as the + /// `set_name` function but truncates the name if it's too long. /// /// Inputs: /// - `new_name` -- The name to be put into the user profile diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 5ec0b3b7..96562ea0 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -224,9 +224,15 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); - CHECK_THROWS(c.set_nickname("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" + "23456789012345678901234567890A")); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "234567890123456789012345678901234567890A")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 0ea57f69..df70900a 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -132,9 +132,15 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo2.is_destroyed()); - CHECK_THROWS(ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(ginfo1.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(ginfo1.get_name() == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890A")); + CHECK_NOTHROW( + ginfo1.set_name_truncated("123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890A")); + CHECK(ginfo1.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 97bf6205..c66a8c95 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -266,7 +266,13 @@ TEST_CASE("Group Members", "[config][groups][members]") { } auto m = gmem1.get_or_construct(sids[0]); - CHECK_THROWS(m.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(m.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(m.name == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + m.set_name("123456789012345678901234567890123456789012345678901234567890123456789012345" + "6789012345678901234567890A")); + CHECK_NOTHROW( + m.set_name_truncated("12345678901234567890123456789012345678901234567890123456789012345" + "67890123456789012345678901234567890A")); + CHECK(m.name == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } From c0cf732e1e64451476d39e1ee79547e56dcfd2e8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Jul 2024 15:34:59 +1000 Subject: [PATCH 319/572] Wrapped a few more functions in the exception wrapper --- src/config/convo_info_volatile.cpp | 34 ++++++++++++------------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 80b8447a..20f2bb06 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -608,7 +608,7 @@ LIBSESSION_C_API void convo_info_volatile_set_1to1( } LIBSESSION_C_API void convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo) { - unbox(conf)->set(convo::community{*convo}); + wrap_exceptions(conf, [&] { unbox(conf)->set(convo::community{*convo}); }); } LIBSESSION_C_API void convo_info_volatile_set_group( config_object* conf, const convo_info_volatile_group* convo) { @@ -621,34 +621,26 @@ LIBSESSION_C_API void convo_info_volatile_set_legacy_group( } LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { - try { - return unbox(conf)->erase_1to1(session_id); - } catch (...) { - return false; - } + return wrap_exceptions( + conf, [&] { return unbox(conf)->erase_1to1(session_id); }, false); } LIBSESSION_C_API bool convo_info_volatile_erase_community( config_object* conf, const char* base_url, const char* room) { - try { - return unbox(conf)->erase_community(base_url, room); - } catch (...) { - return false; - } + return wrap_exceptions( + conf, + [&] { return unbox(conf)->erase_community(base_url, room); }, + false); } LIBSESSION_C_API bool convo_info_volatile_erase_group(config_object* conf, const char* group_id) { - try { - return unbox(conf)->erase_group(group_id); - } catch (...) { - return false; - } + return wrap_exceptions( + conf, [&] { return unbox(conf)->erase_group(group_id); }, false); } LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( config_object* conf, const char* group_id) { - try { - return unbox(conf)->erase_legacy_group(group_id); - } catch (...) { - return false; - } + return wrap_exceptions( + conf, + [&] { return unbox(conf)->erase_legacy_group(group_id); }, + false); } LIBSESSION_C_API size_t convo_info_volatile_size(const config_object* conf) { From 2163461ae13ded4414da0f43aa876e0706504068 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Jul 2024 16:36:29 +1000 Subject: [PATCH 320/572] Added custom handling for a HTML 400 error --- src/network.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/network.cpp b/src/network.cpp index df67cc93..2e1a4b9d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2244,6 +2244,7 @@ void Network::handle_errors( // cases so they can be handled properly below if (status_code == -1 && response) { const std::unordered_map> response_map = { + {"400 Bad Request", {400, false}}, {"500 Internal Server Error", {500, false}}, {"502 Bad Gateway", {502, false}}, {"503 Service Unavailable", {503, false}}, From 88f45a940ee0166260cafcb3da6cea8541ee2a0e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Jul 2024 12:38:32 +1000 Subject: [PATCH 321/572] Additional invite/promotion state and missing C functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added an 'INVITE_NOT_SENT' status which is the default used when creating a new group memeber • Added a groups_keys_free function to the C API • Added a groups_keys_storage_namespace function to the C API • Updated the promotion functions based on the new status options --- include/session/config/groups/keys.h | 19 ++++++ include/session/config/groups/members.h | 5 +- include/session/config/groups/members.hpp | 78 +++++++++++++++++++---- src/config/groups/keys.cpp | 8 +++ src/config/groups/members.cpp | 5 +- tests/test_group_members.cpp | 26 +++++--- 6 files changed, 114 insertions(+), 27 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 3a396cbc..6c50f23f 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -74,6 +74,25 @@ LIBSESSION_EXPORT int groups_keys_init( size_t dumplen, char* error) LIBSESSION_WARN_UNUSED; +/// API: base/groups_keys_free +/// +/// Frees a config keys object created with groups_keys_init. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_group_keys object +LIBSESSION_EXPORT void groups_keys_free(config_group_keys* conf); + +/// API: base/groups_keys_storage_namespace +/// +/// Returns the numeric namespace in which config_group_keys messages should be stored. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_group_keys object +/// +/// Outputs: +/// - `int16_t` -- integer of the namespace +LIBSESSION_EXPORT int16_t groups_keys_storage_namespace(const config_group_keys* conf); + /// API: groups/groups_keys_size /// /// Returns the number of decryption keys stored in this Keys object. Mainly for diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index ae5d9490..eb57b165 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -8,7 +8,7 @@ extern "C" { #include "../profile_pic.h" #include "../util.h" -enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2 }; +enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2, INVITE_NOT_SENT = 3 }; enum groups_members_remove_status { REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2 }; typedef struct config_group_member { @@ -19,7 +19,8 @@ typedef struct config_group_member { user_profile_pic profile_pic; bool admin; - int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send, + // INVITE_NOT_SENT = invite hasn't been sent yet int promoted; // same value as `invited`, but for promotion-to-admin int removed; // 0 == unset, REMOVED_MEMBER = removed, REMOVED_MEMBER_AND_MESSAGES = remove // member and their messages diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..8843c33c 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -25,6 +25,7 @@ using namespace std::literals; /// I - invite status; this will be one of: /// - 1 if the invite has been issued but not yet accepted. /// - 2 if an invite was created but failed to send for some reason (and thus can be resent) +/// - 3 if a member has been added to the group but the invite hasn't been sent yet. /// - omitted once an invite is accepted. (This also gets omitted if the `A` admin flag gets /// set). /// s - invite supplemental keys flag (only set when `I` is set): if set (to 1) then this invite @@ -37,9 +38,10 @@ using namespace std::literals; /// - 1 if a promotion has been sent. /// - 2 if a promotion was created but failed to send for some reason (and thus should be /// resent) +/// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet. /// - omitted once the promotion is accepted (i.e. once `A` gets set). -constexpr int INVITE_SENT = 1, INVITE_FAILED = 2; +constexpr int INVITE_SENT = 1, INVITE_FAILED = 2, INVITE_NOT_SENT = 3; constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; /// Struct containing member details @@ -102,7 +104,7 @@ struct member { // Flags to track an invited user. This value is typically not used directly, but rather via // the `set_invited()`, `invite_pending()` and similar methods. - int invite_status = 0; + int invite_status = INVITE_NOT_SENT; /// API: groups/member::set_invited /// @@ -127,6 +129,17 @@ struct member { supplement = false; } + /// API: groups/member::invite_needs_send + /// + /// Returns whether the user needs an invite sent to them. Returns true if so (whether or + /// not that invitation has been sent). + /// + /// Inputs: none + /// + /// Outputs: + /// - `bool` -- true if the user needs an invitation to be sent, false otherwise. + bool invite_needs_send() const { return invite_status == INVITE_NOT_SENT; } + /// API: groups/member::invite_pending /// /// Returns whether the user currently has a pending invitation. Returns true if so (whether or @@ -155,20 +168,55 @@ struct member { /// API: groups/member::set_promoted /// - /// Sets the "promoted" flag for this user. This marks the user as having a pending - /// promotion-to-admin in the group. The optional `failed` parameter can be specified as true - /// if the promotion was issued but failed to send for some reason (this is intended as a signal - /// to other clients that the promotion should be reissued). + /// This marks the user as having a pending promotion-to-admin in the group, waiting for the + /// promotion message to be sent to them. + void set_promoted() { + admin = true; + invite_status = 0; + promotion_status = INVITE_NOT_SENT; + } + + /// API: groups/member::set_promotion_sent /// - /// Note that this flag is ignored when the `admin` field is set to true. + /// This marks the user as having a pending promotion-to-admin in the group, and that a + /// promotion message has been sent to them. + void set_promotion_sent() { + admin = true; + invite_status = 0; + promotion_status = INVITE_SENT; + } + + /// API: groups/member::set_promotion_failed /// - /// Inputs: - /// - `failed`: can be specified as true to mark the promotion status as "failed-to-send". If - /// omitted or false then the promotion status is set to "sent". - void set_promoted(bool failed = false) { - promotion_status = failed ? INVITE_FAILED : INVITE_SENT; + /// This marks the user as being promoted to an admin, but that their promotion message failed + /// to send (this is intended as a signal to other clients that the promotion should be + /// reissued). + void set_promotion_failed() { + admin = true; + invite_status = 0; + promotion_status = INVITE_FAILED; + } + + /// API: groups/member::accept_promotion + /// + /// This marks the user as having accepted a promotion to admin in the group. + void accept_promotion() { + admin = true; + invite_status = 0; + promotion_status = 0; } + /// API: groups/member::promotion_needs_send + /// + /// Returns whether the user needs a promotion to admin status to be sent. + /// Returns true if so (whether or not that promotion has failed). + /// + /// Inputs: None + /// + /// Outputs: + /// - `bool` -- true if the user needs a promotion to be sent, false otherwise. + bool promotion_needs_send() const { return promotion_status == INVITE_NOT_SENT; } + /// API: groups/member::promotion_pending /// /// Returns whether the user currently has a pending invitation/promotion to admin status. @@ -178,7 +226,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a pending promotion, false otherwise. - bool promotion_pending() const { return !admin && promotion_status > 0; } + bool promotion_pending() const { return promotion_status > 0; } /// API: groups/member::promotion_failed /// @@ -189,7 +237,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a failed pending promotion - bool promotion_failed() const { return !admin && promotion_status == INVITE_FAILED; } + bool promotion_failed() const { return promotion_status == INVITE_FAILED; } /// API: groups/member::promoted /// @@ -215,6 +263,8 @@ struct member { /// - `messages`: can be specified as true to indicate any messages sent by the member /// should also be removed upon a successful member removal. void set_removed(bool messages = false) { + invite_status = 0; + promotion_status = 0; removed_status = messages ? REMOVED_MEMBER_AND_MESSAGES : REMOVED_MEMBER; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index bc1628ea..f1477361 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1432,6 +1432,14 @@ LIBSESSION_C_API int groups_keys_init( return SESSION_ERR_NONE; } +LIBSESSION_C_API void groups_keys_free(config_group_keys* conf) { + delete conf; +} + +LIBSESSION_EXPORT int16_t groups_keys_storage_namespace(const config_group_keys* conf) { + return static_cast(unbox(conf).storage_namespace()); +} + LIBSESSION_C_API size_t groups_keys_size(const config_group_keys* conf) { return unbox(conf).size(); } diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..07f87150 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -49,7 +49,7 @@ void Members::set(const member& mem) { mem.profile_picture.key); set_flag(info["A"], mem.admin); - set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); + set_positive_int(info["P"], mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); set_flag(info["s"], mem.supplement); set_positive_int(info["R"], mem.removed_status); @@ -69,7 +69,7 @@ void member::load(const dict& info_dict) { admin = maybe_int(info_dict, "A").value_or(0); invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); - promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); + promotion_status = maybe_int(info_dict, "P").value_or(0); removed_status = maybe_int(info_dict, "R").value_or(0); supplement = invite_pending() && !promoted() ? maybe_int(info_dict, "s").value_or(0) : 0; } @@ -161,6 +161,7 @@ void member::into(config_group_member& m) const { m.admin = admin; static_assert(groups::INVITE_SENT == ::INVITE_SENT); static_assert(groups::INVITE_FAILED == ::INVITE_FAILED); + static_assert(groups::INVITE_NOT_SENT == ::INVITE_NOT_SENT); m.invited = invite_status; m.promoted = promotion_status; m.removed = removed_status; diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 747b2adb..80c0bbdb 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -71,7 +71,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // 10 admins: for (int i = 0; i < 10; i++) { auto m = gmem1.get_or_construct(sids[i]); - m.admin = true; + m.accept_promotion(); m.name = "Admin " + std::to_string(i); m.profile_picture.url = "http://example.com/" + std::to_string(i); m.profile_picture.key = @@ -115,7 +115,6 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem2) { CHECK(m.session_id == sids[i]); - CHECK_FALSE(m.invite_pending()); CHECK_FALSE(m.invite_failed()); CHECK_FALSE(m.promotion_pending()); CHECK_FALSE(m.promotion_failed()); @@ -123,11 +122,15 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.should_remove_messages()); CHECK_FALSE(m.supplement); if (i < 10) { + CHECK_FALSE(m.invite_needs_send()); + CHECK_FALSE(m.invite_pending()); CHECK(m.admin); CHECK(m.name == "Admin " + std::to_string(i)); CHECK_FALSE(m.profile_picture.empty()); CHECK(m.promoted()); } else { + CHECK(m.invite_needs_send()); + CHECK(m.invite_pending()); CHECK_FALSE(m.admin); CHECK_FALSE(m.promoted()); if (i < 20) { @@ -164,7 +167,10 @@ TEST_CASE("Group Members", "[config][groups][members]") { } for (int i = 58; i < 62; i++) { auto m = gmem2.get_or_construct(sids[i]); - m.set_promoted(i >= 60); + if (i >= 60) + m.set_promotion_failed(); + else + m.set_promotion_sent(); gmem2.set(m); } for (int i = 62; i < 66; i++) { @@ -188,7 +194,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem1) { CHECK(m.session_id == sids[i]); - CHECK(m.admin == i < 10); + CHECK(m.admin == (i < 10 || (i >= 58 && i < 62))); CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : i < 10 ? "Admin " + std::to_string(i) : "Member " + std::to_string(i))); @@ -197,7 +203,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_pending() == (50 <= i && i < 58)); + CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_pending() == (i >= 10 && i < 58)); CHECK(m.invite_failed() == (55 <= i && i < 58)); CHECK(m.supplement == (i % 2 && 50 < i && i < 58)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); @@ -222,11 +229,11 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.set(m); } else if (i == 58) { auto m = gmem1.get(sids[i]).value(); - m.admin = true; + m.accept_promotion(); gmem1.set(m); } else if (i == 59) { auto m = gmem1.get(sids[i]).value(); - m.set_promoted(); + m.set_promotion_sent(); gmem1.set(m); } } @@ -241,7 +248,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem2) { CHECK(m.session_id == sids[i]); - CHECK(m.admin == (i < 10 || i == 58)); + CHECK(m.admin == (i < 10 || (i >= 58 && i < 62))); CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : i < 10 ? "Admin " + std::to_string(i) : "Member " + std::to_string(i))); @@ -250,7 +257,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_pending() == (55 <= i && i < 58)); + CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_pending() == ((i >= 10 && i < 50) || i == 53 || (i >= 55 && i < 58))); CHECK(m.invite_failed() == (i == 57)); CHECK(m.supplement == (i == 55 || i == 57)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); From 1716b637aa93e085ffd9896cd53f1bbef4b8c80e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Jul 2024 15:28:10 +1000 Subject: [PATCH 322/572] Renamed a couple of functions, updated docs, fixed 'promotion_x' funcs --- include/session/config/groups/members.hpp | 29 ++++++++++------------- tests/test_group_members.cpp | 12 +++++----- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 8843c33c..7a412d5f 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,14 +81,11 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member is an admin. + /// Flag that is set to indicate to the group that this member has been promoted to an admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could /// have lost (or never had) the keys even if this is set. - /// - /// See also `promoted()` if you want to check for either an admin or someone being promoted to - /// admin. bool admin = false; /// API: groups/member::supplement @@ -129,16 +126,16 @@ struct member { supplement = false; } - /// API: groups/member::invite_needs_send + /// API: groups/member::invite_not_sent /// - /// Returns whether the user needs an invite sent to them. Returns true if so (whether or - /// not that invitation has been sent). + /// Returns true if there has never been an attempt to send an invitation to the user. Returns + /// false otherwise. /// /// Inputs: none /// /// Outputs: /// - `bool` -- true if the user needs an invitation to be sent, false otherwise. - bool invite_needs_send() const { return invite_status == INVITE_NOT_SENT; } + bool invite_not_sent() const { return invite_status == INVITE_NOT_SENT; } /// API: groups/member::invite_pending /// @@ -197,25 +194,25 @@ struct member { promotion_status = INVITE_FAILED; } - /// API: groups/member::accept_promotion + /// API: groups/member::set_promotion_accepted /// /// This marks the user as having accepted a promotion to admin in the group. - void accept_promotion() { + void set_promotion_accepted() { admin = true; invite_status = 0; promotion_status = 0; } - /// API: groups/member::promotion_needs_send + /// API: groups/member::promotion_not_sent /// - /// Returns whether the user needs a promotion to admin status to be sent. - /// Returns true if so (whether or not that promotion has failed). + /// Returns true if the user is an admin but there has never been an attempt to send a promotion + /// to admin status sent to them. Returns false otherwise. /// /// Inputs: None /// /// Outputs: /// - `bool` -- true if the user needs a promotion to be sent, false otherwise. - bool promotion_needs_send() const { return promotion_status == INVITE_NOT_SENT; } + bool promotion_not_sent() const { return admin && promotion_status == INVITE_NOT_SENT; } /// API: groups/member::promotion_pending /// @@ -226,7 +223,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a pending promotion, false otherwise. - bool promotion_pending() const { return promotion_status > 0; } + bool promotion_pending() const { return admin && promotion_status > 0; } /// API: groups/member::promotion_failed /// @@ -237,7 +234,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a failed pending promotion - bool promotion_failed() const { return promotion_status == INVITE_FAILED; } + bool promotion_failed() const { return admin && promotion_status == INVITE_FAILED; } /// API: groups/member::promoted /// diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 80c0bbdb..42e3e1ba 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -71,7 +71,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // 10 admins: for (int i = 0; i < 10; i++) { auto m = gmem1.get_or_construct(sids[i]); - m.accept_promotion(); + m.set_promotion_accepted(); m.name = "Admin " + std::to_string(i); m.profile_picture.url = "http://example.com/" + std::to_string(i); m.profile_picture.key = @@ -122,14 +122,14 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.should_remove_messages()); CHECK_FALSE(m.supplement); if (i < 10) { - CHECK_FALSE(m.invite_needs_send()); + CHECK_FALSE(m.invite_not_sent()); CHECK_FALSE(m.invite_pending()); CHECK(m.admin); CHECK(m.name == "Admin " + std::to_string(i)); CHECK_FALSE(m.profile_picture.empty()); CHECK(m.promoted()); } else { - CHECK(m.invite_needs_send()); + CHECK(m.invite_not_sent()); CHECK(m.invite_pending()); CHECK_FALSE(m.admin); CHECK_FALSE(m.promoted()); @@ -203,7 +203,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_not_sent() == (i >= 10 && i < 50)); CHECK(m.invite_pending() == (i >= 10 && i < 58)); CHECK(m.invite_failed() == (55 <= i && i < 58)); CHECK(m.supplement == (i % 2 && 50 < i && i < 58)); @@ -229,7 +229,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.set(m); } else if (i == 58) { auto m = gmem1.get(sids[i]).value(); - m.accept_promotion(); + m.set_promotion_accepted(); gmem1.set(m); } else if (i == 59) { auto m = gmem1.get(sids[i]).value(); @@ -257,7 +257,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_not_sent() == (i >= 10 && i < 50)); CHECK(m.invite_pending() == ((i >= 10 && i < 50) || i == 53 || (i >= 55 && i < 58))); CHECK(m.invite_failed() == (i == 57)); CHECK(m.supplement == (i == 55 || i == 57)); From 449c86d645df7a82059693ee70178725ad00ffab Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Jul 2024 17:43:48 +1000 Subject: [PATCH 323/572] Optimisations to the path building logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the path building logic to prevent a single IP being used multiple times in the same path • Updated the guard node testing to have an exponential backoff on testing nodes --- include/session/network.hpp | 2 ++ src/network.cpp | 58 +++++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 59b1d79b..23ef930b 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -470,6 +470,7 @@ class Network { /// Inputs: /// - 'request_id' - [in] id for the request which triggered the call. /// - `path_type` -- [in] the type of path to validate the size for. + /// - `test_attempt` -- [in] the number of test which have occurred before this one. /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's /// drained. /// - `callback` -- [in] callback to be triggered once we make a successful request. NOTE: If @@ -478,6 +479,7 @@ class Network { void find_valid_guard_node_recursive( std::string request_id, PathType path_type, + int64_t test_attempt, std::vector target_nodes, std::function< void(std::optional valid_guard_node, diff --git a/src/network.cpp b/src/network.cpp index 2e1a4b9d..f071f826 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -983,6 +983,7 @@ void Network::with_paths_and_pool( find_valid_guard_node_recursive( request_id, path_type, + 0, nodes_to_test[i], [prom](std::optional valid_guard_node, std::vector unused_nodes, @@ -1029,10 +1030,23 @@ void Network::with_paths_and_pool( for (auto& info : valid_nodes) { std::vector path{info.node}; - for (auto i = 0; i < path_size - 1; i++) { + while (path.size() < path_size) { + if (unused_nodes.empty()) + throw std::runtime_error{"Not enough remaining nodes."}; + auto node = unused_nodes.back(); unused_nodes.pop_back(); - path.push_back(node); + + // Ensure we don't put two nodes with the same IP into the same path + auto snode_with_ip_it = std::find_if( + path.begin(), + path.end(), + [&node](const auto& existing_node) { + return existing_node.to_ipv4() == node.to_ipv4(); + }); + + if (snode_with_ip_it == path.end()) + path.push_back(node); } paths_result.emplace_back(onion_path{std::move(info), path, 0, 0}); @@ -1390,6 +1404,7 @@ void Network::get_service_nodes_recursive( void Network::find_valid_guard_node_recursive( std::string request_id, PathType path_type, + int64_t test_attempt, std::vector target_nodes, std::function< void(std::optional valid_guard_node, @@ -1412,7 +1427,13 @@ void Network::find_valid_guard_node_recursive( path_type, target_node, 3s, - [this, path_type, target_node, target_nodes, request_id, cb = std::move(callback)]( + [this, + path_type, + test_attempt, + target_node, + target_nodes, + request_id, + cb = std::move(callback)]( std::vector version, connection_info info, std::optional error) { @@ -1446,12 +1467,31 @@ void Network::find_valid_guard_node_recursive( target_node.to_string(), request_id, e.what()); - std::thread retry_thread( - [this, path_type, remaining_nodes, request_id, cb = std::move(cb)] { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - find_valid_guard_node_recursive( - request_id, path_type, remaining_nodes, cb); - }); + + // Delay the next test based on the error we received + std::chrono::duration delay = std::chrono::milliseconds(100); + + // If we got a network unreachable error then we want to delay on an exponential + // curve so that we don't trash the battery life in the device loses connection + if (error) { + std::chrono::duration maxDelay = 3s; + delay = std::chrono::milliseconds(std::min( + std::chrono::duration_cast(maxDelay) + .count(), + static_cast(100 * std::pow(2, test_attempt)))); + } + + std::thread retry_thread([this, + delay, + path_type, + test_attempt, + remaining_nodes, + request_id, + cb = std::move(cb)] { + std::this_thread::sleep_for(delay); + find_valid_guard_node_recursive( + request_id, path_type, test_attempt + 1, remaining_nodes, cb); + }); retry_thread.detach(); } }); From 8d944ab0fbab1c5f0fe8861285daffcda7faf9c5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 17 Jul 2024 13:53:34 -0300 Subject: [PATCH 324/572] Delete C++ object in network_free (There are other similar deleters that need freeing that should come once this gets rebased with / merged with PR #92). --- src/network.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/network.cpp b/src/network.cpp index f071f826..e93fe647 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2814,6 +2814,7 @@ LIBSESSION_C_API bool network_init( } LIBSESSION_C_API void network_free(network_object* network) { + delete static_cast(network->internals); delete network; } From bb392c3f1eed7ce5aef58f36033af7a51f50adcd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 17 Jul 2024 13:47:45 -0300 Subject: [PATCH 325/572] Fix C free functions These were meant to delete the internal C++ object, but weren't actually doing so, and thus would be leaking the C++ objects when supposedly being freed from the C API. Also adds a missing `onion_request_builder_free` function and removes the (wrong) doc implying that `onion_request_builder_build` frees the object. --- include/session/onionreq/builder.h | 14 +++++++++++--- src/config/base.cpp | 1 + src/config/groups/keys.cpp | 1 + src/onionreq/builder.cpp | 12 ++++++++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 4cb72a1e..3efd7c5a 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -25,13 +25,21 @@ typedef struct onion_request_builder_object { /// /// Constructs an onion request builder and sets a pointer to it in `builder`. /// -/// When done with the object the `builder` must be destroyed by either passing the pointer to -/// onion_request_builder_free() or onion_request_builder_build(). +/// When done with the object the `builder` must be destroyed by passing the pointer to +/// onion_request_builder_free(). /// /// Inputs: /// - `builder` -- [out] Pointer to the builder object LIBSESSION_EXPORT void onion_request_builder_init(onion_request_builder_object** builder); +/// API: groups/onion_request_builder_free +/// +/// Properly destroys an onion request builder instance. +/// +/// Inputs: +/// - `builder` -- [out] Pointer to the builder object to be freed +LIBSESSION_EXPORT void onion_request_builder_free(onion_request_builder_object* builder); + /// API: onion_request_builder_set_enc_type /// /// Wrapper around session::onionreq::Builder::onion_request_builder_set_enc_type. @@ -173,4 +181,4 @@ LIBSESSION_EXPORT bool onion_request_builder_build( #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/config/base.cpp b/src/config/base.cpp index 2f22f7ca..47af4c14 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -623,6 +623,7 @@ using namespace session; using namespace session::config; LIBSESSION_EXPORT void config_free(config_object* conf) { + delete static_cast*>(conf->internals); delete conf; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index f1477361..55872239 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1433,6 +1433,7 @@ LIBSESSION_C_API int groups_keys_init( } LIBSESSION_C_API void groups_keys_free(config_group_keys* conf) { + delete static_cast(conf->internals); delete conf; } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 54143d14..27e39bf0 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -179,12 +179,16 @@ extern "C" { using session::ustring; LIBSESSION_C_API void onion_request_builder_init(onion_request_builder_object** builder) { - auto c = std::make_unique(); auto c_builder = std::make_unique(); - c_builder->internals = c.release(); + c_builder->internals = new session::onionreq::Builder{}; *builder = c_builder.release(); } +LIBSESSION_C_API void onion_request_builder_free(onion_request_builder_object* builder) { + delete static_cast(builder->internals); + delete builder; +} + LIBSESSION_C_API void onion_request_builder_set_enc_type( onion_request_builder_object* builder, ENCRYPT_TYPE enc_type) { assert(builder); @@ -252,7 +256,7 @@ LIBSESSION_C_API bool onion_request_builder_build( assert(builder && payload_in); try { - auto unboxed_builder = unbox(builder); + auto& unboxed_builder = unbox(builder); auto payload = unboxed_builder.build(ustring{payload_in, payload_in_len}); if (unboxed_builder.final_hop_x25519_keypair) { @@ -272,4 +276,4 @@ LIBSESSION_C_API bool onion_request_builder_build( return false; } } -} \ No newline at end of file +} From 5481ef61614d7a0d68185c52a18b721621982f9e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 08:56:19 +1000 Subject: [PATCH 326/572] Added a utf8_truncate function and use it when truncating user content --- include/session/util.hpp | 59 +++++++++++++++++++++++++++++++++++ src/config/contacts.cpp | 9 +++--- src/config/groups/info.cpp | 8 ++--- src/config/groups/members.cpp | 4 +-- src/config/user_profile.cpp | 4 +-- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index 65ae5fb2..6373546c 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -369,4 +369,63 @@ std::vector to_view_vector(const Container& c) { return to_view_vector(c.begin(), c.end()); } +/// Truncates a utf-8 encoded string to at most `n` bytes long, but with care as to not truncate in +/// the middle of a unicode codepoint. If the `n` length would shorten the string such that it +/// terminates in the middle of a utf-8 encoded unicode codepoint then the string is shortened +/// further to not include the sliced unicode codepoint. +/// +/// For example, "happy 🎂🎂🎂!!" in utf8 encoding is 20 bytes long: +/// "happy \xf0\x9f\x8e\x82\xf0\x9f\x8e\x82\xf0\x9f\x8e\x82!!", that is: +/// - "happy " (6 bytes) +/// - 🎂 = 0xf0 0x9f 0x8e 0x82 (12 bytes = 3 × 4 bytes each) +/// - "!!" (2 bytes) +/// Truncating this to different lengths results in: +/// - 20, 21, or higher - the 20-byte full string +/// - 19: "happy 🎂🎂🎂!" +/// - 18: "happy 🎂🎂🎂" +/// - 17: "happy 🎂🎂" (14 bytes) +/// - 16, 15, 14: same result as 17 +/// - 13, 12, 11, 10: "happy 🎂" +/// - 9, 8, 7, 6: "happy " +/// - 5: "happy" +/// - 4: "happ" +/// - 3: "hap" +/// - 2: "ha" +/// - 1: "a" +/// - 0: "" +/// +/// This function is *not* (currently) aware of unicode "characters", but merely codepoints (because +/// grapheme clusters get incredibly complicated). This is only designed to prevent invalid utf8 +/// encodings. For example, the pair 🇦🇺 (REGIONAL INDICATOR SYMBOL LETTER A, REGIONAL INDICATOR +/// SYMBOL LETTER U) is often rendered as a single Australian flag, but could get chopped here into +/// just 🇦 (REGIONAL INDICATOR SYMBOL LETTER A) rather than removing the getting split in the middle +/// of the pair, which would show up as a decorated A rather than an Australian flag. Another +/// example, é (LATIN SMALL LETTER E, COMBINING ACUTE ACCENT) could get chopped between the e and +/// the accent modifier, and end up as just "e" in the truncated string. +/// +inline std::string utf8_truncate(std::string val, size_t n) { + if (val.size() <= n) + return val; + // The *first* char in a utf8 sequence is either: + // 0b0....... -- single byte encoding, for values up to 0x7f (ascii) + // 0b11...... -- multi-byte encoding for values >= 0x80; the number of sequential high bit 1's + // in the first character indicate the sequence length (e.g. 0b1110.... starts a 3-byte + // sequence). In our birthday cake encoding, the first byte is \xf0 == 0b11110000, and so it is + // a 4-byte sequence. + // + // That leaves 0x10...... bytes as continuation bytes, each one holding 6 bits of the unicode + // codepoint, in big endian order, so our birthday cake (in bits): 0b11110000 0b10011111 + // 0b10001110 0b10000010 is the unicode value 0b000 011111 001110 000010 == 0x1f382 == U+1F382: + // BIRTHDAY CAKE). + // + // To prevent slicing, then, we just have to ensure the the first byte after the slice point is + // *not* a continuation byte (and therefore is either a plain ascii character codepoint, or is + // the start of a multi-character codepoint). + while (n > 0 && (val[n] & 0b1100'0000) == 0b1000'0000) + --n; + + val.resize(n); + return val; +} + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index b307c037..1e0437a0 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -41,8 +41,9 @@ contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { void contact_info::set_name(std::string n) { if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - name = std::move(n); + name = std::move(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); + else + name = std::move(n); } void contact_info::set_nickname(std::string n) { @@ -52,9 +53,7 @@ void contact_info::set_nickname(std::string n) { } void contact_info::set_nickname_truncated(std::string n) { - if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - set_nickname(n); + set_nickname(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); } Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 3c0478fa..e9b57a92 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -36,9 +36,7 @@ void Info::set_name(std::string_view new_name) { } void Info::set_name_truncated(std::string new_name) { - if (new_name.size() > NAME_MAX_LENGTH) - new_name.resize(NAME_MAX_LENGTH); - set_name(new_name); + set_name(utf8_truncate(std::move(new_name), NAME_MAX_LENGTH)); } std::optional Info::get_description() const { @@ -54,9 +52,7 @@ void Info::set_description(std::string_view new_desc) { } void Info::set_description_truncated(std::string new_desc) { - if (new_desc.size() > DESCRIPTION_MAX_LENGTH) - new_desc.resize(DESCRIPTION_MAX_LENGTH); - set_description(new_desc); + set_description(utf8_truncate(std::move(new_desc), DESCRIPTION_MAX_LENGTH)); } profile_pic Info::get_profile_pic() const { diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index d4919250..aaf80b9e 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -174,9 +174,7 @@ void member::set_name(std::string n) { } void member::set_name_truncated(std::string n) { - if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - set_name(n); + set_name(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); } } // namespace session::config::groups diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index ebd5fc3b..96906b74 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -45,9 +45,7 @@ void UserProfile::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } void UserProfile::set_name_truncated(std::string new_name) { - if (new_name.size() > contact_info::MAX_NAME_LENGTH) - new_name.resize(contact_info::MAX_NAME_LENGTH); - set_name(new_name); + set_name(utf8_truncate(std::move(new_name), contact_info::MAX_NAME_LENGTH)); } LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { try { From eaea8d90039aebebe222aa4bb92bea05d1eb33c4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 09:01:42 +1000 Subject: [PATCH 327/572] Small docs change --- include/session/config/groups/members.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 7a412d5f..cf73d02e 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,7 +81,7 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member has been promoted to an admin. + /// Flag that is set to indicate to the group that this member is an admin or has been promoted to admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could From ee0802e82ee8ae6a06d8881336045e90683dbb1e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 10:49:24 +1000 Subject: [PATCH 328/572] Ran the formatter -_- --- include/session/config/groups/members.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index cf73d02e..4d2f95d2 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,7 +81,8 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member is an admin or has been promoted to admin. + /// Flag that is set to indicate to the group that this member is an admin or has been promoted + /// to admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could From e76222733277e4ae833111cfbf44b5b5e0cb94c7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Jul 2024 09:02:49 +1000 Subject: [PATCH 329/572] Added some tests for the new utf8 truncation logic --- tests/test_config_contacts.cpp | 12 ++++++++ tests/test_config_userprofile.cpp | 47 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 96562ea0..c8851953 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -233,6 +233,18 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" "901234567890"); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "234567890123456789012345678901234567🎂")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567"); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "2345678901234567890123456789012345🎂🎂")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345🎂"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 66c81cac..f39062e0 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "utils.hpp" @@ -20,6 +21,52 @@ void log_msg(config_log_level lvl, const char* msg, void*) { << ": " << msg); } +TEST_CASE("UserProfile", "[config][user_profile]") { + + 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)); + + session::config::UserProfile profile{ustring_view{seed}, std::nullopt}; + + CHECK_THROWS( + profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789" + "01" + "23456789012345678901234567890A")); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "234567890123456789012345678901234567890A")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "234567890123456789012345678901234567🎂")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567"); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "2345678901234567890123456789012345🎂🎂")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345🎂"); +} + TEST_CASE("user profile C API", "[config][user_profile][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; From c829255b549ec845eeedd84205b0103eccb92e45 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Jul 2024 15:50:08 +1000 Subject: [PATCH 330/572] Fixed an issue in the C++ to C conversion of a service node --- src/network.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index e93fe647..a4970405 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1480,7 +1480,7 @@ void Network::find_valid_guard_node_recursive( .count(), static_cast(100 * std::pow(2, test_attempt)))); } - + std::thread retry_thread([this, delay, path_type, @@ -2749,8 +2749,12 @@ std::vector convert_service_nodes( std::vector converted_nodes; for (auto& node : nodes) { auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); + auto ipv4 = node.to_ipv4(); network_service_node converted_node; - std::memcpy(converted_node.ip, node.host().data(), sizeof(converted_node.ip)); + converted_node.ip[0] = (ipv4.addr >> 24) & 0xFF; + converted_node.ip[1] = (ipv4.addr >> 16) & 0xFF; + converted_node.ip[2] = (ipv4.addr >> 8) & 0xFF; + converted_node.ip[3] = ipv4.addr & 0xFF; strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.quic_port = node.port(); From 13c2f35ad909d663a9ee278ba4038ddbee1264cb Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 23 Jul 2024 20:02:05 +1000 Subject: [PATCH 331/572] Added a version check for the final path node and a couple other fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the code to retrieve the storage_server_version and check that the final path node is greater than 2.8.0 to improve requests during the current network upgrade • Updated the disk write thread to write each file to a stringstream and then write the stream to disk at once (instead of line by line) • Split off the service node failure counts to be stored in their own file (reduce amount of data written when getting a snode failure) • Fixed the C++ -> C conversion of the service_node ip address --- include/session/network.hpp | 35 +- include/session/onionreq/builder.hpp | 6 +- src/network.cpp | 764 +++++++++++++++++---------- src/onionreq/builder.cpp | 6 +- tests/test_network.cpp | 23 +- 5 files changed, 538 insertions(+), 296 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 4fd04c96..14ae5ef7 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -11,7 +11,6 @@ namespace session::network { namespace fs = std::filesystem; -using service_node = oxen::quic::RemoteAddress; using network_response_callback_t = std::function response)>; @@ -28,6 +27,39 @@ enum class PathType { download, }; +struct service_node : public oxen::quic::RemoteAddress { + public: + std::vector storage_server_version; + + service_node() = delete; + + template + service_node( + std::string_view remote_pk, std::vector storage_server_version, Opt&&... opts) : + oxen::quic::RemoteAddress{remote_pk, std::forward(opts)...}, + storage_server_version{storage_server_version} {} + + template + service_node(ustring_view remote_pk, std::vector storage_server_version, Opt&&... opts) : + oxen::quic::RemoteAddress{remote_pk, std::forward(opts)...}, + storage_server_version{storage_server_version} {} + + service_node(const service_node& obj) : + oxen::quic::RemoteAddress{obj}, storage_server_version{obj.storage_server_version} {} + service_node& operator=(const service_node& obj) { + storage_server_version = obj.storage_server_version; + oxen::quic::RemoteAddress::operator=(obj); + _copy_internals(obj); + return *this; + } + + bool operator==(const service_node& other) const { + return static_cast(*this) == + static_cast(other) && + storage_server_version == other.storage_server_version; + } +}; + struct connection_info { service_node node; std::shared_ptr conn; @@ -81,6 +113,7 @@ class Network { bool shut_down_disk_thread = false; bool need_write = false; bool need_pool_write = false; + bool need_failure_counts_write = false; bool need_swarm_write = false; bool need_clear_cache = false; diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 1eb97f95..e1a17afe 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -7,6 +7,10 @@ #include "key_types.hpp" +namespace session::network { +struct service_node; +} + namespace session::onionreq { struct ServerDestination { @@ -35,7 +39,7 @@ struct ServerDestination { method{std::move(method)} {} }; -using network_destination = std::variant; +using network_destination = std::variant; enum class EncryptType { aes_gcm, diff --git a/src/network.cpp b/src/network.cpp index ca1990d8..a030b3d6 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -60,6 +60,9 @@ namespace { // The smallest size the snode pool can get to before we need to fetch more. constexpr uint16_t min_snode_pool_count = 12; + // The smallest size a swarm can get to before we need to fetch it again. + constexpr uint16_t min_swarm_snode_count = 3; + // The number of snodes (including the guard snode) in a path. constexpr uint8_t path_size = 3; @@ -75,7 +78,7 @@ namespace { // File names const fs::path file_testnet{u8"testnet"}, file_snode_pool{u8"snode_pool"}, file_snode_pool_updated{u8"snode_pool_updated"}, swarm_dir{u8"swarm"}, - default_cache_path{u8"."}; + default_cache_path{u8"."}, file_snode_failure_counts{u8"snode_failure_counts"}; constexpr auto node_not_found_prefix = "502 Bad Gateway\n\nNext node not found: "sv; constexpr auto node_not_found_prefix_no_status = "Next node not found: "sv; @@ -106,21 +109,49 @@ namespace { return 2; // Default } + /// Converts a string such as "1.2.3" to a vector of ints {1,2,3}. Throws if something + /// in/around the .'s isn't parseable as an integer. + std::vector parse_version(std::string_view vers, bool trim_trailing_zero = true) { + auto v_s = session::split(vers, "."); + std::vector result; + for (const auto& piece : v_s) + if (!quic::parse_int(piece, result.emplace_back())) + throw std::invalid_argument{"Invalid version"}; + + // Remove any trailing `0` values (but ensure we at least end up with a "0" version) + if (trim_trailing_zero) + while (result.size() > 1 && result.back() == 0) + result.pop_back(); + + return result; + } + service_node node_from_json(nlohmann::json json) { auto pk_ed = json["pubkey_ed25519"].get(); if (pk_ed.size() != 64 || !oxenc::is_hex(pk_ed)) throw std::invalid_argument{ "Invalid service node json: pubkey_ed25519 is not a valid, hex pubkey"}; + + // When parsing a node from JSON it'll generally be from the 'get_swarm` endpoint or a 421 + // error neither of which contain the `storage_server_version` - luckily we don't need the + // version for these two cases so can just default it to `0` + std::vector storage_server_version = {0}; + if (json.contains("storage_server_version")) + storage_server_version = + parse_version(json["storage_server_version"].get()); + return {oxenc::from_hex(pk_ed), + storage_server_version, json["ip"].get(), json["port_omq"].get()}; } - std::pair node_from_disk(std::string_view str) { + service_node node_from_disk(std::string_view str, bool can_ignore_version = false) { + // Format is "{ip}|{port}|{version}|{ed_pubkey} auto parts = split(str, "|"); if (parts.size() != 4) throw std::invalid_argument("Invalid service node serialisation: {}"_format(str)); - if (parts[2].size() != 64 || !oxenc::is_hex(parts[2])) + if (parts[3].size() != 64 || !oxenc::is_hex(parts[3])) throw std::invalid_argument{ "Invalid service node serialisation: pubkey is not hex or has wrong size"}; @@ -128,40 +159,32 @@ namespace { if (!quic::parse_int(parts[1], port)) throw std::invalid_argument{"Invalid service node serialization: invalid port"}; - uint8_t failure_count; - if (!quic::parse_int(parts[3], failure_count)) - throw std::invalid_argument{"Invalid service node serialization: invalid port"}; + std::vector storage_server_version = parse_version(parts[2]); + if (!can_ignore_version && storage_server_version == std::vector{0}) + throw std::invalid_argument{"Invalid service node serialization: invalid version"}; return { - { - oxenc::from_hex(parts[2]), // ed25519_pubkey - std::string(parts[0]), // ip - port, // port - }, - failure_count // failure_count + oxenc::from_hex(parts[3]), // ed25519_pubkey + storage_server_version, // storage_server_version + std::string(parts[0]), // ip + port, // port }; } const std::vector seed_nodes_testnet{ - node_from_disk("144.76.164.202|35400|" - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9|0"sv) - .first}; + node_from_disk("144.76.164.202|35400|2.8.0|" + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"sv)}; const std::vector seed_nodes_mainnet{ - node_from_disk("144.76.164.202|20200|" - "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef|0"sv) - .first, - node_from_disk("88.99.102.229|20201|" - "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce|0"sv) - .first, - node_from_disk("195.16.73.17|20202|" - "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f|0"sv) - .first, - node_from_disk("104.194.11.120|20203|" - "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b|0"sv) - .first, - node_from_disk("104.194.8.115|20204|" - "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0|0"sv) - .first}; + node_from_disk("144.76.164.202|20200|2.8.0|" + "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef"sv), + node_from_disk("88.99.102.229|20201|2.8.0|" + "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce"sv), + node_from_disk("195.16.73.17|20202|2.8.0|" + "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f"sv), + node_from_disk("104.194.11.120|20203|2.8.0|" + "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b"sv), + node_from_disk("104.194.8.115|20204|2.8.0|" + "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0"sv)}; constexpr auto file_server = "filev2.getsession.org"sv; constexpr auto file_server_pubkey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"sv; @@ -181,34 +204,16 @@ namespace { }; }; - /// Converts a string such as "1.2.3" to a vector of ints {1,2,3}. Throws if something - /// in/around - /// the .'s isn't parseable as an integer. - std::vector parse_version(std::string_view vers, bool trim_trailing_zero = true) { - auto v_s = session::split(vers, "."); - std::vector result; - for (const auto& piece : v_s) - if (!quic::parse_int(piece, result.emplace_back())) - throw std::invalid_argument{"Invalid version"}; - - // Remove any trailing `0` values (but ensure we at least end up with a "0" version) - if (trim_trailing_zero) - while (result.size() > 1 && result.back() == 0) - result.pop_back(); - - return result; - } - - std::string node_to_disk( - service_node node, std::unordered_map failure_counts) { + std::string node_to_disk(service_node node) { + // Format is "{ip}|{port}|{version}|{ed_pubkey} auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); return fmt::format( "{}|{}|{}|{}", node.host(), node.port(), - ed25519_pubkey_hex, - failure_counts.try_emplace(node.to_string(), 0).first->second); + "{}"_format(fmt::join(node.storage_server_version, ".")), + ed25519_pubkey_hex); } session::onionreq::x25519_pubkey compute_xpk(ustring_view ed25519_pk) { @@ -221,14 +226,14 @@ namespace { } std::optional node_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) + if (auto* dest = std::get_if(&destination)) return *dest; return std::nullopt; } session::onionreq::x25519_pubkey pubkey_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) + if (auto* dest = std::get_if(&destination)) return compute_xpk(dest->view_remote_key()); if (auto* dest = std::get_if(&destination)) @@ -341,20 +346,56 @@ void Network::load_cache_from_disk() { if (fs::exists(pool_path)) { auto file = open_for_reading(pool_path); std::vector loaded_pool; - std::unordered_map loaded_failure_count; std::string line; + auto invalid_entries = 0; while (std::getline(file, line)) { try { - auto [node, failure_count] = node_from_disk(line); - loaded_pool.push_back(node); - loaded_failure_count[node.to_string()] = failure_count; + loaded_pool.push_back(node_from_disk(line)); } catch (...) { - log::warning(cat, "Skipping invalid entry in snode pool cache."); + ++invalid_entries; } } + if (invalid_entries > 0) + log::warning( + cat, "Skipped {} invalid entries in snode pool cache.", invalid_entries); + snode_pool = loaded_pool; + } + + // Load the failure counts + auto failure_counts_path = cache_path / file_snode_failure_counts; + if (fs::exists(failure_counts_path)) { + auto file = open_for_reading(failure_counts_path); + std::unordered_map loaded_failure_count; + std::string line; + auto invalid_entries = 0; + + while (std::getline(file, line)) { + try { + auto parts = split(line, "|"); + uint8_t failure_count; + + if (parts.size() != 2) + throw std::invalid_argument( + "Invalid failure count serialisation: {}"_format(line)); + if (!quic::parse_int(parts[1], failure_count)) + throw std::invalid_argument{ + "Invalid failure count serialization: invalid failure count"}; + + loaded_failure_count[std::string(parts[0])] = failure_count; + } catch (...) { + ++invalid_entries; + } + } + + if (invalid_entries > 0) + log::warning( + cat, + "Skipped {} invalid entries in snode failure count cache.", + invalid_entries); + snode_failure_counts = loaded_failure_count; } @@ -362,6 +403,7 @@ void Network::load_cache_from_disk() { auto time_now = std::chrono::system_clock::now(); std::unordered_map> loaded_cache; std::vector caches_to_remove; + auto invalid_swarm_entries = 0; for (auto& entry : fs::directory_iterator(swarm_path)) { // If the pubkey was valid then process the content @@ -391,14 +433,15 @@ void Network::load_cache_from_disk() { throw load_cache_exception{"Expired swarm cache."}; } - // Otherwise try to parse as a node - nodes.push_back(node_from_disk(line).first); + // Otherwise try to parse as a node (for the swarm cache we can ignore invalid + // versions as the `get_swarm` API doesn't return version info) + nodes.push_back(node_from_disk(line, true)); } catch (const std::exception& e) { // Don't bother logging for expired entries (we include the count separately at // the end) if (dynamic_cast(&e) == nullptr) { - log::warning(cat, "Skipping invalid entry in swarm cache: {}", e.what()); + ++invalid_swarm_entries; } // The cache is invalid, we should remove it @@ -416,6 +459,9 @@ void Network::load_cache_from_disk() { caches_to_remove.emplace_back(path); } + if (invalid_swarm_entries > 0) + log::warning(cat, "Skipped {} invalid entries in swarm cache.", invalid_swarm_entries); + swarm_cache = loaded_cache; // Remove any expired cache files @@ -461,9 +507,12 @@ void Network::disk_write_thread_loop() { pool_tmp += u8"_new"; { - auto file = open_for_writing(pool_tmp); + std::stringstream ss; for (auto& snode : snode_pool_write) - file << node_to_disk(snode, snode_failure_counts_write) << '\n'; + ss << node_to_disk(snode) << '\n'; + + std::ofstream file(pool_tmp, std::ios::binary); + file << ss.rdbuf(); } fs::rename(pool_tmp, pool_path); @@ -476,6 +525,25 @@ void Network::disk_write_thread_loop() { log::debug(cat, "Finished writing snode pool cache to disk."); } + // Save the snode failure counts to disk + if (need_failure_counts_write) { + auto failure_counts_path = cache_path / file_snode_failure_counts; + auto failure_counts_tmp = failure_counts_path; + failure_counts_tmp += u8"_new"; + + { + std::stringstream ss; + for (auto& [key, count] : snode_failure_counts_write) + ss << fmt::format("{}|{}", key, count) << '\n'; + + std::ofstream file(failure_counts_tmp, std::ios::binary); + file << ss.rdbuf(); + } + + fs::rename(failure_counts_tmp, failure_counts_path); + log::debug(cat, "Finished writing snode failure counts to disk."); + } + // Write the swarm cache to disk if (need_swarm_write) { auto time_now = std::chrono::system_clock::now(); @@ -484,15 +552,17 @@ void Network::disk_write_thread_loop() { auto swarm_path = swarm_base / key; auto swarm_tmp = swarm_path; swarm_tmp += u8"_new"; - auto swarm_file = open_for_writing(swarm_tmp); - // Write the timestamp to the file - swarm_file << std::chrono::system_clock::to_time_t(time_now) << '\n'; + // Write the timestamp + std::stringstream ss; + ss << std::chrono::system_clock::to_time_t(time_now) << '\n'; - // Write the nodes to the file + // Write the nodes for (auto& snode : swarm) - swarm_file << node_to_disk(snode, snode_failure_counts_write) - << '\n'; + ss << node_to_disk(snode) << '\n'; + + std::ofstream swarm_file(swarm_tmp, std::ios::binary); + swarm_file << ss.rdbuf(); fs::rename(swarm_tmp, swarm_path); } @@ -500,6 +570,7 @@ void Network::disk_write_thread_loop() { } need_pool_write = false; + need_failure_counts_write = false; need_swarm_write = false; need_write = false; } catch (const std::exception& e) { @@ -511,6 +582,7 @@ void Network::disk_write_thread_loop() { if (need_clear_cache) { snode_pool = {}; last_snode_pool_update = {}; + snode_failure_counts = {}; swarm_cache = {}; lock.unlock(); @@ -687,6 +759,7 @@ using paths_and_pool_result = using paths_and_pool_info = std::tuple< std::vector, std::vector, + std::unordered_map>, std::chrono::system_clock::time_point, bool>; @@ -699,9 +772,13 @@ void Network::with_paths_and_pool( std::vector pool, std::optional error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - auto [current_paths, pool, last_pool_update, currently_suspended] = + auto [current_paths, pool, current_swarms, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { - return {paths_for_type(path_type), snode_pool, last_snode_pool_update, suspended}; + return {paths_for_type(path_type), + snode_pool, + swarm_cache, + last_snode_pool_update, + suspended}; }); // If the network is currently suspended then fail immediately @@ -724,10 +801,11 @@ void Network::with_paths_and_pool( auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get( [this, path_type, request_id, excluded_node]() mutable -> paths_and_pool_result { - auto [current_paths, pool, last_pool_update, currently_suspended] = + auto [current_paths, pool, current_swarms, last_pool_update, currently_suspended] = net.call_get([this, path_type]() -> paths_and_pool_info { return {paths_for_type(path_type), snode_pool, + swarm_cache, last_snode_pool_update, suspended}; }); @@ -756,8 +834,11 @@ void Network::with_paths_and_pool( // If the pool isn't valid then we should update it CSRNG rng; + auto swarms_updated = false; std::vector pool_result = pool; std::vector paths_result = current_valid_paths; + std::unordered_map> swarm_result = + current_swarms; // Populate the snode pool if needed if (!pool_valid) { @@ -800,7 +881,7 @@ void Network::with_paths_and_pool( get_service_nodes( request_id, pool_result.front(), - 256, + std::nullopt, handle_nodes_response(std::move(prom))); // We want to block the `get_snode_pool_loop` until we have retrieved @@ -888,13 +969,52 @@ void Network::with_paths_and_pool( // Since we sorted it we now need to shuffle it again std::shuffle(intersection.begin(), intersection.end(), rng); - // Update the cache to be the first 256 nodes from - // the intersection - auto size = std::min(256, static_cast(intersection.size())); - pool_result = std::vector( - intersection.begin(), intersection.begin() + size); + // Update the cache to be the intersection + pool_result = intersection; log::info(cat, "Retrieved snode pool for {}.", request_id); } + + // Since the snode pool has been updated, and we now fetch the entire list, + // we should check the current_swarms and remove any nodes which aren't + // present (as they are potentially decomissioned) + for (auto& [key, nodes] : swarm_result) { + nodes.erase( + std::remove_if( + nodes.begin(), + nodes.end(), + [&](service_node& node) { + auto find_it = std::find_if( + pool_result.begin(), + pool_result.end(), + [&](const service_node& pool_node) { + return static_cast< + const oxen::quic:: + RemoteAddress&>( + node) == + static_cast< + const oxen::quic:: + RemoteAddress&>( + pool_node) && + node.storage_server_version == + pool_node + .storage_server_version; + }); + + if (find_it != pool_result.end()) { + // Node found in pool_result, update it + bool version_changed = + node.storage_server_version != + find_it->storage_server_version; + node = *find_it; + swarms_updated |= version_changed; + return false; // Keep this node + } else { + swarms_updated = true; + return true; // Remove this node + } + }), + nodes.end()); + } } catch (const std::exception& e) { log::info(cat, "Failed to get snode pool for {}: {}", request_id, e.what()); return {{}, {}, e.what()}; @@ -1024,13 +1144,48 @@ void Network::with_paths_and_pool( throw std::runtime_error{"Not enough remaining nodes."}; // Build the new paths + std::vector min_final_node_version = parse_version("2.8.0"); + for (auto& info : valid_nodes) { std::vector path{info.node}; for (auto i = 0; i < path_size - 1; i++) { - auto node = unused_nodes.back(); - unused_nodes.pop_back(); - path.push_back(node); + // If this is the final node then we only want to consider nodes + // which are at least the 'min_final_node_version' + // + // Note: This should be able to be removed after the mandatory + // service node update period for 2.8.0 finishes + if (i == path_size - 2) { + auto it = std::find_if( + unused_nodes.begin(), + unused_nodes.end(), + [min_final_node_version](const service_node& node) { + return node.storage_server_version >= + min_final_node_version; + }); + + if (it != unused_nodes.end()) { + path.push_back(*it); + unused_nodes.erase(it); + break; + } + + // If we couldn't find a suitable node then just fallback to the + // old logic + log::warning( + cat, + "Unable to find suitable final node when building {} " + "path for {}", + path_type_name(path_type, single_path_mode), + request_id); + auto node = unused_nodes.back(); + unused_nodes.pop_back(); + path.push_back(node); + } else { + auto node = unused_nodes.back(); + unused_nodes.pop_back(); + path.push_back(node); + } } paths_result.emplace_back(onion_path{std::move(info), path, 0, 0}); @@ -1041,7 +1196,7 @@ void Network::with_paths_and_pool( path.begin(), path.end(), std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); + [](const service_node& node) { return node.to_string(); }); auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); log::info( cat, @@ -1472,7 +1627,10 @@ void Network::get_service_nodes( nlohmann::json params{ {"active_only", true}, {"fields", - {{"public_ip", true}, {"pubkey_ed25519", true}, {"storage_lmq_port", true}}}}; + {{"public_ip", true}, + {"pubkey_ed25519", true}, + {"storage_lmq_port", true}, + {"storage_server_version", true}}}}; if (limit) params["limit"] = *limit; @@ -1503,10 +1661,26 @@ void Network::get_service_nodes( while (!node.is_finished()) { auto node_consumer = node.consume_dict_consumer(); + auto pubkey_ed25519 = + oxenc::from_hex(consume_string(node_consumer, "pubkey_ed25519")); + auto public_ip = consume_string(node_consumer, "public_ip"); + auto storage_lmq_port = + consume_integer(node_consumer, "storage_lmq_port"); + + std::vector storage_server_version; + node_consumer.skip_until("storage_server_version"); + auto version_consumer = node_consumer.consume_list_consumer(); + + while (!version_consumer.is_finished()) { + storage_server_version.emplace_back( + version_consumer.consume_integer()); + } + result.emplace_back( - oxenc::from_hex(consume_string(node_consumer, "pubkey_ed25519")), - consume_string(node_consumer, "public_ip"), - consume_integer(node_consumer, "storage_lmq_port")); + pubkey_ed25519, + storage_server_version, + public_ip, + storage_lmq_port); } // Output the result @@ -1572,8 +1746,8 @@ void Network::get_swarm( return swarm_cache[swarm_pubkey.hex()]; }); - // If we have a cached swarm then return it - if (cached_swarm) + // If we have a cached swarm, and it meets the minimum size requirements, then return it + if (cached_swarm && cached_swarm->size() >= min_swarm_snode_count) return callback(*cached_swarm); // Pick a random node from the snode pool to fetch the swarm from @@ -2062,7 +2236,7 @@ std::pair> Network::process_v3_onion_respons } if (!oxenc::is_base64(base64_iv_and_ciphertext)) - throw std::runtime_error{"Invalid base64 encoded IV and ciphertext: " + response + "."}; + throw std::runtime_error{"Invalid base64 encoded IV and ciphertext."}; ustring iv_and_ciphertext; oxenc::from_base64( @@ -2281,18 +2455,20 @@ void Network::handle_errors( if (!handle_response || !info.swarm_pubkey) throw std::invalid_argument{"Unable to handle redirect."}; - auto cached_swarm = net.call_get( - [this, swarm_pubkey = *info.swarm_pubkey]() -> std::vector { - return swarm_cache[swarm_pubkey.hex()]; - }); - - if (cached_swarm.empty()) - throw std::invalid_argument{"Unable to handle redirect."}; - // If this was the first 421 then we want to retry using another node in the // swarm to get confirmation that we should switch to a different swarm if (!info.retry_reason || info.retry_reason != request_info::RetryReason::redirect) { + auto cached_swarm = net.call_get( + [this, + swarm_pubkey = *info.swarm_pubkey]() -> std::vector { + return swarm_cache[swarm_pubkey.hex()]; + }); + + if (cached_swarm.empty()) + throw std::invalid_argument{ + "Unable to handle redirect due to lack of swarm."}; + CSRNG rng; std::vector swarm_copy = cached_swarm; std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); @@ -2384,156 +2560,188 @@ void Network::handle_errors( default: break; } - // Check if we got an error specifying the specific node that failed - auto updated_failure_counts = net.call_get( - [this]() -> std::unordered_map { return snode_failure_counts; }); - auto updated_path = info.path; - bool found_invalid_node = false; - - if (response) { - std::optional ed25519PublicKey; - - // Check if the response has one of the 'node_not_found' prefixes - if (response->starts_with(node_not_found_prefix)) - ed25519PublicKey = {response->data() + node_not_found_prefix.size()}; - else if (response->starts_with(node_not_found_prefix_no_status)) - ed25519PublicKey = {response->data() + node_not_found_prefix_no_status.size()}; - - // If we found a result then try to extract the pubkey and process it - if (ed25519PublicKey && ed25519PublicKey->size() == 64 && - oxenc::is_hex(*ed25519PublicKey)) { - session::onionreq::ed25519_pubkey edpk = - session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); - auto edpk_view = to_unsigned_sv(edpk.view()); - - auto snode_it = std::find_if( - updated_path.nodes.begin(), - updated_path.nodes.end(), - [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); - - // If we found an invalid node then store it to increment the failure count - if (snode_it != updated_path.nodes.end()) { - found_invalid_node = true; + // Update the cache (want to wait until this has been completed in case another failure happens + // while this is processing as it might result in missing a failure) + std::condition_variable cv; + std::mutex mtx; + bool done = false; - auto failure_count = - updated_failure_counts.try_emplace(snode_it->to_string(), 0).first->second; - updated_failure_counts[snode_it->to_string()] = failure_count + 1; + // Check if we got an error specifying the specific node that failed + net.call([this, + request_id = info.request_id, + path_type = info.path_type, + target = info.target, + timeout, + old_path = info.path, + response, + &cv, + &mtx, + &done]() mutable { + auto updated_path = old_path; + auto updated_swarm_cache = swarm_cache; + auto updated_failure_counts = snode_failure_counts; + bool found_invalid_node = false; + std::vector nodes_to_drop; + + if (response) { + std::optional ed25519PublicKey; + + // Check if the response has one of the 'node_not_found' prefixes + if (response->starts_with(node_not_found_prefix)) + ed25519PublicKey = {response->data() + node_not_found_prefix.size()}; + else if (response->starts_with(node_not_found_prefix_no_status)) + ed25519PublicKey = {response->data() + node_not_found_prefix_no_status.size()}; + + // If we found a result then try to extract the pubkey and process it + if (ed25519PublicKey && ed25519PublicKey->size() == 64 && + oxenc::is_hex(*ed25519PublicKey)) { + session::onionreq::ed25519_pubkey edpk = + session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); + auto edpk_view = to_unsigned_sv(edpk.view()); + + auto snode_it = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&edpk_view](const auto& node) { + return node.view_remote_key() == edpk_view; + }); - // If the specific node has failed too many times then we should try to repair the - // existing path by replace the bad node with another one - if (updated_failure_counts[snode_it->to_string()] >= snode_failure_threshold) { - try { - // If the node that's gone bad is the guard node then we just have to drop - // the path - if (snode_it == updated_path.nodes.begin()) - throw std::runtime_error{"Cannot recover if guard node is bad"}; - - // Try to find an unused node to patch the path - auto [paths, unused_snodes] = net.call_get( - [this, path_type = info.path_type]() - -> std::pair< - std::vector, - std::vector> { - return {paths_for_type(path_type), snode_pool}; - }); - std::vector path_nodes; - - for (const auto& path : paths) - path_nodes.insert( - path_nodes.end(), path.nodes.begin(), path.nodes.end()); - - unused_snodes.erase( - std::remove_if( - unused_snodes.begin(), - unused_snodes.end(), - [&](const service_node& node) { - return std::find( - path_nodes.begin(), - path_nodes.end(), - node) != path_nodes.end(); - }), - unused_snodes.end()); - - if (unused_snodes.empty()) - throw std::runtime_error{"No remaining nodes"}; - - CSRNG rng; - std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); - - std::replace( - updated_path.nodes.begin(), - updated_path.nodes.end(), - *snode_it, - unused_snodes.front()); - log::info( - cat, - "Found bad node in path for {}, replacing node.", - path_type_name(info.path_type, single_path_mode)); - } catch (...) { - // There aren't enough unused nodes remaining so we need to drop the path - updated_path.failure_count = path_failure_threshold; + // If we found an invalid node then store it to increment the failure count + if (snode_it != updated_path.nodes.end()) { + found_invalid_node = true; + + auto failure_count = + updated_failure_counts.try_emplace(snode_it->to_string(), 0) + .first->second; + updated_failure_counts[snode_it->to_string()] = failure_count + 1; + + // If the specific node has failed too many times then we should try to repair + // the existing path by replace the bad node with another one + if (updated_failure_counts[snode_it->to_string()] >= snode_failure_threshold) { + nodes_to_drop.emplace_back(*snode_it); + + try { + // If the node that's gone bad is the guard node then we just have to + // drop the path + if (snode_it == updated_path.nodes.begin()) + throw std::runtime_error{"Cannot recover if guard node is bad"}; + + // Try to find an unused node to patch the path + auto paths = paths_for_type(path_type); + auto unused_snodes = snode_pool; + std::vector path_nodes; + + for (const auto& path : paths) + path_nodes.insert( + path_nodes.end(), path.nodes.begin(), path.nodes.end()); + + unused_snodes.erase( + std::remove_if( + unused_snodes.begin(), + unused_snodes.end(), + [&](const service_node& node) { + return std::find( + path_nodes.begin(), + path_nodes.end(), + node) != path_nodes.end(); + }), + unused_snodes.end()); + + if (unused_snodes.empty()) + throw std::runtime_error{"No remaining nodes"}; + + CSRNG rng; + std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); + + std::replace( + updated_path.nodes.begin(), + updated_path.nodes.end(), + *snode_it, + unused_snodes.front()); + log::info( + cat, + "Found bad node in path for {}, replacing node.", + path_type_name(path_type, single_path_mode)); + } catch (...) { + // There aren't enough unused nodes remaining so we need to drop the + // path + updated_path.failure_count = path_failure_threshold; - log::info( - cat, - "Unable to replace bad node in path for {}.", - path_type_name(info.path_type, single_path_mode)); + log::info( + cat, + "Unable to replace bad node in path for {}.", + path_type_name(path_type, single_path_mode)); + } } } } } - } - // If we didn't find the specific node or the paths connection was closed then increment the - // path failure count - if (!found_invalid_node || !updated_path.conn_info.is_valid()) { - if (timeout) - updated_path.timeout_count += 1; - else - updated_path.failure_count += 1; - - // If the path has failed or timed out too many times we want to drop the guard - // snode (marking it as invalid) and increment the failure count of each node in - // the path) - if (updated_path.failure_count >= path_failure_threshold || - updated_path.timeout_count >= path_timeout_threshold) { - for (auto& it : updated_path.nodes) { + // If we didn't find the specific node or the paths connection was closed then increment the + // path failure count + if (!found_invalid_node || !updated_path.conn_info.is_valid()) { + if (timeout) + updated_path.timeout_count += 1; + else + updated_path.failure_count += 1; + + // If the path has failed or timed out too many times we want to drop the guard + // snode (marking it as invalid) and increment the failure count of each node in + // the path) + if (updated_path.failure_count >= path_failure_threshold || + updated_path.timeout_count >= path_timeout_threshold) { + for (auto& it : updated_path.nodes) { + auto failure_count = + updated_failure_counts.try_emplace(it.to_string(), 0).first->second; + updated_failure_counts[it.to_string()] = failure_count + 1; + + if (updated_failure_counts[it.to_string()] >= snode_failure_threshold) + nodes_to_drop.emplace_back(it); + } + + // Set the failure count of the guard node to match the threshold so we drop it + updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; + nodes_to_drop.emplace_back(updated_path.nodes[0]); + } else if (updated_path.nodes.size() < path_size) { + // If the path doesn't have enough nodes then it's likely that this failure was + // triggered when trying to establish a new path and, as such, we should increase + // the failure count of the guard node since it is probably invalid auto failure_count = - updated_failure_counts.try_emplace(it.to_string(), 0).first->second; - updated_failure_counts[it.to_string()] = failure_count + 1; - } + updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) + .first->second; + updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; - // Set the failure count of the guard node to match the threshold so we drop it - updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; - } else if (updated_path.nodes.size() < path_size) { - // If the path doesn't have enough nodes then it's likely that this failure was - // triggered when trying to establish a new path and, as such, we should increase the - // failure count of the guard node since it is probably invalid - auto failure_count = - updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) - .first->second; - updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; + if (updated_failure_counts[updated_path.nodes[0].to_string()] >= + snode_failure_threshold) + nodes_to_drop.emplace_back(updated_path.nodes[0]); + } } - } - // Update the cache (want to wait until this has been completed incase) - std::condition_variable cv; - std::mutex mtx; - bool done = false; + // Remove any nodes from 'nodes_to_drop' which don't actually need to be dropped + bool requires_swarm_cache_update = false; - net.call([this, - request_id = info.request_id, - path_type = info.path_type, - target_node = info.target, - swarm_pubkey = info.swarm_pubkey, - old_path = info.path, - updated_failure_counts, - updated_path, - &cv, - &mtx, - &done]() mutable { - auto already_handled_failure = false; + if (!nodes_to_drop.empty()) { + for (auto& [key, nodes] : updated_swarm_cache) { + for (const auto& drop_node : nodes_to_drop) { + auto it = std::remove(nodes.begin(), nodes.end(), drop_node); + if (it != nodes.end()) { + nodes.erase(it, nodes.end()); + requires_swarm_cache_update = true; + } + } + } + } + for (const auto& [key, value] : updated_failure_counts) + log::info(cat, "RAWR1 failure count for {} is {}.", key, value); + log::info(cat, "RAWR2 Dropping {} nodes.", nodes_to_drop.size()); + // No need to track the failure counts of nodes which have been dropped, or haven't failed + std::erase_if(updated_failure_counts, [](const auto& item) { + return item.second == 0 || item.second >= snode_failure_threshold; + }); // Drop the path if invalid + auto already_handled_failure = false; + if (updated_path.failure_count >= path_failure_threshold || updated_path.timeout_count >= path_timeout_threshold) { auto old_paths_size = paths_for_type(path_type).size(); @@ -2583,7 +2791,7 @@ void Network::handle_errors( path_description); else { // If the path was already dropped then the snode pool would have already been - // updated so update the `already_handled_failure` to avoid double-handle the + // updated so update the `already_handled_failure` to avoid double-handling the // failure already_handled_failure = true; log::info( @@ -2611,68 +2819,49 @@ void Network::handle_errors( } } - // If we hadn't already handled the failure then update the failure counts and connection - // status - if (!already_handled_failure) { - // Update the snode failure counts - snode_failure_counts = updated_failure_counts; - - // Update the network status if we've removed all standard paths - if (standard_paths.empty()) - update_status(ConnectionStatus::disconnected); - } - - // Since we've finished updating the path and failure count states we can stop blocking - // the caller (no need to wait for the snode cache to update) - { - std::lock_guard lock(mtx); - done = true; - } - cv.notify_one(); - // We've already handled the failure so don't update the cache - if (already_handled_failure) + if (already_handled_failure) { + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); return; + } + + // Update the network status if we've removed all standard paths + if (standard_paths.empty()) + update_status(ConnectionStatus::disconnected); // Update the snode cache { std::lock_guard lock{snode_cache_mutex}; - for (size_t i = 0; i < updated_path.nodes.size(); ++i) - if (updated_failure_counts.try_emplace(updated_path.nodes[i].to_string(), 0) - .first->second >= snode_failure_threshold) { - snode_pool.erase( - std::remove(snode_pool.begin(), snode_pool.end(), old_path.nodes[i]), - snode_pool.end()); - - if (swarm_pubkey) - if (swarm_cache.contains(swarm_pubkey->hex())) { - auto updated_swarm = swarm_cache[swarm_pubkey->hex()]; - updated_swarm.erase( - std::remove( - updated_swarm.begin(), - updated_swarm.end(), - old_path.nodes[i]), - updated_swarm.end()); - swarm_cache[swarm_pubkey->hex()] = updated_swarm; - } - } else - std::replace( - snode_pool.begin(), - snode_pool.end(), - old_path.nodes[i], - updated_path.nodes[i]); + // Update the snode failure counts + snode_failure_counts = updated_failure_counts; + need_failure_counts_write = true; + need_swarm_write = requires_swarm_cache_update; - // If the target node is invalid then remove it from the snode pool - if (updated_failure_counts[target_node.to_string()] >= snode_failure_threshold) + if (requires_swarm_cache_update) + swarm_cache = updated_swarm_cache; + + for (const auto& node : nodes_to_drop) { snode_pool.erase( - std::remove(snode_pool.begin(), snode_pool.end(), target_node), - snode_pool.end()); + std::remove(snode_pool.begin(), snode_pool.end(), node), snode_pool.end()); + need_pool_write = true; + } - need_pool_write = true; - need_swarm_write = (swarm_pubkey && swarm_cache.contains(swarm_pubkey->hex())); need_write = true; } + + // Now that the cache is updated we can unblock this thread + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); + + // We also need to notify the disk write thread that it can persist the changes to disk snode_cache_cv.notify_one(); }); @@ -2691,8 +2880,12 @@ std::vector convert_service_nodes( std::vector converted_nodes; for (auto& node : nodes) { auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); + auto ipv4 = node.to_ipv4(); network_service_node converted_node; - std::memcpy(converted_node.ip, node.host().data(), sizeof(converted_node.ip)); + converted_node.ip[0] = (ipv4.addr >> 24) & 0xFF; + converted_node.ip[1] = (ipv4.addr >> 16) & 0xFF; + converted_node.ip[2] = (ipv4.addr >> 8) & 0xFF; + converted_node.ip[3] = ipv4.addr & 0xFF; strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination converted_node.quic_port = node.port(); @@ -2869,6 +3062,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( unbox(network).send_onion_request( service_node{ oxenc::from_hex({node.ed25519_pubkey_hex, 64}), + {0}, // For a destination node we don't care about the version "{}"_format(fmt::join(ip, ".")), node.quic_port}, body, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 0fde1f5b..9e3e434d 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -21,6 +21,7 @@ #include #include "session/export.h" +#include "session/network.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/hop_encryption.hpp" #include "session/onionreq/key_types.hpp" @@ -54,7 +55,7 @@ EncryptType parse_enc_type(std::string_view enc_type) { void Builder::set_destination(network_destination destination) { ed25519_public_key_.reset(); - if (auto* dest = std::get_if(&destination)) + if (auto* dest = std::get_if(&destination)) ed25519_public_key_.emplace(ed25519_pubkey::from_bytes(dest->view_remote_key())); else if (auto* dest = std::get_if(&destination)) { host_.emplace(dest->host); @@ -289,8 +290,9 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( std::array target_ip; std::memcpy(target_ip.data(), ip, target_ip.size()); - unbox(builder).set_destination(oxen::quic::RemoteAddress( + unbox(builder).set_destination(session::network::service_node( oxenc::from_hex({ed25519_pubkey, 64}), + {0}, "{}"_format(fmt::join(target_ip, ".")), quic_port)); } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 6c3ee422..c295ba01 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -119,10 +119,10 @@ TEST_CASE("Network error handling", "[network]") { "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto x_pk_hex = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"; - auto target = service_node{ed_pk, "0.0.0.0", uint16_t{0}}; - auto target2 = service_node{ed_pk2, "0.0.0.1", uint16_t{1}}; - auto target3 = service_node{ed_pk2, "0.0.0.2", uint16_t{2}}; - auto target4 = service_node{ed_pk2, "0.0.0.3", uint16_t{3}}; + auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; + auto target2 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; + auto target3 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.2", uint16_t{2}}; + auto target4 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.3", uint16_t{3}}; auto path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", @@ -231,7 +231,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); - CHECK(network.get_failure_count(target) == 3); // Guard node should be set to failure threshold + CHECK(network.get_failure_count(target) == 0); // Guard node will have been dropped CHECK(network.get_failure_count(target2) == 1); // Other nodes get their failure count incremented CHECK(network.get_failure_count(target3) == @@ -317,7 +317,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(result.status_code == 500); CHECK(result.response == response); CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 10); + CHECK(network.get_failure_count(target2) == 0); // Node will have been dropped CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Incremented because conn_info is invalid @@ -411,6 +411,14 @@ TEST_CASE("Network error handling", "[network]") { {{"ip", "1.1.1.1"}, {"port_omq", 1}, {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); + snodes.push_back( + {{"ip", "2.2.2.2"}, + {"port_omq", 2}, + {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); + snodes.push_back( + {{"ip", "3.3.3.3"}, + {"port_omq", 3}, + {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); nlohmann::json swarm_json{{"snodes", snodes}}; response = swarm_json.dump(); network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target}); @@ -440,7 +448,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 0); network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm) { - REQUIRE(swarm.size() == 1); + REQUIRE(swarm.size() == 3); CHECK(swarm.front().to_string() == "1.1.1.1:1"); CHECK(oxenc::to_hex(swarm.front().view_remote_key()) == oxenc::to_hex(ed_pk)); }); @@ -504,6 +512,7 @@ TEST_CASE("Network error handling", "[network]") { TEST_CASE("Network onion request", "[send_onion_request][network]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + {2, 8, 0}, "144.76.164.202", uint16_t{35400}}; auto network = Network(std::nullopt, true, true, false); From e446eda014495e865b5450eb45511af21f93b12c Mon Sep 17 00:00:00 2001 From: yougotwill Date: Wed, 24 Jul 2024 11:54:31 +1000 Subject: [PATCH 332/572] feat: moved community url max length and qs_pubkey to cpp header it is now used in libsession_util_nodejs --- include/session/config/community.hpp | 4 ++++ src/config/community.cpp | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index a3110cbd..d6d9b27f 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -18,6 +18,10 @@ struct community { // 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX') static constexpr size_t BASE_URL_MAX_LENGTH = 267; static constexpr size_t ROOM_MAX_LENGTH = 64; + static constexpr std::string_view qs_pubkey{"?public_key="}; + static const size_t FULL_URL_MAX_LENGTH = BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + + ROOM_MAX_LENGTH + qs_pubkey.size() + + 64 /*pubkey hex*/ + 1 /*null terminator*/; community() = default; diff --git a/src/config/community.cpp b/src/config/community.cpp index d6c2ed85..4c1a32e5 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -74,8 +74,6 @@ void community::set_room(std::string_view room) { localized_room_ = room; } -static constexpr std::string_view qs_pubkey{"?public_key="}; - std::string community::full_url() const { return full_url(base_url(), room(), pubkey()); } @@ -230,8 +228,7 @@ LIBSESSION_C_API const size_t COMMUNITY_BASE_URL_MAX_LENGTH = LIBSESSION_C_API const size_t COMMUNITY_ROOM_MAX_LENGTH = session::config::community::ROOM_MAX_LENGTH; LIBSESSION_C_API const size_t COMMUNITY_FULL_URL_MAX_LENGTH = - COMMUNITY_BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + COMMUNITY_ROOM_MAX_LENGTH + - session::config::qs_pubkey.size() + 64 /*pubkey hex*/ + 1 /*null terminator*/; + session::config::community::FULL_URL_MAX_LENGTH; LIBSESSION_C_API bool community_parse_full_url( const char* full_url, char* base_url, char* room_token, unsigned char* pubkey) { From 67e7158e79678ba304ef330071be7063213fd66f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 24 Jul 2024 23:25:26 -0300 Subject: [PATCH 333/572] allow protobuf to come from the system lib --- .drone.jsonnet | 2 +- external/CMakeLists.txt | 31 +++++++++++++++++-------------- proto/CMakeLists.txt | 17 ----------------- 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index b05e7668..a411141f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,7 +10,7 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local libngtcp2_deps = ['libgnutls28-dev']; +local libngtcp2_deps = ['libgnutls28-dev', 'libprotobuf-dev']; local default_deps_nocxx = [ 'nlohmann-json3-dev', diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 7344e118..0327de9d 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -33,19 +33,6 @@ if(SUBMODULE_CHECK) endif() endif() -set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) -set(protobuf_INSTALL ON CACHE BOOL "" FORCE) -set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) -set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") -set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) -add_subdirectory(protobuf) - - if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() @@ -67,7 +54,7 @@ macro(system_or_submodule BIGNAME smallname pkgconf subdir) message(STATUS "using ${smallname} submodule") add_subdirectory(${subdir}) endif() - if(NOT TARGET ${smallname}::${smallname}) + if(TARGET ${smallname} AND NOT TARGET ${smallname}::${smallname}) add_library(${smallname}::${smallname} ALIAS ${smallname}) endif() endmacro() @@ -146,6 +133,22 @@ libsodium_internal_subdir() libsession_static_bundle(libsodium::sodium-internal) +set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) +set(protobuf_INSTALL ON CACHE BOOL "" FORCE) +set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) +set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") +set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) +system_or_submodule(PROTOBUF_LITE protobuf_lite protobuf-lite>=3.21 protobuf) +if(TARGET PkgConfig::PROTOBUF_LITE AND NOT TARGET protobuf::libprotobuf-lite) + add_library(protobuf::libprotobuf-lite ALIAS PkgConfig::PROTOBUF_LITE) +endif() + + set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "") set(ZSTD_BUILD_TESTS OFF CACHE BOOL "") set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "") diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index a99ded2f..e7a05a5c 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -1,21 +1,4 @@ -function(check_target target) - if (NOT TARGET ${target}) - message(FATAL_ERROR "Project failed to compile required target: ${target}") - endif() -endfunction() - -if (BUILD_SHARED_LIBS AND NOT BUILD_STATIC_DEPS) - find_package(PkgConfig REQUIRED) - pkg_check_modules(PROTOBUF_LITE protobuf-lite>=3.21 IMPORTED_TARGET) - if(PROTOBUF_LITE_FOUND) - add_library(protobuf_lite INTERFACE IMPORTED) - target_link_libraries(protobuf_lite INTERFACE PkgConfig::PROTOBUF_LITE) - add_library(protobuf::libprotobuf-lite ALIAS protobuf_lite) - endif() -endif() - -check_target(protobuf::libprotobuf-lite) libsession_static_bundle(protobuf::libprotobuf-lite) From ca1b01bab9d8d072b4d430cb23d9eacf27b755d4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:18:52 +1000 Subject: [PATCH 334/572] Added functions for generating a blinded id for version check auth --- include/session/blinding.h | 32 +++++++++++++++ include/session/blinding.hpp | 19 +++++++++ src/blinding.cpp | 80 ++++++++++++++++++++++++++++++++++++ tests/test_blinding.cpp | 20 +++++++++ 4 files changed, 151 insertions(+) diff --git a/include/session/blinding.h b/include/session/blinding.h index 715ec8d9..cf51921b 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -50,6 +50,24 @@ LIBSESSION_EXPORT bool session_blind25_key_pair( unsigned char* blinded_pk_out, /* 32 byte output buffer */ unsigned char* blinded_sk_out /* 32 byte output buffer */); +/// API: crypto/session_blind_version_key_pair +/// +/// This function attempts to generate a blind-version key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the user (64 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 64 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind_version_key_pair( + const unsigned char* ed25519_seckey, /* 64 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 64 byte output buffer */); + /// API: crypto/session_blind15_sign /// /// This function attempts to generate a signature for a message using a blind15 private key. @@ -94,6 +112,20 @@ LIBSESSION_EXPORT bool session_blind25_sign( size_t msg_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes). Returns the blinded public key, signature and +/// timestamp. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +LIBSESSION_EXPORT bool session_blind_version_sign( + const unsigned char* ed25519_seckey, /* 64 bytes */ + CLIENT_PLATFORM platform, + size_t timestamp, + unsigned char* blinded_sig_out /* 64 byte output buffer */); + /// API: crypto/session_blind25_sign /// /// This function attempts to generate a signature for a message using a blind25 private key. diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index c6e41cf5..5e1de446 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -138,6 +138,15 @@ std::pair blind15_key_pair( std::pair blind25_key_pair( ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); +/// Computes a version-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key and +/// private key (NOT a seed). +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind_version_key_pair(ustring_view ed25519_sk); + /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). /// @@ -158,6 +167,16 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key, +/// signature and timestamp. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp); + /// Takes in a standard session_id and returns a flag indicating whether it matches the given /// blinded_id for a given server_pk. /// diff --git a/src/blinding.cpp b/src/blinding.cpp index a9892fe8..2e5eac2a 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -298,6 +298,37 @@ std::pair blind25_key_pair( return result; } +static const auto version_blinding_hash_key_sig = to_unsigned_sv("VersionCheckKey_sig"sv); + +std::pair blind_version_key_pair(ustring_view ed25519_sk) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " + "value"}; + + uc32 seed; + crypto_generichash_blake2b( + seed.data(), + seed.size(), + ed25519_sk.data(), + 32, + version_blinding_hash_key_sig.data(), + version_blinding_hash_key_sig.size()); + + uc32 pk; + cleared_uc64 sk; + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data())) + throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; + + return {pk, sk}; +} + static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); @@ -427,6 +458,24 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } +ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp) { + auto [pk, sk] = blind_version_key_pair(ed25519_sk); + + // Signature should be on `TIMESTAMP || METHOD || PATH` + ustring buf; + buf.reserve(10 + 6 + 33); + buf += to_unsigned_sv(std::to_string(timestamp)); + buf += to_unsigned("GET"); + + switch (platform) { + case Platform::android: buf += to_unsigned("/session_version?platform=android"); break; + case Platform::desktop: buf += to_unsigned("/session_version?platform=desktop"); break; + case Platform::ios: buf += to_unsigned("/session_version?platform=ios"); break; + } + + return ed25519::sign({sk.data(), sk.size()}, buf); +} + bool session_id_matches_blinded_id( std::string_view session_id, std::string_view blinded_id, std::string_view server_pk) { if (session_id.size() != 66 || !oxenc::is_hex(session_id)) @@ -492,6 +541,21 @@ LIBSESSION_C_API bool session_blind25_key_pair( } } +LIBSESSION_C_API bool session_blind_version_key_pair( + const unsigned char* ed25519_seckey, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { + try { + auto result = session::blind_version_key_pair({ed25519_seckey, 64}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_blind15_sign( const unsigned char* ed25519_seckey, const unsigned char* server_pk, @@ -526,6 +590,22 @@ LIBSESSION_C_API bool session_blind25_sign( } } +LIBSESSION_C_API bool session_blind_version_sign( + const unsigned char* ed25519_seckey, + CLIENT_PLATFORM platform, + size_t timestamp, + unsigned char* blinded_sig_out) { + try { + auto result = session::blind_version_sign( + {ed25519_seckey, 64}, static_cast(platform), timestamp); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_id_matches_blinded_id( const char* session_id, const char* blinded_id, const char* server_pk) { try { diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 0b347735..aa9bb89a 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -271,6 +271,26 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); } +TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { + REQUIRE(sodium_init() >= 0); + + auto [pubkey, seckey] = blind_version_key_pair(to_usv(seed1)); + CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == + "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); + CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == + "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f6988e8adb27e7b8ce776fcc25b" + "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); +} + +TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { + REQUIRE(sodium_init() >= 0); + + auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); + CHECK(oxenc::to_hex(signature.begin(), signature.end()) == + "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e920fa57daf4627c68f43fcbddb" + "2d465d5ea11def523f3befb2bbee39c769676305"); +} + TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { std::array server_pks = { "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, From c89077f4ecd0b26e6c4376fd614c225f5de3397d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:34:49 +1000 Subject: [PATCH 335/572] Added missing headers --- include/session/platform.h | 15 +++++++++++++++ include/session/platform.hpp | 11 +++++++++++ 2 files changed, 26 insertions(+) create mode 100644 include/session/platform.h create mode 100644 include/session/platform.hpp diff --git a/include/session/platform.h b/include/session/platform.h new file mode 100644 index 00000000..3c89614b --- /dev/null +++ b/include/session/platform.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum CLIENT_PLATFORM { + CLIENT_PLATFORM_ANDROID = 0, + CLIENT_PLATFORM_DESKTOP = 1, + CLIENT_PLATFORM_IOS = 2, +} CLIENT_PLATFORM; + +#ifdef __cplusplus +} +#endif diff --git a/include/session/platform.hpp b/include/session/platform.hpp new file mode 100644 index 00000000..46463f74 --- /dev/null +++ b/include/session/platform.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace session { + +enum class Platform { + android, + desktop, + ios, +}; + +} // namespace session From b0808817da5509dc432ec520ae9199cbc1ed12b4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:46:04 +1000 Subject: [PATCH 336/572] Added missing headers --- include/session/blinding.hpp | 2 ++ src/blinding.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 5e1de446..9d3def8a 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -4,6 +4,8 @@ #include #include "util.hpp" +#include "platform.hpp" +#include "sodium_array.hpp" namespace session { diff --git a/src/blinding.cpp b/src/blinding.cpp index 2e5eac2a..e1a39fbe 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -9,7 +9,10 @@ #include #include +#include "session/ed25519.hpp" #include "session/export.h" +#include "session/platform.h" +#include "session/platform.hpp" #include "session/xed25519.hpp" namespace session { From 04f49d6cfa94066831d0a40f20348691b4e476af Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:55:07 +1000 Subject: [PATCH 337/572] Added new sodium_array header changes --- include/session/blinding.hpp | 1 - include/session/config/base.hpp | 1 + include/session/multi_encrypt.hpp | 2 +- include/session/sodium_array.hpp | 231 ++++++++++++++++++++++++++++++ include/session/util.hpp | 223 ---------------------------- src/ed25519.cpp | 2 +- src/session_encrypt.cpp | 2 +- src/sodium_array.cpp | 23 +++ 8 files changed, 258 insertions(+), 227 deletions(-) create mode 100644 include/session/sodium_array.hpp create mode 100644 src/sodium_array.cpp diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 9d3def8a..d2d67d33 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -3,7 +3,6 @@ #include #include -#include "util.hpp" #include "platform.hpp" #include "sodium_array.hpp" diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 138ba51f..a0860306 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -10,6 +10,7 @@ #include #include +#include "../sodium_array.hpp" #include "base.h" #include "namespaces.hpp" diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 7d73dfed..533f055e 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -8,8 +8,8 @@ #include #include +#include "sodium_array.hpp" #include "types.hpp" -#include "util.hpp" // Helper functions for implementing multiply encrypted messages by creating separate copies of the // message for each message recipient. This is used most prominently in group key update messages diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp new file mode 100644 index 00000000..a0a52de0 --- /dev/null +++ b/include/session/sodium_array.hpp @@ -0,0 +1,231 @@ +#pragma once + +#include "util.hpp" + +namespace session { + +// Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure +void* sodium_buffer_allocate(size_t size); +// Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. +void sodium_buffer_deallocate(void* p); +// Calls sodium_memzero to zero a buffer +void sodium_zero_buffer(void* ptr, size_t size); + +// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but +// more secure for sensitive data). +template +struct sodium_ptr { + private: + T* x; + + public: + sodium_ptr() : x{nullptr} {} + sodium_ptr(std::nullptr_t) : sodium_ptr{} {} + ~sodium_ptr() { reset(x); } + + // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` + // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly + // to destruct and deallocate the existing object. + template + T& emplace(Args&&... args) { + if (x) + reset(); + x = static_cast(sodium_buffer_allocate(sizeof(T))); + new (x) T(std::forward(args)...); + return *x; + } + + void reset() { + if (x) { + x->~T(); + sodium_buffer_deallocate(x); + x = nullptr; + } + } + void operator=(std::nullptr_t) { reset(); } + + T& operator*() { return *x; } + const T& operator*() const { return *x; } + + T* operator->() { return x; } + const T* operator->() const { return x; } + + explicit operator bool() const { return x != nullptr; } +}; + +// Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only +// be used with trivially destructible types. +template >> +struct sodium_cleared : T { + using T::T; + + ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } +}; + +template +using cleared_array = sodium_cleared>; + +using cleared_uc32 = cleared_array<32>; +using cleared_uc64 = cleared_array<64>; + +// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation +// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes +// extra precautions, intended for storing sensitive values. +template +struct sodium_array { + private: + T* buf; + size_t len; + + public: + // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). + sodium_array() : buf{nullptr}, len{0} {} + + // Constructs an array with a given size, default-constructing the individual elements. + template >> + explicit sodium_array(size_t length) : + buf{length == 0 ? nullptr + : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, + len{0} { + + if (length > 0) { + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else if constexpr (std::is_nothrow_default_constructible_v) { + for (; len < length; len++) + new (buf[len]) T(); + } else { + try { + for (; len < length; len++) + new (buf[len]) T(); + } catch (...) { + reset(); + throw; + } + } + } + } + + ~sodium_array() { reset(); } + + // Moveable: ownership is transferred to the new object and the old object becomes empty. + sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { + other.buf = nullptr; + other.len = 0; + } + sodium_array& operator=(sodium_array&& other) { + sodium_buffer_deallocate(buf); + buf = other.buf; + len = other.len; + other.buf = nullptr; + other.len = 0; + } + + // Non-copyable + sodium_array(const sodium_array&) = delete; + sodium_array& operator=(const sodium_array&) = delete; + + // Destroys the held array; after destroying elements the allocated space is overwritten with + // 0s before being deallocated. + void reset() { + if (buf) { + if constexpr (!std::is_trivially_destructible_v) + while (len > 0) + buf[--len].~T(); + + sodium_buffer_deallocate(buf); + } + buf = nullptr; + len = 0; + } + + // Calls reset() to destroy the current value (if any) and then allocates a new + // default-constructed one of the given size. + template >> + void reset(size_t length) { + reset(); + if (length > 0) { + buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); + if constexpr (std::is_trivial_v) { + std::memset(buf, 0, length * sizeof(T)); + len = length; + } else { + for (; len < length; len++) + new (buf[len]) T(); + } + } + } + + // Loads the array from a pointer and size; this first resets a value (if present), allocates a + // new array of the given size, the copies the given value(s) into the new buffer. T must be + // copyable. This is *not* safe to use if `buf` points into the currently allocated data. + template >> + void load(const T* data, size_t length) { + reset(length); + if (length == 0) + return; + + if constexpr (std::is_trivially_copyable_v) + std::memcpy(buf, data, sizeof(T) * length); + else + for (; len < length; len++) + new (buf[len]) T(data[len]); + } + + const T& operator[](size_t i) const { + assert(i < len); + return buf[i]; + } + T& operator[](size_t i) { + assert(i < len); + return buf[i]; + } + + T* data() { return buf; } + const T* data() const { return buf; } + + size_t size() const { return len; } + bool empty() const { return len == 0; } + explicit operator bool() const { return !empty(); } + + T* begin() { return buf; } + const T* begin() const { return buf; } + T* end() { return buf + len; } + const T* end() const { return buf + len; } + + using difference_type = ptrdiff_t; + using value_type = T; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::random_access_iterator_tag; +}; + +// sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with +// sensitive data. It is as a result slower and has more overhead than a standard allocator and +// intended for use with a container (such as std::vector) when storing keys. +template +struct sodium_allocator { + using value_type = T; + + [[nodiscard]] static T* allocate(std::size_t n) { + return static_cast(sodium_buffer_allocate(n * sizeof(T))); + } + + static void deallocate(T* p, std::size_t) { sodium_buffer_deallocate(p); } + + template + bool operator==(const sodium_allocator&) const noexcept { + return true; + } + template + bool operator!=(const sodium_allocator&) const noexcept { + return false; + } +}; + +/// Vector that uses sodium's secure (but heavy) memory allocations +template +using sodium_vector = std::vector>; + +} // namespace session diff --git a/include/session/util.hpp b/include/session/util.hpp index 6373546c..22513ff9 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -87,232 +87,9 @@ inline constexpr bool end_with(std::string_view str, std::string_view suffix) { return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix; } -// Calls sodium_malloc for secure allocation; throws a std::bad_alloc on allocation failure -void* sodium_buffer_allocate(size_t size); -// Frees a pointer constructed with sodium_buffer_allocate. Does nothing if `p` is nullptr. -void sodium_buffer_deallocate(void* p); -// Calls sodium_memzero to zero a buffer -void sodium_zero_buffer(void* ptr, size_t size); - -// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but -// more secure for sensitive data). -template -struct sodium_ptr { - private: - T* x; - - public: - sodium_ptr() : x{nullptr} {} - sodium_ptr(std::nullptr_t) : sodium_ptr{} {} - ~sodium_ptr() { reset(x); } - - // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` - // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly - // to destruct and deallocate the existing object. - template - T& emplace(Args&&... args) { - if (x) - reset(); - x = static_cast(sodium_buffer_allocate(sizeof(T))); - new (x) T(std::forward(args)...); - return *x; - } - - void reset() { - if (x) { - x->~T(); - sodium_buffer_deallocate(x); - x = nullptr; - } - } - void operator=(std::nullptr_t) { reset(); } - - T& operator*() { return *x; } - const T& operator*() const { return *x; } - - T* operator->() { return x; } - const T* operator->() const { return x; } - - explicit operator bool() const { return x != nullptr; } -}; - -// Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only -// be used with trivially destructible types. -template >> -struct sodium_cleared : T { - using T::T; - - ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } -}; - -template -using cleared_array = sodium_cleared>; - using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; - -// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation -// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes -// extra precautions, intended for storing sensitive values. -template -struct sodium_array { - private: - T* buf; - size_t len; - - public: - // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). - sodium_array() : buf{nullptr}, len{0} {} - - // Constructs an array with a given size, default-constructing the individual elements. - template >> - explicit sodium_array(size_t length) : - buf{length == 0 ? nullptr - : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, - len{0} { - - if (length > 0) { - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else if constexpr (std::is_nothrow_default_constructible_v) { - for (; len < length; len++) - new (buf[len]) T(); - } else { - try { - for (; len < length; len++) - new (buf[len]) T(); - } catch (...) { - reset(); - throw; - } - } - } - } - - ~sodium_array() { reset(); } - - // Moveable: ownership is transferred to the new object and the old object becomes empty. - sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { - other.buf = nullptr; - other.len = 0; - } - sodium_array& operator=(sodium_array&& other) { - sodium_buffer_deallocate(buf); - buf = other.buf; - len = other.len; - other.buf = nullptr; - other.len = 0; - } - - // Non-copyable - sodium_array(const sodium_array&) = delete; - sodium_array& operator=(const sodium_array&) = delete; - - // Destroys the held array; after destroying elements the allocated space is overwritten with - // 0s before being deallocated. - void reset() { - if (buf) { - if constexpr (!std::is_trivially_destructible_v) - while (len > 0) - buf[--len].~T(); - - sodium_buffer_deallocate(buf); - } - buf = nullptr; - len = 0; - } - - // Calls reset() to destroy the current value (if any) and then allocates a new - // default-constructed one of the given size. - template >> - void reset(size_t length) { - reset(); - if (length > 0) { - buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else { - for (; len < length; len++) - new (buf[len]) T(); - } - } - } - - // Loads the array from a pointer and size; this first resets a value (if present), allocates a - // new array of the given size, the copies the given value(s) into the new buffer. T must be - // copyable. This is *not* safe to use if `buf` points into the currently allocated data. - template >> - void load(const T* data, size_t length) { - reset(length); - if (length == 0) - return; - - if constexpr (std::is_trivially_copyable_v) - std::memcpy(buf, data, sizeof(T) * length); - else - for (; len < length; len++) - new (buf[len]) T(data[len]); - } - - const T& operator[](size_t i) const { - assert(i < len); - return buf[i]; - } - T& operator[](size_t i) { - assert(i < len); - return buf[i]; - } - - T* data() { return buf; } - const T* data() const { return buf; } - - size_t size() const { return len; } - bool empty() const { return len == 0; } - explicit operator bool() const { return !empty(); } - - T* begin() { return buf; } - const T* begin() const { return buf; } - T* end() { return buf + len; } - const T* end() const { return buf + len; } - - using difference_type = ptrdiff_t; - using value_type = T; - using pointer = value_type*; - using reference = value_type&; - using iterator_category = std::random_access_iterator_tag; -}; - -// sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with -// sensitive data. It is as a result slower and has more overhead than a standard allocator and -// intended for use with a container (such as std::vector) when storing keys. -template -struct sodium_allocator { - using value_type = T; - - [[nodiscard]] static T* allocate(std::size_t n) { - return static_cast(sodium_buffer_allocate(n * sizeof(T))); - } - - static void deallocate(T* p, std::size_t) { sodium_buffer_deallocate(p); } - - template - bool operator==(const sodium_allocator&) const noexcept { - return true; - } - template - bool operator!=(const sodium_allocator&) const noexcept { - return false; - } -}; - -/// Vector that uses sodium's secure (but heavy) memory allocations -template -using sodium_vector = std::vector>; template using string_view_char_type = std::conditional_t< diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 87297b09..a4483a2f 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -6,7 +6,7 @@ #include #include "session/export.h" -#include "session/util.hpp" +#include "session/sodium_array.hpp" namespace session::ed25519 { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 87d043b0..fdb831f6 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -17,7 +17,7 @@ #include #include "session/blinding.hpp" -#include "session/util.hpp" +#include "session/sodium_array.hpp" using namespace std::literals; diff --git a/src/sodium_array.cpp b/src/sodium_array.cpp new file mode 100644 index 00000000..14ce50eb --- /dev/null +++ b/src/sodium_array.cpp @@ -0,0 +1,23 @@ +#include + +#include + +namespace session { + +void* sodium_buffer_allocate(size_t length) { + if (auto* p = sodium_malloc(length)) + return p; + throw std::bad_alloc{}; +} + +void sodium_buffer_deallocate(void* p) { + if (p) + sodium_free(p); +} + +void sodium_zero_buffer(void* ptr, size_t size) { + if (ptr) + sodium_memzero(ptr, size); +} + +} // namespace session From f4c8d46dfaca8946f6269de339f07718c7154d1c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 09:00:57 +1000 Subject: [PATCH 338/572] Addressed PR comments --- src/CMakeLists.txt | 1 + src/blinding.cpp | 40 ++++++++++++++-------------------------- src/util.cpp | 15 --------------- tests/test_blinding.cpp | 6 +++--- 4 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c3f1158..4dbc11a8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,7 @@ add_libsession_util_library(crypto multi_encrypt.cpp random.cpp session_encrypt.cpp + sodium_array.cpp util.cpp xed25519.cpp ) diff --git a/src/blinding.cpp b/src/blinding.cpp index e1a39fbe..ebf0fdde 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -304,32 +304,26 @@ std::pair blind25_key_pair( static const auto version_blinding_hash_key_sig = to_unsigned_sv("VersionCheckKey_sig"sv); std::pair blind_version_key_pair(ustring_view ed25519_sk) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) + if (ed25519_sk.size() != 32 && ed25519_sk.size() != 64) throw std::invalid_argument{ "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " "value"}; - uc32 seed; + std::pair result; + auto& [pk, sk] = result; crypto_generichash_blake2b( - seed.data(), - seed.size(), + sk.data(), + 32, ed25519_sk.data(), 32, version_blinding_hash_key_sig.data(), version_blinding_hash_key_sig.size()); - uc32 pk; - cleared_uc64 sk; - if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data())) + // Reuse `sk` to avoid needing extra secure erasing: + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), sk.data())) throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; - return {pk, sk}; + return result; } static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); @@ -518,8 +512,7 @@ LIBSESSION_C_API bool session_blind15_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -534,8 +527,7 @@ LIBSESSION_C_API bool session_blind25_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -549,8 +541,7 @@ LIBSESSION_C_API bool session_blind_version_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind_version_key_pair({ed25519_seckey, 64}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind_version_key_pair({ed25519_seckey, 64}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -566,9 +557,8 @@ LIBSESSION_C_API bool session_blind15_sign( size_t msg_len, unsigned char* blinded_sig_out) { try { - auto result = session::blind15_sign( + auto sig = session::blind15_sign( {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -583,9 +573,8 @@ LIBSESSION_C_API bool session_blind25_sign( size_t msg_len, unsigned char* blinded_sig_out) { try { - auto result = session::blind25_sign( + auto sig = session::blind25_sign( {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -599,9 +588,8 @@ LIBSESSION_C_API bool session_blind_version_sign( size_t timestamp, unsigned char* blinded_sig_out) { try { - auto result = session::blind_version_sign( + auto sig = session::blind_version_sign( {ed25519_seckey, 64}, static_cast(platform), timestamp); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { diff --git a/src/util.cpp b/src/util.cpp index 8a4d5b44..123aacfe 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -3,20 +3,5 @@ #include namespace session { -void* sodium_buffer_allocate(size_t length) { - if (auto* p = sodium_malloc(length)) - return p; - throw std::bad_alloc{}; -} - -void sodium_buffer_deallocate(void* p) { - if (p) - sodium_free(p); -} - -void sodium_zero_buffer(void* ptr, size_t size) { - if (ptr) - sodium_memzero(ptr, size); -} } // namespace session diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index aa9bb89a..a684cc5e 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -278,7 +278,7 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == - "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f6988e8adb27e7b8ce776fcc25b" + "4091bdaafefd7ddc3398db877c894716b2b24f12dac15ad414a3e4f0b6ac1c6788e8adb27e7b8ce776fcc25b" "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); } @@ -287,8 +287,8 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == - "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e920fa57daf4627c68f43fcbddb" - "2d465d5ea11def523f3befb2bbee39c769676305"); + "adfb25eafec6fb0037fc39145e64badfbdd08cc95a77c01577ecc623fd856fb05b90a921fc6b805e6a730ca9" + "505e1c069f256020a76beb2ecbb6e32c47e12104"); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From e999e1653bd08fef8fd9c4baa35aa4258c31df7b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 12:12:40 +1000 Subject: [PATCH 339/572] Added a missing import --- include/session/blinding.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/session/blinding.h b/include/session/blinding.h index cf51921b..97318501 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -6,6 +6,7 @@ extern "C" { #include +#include "platform.h" #include "export.h" /// API: crypto/session_blind15_key_pair From bc69a538bdafda389c50555ef88b78573cf585d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 12:58:07 +1000 Subject: [PATCH 340/572] Updated the docs to be correct, and explicit about a unix timestamp --- include/session/blinding.h | 9 +++------ include/session/blinding.hpp | 7 ++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 97318501..a2f39b2d 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -6,8 +6,8 @@ extern "C" { #include -#include "platform.h" #include "export.h" +#include "platform.h" /// API: crypto/session_blind15_key_pair /// @@ -116,11 +116,8 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// -/// Takes the Ed25519 secret key (64 bytes). Returns the blinded public key, signature and -/// timestamp. -/// -/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. -/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +/// Takes the Ed25519 secret key (64 bytes), platform and unix timestamp. Returns a version-blinded +/// signature. LIBSESSION_EXPORT bool session_blind_version_sign( const unsigned char* ed25519_seckey, /* 64 bytes */ CLIENT_PLATFORM platform, diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index d2d67d33..c6cc1ee3 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -171,11 +171,8 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// -/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key, -/// signature and timestamp. -/// -/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. -/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), current platform and unix timestamp. +/// Returns the version-blinded signature. ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp); /// Takes in a standard session_id and returns a flag indicating whether it matches the given From d464baf31ea982e6b63e9c3f9261a718c476c8ae Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 25 Jul 2024 14:04:19 -0300 Subject: [PATCH 341/572] Avoid reuse of cleared_uc64 data libsodium is apparently not tolerant of overlapping input/output ranges for recovering a pubkey/seckey pair from seed. --- src/blinding.cpp | 5 +++-- tests/test_blinding.cpp | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/blinding.cpp b/src/blinding.cpp index ebf0fdde..44c1549d 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -310,9 +310,10 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk) { "value"}; std::pair result; + cleared_uc32 blind_seed; auto& [pk, sk] = result; crypto_generichash_blake2b( - sk.data(), + blind_seed.data(), 32, ed25519_sk.data(), 32, @@ -320,7 +321,7 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk) { version_blinding_hash_key_sig.size()); // Reuse `sk` to avoid needing extra secure erasing: - if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), sk.data())) + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), blind_seed.data())) throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; return result; diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index a684cc5e..56a7bc90 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -277,9 +277,22 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { auto [pubkey, seckey] = blind_version_key_pair(to_usv(seed1)); CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); + + CHECK(oxenc::to_hex(seckey.begin() + 32, seckey.end()) == + oxenc::to_hex(pubkey.begin(), pubkey.end())); + + // Hash ourselves just to make sure we get what we expect for the seed part of the secret key: + cleared_uc32 expect_seed; + static const auto hash_key = to_unsigned_sv("VersionCheckKey_sig"sv); + crypto_generichash_blake2b( + expect_seed.data(), 32, seed1.data(), 32, hash_key.data(), hash_key.size()); + + CHECK(oxenc::to_hex(seckey.begin(), seckey.begin() + 32) == + oxenc::to_hex(expect_seed.begin(), expect_seed.end())); + CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == - "4091bdaafefd7ddc3398db877c894716b2b24f12dac15ad414a3e4f0b6ac1c6788e8adb27e7b8ce776fcc25b" - "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); + "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f69" + "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); } TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { @@ -287,8 +300,8 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == - "adfb25eafec6fb0037fc39145e64badfbdd08cc95a77c01577ecc623fd856fb05b90a921fc6b805e6a730ca9" - "505e1c069f256020a76beb2ecbb6e32c47e12104"); + "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e92" + "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From 7f5e30287c955fa9afb5375c8f9b1674b978fec3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Jul 2024 10:21:42 +1000 Subject: [PATCH 342/572] Fixed a bug with the version check API auth generation --- src/network.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index 8e2af0b6..e6db8504 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2247,7 +2247,9 @@ void Network::get_client_version( // Generate the auth signature auto blinded_keys = blind_version_key_pair(to_unsigned_sv(seckey.view())); - auto timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + auto timestamp = std::chrono::duration_cast( + (std::chrono::system_clock::now()).time_since_epoch()) + .count(); auto signature = blind_version_sign(to_unsigned_sv(seckey.view()), platform, timestamp); auto pubkey = x25519_pubkey::from_hex(file_server_pubkey); std::string blinded_pk_hex; From 38c55de55fc33746f9f3d641df96d8afd803e755 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Jul 2024 12:38:32 +1000 Subject: [PATCH 343/572] Additional invite/promotion state and missing C functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added an 'INVITE_NOT_SENT' status which is the default used when creating a new group memeber • Added a groups_keys_free function to the C API • Added a groups_keys_storage_namespace function to the C API • Updated the promotion functions based on the new status options --- include/session/config/groups/keys.h | 19 ++++++ include/session/config/groups/members.h | 5 +- include/session/config/groups/members.hpp | 78 +++++++++++++++++++---- src/config/groups/keys.cpp | 8 +++ src/config/groups/members.cpp | 5 +- tests/test_group_members.cpp | 26 +++++--- 6 files changed, 114 insertions(+), 27 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 3a396cbc..6c50f23f 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -74,6 +74,25 @@ LIBSESSION_EXPORT int groups_keys_init( size_t dumplen, char* error) LIBSESSION_WARN_UNUSED; +/// API: base/groups_keys_free +/// +/// Frees a config keys object created with groups_keys_init. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_group_keys object +LIBSESSION_EXPORT void groups_keys_free(config_group_keys* conf); + +/// API: base/groups_keys_storage_namespace +/// +/// Returns the numeric namespace in which config_group_keys messages should be stored. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to config_group_keys object +/// +/// Outputs: +/// - `int16_t` -- integer of the namespace +LIBSESSION_EXPORT int16_t groups_keys_storage_namespace(const config_group_keys* conf); + /// API: groups/groups_keys_size /// /// Returns the number of decryption keys stored in this Keys object. Mainly for diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index ae5d9490..eb57b165 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -8,7 +8,7 @@ extern "C" { #include "../profile_pic.h" #include "../util.h" -enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2 }; +enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2, INVITE_NOT_SENT = 3 }; enum groups_members_remove_status { REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2 }; typedef struct config_group_member { @@ -19,7 +19,8 @@ typedef struct config_group_member { user_profile_pic profile_pic; bool admin; - int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send + int invited; // 0 == unset, INVITE_SENT = invited, INVITED_FAILED = invite failed to send, + // INVITE_NOT_SENT = invite hasn't been sent yet int promoted; // same value as `invited`, but for promotion-to-admin int removed; // 0 == unset, REMOVED_MEMBER = removed, REMOVED_MEMBER_AND_MESSAGES = remove // member and their messages diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..8843c33c 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -25,6 +25,7 @@ using namespace std::literals; /// I - invite status; this will be one of: /// - 1 if the invite has been issued but not yet accepted. /// - 2 if an invite was created but failed to send for some reason (and thus can be resent) +/// - 3 if a member has been added to the group but the invite hasn't been sent yet. /// - omitted once an invite is accepted. (This also gets omitted if the `A` admin flag gets /// set). /// s - invite supplemental keys flag (only set when `I` is set): if set (to 1) then this invite @@ -37,9 +38,10 @@ using namespace std::literals; /// - 1 if a promotion has been sent. /// - 2 if a promotion was created but failed to send for some reason (and thus should be /// resent) +/// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet. /// - omitted once the promotion is accepted (i.e. once `A` gets set). -constexpr int INVITE_SENT = 1, INVITE_FAILED = 2; +constexpr int INVITE_SENT = 1, INVITE_FAILED = 2, INVITE_NOT_SENT = 3; constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; /// Struct containing member details @@ -102,7 +104,7 @@ struct member { // Flags to track an invited user. This value is typically not used directly, but rather via // the `set_invited()`, `invite_pending()` and similar methods. - int invite_status = 0; + int invite_status = INVITE_NOT_SENT; /// API: groups/member::set_invited /// @@ -127,6 +129,17 @@ struct member { supplement = false; } + /// API: groups/member::invite_needs_send + /// + /// Returns whether the user needs an invite sent to them. Returns true if so (whether or + /// not that invitation has been sent). + /// + /// Inputs: none + /// + /// Outputs: + /// - `bool` -- true if the user needs an invitation to be sent, false otherwise. + bool invite_needs_send() const { return invite_status == INVITE_NOT_SENT; } + /// API: groups/member::invite_pending /// /// Returns whether the user currently has a pending invitation. Returns true if so (whether or @@ -155,20 +168,55 @@ struct member { /// API: groups/member::set_promoted /// - /// Sets the "promoted" flag for this user. This marks the user as having a pending - /// promotion-to-admin in the group. The optional `failed` parameter can be specified as true - /// if the promotion was issued but failed to send for some reason (this is intended as a signal - /// to other clients that the promotion should be reissued). + /// This marks the user as having a pending promotion-to-admin in the group, waiting for the + /// promotion message to be sent to them. + void set_promoted() { + admin = true; + invite_status = 0; + promotion_status = INVITE_NOT_SENT; + } + + /// API: groups/member::set_promotion_sent /// - /// Note that this flag is ignored when the `admin` field is set to true. + /// This marks the user as having a pending promotion-to-admin in the group, and that a + /// promotion message has been sent to them. + void set_promotion_sent() { + admin = true; + invite_status = 0; + promotion_status = INVITE_SENT; + } + + /// API: groups/member::set_promotion_failed /// - /// Inputs: - /// - `failed`: can be specified as true to mark the promotion status as "failed-to-send". If - /// omitted or false then the promotion status is set to "sent". - void set_promoted(bool failed = false) { - promotion_status = failed ? INVITE_FAILED : INVITE_SENT; + /// This marks the user as being promoted to an admin, but that their promotion message failed + /// to send (this is intended as a signal to other clients that the promotion should be + /// reissued). + void set_promotion_failed() { + admin = true; + invite_status = 0; + promotion_status = INVITE_FAILED; + } + + /// API: groups/member::accept_promotion + /// + /// This marks the user as having accepted a promotion to admin in the group. + void accept_promotion() { + admin = true; + invite_status = 0; + promotion_status = 0; } + /// API: groups/member::promotion_needs_send + /// + /// Returns whether the user needs a promotion to admin status to be sent. + /// Returns true if so (whether or not that promotion has failed). + /// + /// Inputs: None + /// + /// Outputs: + /// - `bool` -- true if the user needs a promotion to be sent, false otherwise. + bool promotion_needs_send() const { return promotion_status == INVITE_NOT_SENT; } + /// API: groups/member::promotion_pending /// /// Returns whether the user currently has a pending invitation/promotion to admin status. @@ -178,7 +226,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a pending promotion, false otherwise. - bool promotion_pending() const { return !admin && promotion_status > 0; } + bool promotion_pending() const { return promotion_status > 0; } /// API: groups/member::promotion_failed /// @@ -189,7 +237,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a failed pending promotion - bool promotion_failed() const { return !admin && promotion_status == INVITE_FAILED; } + bool promotion_failed() const { return promotion_status == INVITE_FAILED; } /// API: groups/member::promoted /// @@ -215,6 +263,8 @@ struct member { /// - `messages`: can be specified as true to indicate any messages sent by the member /// should also be removed upon a successful member removal. void set_removed(bool messages = false) { + invite_status = 0; + promotion_status = 0; removed_status = messages ? REMOVED_MEMBER_AND_MESSAGES : REMOVED_MEMBER; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 72d292bd..48ec52f6 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1455,6 +1455,14 @@ LIBSESSION_C_API int groups_keys_init( return SESSION_ERR_NONE; } +LIBSESSION_C_API void groups_keys_free(config_group_keys* conf) { + delete conf; +} + +LIBSESSION_EXPORT int16_t groups_keys_storage_namespace(const config_group_keys* conf) { + return static_cast(unbox(conf).storage_namespace()); +} + LIBSESSION_C_API size_t groups_keys_size(const config_group_keys* conf) { return unbox(conf).size(); } diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index bce1ad05..878f2142 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -49,7 +49,7 @@ void Members::set(const member& mem) { mem.profile_picture.key); set_flag(info["A"], mem.admin); - set_positive_int(info["P"], mem.admin ? 0 : mem.promotion_status); + set_positive_int(info["P"], mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); set_flag(info["s"], mem.supplement); set_positive_int(info["R"], mem.removed_status); @@ -69,7 +69,7 @@ void member::load(const dict& info_dict) { admin = maybe_int(info_dict, "A").value_or(0); invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); - promotion_status = admin ? 0 : maybe_int(info_dict, "P").value_or(0); + promotion_status = maybe_int(info_dict, "P").value_or(0); removed_status = maybe_int(info_dict, "R").value_or(0); supplement = invite_pending() && !promoted() ? maybe_int(info_dict, "s").value_or(0) : 0; } @@ -161,6 +161,7 @@ void member::into(config_group_member& m) const { m.admin = admin; static_assert(groups::INVITE_SENT == ::INVITE_SENT); static_assert(groups::INVITE_FAILED == ::INVITE_FAILED); + static_assert(groups::INVITE_NOT_SENT == ::INVITE_NOT_SENT); m.invited = invite_status; m.promoted = promotion_status; m.removed = removed_status; diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 747b2adb..80c0bbdb 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -71,7 +71,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // 10 admins: for (int i = 0; i < 10; i++) { auto m = gmem1.get_or_construct(sids[i]); - m.admin = true; + m.accept_promotion(); m.name = "Admin " + std::to_string(i); m.profile_picture.url = "http://example.com/" + std::to_string(i); m.profile_picture.key = @@ -115,7 +115,6 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem2) { CHECK(m.session_id == sids[i]); - CHECK_FALSE(m.invite_pending()); CHECK_FALSE(m.invite_failed()); CHECK_FALSE(m.promotion_pending()); CHECK_FALSE(m.promotion_failed()); @@ -123,11 +122,15 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.should_remove_messages()); CHECK_FALSE(m.supplement); if (i < 10) { + CHECK_FALSE(m.invite_needs_send()); + CHECK_FALSE(m.invite_pending()); CHECK(m.admin); CHECK(m.name == "Admin " + std::to_string(i)); CHECK_FALSE(m.profile_picture.empty()); CHECK(m.promoted()); } else { + CHECK(m.invite_needs_send()); + CHECK(m.invite_pending()); CHECK_FALSE(m.admin); CHECK_FALSE(m.promoted()); if (i < 20) { @@ -164,7 +167,10 @@ TEST_CASE("Group Members", "[config][groups][members]") { } for (int i = 58; i < 62; i++) { auto m = gmem2.get_or_construct(sids[i]); - m.set_promoted(i >= 60); + if (i >= 60) + m.set_promotion_failed(); + else + m.set_promotion_sent(); gmem2.set(m); } for (int i = 62; i < 66; i++) { @@ -188,7 +194,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem1) { CHECK(m.session_id == sids[i]); - CHECK(m.admin == i < 10); + CHECK(m.admin == (i < 10 || (i >= 58 && i < 62))); CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : i < 10 ? "Admin " + std::to_string(i) : "Member " + std::to_string(i))); @@ -197,7 +203,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_pending() == (50 <= i && i < 58)); + CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_pending() == (i >= 10 && i < 58)); CHECK(m.invite_failed() == (55 <= i && i < 58)); CHECK(m.supplement == (i % 2 && 50 < i && i < 58)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); @@ -222,11 +229,11 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.set(m); } else if (i == 58) { auto m = gmem1.get(sids[i]).value(); - m.admin = true; + m.accept_promotion(); gmem1.set(m); } else if (i == 59) { auto m = gmem1.get(sids[i]).value(); - m.set_promoted(); + m.set_promotion_sent(); gmem1.set(m); } } @@ -241,7 +248,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { int i = 0; for (auto& m : gmem2) { CHECK(m.session_id == sids[i]); - CHECK(m.admin == (i < 10 || i == 58)); + CHECK(m.admin == (i < 10 || (i >= 58 && i < 62))); CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : i < 10 ? "Admin " + std::to_string(i) : "Member " + std::to_string(i))); @@ -250,7 +257,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_pending() == (55 <= i && i < 58)); + CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_pending() == ((i >= 10 && i < 50) || i == 53 || (i >= 55 && i < 58))); CHECK(m.invite_failed() == (i == 57)); CHECK(m.supplement == (i == 55 || i == 57)); CHECK(m.promoted() == (i < 10 || (i >= 58 && i < 62))); From 06c4709289e61a1caef9152d02bdbf2b19f2158a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 17 Jul 2024 15:28:10 +1000 Subject: [PATCH 344/572] Renamed a couple of functions, updated docs, fixed 'promotion_x' funcs --- include/session/config/groups/members.hpp | 29 ++++++++++------------- tests/test_group_members.cpp | 12 +++++----- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 8843c33c..7a412d5f 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,14 +81,11 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member is an admin. + /// Flag that is set to indicate to the group that this member has been promoted to an admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could /// have lost (or never had) the keys even if this is set. - /// - /// See also `promoted()` if you want to check for either an admin or someone being promoted to - /// admin. bool admin = false; /// API: groups/member::supplement @@ -129,16 +126,16 @@ struct member { supplement = false; } - /// API: groups/member::invite_needs_send + /// API: groups/member::invite_not_sent /// - /// Returns whether the user needs an invite sent to them. Returns true if so (whether or - /// not that invitation has been sent). + /// Returns true if there has never been an attempt to send an invitation to the user. Returns + /// false otherwise. /// /// Inputs: none /// /// Outputs: /// - `bool` -- true if the user needs an invitation to be sent, false otherwise. - bool invite_needs_send() const { return invite_status == INVITE_NOT_SENT; } + bool invite_not_sent() const { return invite_status == INVITE_NOT_SENT; } /// API: groups/member::invite_pending /// @@ -197,25 +194,25 @@ struct member { promotion_status = INVITE_FAILED; } - /// API: groups/member::accept_promotion + /// API: groups/member::set_promotion_accepted /// /// This marks the user as having accepted a promotion to admin in the group. - void accept_promotion() { + void set_promotion_accepted() { admin = true; invite_status = 0; promotion_status = 0; } - /// API: groups/member::promotion_needs_send + /// API: groups/member::promotion_not_sent /// - /// Returns whether the user needs a promotion to admin status to be sent. - /// Returns true if so (whether or not that promotion has failed). + /// Returns true if the user is an admin but there has never been an attempt to send a promotion + /// to admin status sent to them. Returns false otherwise. /// /// Inputs: None /// /// Outputs: /// - `bool` -- true if the user needs a promotion to be sent, false otherwise. - bool promotion_needs_send() const { return promotion_status == INVITE_NOT_SENT; } + bool promotion_not_sent() const { return admin && promotion_status == INVITE_NOT_SENT; } /// API: groups/member::promotion_pending /// @@ -226,7 +223,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a pending promotion, false otherwise. - bool promotion_pending() const { return promotion_status > 0; } + bool promotion_pending() const { return admin && promotion_status > 0; } /// API: groups/member::promotion_failed /// @@ -237,7 +234,7 @@ struct member { /// /// Outputs: /// - `bool` -- true if the user has a failed pending promotion - bool promotion_failed() const { return promotion_status == INVITE_FAILED; } + bool promotion_failed() const { return admin && promotion_status == INVITE_FAILED; } /// API: groups/member::promoted /// diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 80c0bbdb..42e3e1ba 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -71,7 +71,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // 10 admins: for (int i = 0; i < 10; i++) { auto m = gmem1.get_or_construct(sids[i]); - m.accept_promotion(); + m.set_promotion_accepted(); m.name = "Admin " + std::to_string(i); m.profile_picture.url = "http://example.com/" + std::to_string(i); m.profile_picture.key = @@ -122,14 +122,14 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.should_remove_messages()); CHECK_FALSE(m.supplement); if (i < 10) { - CHECK_FALSE(m.invite_needs_send()); + CHECK_FALSE(m.invite_not_sent()); CHECK_FALSE(m.invite_pending()); CHECK(m.admin); CHECK(m.name == "Admin " + std::to_string(i)); CHECK_FALSE(m.profile_picture.empty()); CHECK(m.promoted()); } else { - CHECK(m.invite_needs_send()); + CHECK(m.invite_not_sent()); CHECK(m.invite_pending()); CHECK_FALSE(m.admin); CHECK_FALSE(m.promoted()); @@ -203,7 +203,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_not_sent() == (i >= 10 && i < 50)); CHECK(m.invite_pending() == (i >= 10 && i < 58)); CHECK(m.invite_failed() == (55 <= i && i < 58)); CHECK(m.supplement == (i % 2 && 50 < i && i < 58)); @@ -229,7 +229,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem1.set(m); } else if (i == 58) { auto m = gmem1.get(sids[i]).value(); - m.accept_promotion(); + m.set_promotion_accepted(); gmem1.set(m); } else if (i == 59) { auto m = gmem1.get(sids[i]).value(); @@ -257,7 +257,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/" + std::to_string(i) : "")); - CHECK(m.invite_needs_send() == (i >= 10 && i < 50)); + CHECK(m.invite_not_sent() == (i >= 10 && i < 50)); CHECK(m.invite_pending() == ((i >= 10 && i < 50) || i == 53 || (i >= 55 && i < 58))); CHECK(m.invite_failed() == (i == 57)); CHECK(m.supplement == (i == 55 || i == 57)); From 64fa7fab56e2445a724221b51d58c88e5019dd49 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 17 Jul 2024 13:47:45 -0300 Subject: [PATCH 345/572] Fix C free functions These were meant to delete the internal C++ object, but weren't actually doing so, and thus would be leaking the C++ objects when supposedly being freed from the C API. Also adds a missing `onion_request_builder_free` function and removes the (wrong) doc implying that `onion_request_builder_build` frees the object. --- include/session/onionreq/builder.h | 12 ++++++++++-- src/config/base.cpp | 1 + src/config/groups/keys.cpp | 1 + src/onionreq/builder.cpp | 10 +++++++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h index 40fb003f..4320276e 100644 --- a/include/session/onionreq/builder.h +++ b/include/session/onionreq/builder.h @@ -26,13 +26,21 @@ typedef struct onion_request_builder_object { /// /// Constructs an onion request builder and sets a pointer to it in `builder`. /// -/// When done with the object the `builder` must be destroyed by either passing the pointer to -/// onion_request_builder_free() or onion_request_builder_build(). +/// When done with the object the `builder` must be destroyed by passing the pointer to +/// onion_request_builder_free(). /// /// Inputs: /// - `builder` -- [out] Pointer to the builder object LIBSESSION_EXPORT void onion_request_builder_init(onion_request_builder_object** builder); +/// API: groups/onion_request_builder_free +/// +/// Properly destroys an onion request builder instance. +/// +/// Inputs: +/// - `builder` -- [out] Pointer to the builder object to be freed +LIBSESSION_EXPORT void onion_request_builder_free(onion_request_builder_object* builder); + /// API: onion_request_builder_set_enc_type /// /// Wrapper around session::onionreq::Builder::onion_request_builder_set_enc_type. diff --git a/src/config/base.cpp b/src/config/base.cpp index 2518751a..603ee7cf 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -656,6 +656,7 @@ using namespace session; using namespace session::config; LIBSESSION_EXPORT void config_free(config_object* conf) { + delete static_cast*>(conf->internals); delete conf; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 48ec52f6..6c624a0b 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1456,6 +1456,7 @@ LIBSESSION_C_API int groups_keys_init( } LIBSESSION_C_API void groups_keys_free(config_group_keys* conf) { + delete static_cast(conf->internals); delete conf; } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 9e3e434d..270e6248 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -257,12 +257,16 @@ extern "C" { using session::ustring; LIBSESSION_C_API void onion_request_builder_init(onion_request_builder_object** builder) { - auto c = std::make_unique(); auto c_builder = std::make_unique(); - c_builder->internals = c.release(); + c_builder->internals = new session::onionreq::Builder{}; *builder = c_builder.release(); } +LIBSESSION_C_API void onion_request_builder_free(onion_request_builder_object* builder) { + delete static_cast(builder->internals); + delete builder; +} + LIBSESSION_C_API void onion_request_builder_set_enc_type( onion_request_builder_object* builder, ENCRYPT_TYPE enc_type) { assert(builder); @@ -347,7 +351,7 @@ LIBSESSION_C_API bool onion_request_builder_build( assert(builder && payload_in); try { - auto unboxed_builder = unbox(builder); + auto& unboxed_builder = unbox(builder); auto payload = unboxed_builder.build(ustring{payload_in, payload_in_len}); if (unboxed_builder.final_hop_x25519_keypair) { From ad32b750882b07fcf95fb6fad7b0395082b38d9d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 09:01:42 +1000 Subject: [PATCH 346/572] Small docs change --- include/session/config/groups/members.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 7a412d5f..cf73d02e 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,7 +81,7 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member has been promoted to an admin. + /// Flag that is set to indicate to the group that this member is an admin or has been promoted to admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could From b5b06b9ad6bb18f3bb155da44574a2a61e57a42c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 10:49:24 +1000 Subject: [PATCH 347/572] Ran the formatter -_- --- include/session/config/groups/members.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index cf73d02e..4d2f95d2 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -81,7 +81,8 @@ struct member { /// /// Member variable /// - /// Flag that is set to indicate to the group that this member is an admin or has been promoted to admin. + /// Flag that is set to indicate to the group that this member is an admin or has been promoted + /// to admin. /// /// Note that this is only informative but isn't a permission gate: someone could still possess /// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could From a05a5bee8feb39c58db6c8003bf4450005024eb7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Jul 2024 08:56:08 +1000 Subject: [PATCH 348/572] Added 'set_{x}_truncated' functions so clients have non-throwing versions --- include/session/config/contacts.hpp | 11 +++++++++++ include/session/config/groups/info.hpp | 21 ++++++++++++++++++++- include/session/config/groups/members.hpp | 12 ++++++++++++ include/session/config/user_profile.hpp | 8 ++++++++ src/config/contacts.cpp | 11 +++++++++++ src/config/groups/info.cpp | 12 ++++++++++++ src/config/groups/members.cpp | 6 ++++++ src/config/user_profile.cpp | 5 +++++ tests/test_config_contacts.cpp | 4 ++++ tests/test_group_info.cpp | 4 ++++ tests/test_group_members.cpp | 5 +++++ 11 files changed, 98 insertions(+), 1 deletion(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 7ac4b237..062c2e75 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -90,6 +90,7 @@ struct contact_info { /// - `name` -- Name to assign to the contact void set_name(std::string name); void set_nickname(std::string nickname); + void set_nickname_truncated(std::string nickname); private: friend class Contacts; @@ -206,6 +207,16 @@ class Contacts : public ConfigBase { /// - `nickname` -- string of the contacts nickname void set_nickname(std::string_view session_id, std::string nickname); + /// API: contacts/contacts::set_nickname_truncated + /// + /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates the value when it's too long. (If setting multiple fields at once you + /// should use `set()` instead). + /// + /// Inputs: + /// - `session_id` -- hex string of the session id + /// - `nickname` -- string of the contacts nickname + void set_nickname_truncated(std::string_view session_id, std::string nickname); + /// API: contacts/contacts::set_profile_pic /// /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 4010bf35..7650dc18 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -104,12 +104,22 @@ class Info final : public ConfigBase { /// /// Sets the group name; if given an empty string then the name is removed. /// - /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes it will be truncated. + /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes an error will be thrown. /// /// Inputs: /// - `new_name` -- The name to be put into the group Info void set_name(std::string_view new_name); + /// API: groups/Info::set_name_truncated + /// + /// Sets the group name; if given an empty string then the name is removed. + /// + /// If given a name longer than `Info::NAME_MAX_LENGTH` (100) bytes it will be truncated. + /// + /// Inputs: + /// - `new_name` -- The name to be put into the group Info + void set_name_truncated(std::string new_name); + /// API: groups/Info::get_description /// /// Returns the group description, or std::nullopt if there is no group description set. @@ -132,6 +142,15 @@ class Info final : public ConfigBase { /// - `new_desc` -- The new description to be put into the group Info void set_description(std::string_view new_desc); + /// API: groups/Info::set_description_truncated + /// + /// Sets the optional group description; if given an empty string then an existing description + /// is removed. The same as `set_description` but if the name is too long it'll be truncated. + /// + /// Inputs: + /// - `new_desc` -- The new description to be put into the group Info + void set_description_truncated(std::string new_desc); + /// API: groups/Info::get_profile_pic /// /// Gets the group's current profile pic URL and decryption key. The returned object will diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 4d2f95d2..60e931ec 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -308,6 +308,18 @@ struct member { /// - `name` -- Name to assign to the contact void set_name(std::string name); + /// API: groups/member::set_name_truncated + /// + /// Sets a name; this is exactly the same as assigning to .name directly, except that we truncate + /// if the given name is longer than MAX_NAME_LENGTH. + /// + /// Note that you can set a longer name directly into the `.name` member, but it will be + /// truncated when serializing the record. + /// + /// Inputs: + /// - `name` -- Name to assign to the contact + void set_name_truncated(std::string name); + private: friend class Members; void load(const dict& info_dict); diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index d99f19e9..f5483e62 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -87,6 +87,14 @@ class UserProfile final : public ConfigBase { /// - `new_name` -- The name to be put into the user profile void set_name(std::string_view new_name); + /// API: user_profile/UserProfile::set_name_truncated + /// + /// Sets the user profile name; if given an empty string then the name is removed. Same as the `set_name` function but truncates the name if it's too long. + /// + /// Inputs: + /// - `new_name` -- The name to be put into the user profile + void set_name_truncated(std::string new_name); + /// API: user_profile/UserProfile::get_profile_pic /// /// Gets the user's current profile pic URL and decryption key. The returned object will diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 193ec063..3ff5af9c 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -51,6 +51,12 @@ void contact_info::set_nickname(std::string n) { nickname = std::move(n); } +void contact_info::set_nickname_truncated(std::string n) { + if (n.size() > MAX_NAME_LENGTH) + n.resize(MAX_NAME_LENGTH); + set_nickname(n); +} + Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); @@ -258,6 +264,11 @@ void Contacts::set_nickname(std::string_view session_id, std::string nickname) { c.set_nickname(std::move(nickname)); set(c); } +void Contacts::set_nickname_truncated(std::string_view session_id, std::string nickname) { + auto c = get_or_construct(session_id); + c.set_nickname_truncated(std::move(nickname)); + set(c); +} void Contacts::set_profile_pic(std::string_view session_id, profile_pic pic) { auto c = get_or_construct(session_id); c.profile_picture = std::move(pic); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 1601ebcc..95125299 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -35,6 +35,12 @@ void Info::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } +void Info::set_name_truncated(std::string new_name) { + if (new_name.size() > NAME_MAX_LENGTH) + new_name.resize(NAME_MAX_LENGTH); + set_name(new_name); +} + std::optional Info::get_description() const { if (auto* s = data["o"].string(); s && !s->empty()) return *s; @@ -47,6 +53,12 @@ void Info::set_description(std::string_view new_desc) { set_nonempty_str(data["o"], new_desc); } +void Info::set_description_truncated(std::string new_desc) { + if (new_desc.size() > DESCRIPTION_MAX_LENGTH) + new_desc.resize(DESCRIPTION_MAX_LENGTH); + set_description(new_desc); +} + profile_pic Info::get_profile_pic() const { profile_pic pic{}; if (auto* url = data["p"].string(); url && !url->empty()) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 878f2142..9a51f35d 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -174,6 +174,12 @@ void member::set_name(std::string n) { name = std::move(n); } +void member::set_name_truncated(std::string n) { + if (n.size() > MAX_NAME_LENGTH) + n.resize(MAX_NAME_LENGTH); + set_name(n); +} + } // namespace session::config::groups using namespace session; diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 4a6145a2..d83fb102 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -44,6 +44,11 @@ void UserProfile::set_name(std::string_view new_name) { throw std::invalid_argument{"Invalid profile name: exceeds maximum length"}; set_nonempty_str(data["n"], new_name); } +void UserProfile::set_name_truncated(std::string new_name) { + if (new_name.size() > contact_info::MAX_NAME_LENGTH) + new_name.resize(contact_info::MAX_NAME_LENGTH); + set_name(new_name); +} LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { return wrap_exceptions( conf, diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 17883d75..5ec0b3b7 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -223,6 +223,10 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); + + CHECK_THROWS(c.set_nickname("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 1896b47f..0ea57f69 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -131,6 +131,10 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo2.is_destroyed()); + + CHECK_THROWS(ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(ginfo1.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(ginfo1.get_name() == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 42e3e1ba..116b1496 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -272,4 +272,9 @@ TEST_CASE("Group Members", "[config][groups][members]") { } CHECK(i == 66); } + + auto m = gmem1.get_or_construct(sids[0]); + CHECK_THROWS(m.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK_NOTHROW(m.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); + CHECK(m.name == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); } From caab7a8855543cdacffc5ca9839610890f820632 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Jul 2024 10:54:29 +1000 Subject: [PATCH 349/572] Ran the formatter --- include/session/config/contacts.hpp | 5 +++-- include/session/config/groups/members.hpp | 4 ++-- include/session/config/user_profile.hpp | 3 ++- tests/test_config_contacts.cpp | 12 +++++++++--- tests/test_group_info.cpp | 12 +++++++++--- tests/test_group_members.cpp | 12 +++++++++--- 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 062c2e75..08a7d792 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -209,8 +209,9 @@ class Contacts : public ConfigBase { /// API: contacts/contacts::set_nickname_truncated /// - /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates the value when it's too long. (If setting multiple fields at once you - /// should use `set()` instead). + /// Alternative to `set()` for setting a single field. The same as `set_name` except truncates + /// the value when it's too long. (If setting multiple fields at once you should use `set()` + /// instead). /// /// Inputs: /// - `session_id` -- hex string of the session id diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 60e931ec..07e8fc13 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -310,8 +310,8 @@ struct member { /// API: groups/member::set_name_truncated /// - /// Sets a name; this is exactly the same as assigning to .name directly, except that we truncate - /// if the given name is longer than MAX_NAME_LENGTH. + /// Sets a name; this is exactly the same as assigning to .name directly, except that we + /// truncate if the given name is longer than MAX_NAME_LENGTH. /// /// Note that you can set a longer name directly into the `.name` member, but it will be /// truncated when serializing the record. diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index f5483e62..8f5db6b7 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -89,7 +89,8 @@ class UserProfile final : public ConfigBase { /// API: user_profile/UserProfile::set_name_truncated /// - /// Sets the user profile name; if given an empty string then the name is removed. Same as the `set_name` function but truncates the name if it's too long. + /// Sets the user profile name; if given an empty string then the name is removed. Same as the + /// `set_name` function but truncates the name if it's too long. /// /// Inputs: /// - `new_name` -- The name to be put into the user profile diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 5ec0b3b7..96562ea0 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -224,9 +224,15 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); - CHECK_THROWS(c.set_nickname("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" + "23456789012345678901234567890A")); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "234567890123456789012345678901234567890A")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 0ea57f69..df70900a 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -132,9 +132,15 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); CHECK(ginfo2.is_destroyed()); - CHECK_THROWS(ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(ginfo1.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(ginfo1.get_name() == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + ginfo1.set_name("1234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890A")); + CHECK_NOTHROW( + ginfo1.set_name_truncated("123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890A")); + CHECK(ginfo1.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 116b1496..e2304351 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -274,7 +274,13 @@ TEST_CASE("Group Members", "[config][groups][members]") { } auto m = gmem1.get_or_construct(sids[0]); - CHECK_THROWS(m.set_name("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK_NOTHROW(m.set_name_truncated("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890A")); - CHECK(m.name == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + CHECK_THROWS( + m.set_name("123456789012345678901234567890123456789012345678901234567890123456789012345" + "6789012345678901234567890A")); + CHECK_NOTHROW( + m.set_name_truncated("12345678901234567890123456789012345678901234567890123456789012345" + "67890123456789012345678901234567890A")); + CHECK(m.name == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); } From e956d9daea88e9fb832ca0ff6c496c794887d6d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Jul 2024 08:56:19 +1000 Subject: [PATCH 350/572] Added a utf8_truncate function and use it when truncating user content --- include/session/util.hpp | 59 +++++++++++++++++++++++++++++++++++ src/config/contacts.cpp | 9 +++--- src/config/groups/info.cpp | 8 ++--- src/config/groups/members.cpp | 4 +-- src/config/user_profile.cpp | 4 +-- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index 76da4061..fc2c4971 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -156,4 +156,63 @@ std::vector split( std::tuple, std::optional> parse_url( std::string_view url); +/// Truncates a utf-8 encoded string to at most `n` bytes long, but with care as to not truncate in +/// the middle of a unicode codepoint. If the `n` length would shorten the string such that it +/// terminates in the middle of a utf-8 encoded unicode codepoint then the string is shortened +/// further to not include the sliced unicode codepoint. +/// +/// For example, "happy 🎂🎂🎂!!" in utf8 encoding is 20 bytes long: +/// "happy \xf0\x9f\x8e\x82\xf0\x9f\x8e\x82\xf0\x9f\x8e\x82!!", that is: +/// - "happy " (6 bytes) +/// - 🎂 = 0xf0 0x9f 0x8e 0x82 (12 bytes = 3 × 4 bytes each) +/// - "!!" (2 bytes) +/// Truncating this to different lengths results in: +/// - 20, 21, or higher - the 20-byte full string +/// - 19: "happy 🎂🎂🎂!" +/// - 18: "happy 🎂🎂🎂" +/// - 17: "happy 🎂🎂" (14 bytes) +/// - 16, 15, 14: same result as 17 +/// - 13, 12, 11, 10: "happy 🎂" +/// - 9, 8, 7, 6: "happy " +/// - 5: "happy" +/// - 4: "happ" +/// - 3: "hap" +/// - 2: "ha" +/// - 1: "a" +/// - 0: "" +/// +/// This function is *not* (currently) aware of unicode "characters", but merely codepoints (because +/// grapheme clusters get incredibly complicated). This is only designed to prevent invalid utf8 +/// encodings. For example, the pair 🇦🇺 (REGIONAL INDICATOR SYMBOL LETTER A, REGIONAL INDICATOR +/// SYMBOL LETTER U) is often rendered as a single Australian flag, but could get chopped here into +/// just 🇦 (REGIONAL INDICATOR SYMBOL LETTER A) rather than removing the getting split in the middle +/// of the pair, which would show up as a decorated A rather than an Australian flag. Another +/// example, é (LATIN SMALL LETTER E, COMBINING ACUTE ACCENT) could get chopped between the e and +/// the accent modifier, and end up as just "e" in the truncated string. +/// +inline std::string utf8_truncate(std::string val, size_t n) { + if (val.size() <= n) + return val; + // The *first* char in a utf8 sequence is either: + // 0b0....... -- single byte encoding, for values up to 0x7f (ascii) + // 0b11...... -- multi-byte encoding for values >= 0x80; the number of sequential high bit 1's + // in the first character indicate the sequence length (e.g. 0b1110.... starts a 3-byte + // sequence). In our birthday cake encoding, the first byte is \xf0 == 0b11110000, and so it is + // a 4-byte sequence. + // + // That leaves 0x10...... bytes as continuation bytes, each one holding 6 bits of the unicode + // codepoint, in big endian order, so our birthday cake (in bits): 0b11110000 0b10011111 + // 0b10001110 0b10000010 is the unicode value 0b000 011111 001110 000010 == 0x1f382 == U+1F382: + // BIRTHDAY CAKE). + // + // To prevent slicing, then, we just have to ensure the the first byte after the slice point is + // *not* a continuation byte (and therefore is either a plain ascii character codepoint, or is + // the start of a multi-character codepoint). + while (n > 0 && (val[n] & 0b1100'0000) == 0b1000'0000) + --n; + + val.resize(n); + return val; +} + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 3ff5af9c..89291087 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -41,8 +41,9 @@ contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { void contact_info::set_name(std::string n) { if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - name = std::move(n); + name = std::move(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); + else + name = std::move(n); } void contact_info::set_nickname(std::string n) { @@ -52,9 +53,7 @@ void contact_info::set_nickname(std::string n) { } void contact_info::set_nickname_truncated(std::string n) { - if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - set_nickname(n); + set_nickname(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); } Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 95125299..4fbb2d86 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -36,9 +36,7 @@ void Info::set_name(std::string_view new_name) { } void Info::set_name_truncated(std::string new_name) { - if (new_name.size() > NAME_MAX_LENGTH) - new_name.resize(NAME_MAX_LENGTH); - set_name(new_name); + set_name(utf8_truncate(std::move(new_name), NAME_MAX_LENGTH)); } std::optional Info::get_description() const { @@ -54,9 +52,7 @@ void Info::set_description(std::string_view new_desc) { } void Info::set_description_truncated(std::string new_desc) { - if (new_desc.size() > DESCRIPTION_MAX_LENGTH) - new_desc.resize(DESCRIPTION_MAX_LENGTH); - set_description(new_desc); + set_description(utf8_truncate(std::move(new_desc), DESCRIPTION_MAX_LENGTH)); } profile_pic Info::get_profile_pic() const { diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 9a51f35d..96a772d5 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -175,9 +175,7 @@ void member::set_name(std::string n) { } void member::set_name_truncated(std::string n) { - if (n.size() > MAX_NAME_LENGTH) - n.resize(MAX_NAME_LENGTH); - set_name(n); + set_name(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); } } // namespace session::config::groups diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index d83fb102..f01cded6 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -45,9 +45,7 @@ void UserProfile::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } void UserProfile::set_name_truncated(std::string new_name) { - if (new_name.size() > contact_info::MAX_NAME_LENGTH) - new_name.resize(contact_info::MAX_NAME_LENGTH); - set_name(new_name); + set_name(utf8_truncate(std::move(new_name), contact_info::MAX_NAME_LENGTH)); } LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { return wrap_exceptions( From a15533e6add2b09a604fc3914306baa0a7010dc8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Jul 2024 09:02:49 +1000 Subject: [PATCH 351/572] Added some tests for the new utf8 truncation logic --- tests/test_config_contacts.cpp | 12 ++++++++ tests/test_config_userprofile.cpp | 47 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 96562ea0..c8851953 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -233,6 +233,18 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.nickname == "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" "901234567890"); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "234567890123456789012345678901234567🎂")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567"); + CHECK_NOTHROW( + c.set_nickname_truncated("1234567890123456789012345678901234567890123456789012345678901" + "2345678901234567890123456789012345🎂🎂")); + CHECK(c.nickname == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345🎂"); } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 5774ab57..45872b1b 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "utils.hpp" @@ -12,6 +13,52 @@ using namespace std::literals; using namespace oxenc::literals; +TEST_CASE("UserProfile", "[config][user_profile]") { + + 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)); + + session::config::UserProfile profile{ustring_view{seed}, std::nullopt}; + + CHECK_THROWS( + profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789" + "01" + "23456789012345678901234567890A")); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "234567890123456789012345678901234567890A")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890"); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "234567890123456789012345678901234567🎂")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567"); + CHECK_NOTHROW( + profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" + "01" + "2345678901234567890123456789012345🎂🎂")); + CHECK(profile.get_name() == + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345🎂"); +} + TEST_CASE("user profile C API", "[config][user_profile][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; From d3a2eca7c93c6abdf73271cca4da780501904d3b Mon Sep 17 00:00:00 2001 From: yougotwill Date: Wed, 24 Jul 2024 11:54:31 +1000 Subject: [PATCH 352/572] feat: moved community url max length and qs_pubkey to cpp header it is now used in libsession_util_nodejs --- include/session/config/community.hpp | 4 ++++ src/config/community.cpp | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index a3110cbd..d6d9b27f 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -18,6 +18,10 @@ struct community { // 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX') static constexpr size_t BASE_URL_MAX_LENGTH = 267; static constexpr size_t ROOM_MAX_LENGTH = 64; + static constexpr std::string_view qs_pubkey{"?public_key="}; + static const size_t FULL_URL_MAX_LENGTH = BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + + ROOM_MAX_LENGTH + qs_pubkey.size() + + 64 /*pubkey hex*/ + 1 /*null terminator*/; community() = default; diff --git a/src/config/community.cpp b/src/config/community.cpp index 78894a80..8a4f1351 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -74,8 +74,6 @@ void community::set_room(std::string_view room) { localized_room_ = room; } -static constexpr std::string_view qs_pubkey{"?public_key="}; - std::string community::full_url() const { return full_url(base_url(), room(), pubkey()); } @@ -172,8 +170,7 @@ LIBSESSION_C_API const size_t COMMUNITY_BASE_URL_MAX_LENGTH = LIBSESSION_C_API const size_t COMMUNITY_ROOM_MAX_LENGTH = session::config::community::ROOM_MAX_LENGTH; LIBSESSION_C_API const size_t COMMUNITY_FULL_URL_MAX_LENGTH = - COMMUNITY_BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + COMMUNITY_ROOM_MAX_LENGTH + - session::config::qs_pubkey.size() + 64 /*pubkey hex*/ + 1 /*null terminator*/; + session::config::community::FULL_URL_MAX_LENGTH; LIBSESSION_C_API bool community_parse_full_url( const char* full_url, char* base_url, char* room_token, unsigned char* pubkey) { From 94d53baa4a71013108f2112eee4358baafd57d6e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 24 Jul 2024 23:25:26 -0300 Subject: [PATCH 353/572] allow protobuf to come from the system lib --- .drone.jsonnet | 2 +- external/CMakeLists.txt | 31 +++++++++++++++++-------------- proto/CMakeLists.txt | 17 ----------------- 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 2c15bf83..36bafce3 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -10,7 +10,7 @@ local submodules = { local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; -local libngtcp2_deps = ['libgnutls28-dev', 'libngtcp2-dev', 'libngtcp2-crypto-gnutls-dev']; +local libngtcp2_deps = ['libgnutls28-dev', 'libprotobuf-dev', 'libngtcp2-dev', 'libngtcp2-crypto-gnutls-dev']; local default_deps_nocxx = [ 'nlohmann-json3-dev', diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 194db24b..659b3a7b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -33,19 +33,6 @@ if(SUBMODULE_CHECK) endif() endif() -set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) -set(protobuf_INSTALL ON CACHE BOOL "" FORCE) -set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) -set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) -set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") -set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) -add_subdirectory(protobuf) - - if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() @@ -67,7 +54,7 @@ macro(system_or_submodule BIGNAME smallname pkgconf subdir) message(STATUS "using ${smallname} submodule") add_subdirectory(${subdir}) endif() - if(NOT TARGET ${smallname}::${smallname}) + if(TARGET ${smallname} AND NOT TARGET ${smallname}::${smallname}) add_library(${smallname}::${smallname} ALIAS ${smallname}) endif() if(BUILD_STATIC_DEPS AND STATIC_BUNDLE) @@ -167,6 +154,22 @@ libsodium_internal_subdir() libsession_static_bundle(libsodium::sodium-internal) +set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) +set(protobuf_INSTALL ON CACHE BOOL "" FORCE) +set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) +set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") +set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) +system_or_submodule(PROTOBUF_LITE protobuf_lite protobuf-lite>=3.21 protobuf) +if(TARGET PkgConfig::PROTOBUF_LITE AND NOT TARGET protobuf::libprotobuf-lite) + add_library(protobuf::libprotobuf-lite ALIAS PkgConfig::PROTOBUF_LITE) +endif() + + set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "") set(ZSTD_BUILD_TESTS OFF CACHE BOOL "") set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "") diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 7c682a80..324b116b 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -1,21 +1,4 @@ -function(check_target target) - if (NOT TARGET ${target}) - message(FATAL_ERROR "Project failed to compile required target: ${target}") - endif() -endfunction() - -if (BUILD_SHARED_LIBS AND NOT BUILD_STATIC_DEPS) - find_package(PkgConfig REQUIRED) - pkg_check_modules(PROTOBUF_LITE protobuf-lite>=3.21 IMPORTED_TARGET) - if(PROTOBUF_LITE_FOUND) - add_library(protobuf_lite INTERFACE IMPORTED) - target_link_libraries(protobuf_lite INTERFACE PkgConfig::PROTOBUF_LITE) - add_library(protobuf::libprotobuf-lite ALIAS protobuf_lite) - endif() -endif() - -check_target(protobuf::libprotobuf-lite) libsession_static_bundle(protobuf::libprotobuf-lite) From 84f258bd2ac6ed55700a084ba94321fc281f77b1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:46:04 +1000 Subject: [PATCH 354/572] Added missing headers --- include/session/blinding.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index d2d67d33..9d3def8a 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -3,6 +3,7 @@ #include #include +#include "util.hpp" #include "platform.hpp" #include "sodium_array.hpp" From 7e20d4351bedc2cd08de611dc797e90574d06125 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Jun 2024 17:55:07 +1000 Subject: [PATCH 355/572] Added new sodium_array header changes --- include/session/blinding.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 9d3def8a..d2d67d33 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -3,7 +3,6 @@ #include #include -#include "util.hpp" #include "platform.hpp" #include "sodium_array.hpp" From 7664449c8382c41e2eaeda7fbdc96597c04571c3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 09:00:57 +1000 Subject: [PATCH 356/572] Addressed PR comments --- src/blinding.cpp | 40 ++++++++++++++-------------------------- tests/test_blinding.cpp | 6 +++--- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/blinding.cpp b/src/blinding.cpp index e1a39fbe..ebf0fdde 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -304,32 +304,26 @@ std::pair blind25_key_pair( static const auto version_blinding_hash_key_sig = to_unsigned_sv("VersionCheckKey_sig"sv); std::pair blind_version_key_pair(ustring_view ed25519_sk) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) + if (ed25519_sk.size() != 32 && ed25519_sk.size() != 64) throw std::invalid_argument{ "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " "value"}; - uc32 seed; + std::pair result; + auto& [pk, sk] = result; crypto_generichash_blake2b( - seed.data(), - seed.size(), + sk.data(), + 32, ed25519_sk.data(), 32, version_blinding_hash_key_sig.data(), version_blinding_hash_key_sig.size()); - uc32 pk; - cleared_uc64 sk; - if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data())) + // Reuse `sk` to avoid needing extra secure erasing: + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), sk.data())) throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; - return {pk, sk}; + return result; } static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); @@ -518,8 +512,7 @@ LIBSESSION_C_API bool session_blind15_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -534,8 +527,7 @@ LIBSESSION_C_API bool session_blind25_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -549,8 +541,7 @@ LIBSESSION_C_API bool session_blind_version_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto result = session::blind_version_key_pair({ed25519_seckey, 64}); - auto [b_pk, b_sk] = result; + auto [b_pk, b_sk] = session::blind_version_key_pair({ed25519_seckey, 64}); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -566,9 +557,8 @@ LIBSESSION_C_API bool session_blind15_sign( size_t msg_len, unsigned char* blinded_sig_out) { try { - auto result = session::blind15_sign( + auto sig = session::blind15_sign( {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -583,9 +573,8 @@ LIBSESSION_C_API bool session_blind25_sign( size_t msg_len, unsigned char* blinded_sig_out) { try { - auto result = session::blind25_sign( + auto sig = session::blind25_sign( {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -599,9 +588,8 @@ LIBSESSION_C_API bool session_blind_version_sign( size_t timestamp, unsigned char* blinded_sig_out) { try { - auto result = session::blind_version_sign( + auto sig = session::blind_version_sign( {ed25519_seckey, 64}, static_cast(platform), timestamp); - auto sig = result; std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index aa9bb89a..a684cc5e 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -278,7 +278,7 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == - "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f6988e8adb27e7b8ce776fcc25b" + "4091bdaafefd7ddc3398db877c894716b2b24f12dac15ad414a3e4f0b6ac1c6788e8adb27e7b8ce776fcc25b" "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); } @@ -287,8 +287,8 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == - "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e920fa57daf4627c68f43fcbddb" - "2d465d5ea11def523f3befb2bbee39c769676305"); + "adfb25eafec6fb0037fc39145e64badfbdd08cc95a77c01577ecc623fd856fb05b90a921fc6b805e6a730ca9" + "505e1c069f256020a76beb2ecbb6e32c47e12104"); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From 6a6c9d82904baa6b03f63e77d047d4dbb3b182ad Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 12:12:40 +1000 Subject: [PATCH 357/572] Added a missing import --- include/session/blinding.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/session/blinding.h b/include/session/blinding.h index c83b9f09..56469212 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -6,6 +6,7 @@ extern "C" { #include +#include "platform.h" #include "export.h" #include "platform.h" From e452022082f918345ba71f8c768e1b8b86f5240e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Jul 2024 12:58:07 +1000 Subject: [PATCH 358/572] Updated the docs to be correct, and explicit about a unix timestamp --- include/session/blinding.h | 8 ++------ include/session/blinding.hpp | 7 ++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 56469212..a2f39b2d 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -6,7 +6,6 @@ extern "C" { #include -#include "platform.h" #include "export.h" #include "platform.h" @@ -117,11 +116,8 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// -/// Takes the Ed25519 secret key (64 bytes). Returns the blinded public key, signature and -/// timestamp. -/// -/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. -/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +/// Takes the Ed25519 secret key (64 bytes), platform and unix timestamp. Returns a version-blinded +/// signature. LIBSESSION_EXPORT bool session_blind_version_sign( const unsigned char* ed25519_seckey, /* 64 bytes */ CLIENT_PLATFORM platform, diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index d2d67d33..c6cc1ee3 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -171,11 +171,8 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// -/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key, -/// signature and timestamp. -/// -/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. -/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), current platform and unix timestamp. +/// Returns the version-blinded signature. ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp); /// Takes in a standard session_id and returns a flag indicating whether it matches the given From 659b40896edb1cc754cb5b89f7f145d0fabb4f47 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 25 Jul 2024 14:04:19 -0300 Subject: [PATCH 359/572] Avoid reuse of cleared_uc64 data libsodium is apparently not tolerant of overlapping input/output ranges for recovering a pubkey/seckey pair from seed. --- src/blinding.cpp | 5 +++-- tests/test_blinding.cpp | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/blinding.cpp b/src/blinding.cpp index ebf0fdde..44c1549d 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -310,9 +310,10 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk) { "value"}; std::pair result; + cleared_uc32 blind_seed; auto& [pk, sk] = result; crypto_generichash_blake2b( - sk.data(), + blind_seed.data(), 32, ed25519_sk.data(), 32, @@ -320,7 +321,7 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk) { version_blinding_hash_key_sig.size()); // Reuse `sk` to avoid needing extra secure erasing: - if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), sk.data())) + if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), blind_seed.data())) throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; return result; diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index a684cc5e..56a7bc90 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -277,9 +277,22 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { auto [pubkey, seckey] = blind_version_key_pair(to_usv(seed1)); CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); + + CHECK(oxenc::to_hex(seckey.begin() + 32, seckey.end()) == + oxenc::to_hex(pubkey.begin(), pubkey.end())); + + // Hash ourselves just to make sure we get what we expect for the seed part of the secret key: + cleared_uc32 expect_seed; + static const auto hash_key = to_unsigned_sv("VersionCheckKey_sig"sv); + crypto_generichash_blake2b( + expect_seed.data(), 32, seed1.data(), 32, hash_key.data(), hash_key.size()); + + CHECK(oxenc::to_hex(seckey.begin(), seckey.begin() + 32) == + oxenc::to_hex(expect_seed.begin(), expect_seed.end())); + CHECK(oxenc::to_hex(seckey.begin(), seckey.end()) == - "4091bdaafefd7ddc3398db877c894716b2b24f12dac15ad414a3e4f0b6ac1c6788e8adb27e7b8ce776fcc25b" - "c1501fb2888fcac0308e52fb10044f789ae1a8fa"); + "91faddc2c36da4f7bcf24fd977d9ca5346ae7489cfd43c58cad9eaaa6ed60f69" + "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); } TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { @@ -287,8 +300,8 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == - "adfb25eafec6fb0037fc39145e64badfbdd08cc95a77c01577ecc623fd856fb05b90a921fc6b805e6a730ca9" - "505e1c069f256020a76beb2ecbb6e32c47e12104"); + "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e92" + "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From 9d1b1f7e87b1061b051f2fe5e729135a6fd8c299 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Jul 2024 12:31:23 +1000 Subject: [PATCH 360/572] Fixed a few issues resulting from merges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed some build issues from the networking merge • Fixed a build macro name collision • Fixed the broken unit tests • Added a FIXME to the needs_dump workaround --- external/CMakeLists.txt | 10 +++++----- include/session/config/base.hpp | 10 +++++++--- include/session/util.hpp | 2 +- src/config/base.cpp | 4 +++- src/network.cpp | 21 ++++++++++++--------- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 659b3a7b..c9c8b160 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -37,7 +37,7 @@ if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() -macro(system_or_submodule BIGNAME smallname pkgconf subdir) +macro(libsession_system_or_submodule BIGNAME smallname pkgconf subdir) option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES) pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET GLOBAL) @@ -102,14 +102,14 @@ endif() set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") if(ENABLE_ONIONREQ) - system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) + libsession_system_or_submodule(OXENQUIC quic liboxenquic>=1.1.0 oxen-libquic) endif() if(NOT TARGET oxenc::oxenc) # The oxenc target will already exist if we load libquic above via submodule set(OXENC_BUILD_TESTS OFF CACHE BOOL "") set(OXENC_BUILD_DOCS OFF CACHE BOOL "") - system_or_submodule(OXENC oxenc liboxenc>=1.1.0 oxen-libquic/external/oxen-encoding) + libsession_system_or_submodule(OXENC oxenc liboxenc>=1.1.0 oxen-libquic/external/oxen-encoding) endif() if(NOT TARGET oxen::logging) @@ -164,7 +164,7 @@ set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) -system_or_submodule(PROTOBUF_LITE protobuf_lite protobuf-lite>=3.21 protobuf) +libsession_system_or_submodule(PROTOBUF_LITE protobuf_lite protobuf-lite>=3.21 protobuf) if(TARGET PkgConfig::PROTOBUF_LITE AND NOT TARGET protobuf::libprotobuf-lite) add_library(protobuf::libprotobuf-lite ALIAS PkgConfig::PROTOBUF_LITE) endif() @@ -193,4 +193,4 @@ libsession_static_bundle(libzstd_static) set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use -system_or_submodule(NLOHMANN nlohmann_json nlohmann_json>=3.7.0 nlohmann-json) +libsession_system_or_submodule(NLOHMANN nlohmann_json nlohmann_json>=3.7.0 nlohmann-json) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 7c3bd5de..e790c31f 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -181,7 +181,7 @@ class ConfigBase : public ConfigSig { // calling set_state, which sets to to true implicitly). bool _needs_dump = false; - ustring last_dumped = to_unsigned(""); + ustring last_dumped; // Sets the current state; this also sets _needs_dump to true. If transitioning to a dirty // state and we know our current message hash, that hash gets added to `old_hashes_` to be @@ -1100,12 +1100,16 @@ class ConfigBase : public ConfigSig { /// Outputs: /// - `bool` -- Returns true if something has changed since last call to dump virtual bool needs_dump() const { - if(_needs_dump) { + // FIXME: There is an issue where modifying the config, triggering a dump and then modifying + // the config again won't result in this function returning true. This is a temporary + // workaround until a more permanent solution can be implemented (when removing this the + // `last_dumped` should be removed everywhere it's used). + if (_needs_dump) { return _needs_dump; } auto current_dump = this->make_dump(); auto dump_did_change = this->last_dumped != current_dump; - return dump_did_change; + return dump_did_change; } /// API: base/ConfigBase::add_key diff --git a/include/session/util.hpp b/include/session/util.hpp index fc2c4971..b6825236 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -9,9 +9,9 @@ #include #include #include +#include #include #include -#include #include "types.hpp" diff --git a/src/config/base.cpp b/src/config/base.cpp index 603ee7cf..543e13f5 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -401,8 +401,10 @@ ConfigBase::ConfigBase( if (dump) { init_from_dump(from_unsigned_sv(*dump)); this->last_dumped = ustring{*dump}; - } else + } else { _config = std::make_unique(); + this->last_dumped = make_dump(); + } init_sig_keys(ed25519_pubkey, ed25519_secretkey); } diff --git a/src/network.cpp b/src/network.cpp index e6db8504..f2757662 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1156,7 +1156,9 @@ void Network::with_paths_and_pool( if (unused_nodes.empty()) throw std::runtime_error{"Not enough remaining nodes."}; - service_node node; + // Default to using the last unused node + auto node = unused_nodes.back(); + auto using_last_unused_node = true; // If this is the final node then we only want to consider nodes // which are at least the 'min_final_node_version' @@ -1175,6 +1177,7 @@ void Network::with_paths_and_pool( if (it != unused_nodes.end()) { node = *it; unused_nodes.erase(it); + using_last_unused_node = false; } else { // If we couldn't find a suitable node then just fallback to // the old logic @@ -1185,14 +1188,14 @@ void Network::with_paths_and_pool( "path for {}", path_type_name(path_type, single_path_mode), request_id); - node = unused_nodes.back(); - unused_nodes.pop_back(); } - } else { - node = unused_nodes.back(); - unused_nodes.pop_back(); } + // If we are using the last unused node then we need to remove it + // from the vector here + if (using_last_unused_node) + unused_nodes.pop_back(); + // Ensure we don't put two nodes with the same IP into the same path auto snode_with_ip_it = std::find_if( path.begin(), @@ -2248,8 +2251,8 @@ void Network::get_client_version( // Generate the auth signature auto blinded_keys = blind_version_key_pair(to_unsigned_sv(seckey.view())); auto timestamp = std::chrono::duration_cast( - (std::chrono::system_clock::now()).time_since_epoch()) - .count(); + (std::chrono::system_clock::now()).time_since_epoch()) + .count(); auto signature = blind_version_sign(to_unsigned_sv(seckey.view()), platform, timestamp); auto pubkey = x25519_pubkey::from_hex(file_server_pubkey); std::string blinded_pk_hex; @@ -2632,7 +2635,7 @@ void Network::handle_errors( path_type = info.path_type, target = info.target, timeout, - old_path = info.path, + old_path = path, response, &cv, &mtx, From d6147ef385e39d3fae64611dcf16ac1e14bab333 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 27 Jul 2024 11:37:53 +1000 Subject: [PATCH 361/572] Fixed merge bug, removed extra timeout threshold, needs_dump fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an issue where the priority last node logic was applying to the second last node • Removed the larger `path_timeout_threshold` (shouldn't be needed due to the network fixes) • Added `needs_dump` fix --- include/session/config/base.hpp | 15 +-------- include/session/network.hpp | 1 - src/config/base.cpp | 10 +++--- src/network.cpp | 54 ++++++++++++++++++++------------- tests/test_config_contacts.cpp | 34 +++++++++++++++++++++ 5 files changed, 72 insertions(+), 42 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index e790c31f..d9a14b2d 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -181,8 +181,6 @@ class ConfigBase : public ConfigSig { // calling set_state, which sets to to true implicitly). bool _needs_dump = false; - ustring last_dumped; - // Sets the current state; this also sets _needs_dump to true. If transitioning to a dirty // state and we know our current message hash, that hash gets added to `old_hashes_` to be // deleted at the next push. @@ -1099,18 +1097,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if something has changed since last call to dump - virtual bool needs_dump() const { - // FIXME: There is an issue where modifying the config, triggering a dump and then modifying - // the config again won't result in this function returning true. This is a temporary - // workaround until a more permanent solution can be implemented (when removing this the - // `last_dumped` should be removed everywhere it's used). - if (_needs_dump) { - return _needs_dump; - } - auto current_dump = this->make_dump(); - auto dump_did_change = this->last_dumped != current_dump; - return dump_did_change; - } + virtual bool needs_dump() const { return _needs_dump; } /// API: base/ConfigBase::add_key /// diff --git a/include/session/network.hpp b/include/session/network.hpp index 04254f1b..b97c78e3 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -72,7 +72,6 @@ struct onion_path { connection_info conn_info; std::vector nodes; uint8_t failure_count; - uint8_t timeout_count; bool operator==(const onion_path& other) const { // The `conn_info` and failure/timeout counts can be reset for a path in a number diff --git a/src/config/base.cpp b/src/config/base.cpp index 543e13f5..733b9e50 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -46,6 +46,8 @@ MutableConfigMessage& ConfigBase::dirty() { if (_state != ConfigState::Dirty) { set_state(ConfigState::Dirty); _config = std::make_unique(*_config, increment_seqno); + } else { + _needs_dump = true; } if (auto* mut = dynamic_cast(_config.get())) @@ -368,7 +370,6 @@ ustring ConfigBase::dump() { auto d = make_dump(); _needs_dump = false; - this->last_dumped = d; return d; } @@ -398,13 +399,10 @@ ConfigBase::ConfigBase( if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; - if (dump) { + if (dump) init_from_dump(from_unsigned_sv(*dump)); - this->last_dumped = ustring{*dump}; - } else { + else _config = std::make_unique(); - this->last_dumped = make_dump(); - } init_sig_keys(ed25519_pubkey, ed25519_secretkey); } diff --git a/src/network.cpp b/src/network.cpp index f2757662..23874e52 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -69,9 +69,6 @@ namespace { // The number of times a path can fail before it's replaced. constexpr uint16_t path_failure_threshold = 3; - // The number of times a path can timeout before it's replaced. - constexpr uint16_t path_timeout_threshold = 10; - // The number of times a snode can fail before it's replaced. constexpr uint16_t snode_failure_threshold = 3; @@ -739,8 +736,7 @@ std::pair> Network::get_connection_i // Depending on the state of the snode pool cache it's possible for certain // errors to result in being permanently unable to establish a connection, to // avoid this we handle those error codes and drop - handle_node_error( - target, path_type, {{target, nullptr, nullptr}, {target}, 0, 0}); + handle_node_error(target, path_type, {{target, nullptr, nullptr}, {target}, 0}); }); if (!*done) { @@ -1165,7 +1161,7 @@ void Network::with_paths_and_pool( // // Note: This should be able to be removed after the mandatory // service node update period for 2.8.0 finishes - if (path.size() == path_size - 2) { + if (path.size() == path_size - 1) { auto it = std::find_if( unused_nodes.begin(), unused_nodes.end(), @@ -1208,7 +1204,7 @@ void Network::with_paths_and_pool( path.push_back(node); } - paths_result.emplace_back(onion_path{std::move(info), path, 0, 0}); + paths_result.emplace_back(onion_path{std::move(info), path, 0}); // Log that a path was built std::vector node_descriptions; @@ -1413,7 +1409,7 @@ void Network::with_path( // No need to call the 'paths_changed' callback as the paths haven't // actually changed, just their connection info - auto updated_path = onion_path{std::move(info), path.nodes, 0, 0}; + auto updated_path = onion_path{std::move(info), path.nodes, 0}; auto paths_count = net.call_get( [this, path_type, path, updated_path]() mutable -> uint8_t { switch (path_type) { @@ -1942,7 +1938,7 @@ void Network::send_request( } catch (const status_code_exception& e) { handle_errors( info, - {{target, nullptr, nullptr}, {target}, 0, 0}, + {{target, nullptr, nullptr}, {target}, 0}, false, e.status_code, e.what(), @@ -1950,7 +1946,7 @@ void Network::send_request( } catch (const std::exception& e) { handle_errors( info, - {{target, nullptr, nullptr}, {target}, 0, 0}, + {{target, nullptr, nullptr}, {target}, 0}, resp.timed_out, -1, e.what(), @@ -2072,6 +2068,17 @@ void Network::send_onion_request( "non-success status " "code.")}; + // For debugging purposes if the error was a redirect retry then + // we want to log that the retry was successful as this will + // help identify how often we are receiving incorrect 421 errors + if (info.retry_reason == request_info::RetryReason::redirect) + log::info( + cat, + "Received valid response after 421 retry in " + "request {} for {}.", + info.request_id, + path_type_name(info.path_type, single_path_mode)); + // Try process the body in case it was a batch request which // failed std::optional results; @@ -2479,6 +2486,17 @@ void Network::handle_errors( } } + // In trace mode log all error info + log::trace( + cat, + "Received network error in request {} for {}, status_code: {}, timeout: {}, response: " + "{}.", + info.request_id, + path_type_name(info.path_type, single_path_mode), + status_code, + timeout, + response.value_or("(No Response)")); + // A timeout could be caused because the destination is unreachable rather than the the path // (eg. if a user has an old SOGS which is no longer running on their device they will get a // timeout) so if we timed out while sending a proxied request we assume something is wrong on @@ -2738,16 +2756,11 @@ void Network::handle_errors( // If we didn't find the specific node or the paths connection was closed then increment the // path failure count if (!found_invalid_node || !updated_path.conn_info.is_valid()) { - if (timeout) - updated_path.timeout_count += 1; - else - updated_path.failure_count += 1; + updated_path.failure_count += 1; - // If the path has failed or timed out too many times we want to drop the guard - // snode (marking it as invalid) and increment the failure count of each node in - // the path) - if (updated_path.failure_count >= path_failure_threshold || - updated_path.timeout_count >= path_timeout_threshold) { + // If the path has failed too many times we want to drop the guard snode (marking it as + // invalid) and increment the failure count of each node in the path) + if (updated_path.failure_count >= path_failure_threshold) { for (auto& it : updated_path.nodes) { auto failure_count = updated_failure_counts.try_emplace(it.to_string(), 0).first->second; @@ -2797,8 +2810,7 @@ void Network::handle_errors( // Drop the path if invalid auto already_handled_failure = false; - if (updated_path.failure_count >= path_failure_threshold || - updated_path.timeout_count >= path_timeout_threshold) { + if (updated_path.failure_count >= path_failure_threshold) { auto old_paths_size = paths_for_type(path_type).size(); // Close the connection immediately (just in case there are other requests happening) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index c8851953..c0dec888 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -448,3 +448,37 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { // With tons of duplicate info the push should have been nicely compressible: CHECK(dump.size() > 1'320'000); } + +TEST_CASE("needs_dump bug", "[config][needs_dump]") { + + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + + session::config::Contacts contacts{ustring_view{seed}, std::nullopt}; + + CHECK_FALSE(contacts.needs_dump()); + + auto c = contacts.get_or_construct( + "050000000000000000000000000000000000000000000000000000000000000000"sv); + + c.approved = true; + contacts.set(c); + + CHECK(contacts.needs_dump()); + + c.approved_me = true; + contacts.set(c); + + CHECK(contacts.needs_dump()); + + (void)contacts.dump(); + + CHECK_FALSE(contacts.needs_dump()); + + c.approved = false; + contacts.set(c); + CHECK(contacts.needs_dump()); + + c.approved_me = false; + contacts.set(c); + CHECK(contacts.needs_dump()); +} From aa62ac55d0775c323b477d7108463646637fb07c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 26 Jul 2024 11:54:40 -0300 Subject: [PATCH 362/572] Fix dirty() not setting _needs_dump when already dirty If a dirty config gets dumped, _needs_dump gets reset to false, but then if it is updated again, _needs_dump wasn't getting set because the config was already dirty, and thus not entering the code path that reset _needs_dump. --- src/config/base.cpp | 2 ++ tests/test_config_contacts.cpp | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/config/base.cpp b/src/config/base.cpp index 47af4c14..b68b79f6 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -39,6 +39,8 @@ MutableConfigMessage& ConfigBase::dirty() { if (_state != ConfigState::Dirty) { set_state(ConfigState::Dirty); _config = std::make_unique(*_config, increment_seqno); + } else { + _needs_dump = true; } if (auto* mut = dynamic_cast(_config.get())) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index c8851953..c0dec888 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -448,3 +448,37 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { // With tons of duplicate info the push should have been nicely compressible: CHECK(dump.size() > 1'320'000); } + +TEST_CASE("needs_dump bug", "[config][needs_dump]") { + + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + + session::config::Contacts contacts{ustring_view{seed}, std::nullopt}; + + CHECK_FALSE(contacts.needs_dump()); + + auto c = contacts.get_or_construct( + "050000000000000000000000000000000000000000000000000000000000000000"sv); + + c.approved = true; + contacts.set(c); + + CHECK(contacts.needs_dump()); + + c.approved_me = true; + contacts.set(c); + + CHECK(contacts.needs_dump()); + + (void)contacts.dump(); + + CHECK_FALSE(contacts.needs_dump()); + + c.approved = false; + contacts.set(c); + CHECK(contacts.needs_dump()); + + c.approved_me = false; + contacts.set(c); + CHECK(contacts.needs_dump()); +} From 23c943d44e0127dc243de3ed4f72c550e06638ee Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 31 Jul 2024 12:25:54 +1000 Subject: [PATCH 363/572] Fixed a few bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an issue where the headers weren't correctly passed when downloading files • Fixed an issue where retrying a request due to a decryption failure was invalid • Added a check to ensure the cache_path exists before trying to remove it (was seeing errors in some cases) --- include/session/network.hpp | 1 + src/network.cpp | 81 +++++++++++++++---------------------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index b97c78e3..1f185166 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -88,6 +88,7 @@ struct request_info { std::string request_id; service_node target; + session::onionreq::network_destination destination; std::string endpoint; std::optional body; std::optional original_body; diff --git a/src/network.cpp b/src/network.cpp index 23874e52..0aadd438 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -305,7 +305,7 @@ void Network::load_cache_from_disk() { // If the cache is for the wrong network then delete everything auto testnet_stub = cache_path / file_testnet; bool cache_is_for_testnet = fs::exists(testnet_stub); - if (use_testnet != cache_is_for_testnet) + if (use_testnet != cache_is_for_testnet && fs::exists(cache_path)) fs::remove_all(cache_path); // Create the cache directory (and swarm_dir, inside it) if needed @@ -462,8 +462,8 @@ void Network::load_cache_from_disk() { swarm_cache = loaded_cache; // Remove any expired cache files - for (auto& cache_path : caches_to_remove) - fs::remove_all(cache_path); + for (auto& expired_cache : caches_to_remove) + fs::remove_all(expired_cache); log::info( cat, @@ -473,7 +473,9 @@ void Network::load_cache_from_disk() { caches_to_remove.size()); } catch (const std::exception& e) { log::error(cat, "Failed to load snode cache, will rebuild ({}).", e.what()); - fs::remove_all(cache_path); + + if (fs::exists(cache_path)) + fs::remove_all(cache_path); } } @@ -583,7 +585,8 @@ void Network::disk_write_thread_loop() { swarm_cache = {}; lock.unlock(); - fs::remove_all(cache_path); + if (fs::exists(cache_path)) + fs::remove_all(cache_path); lock.lock(); need_clear_cache = false; } @@ -2001,6 +2004,7 @@ void Network::send_onion_request( request_info info{ request_id, path->nodes[0], + destination, "onion_req", onion_req_payload, body, @@ -2417,6 +2421,7 @@ std::pair Network::validate_response(quic::message resp, void Network::handle_node_error(service_node node, PathType path_type, onion_path path) { handle_errors( {"Node Error", + node, node, "", std::nullopt, @@ -2458,7 +2463,7 @@ void Network::handle_errors( path_type_name(info.path_type, single_path_mode)); return send_onion_request( info.path_type, - info.target, + info.destination, info.original_body, info.swarm_pubkey, info.timeout, @@ -2651,8 +2656,6 @@ void Network::handle_errors( net.call([this, request_id = info.request_id, path_type = info.path_type, - target = info.target, - timeout, old_path = path, response, &cv, @@ -2962,6 +2965,25 @@ std::vector convert_service_nodes( return converted_nodes; } +ServerDestination convert_server_destination(const network_server_destination server) { + std::optional>> headers; + if (server.headers_size > 0) { + headers = std::vector>{}; + + for (size_t i = 0; i < server.headers_size; i++) + headers->emplace_back(server.headers[i], server.header_values[i]); + } + + return ServerDestination{ + server.protocol, + server.host, + server.endpoint, + x25519_pubkey::from_hex({server.x25519_pubkey, 64}), + server.port, + headers, + server.method}; +} + } // namespace session::network // MARK: C API @@ -3168,27 +3190,12 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( server.x25519_pubkey && callback); try { - std::optional>> headers; - if (server.headers_size > 0) { - headers = std::vector>{}; - - for (size_t i = 0; i < server.headers_size; i++) - headers->emplace_back(server.headers[i], server.header_values[i]); - } - std::optional body; if (body_size > 0) body = {body_, body_size}; unbox(network).send_onion_request( - ServerDestination{ - server.protocol, - server.host, - server.endpoint, - x25519_pubkey::from_hex({server.x25519_pubkey, 64}), - server.port, - headers, - server.method}, + convert_server_destination(server), body, std::nullopt, std::chrono::milliseconds{timeout_ms}, @@ -3225,28 +3232,13 @@ LIBSESSION_C_API void network_upload_to_server( server.x25519_pubkey && callback); try { - std::optional>> headers; - if (server.headers_size > 0) { - headers = std::vector>{}; - - for (size_t i = 0; i < server.headers_size; i++) - headers->emplace_back(server.headers[i], server.header_values[i]); - } - std::optional file_name; if (file_name_) file_name = file_name_; unbox(network).upload_file_to_server( {data, data_len}, - ServerDestination{ - server.protocol, - server.host, - server.endpoint, - x25519_pubkey::from_hex({server.x25519_pubkey, 64}), - server.port, - headers, - server.method}, + convert_server_destination(server), file_name, std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( @@ -3280,14 +3272,7 @@ LIBSESSION_C_API void network_download_from_server( try { unbox(network).download_file( - ServerDestination{ - server.protocol, - server.host, - server.endpoint, - x25519_pubkey::from_hex({server.x25519_pubkey, 64}), - server.port, - std::nullopt, - server.method}, + convert_server_destination(server), std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, From 59a4de45b56641209f1d15bccf28bba6db72b5d2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 1 Aug 2024 22:00:05 -0300 Subject: [PATCH 364/572] Compilation error/warning fixes - std::min was complaining on my system about a disagreement between a deduced long int and a long long int. Simplified it to just apply the min on the milliseconds type directly. - Add a log and throw if an unhandled path_type occurs; this shouldn't actually fire (unless a new type gets added, or someone abuses the enum), but silences the warning. --- src/network.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 0aadd438..4863cd10 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1440,6 +1440,11 @@ void Network::with_path( updated_path); return download_paths.size(); } + log::error( + cat, + "Internal error: unhandled path type {}", + static_cast(path_type)); + throw std::logic_error{"Internal error: unhandled path type!"}; }); return {updated_path, paths_count}; @@ -1632,11 +1637,8 @@ void Network::find_valid_guard_node_recursive( // If we got a network unreachable error then we want to delay on an exponential // curve so that we don't trash the battery life in the device loses connection if (error) { - std::chrono::duration maxDelay = 3s; - delay = std::chrono::milliseconds(std::min( - std::chrono::duration_cast(maxDelay) - .count(), - static_cast(100 * std::pow(2, test_attempt)))); + constexpr std::chrono::milliseconds maxDelay = 3s; + delay = std::min(maxDelay, 100ms * (1 << test_attempt)); } std::thread retry_thread([this, From f0016f512956c3f8fef83e3daf17e76fec272968 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 2 Aug 2024 11:15:18 +1000 Subject: [PATCH 365/572] Removed some duplicate code --- include/session/network.hpp | 2 ++ src/network.cpp | 35 ++++++++++++++--------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 1f185166..6bd9682f 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -399,6 +399,8 @@ class Network { std::vector pool, std::optional error)> callback); + void build_paths_and_pool_in_background(std::string caller, PathType path_type); + /// API: network/with_path /// /// Retrieves a valid onion request path to perform a request on. If there aren't currently any diff --git a/src/network.cpp b/src/network.cpp index 4863cd10..a1a35147 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -275,17 +275,8 @@ Network::Network( } // Kick off a separate thread to build paths (may as well kick this off early) - if (pre_build_paths) { - std::thread build_paths_thread( - &Network::with_paths_and_pool, - this, - "Constructor", - PathType::standard, - std::nullopt, - [](std::vector, std::vector, std::optional) { - }); - build_paths_thread.detach(); - } + if (pre_build_paths) + build_paths_and_pool_in_background("Constructor", PathType::standard); } Network::~Network() { @@ -1282,6 +1273,17 @@ void Network::with_paths_and_pool( return callback(updated_paths, updated_pool, error); } +void Network::build_paths_and_pool_in_background(std::string caller, PathType path_type) { + std::thread build_paths_thread( + &Network::with_paths_and_pool, + this, + caller, + path_type, + std::nullopt, + [](std::vector, std::vector, std::optional) {}); + build_paths_thread.detach(); +} + std::vector Network::valid_paths(std::vector paths) { auto valid_paths = paths; auto valid_paths_end = @@ -1485,16 +1487,7 @@ void Network::with_path( __PRETTY_FUNCTION__, request_id, new_request_id); - std::thread build_additional_paths_thread( - &Network::with_paths_and_pool, - this, - new_request_id, - path_type, - std::nullopt, - [](std::optional>, - std::vector, - std::optional) {}); - build_additional_paths_thread.detach(); + build_paths_and_pool_in_background(new_request_id, path_type); } // We have a valid path for the standard path type then update the status in case we had From 85147d8a418180df226c51d61d47cbd95a15290e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 5 Aug 2024 11:15:53 +1000 Subject: [PATCH 366/572] feat: allow config wrappers to be extended --- include/session/config/groups/info.hpp | 2 +- include/session/config/groups/keys.hpp | 2 +- include/session/config/groups/members.hpp | 2 +- include/session/config/user_profile.hpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 7650dc18..fbd44629 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -26,7 +26,7 @@ using namespace std::literals; /// p - group profile url /// q - group profile decryption key (binary) -class Info final : public ConfigBase { +class Info : public ConfigBase { public: /// Limits for the name & description strings, in bytes. If longer, we truncate to these diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 71149385..fc194e15 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -72,7 +72,7 @@ using namespace std::literals; /// - A new key and nonce is created from a 56-byte H(M0 || M1 || ... || Mn || g || S, /// key="SessionGroupKeyGen"), where S = H(group_seed, key="SessionGroupKeySeed"). -class Keys final : public ConfigSig { +class Keys : public ConfigSig { Ed25519Secret user_ed25519_sk; diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 07e8fc13..d1e6ee50 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -325,7 +325,7 @@ struct member { void load(const dict& info_dict); }; -class Members final : public ConfigBase { +class Members : public ConfigBase { public: // No default constructor diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 8f5db6b7..2f4b3c45 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -25,7 +25,7 @@ using namespace std::literals; /// omitted if the setting has not been explicitly set (or has been explicitly cleared for some /// reason). -class UserProfile final : public ConfigBase { +class UserProfile : public ConfigBase { public: // No default constructor From 642f48f248927263c294cf7e9a24277ce4b00341 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 5 Aug 2024 15:17:26 +1000 Subject: [PATCH 367/572] chore: remove unused hacky_list on swarm-auth-test.cpp --- tests/swarm-auth-test.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/swarm-auth-test.cpp b/tests/swarm-auth-test.cpp index 3d235f15..07d2335f 100644 --- a/tests/swarm-auth-test.cpp +++ b/tests/swarm-auth-test.cpp @@ -46,15 +46,6 @@ static std::string session_id_from_ed(ustring_view ed_pk) { return sid; } -// Hacky little class that implements `[n]` on a std::list. This is inefficient (since it access -// has to iterate n times through the list) but we only use it on small lists in this test code so -// convenience wins over efficiency. (Why not just use a vector? Because vectors requires `T` to -// be moveable, so we'd either have to use std::unique_ptr for members, which is also annoying). -template -struct hacky_list : std::list { - T& operator[](size_t n) { return *std::next(std::begin(*this), n); } -}; - struct pseudo_client { std::array secret_key; const ustring_view public_key{secret_key.data() + 32, 32}; From 0f79e97f2eed8510b2f04fa6e5234a1755866d1c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 6 Aug 2024 17:15:20 +1000 Subject: [PATCH 368/572] Removed the 'paths_and_pool_loop' and refactored the path build logic --- include/session/network.hpp | 331 ++-- src/network.cpp | 2972 ++++++++++++++++------------------- tests/CMakeLists.txt | 10 +- tests/test_network.cpp | 52 +- 4 files changed, 1528 insertions(+), 1837 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 6bd9682f..29062673 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -73,6 +73,8 @@ struct onion_path { std::vector nodes; uint8_t failure_count; + bool is_valid() const { return !nodes.empty() && conn_info.is_valid(); }; + bool operator==(const onion_path& other) const { // The `conn_info` and failure/timeout counts can be reset for a path in a number // of situations so just use the nodes to determine if the paths match @@ -87,7 +89,6 @@ struct request_info { }; std::string request_id; - service_node target; session::onionreq::network_destination destination; std::string endpoint; std::optional body; @@ -96,6 +97,9 @@ struct request_info { PathType path_type; std::chrono::milliseconds timeout; bool node_destination; + + /// The reason we are retrying the request (if it's a retry). Generally only used for internal + /// purposes (like receiving a `421`) in order to prevent subsequent retries. std::optional retry_reason; }; @@ -117,22 +121,38 @@ class Network { bool need_clear_cache = false; // Values persisted to disk - std::vector snode_pool; + std::vector snode_cache; std::unordered_map snode_failure_counts; - std::chrono::system_clock::time_point last_snode_pool_update{}; + std::chrono::system_clock::time_point last_snode_cache_update{}; std::unordered_map> swarm_cache; std::thread disk_write_thread; + // General values bool suspended = false; ConnectionStatus status; oxen::quic::Network net; - std::vector standard_paths; - std::vector upload_paths; - std::vector download_paths; - std::shared_ptr paths_and_pool_loop; - std::shared_ptr endpoint; + std::unordered_map> paths; + + // Resume queues throttling + bool has_scheduled_resume_queues = false; + std::chrono::system_clock::time_point last_resume_queues_timestamp{}; + + // Snode refreshing values + bool refreshing_snode_cache = false; + int snode_cache_refresh_failure_count; + std::vector> after_snode_cache_refresh; + + // Path building values + int general_path_build_failures; + std::vector path_build_queue; + std::vector unused_path_build_nodes; + std::unordered_map> in_progress_path_builds; + + // Pending requests + std::unordered_map>> + request_queue; public: friend class TestNetwork; @@ -224,21 +244,27 @@ class Network { /// Sends a request via onion routing to the provided service node or server destination. /// /// Inputs: + /// - 'type' - [in] the type of paths to send the request across. /// - `destination` -- [in] service node or server destination information. /// - `body` -- [in] data to send to the specified destination. /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. /// Should be NULL if the request is not associated with a swarm. /// - `timeout` -- [in] timeout in milliseconds to use for the request. - /// - `is_retry` -- [in] flag indicating whether this request is a retry. Generally only used - /// for internal purposes for cases which should retry automatically (like receiving a `421`) in - /// order to prevent subsequent retries. /// - `handle_response` -- [in] callback to be called with the result of the request. + void send_onion_request( + PathType type, + onionreq::network_destination destination, + std::optional body, + std::optional swarm_pubkey, + std::chrono::milliseconds timeout, + network_response_callback_t handle_response); void send_onion_request( onionreq::network_destination destination, std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, network_response_callback_t handle_response); + void send_onion_request(request_info info, network_response_callback_t handle_response); /// API: network/upload_file_to_server /// @@ -303,47 +329,20 @@ class Network { network_response_callback_t handle_response); private: - /// API: network/paths_for_type - /// - /// Internal function to retrieve the onion paths for a given request type + /// API: network/all_path_ips /// - /// Inputs: - /// - 'path_type' - [in] the type of paths to retrieve. - std::vector paths_for_type(PathType type) const { - if (single_path_mode) - return standard_paths; - - switch (type) { - case PathType::standard: return standard_paths; - case PathType::upload: return upload_paths; - case PathType::download: return download_paths; - } - return standard_paths; // Default - }; - - /// API: network/all_path_nodes - /// - /// Internal function to retrieve all of the nodes current used in paths - std::vector all_path_nodes() const { - std::vector result; + /// Internal function to retrieve all of the node ips current used in paths + std::vector all_path_ips() const { + std::vector result; - for (auto& paths : {&standard_paths, &upload_paths, &download_paths}) - for (auto& path : *paths) - for (auto& node : path.nodes) - result.emplace_back(node); + for (const auto& [path_type, paths_for_type] : paths) + for (const auto& path : paths_for_type) + for (const auto& node : path.nodes) + result.emplace_back(node.to_ipv4()); return result; }; - /// API: network/update_status - /// - /// Internal function to update the connection status and trigger the `status_changed` hook if - /// provided, this method ignores invalid or unchanged status changes. - /// - /// Inputs: - /// - 'updated_status' - [in] the updated connection status. - void update_status(ConnectionStatus updated_status); - /// API: network/disk_write_thread_loop /// /// Body of the disk writer which runs until signalled to stop. This is intended to run in its @@ -357,125 +356,107 @@ class Network { /// data exists. void load_cache_from_disk(); + /// API: network/update_status + /// + /// Internal function to update the connection status and trigger the `status_changed` hook if + /// provided, this method ignores invalid or unchanged status changes. + /// + /// Inputs: + /// - 'updated_status' - [in] the updated connection status. + void update_status(ConnectionStatus updated_status); + /// API: network/get_endpoint /// /// Retrieves or creates a new endpoint pointer. std::shared_ptr get_endpoint(); - /// API: network/get_connection_info + /// API: network/establish_connection /// - /// Creates a connection for the target node and waits for the connection to be established (or - /// closed in case it fails to establish) before returning the connection. + /// Establishes a connection to the target node and triggers the callback once the connection is + /// established (or closed in case it fails). /// /// Inputs: /// - 'request_id' - [in] id for the request which triggered the call. - /// - 'path_type' - [in] the type of paths to retrieve. + /// - 'path_type' - [in] the type of paths this connection is for. /// - `target` -- [in] the target service node to connect to. - /// - /// Returns: - /// - A pair of the connection info for the target service node and an optional error if the - /// connnection was unable to be established. - std::pair> get_connection_info( - std::string request_id, PathType path_type, service_node target); - - /// API: network/with_paths_and_pool - /// - /// Retrieves the current onion request paths and service node pool from the cache. If the - /// cache is empty it will first be populated from the network. - /// - /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - /// - `path_type` -- [in] the type of path the request should be sent along. - /// - `excluded_node` -- [in, optional] node which should not be included in the path. - /// - `callback` -- [in] callback to be triggered once we have built the paths and service node - /// pool. NOTE: If we are unable to build the paths or retrieve the service node pool the - /// callback will be triggered with empty lists and an error. - void with_paths_and_pool( + /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the + /// `quic::DEFAULT_HANDSHAKE_TIMEOUT` will be used. + /// - `callback` -- [in] callback to be called with connection info once the connection is + /// established or fails. + void establish_connection( std::string request_id, PathType path_type, - std::optional excluded_node, - std::function< - void(std::vector updated_paths, - std::vector pool, - std::optional error)> callback); - - void build_paths_and_pool_in_background(std::string caller, PathType path_type); + service_node target, + std::optional timeout, + std::function error)> callback); - /// API: network/with_path + /// API: network/resume_queues /// - /// Retrieves a valid onion request path to perform a request on. If there aren't currently any - /// paths then new paths will be constructed by opening and testing connections to random - /// service nodes in the snode pool. + /// This function is the backbone of the Network class, it will: + /// - Build/refresh the snode cache + /// - Try to recover connections to paths + /// - Build any queued path builds + /// - Start any queued requests that are now valid /// - /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - /// - `path_type` -- [in] the type of path the request should be sent along. - /// - `excluded_node` -- [in, optional] node which should not be included in the path. - /// - `callback` -- [in] callback to be triggered once we have a valid path, NULL if we are - /// unable to find a valid path. - void with_path( - std::string request_id, - PathType path_type, - std::optional excluded_node, - std::function path, std::optional error)> - callback); + /// When most of these processes finish they call this function again to move through the next + /// step in the process. Note: Due to this "looping" behaviour there is a built in throttling + /// mechanism to avoid running the logic excessively. + void resume_queues(); - /// API: network/valid_paths - /// - /// Filters the provided paths down to a list of valid paths. + /// API: network/refresh_snode_cache /// - /// Inputs: - /// - `paths` -- [in] paths to validate. - /// - /// Outputs: - /// - A list of valid paths. - std::vector valid_paths(std::vector paths); + /// This function refreshes the snode cache. If the current cache is to small (or not present) + /// this will fetch the cache from a random seed node, otherwise it will randomly pick a number + /// of nodes and set the cache to the intersection of the results. + void refresh_snode_cache(); - /// API: network/validate_paths_and_pool_sizes + /// API: network/build_path /// - /// Returns a pair of bools indicating whether the provided paths and pool are valid. + /// Build a new onion request path for the specified type by opening and testing connections to + /// random service nodes in the snode pool. /// /// Inputs: - /// - `path_type` -- [in] the type of path to validate the size for. - /// - `paths` -- [in] paths to validate size for. - /// - `pool` -- [in] pool to validate size for. - /// - `last_pool_update` -- [in] timestamp for when the pool was last updated. - /// - /// Outputs: - /// - A pair of flags indicating whether the provided paths and pool have the correct sizes. - std::pair validate_paths_and_pool_sizes( - PathType path_type, - std::vector paths, - std::vector pool, - std::chrono::system_clock::time_point last_pool_update); + /// - 'existing_request_id' - [in, optional] id for an existing build_path request. Generally + /// this will only be set when retrying a path build. + /// - `path_type` -- [in] the type of path to build. + void build_path(std::optional existing_request_id, PathType path_type); - /// API: network/validate_response + /// API: network/recover_path /// - /// Processes a quic response to extract the status code and body or throw if it errored or - /// received a non-successful status code. + /// Attempt to "recover" an existing onion request path. This will attempt to establish a new + /// connection to the guard node of the path, if unable to establish a new connection the path + /// will be dropped an a new path build will be enqueued. /// /// Inputs: - /// - `resp` -- [in] the quic response. - /// - `is_bencoded` -- [in] flag indicating whether the response will be bencoded or JSON. - /// - /// Returns: - /// - `std::pair` -- the status code and response body (for a bencoded - /// response this is just the direct response body from quic as it simplifies consuming the - /// response elsewhere). - std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + /// - `path_type` -- [in] the type for the provided path. + /// - 'path' - [in] the path to try to reconnect to. + void recover_path(PathType path_type, onion_path path); - /// API: network/find_possible_path + /// API: network/find_valid_path /// - /// Picks a random path from the provided paths excluding the provided node if one is available. + /// Find a random path from the provided paths which is valid for the provided request. Note: + /// if the Network is setup in `single_path_mode` then the path returned may include the + /// destination for the request. /// /// Inputs: - /// - `excluded_node` -- [in, optional] node which should not be included in the paths. + /// - `info` -- [in] request to select a path for. /// - `paths` -- [in] paths to select from. /// /// Outputs: - /// - The possible path, if found, and the number of paths provided. - std::pair, uint8_t> find_possible_path( - std::optional excluded_node, std::vector paths); + /// - The possible path, if found. + std::optional find_valid_path(request_info info, std::vector paths); + + /// API: network/enqueue_path_build_if_needed + /// + /// Adds a path build to the path build queue for the specified type if the total current or + /// pending paths is below the minimum threshold for the given type. Note: This may result in + /// more paths than the minimum threshold being built but not allowing that behaviour could + /// result in a request that never gets sent due to it's destination being present in the + /// existing path(s) for the type. + /// + /// Inputs: + /// - `path_type` -- [in] the type of path to be built. + void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable); /// API: network/get_service_nodes_recursive /// @@ -497,30 +478,6 @@ class Network { std::function nodes, std::optional error)> callback); - /// API: network/find_valid_guard_node_recursive - /// - /// A recursive function that sends a request to provided nodes until a successful response is - /// received or the list is drained. - /// - /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - /// - `path_type` -- [in] the type of path to validate the size for. - /// - `test_attempt` -- [in] the number of test which have occurred before this one. - /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's - /// drained. - /// - `callback` -- [in] callback to be triggered once we make a successful request. NOTE: If - /// we drain the `target_nodes` and haven't gotten a successful response then the callback will - /// be invoked with a std::nullopt `valid_guard_node` and `unused_nodes`. - void find_valid_guard_node_recursive( - std::string request_id, - PathType path_type, - int64_t test_attempt, - std::vector target_nodes, - std::function< - void(std::optional valid_guard_node, - std::vector unused_nodes, - std::optional)> callback); - /// API: network/get_service_nodes /// /// Retrieves all or a random subset of service nodes from the given node. @@ -538,7 +495,7 @@ class Network { std::function nodes, std::optional error)> callback); - /// API: network/get_version + /// API: network/get_snode_version /// /// Retrieves the version information for a given service node. /// @@ -550,7 +507,7 @@ class Network { /// `quic::DEFAULT_TIMEOUT` will be used. /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an /// error occurs an empty list and an error will be provided. - void get_version( + void get_snode_version( std::string request_id, PathType path_type, service_node node, @@ -560,33 +517,6 @@ class Network { connection_info info, std::optional error)> callback); - /// API: network/send_onion_request - /// - /// Sends a request via onion routing to the provided service node or server destination. - /// - /// Inputs: - /// - 'type' - [in] the type of paths to send the request across. - /// - `destination` -- [in] service node or server destination information. - /// - `body` -- [in] data to send to the specified destination. - /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. - /// Should be NULL if the request is not associated with a swarm. - /// - `timeout` -- [in] timeout in milliseconds to use for the request. - /// - `existing_request_id` -- [in] the request id for the existing request when this request is - /// a retry. Mostly to simplify debugging. - /// - `retry_reason` -- [in] the reason we are retrying the request (if it's a retry). Generally - /// only used for internal purposes (like - // receiving a `421`) in order to prevent subsequent retries. - /// - `handle_response` -- [in] callback to be called with the result of the request. - void send_onion_request( - PathType type, - onionreq::network_destination destination, - std::optional body, - std::optional swarm_pubkey, - std::chrono::milliseconds timeout, - std::optional existing_request_id, - std::optional retry_reason, - network_response_callback_t handle_response); - /// API: network/process_v3_onion_response /// /// Processes a v3 onion request response. @@ -613,6 +543,21 @@ class Network { std::pair> process_v4_onion_response( session::onionreq::Builder builder, std::string response); + /// API: network/validate_response + /// + /// Processes a quic response to extract the status code and body or throw if it errored or + /// received a non-successful status code. + /// + /// Inputs: + /// - `resp` -- [in] the quic response. + /// - `is_bencoded` -- [in] flag indicating whether the response will be bencoded or JSON. + /// + /// Returns: + /// - `std::pair` -- the status code and response body (for a bencoded + /// response this is just the direct response body from quic as it simplifies consuming the + /// response elsewhere). + std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + /// API: network/handle_errors /// /// Processes a non-success response to automatically perform any standard operations based on @@ -621,7 +566,7 @@ class Network { /// /// Inputs: /// - `info` -- [in] the information for the request that was made. - /// - `path` -- [in] the onion path the request was sent along. + /// - `conn_info` -- [in] the connection info for the request that failed. /// - `timeout` -- [in, optional] flag indicating whether the request timed out. /// - `status_code` -- [in, optional] the status code returned from the network. /// - `response` -- [in, optional] response data returned from the network. @@ -629,7 +574,7 @@ class Network { /// information after processing the error. void handle_errors( request_info info, - onion_path path, + connection_info conn_info, bool timeout, std::optional status_code, std::optional response, @@ -645,8 +590,14 @@ class Network { /// Inputs: /// - `node` -- [in] the node to increment the failure count for. /// - `path_type` -- [in] type of path the node (or provided path) belong to. - /// - `path` -- [in] path to increment the failure count for. - void handle_node_error(service_node node, PathType path_type, onion_path path); + /// - `conn_info` -- [in] the connection info for the request that failed. + /// - `request_id` -- [in] the request id for the original request which resulted in a node + /// error. + void handle_node_error( + service_node node, + PathType path_type, + connection_info conn_info, + std::string request_id); }; } // namespace session::network diff --git a/src/network.cpp b/src/network.cpp index a1a35147..53210ff9 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -51,14 +51,26 @@ namespace { std::runtime_error(message), status_code{status_code} {} }; + constexpr int16_t error_network_suspended = -10001; + constexpr int16_t error_building_onion_request = -10002; + + // The minimum time which should pass between `resume_queues` executions + constexpr auto resume_queues_throttle_duration = 100ms; + // The amount of time the snode cache can be used before it needs to be refreshed constexpr auto snode_cache_expiration_duration = 2h; // The amount of time a swarm cache can be used before it needs to be refreshed constexpr auto swarm_cache_expiration_duration = (24h * 7); - // The smallest size the snode pool can get to before we need to fetch more. - constexpr uint16_t min_snode_pool_count = 12; + // The smallest size the snode cache can get to before we need to fetch more. + constexpr size_t min_snode_cache_count = 12; + + // The number of snodes to use to refresh the cache. + constexpr size_t num_snodes_to_refresh_cache_from = 3; + + // The number of times to retry refreshing the cache from each snode. + constexpr size_t snode_cache_refresh_retries = 3; // The smallest size a swarm can get to before we need to fetch it again. constexpr uint16_t min_swarm_snode_count = 3; @@ -93,8 +105,8 @@ namespace { return "standard"; // Default } - // The number of paths we want to maintain. - uint8_t target_path_count(PathType path_type, bool single_path_mode) { + // The mininum number of paths we want to maintain + uint8_t min_path_count(PathType path_type, bool single_path_mode) { if (single_path_mode) return 1; @@ -253,6 +265,12 @@ namespace { "Unable to find entry in dict for key '" + std::string(key) + "'"}; return dict.next_integer().second; } + + std::chrono::milliseconds retry_delay( + int num_failures, std::chrono::milliseconds max_delay_ms = 3000ms) { + return std::chrono::milliseconds(std::min( + max_delay_ms.count(), static_cast(100 * std::pow(2, num_failures)))); + } } // namespace // MARK: Initialization @@ -266,8 +284,6 @@ Network::Network( should_cache_to_disk{cache_path}, single_path_mode{single_path_mode}, cache_path{cache_path.value_or(default_cache_path)} { - paths_and_pool_loop = std::make_shared(); - // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { load_cache_from_disk(); @@ -275,8 +291,12 @@ Network::Network( } // Kick off a separate thread to build paths (may as well kick this off early) - if (pre_build_paths) - build_paths_and_pool_in_background("Constructor", PathType::standard); + if (pre_build_paths) { + for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) + path_build_queue.emplace_back(PathType::standard); + + net.call_soon([this] { resume_queues(); }); + } } Network::~Network() { @@ -323,7 +343,7 @@ void Network::load_cache_from_disk() { if (!quic::parse_int(timestamp_str, timestamp)) throw std::runtime_error{"invalid file data: expected timestamp first line"}; - last_snode_pool_update = std::chrono::system_clock::from_time_t(timestamp); + last_snode_cache_update = std::chrono::system_clock::from_time_t(timestamp); } catch (const std::exception& e) { log::error(cat, "Ignoring invalid last update timestamp file: {}", e.what()); } @@ -333,13 +353,13 @@ void Network::load_cache_from_disk() { auto pool_path = cache_path / file_snode_pool; if (fs::exists(pool_path)) { auto file = open_for_reading(pool_path); - std::vector loaded_pool; + std::vector loaded_cache; std::string line; auto invalid_entries = 0; while (std::getline(file, line)) { try { - loaded_pool.push_back(node_from_disk(line)); + loaded_cache.push_back(node_from_disk(line)); } catch (...) { ++invalid_entries; } @@ -349,7 +369,7 @@ void Network::load_cache_from_disk() { log::warning( cat, "Skipped {} invalid entries in snode pool cache.", invalid_entries); - snode_pool = loaded_pool; + snode_cache = loaded_cache; } // Load the failure counts @@ -459,7 +479,7 @@ void Network::load_cache_from_disk() { log::info( cat, "Loaded cache of {} snodes, {} swarms ({} expired swarms).", - snode_pool.size(), + snode_cache.size(), swarm_cache.size(), caches_to_remove.size()); } catch (const std::exception& e) { @@ -478,9 +498,9 @@ void Network::disk_write_thread_loop() { if (need_write) { // Make local copies so that we can release the lock and not // worry about other threads wanting to change things: - auto snode_pool_write = snode_pool; + auto snode_cache_write = snode_cache; auto snode_failure_counts_write = snode_failure_counts; - auto last_pool_update_write = last_snode_pool_update; + auto last_pool_update_write = last_snode_cache_update; auto swarm_cache_write = swarm_cache; lock.unlock(); @@ -498,7 +518,7 @@ void Network::disk_write_thread_loop() { { std::stringstream ss; - for (auto& snode : snode_pool_write) + for (auto& snode : snode_cache_write) ss << node_to_disk(snode) << '\n'; std::ofstream file(pool_tmp, std::ios::binary); @@ -551,6 +571,8 @@ void Network::disk_write_thread_loop() { for (auto& snode : swarm) ss << node_to_disk(snode) << '\n'; + // FIXME: In the future we should store the swarm info in the encrypted + // database instead of a plaintext file std::ofstream swarm_file(swarm_tmp, std::ios::binary); swarm_file << ss.rdbuf(); @@ -570,8 +592,8 @@ void Network::disk_write_thread_loop() { lock.lock(); } if (need_clear_cache) { - snode_pool = {}; - last_snode_pool_update = {}; + snode_cache = {}; + last_snode_cache_update = {}; snode_failure_counts = {}; swarm_cache = {}; @@ -586,6 +608,18 @@ void Network::disk_write_thread_loop() { } } +void Network::clear_cache() { + net.call([this]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + need_clear_cache = true; + } + snode_cache_cv.notify_one(); + }); +} + +// MARK: Connection + void Network::suspend() { net.call([this]() mutable { suspended = true; @@ -607,8 +641,8 @@ void Network::close_connections() { endpoint.reset(); // Explicitly reset the connection and stream (just in case) - for (auto& paths : {&standard_paths, &upload_paths, &download_paths}) { - for (auto& path : *paths) { + for (auto& [type, paths_for_type] : paths) { + for (auto& path : paths_for_type) { path.conn_info.conn.reset(); path.conn_info.stream.reset(); } @@ -619,18 +653,6 @@ void Network::close_connections() { }); } -void Network::clear_cache() { - net.call([this]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - need_clear_cache = true; - } - snode_cache_cv.notify_one(); - }); -} - -// MARK: Connection - void Network::update_status(ConnectionStatus updated_status) { // Ignore updates which don't change the status if (status == updated_status) @@ -659,875 +681,794 @@ std::shared_ptr Network::get_endpoint() { }); } -std::pair> Network::get_connection_info( - std::string request_id, PathType path_type, service_node target) { +void Network::establish_connection( + std::string request_id, + PathType path_type, + service_node target, + std::optional timeout, + std::function error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); // If the network is currently suspended then don't try to open a connection if (currently_suspended) - return {{target, nullptr, nullptr}, "Network is suspended."}; + return callback({target, nullptr, nullptr}, "Network is suspended."); + auto conn_key_pair = ed25519::ed25519_key_pair(); + auto creds = quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(conn_key_pair.second)); auto cb_called = std::make_shared(); - auto mutex = std::make_shared(); - auto cv = std::make_shared(); - auto connection_established = std::make_shared(false); - auto done = std::make_shared(false); - auto connection_key_pair = ed25519::ed25519_key_pair(); - auto creds = - quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(connection_key_pair.second)); + auto cb = std::make_shared)>>( + std::move(callback)); + auto conn_promise = std::promise>(); + auto conn_future = conn_promise.get_future().share(); + auto handshake_timeout = + timeout ? std::optional{quic::opt::handshake_timeout{ + std::chrono::duration_cast(*timeout)}} + : std::nullopt; auto c = get_endpoint()->connect( target, creds, quic::opt::keep_alive{10s}, - [mutex, cv, connection_established, done, request_id, cb_called]( - quic::connection_interface&) { - log::trace( - cat, "{} connection established for {}.", __PRETTY_FUNCTION__, request_id); + handshake_timeout, + [this, request_id, target, cb, cb_called, conn_future]( + quic::connection_interface&) mutable { + log::trace(cat, "Connection established for {}.", request_id); - if (cb_called) + // Just in case, call it within a `net.call` + net.call([&] { std::call_once(*cb_called, [&]() { - { - std::lock_guard lock(*mutex); - *connection_established = true; - *done = true; + if (cb) { + auto conn = conn_future.get(); + (*cb)({target, conn, conn->open_stream()}, + std::nullopt); + cb.reset(); } - cv->notify_one(); }); + }); }, - [this, path_type, target, mutex, cv, done, request_id, cb_called]( - quic::connection_interface& conn, uint64_t error_code) { - log::trace(cat, "{} connection closed for {}.", __PRETTY_FUNCTION__, request_id); - - // Trigger the callback first before updating the paths in case this was triggered - // when try to establish a connection - if (cb_called) { + [this, path_type, target, request_id, cb, cb_called, conn_future]( + quic::connection_interface& conn, uint64_t error_code) mutable { + log::trace(cat, "Connection closed for {}.", request_id); + + // Just in case, call it within a `net.call` + net.call([&] { + // Trigger the callback first before updating the paths in case this was + // triggered when try to establish a connection std::call_once(*cb_called, [&]() { - { - std::lock_guard lock(*mutex); - *done = true; + if (cb) { + (*cb)({target, nullptr, nullptr}, std::nullopt); + cb.reset(); } - cv->notify_one(); }); - } - // When the connection is closed, update the path and connection status - auto current_paths = net.call_get([this, path_type]() -> std::vector { - return paths_for_type(path_type); - }); - auto target_path = std::find_if( - current_paths.begin(), current_paths.end(), [&target](const auto& path) { - return !path.nodes.empty() && target == path.nodes.front(); - }); + // When the connection is closed we update the path and reset it's connection + // info so we can recover the path later if desired + auto conn_info = conn_future.get(); + auto current_paths = paths[path_type]; + auto target_path = std::find_if( + current_paths.begin(), + current_paths.end(), + [&target](const auto& path) { + return !path.nodes.empty() && target == path.nodes.front(); + }); - if (target_path != current_paths.end() && target_path->conn_info.conn && - conn.reference_id() == target_path->conn_info.conn->reference_id()) { - target_path->conn_info.conn.reset(); - target_path->conn_info.stream.reset(); - handle_node_error(target, path_type, *target_path); - } else if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) - // Depending on the state of the snode pool cache it's possible for certain - // errors to result in being permanently unable to establish a connection, to - // avoid this we handle those error codes and drop - handle_node_error(target, path_type, {{target, nullptr, nullptr}, {target}, 0}); + if (target_path != current_paths.end() && target_path->conn_info.conn && + conn.reference_id() == target_path->conn_info.conn->reference_id()) { + target_path->conn_info.conn.reset(); + target_path->conn_info.stream.reset(); + + handle_node_error(target, path_type, target_path->conn_info, request_id); + } else if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) + // Depending on the state of the snode pool cache it's possible for certain + // errors to result in being permanently unable to establish a connection, + // to avoid this we handle those error codes and drop + handle_node_error( + target, path_type, {target, nullptr, nullptr}, request_id); + }); }); - if (!*done) { - std::unique_lock lock(*mutex); - cv->wait(lock, [&done] { return *done; }); - } + conn_promise.set_value(c); +} - if (!*connection_established) - return {{target, nullptr, nullptr}, "Network is unreachable."}; +// MARK: Request Queues and Path Building - return {{target, c, c->open_stream()}, std::nullopt}; -} +void Network::resume_queues() { + if (suspended) { + log::info(cat, "Ignoring resume queues as network is suspended."); -// MARK: Snode Pool and Onion Path + // If we have any requests in the queue then we should trigger their callbacks + for (auto& [path_type, requests] : request_queue) + for (auto& [info, callback] : requests) + callback(false, false, error_network_suspended, "Network is suspended."); -using paths_and_pool_result = - std::tuple, std::vector, std::optional>; -using paths_and_pool_info = std::tuple< - std::vector, - std::vector, - std::unordered_map>, - std::chrono::system_clock::time_point, - bool>; + // Clear the map after processing all callbacks + request_queue.clear(); + return; + } -void Network::with_paths_and_pool( - std::string request_id, - PathType path_type, - std::optional excluded_node, - std::function< - void(std::vector updated_paths, - std::vector pool, - std::optional error)> callback) { - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - auto [current_paths, pool, current_swarms, last_pool_update, currently_suspended] = - net.call_get([this, path_type]() -> paths_and_pool_info { - return {paths_for_type(path_type), - snode_pool, - swarm_cache, - last_snode_pool_update, - suspended}; + // Throttle this function as it can get called very frequently and running the logic excessively + // could cause battery drain + auto time_since_last_run = std::chrono::duration_cast( + std::chrono::system_clock::now() - last_resume_queues_timestamp); + if (time_since_last_run < resume_queues_throttle_duration) { + if (!has_scheduled_resume_queues) { + has_scheduled_resume_queues = true; + + auto delay = (resume_queues_throttle_duration - time_since_last_run); + net.call_later(delay, [this]() { + has_scheduled_resume_queues = false; + resume_queues(); }); + } - // If the network is currently suspended then fail immediately - if (currently_suspended) - return callback({}, {}, "Network is suspended"); - - // Check if the current data is valid, and if so just return it - auto current_valid_paths = valid_paths(current_paths); - auto [paths_valid, pool_valid] = - validate_paths_and_pool_sizes(path_type, current_valid_paths, pool, last_pool_update); + return; + } + last_resume_queues_timestamp = std::chrono::system_clock::now(); + + // Determine the number of existing paths + std::vector existing_path_type_names; + for (const auto& [path_type, paths_for_type] : paths) + existing_path_type_names.insert( + existing_path_type_names.end(), + paths_for_type.size(), + path_type_name(path_type, single_path_mode)); + + // Only generate the stats if we are actually going to log them + if (log::get_level(cat) == log::Level::trace) { + auto request_count = 0; + std::vector pending_path_type_names; + std::vector in_progress_path_type_names; + std::transform( + path_build_queue.begin(), + path_build_queue.end(), + std::back_inserter(pending_path_type_names), + [this](const PathType& type) { return path_type_name(type, single_path_mode); }); + std::transform( + in_progress_path_builds.begin(), + in_progress_path_builds.end(), + std::back_inserter(in_progress_path_type_names), + [this](const auto& pending_build) { + return path_type_name(pending_build.second.first, single_path_mode); + }); + auto format_names = [](std::vector names) { + return (names.empty() ? "0" : "{} ({})"_format(names.size(), fmt::join(names, ", "))); + }; + for (const auto& [type, requests] : request_queue) + request_count += requests.size(); - if (paths_valid && pool_valid) { log::trace( cat, - "{} returning valid cached paths and pool for {}.", - __PRETTY_FUNCTION__, - request_id); - return callback(current_valid_paths, pool, std::nullopt); + "Resuming queues snodes: {}, paths: {}, path_builds: " + "{}, in_progress_path_builds: {}, requests: {}.", + snode_cache.size(), + format_names(existing_path_type_names), + format_names(pending_path_type_names), + format_names(in_progress_path_type_names), + request_count); } - auto [updated_paths, updated_pool, error] = paths_and_pool_loop->call_get( - [this, path_type, request_id, excluded_node]() mutable -> paths_and_pool_result { - auto [current_paths, pool, current_swarms, last_pool_update, currently_suspended] = - net.call_get([this, path_type]() -> paths_and_pool_info { - return {paths_for_type(path_type), - snode_pool, - swarm_cache, - last_snode_pool_update, - suspended}; - }); + // If we haven't set a connection status yet then do so now + if (status == ConnectionStatus::unknown) + update_status(ConnectionStatus::connecting); + + // If the snode cache is too small then we need to update it before we try to build any paths + if (snode_cache.size() < min_snode_cache_count) { + net.call_soon([this]() { refresh_snode_cache(); }); + return; + } - // If the network is currently suspended then fail immediately - if (currently_suspended) - return {{}, {}, "Network is suspended"}; + // Otherwise check if it's been too long since the last update and, if so, trigger a refresh + auto cache_lifetime = std::chrono::duration_cast( + std::chrono::system_clock::now() - last_snode_cache_update); - // Check if the current data is valid, and if so just return it - auto current_valid_paths = valid_paths(current_paths); - auto [paths_valid, pool_valid] = validate_paths_and_pool_sizes( - path_type, current_valid_paths, pool, last_pool_update); + if (cache_lifetime < 0s || cache_lifetime > snode_cache_expiration_duration) + net.call_soon([this]() { refresh_snode_cache(); }); - if (paths_valid && pool_valid) { - log::trace( - cat, - "{} whithin loop cache has already been updated for {}.", - __PRETTY_FUNCTION__, - request_id); - return {current_valid_paths, pool, std::nullopt}; - } + // Schedule any path builds (or recoveries) + std::unordered_map num_pending_paths; - // Update the network status - if (path_type == PathType::standard) - net.call([this]() mutable { update_status(ConnectionStatus::connecting); }); + for (const auto& path_type : path_build_queue) { + num_pending_paths[path_type]++; - // If the pool isn't valid then we should update it - CSRNG rng; - auto swarms_updated = false; - std::vector pool_result = pool; - std::vector paths_result = current_valid_paths; - std::unordered_map> swarm_result = - current_swarms; - - // Populate the snode pool if needed - if (!pool_valid) { - log::info( - cat, - "Snode pool cache no longer valid for {}, need to refetch.", - request_id); + // If we have an existing path that is not valid then we should try to recover that instead + // of trying to build a new path (after triggering the recovery we remove it from + // `existing_paths` to ensure we don't try to recover it multiple times) + auto& existing_paths = paths[path_type]; + if (!existing_paths.empty()) { + auto it = std::find_if( + existing_paths.begin(), existing_paths.end(), [](const onion_path& path) { + return !path.is_valid(); + }); - // Define the response handler to avoid code duplication - auto handle_nodes_response = - [](std::promise>&& prom) { - return [&prom](std::vector nodes, - std::optional error) { - try { - if (nodes.empty()) - throw std::runtime_error{ - error.value_or("No nodes received.")}; - prom.set_value(nodes); - } catch (...) { - prom.set_exception(std::current_exception()); - } - }; - }; + if (it != existing_paths.end()) { + recover_path(path_type, *it); + existing_paths.erase(it); + continue; + } + } - try { - // If we don't have enough nodes in the current cached pool then we need to - // fetch from the seed nodes - if (pool_result.size() < min_snode_pool_count) { - log::info(cat, "Fetching from seed nodes for {}.", request_id); - pool_result = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); - - // Just in case, make sure the seed nodes are have values - if (pool_result.empty()) - throw std::runtime_error{"Insufficient seed nodes."}; - - std::shuffle(pool_result.begin(), pool_result.end(), rng); - std::promise> prom; - std::future> prom_future = prom.get_future(); - - get_service_nodes( - request_id, - pool_result.front(), - std::nullopt, - handle_nodes_response(std::move(prom))); - - // We want to block the `get_snode_pool_loop` until we have retrieved - // the snode pool so we don't double up on requests - pool_result = prom_future.get(); - log::info( - cat, "Retrieved snode pool from seed node for {}.", request_id); - } else { - // Pick ~9 random snodes from the current cache to fetch nodes from (we - // want to fetch from 3 snodes and retry up to 3 times if needed) - std::shuffle(pool_result.begin(), pool_result.end(), rng); - size_t num_retries = - std::min(pool_result.size() / 3, static_cast(3)); - - log::info( - cat, - "Fetching from random expired cache nodes for {}.", - request_id); - std::vector nodes1( - pool_result.begin(), pool_result.begin() + num_retries); - std::vector nodes2( - pool_result.begin() + num_retries, - pool_result.begin() + (num_retries * 2)); - std::vector nodes3( - pool_result.begin() + (num_retries * 2), - pool_result.begin() + (num_retries * 3)); - std::promise> prom1; - std::promise> prom2; - std::promise> prom3; - std::future> prom_future1 = - prom1.get_future(); - std::future> prom_future2 = - prom2.get_future(); - std::future> prom_future3 = - prom3.get_future(); - - // Kick off 3 concurrent requests - get_service_nodes_recursive( - "{}-1"_format(request_id), - nodes1, - std::nullopt, - handle_nodes_response(std::move(prom1))); - get_service_nodes_recursive( - "{}-2"_format(request_id), - nodes2, - std::nullopt, - handle_nodes_response(std::move(prom2))); - get_service_nodes_recursive( - "{}-3"_format(request_id), - nodes3, - std::nullopt, - handle_nodes_response(std::move(prom3))); - - // We want to block the `get_snode_pool_loop` until we have retrieved - // the snode pool so we don't double up on requests - auto result_nodes1 = prom_future1.get(); - auto result_nodes2 = prom_future2.get(); - auto result_nodes3 = prom_future3.get(); - - // Sort the vectors (so make it easier to find the - // intersection) - std::stable_sort(result_nodes1.begin(), result_nodes1.end()); - std::stable_sort(result_nodes2.begin(), result_nodes2.end()); - std::stable_sort(result_nodes3.begin(), result_nodes3.end()); - - // Get the intersection of the vectors - std::vector intersection1_2; - std::vector intersection; - - std::set_intersection( - result_nodes1.begin(), - result_nodes1.end(), - result_nodes2.begin(), - result_nodes2.end(), - std::back_inserter(intersection1_2), - [](const auto& a, const auto& b) { return a == b; }); - std::set_intersection( - intersection1_2.begin(), - intersection1_2.end(), - result_nodes3.begin(), - result_nodes3.end(), - std::back_inserter(intersection), - [](const auto& a, const auto& b) { return a == b; }); - - // Since we sorted it we now need to shuffle it again - std::shuffle(intersection.begin(), intersection.end(), rng); - - // Update the cache to be the intersection - pool_result = intersection; - log::info(cat, "Retrieved snode pool for {}.", request_id); - } + // Otherwise we need to build a new path + build_path(std::nullopt, path_type); + } - // Since the snode pool has been updated, and we now fetch the entire list, - // we should check the current_swarms and remove any nodes which aren't - // present (as they are potentially decomissioned) - for (auto& [key, nodes] : swarm_result) { - nodes.erase( - std::remove_if( - nodes.begin(), - nodes.end(), - [&](service_node& node) { - auto find_it = std::find_if( - pool_result.begin(), - pool_result.end(), - [&](const service_node& pool_node) { - return static_cast< - const oxen::quic:: - RemoteAddress&>( - node) == - static_cast< - const oxen::quic:: - RemoteAddress&>( - pool_node) && - node.storage_server_version == - pool_node - .storage_server_version; - }); - - if (find_it != pool_result.end()) { - // Node found in pool_result, update it - bool version_changed = - node.storage_server_version != - find_it->storage_server_version; - node = *find_it; - swarms_updated |= version_changed; - return false; // Keep this node - } else { - swarms_updated = true; - return true; // Remove this node - } - }), - nodes.end()); - } - } catch (const std::exception& e) { - log::info(cat, "Failed to get snode pool for {}: {}", request_id, e.what()); - return {{}, {}, e.what()}; + // Now that we've triggered all request path builds/recoveries we can clear the path_build_queue + path_build_queue.clear(); + + // If there are left over invalid paths in `paths` and we have more than the minimum number of + // required paths (including the builds/recoveries started above) then we can just drop the + // extra invalid paths + for (auto& [path_type, existing_paths] : paths) { + size_t pending_count = num_pending_paths[path_type]; + size_t total_count = (existing_paths.size() + pending_count); + size_t target_count = min_path_count(path_type, single_path_mode); + + // If we don't have enough paths then do nothing + if (total_count <= target_count) + continue; + + std::vector valid_paths; + std::vector invalid_paths; + valid_paths.reserve(existing_paths.size()); + invalid_paths.reserve(existing_paths.size()); + + for (auto& path : existing_paths) { + if (path.is_valid()) + valid_paths.emplace_back(path); + else + invalid_paths.emplace_back(path); + } + + // Keep all valid paths and only enough invalid paths to meet the minimum requirements + size_t remaining_slots = + std::max(target_count - valid_paths.size(), static_cast(0)); + existing_paths = std::move(valid_paths); + existing_paths.insert( + existing_paths.end(), + invalid_paths.begin(), + invalid_paths.begin() + std::min(remaining_slots, invalid_paths.size())); + } + + // Now that we've scheduled any required path builds we should try to resume any pending + // requests in case they now have a valid paths + if (existing_path_type_names.size() > 0) { + std::unordered_set already_enqueued_paths; + + for (auto& [path_type, requests] : request_queue) + std::erase_if(requests, [this, &already_enqueued_paths](const auto& request) { + // If there are no valid paths to send the request then enqueue a new path build (if + // needed) leave the request in the queue + if (!find_valid_path(request.first, paths[request.first.path_type])) { + if (!already_enqueued_paths.contains(request.first.path_type)) { + already_enqueued_paths.insert(request.first.path_type); + enqueue_path_build_if_needed(request.first.path_type, true); } + net.call_soon([this]() { resume_queues(); }); + return false; } - // Build new paths if needed - if (!paths_valid) { - try { - // Get the possible guard nodes - log::info( - cat, - "Building paths of type {} for {}.", - path_type_name(path_type, single_path_mode), - request_id); - std::vector nodes_to_exclude; - std::vector possible_guard_nodes; - - if (excluded_node) - nodes_to_exclude.push_back(*excluded_node); - - for (auto& path : paths_result) - nodes_to_exclude.insert( - nodes_to_exclude.end(), path.nodes.begin(), path.nodes.end()); - - if (nodes_to_exclude.empty()) - possible_guard_nodes = pool_result; - else - std::copy_if( - pool_result.begin(), - pool_result.end(), - std::back_inserter(possible_guard_nodes), - [&nodes_to_exclude](const auto& node) { - return std::find( - nodes_to_exclude.begin(), - nodes_to_exclude.end(), - node) == nodes_to_exclude.end(); - }); - - if (possible_guard_nodes.empty()) - throw std::runtime_error{ - "Unable to build paths due to lack of possible guard nodes."}; - - // Now that we have a list of possible guard nodes we need to build the - // paths, first off we need to find valid guard nodes for the paths - std::shuffle(possible_guard_nodes.begin(), possible_guard_nodes.end(), rng); - - // Split the possible nodes list into a list of lists (one list could run - // out before the other but in most cases this should work fine) - size_t required_paths = - (target_path_count(path_type, single_path_mode) - - current_valid_paths.size()); - size_t chunk_size = (possible_guard_nodes.size() / required_paths); - std::vector> nodes_to_test; - auto start = 0; - - for (size_t i = 0; i < required_paths; ++i) { - auto end = std::min(start + chunk_size, possible_guard_nodes.size()); - - if (i == required_paths - 1) - end = possible_guard_nodes.size(); - - nodes_to_test.emplace_back( - possible_guard_nodes.begin() + start, - possible_guard_nodes.begin() + end); - start = end; - } + net.call_soon([this, info = request.first, cb = std::move(request.second)]() { + send_onion_request(info, cb); + }); + return true; + }); + } +} - // Start testing guard nodes based on the number of paths we want to build - std::vector< - std::future>>> - futures; - futures.reserve(required_paths); - - for (size_t i = 0; i < required_paths; ++i) { - std::promise>> - guard_node_prom; - futures.emplace_back(guard_node_prom.get_future()); - - auto prom = std::make_shared>>>( - std::move(guard_node_prom)); - - find_valid_guard_node_recursive( - request_id, - path_type, - 0, - nodes_to_test[i], - [prom](std::optional valid_guard_node, - std::vector unused_nodes, - std::optional error) { - try { - if (!valid_guard_node) - throw std::runtime_error{ - error.value_or("Failed to find valid guard " - "node.")}; - prom->set_value({*valid_guard_node, unused_nodes}); - } catch (...) { - prom->set_exception(std::current_exception()); - } - }); - } +void Network::refresh_snode_cache() { + if (suspended) { + log::info(cat, "Ignoring snode cache refresh as network is suspended."); + return; + } - // Combine the results (we want to block the `paths_and_pool_loop` until we - // have retrieved the valid guard nodes so we don't double up on requests - std::vector valid_nodes; - std::vector unused_nodes; - - for (auto& fut : futures) { - auto result = fut.get(); - valid_nodes.emplace_back(result.first); - unused_nodes.insert( - unused_nodes.begin(), - result.second.begin(), - result.second.end()); - } + // Only allow a single cache refresh at a time + if (refreshing_snode_cache) { + log::info(cat, "Snode cache refresh ignored due to in progress refresh."); + return; + } - // Make sure we ended up getting enough valid nodes - auto have_enough_guard_nodes = - (current_valid_paths.size() + valid_nodes.size() >= - target_path_count(path_type, single_path_mode)); - auto have_enough_unused_nodes = - (unused_nodes.size() >= - ((path_size - 1) * - target_path_count(path_type, single_path_mode))); - - if (!have_enough_guard_nodes || !have_enough_unused_nodes) - throw std::runtime_error{"Not enough remaining nodes."}; - - // Build the new paths - std::vector min_final_node_version = parse_version("2.8.0"); - - for (auto& info : valid_nodes) { - std::vector path{info.node}; - - while (path.size() < path_size) { - if (unused_nodes.empty()) - throw std::runtime_error{"Not enough remaining nodes."}; - - // Default to using the last unused node - auto node = unused_nodes.back(); - auto using_last_unused_node = true; - - // If this is the final node then we only want to consider nodes - // which are at least the 'min_final_node_version' - // - // Note: This should be able to be removed after the mandatory - // service node update period for 2.8.0 finishes - if (path.size() == path_size - 1) { - auto it = std::find_if( - unused_nodes.begin(), - unused_nodes.end(), - [min_final_node_version](const service_node& node) { - return node.storage_server_version >= - min_final_node_version; - }); - - if (it != unused_nodes.end()) { - node = *it; - unused_nodes.erase(it); - using_last_unused_node = false; - } else { - // If we couldn't find a suitable node then just fallback to - // the old logic - log::warning( - cat, - "Unable to find suitable final node when building " - "{} " - "path for {}", - path_type_name(path_type, single_path_mode), - request_id); - } - } + refreshing_snode_cache = true; - // If we are using the last unused node then we need to remove it - // from the vector here - if (using_last_unused_node) - unused_nodes.pop_back(); - - // Ensure we don't put two nodes with the same IP into the same path - auto snode_with_ip_it = std::find_if( - path.begin(), - path.end(), - [&node](const auto& existing_node) { - return existing_node.to_ipv4() == node.to_ipv4(); - }); - - if (snode_with_ip_it == path.end()) - path.push_back(node); - } + // If we don't have enough nodes in the current cached then we need to fetch + // from the seed nodes which is a trusted source so we can update the cache + // from a single response + // + // If the current cache is large enough then we want to getch from a number + // of random nodes and use the intersection of their responses as the update + auto use_seed_node = (snode_cache.size() < min_snode_cache_count); + size_t num_requests = (use_seed_node ? 1 : num_snodes_to_refresh_cache_from); + log::info(cat, "Refreshing snode cache{}.", (use_seed_node ? " from seed nodes" : "")); + + // Define a handler function + auto processing_responses = + [this, use_seed_node, num_requests]( + std::shared_ptr>> all_nodes) { + // There are still pending requests so just wait + if (all_nodes->size() != num_requests) + return; + + auto any_nodes_request_failed = + std::any_of(all_nodes->begin(), all_nodes->end(), [](const auto& n) { + return n.empty(); + }); - paths_result.emplace_back(onion_path{std::move(info), path, 0}); - - // Log that a path was built - std::vector node_descriptions; - std::transform( - path.begin(), - path.end(), - std::back_inserter(node_descriptions), - [](const service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info( - cat, - "Built new onion request path of type {} for {}: [{}]", - path_type_name(path_type, single_path_mode), - request_id, - path_description); - } - } catch (const std::exception& e) { - log::info( + if (any_nodes_request_failed) { + // If the current cache is still usable just send a warning and don't bother + // retrying + if (!use_seed_node) { + log::warning( cat, - "Unable to build paths of type {} for {} due to error: {}", - path_type_name(path_type, single_path_mode), - request_id, - e.what()); - return {{}, {}, e.what()}; + "Failed to refresh snode cache due to request failing to retrieve " + "nodes"); + refreshing_snode_cache = false; + return; } - } - // Store to instance variables - net.call([this, - path_type, - pool_result, - paths_result, - pool_valid, - paths_valid]() mutable { - if (!paths_valid) { - switch (path_type) { - case PathType::standard: standard_paths = paths_result; break; - case PathType::upload: upload_paths = paths_result; break; - case PathType::download: download_paths = paths_result; break; - } + // Otherwise schedule a retry after a short delay + snode_cache_refresh_failure_count++; + refreshing_snode_cache = false; - // Call the paths_changed callback if provided - if (path_type == PathType::standard && paths_changed) { - std::vector> raw_paths; - for (auto& path : paths_result) - raw_paths.emplace_back(path.nodes); + auto cache_refresh_retry_delay = retry_delay(snode_cache_refresh_failure_count); + log::error( + cat, + "Failed to refresh snode cache{} due to request failing to retrieve " + "nodes, will retry after: {}ms", + (use_seed_node ? " from seed nodes" : ""), + cache_refresh_retry_delay.count()); + net.call_later(cache_refresh_retry_delay, [this]() { refresh_snode_cache(); }); + return; + } - paths_changed(raw_paths); - } + // Sort the vectors (so make it easier to find the intersection) + for (auto& nodes : *all_nodes) + std::stable_sort(nodes.begin(), nodes.end()); + + auto nodes = (*all_nodes)[0]; + + // If we triggered multiple requests then get the intersection of all vectors + if (all_nodes->size() > 1) { + for (size_t i = 1; i < all_nodes->size(); ++i) { + std::vector temp; + std::set_intersection( + nodes.begin(), + nodes.end(), + (*all_nodes)[i].begin(), + (*all_nodes)[i].end(), + std::back_inserter(temp), + [](const auto& a, const auto& b) { return a == b; }); + nodes = std::move(temp); } + } - // Only update the disk cache if the snode pool was updated - if (!pool_valid) { - { - std::lock_guard lock{snode_cache_mutex}; - snode_pool = pool_result; - last_snode_pool_update = std::chrono::system_clock::now(); - need_pool_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - } + // Shuffle the nodes so we don't have a specific order + CSRNG rng; + std::shuffle(nodes.begin(), nodes.end(), rng); + + // Update the disk cache if the snode pool was updated + { + std::lock_guard lock{snode_cache_mutex}; + snode_cache = nodes; + last_snode_cache_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + + // Resume any queued processes + log::info(cat, "Refreshing snode cache completed with {} nodes.", nodes.size()); + refreshing_snode_cache = false; + + for (const auto& callback : after_snode_cache_refresh) + net.call_soon([cb = std::move(callback)]() { cb(); }); + after_snode_cache_refresh.clear(); + net.call_soon([this]() { resume_queues(); }); + }; + auto handle_response = + [processing_responses]( + std::shared_ptr>> all_nodes) { + return [all_nodes, processing_responses]( + std::vector nodes, std::optional) { + all_nodes->emplace_back(nodes); + processing_responses(all_nodes); + }; + }; - // Standard paths were successfully built, update the connection status - if (path_type == PathType::standard) - update_status(ConnectionStatus::connected); - }); + std::vector nodes; - return {paths_result, pool_result, std::nullopt}; - }); + if (use_seed_node) + nodes = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); + else + nodes = snode_cache; - return callback(updated_paths, updated_pool, error); -} + // Just in case, make sure we actually have nodes to send requests to + if (nodes.empty()) { + log::error(cat, "Failed to refresh snode cache from seed nodes: Insufficient seed nodes."); + refreshing_snode_cache = false; + return; + } -void Network::build_paths_and_pool_in_background(std::string caller, PathType path_type) { - std::thread build_paths_thread( - &Network::with_paths_and_pool, - this, - caller, - path_type, - std::nullopt, - [](std::vector, std::vector, std::optional) {}); - build_paths_thread.detach(); + // Shuffle to ensure we pick random nodes to fetch from + CSRNG rng; + std::shuffle(nodes.begin(), nodes.end(), rng); + + // Kick off the requests concurrently + // + // It's possible, even likely, that some of the nodes could be unavailable so + // we want to try to cycle through a few nodes if any of the requests fail + // so calculate how many we can cycle through + size_t num_attempts = std::min(nodes.size() / num_requests, snode_cache_refresh_retries); + auto all_nodes = std::make_shared>>(); + all_nodes->reserve(num_requests); + + for (size_t i = 0; i < num_requests; ++i) { + std::vector chunk( + nodes.begin() + (i * num_attempts), nodes.begin() + ((i + 1) * num_attempts)); + + get_service_nodes_recursive( + "Refresh Snode Cache (Node {})"_format(i + 1), + chunk, + std::nullopt, + handle_response(all_nodes)); + } } -std::vector Network::valid_paths(std::vector paths) { - auto valid_paths = paths; - auto valid_paths_end = - std::remove_if(valid_paths.begin(), valid_paths.end(), [](onion_path path) { - return !path.conn_info.is_valid(); - }); - valid_paths.erase(valid_paths_end, valid_paths.end()); +void Network::build_path(std::optional existing_request_id, PathType path_type) { + if (suspended) { + log::info(cat, "Ignoring path build request as network is suspended."); + return; + } - return valid_paths; -} + auto request_id = existing_request_id.value_or(random::random_base32(4)); + auto path_name = path_type_name(path_type, single_path_mode); -std::pair Network::validate_paths_and_pool_sizes( - PathType path_type, - std::vector paths, - std::vector pool, - std::chrono::system_clock::time_point last_pool_update) { - auto cache_duration = std::chrono::duration_cast( - std::chrono::system_clock::now() - last_pool_update); - auto cache_has_expired = - (cache_duration <= 0s && cache_duration > snode_cache_expiration_duration); - - return {(paths.size() >= target_path_count(path_type, single_path_mode)), - (pool.size() >= min_snode_pool_count && !cache_has_expired)}; -} + // Check that we have enough snodes before continuing (shouldn't be an issue but if something + // calls `build_path` in the future then this drive it back into the standard loop) + if (snode_cache.size() < min_snode_cache_count) { + log::warning( + cat, + "Re-queing {} path build due to insufficient nodes ({}).", + path_name, + request_id); + in_progress_path_builds.erase(request_id); + path_build_queue.emplace_back(path_type); + net.call_soon([this]() { resume_queues(); }); + return; + } -void Network::with_path( - std::string request_id, - PathType path_type, - std::optional excluded_node, - std::function path, std::optional error)> - callback) { - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - auto [current_paths, currently_suspended] = - net.call_get([this, path_type]() -> std::pair, bool> { - return {paths_for_type(path_type), suspended}; - }); + CSRNG rng; + log::info(cat, "Building {} path ({}).", path_name, request_id); - // If the network is currently suspended then fail immediately - if (currently_suspended) - return callback(std::nullopt, "Network is suspended"); + // If we don't have any in-progress path builds then reset the 'unused_path_build_nodes' + // to be the `snode_cache` minus nodes in any current paths + if (in_progress_path_builds.empty()) { + std::vector existing_path_node_ips = all_path_ips(); + std::copy_if( + snode_cache.begin(), + snode_cache.end(), + std::back_inserter(unused_path_build_nodes), + [&existing_path_node_ips](const auto& node) { + return std::find( + existing_path_node_ips.begin(), + existing_path_node_ips.end(), + node.to_ipv4()) == existing_path_node_ips.end(); + }); - std::pair, uint8_t> path_info = - find_possible_path(excluded_node, current_paths); - auto& [target_path, paths_count] = path_info; + // Shuffle the `unused_path_build_nodes` value so we build paths from random nodes + std::shuffle(unused_path_build_nodes.begin(), unused_path_build_nodes.end(), rng); + general_path_build_failures = 0; + } - // The path doesn't have a valid connection so we should try to reconnect (we will end - // up updating the `paths` value so should do this in a blocking way) - if (target_path && !target_path->conn_info.is_valid()) { - log::trace( + // Make sure we have possible guard nodes before continuing + if (unused_path_build_nodes.empty()) { + log::warning( cat, - "{} found invalid connection for {}, will try to recover.", - __PRETTY_FUNCTION__, + "Unable to build {} path due to lack of possible guard nodes ({}).", + path_name, request_id); + general_path_build_failures++; + path_build_queue.emplace_back(path_type); - path_info = paths_and_pool_loop->call_get( - [this, path_type, request_id, path = *target_path]() mutable - -> std::pair, uint8_t> { - // Since this may have been blocked by another thread we should start by - // making sure the target path is still one of the current paths - auto current_paths = - net.call_get([this, path_type]() -> std::vector { - return paths_for_type(path_type); - }); - auto target_path_it = - std::find(current_paths.begin(), current_paths.end(), path); + auto delay = retry_delay(general_path_build_failures); + net.call_later(delay, [this]() { resume_queues(); }); + return; + } - // If we didn't find the path then don't bother continuing - if (target_path_it == current_paths.end()) { - log::trace( - cat, - "{} path with invalid connection for {} no longer exists.", - __PRETTY_FUNCTION__, - request_id); - return {std::nullopt, current_paths.size()}; - } + // Add this build to the `in_progress_path_builds` map if it doesn't already exist + in_progress_path_builds.try_emplace(request_id, path_type, 0); + + // Exclude nodes targeted in the request_queue from the path (so any queued requests will be + // able to be sent once the path is built) + std::vector nodes_to_exclude; + + for (const auto& [info, callback] : request_queue[path_type]) + if (auto* dest = std::get_if(&info.destination)) + nodes_to_exclude.emplace_back(*dest); + + // Get the possible guard nodes + service_node target_node = unused_path_build_nodes.back(); + + if (nodes_to_exclude.empty()) + unused_path_build_nodes.pop_back(); + else { + auto it = std::find_if( + unused_path_build_nodes.begin(), + unused_path_build_nodes.end(), + [&nodes_to_exclude](const service_node& node) { + return std::find(nodes_to_exclude.begin(), nodes_to_exclude.end(), node) == + nodes_to_exclude.end(); + }); - // It's possible that multiple requests were queued up waiting on the connection - // the be reestablished so check to see if the path is now valid and return it - // if it is - if (target_path_it->conn_info.is_valid()) { - log::trace( - cat, - "{} connection to {} for {} has already been recovered.", - __PRETTY_FUNCTION__, - target_path_it->nodes[0], - request_id); - return {*target_path_it, current_paths.size()}; - } + if (it == unused_path_build_nodes.end()) { + log::warning( + cat, + "Unable to build paths due to lack of possible guard nodes ({}).", + request_id); + general_path_build_failures++; + path_build_queue.emplace_back(path_type); + in_progress_path_builds.erase(request_id); + + auto delay = retry_delay(general_path_build_failures); + net.call_later(delay, [this]() { resume_queues(); }); + return; + } + + target_node = *it; + unused_path_build_nodes.erase(it); + } + + // Make a request to the guard node to ensure it's reachable + log::info(cat, "Testing guard snode: {} for {}", target_node.to_string(), request_id); + + get_snode_version( + request_id, + path_type, + target_node, + 3s, + [this, path_name, path_type, target_node, request_id]( + std::vector, connection_info info, std::optional error) { + log::trace(cat, "Got snode version response for {}.", request_id); + + try { + if (error) + throw std::runtime_error{"Testing {} for {} failed with error: {}"_format( + target_node.to_string(), request_id, *error)}; - // Try to retrieve a valid connection for the guard node + // Build the new paths log::info( cat, - "Connection to {} with type {} for {} path no longer valid, attempting " - "reconnection.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), + "Guard snode {} valid for {}.", + target_node.to_string(), request_id); - auto [info, error] = get_connection_info(request_id, path_type, path.nodes[0]); + std::vector path_nodes{info.node}; - // It's possible that the connection was created successfully, and reported as - // valid, but isn't actually valid (eg. it was shutdown immediately due to the - // network being unreachable) so to avoid this we wait for either the connection - // to be established or the connection to fail before continuing - if (!info.is_valid()) { - log::info( - cat, - "Reconnection to {} with type {} for {} path failed with error: " - "{}.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), - request_id, - error.value_or("Unknown error.")); - return {std::nullopt, current_paths.size()}; + while (path_nodes.size() < path_size) { + if (unused_path_build_nodes.empty()) + throw std::runtime_error{ + "Unable to build {} path due to lack of unused path build nodes ({})."_format( + path_name, request_id)}; + + // Grab the next unused node to continue building the path + auto node = unused_path_build_nodes.back(); + unused_path_build_nodes.pop_back(); + + // Ensure we don't put two nodes with the same IP into the same path + auto snode_with_ip_it = std::find_if( + path_nodes.begin(), + path_nodes.end(), + [&node](const auto& existing_node) { + return existing_node.to_ipv4() == node.to_ipv4(); + }); + + if (snode_with_ip_it == path_nodes.end()) + path_nodes.push_back(node); } - // Knowing that the reconnection succeeded is helpful for debugging + // Store the new path + auto path = onion_path{std::move(info), path_nodes, 0}; + paths[path_type].emplace_back(path); + + // Log that a path was built + std::vector node_descriptions; + std::transform( + path_nodes.begin(), + path_nodes.end(), + std::back_inserter(node_descriptions), + [](const service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); log::info( cat, - "Reconnection to {} with type {} for {} path successful.", - target_path_it->nodes[0], - path_type_name(path_type, single_path_mode), - request_id); + "Built new {} onion request path ({}): [{}]", + path_name, + request_id, + path_description); // If the connection info is valid and it's a standard path then update the - // connection status back to connected - if (path_type == PathType::standard) + // connection status to connected + if (path_type == PathType::standard) { update_status(ConnectionStatus::connected); - // No need to call the 'paths_changed' callback as the paths haven't - // actually changed, just their connection info - auto updated_path = onion_path{std::move(info), path.nodes, 0}; - auto paths_count = net.call_get( - [this, path_type, path, updated_path]() mutable -> uint8_t { - switch (path_type) { - case PathType::standard: - std::replace( - standard_paths.begin(), - standard_paths.end(), - path, - updated_path); - return standard_paths.size(); - - case PathType::upload: - std::replace( - upload_paths.begin(), - upload_paths.end(), - path, - updated_path); - return upload_paths.size(); - - case PathType::download: - std::replace( - download_paths.begin(), - download_paths.end(), - path, - updated_path); - return download_paths.size(); - } - log::error( - cat, - "Internal error: unhandled path type {}", - static_cast(path_type)); - throw std::logic_error{"Internal error: unhandled path type!"}; - }); + // If a paths_changed callback was provided then call it + if (paths_changed) { + std::vector> raw_paths; + for (auto& path : paths[path_type]) + raw_paths.emplace_back(path.nodes); - return {updated_path, paths_count}; - }); - } + paths_changed(raw_paths); + } + } - // If we didn't get a target path then we have to build paths - if (!target_path) { - log::trace(cat, "{} no path found for {}.", __PRETTY_FUNCTION__, request_id); - return with_paths_and_pool( - request_id, - path_type, - excluded_node, - [this, excluded_node, cb = std::move(callback)]( - std::vector updated_paths, - std::vector, - std::optional error) { - if (error) - return cb(std::nullopt, *error); + // If the network happened to get suspended just before the guard node was + // connected then we may need to reset the connection info so the new path is + // also in a suspended state + if (suspended) { + log::info( + cat, + "Network is suspended, suspending new {} path ({})", + path_name, + request_id); + info.conn.reset(); + info.stream.reset(); + } + + // Resume any queued requests + in_progress_path_builds.erase(request_id); + net.call_soon([this]() { resume_queues(); }); + } catch (const std::exception& e) { + // Log the error and loop after a slight delay (don't want to drain the pool + // too quickly if the network goes down) + log::info(cat, "{}", e.what()); + + // Delay the next path build attempt based on the error we received + auto failure_count = in_progress_path_builds[request_id].second; + in_progress_path_builds[request_id] = {path_type, failure_count + 1}; + auto delay = retry_delay(failure_count + 1); + net.call_later(delay, [this, request_id, path_type]() { + build_path(request_id, path_type); + }); + } + }); +} - auto [target_path, paths_count] = - find_possible_path(excluded_node, updated_paths); +void Network::recover_path(PathType path_type, onion_path path) { + // Try to re-establish a connection to the guard node + auto request_id = random::random_base32(4); + log::trace( + cat, + "Connection to {} for {} path no longer valid, attempting reconnection ({}).", + path.nodes.front(), + path_type_name(path_type, single_path_mode), + request_id); - if (!target_path) - return cb(std::nullopt, "Unable to find valid path."); + establish_connection( + request_id, + path_type, + path.nodes.front(), + 3s, + [this, request_id, path_type, old_path = path]( + connection_info info, std::optional error) { + auto guard_node = old_path.nodes.front(); - cb(*target_path, std::nullopt); - }); - } + if (!info.is_valid()) { + log::info( + cat, + "Reconnection to {} for {} path failed ({}) with error: {}.", + guard_node, + path_type_name(path_type, single_path_mode), + request_id, + error.value_or("Unknown error")); - // Build additional paths in the background if we don't have enough - if (paths_count < target_path_count(path_type, single_path_mode)) { - auto new_request_id = random::random_base32(4); - log::trace( - cat, - "{} found path, but we don't have the desired number so starting a background path " - "build from {} with new id: {}.", - __PRETTY_FUNCTION__, - request_id, - new_request_id); - build_paths_and_pool_in_background(new_request_id, path_type); - } + // If we failed to reconnect to the path, and it was a standard path, then we + // should call 'paths_changed' as it is has now been "officially" dropped + if (path_type == PathType::standard && paths_changed) { + std::vector> raw_paths; + for (auto& path : paths[path_type]) + raw_paths.emplace_back(path.nodes); + + paths_changed(raw_paths); + } + + // Now enqueue a new path build (if needed) for the type of path we dropped and + // resume the queues to trigger the path build (if needed) + enqueue_path_build_if_needed(path_type, false); + net.call_soon([this]() { resume_queues(); }); + return; + } + + // Knowing that the reconnection succeeded is helpful for debugging + log::info( + cat, + "Reconnection to {} for {} path successful ({}).", + guard_node, + path_type_name(path_type, single_path_mode), + request_id); + + // If the connection info is valid and it's a standard path then update the + // connection status back to connected + if (path_type == PathType::standard) + update_status(ConnectionStatus::connected); - // We have a valid path for the standard path type then update the status in case we had - // flagged it as disconnected for some reason - if (path_type == PathType::standard) - net.call([this]() mutable { update_status(ConnectionStatus::connected); }); + // No need to call the 'paths_changed' callback as the paths haven't + // actually changed, just their connection info + paths[path_type].emplace_back(onion_path{std::move(info), old_path.nodes, 0}); - callback(target_path, std::nullopt); + // Resume any queued requests + net.call_soon([this]() { resume_queues(); }); + }); } -std::pair, uint8_t> Network::find_possible_path( - std::optional excluded_node, std::vector paths) { +std::optional Network::find_valid_path( + request_info info, std::vector paths) { if (paths.empty()) - return {std::nullopt, paths.size()}; + return std::nullopt; + // Only include paths with valid connections as options std::vector possible_paths; + std::copy_if( + paths.begin(), paths.end(), std::back_inserter(possible_paths), [&](const auto& path) { + return path.is_valid(); + }); - if (!excluded_node) - possible_paths = paths; - else + // If the request destination is a node then only select a path that doesn't include the IP of + // the destination + if (auto target = node_for_destination(info.destination)) { + std::vector ip_excluded_paths; std::copy_if( - paths.begin(), - paths.end(), - std::back_inserter(possible_paths), - [&excluded_node](const auto& path) { - return !path.nodes.empty() && - (!excluded_node || - std::find(path.nodes.begin(), path.nodes.end(), excluded_node) == - path.nodes.end()); + possible_paths.begin(), + possible_paths.end(), + std::back_inserter(ip_excluded_paths), + [excluded_ip = target->to_ipv4()](const auto& path) { + return std::none_of( + path.nodes.begin(), path.nodes.end(), [&excluded_ip](const auto& node) { + return node.to_ipv4() == excluded_ip; + }); }); + if (single_path_mode && ip_excluded_paths.empty()) + log::warning( + cat, + "Path should have been excluded due to matching IP for {} but network is in " + "single path mode.", + info.request_id); + else + possible_paths = ip_excluded_paths; + } + if (possible_paths.empty()) - return {std::nullopt, paths.size()}; + return std::nullopt; CSRNG rng; std::shuffle(possible_paths.begin(), possible_paths.end(), rng); - return {possible_paths.front(), paths.size()}; + return possible_paths.front(); }; +void Network::enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable) { + auto current_paths = paths[path_type].size(); + + // In `single_path_mode` we never build additional paths + if (current_paths > 0 && single_path_mode) + return; + + // Get the number pending paths + auto pending_build_count = + std::count(path_build_queue.begin(), path_build_queue.end(), path_type); + auto in_progress_build_count = std::count_if( + in_progress_path_builds.begin(), + in_progress_path_builds.end(), + [&path_type](const auto& pair) { return pair.second.first == path_type; }); + auto pending_paths = (pending_build_count + in_progress_build_count); + + // We only want to enqueue a new path build if: + // - We don't have the minimum number of paths for the specified type + // - We don't have any pending builds + // - The current paths are unsuitable for the request + auto min_paths = min_path_count(path_type, single_path_mode); + + if ((current_paths + pending_paths) < min_paths || + (existing_paths_unsuitable && pending_paths == 0)) + path_build_queue.emplace_back(path_type); +} + // MARK: Multi-request logic void Network::get_service_nodes_recursive( @@ -1547,106 +1488,17 @@ void Network::get_service_nodes_recursive( [this, limit, target_nodes, request_id, cb = std::move(callback)]( std::vector nodes, std::optional error) { // If we got nodes then stop looping and return them - if (!nodes.empty()) - return cb(nodes, error); + if (!nodes.empty()) { + cb(nodes, error); + return; + } // Loop if we didn't get any nodes std::vector remaining_nodes( target_nodes.begin() + 1, target_nodes.end()); - get_service_nodes_recursive(request_id, remaining_nodes, limit, cb); - }); -} - -void Network::find_valid_guard_node_recursive( - std::string request_id, - PathType path_type, - int64_t test_attempt, - std::vector target_nodes, - std::function< - void(std::optional valid_guard_node, - std::vector unused_nodes, - std::optional)> callback) { - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - if (target_nodes.empty()) - return callback(std::nullopt, {}, "Failed to find valid guard node."); - - // If the network is currently suspended then fail immediately - auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); - if (currently_suspended) - return callback(std::nullopt, {}, "Network is suspended"); - - auto target_node = target_nodes.front(); - log::info(cat, "Testing guard snode: {} for {}", target_node.to_string(), request_id); - - get_version( - request_id, - path_type, - target_node, - 3s, - [this, - path_type, - test_attempt, - target_node, - target_nodes, - request_id, - cb = std::move(callback)]( - std::vector version, - connection_info info, - std::optional error) { - log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); - std::vector remaining_nodes( - target_nodes.begin() + 1, target_nodes.end()); - - try { - if (error) - throw std::runtime_error{*error}; - - // Ensure the node meets the minimum version requirements after a slight - // delay (don't want to drain the pool if the network goes down) - std::vector min_version = parse_version("2.0.7"); - if (version < min_version) - throw std::runtime_error{ - "Outdated node version ({})"_format(fmt::join(version, "."))}; - - log::info( - cat, - "Guard snode {} valid for {}.", - target_node.to_string(), - request_id); - cb(info, remaining_nodes, std::nullopt); - } catch (const std::exception& e) { - // Log the error and loop after a slight delay (don't want to drain the pool - // too quickly if the network goes down) - log::info( - cat, - "Testing {} for {} failed with error: {}", - target_node.to_string(), - request_id, - e.what()); - - // Delay the next test based on the error we received - std::chrono::duration delay = std::chrono::milliseconds(100); - - // If we got a network unreachable error then we want to delay on an exponential - // curve so that we don't trash the battery life in the device loses connection - if (error) { - constexpr std::chrono::milliseconds maxDelay = 3s; - delay = std::min(maxDelay, 100ms * (1 << test_attempt)); - } - - std::thread retry_thread([this, - delay, - path_type, - test_attempt, - remaining_nodes, - request_id, - cb = std::move(cb)] { - std::this_thread::sleep_for(delay); - find_valid_guard_node_recursive( - request_id, path_type, test_attempt + 1, remaining_nodes, cb); - }); - retry_thread.detach(); - } + net.call_soon([this, request_id, remaining_nodes, limit, cb = std::move(cb)]() { + get_service_nodes_recursive(request_id, remaining_nodes, limit, cb); + }); }); } @@ -1659,79 +1511,92 @@ void Network::get_service_nodes( std::function nodes, std::optional error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - auto [info, error] = get_connection_info(request_id, PathType::standard, node); - - if (!info.is_valid()) - return callback({}, error.value_or("Unknown error.")); - - nlohmann::json params{ - {"active_only", true}, - {"fields", - {{"public_ip", true}, - {"pubkey_ed25519", true}, - {"storage_lmq_port", true}, - {"storage_server_version", true}}}}; - - if (limit) - params["limit"] = *limit; - - oxenc::bt_dict_producer payload; - payload.append("endpoint", "get_service_nodes"); - payload.append("params", params.dump()); - - info.stream->command( - "oxend_request", - payload.view(), - [this, request_id, cb = std::move(callback)](quic::message resp) { - log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); - try { - auto [status_code, body] = validate_response(resp, true); - - oxenc::bt_list_consumer result_bencode{body}; - result_bencode.skip_value(); // Skip the status code (already validated) - auto response_dict = result_bencode.consume_dict_consumer(); - response_dict.skip_until("result"); - - auto result_dict = response_dict.consume_dict_consumer(); - result_dict.skip_until("service_node_states"); - - // Process the node list - std::vector result; - auto node = result_dict.consume_list_consumer(); - - while (!node.is_finished()) { - auto node_consumer = node.consume_dict_consumer(); - auto pubkey_ed25519 = - oxenc::from_hex(consume_string(node_consumer, "pubkey_ed25519")); - auto public_ip = consume_string(node_consumer, "public_ip"); - auto storage_lmq_port = - consume_integer(node_consumer, "storage_lmq_port"); - - std::vector storage_server_version; - node_consumer.skip_until("storage_server_version"); - auto version_consumer = node_consumer.consume_list_consumer(); - - while (!version_consumer.is_finished()) { - storage_server_version.emplace_back( - version_consumer.consume_integer()); - } - result.emplace_back( - pubkey_ed25519, - storage_server_version, - public_ip, - storage_lmq_port); - } + establish_connection( + request_id, + PathType::standard, + node, + 3s, + [this, request_id, limit, cb = std::move(callback)]( + connection_info info, std::optional error) { + if (!info.is_valid()) + return cb({}, error.value_or("Unknown error.")); + + nlohmann::json params{ + {"active_only", true}, + {"fields", + {{"public_ip", true}, + {"pubkey_ed25519", true}, + {"storage_lmq_port", true}, + {"storage_server_version", true}}}}; + + if (limit) + params["limit"] = *limit; + + oxenc::bt_dict_producer payload; + payload.append("endpoint", "get_service_nodes"); + payload.append("params", params.dump()); + + info.stream->command( + "oxend_request", + payload.view(), + [this, request_id, cb = std::move(cb)](quic::message resp) { + log::trace( + cat, + "{} got response for {}.", + __PRETTY_FUNCTION__, + request_id); + std::vector result; - // Output the result - cb(result, std::nullopt); - } catch (const std::exception& e) { - cb({}, e.what()); - } + try { + auto [status_code, body] = validate_response(resp, true); + + oxenc::bt_list_consumer result_bencode{body}; + result_bencode + .skip_value(); // Skip the status code (already validated) + auto response_dict = result_bencode.consume_dict_consumer(); + response_dict.skip_until("result"); + + auto result_dict = response_dict.consume_dict_consumer(); + result_dict.skip_until("service_node_states"); + + // Process the node list + auto node = result_dict.consume_list_consumer(); + + while (!node.is_finished()) { + auto node_consumer = node.consume_dict_consumer(); + auto pubkey_ed25519 = oxenc::from_hex( + consume_string(node_consumer, "pubkey_ed25519")); + auto public_ip = consume_string(node_consumer, "public_ip"); + auto storage_lmq_port = consume_integer( + node_consumer, "storage_lmq_port"); + + std::vector storage_server_version; + node_consumer.skip_until("storage_server_version"); + auto version_consumer = node_consumer.consume_list_consumer(); + + while (!version_consumer.is_finished()) { + storage_server_version.emplace_back( + version_consumer.consume_integer()); + } + + result.emplace_back( + pubkey_ed25519, + storage_server_version, + public_ip, + storage_lmq_port); + } + } catch (const std::exception& e) { + return cb({}, e.what()); + } + + // Output the result + cb(result, std::nullopt); + }); }); } -void Network::get_version( +void Network::get_snode_version( std::string request_id, PathType path_type, service_node node, @@ -1740,37 +1605,48 @@ void Network::get_version( std::vector version, connection_info info, std::optional error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - auto [info, error] = get_connection_info(request_id, path_type, node); - - if (!info.is_valid()) - return callback({}, info, error.value_or("Unknown error.")); - - oxenc::bt_dict_producer payload; - info.stream->command( - "info", - payload.view(), + establish_connection( + request_id, + path_type, + node, timeout, - [this, info, request_id, cb = std::move(callback)](quic::message resp) { - log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); - try { - auto [status_code, body] = validate_response(resp, true); - - oxenc::bt_list_consumer result_bencode{body}; - result_bencode.skip_value(); // Skip the status code (already validated) - auto response_dict = result_bencode.consume_dict_consumer(); - response_dict.skip_until("result"); - - std::vector version; - response_dict.skip_until("version"); - auto version_list = response_dict.consume_list_consumer(); + [this, request_id, timeout, cb = std::move(callback)]( + connection_info info, std::optional error) { + if (!info.is_valid()) + return cb({}, info, error.value_or("Unknown error.")); + + oxenc::bt_dict_producer payload; + info.stream->command( + "info", + payload.view(), + timeout, + [this, info, request_id, cb = std::move(cb)](quic::message resp) { + log::trace( + cat, + "{} got response for {}.", + __PRETTY_FUNCTION__, + request_id); + std::vector version; - while (!version_list.is_finished()) - version.emplace_back(version_list.consume_integer()); + try { + auto [status_code, body] = validate_response(resp, true); + + oxenc::bt_list_consumer result_bencode{body}; + result_bencode + .skip_value(); // Skip the status code (already validated) + auto response_dict = result_bencode.consume_dict_consumer(); + response_dict.skip_until("version"); + auto version_list = response_dict.consume_list_consumer(); + + while (!version_list.is_finished()) + version.emplace_back(version_list.consume_integer()); + } catch (const std::exception& e) { + return cb({}, info, e.what()); + } - cb(version, info, std::nullopt); - } catch (const std::exception& e) { - return cb({}, info, e.what()); - } + // Output the result + cb(version, info, std::nullopt); + }); }); } @@ -1779,97 +1655,92 @@ void Network::get_swarm( std::function swarm)> callback) { auto request_id = random::random_base32(4); log::trace(cat, "{} called for {} as {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex(), request_id); - auto cached_swarm = - net.call_get([this, swarm_pubkey]() -> std::optional> { - if (!swarm_cache.contains(swarm_pubkey.hex())) - return std::nullopt; - return swarm_cache[swarm_pubkey.hex()]; - }); - // If we have a cached swarm, and it meets the minimum size requirements, then return it - if (cached_swarm && cached_swarm->size() >= min_swarm_snode_count) - return callback(*cached_swarm); + net.call([this, request_id, swarm_pubkey, cb = std::move(callback)]() { + // If we have a cached swarm, and it meets the minimum size requirements, then return it + if (swarm_cache[swarm_pubkey.hex()].size() > min_swarm_snode_count) + return cb(swarm_cache[swarm_pubkey.hex()]); - // Pick a random node from the snode pool to fetch the swarm from - log::info( - cat, - "No cached swarm for {} as {}, fetching from random node.", - swarm_pubkey.hex(), - request_id); + // Pick a random node from the snode pool to fetch the swarm from + log::info( + cat, + "Get swarm had no valid cached swarm for {}, fetching from random node ({}).", + swarm_pubkey.hex(), + request_id); - with_paths_and_pool( - request_id, - PathType::standard, - std::nullopt, - [this, swarm_pubkey, request_id, cb = std::move(callback)]( - std::vector, - std::vector pool, - std::optional) { - if (pool.empty()) - return cb({}); - - auto updated_pool = pool; - CSRNG rng; - std::shuffle(updated_pool.begin(), updated_pool.end(), rng); - auto node = updated_pool.front(); + // If we have no snode cache then we need to rebuild the cache and run this request again + // once it's rebuild + if (snode_cache.empty()) { + after_snode_cache_refresh.emplace_back( + [this, swarm_pubkey, cb = std::move(cb)]() { get_swarm(swarm_pubkey, cb); }); + net.call_soon([this]() { refresh_snode_cache(); }); + return; + } - nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; - nlohmann::json payload{ - {"method", "get_swarm"}, - {"params", params}, - }; + CSRNG rng; + auto random_cache = snode_cache; + std::shuffle(random_cache.begin(), random_cache.end(), rng); - send_onion_request( - PathType::standard, - node, - ustring{quic::to_usv(payload.dump())}, - swarm_pubkey, - quic::DEFAULT_TIMEOUT, - std::nullopt, - std::nullopt, - [this, swarm_pubkey, request_id, cb = std::move(cb)]( - bool success, - bool timeout, - int16_t, - std::optional response) { - log::trace( - cat, - "{} got response for {} as {}.", - __PRETTY_FUNCTION__, - swarm_pubkey.hex(), - request_id); - if (!success || timeout || !response) - return cb({}); + nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; + nlohmann::json payload{ + {"method", "get_swarm"}, + {"params", params}, + }; + request_info info{ + request_id, + random_cache.front(), + "onion_req", + std::nullopt, // Will be generated after retrieving the path + ustring{quic::to_usv(payload.dump())}, + swarm_pubkey, + PathType::standard, + quic::DEFAULT_TIMEOUT, + true, + std::nullopt}; + + send_onion_request( + info, + [this, swarm_pubkey, request_id, cb = std::move(cb)]( + bool success, bool timeout, int16_t, std::optional response) { + log::trace( + cat, + "{} got response for {} as {}.", + __PRETTY_FUNCTION__, + swarm_pubkey.hex(), + request_id); + if (!success || timeout || !response) + return cb({}); - std::vector swarm; + std::vector swarm; - try { - nlohmann::json response_json = nlohmann::json::parse(*response); + try { + nlohmann::json response_json = nlohmann::json::parse(*response); - if (!response_json.contains("snodes") || - !response_json["snodes"].is_array()) - throw std::runtime_error{"JSON missing swarm field."}; + if (!response_json.contains("snodes") || + !response_json["snodes"].is_array()) + throw std::runtime_error{"JSON missing swarm field."}; - for (auto& snode : response_json["snodes"]) - swarm.emplace_back(node_from_json(snode)); - } catch (...) { - return cb({}); - } + for (auto& snode : response_json["snodes"]) + swarm.emplace_back(node_from_json(snode)); + } catch (...) { + return cb({}); + } - // Update the cache - net.call([this, swarm_pubkey, swarm]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey.hex()] = swarm; - need_swarm_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - }); + // Update the cache + log::info(cat, "Retrieved swarm for {} ({}).", swarm_pubkey.hex(), request_id); + net.call([this, swarm_pubkey, swarm]() mutable { + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[swarm_pubkey.hex()] = swarm; + need_swarm_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); + }); - cb(swarm); - }); - }); + cb(swarm); + }); + }); } void Network::set_swarm( @@ -1889,25 +1760,22 @@ void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { auto request_id = random::random_base32(4); log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - with_paths_and_pool( - request_id, - PathType::standard, - std::nullopt, - [count, request_id, cb = std::move(callback)]( - std::vector, - std::vector pool, - std::optional) { - log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); - if (pool.size() < count) - return cb({}); - - auto random_pool = pool; - CSRNG rng; - std::shuffle(random_pool.begin(), random_pool.end(), rng); - std::vector result(random_pool.begin(), random_pool.begin() + count); - cb(result); - }); + net.call([this, request_id, count, cb = std::move(callback)]() mutable { + // If we don't have sufficient nodes in the snode cache then add this to + if (snode_cache.size() < count) { + after_snode_cache_refresh.emplace_back( + [this, count, cb = std::move(cb)]() { get_random_nodes(count, cb); }); + net.call_soon([this]() { refresh_snode_cache(); }); + return; + } + + // Otherwise callback with the requested random number of nodes + CSRNG rng; + auto random_cache = snode_cache; + std::shuffle(random_cache.begin(), random_cache.end(), rng); + cb(std::vector(random_cache.begin(), random_cache.begin() + count)); + }); } // MARK: Request Handling @@ -1915,6 +1783,7 @@ void Network::get_random_nodes( void Network::send_request( request_info info, connection_info conn_info, network_response_callback_t handle_response) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, info.request_id); + if (!conn_info.is_valid()) return handle_response(false, false, -1, "Network is unreachable."); @@ -1927,245 +1796,219 @@ void Network::send_request( info.endpoint, payload, info.timeout, - [this, info, target = info.target, cb = std::move(handle_response)]( - quic::message resp) { + [this, info, conn_info, cb = std::move(handle_response)](quic::message resp) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); + try { auto [status_code, body] = validate_response(resp, false); cb(true, false, status_code, body); } catch (const status_code_exception& e) { - handle_errors( - info, - {{target, nullptr, nullptr}, {target}, 0}, - false, - e.status_code, - e.what(), - cb); + handle_errors(info, conn_info, false, e.status_code, e.what(), cb); } catch (const std::exception& e) { - handle_errors( - info, - {{target, nullptr, nullptr}, {target}, 0}, - resp.timed_out, - -1, - e.what(), - cb); + handle_errors(info, conn_info, resp.timed_out, -1, e.what(), cb); } }); } void Network::send_onion_request( - PathType path_type, - network_destination destination, + onionreq::network_destination destination, std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - std::optional existing_request_id, - std::optional retry_reason, network_response_callback_t handle_response) { - auto request_id = existing_request_id.value_or(random::random_base32(4)); - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - with_path( - request_id, - path_type, - node_for_destination(destination), - [this, - path_type, - destination = std::move(destination), - body, - swarm_pubkey, - timeout, - request_id, - retry_reason, - cb = std::move(handle_response)]( - std::optional path, std::optional error) { - log::trace(cat, "{} got path for {}.", __PRETTY_FUNCTION__, request_id); - if (!path) - return cb(false, false, -1, error.value_or("No valid onion paths.")); - - try { - // Construct the onion request - auto builder = Builder(); - builder.set_destination(destination); - builder.set_destination_pubkey(pubkey_for_destination(destination)); - - for (auto& node : path->nodes) - builder.add_hop( - {ed25519_pubkey::from_bytes(node.view_remote_key()), - compute_xpk(node.view_remote_key())}); - - auto payload = builder.generate_payload(body); - auto onion_req_payload = builder.build(payload); - - request_info info{ - request_id, - path->nodes[0], - destination, - "onion_req", - onion_req_payload, - body, - swarm_pubkey, - path_type, - timeout, - node_for_destination(destination).has_value(), - retry_reason}; - - send_request( - info, - path->conn_info, - [this, - builder = std::move(builder), - info, - path = *path, - destination = std::move(destination), - cb = std::move(cb)]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - log::trace( - cat, - "{} got response for {}.", - __PRETTY_FUNCTION__, - info.request_id); - - // If the request was reported as a failure or a timeout then we - // will have already handled the errors so just trigger the callback - if (!success || timeout) - return cb(success, timeout, status_code, response); - - try { - // Ensure the response is long enough to be processed, if not - // then handle it as an error - if (!ResponseParser::response_long_enough( - builder.enc_type, response->size())) - throw status_code_exception{ - status_code, - "Response is too short to be an onion request " - "response: " + - *response}; - - // Otherwise, process the onion request response - std::pair> - processed_response; - - // The SnodeDestination runs via V3 onion requests and the - // ServerDestination runs via V4 - if (std::holds_alternative(destination)) - processed_response = - process_v3_onion_response(builder, *response); - else if (std::holds_alternative(destination)) - processed_response = - process_v4_onion_response(builder, *response); - - // If we got a non 2xx status code, return the error - auto& [processed_status_code, processed_body] = - processed_response; - if (processed_status_code < 200 || processed_status_code > 299) - throw status_code_exception{ - processed_status_code, - processed_body.value_or("Request returned " - "non-success status " - "code.")}; - - // For debugging purposes if the error was a redirect retry then - // we want to log that the retry was successful as this will - // help identify how often we are receiving incorrect 421 errors - if (info.retry_reason == request_info::RetryReason::redirect) - log::info( - cat, - "Received valid response after 421 retry in " - "request {} for {}.", - info.request_id, - path_type_name(info.path_type, single_path_mode)); - - // Try process the body in case it was a batch request which - // failed - std::optional results; - if (processed_body) { - try { - auto processed_body_json = - nlohmann::json::parse(*processed_body); - - // If it wasn't a batch/sequence request then assume it - // was successful and return no error - if (processed_body_json.contains("results")) - results = processed_body_json["results"]; - } catch (...) { - } - } - - // If there was no 'results' array then it wasn't a batch - // request so we can stop here and return - if (!results) - return cb( - true, false, processed_status_code, processed_body); - - // Otherwise we want to check if all of the results have the - // same status code and, if so, handle that failure case - // (default the 'error_body' to the 'processed_body' in case we - // don't get an explicit error) - int16_t single_status_code = -1; - std::optional error_body = processed_body; - for (const auto& result : results->items()) { - if (result.value().contains("code") && - result.value()["code"].is_number() && - (single_status_code == -1 || - result.value()["code"].get() != - single_status_code)) - single_status_code = - result.value()["code"].get(); - else { - // Either there was no code, or the code was different - // from a former code in which case there wasn't an - // individual detectable error (ie. it needs specific - // handling) so return no error - single_status_code = 200; - break; - } - - if (result.value().contains("body") && - result.value()["body"].is_string()) - error_body = result.value()["body"].get(); - } - - // If all results contained the same error then handle it as a - // single error - if (single_status_code < 200 || single_status_code > 299) - throw status_code_exception{ - single_status_code, - error_body.value_or("Sub-request returned " - "non-success status code.")}; - - // Otherwise some requests succeeded and others failed so - // succeed with the processed data - return cb(true, false, processed_status_code, processed_body); - } catch (const status_code_exception& e) { - handle_errors(info, path, false, e.status_code, e.what(), cb); - } catch (const std::exception& e) { - handle_errors(info, path, false, -1, e.what(), cb); - } - }); - } catch (const std::exception& e) { - cb(false, false, -1, e.what()); - } - }); + send_onion_request( + PathType::standard, destination, body, swarm_pubkey, timeout, handle_response); } void Network::send_onion_request( + PathType path_type, network_destination destination, std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, network_response_callback_t handle_response) { - send_onion_request( - PathType::standard, + request_info info{ + random::random_base32(4), destination, + "onion_req", + std::nullopt, // Will be generated after retrieving the path body, swarm_pubkey, + path_type, timeout, - std::nullopt, - std::nullopt, - handle_response); + node_for_destination(destination).has_value(), + std::nullopt}; + + send_onion_request(info, handle_response); +} + +void Network::send_onion_request(request_info info, network_response_callback_t handle_response) { + auto path_name = path_type_name(info.path_type, single_path_mode); + log::trace(cat, "{} called for {} path ({}).", __PRETTY_FUNCTION__, path_name, info.request_id); + + // Try to retrieve a valid path for this request + auto [path, callback] = net.call_get( + [this, info, cb = std::move(handle_response)]() mutable + -> std::pair, std::optional> { + // If the network is suspended then fail immediately + if (suspended) { + cb(false, false, error_network_suspended, "Network is suspended."); + return {std::nullopt, std::nullopt}; + } + + // Otherwise try to retrieve a valid path + if (auto path = find_valid_path(info, paths[info.path_type])) + return {path, std::move(cb)}; + + // Currently there are no valid paths so enqueue a new build (if needed) and add the + // request to the queue to be run one the path build completes + request_queue[info.path_type].emplace_back(info, std::move(cb)); + enqueue_path_build_if_needed(info.path_type, true); + net.call_soon([this]() { resume_queues(); }); + return {std::nullopt, std::nullopt}; + }); + + // If either of these don't exist then we will have added this request to the queue to be run + // one we have a valid path + if (!path || !callback) + return; + + log::trace(cat, "{} got {} path for {}.", __PRETTY_FUNCTION__, path_name, info.request_id); + + // Construct the onion request + auto builder = Builder(); + try { + builder.set_destination(info.destination); + builder.set_destination_pubkey(pubkey_for_destination(info.destination)); + + for (auto& node : path->nodes) + builder.add_hop( + {ed25519_pubkey::from_bytes(node.view_remote_key()), + compute_xpk(node.view_remote_key())}); + + // Update the `request_info` to have the onion request payload + auto payload = builder.generate_payload(info.original_body); + info.body = builder.build(payload); + } catch (const std::exception& e) { + return (*callback)(false, false, error_building_onion_request, e.what()); + } + + // Actually send the request + send_request( + info, + path->conn_info, + [this, builder = std::move(builder), info, path = *path, cb = std::move(*callback)]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); + + // If the request was reported as a failure or a timeout then we + // will have already handled the errors so just trigger the callback + if (!success || timeout) + return cb(success, timeout, status_code, response); + + try { + // Ensure the response is long enough to be processed, if not + // then handle it as an error + if (!ResponseParser::response_long_enough(builder.enc_type, response->size())) + throw status_code_exception{ + status_code, + "Response is too short to be an onion request response: " + + *response}; + + // Otherwise, process the onion request response + std::pair> processed_response; + + // The SnodeDestination runs via V3 onion requests and the + // ServerDestination runs via V4 + if (std::holds_alternative(info.destination)) + processed_response = process_v3_onion_response(builder, *response); + else if (std::holds_alternative(info.destination)) + processed_response = process_v4_onion_response(builder, *response); + + // If we got a non 2xx status code, return the error + auto& [processed_status_code, processed_body] = processed_response; + if (processed_status_code < 200 || processed_status_code > 299) + throw status_code_exception{ + processed_status_code, + processed_body.value_or("Request returned " + "non-success status " + "code.")}; + + // For debugging purposes if the error was a redirect retry then + // we want to log that the retry was successful as this will + // help identify how often we are receiving incorrect 421 errors + if (info.retry_reason == request_info::RetryReason::redirect) + log::info( + cat, + "Received valid response after 421 retry in " + "request {} for {}.", + info.request_id, + path_type_name(info.path_type, single_path_mode)); + + // Try process the body in case it was a batch request which + // failed + std::optional results; + if (processed_body) { + try { + auto processed_body_json = nlohmann::json::parse(*processed_body); + + // If it wasn't a batch/sequence request then assume it + // was successful and return no error + if (processed_body_json.contains("results")) + results = processed_body_json["results"]; + } catch (...) { + } + } + + // If there was no 'results' array then it wasn't a batch + // request so we can stop here and return + if (!results) + return cb(true, false, processed_status_code, processed_body); + + // Otherwise we want to check if all of the results have the + // same status code and, if so, handle that failure case + // (default the 'error_body' to the 'processed_body' in case we + // don't get an explicit error) + int16_t single_status_code = -1; + std::optional error_body = processed_body; + for (const auto& result : results->items()) { + if (result.value().contains("code") && result.value()["code"].is_number() && + (single_status_code == -1 || + result.value()["code"].get() != single_status_code)) + single_status_code = result.value()["code"].get(); + else { + // Either there was no code, or the code was different + // from a former code in which case there wasn't an + // individual detectable error (ie. it needs specific + // handling) so return no error + single_status_code = 200; + break; + } + + if (result.value().contains("body") && result.value()["body"].is_string()) + error_body = result.value()["body"].get(); + } + + // If all results contained the same error then handle it as a + // single error + if (single_status_code < 200 || single_status_code > 299) + throw status_code_exception{ + single_status_code, + error_body.value_or("Sub-request returned " + "non-success status code.")}; + + // Otherwise some requests succeeded and others failed so + // succeed with the processed data + return cb(true, false, processed_status_code, processed_body); + } catch (const status_code_exception& e) { + handle_errors(info, path.conn_info, false, e.status_code, e.what(), cb); + } catch (const std::exception& e) { + handle_errors(info, path.conn_info, false, -1, e.what(), cb); + } + }); } void Network::upload_file_to_server( @@ -2205,8 +2048,6 @@ void Network::upload_file_to_server( data, std::nullopt, timeout, - std::nullopt, - std::nullopt, handle_response); } @@ -2231,14 +2072,7 @@ void Network::download_file( std::chrono::milliseconds timeout, network_response_callback_t handle_response) { send_onion_request( - PathType::download, - server, - std::nullopt, - std::nullopt, - timeout, - std::nullopt, - std::nullopt, - handle_response); + PathType::download, server, std::nullopt, std::nullopt, timeout, handle_response); } void Network::get_client_version( @@ -2281,8 +2115,6 @@ void Network::get_client_version( std::nullopt, pubkey, timeout, - std::nullopt, - std::nullopt, handle_response); } @@ -2413,10 +2245,10 @@ std::pair Network::validate_response(quic::message resp, return {status_code, response_string}; } -void Network::handle_node_error(service_node node, PathType path_type, onion_path path) { +void Network::handle_node_error( + service_node node, PathType path_type, connection_info conn_info, std::string request_id) { handle_errors( - {"Node Error", - node, + {request_id, node, "", std::nullopt, @@ -2426,22 +2258,23 @@ void Network::handle_node_error(service_node node, PathType path_type, onion_pat 0ms, false, std::nullopt}, - path, + conn_info, false, std::nullopt, - std::nullopt, + "Node Error", std::nullopt); } void Network::handle_errors( request_info info, - onion_path path, + connection_info conn_info, bool timeout_, std::optional status_code_, std::optional response, std::optional handle_response) { bool timeout = timeout_; auto status_code = status_code_.value_or(-1); + auto path_name = path_type_name(info.path_type, single_path_mode); // There is an issue which can occur where we get invalid data back and are unable to decrypt // it, if we do see this behaviour then we want to retry the request on the off chance it @@ -2453,18 +2286,15 @@ void Network::handle_errors( if (!info.retry_reason && response && *response == session::onionreq::decryption_failed_error) { log::info( cat, - "Received decryption failure in request {} for {}, retrying.", + "Received decryption failure in request {} on {} path, retrying.", info.request_id, - path_type_name(info.path_type, single_path_mode)); - return send_onion_request( - info.path_type, - info.destination, - info.original_body, - info.swarm_pubkey, - info.timeout, - info.request_id, - request_info::RetryReason::decryption_failure, - (*handle_response)); + path_name); + auto updated_info = info; + updated_info.retry_reason = request_info::RetryReason::decryption_failure; + request_queue[updated_info.path_type].emplace_back( + updated_info, std::move(*handle_response)); + net.call_soon([this]() { resume_queues(); }); + return; } // A number of server errors can return HTML data but no status code, we want to extract those @@ -2489,10 +2319,10 @@ void Network::handle_errors( // In trace mode log all error info log::trace( cat, - "Received network error in request {} for {}, status_code: {}, timeout: {}, response: " - "{}.", + "Received network error in request {} on {} path, status_code: {}, timeout: {}, " + "response: {}", info.request_id, - path_type_name(info.path_type, single_path_mode), + path_name, status_code, timeout, response.value_or("(No Response)")); @@ -2533,18 +2363,16 @@ void Network::handle_errors( try { // If there is no response handler or no swarm information was provided then we // should just replace the swarm - if (!handle_response || !info.swarm_pubkey) + auto target = node_for_destination(info.destination); + + if (!handle_response || !info.swarm_pubkey || !target) throw std::invalid_argument{"Unable to handle redirect."}; // If this was the first 421 then we want to retry using another node in the // swarm to get confirmation that we should switch to a different swarm if (!info.retry_reason || info.retry_reason != request_info::RetryReason::redirect) { - auto cached_swarm = net.call_get( - [this, - swarm_pubkey = *info.swarm_pubkey]() -> std::vector { - return swarm_cache[swarm_pubkey.hex()]; - }); + auto cached_swarm = swarm_cache[info.swarm_pubkey->hex()]; if (cached_swarm.empty()) throw std::invalid_argument{ @@ -2557,7 +2385,7 @@ void Network::handle_errors( std::optional random_node; for (auto& node : swarm_copy) { - if (node == info.target) + if (node == *target) continue; random_node = node; @@ -2569,19 +2397,17 @@ void Network::handle_errors( log::info( cat, - "Received 421 error in request {} for {}, retrying once before " + "Received 421 error in request {} on {} path, retrying once before " "updating swarm.", info.request_id, - path_type_name(info.path_type, single_path_mode)); - return send_onion_request( - info.path_type, - *random_node, - info.original_body, - info.swarm_pubkey, - info.timeout, - info.request_id, - request_info::RetryReason::redirect, - (*handle_response)); + path_name); + auto updated_info = info; + updated_info.destination = *random_node; + updated_info.retry_reason = request_info::RetryReason::redirect; + request_queue[updated_info.path_type].emplace_back( + updated_info, std::move(*handle_response)); + net.call_soon([this]() { resume_queues(); }); + return; } if (!response) @@ -2603,21 +2429,18 @@ void Network::handle_errors( log::info( cat, - "Retry for request {} resulted in another 421 for {}, updating swarm.", + "Retry for request {} resulted in another 421 on {} path, updating swarm.", info.request_id, - path_type_name(info.path_type, single_path_mode)); + path_name); // Update the cache - net.call([this, swarm_pubkey = *info.swarm_pubkey, swarm]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey.hex()] = swarm; - need_swarm_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - }); - + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[info.swarm_pubkey->hex()] = swarm; + need_swarm_write = true; + need_write = true; + } + snode_cache_cv.notify_one(); return (*handle_response)(false, false, status_code, response); } catch (...) { } @@ -2641,300 +2464,209 @@ void Network::handle_errors( default: break; } - // Update the cache (want to wait until this has been completed in case another failure happens - // while this is processing as it might result in missing a failure) - std::condition_variable cv; - std::mutex mtx; - bool done = false; - - // Check if we got an error specifying the specific node that failed - net.call([this, - request_id = info.request_id, - path_type = info.path_type, - old_path = path, - response, - &cv, - &mtx, - &done]() mutable { - auto updated_path = old_path; - auto updated_swarm_cache = swarm_cache; - auto updated_failure_counts = snode_failure_counts; - bool found_invalid_node = false; - std::vector nodes_to_drop; - - if (response) { - std::optional ed25519PublicKey; - - // Check if the response has one of the 'node_not_found' prefixes - if (response->starts_with(node_not_found_prefix)) - ed25519PublicKey = {response->data() + node_not_found_prefix.size()}; - else if (response->starts_with(node_not_found_prefix_no_status)) - ed25519PublicKey = {response->data() + node_not_found_prefix_no_status.size()}; - - // If we found a result then try to extract the pubkey and process it - if (ed25519PublicKey && ed25519PublicKey->size() == 64 && - oxenc::is_hex(*ed25519PublicKey)) { - session::onionreq::ed25519_pubkey edpk = - session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); - auto edpk_view = to_unsigned_sv(edpk.view()); - - auto snode_it = std::find_if( - updated_path.nodes.begin(), - updated_path.nodes.end(), - [&edpk_view](const auto& node) { - return node.view_remote_key() == edpk_view; - }); - - // If we found an invalid node then store it to increment the failure count - if (snode_it != updated_path.nodes.end()) { - found_invalid_node = true; + // Retrieve the path for the connection_info (no paths share the same guard node so we can use + // that to find it) + auto path_it = std::find_if( + paths[info.path_type].begin(), + paths[info.path_type].end(), + [guard_node = conn_info.node](const auto& path) { + return !path.nodes.empty() && path.nodes.front() == guard_node; + }); - auto failure_count = - updated_failure_counts.try_emplace(snode_it->to_string(), 0) - .first->second; - updated_failure_counts[snode_it->to_string()] = failure_count + 1; + // If the path was already dropped then the snode pool would have already been + // updated so just log the failure and call the callback + if (path_it == paths[info.path_type].end()) { + log::info( + cat, "Request {} failed but {} path already dropped.", info.request_id, path_name); - // If the specific node has failed too many times then we should try to repair - // the existing path by replace the bad node with another one - if (failure_count + 1 >= snode_failure_threshold) { - nodes_to_drop.emplace_back(*snode_it); + if (handle_response) + (*handle_response)(false, false, status_code, response); + return; + } - try { - // If the node that's gone bad is the guard node then we just have to - // drop the path - if (snode_it == updated_path.nodes.begin()) - throw std::runtime_error{"Cannot recover if guard node is bad"}; - - // Try to find an unused node to patch the path - auto unused_snodes = snode_pool; - auto path_nodes = all_path_nodes(); - - unused_snodes.erase( - std::remove_if( - unused_snodes.begin(), - unused_snodes.end(), - [&](const service_node& node) { - return std::find( - path_nodes.begin(), - path_nodes.end(), - node) != path_nodes.end(); - }), - unused_snodes.end()); - - if (unused_snodes.empty()) - throw std::runtime_error{"No remaining nodes"}; - - CSRNG rng; - std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); - - std::replace( - updated_path.nodes.begin(), - updated_path.nodes.end(), - *snode_it, - unused_snodes.front()); - log::info( - cat, - "Found bad node in path for {}, replacing node.", - path_type_name(path_type, single_path_mode)); - } catch (...) { - // There aren't enough unused nodes remaining so we need to drop the - // path - updated_path.failure_count = path_failure_threshold; + // Update the failure counts and paths + auto updated_path = *path_it; + auto updated_failure_counts = snode_failure_counts; + bool found_invalid_node = false; + std::vector nodes_to_drop; + + if (response) { + std::optional ed25519PublicKey; + + // Check if the response has one of the 'node_not_found' prefixes + if (response->starts_with(node_not_found_prefix)) + ed25519PublicKey = {response->data() + node_not_found_prefix.size()}; + else if (response->starts_with(node_not_found_prefix_no_status)) + ed25519PublicKey = {response->data() + node_not_found_prefix_no_status.size()}; + + // If we found a result then try to extract the pubkey and process it + if (ed25519PublicKey && ed25519PublicKey->size() == 64 && + oxenc::is_hex(*ed25519PublicKey)) { + session::onionreq::ed25519_pubkey edpk = + session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); + auto edpk_view = to_unsigned_sv(edpk.view()); + + auto snode_it = std::find_if( + updated_path.nodes.begin(), + updated_path.nodes.end(), + [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); + + // If we found an invalid node then store it to increment the failure count + if (snode_it != updated_path.nodes.end()) { + found_invalid_node = true; + + auto failure_count = updated_failure_counts[snode_it->to_string()]; + updated_failure_counts[snode_it->to_string()] = failure_count + 1; + + // If the specific node has failed too many times then we should try to repair + // the existing path by replace the bad node with another one + if (failure_count + 1 >= snode_failure_threshold) { + nodes_to_drop.emplace_back(*snode_it); - log::info( - cat, - "Unable to replace bad node in path for {}.", - path_type_name(path_type, single_path_mode)); - } + try { + // If the node that's gone bad is the guard node then we just have to + // drop the path + if (snode_it == updated_path.nodes.begin()) + throw std::runtime_error{"Cannot recover if guard node is bad"}; + + // Try to find an unused node to patch the path + std::vector unused_snodes; + std::vector existing_path_node_ips = all_path_ips(); + + std::copy_if( + snode_cache.begin(), + snode_cache.end(), + std::back_inserter(unused_snodes), + [&existing_path_node_ips](const auto& node) { + return std::find( + existing_path_node_ips.begin(), + existing_path_node_ips.end(), + node.to_ipv4()) == existing_path_node_ips.end(); + }); + + if (unused_snodes.empty()) + throw std::runtime_error{"No remaining nodes"}; + + CSRNG rng; + std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); + + std::replace( + updated_path.nodes.begin(), + updated_path.nodes.end(), + *snode_it, + unused_snodes.front()); + log::info(cat, "Found bad node in {} path, replacing node.", path_name); + } catch (...) { + // There aren't enough unused nodes remaining so we need to drop the + // path + updated_path.failure_count = path_failure_threshold; + log::info(cat, "Unable to replace bad node in {} path.", path_name); } } } } + } - // If we didn't find the specific node or the paths connection was closed then increment the - // path failure count - if (!found_invalid_node || !updated_path.conn_info.is_valid()) { - updated_path.failure_count += 1; - - // If the path has failed too many times we want to drop the guard snode (marking it as - // invalid) and increment the failure count of each node in the path) - if (updated_path.failure_count >= path_failure_threshold) { - for (auto& it : updated_path.nodes) { - auto failure_count = - updated_failure_counts.try_emplace(it.to_string(), 0).first->second; - updated_failure_counts[it.to_string()] = failure_count + 1; - - if (failure_count + 1 >= snode_failure_threshold) - nodes_to_drop.emplace_back(it); - } + // If we didn't find the specific node or the paths connection was closed then increment the + // path failure count + if (!found_invalid_node || !updated_path.conn_info.is_valid()) { + updated_path.failure_count += 1; - // Set the failure count of the guard node to match the threshold so we drop it - updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; - nodes_to_drop.emplace_back(updated_path.nodes[0]); - } else if (updated_path.nodes.size() < path_size) { - // If the path doesn't have enough nodes then it's likely that this failure was - // triggered when trying to establish a new path and, as such, we should increase - // the failure count of the guard node since it is probably invalid - auto failure_count = - updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) - .first->second; - updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; + // If the path has failed too many times we want to drop the guard snode (marking it as + // invalid) and increment the failure count of each node in the path) + if (updated_path.failure_count >= path_failure_threshold) { + for (auto& it : updated_path.nodes) { + auto failure_count = updated_failure_counts[it.to_string()]; + updated_failure_counts[it.to_string()] = failure_count + 1; if (failure_count + 1 >= snode_failure_threshold) - nodes_to_drop.emplace_back(updated_path.nodes[0]); - } - } - - // Remove any nodes from 'nodes_to_drop' which don't actually need to be dropped - bool requires_swarm_cache_update = false; - - if (!nodes_to_drop.empty()) { - for (auto& [key, nodes] : updated_swarm_cache) { - for (const auto& drop_node : nodes_to_drop) { - auto it = std::remove(nodes.begin(), nodes.end(), drop_node); - if (it != nodes.end()) { - nodes.erase(it, nodes.end()); - requires_swarm_cache_update = true; - } - } - } - } - - // No need to track the failure counts of nodes which have been dropped, or haven't failed - std::erase_if(updated_failure_counts, [](const auto& item) { - return item.second == 0 || item.second >= snode_failure_threshold; - }); - - // Drop the path if invalid - auto already_handled_failure = false; - - if (updated_path.failure_count >= path_failure_threshold) { - auto old_paths_size = paths_for_type(path_type).size(); - - // Close the connection immediately (just in case there are other requests happening) - if (old_path.conn_info.conn) - old_path.conn_info.conn->close_connection(); - - old_path.conn_info.conn.reset(); - old_path.conn_info.stream.reset(); - - switch (path_type) { - case PathType::standard: - standard_paths.erase( - std::remove(standard_paths.begin(), standard_paths.end(), old_path), - standard_paths.end()); - break; - - case PathType::upload: - upload_paths.erase( - std::remove(upload_paths.begin(), upload_paths.end(), old_path), - upload_paths.end()); - break; - - case PathType::download: - download_paths.erase( - std::remove(download_paths.begin(), download_paths.end(), old_path), - download_paths.end()); - break; + nodes_to_drop.emplace_back(it); } - std::vector node_descriptions; - std::transform( - old_path.nodes.begin(), - old_path.nodes.end(), - std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - auto new_paths_size = paths_for_type(path_type).size(); - - if (new_paths_size != old_paths_size) - log::info( - cat, - "Dropping path of type {} for {}: [{}]", - path_type_name(path_type, single_path_mode), - request_id, - path_description); - else { - // If the path was already dropped then the snode pool would have already been - // updated so update the `already_handled_failure` to avoid double-handling the - // failure - already_handled_failure = true; - log::info( - cat, - "Path of type: {} already dropped for {}: [{}]", - path_type_name(path_type, single_path_mode), - request_id, - path_description); - } - } else { - switch (path_type) { - case PathType::standard: - std::replace( - standard_paths.begin(), standard_paths.end(), old_path, updated_path); - break; - - case PathType::upload: - std::replace(upload_paths.begin(), upload_paths.end(), old_path, updated_path); - break; - - case PathType::download: - std::replace( - download_paths.begin(), download_paths.end(), old_path, updated_path); - break; - } + // Set the failure count of the guard node to match the threshold so we drop it + updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; + nodes_to_drop.emplace_back(updated_path.nodes[0]); + } else if (updated_path.nodes.size() < path_size) { + // If the path doesn't have enough nodes then it's likely that this failure was + // triggered when trying to establish a new path and, as such, we should increase + // the failure count of the guard node since it is probably invalid + auto failure_count = updated_failure_counts[updated_path.nodes[0].to_string()]; + updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; + + if (failure_count + 1 >= snode_failure_threshold) + nodes_to_drop.emplace_back(updated_path.nodes[0]); } + } - // We've already handled the failure so don't update the cache - if (already_handled_failure) { - { - std::lock_guard lock(mtx); - done = true; + // Remove any nodes from 'nodes_to_drop' which don't actually need to be dropped + auto updated_swarm_cache = swarm_cache; + bool requires_swarm_cache_update = false; + + if (!nodes_to_drop.empty()) { + for (auto& [key, nodes] : updated_swarm_cache) { + for (const auto& drop_node : nodes_to_drop) { + auto it = std::remove(nodes.begin(), nodes.end(), drop_node); + if (it != nodes.end()) { + nodes.erase(it, nodes.end()); + requires_swarm_cache_update = true; + } } - cv.notify_one(); - return; } + } - // Update the network status if we've removed all standard paths - if (standard_paths.empty()) - update_status(ConnectionStatus::disconnected); - - // Update the snode cache - { - std::lock_guard lock{snode_cache_mutex}; + // No need to track the failure counts of nodes which have been dropped, or haven't failed + std::erase_if(updated_failure_counts, [](const auto& item) { + return item.second == 0 || item.second >= snode_failure_threshold; + }); - // Update the snode failure counts - snode_failure_counts = updated_failure_counts; - need_failure_counts_write = true; - need_swarm_write = requires_swarm_cache_update; + // Drop the path if invalid + if (updated_path.failure_count >= path_failure_threshold) { + // Close the connection immediately (just in case there are other requests happening) + if (path_it->conn_info.conn) + path_it->conn_info.conn->close_connection(); + + auto path_nodes = path_it->nodes; + path_it->conn_info.conn.reset(); + path_it->conn_info.stream.reset(); + paths[info.path_type].erase(path_it); + + std::vector node_descriptions; + std::transform( + path_nodes.begin(), + path_nodes.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + log::info(cat, "Dropping {} path ({}): [{}]", path_name, info.request_id, path_description); + } else + std::replace( + paths[info.path_type].begin(), paths[info.path_type].end(), *path_it, updated_path); + + // Update the network status if we've removed all standard paths + if (paths[PathType::standard].empty()) + update_status(ConnectionStatus::disconnected); - if (requires_swarm_cache_update) - swarm_cache = updated_swarm_cache; + // Update the snode cache + { + std::lock_guard lock{snode_cache_mutex}; - for (const auto& node : nodes_to_drop) { - snode_pool.erase( - std::remove(snode_pool.begin(), snode_pool.end(), node), snode_pool.end()); - need_pool_write = true; - } + // Update the snode failure counts + snode_failure_counts = updated_failure_counts; + need_failure_counts_write = true; + need_swarm_write = requires_swarm_cache_update; - need_write = true; - } + if (requires_swarm_cache_update) + swarm_cache = updated_swarm_cache; - // Now that the cache is updated we can unblock this thread - { - std::lock_guard lock(mtx); - done = true; + for (const auto& node : nodes_to_drop) { + snode_cache.erase( + std::remove(snode_cache.begin(), snode_cache.end(), node), snode_cache.end()); + need_pool_write = true; } - cv.notify_one(); - // We also need to notify the disk write thread that it can persist the changes to disk - snode_cache_cv.notify_one(); - }); - - // Wait for the failure states to complete updating before triggering the callback - { - std::unique_lock lock(mtx); - cv.wait(lock, [&] { return done; }); + need_write = true; } + snode_cache_cv.notify_one(); if (handle_response) (*handle_response)(false, false, status_code, response); @@ -3145,6 +2877,7 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( std::memcpy(ip.data(), node.ip, ip.size()); unbox(network).send_onion_request( + PathType::standard, service_node{ oxenc::from_hex({node.ed25519_pubkey_hex, 64}), {0}, // For a destination node we don't care about the version @@ -3190,6 +2923,7 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( body = {body_, body_size}; unbox(network).send_onion_request( + PathType::standard, convert_server_destination(server), body, std::nullopt, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ba1a0fff..d359c9ce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release") add_definitions(-DRELEASE_BUILD) endif() -add_executable(testAll +set(LIB_SESSION_UTESTS_SOURCES test_blinding.cpp test_bt_merge.cpp test_bugs.cpp @@ -23,14 +23,18 @@ add_executable(testAll test_hash.cpp test_logging.cpp test_multi_encrypt.cpp - test_network.cpp - test_onionreq.cpp test_proto.cpp test_random.cpp test_session_encrypt.cpp test_xed25519.cpp ) +if (ENABLE_ONIONREQ) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_network.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) +endif() + +add_executable(testAll ${LIB_SESSION_UTESTS_SOURCES}) add_library(Catch2Wrapper INTERFACE) target_link_libraries(Catch2Wrapper INTERFACE Catch2::Catch2WithMain) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index baf2ef53..a4db9464 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include #include @@ -32,14 +34,15 @@ class TestNetwork { bool pre_build_paths) : network(cache_path, use_testnet, single_path_mode, pre_build_paths) {} - void set_snode_pool(std::vector pool) { network.snode_pool = pool; } + void set_snode_cache(std::vector cache) { + // Need to set the `last_snode_cache_update` to `1s` ago because otherwise it'll be + // considered invalid when checking the cache validity + network.snode_cache = cache; + network.last_snode_cache_update = (std::chrono::system_clock::now() - 10s); + } void set_paths(PathType path_type, std::vector paths) { - switch (path_type) { - case PathType::standard: network.standard_paths = paths; break; - case PathType::upload: network.upload_paths = paths; break; - case PathType::download: network.download_paths = paths; break; - } + network.paths[path_type] = paths; } void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { @@ -61,7 +64,7 @@ class TestNetwork { } uint8_t get_failure_count(PathType path_type, onion_path path) { - auto current_paths = network.paths_for_type(path_type); + auto current_paths = network.paths[path_type]; auto target_path = std::find_if( current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { return path_it.nodes[0] == path.nodes[0]; @@ -73,18 +76,16 @@ class TestNetwork { return 0; } - std::vector paths_for(PathType path_type) { - return network.paths_for_type(path_type); - } + std::vector paths_for(PathType path_type) { return network.paths[path_type]; } void handle_errors( request_info info, - onion_path path, + connection_info conn_info, bool timeout, std::optional status_code, std::optional response, std::optional handle_response) { - network.handle_errors(info, path, timeout, status_code, response, handle_response); + network.handle_errors(info, conn_info, timeout, status_code, response, handle_response); } }; } // namespace session::network @@ -149,7 +150,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, code, std::nullopt, @@ -178,7 +179,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, 500, std::nullopt, @@ -206,7 +207,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, 500, std::nullopt, @@ -239,7 +240,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, 500, response, @@ -262,14 +263,14 @@ TEST_CASE("Network error handling", "[network]") { 1); // Incremented because conn_info is invalid // Check general error handling with a path and specific node failure (too many failures) - network.set_snode_pool({target, target2, target3, target4}); + network.set_snode_cache({target, target2, target3, target4}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 9); network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, 500, response, @@ -298,7 +299,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - path, + {target, nullptr, nullptr}, false, 421, std::nullopt, @@ -335,7 +336,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request2, - path, + {target, nullptr, nullptr}, false, 421, std::nullopt, @@ -355,9 +356,10 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check the retry request of a 421 with non-swarm response data is handled like any other error + network.set_paths(PathType::standard, {path}); network.handle_errors( mock_request2, - path, + {target, nullptr, nullptr}, false, 421, "Test", @@ -399,7 +401,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request2, - path, + {target, nullptr, nullptr}, false, 421, response, @@ -439,7 +441,7 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt}; network.handle_errors( mock_request3, - path, + {target, nullptr, nullptr}, true, std::nullopt, "Test", @@ -462,7 +464,7 @@ TEST_CASE("Network error handling", "[network]") { // error and doesn't affect the failure count network.handle_errors( mock_request3, - path, + {target, nullptr, nullptr}, false, std::nullopt, "500 Internal Server Error", @@ -524,7 +526,7 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { TEST_CASE("Network direct request C API", "[network_send_request][network]") { network_object* network; - network_init(&network, nullptr, true, true, false, nullptr); + REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); std::array target_ip = {144, 76, 164, 202}; auto test_service_node = network_service_node{}; test_service_node.quic_port = 35400; From bf0076d8ac624fb3c79acb9c7fcc0f1a5ed76eed Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Aug 2024 08:52:46 +1000 Subject: [PATCH 369/572] PR feedback tweaks round 1 --- include/session/file.hpp | 2 +- include/session/network.hpp | 40 ++++-- include/session/onionreq/builder.hpp | 3 +- src/network.cpp | 188 +++++++++++++-------------- tests/test_network.cpp | 12 +- 5 files changed, 130 insertions(+), 115 deletions(-) diff --git a/include/session/file.hpp b/include/session/file.hpp index 4a7f99f1..a2bf747d 100644 --- a/include/session/file.hpp +++ b/include/session/file.hpp @@ -23,6 +23,6 @@ std::ifstream open_for_reading(const fs::path& filename); std::string read_whole_file(const fs::path& filename); /// Dumps (binary) string contents to disk. The file is overwritten if it already exists. -void write_whole_file(const fs::path& filename, std::string_view contents); +void write_whole_file(const fs::path& filename, std::string_view contents = ""); } // namespace session diff --git a/include/session/network.hpp b/include/session/network.hpp index 29062673..c3f27751 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -5,6 +5,7 @@ #include "onionreq/builder.hpp" #include "onionreq/key_types.hpp" #include "platform.hpp" +#include "session/random.hpp" #include "types.hpp" namespace session::network { @@ -82,7 +83,25 @@ struct onion_path { } }; +namespace detail { + std::optional node_for_destination(onionreq::network_destination destination); + + session::onionreq::x25519_pubkey pubkey_for_destination( + onionreq::network_destination destination); + +} // namespace detail + struct request_info { + static request_info make( + onionreq::network_destination _dest, + std::chrono::milliseconds _timeout, + std::optional _original_body, + std::optional _swarm_pk, + PathType _type = PathType::standard, + std::optional endpoint = "onion_req", + std::optional _req_id = std::nullopt, + std::optional _body = std::nullopt); + enum class RetryReason { decryption_failure, redirect, @@ -96,11 +115,12 @@ struct request_info { std::optional swarm_pubkey; PathType path_type; std::chrono::milliseconds timeout; - bool node_destination; /// The reason we are retrying the request (if it's a retry). Generally only used for internal /// purposes (like receiving a `421`) in order to prevent subsequent retries. - std::optional retry_reason; + std::optional retry_reason{}; + + bool node_destination{detail::node_for_destination(destination).has_value()}; }; class Network { @@ -251,20 +271,13 @@ class Network { /// Should be NULL if the request is not associated with a swarm. /// - `timeout` -- [in] timeout in milliseconds to use for the request. /// - `handle_response` -- [in] callback to be called with the result of the request. - void send_onion_request( - PathType type, - onionreq::network_destination destination, - std::optional body, - std::optional swarm_pubkey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response); void send_onion_request( onionreq::network_destination destination, std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - network_response_callback_t handle_response); - void send_onion_request(request_info info, network_response_callback_t handle_response); + network_response_callback_t handle_response, + PathType type = PathType::standard); /// API: network/upload_file_to_server /// @@ -329,6 +342,11 @@ class Network { network_response_callback_t handle_response); private: + /// API: network/_send_onion_request + /// + /// Internal function invoked by ::send_onion_request after request_info construction + void _send_onion_request(request_info info, network_response_callback_t handle_response); + /// API: network/all_path_ips /// /// Internal function to retrieve all of the node ips current used in paths diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index e1a17afe..4acf0065 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -9,7 +9,8 @@ namespace session::network { struct service_node; -} +struct request_info; +} // namespace session::network namespace session::onionreq { diff --git a/src/network.cpp b/src/network.cpp index 53210ff9..2ccd4bfc 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -25,7 +25,6 @@ #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/onionreq/response_parser.hpp" -#include "session/random.hpp" #include "session/util.hpp" using namespace oxen; @@ -92,6 +91,7 @@ namespace { constexpr auto node_not_found_prefix = "502 Bad Gateway\n\nNext node not found: "sv; constexpr auto node_not_found_prefix_no_status = "Next node not found: "sv; constexpr auto ALPN = "oxenstorage"sv; + constexpr auto ONION = "onion_req"; std::string path_type_name(PathType path_type, bool single_path_mode) { if (single_path_mode) @@ -234,23 +234,6 @@ namespace { return session::onionreq::x25519_pubkey::from_bytes({xpk.data(), 32}); } - std::optional node_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) - return *dest; - - return std::nullopt; - } - - session::onionreq::x25519_pubkey pubkey_for_destination(network_destination destination) { - if (auto* dest = std::get_if(&destination)) - return compute_xpk(dest->view_remote_key()); - - if (auto* dest = std::get_if(&destination)) - return dest->x25519_pubkey; - - throw std::runtime_error{"Invalid destination."}; - } - std::string consume_string(oxenc::bt_dict_consumer dict, std::string_view key) { if (!dict.skip_until(key)) throw std::invalid_argument{ @@ -273,6 +256,45 @@ namespace { } } // namespace +namespace detail { + std::optional node_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return *dest; + + return std::nullopt; + } + + session::onionreq::x25519_pubkey pubkey_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return compute_xpk(dest->view_remote_key()); + + if (auto* dest = std::get_if(&destination)) + return dest->x25519_pubkey; + + throw std::runtime_error{"Invalid destination."}; + } +} // namespace detail + +request_info request_info::make( + onionreq::network_destination _dest, + std::chrono::milliseconds _timeout, + std::optional _original_body, + std::optional _swarm_pk, + PathType _type, + std::optional _req_id, + std::optional _ep, + std::optional _body) { + return request_info{ + _req_id.value_or(random::random_base32(4)), + std::move(_dest), + _ep.value_or(ONION), + std::move(_body), + std::move(_original_body), + std::move(_swarm_pk), + _type, + _timeout}; +} + // MARK: Initialization Network::Network( @@ -315,8 +337,7 @@ void Network::load_cache_from_disk() { try { // If the cache is for the wrong network then delete everything auto testnet_stub = cache_path / file_testnet; - bool cache_is_for_testnet = fs::exists(testnet_stub); - if (use_testnet != cache_is_for_testnet && fs::exists(cache_path)) + if (use_testnet != fs::exists(testnet_stub) && fs::exists(testnet_stub)) fs::remove_all(cache_path); // Create the cache directory (and swarm_dir, inside it) if needed @@ -325,15 +346,15 @@ void Network::load_cache_from_disk() { // If we are using testnet then create a file to indicate that if (use_testnet) - write_whole_file(testnet_stub, ""); + write_whole_file(testnet_stub); // Load the last time the snode pool was updated // // Note: We aren't just reading the write time of the file because Apple consider // accessing file timestamps a method that can be used to track the user (and we // want to avoid being flagged as using such) - auto last_updated_path = cache_path / file_snode_pool_updated; - if (fs::exists(last_updated_path)) { + if (auto last_updated_path = cache_path / file_snode_pool_updated; + fs::exists(last_updated_path)) { try { auto timestamp_str = read_whole_file(last_updated_path); while (timestamp_str.ends_with('\n')) @@ -350,8 +371,7 @@ void Network::load_cache_from_disk() { } // Load the snode pool - auto pool_path = cache_path / file_snode_pool; - if (fs::exists(pool_path)) { + if (auto pool_path = cache_path / file_snode_pool; fs::exists(pool_path)) { auto file = open_for_reading(pool_path); std::vector loaded_cache; std::string line; @@ -373,8 +393,8 @@ void Network::load_cache_from_disk() { } // Load the failure counts - auto failure_counts_path = cache_path / file_snode_failure_counts; - if (fs::exists(failure_counts_path)) { + if (auto failure_counts_path = cache_path / file_snode_failure_counts; + fs::exists(failure_counts_path)) { auto file = open_for_reading(failure_counts_path); std::unordered_map loaded_failure_count; std::string line; @@ -392,7 +412,12 @@ void Network::load_cache_from_disk() { throw std::invalid_argument{ "Invalid failure count serialization: invalid failure count"}; - loaded_failure_count[std::string(parts[0])] = failure_count; + // If we somehow already have a value then we should use whichever has the + // larger failure count (want to avoid keeping a bad node around longer than + // needed) + if (loaded_failure_count.try_emplace(std::string(parts[0]), failure_count) + .first->second < failure_count) + loaded_failure_count[std::string(parts[0])] = failure_count; } catch (...) { ++invalid_entries; } @@ -415,13 +440,13 @@ void Network::load_cache_from_disk() { for (auto& entry : fs::directory_iterator(swarm_path)) { // If the pubkey was valid then process the content - auto file = open_for_reading(entry.path()); + const auto& path = entry.path(); + auto file = open_for_reading(path); std::vector nodes; std::string line; bool checked_swarm_expiration = false; std::chrono::seconds swarm_lifetime = 0s; - const auto& path = entry.path(); - std::string filename{convert_sv(path.filename().u8string())}; + auto filename = path.filename().string(); while (std::getline(file, line)) { try { @@ -512,9 +537,8 @@ void Network::disk_write_thread_loop() { // Save the snode pool to disk if (need_pool_write) { - auto pool_path = cache_path / file_snode_pool; - auto pool_tmp = pool_path; - pool_tmp += u8"_new"; + auto pool_path = cache_path / file_snode_pool, + pool_tmp = pool_path / u8"_new"; { std::stringstream ss; @@ -532,6 +556,7 @@ void Network::disk_write_thread_loop() { cache_path / file_snode_pool_updated, "{}"_format(std::chrono::system_clock::to_time_t( last_pool_update_write))); + need_pool_write = false; log::debug(cat, "Finished writing snode pool cache to disk."); } @@ -551,6 +576,7 @@ void Network::disk_write_thread_loop() { } fs::rename(failure_counts_tmp, failure_counts_path); + need_failure_counts_write = false; log::debug(cat, "Finished writing snode failure counts to disk."); } @@ -578,12 +604,10 @@ void Network::disk_write_thread_loop() { fs::rename(swarm_tmp, swarm_path); } + need_swarm_write = false; log::debug(cat, "Finished writing swarm cache to disk."); } - need_pool_write = false; - need_failure_counts_write = false; - need_swarm_write = false; need_write = false; } catch (const std::exception& e) { log::error(cat, "Failed to write snode cache: {}", e.what()); @@ -948,7 +972,7 @@ void Network::resume_queues() { } net.call_soon([this, info = request.first, cb = std::move(request.second)]() { - send_onion_request(info, cb); + _send_onion_request(std::move(info), std::move(cb)); }); return true; }); @@ -1410,7 +1434,7 @@ std::optional Network::find_valid_path( // If the request destination is a node then only select a path that doesn't include the IP of // the destination - if (auto target = node_for_destination(info.destination)) { + if (auto target = detail::node_for_destination(info.destination)) { std::vector ip_excluded_paths; std::copy_if( possible_paths.begin(), @@ -1686,19 +1710,16 @@ void Network::get_swarm( {"method", "get_swarm"}, {"params", params}, }; - request_info info{ - request_id, + auto info = request_info::make( random_cache.front(), - "onion_req", - std::nullopt, // Will be generated after retrieving the path + quic::DEFAULT_TIMEOUT, ustring{quic::to_usv(payload.dump())}, swarm_pubkey, PathType::standard, - quic::DEFAULT_TIMEOUT, - true, - std::nullopt}; + std::nullopt, + request_id); - send_onion_request( + _send_onion_request( info, [this, swarm_pubkey, request_id, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { @@ -1728,10 +1749,10 @@ void Network::get_swarm( // Update the cache log::info(cat, "Retrieved swarm for {} ({}).", swarm_pubkey.hex(), request_id); - net.call([this, swarm_pubkey, swarm]() mutable { + net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey.hex()] = swarm; + swarm_cache[hex_key] = swarm; need_swarm_write = true; need_write = true; } @@ -1745,10 +1766,10 @@ void Network::get_swarm( void Network::set_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { - net.call([this, swarm_pubkey, swarm]() mutable { + net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; - swarm_cache[swarm_pubkey.hex()] = swarm; + swarm_cache[hex_key] = swarm; need_swarm_write = true; need_write = true; } @@ -1815,34 +1836,19 @@ void Network::send_onion_request( std::optional body, std::optional swarm_pubkey, std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { - send_onion_request( - PathType::standard, destination, body, swarm_pubkey, timeout, handle_response); + network_response_callback_t handle_response, + PathType type) { + _send_onion_request( + request_info::make( + std::move(destination), + timeout, + std::move(body), + std::move(swarm_pubkey), + type), + std::move(handle_response)); } -void Network::send_onion_request( - PathType path_type, - network_destination destination, - std::optional body, - std::optional swarm_pubkey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { - request_info info{ - random::random_base32(4), - destination, - "onion_req", - std::nullopt, // Will be generated after retrieving the path - body, - swarm_pubkey, - path_type, - timeout, - node_for_destination(destination).has_value(), - std::nullopt}; - - send_onion_request(info, handle_response); -} - -void Network::send_onion_request(request_info info, network_response_callback_t handle_response) { +void Network::_send_onion_request(request_info info, network_response_callback_t handle_response) { auto path_name = path_type_name(info.path_type, single_path_mode); log::trace(cat, "{} called for {} path ({}).", __PRETTY_FUNCTION__, path_name, info.request_id); @@ -1879,7 +1885,7 @@ void Network::send_onion_request(request_info info, network_response_callback_t auto builder = Builder(); try { builder.set_destination(info.destination); - builder.set_destination_pubkey(pubkey_for_destination(info.destination)); + builder.set_destination_pubkey(detail::pubkey_for_destination(info.destination)); for (auto& node : path->nodes) builder.add_hop( @@ -2036,7 +2042,6 @@ void Network::upload_file_to_server( headers.emplace_back("Content-Type", "application/octet-stream"); send_onion_request( - PathType::upload, ServerDestination{ server.protocol, server.host, @@ -2048,7 +2053,8 @@ void Network::upload_file_to_server( data, std::nullopt, timeout, - handle_response); + handle_response, + PathType::upload); } void Network::download_file( @@ -2072,7 +2078,7 @@ void Network::download_file( std::chrono::milliseconds timeout, network_response_callback_t handle_response) { send_onion_request( - PathType::download, server, std::nullopt, std::nullopt, timeout, handle_response); + server, std::nullopt, std::nullopt, timeout, handle_response, PathType::download); } void Network::get_client_version( @@ -2109,13 +2115,13 @@ void Network::get_client_version( headers.emplace_back("X-FS-Signature", oxenc::to_base64(signature)); send_onion_request( - PathType::standard, ServerDestination{ "http", std::string(file_server), endpoint, pubkey, 80, headers, "GET"}, std::nullopt, pubkey, timeout, - handle_response); + handle_response, + PathType::standard); } // MARK: Response Handling @@ -2248,16 +2254,8 @@ std::pair Network::validate_response(quic::message resp, void Network::handle_node_error( service_node node, PathType path_type, connection_info conn_info, std::string request_id) { handle_errors( - {request_id, - node, - "", - std::nullopt, - std::nullopt, - std::nullopt, - path_type, - 0ms, - false, - std::nullopt}, + request_info::make( + std::move(node), 0ms, std::nullopt, std::nullopt, path_type, request_id, ""), conn_info, false, std::nullopt, @@ -2363,7 +2361,7 @@ void Network::handle_errors( try { // If there is no response handler or no swarm information was provided then we // should just replace the swarm - auto target = node_for_destination(info.destination); + auto target = detail::node_for_destination(info.destination); if (!handle_response || !info.swarm_pubkey || !target) throw std::invalid_argument{"Unable to handle redirect."}; @@ -2877,7 +2875,6 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( std::memcpy(ip.data(), node.ip, ip.size()); unbox(network).send_onion_request( - PathType::standard, service_node{ oxenc::from_hex({node.ed25519_pubkey_hex, 64}), {0}, // For a destination node we don't care about the version @@ -2923,7 +2920,6 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( body = {body_, body_size}; unbox(network).send_onion_request( - PathType::standard, convert_server_destination(server), body, std::nullopt, diff --git a/tests/test_network.cpp b/tests/test_network.cpp index a4db9464..c93dbda4 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -135,8 +135,8 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt, PathType::standard, 0ms, - true, - std::nullopt}; + std::nullopt, + true}; Result result; auto network = TestNetwork(std::nullopt, true, true, false); @@ -328,8 +328,8 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, - true, - request_info::RetryReason::redirect}; + request_info::RetryReason::redirect, + true}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); @@ -437,8 +437,8 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, - false, - std::nullopt}; + std::nullopt, + false}; network.handle_errors( mock_request3, {target, nullptr, nullptr}, From 367ab1f511eecc3e9d39661ef9dd81937602bb23 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Aug 2024 09:03:57 +1000 Subject: [PATCH 370/572] Attempt to fix a couple of CI build issues --- src/config/base.cpp | 2 +- src/network.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 733b9e50..dcf18bf0 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -383,7 +383,7 @@ ustring ConfigBase::make_dump() const { d.append("$", data_sv); d.append("(", _curr_hash); - d.append_list(")").append(_old_hashes.begin(), _old_hashes.end()); + d.append_list(")").extend(_old_hashes.begin(), _old_hashes.end()); if (auto extra = extra_data(); !extra.empty()) d.append_bt("+", std::move(extra)); diff --git a/src/network.cpp b/src/network.cpp index 2ccd4bfc..63acf440 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -252,7 +252,9 @@ namespace { std::chrono::milliseconds retry_delay( int num_failures, std::chrono::milliseconds max_delay_ms = 3000ms) { return std::chrono::milliseconds(std::min( - max_delay_ms.count(), static_cast(100 * std::pow(2, num_failures)))); + max_delay_ms.count(), + static_cast( + 100 * std::pow(2, num_failures)))); } } // namespace From 7467ee07352a9d4c8c50356050cd17664e707f61 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Aug 2024 09:16:21 +1000 Subject: [PATCH 371/572] Fixed another CI error --- src/config/contacts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 89291087..0fba7b55 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -41,7 +41,7 @@ contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { void contact_info::set_name(std::string n) { if (n.size() > MAX_NAME_LENGTH) - name = std::move(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); + name = utf8_truncate(std::move(n), MAX_NAME_LENGTH); else name = std::move(n); } From bd1ececcd9191a7094bea0f06ed7d901178e7312 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Aug 2024 18:08:02 +1000 Subject: [PATCH 372/572] Added a bunch of network unit tests, fixed some warnings --- include/session/network.hpp | 28 +- src/network.cpp | 72 ++-- tests/test_blinding.cpp | 9 - tests/test_config_userprofile.cpp | 46 --- tests/test_multi_encrypt.cpp | 4 +- tests/test_network.cpp | 652 ++++++++++++++++++++++++++---- tests/utils.hpp | 67 +++ 7 files changed, 690 insertions(+), 188 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index c3f27751..6e534a8e 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -189,7 +189,7 @@ class Network { bool use_testnet, bool single_path_mode, bool pre_build_paths); - ~Network(); + virtual ~Network(); /// API: network/suspend /// @@ -383,6 +383,18 @@ class Network { /// - 'updated_status' - [in] the updated connection status. void update_status(ConnectionStatus updated_status); + /// API: network/retry_delay + /// + /// A function which generates an exponential delay to wait before retrying a request/action + /// based on the provided failure count. + /// + /// Inputs: + /// - 'num_failures' - [in] the number of times the request has already failed. + /// - 'max_delay' - [in] the maximum amount of time to delay for. + virtual std::chrono::milliseconds retry_delay( + int num_failures, + std::chrono::milliseconds max_delay = std::chrono::milliseconds{3000}); + /// API: network/get_endpoint /// /// Retrieves or creates a new endpoint pointer. @@ -419,14 +431,14 @@ class Network { /// When most of these processes finish they call this function again to move through the next /// step in the process. Note: Due to this "looping" behaviour there is a built in throttling /// mechanism to avoid running the logic excessively. - void resume_queues(); + virtual void resume_queues(); /// API: network/refresh_snode_cache /// /// This function refreshes the snode cache. If the current cache is to small (or not present) /// this will fetch the cache from a random seed node, otherwise it will randomly pick a number /// of nodes and set the cache to the intersection of the results. - void refresh_snode_cache(); + virtual void refresh_snode_cache(); /// API: network/build_path /// @@ -437,7 +449,7 @@ class Network { /// - 'existing_request_id' - [in, optional] id for an existing build_path request. Generally /// this will only be set when retrying a path build. /// - `path_type` -- [in] the type of path to build. - void build_path(std::optional existing_request_id, PathType path_type); + virtual void build_path(std::optional existing_request_id, PathType path_type); /// API: network/recover_path /// @@ -448,7 +460,7 @@ class Network { /// Inputs: /// - `path_type` -- [in] the type for the provided path. /// - 'path' - [in] the path to try to reconnect to. - void recover_path(PathType path_type, onion_path path); + virtual void recover_path(PathType path_type, onion_path path); /// API: network/find_valid_path /// @@ -474,7 +486,7 @@ class Network { /// /// Inputs: /// - `path_type` -- [in] the type of path to be built. - void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable); + virtual void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable); /// API: network/get_service_nodes_recursive /// @@ -525,7 +537,7 @@ class Network { /// `quic::DEFAULT_TIMEOUT` will be used. /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an /// error occurs an empty list and an error will be provided. - void get_snode_version( + virtual void get_snode_version( std::string request_id, PathType path_type, service_node node, @@ -590,7 +602,7 @@ class Network { /// - `response` -- [in, optional] response data returned from the network. /// - `handle_response` -- [in, optional] callback to be called with updated response /// information after processing the error. - void handle_errors( + virtual void handle_errors( request_info info, connection_info conn_info, bool timeout, diff --git a/src/network.cpp b/src/network.cpp index 63acf440..30734748 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -248,14 +248,6 @@ namespace { "Unable to find entry in dict for key '" + std::string(key) + "'"}; return dict.next_integer().second; } - - std::chrono::milliseconds retry_delay( - int num_failures, std::chrono::milliseconds max_delay_ms = 3000ms) { - return std::chrono::milliseconds(std::min( - max_delay_ms.count(), - static_cast( - 100 * std::pow(2, num_failures)))); - } } // namespace namespace detail { @@ -698,6 +690,13 @@ void Network::update_status(ConnectionStatus updated_status) { status_changed(updated_status); } +std::chrono::milliseconds Network::retry_delay( + int num_failures, std::chrono::milliseconds max_delay) { + return std::chrono::milliseconds(std::min( + max_delay.count(), + static_cast(100 * std::pow(2, num_failures)))); +} + std::shared_ptr Network::get_endpoint() { return net.call_get([this]() mutable { if (!endpoint) @@ -832,19 +831,17 @@ void Network::resume_queues() { } last_resume_queues_timestamp = std::chrono::system_clock::now(); - // Determine the number of existing paths - std::vector existing_path_type_names; - for (const auto& [path_type, paths_for_type] : paths) - existing_path_type_names.insert( - existing_path_type_names.end(), - paths_for_type.size(), - path_type_name(path_type, single_path_mode)); - // Only generate the stats if we are actually going to log them if (log::get_level(cat) == log::Level::trace) { auto request_count = 0; + std::vector existing_path_type_names; std::vector pending_path_type_names; std::vector in_progress_path_type_names; + for (const auto& [path_type, paths_for_type] : paths) + existing_path_type_names.insert( + existing_path_type_names.end(), + paths_for_type.size(), + path_type_name(path_type, single_path_mode)); std::transform( path_build_queue.begin(), path_build_queue.end(), @@ -957,27 +954,28 @@ void Network::resume_queues() { // Now that we've scheduled any required path builds we should try to resume any pending // requests in case they now have a valid paths - if (existing_path_type_names.size() > 0) { + if (!paths.empty()) { std::unordered_set already_enqueued_paths; for (auto& [path_type, requests] : request_queue) - std::erase_if(requests, [this, &already_enqueued_paths](const auto& request) { - // If there are no valid paths to send the request then enqueue a new path build (if - // needed) leave the request in the queue - if (!find_valid_path(request.first, paths[request.first.path_type])) { - if (!already_enqueued_paths.contains(request.first.path_type)) { - already_enqueued_paths.insert(request.first.path_type); - enqueue_path_build_if_needed(request.first.path_type, true); + if (!paths[path_type].empty()) + std::erase_if(requests, [this, &already_enqueued_paths](const auto& request) { + // If there are no valid paths to send the request then enqueue a new path build + // (if needed) leave the request in the queue + if (!find_valid_path(request.first, paths[request.first.path_type])) { + if (!already_enqueued_paths.contains(request.first.path_type)) { + already_enqueued_paths.insert(request.first.path_type); + enqueue_path_build_if_needed(request.first.path_type, true); + } + net.call_soon([this]() { resume_queues(); }); + return false; } - net.call_soon([this]() { resume_queues(); }); - return false; - } - net.call_soon([this, info = request.first, cb = std::move(request.second)]() { - _send_onion_request(std::move(info), std::move(cb)); + net.call_soon([this, info = request.first, cb = std::move(request.second)]() { + _send_onion_request(std::move(info), std::move(cb)); + }); + return true; }); - return true; - }); } } @@ -1251,13 +1249,17 @@ void Network::build_path(std::optional existing_request_id, PathTyp target_node, 3s, [this, path_name, path_type, target_node, request_id]( - std::vector, connection_info info, std::optional error) { + std::vector version, + connection_info info, + std::optional error) { log::trace(cat, "Got snode version response for {}.", request_id); try { - if (error) + if (version.empty()) throw std::runtime_error{"Testing {} for {} failed with error: {}"_format( - target_node.to_string(), request_id, *error)}; + target_node.to_string(), + request_id, + error.value_or("Unknown Error"))}; // Build the new paths log::info( @@ -1870,7 +1872,7 @@ void Network::_send_onion_request(request_info info, network_response_callback_t // Currently there are no valid paths so enqueue a new build (if needed) and add the // request to the queue to be run one the path build completes - request_queue[info.path_type].emplace_back(info, std::move(cb)); + request_queue[info.path_type].emplace_back(std::move(info), std::move(cb)); enqueue_path_build_if_needed(info.path_type, true); net.call_soon([this]() { resume_queues(); }); return {std::nullopt, std::nullopt}; diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 56a7bc90..1fe320e6 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -26,9 +26,6 @@ constexpr std::array seed2{ 0x45, 0x44, 0xc1, 0xc5, 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, 0xe8, 0xaa, 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x81}; -constexpr ustring_view pub1{seed1.data() + 32, 32}; -constexpr ustring_view pub2{seed2.data() + 32, 32}; - constexpr std::array xpub1{ 0xfe, 0x94, 0xb7, 0xad, 0x4b, 0x7f, 0x1c, 0xc1, 0xbb, 0x92, 0x67, 0x1f, 0x1f, 0x0d, 0x24, 0x3f, 0x22, 0x6e, 0x11, 0x5b, 0x33, 0x77, @@ -40,12 +37,6 @@ constexpr std::array xpub2{ 0x78, 0x81, 0x96, 0x2c, 0x72, 0x36, 0x99, 0x15, 0x20, 0x73, }; -constexpr std::array pub2_abs{ - 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, 0x45, 0x44, 0xc1, 0xc5, - 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, 0xe8, 0xaa, - 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x01, -}; - const std::string session_id1 = "05" + oxenc::to_hex(xpub1.begin(), xpub1.end()); const std::string session_id2 = "05" + oxenc::to_hex(xpub2.begin(), xpub2.end()); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 267c2942..45872b1b 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -59,52 +59,6 @@ TEST_CASE("UserProfile", "[config][user_profile]") { "9012345🎂"); } -TEST_CASE("UserProfile", "[config][user_profile]") { - - 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)); - - session::config::UserProfile profile{ustring_view{seed}, std::nullopt}; - - CHECK_THROWS( - profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789" - "01" - "23456789012345678901234567890A")); - CHECK_NOTHROW( - profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" - "01" - "234567890123456789012345678901234567890A")); - CHECK(profile.get_name() == - "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" - "901234567890"); - CHECK_NOTHROW( - profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" - "01" - "234567890123456789012345678901234567🎂")); - CHECK(profile.get_name() == - "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" - "901234567"); - CHECK_NOTHROW( - profile.set_name_truncated("12345678901234567890123456789012345678901234567890123456789" - "01" - "2345678901234567890123456789012345🎂🎂")); - CHECK(profile.get_name() == - "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678" - "9012345🎂"); -} - TEST_CASE("user profile C API", "[config][user_profile][c]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 0c679622..c84cb1dc 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -36,7 +36,7 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; std::array x_keys; - for (int i = 0; i < seeds.size(); i++) + for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == @@ -200,7 +200,7 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; std::array x_keys; - for (int i = 0; i < seeds.size(); i++) + for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c93dbda4..139f5775 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "utils.hpp" @@ -20,51 +21,71 @@ struct Result { int16_t status_code; std::optional response; }; + } // namespace namespace session::network { -class TestNetwork { +class TestNetwork : public Network { public: - Network network; + std::unordered_map call_counts; + bool ignore_resume_queues = false; + bool ignore_get_snode_version = false; + std::chrono::milliseconds retry_delay_value = 0ms; + std::optional, connection_info, std::optional>> + get_snode_version_response; TestNetwork( std::optional cache_path, bool use_testnet, bool single_path_mode, bool pre_build_paths) : - network(cache_path, use_testnet, single_path_mode, pre_build_paths) {} + Network{cache_path, use_testnet, single_path_mode, pre_build_paths} { + paths_changed = [this](std::vector>) { + call_counts["paths_changed"]++; + }; + } + + void set_suspended(bool suspended_) { suspended = suspended_; } + + bool get_suspended() { return suspended; } + + ConnectionStatus get_status() { return status; } void set_snode_cache(std::vector cache) { // Need to set the `last_snode_cache_update` to `1s` ago because otherwise it'll be // considered invalid when checking the cache validity - network.snode_cache = cache; - network.last_snode_cache_update = (std::chrono::system_clock::now() - 10s); + snode_cache = cache; + last_snode_cache_update = (std::chrono::system_clock::now() - 10s); + } + + void add_path(PathType path_type, std::vector nodes) { + paths[path_type].emplace_back(onion_path{{nodes[0], nullptr, nullptr}, nodes, 0}); } - void set_paths(PathType path_type, std::vector paths) { - network.paths[path_type] = paths; + void set_paths(PathType path_type, std::vector paths_) { + paths[path_type] = paths_; } + std::vector get_paths(PathType path_type) { return paths[path_type]; } + void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { - network.set_swarm(swarm_pubkey, swarm); + Network::set_swarm(swarm_pubkey, swarm); } - void get_swarm( - session::onionreq::x25519_pubkey swarm_pubkey, - std::function swarm)> callback) { - network.get_swarm(swarm_pubkey, callback); + std::vector get_swarm_value(session::onionreq::x25519_pubkey swarm_pubkey) { + return swarm_cache[swarm_pubkey.hex()]; } void set_failure_count(service_node node, uint8_t failure_count) { - network.snode_failure_counts[node.to_string()] = failure_count; + snode_failure_counts[node.to_string()] = failure_count; } uint8_t get_failure_count(service_node node) { - return network.snode_failure_counts.try_emplace(node.to_string(), 0).first->second; + return snode_failure_counts.try_emplace(node.to_string(), 0).first->second; } uint8_t get_failure_count(PathType path_type, onion_path path) { - auto current_paths = network.paths[path_type]; + auto current_paths = paths[path_type]; auto target_path = std::find_if( current_paths.begin(), current_paths.end(), [&path](const auto& path_it) { return path_it.nodes[0] == path.nodes[0]; @@ -76,7 +97,101 @@ class TestNetwork { return 0; } - std::vector paths_for(PathType path_type) { return network.paths[path_type]; } + void add_in_progress_path_builds(std::string request_id, std::pair path_build) { + in_progress_path_builds[request_id] = path_build; + } + + void clear_in_progress_path_builds() { in_progress_path_builds.clear(); } + + std::unordered_map> get_in_progress_path_builds() { + return in_progress_path_builds; + } + + void set_path_build_queue(std::vector path_build_queue_) { + path_build_queue = path_build_queue_; + } + + std::vector get_path_build_queue() { return path_build_queue; } + + void set_general_path_build_failures(int general_path_build_failures_) { + general_path_build_failures = general_path_build_failures_; + } + + int get_general_path_build_failures() { return general_path_build_failures; } + + std::vector get_unused_path_build_nodes() { return unused_path_build_nodes; } + + void add_pending_request(PathType path_type, request_info info) { + request_queue[path_type].emplace_back( + info, [](bool, bool, int16_t, std::optional) {}); + } + + // Overridden Functions + + std::chrono::milliseconds retry_delay(int, std::chrono::milliseconds) override { + return retry_delay_value; + } + + void resume_queues() override { + call_counts["resume_queues"]++; + + if (ignore_resume_queues) + return; + + Network::resume_queues(); + } + + void build_path(std::optional existing_request_id, PathType path_type) override { + call_counts["build_path"]++; + + Network::build_path(existing_request_id, path_type); + } + + void get_snode_version( + std::string request_id, + PathType path_type, + service_node node, + std::optional timeout, + std::function< + void(std::vector version, + connection_info info, + std::optional error)> callback) override { + call_counts["get_snode_version"]++; + + if (ignore_get_snode_version) + return; + + if (get_snode_version_response) { + const auto& [version, info, error] = *get_snode_version_response; + return callback(version, info, error); + } + + Network::get_snode_version(request_id, path_type, node, timeout, callback); + } + + // Exposing Private Functions + + void establish_connection( + std::string request_id, + PathType path_type, + service_node target, + std::optional timeout, + std::function error)> callback) { + Network::establish_connection(request_id, path_type, target, timeout, std::move(callback)); + } + + std::optional find_valid_path(request_info info, std::vector paths) { + return Network::find_valid_path(info, paths); + } + + void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable) override { + return Network::enqueue_path_build_if_needed(path_type, existing_paths_unsuitable); + } + + void send_request( + request_info info, connection_info conn, network_response_callback_t handle_response) { + Network::send_request(info, conn, std::move(handle_response)); + } void handle_errors( request_info info, @@ -84,9 +199,17 @@ class TestNetwork { bool timeout, std::optional status_code, std::optional response, - std::optional handle_response) { - network.handle_errors(info, conn_info, timeout, status_code, response, handle_response); + std::optional handle_response) override { + call_counts["handle_errors"]++; + Network::handle_errors( + info, conn_info, timeout, status_code, response, std::move(handle_response)); } + + // Mocking Functions + + bool called(std::string func_name, int times = 1) { return (call_counts[func_name] >= times); } + + bool did_not_call(std::string func_name) { return !call_counts.contains(func_name); } }; } // namespace session::network @@ -139,6 +262,8 @@ TEST_CASE("Network error handling", "[network]") { true}; Result result; auto network = TestNetwork(std::nullopt, true, true, false); + network.set_suspended(true); // Make no requests in this test + network.ignore_resume_queues = true; // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; @@ -223,13 +348,10 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); - CHECK(network.get_failure_count(target) == 0); // Guard node will have been dropped - CHECK(network.get_failure_count(target2) == - 1); // Other nodes get their failure count incremented - CHECK(network.get_failure_count(target3) == - 1); // Other nodes get their failure count incremented - CHECK(network.get_failure_count(PathType::standard, path) == - 0); // Path will have been dropped and reset + CHECK(network.get_failure_count(target) == 0); // Guard node dropped + CHECK(network.get_failure_count(target2) == 1); // Other nodes incremented + CHECK(network.get_failure_count(target3) == 1); // Other nodes incremented + CHECK(network.get_failure_count(PathType::standard, path) == 0); // Path dropped and reset // Check general error handling with a path and specific node failure (first failure) path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; @@ -420,17 +542,26 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(target2) == 0); CHECK(network.get_failure_count(target3) == 0); CHECK(network.get_failure_count(PathType::standard, path) == 0); - - network.get_swarm(x25519_pubkey::from_hex(x_pk_hex), [ed_pk](std::vector swarm) { - REQUIRE(swarm.size() == 3); - CHECK(swarm.front().to_string() == "1.1.1.1:1"); - CHECK(oxenc::to_hex(swarm.front().view_remote_key()) == oxenc::to_hex(ed_pk)); - }); + REQUIRE(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)).size() == 3); + CHECK(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)).front().to_string() == + "1.1.1.1:1"); + CHECK(oxenc::to_hex(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)) + .front() + .view_remote_key()) == oxenc::to_hex(ed_pk)); // Check a timeout with a sever destination doesn't impact the failure counts + auto server = ServerDestination{ + "https", + "open.getsession.org", + "/rooms", + x25519_pubkey::from_hex("a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da79912" + "38"), + 443, + std::nullopt, + "GET"}; auto mock_request3 = request_info{ "CCCC", - target, + server, "test", std::nullopt, std::nullopt, @@ -484,30 +615,335 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 0); } -TEST_CASE("Network onion request", "[send_onion_request][network]") { +TEST_CASE("Network Path Building", "[network][build_path]") { + const auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + std::optional network; + std::vector snode_cache; + for (uint16_t i = 0; i < 12; ++i) + snode_cache.emplace_back(service_node{ed_pk, {2, 8, 0}, fmt::format("0.0.0.{}", i), i}); + + // Nothing should happen if the network is suspended + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_suspended(true); + network->build_path(std::nullopt, PathType::standard); + CHECK(ALWAYS(10ms, network->did_not_call("resume_queues"))); + + // If the snode cache is too small it puts the path build in the queue and calls resume_queues + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->build_path("Test1", PathType::standard); + CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); + CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); + CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + + // If there are no in progress path builds it refreshes the unused nodes value and adds an + // in-progress build + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_snode_cache(snode_cache); + network->set_general_path_build_failures(10); + network->build_path("Test1", PathType::standard); + CHECK(network->get_unused_path_build_nodes().size() == snode_cache.size() - 1); + CHECK(network->get_general_path_build_failures() == 0); + CHECK(network->get_in_progress_path_builds().contains("Test1")); + + // It should exclude nodes that are already in existing paths + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_snode_cache(snode_cache); + network->add_path(PathType::standard, {snode_cache.begin(), snode_cache.begin() + 3}); + network->build_path("Test1", PathType::standard); + CHECK(network->get_unused_path_build_nodes().size() == (snode_cache.size() - 1 - 3)); + CHECK(network->get_in_progress_path_builds().contains("Test1")); + + // If there are no unused nodes it increments the failure count and re-queues the path build + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_snode_cache(snode_cache); + network->add_path(PathType::standard, snode_cache); + network->build_path("Test1", PathType::standard); + CHECK(network->get_unused_path_build_nodes().empty()); + CHECK(network->get_general_path_build_failures() == 1); + CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); + CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); + + // If it can't build a path after excluding nodes for existing requests it increments the + // failure count and re-queues the path build + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_snode_cache(snode_cache); + network->add_path(PathType::standard, {snode_cache.begin() + 1, snode_cache.end()}); + network->add_pending_request( + PathType::standard, + request_info::make(snode_cache.front(), 0ms, std::nullopt, std::nullopt)); + network->build_path("Test1", PathType::standard); + CHECK(network->get_unused_path_build_nodes().size() == 1); + CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); + CHECK(network->get_general_path_build_failures() == 1); + CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); + CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + + // It fetches the version of the guard node to check reachability + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->ignore_get_snode_version = true; + network->set_snode_cache(snode_cache); + network->build_path("Test1", PathType::standard); + CHECK(network->called("get_snode_version")); + + // If it fails to get the guard node version it increments the pending path build failure count + // and retries again until it runs out of unused nodes + network.emplace(std::nullopt, true, false, false); + network->retry_delay_value = 1ms; // Prevent infinite loop detection + network->ignore_resume_queues = true; + network->get_snode_version_response = {{}, {snode_cache.front(), nullptr, nullptr}, "Error"}; + network->set_snode_cache(snode_cache); + network->build_path("Test1", PathType::standard); + CHECK(EVENTUALLY(20ms, network->called("build_path", 12))); + CHECK(network->get_in_progress_path_builds().contains("Test1")); + CHECK(network->get_in_progress_path_builds()["Test1"].second == 12); + CHECK(network->get_unused_path_build_nodes().empty()); + CHECK(network->called("get_snode_version", 12)); + + // If it runs out of unused nodes after getting the guard node version it increments the pending + // path build failure count and retries the build + network.emplace(std::nullopt, true, false, false); + network->retry_delay_value = 1ms; // Prevent infinite loop detection + network->ignore_resume_queues = true; + network->get_snode_version_response = {{}, {snode_cache.front(), nullptr, nullptr}, "Error"}; + network->set_snode_cache(snode_cache); + network->add_path(PathType::standard, {snode_cache.begin() + 3, snode_cache.end()}); + network->build_path("Test1", PathType::standard); + CHECK(EVENTUALLY(20ms, network->called("build_path", 3))); + CHECK(network->get_general_path_build_failures() == 1); + CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); + CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + + // It stores a successful non-standard path and calls 'resume_queues' but doesn't update the + // status or call the 'paths_changed' hook + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->get_snode_version_response = { + {2, 8, 0}, {snode_cache.front(), nullptr, nullptr}, std::nullopt}; + network->set_snode_cache(snode_cache); + network->build_path("Test1", PathType::download); + CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + CHECK(network->get_paths(PathType::download).size() == 1); + + // It stores a successful 'standard' path, updates the status, calls the 'paths_changed' hook + // and calls 'resume_queues' + network.emplace(std::nullopt, true, false, false); + network->ignore_resume_queues = true; + network->get_snode_version_response = { + {2, 8, 0}, {snode_cache.front(), nullptr, nullptr}, std::nullopt}; + network->set_snode_cache(snode_cache); + network->build_path("Test1", PathType::standard); + CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + CHECK(network->get_paths(PathType::standard).size() == 1); + CHECK(network->get_status() == ConnectionStatus::connected); + CHECK(network->called("paths_changed")); +} + +// TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { +// auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; +// auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; +// auto test_service_node = service_node{ +// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, +// {2, 8, 0}, +// "144.76.164.202", +// uint16_t{35400}}; +// std::optional network; +// network.emplace(std::nullopt, true, false, false); +// auto info = request_info::make(target, 0ms, std::nullopt, std::nullopt); +// auto invalid_path = +// onion_path{{test_service_node, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; + +// // It returns nothing when given no path options +// CHECK_FALSE(network->find_valid_path(info, {}).has_value()); + +// // It ignores invalid paths +// CHECK_FALSE(network->find_valid_path(info, {invalid_path}).has_value()); + +// // Need to get a valid path for subsequent tests +// std::promise>> prom; + +// network->establish_connection( +// "Test", +// PathType::standard, +// test_service_node, +// 3s, +// [&prom](connection_info info, std::optional error) { +// prom.set_value({info, error}); +// }); + +// // Wait for the result to be set +// auto result = prom.get_future().get(); +// REQUIRE(result.first.is_valid()); +// auto valid_path = onion_path{ +// std::move(result.first), std::vector{test_service_node}, uint8_t{0}}; + +// // It excludes paths which include the IP of the target +// auto shared_ip_info = request_info::make(test_service_node, 0ms, std::nullopt, std::nullopt); +// CHECK_FALSE(network->find_valid_path(shared_ip_info, {valid_path}).has_value()); + +// // It returns a path when there is a valid one +// CHECK(network->find_valid_path(info, {valid_path}).has_value()); + +// // In 'single_path_mode' it does allow the path to include the IP of the target (so that +// // requests can still be made) +// network.emplace(std::nullopt, true, true, false); +// CHECK(network->find_valid_path(shared_ip_info, {valid_path}).has_value()); +// } + +TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed]") { + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; + auto network = TestNetwork(std::nullopt, true, false, false); + auto network_single_path = TestNetwork(std::nullopt, true, true, false); + auto invalid_path = onion_path{{target, nullptr, nullptr}, {target}, uint8_t{0}}; + network.ignore_resume_queues = true; + network_single_path.ignore_resume_queues = true; + + // It does not add additional path builds if there is already a path and it's in + // 'single_path_mode' + network_single_path.set_paths(PathType::standard, {invalid_path}); + network_single_path.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network_single_path.get_path_build_queue().empty()); + + // Adds a path build to the queue + network.set_paths(PathType::standard, {}); + network.set_path_build_queue({}); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue() == std::vector{PathType::standard}); + + // Can only add two path build to the queue + network.set_paths(PathType::standard, {}); + network.set_path_build_queue({}); + network.enqueue_path_build_if_needed(PathType::standard, false); + network.enqueue_path_build_if_needed(PathType::standard, false); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue() == + std::vector{PathType::standard, PathType::standard}); + + // Can add a second 'standard' path build even if there is an active 'standard' path + network.set_paths(PathType::standard, {invalid_path}); + network.set_path_build_queue({}); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue() == std::vector{PathType::standard}); + + // Cannot add more path builds if there are already enough active paths of the same type + network.set_paths(PathType::standard, {invalid_path, invalid_path}); + network.set_path_build_queue({}); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue().empty()); + + // Cannot add more path builds if there are already enough in progress path builds of the same + // type + network.set_paths(PathType::standard, {invalid_path, invalid_path}); + network.set_path_build_queue({}); + network.clear_in_progress_path_builds(); + network.add_in_progress_path_builds("Test1", {PathType::standard, 0}); + network.add_in_progress_path_builds("Test2", {PathType::standard, 0}); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue().empty()); + + // Cannot add more path builds if the combination of active and in progress builds of the same + // time are more than the limit + network.set_paths(PathType::standard, {invalid_path}); + network.set_path_build_queue({}); + network.clear_in_progress_path_builds(); + network.add_in_progress_path_builds("Test1", {PathType::standard, 0}); + network.enqueue_path_build_if_needed(PathType::standard, false); + CHECK(network.get_path_build_queue().empty()); + + // Can only add a single 'download' path build + network.set_paths(PathType::download, {}); + network.set_path_build_queue({}); + network.clear_in_progress_path_builds(); + network.enqueue_path_build_if_needed(PathType::download, false); + network.enqueue_path_build_if_needed(PathType::download, false); + CHECK(network.get_path_build_queue() == std::vector{PathType::download}); + + // Can only add a single 'upload' path build + network.set_paths(PathType::upload, {}); + network.set_path_build_queue({}); + network.clear_in_progress_path_builds(); + network.enqueue_path_build_if_needed(PathType::upload, false); + network.enqueue_path_build_if_needed(PathType::upload, false); + CHECK(network.get_path_build_queue() == std::vector{PathType::upload}); +} + +TEST_CASE("Network requests", "[network][establish_connection]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, {2, 8, 0}, "144.76.164.202", uint16_t{35400}}; - auto network = Network(std::nullopt, true, true, false); - std::promise result_promise; + auto network = TestNetwork(std::nullopt, true, true, false); + std::promise>> prom; - network.send_onion_request( + network.establish_connection( + "Test", + PathType::standard, test_service_node, - ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, - std::nullopt, - oxen::quic::DEFAULT_TIMEOUT, - [&result_promise]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - result_promise.set_value({success, timeout, status_code, response}); + 3s, + [&prom](connection_info info, std::optional error) { + prom.set_value({info, error}); + }); + + // Wait for the result to be set + auto result = prom.get_future().get(); + + CHECK(result.first.is_valid()); + CHECK_FALSE(result.second.has_value()); +} + +TEST_CASE("Network requests", "[network][send_request]") { + auto test_service_node = service_node{ + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + {2, 8, 0}, + "144.76.164.202", + uint16_t{35400}}; + auto network = TestNetwork(std::nullopt, true, true, false); + std::promise prom; + + network.establish_connection( + "Test", + PathType::standard, + test_service_node, + 3s, + [&prom, &network, test_service_node]( + connection_info info, std::optional error) { + if (!info.is_valid()) + return prom.set_value({false, false, -1, error.value_or("Unknown Error")}); + + network.send_request( + request_info::make( + test_service_node, + 3s, + ustring{to_usv("{}")}, + std::nullopt, + PathType::standard, + std::nullopt, + "info"), + std::move(info), + [&prom](bool success, + bool timeout, + int16_t status_code, + std::optional response) { + prom.set_value({success, timeout, status_code, response}); + }); }); // Wait for the result to be set - auto result = result_promise.get_future().get(); + auto result = prom.get_future().get(); CHECK(result.success); CHECK_FALSE(result.timeout); @@ -524,45 +960,85 @@ TEST_CASE("Network onion request", "[send_onion_request][network]") { } } -TEST_CASE("Network direct request C API", "[network_send_request][network]") { - network_object* network; - REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); - std::array target_ip = {144, 76, 164, 202}; - auto test_service_node = network_service_node{}; - test_service_node.quic_port = 35400; - std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); - std::strcpy( - test_service_node.ed25519_pubkey_hex, - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); - auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; - - network_send_onion_request_to_snode_destination( - network, - test_service_node, - body.data(), - body.size(), - nullptr, - std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), - [](bool success, - bool timeout, - int16_t status_code, - const char* c_response, - size_t response_size, - void* ctx) { - CHECK(success); - CHECK_FALSE(timeout); - CHECK(status_code == 200); - REQUIRE(response_size != 0); - - auto response_str = std::string(c_response, response_size); - INFO("response_str is: " << response_str); - REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); - - auto response = nlohmann::json::parse(response_str); - CHECK(response.contains("hf")); - CHECK(response.contains("t")); - CHECK(response.contains("version")); - network_free(static_cast(ctx)); - }, - network); -} +// TEST_CASE("Network onion request", "[network][send_onion_request]") { +// auto test_service_node = service_node{ +// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, +// {2, 8, 0}, +// "144.76.164.202", +// uint16_t{35400}}; +// auto network = Network(std::nullopt, true, true, false); +// std::promise result_promise; + +// network.send_onion_request( +// test_service_node, +// ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, +// std::nullopt, +// oxen::quic::DEFAULT_TIMEOUT, +// [&result_promise]( +// bool success, +// bool timeout, +// int16_t status_code, +// std::optional response) { +// result_promise.set_value({success, timeout, status_code, response}); +// }); + +// // Wait for the result to be set +// auto result = result_promise.get_future().get(); + +// CHECK(result.success); +// CHECK_FALSE(result.timeout); +// CHECK(result.status_code == 200); +// REQUIRE(result.response.has_value()); + +// try { +// auto response = nlohmann::json::parse(*result.response); +// CHECK(response.contains("hf")); +// CHECK(response.contains("t")); +// CHECK(response.contains("version")); +// } catch (...) { +// REQUIRE(*result.response == "{VALID JSON}"); +// } +// } + +// TEST_CASE("Network direct request C API", "[network][network_send_request]") { +// network_object* network; +// REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); +// std::array target_ip = {144, 76, 164, 202}; +// auto test_service_node = network_service_node{}; +// test_service_node.quic_port = 35400; +// std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); +// std::strcpy( +// test_service_node.ed25519_pubkey_hex, +// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); +// auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; + +// network_send_onion_request_to_snode_destination( +// network, +// test_service_node, +// body.data(), +// body.size(), +// nullptr, +// std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), +// [](bool success, +// bool timeout, +// int16_t status_code, +// const char* c_response, +// size_t response_size, +// void* ctx) { +// CHECK(success); +// CHECK_FALSE(timeout); +// CHECK(status_code == 200); +// REQUIRE(response_size != 0); + +// auto response_str = std::string(c_response, response_size); +// INFO("response_str is: " << response_str); +// REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); + +// auto response = nlohmann::json::parse(response_str); +// CHECK(response.contains("hf")); +// CHECK(response.contains("t")); +// CHECK(response.contains("version")); +// network_free(static_cast(ctx)); +// }, +// network); +// } diff --git a/tests/utils.hpp b/tests/utils.hpp index 76b145c1..9fbeee69 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "session/config/base.h" @@ -88,3 +89,69 @@ std::vector> view_vec(const std::vector> Validator> +auto eventually_impl(std::chrono::milliseconds timeout, Call&& f, Validator&& isValid) + -> std::invoke_result_t { + using ResultType = std::invoke_result_t; + + // If we already have a value then don't bother with the loop + if (auto result = f(); isValid(result)) + return result; + + auto start = std::chrono::steady_clock::now(); + auto sleep_duration = std::chrono::milliseconds{10}; + while (std::chrono::steady_clock::now() - start < timeout) { + std::this_thread::sleep_for(sleep_duration); + + if (auto result = f(); isValid(result)) + return result; + } + + return ResultType{}; +} + +template > Validator> +bool always_impl(std::chrono::milliseconds duration, Call&& f, Validator&& isValid) { + auto start = std::chrono::steady_clock::now(); + auto sleep_duration = std::chrono::milliseconds{10}; + while (std::chrono::steady_clock::now() - start < duration) { + if (auto result = f(); !isValid(result)) + return false; + std::this_thread::sleep_for(sleep_duration); + } + return true; +} + +template + requires std::is_same_v, bool> +bool eventually_impl(std::chrono::milliseconds timeout, Call&& f) { + return eventually_impl(timeout, f, [](bool result) { return result; }); +} + +template + requires std::is_same_v< + std::invoke_result_t, + std::vector::value_type>> +auto eventually_impl(std::chrono::milliseconds timeout, Call&& f) -> std::invoke_result_t { + using ResultType = std::invoke_result_t; + return eventually_impl(timeout, f, [](const ResultType& result) { return !result.empty(); }); +} + +template + requires std::is_same_v, bool> +bool always_impl(std::chrono::milliseconds duration, Call&& f) { + return always_impl(duration, f, [](bool result) { return result; }); +} + +template + requires std::is_same_v< + std::invoke_result_t, + std::vector::value_type>> +bool always_impl(std::chrono::milliseconds duration, Call&& f) { + using ResultType = std::invoke_result_t; + return always_impl(duration, f, [](const ResultType& result) { return !result.empty(); }); +} + +#define EVENTUALLY(timeout, ...) eventually_impl(timeout, [&]() { return (__VA_ARGS__); }) +#define ALWAYS(duration, ...) always_impl(duration, [&]() { return (__VA_ARGS__); }) From 638616d366b02b18ee17047085e421eb936fc99e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Aug 2024 18:48:52 +1000 Subject: [PATCH 373/572] Fixed one of the broken tests --- tests/test_network.cpp | 89 +++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 139f5775..7fb2dfea 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -751,56 +751,55 @@ TEST_CASE("Network Path Building", "[network][build_path]") { CHECK(network->called("paths_changed")); } -// TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { -// auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; -// auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; -// auto test_service_node = service_node{ -// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, -// {2, 8, 0}, -// "144.76.164.202", -// uint16_t{35400}}; -// std::optional network; -// network.emplace(std::nullopt, true, false, false); -// auto info = request_info::make(target, 0ms, std::nullopt, std::nullopt); -// auto invalid_path = -// onion_path{{test_service_node, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; +TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; + auto test_service_node = service_node{ + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + {2, 8, 0}, + "144.76.164.202", + uint16_t{35400}}; + auto network = TestNetwork(std::nullopt, true, false, false); + auto info = request_info::make(target, 0ms, std::nullopt, std::nullopt); + auto invalid_path = + onion_path{{test_service_node, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; -// // It returns nothing when given no path options -// CHECK_FALSE(network->find_valid_path(info, {}).has_value()); + // It returns nothing when given no path options + CHECK_FALSE(network.find_valid_path(info, {}).has_value()); -// // It ignores invalid paths -// CHECK_FALSE(network->find_valid_path(info, {invalid_path}).has_value()); + // It ignores invalid paths + CHECK_FALSE(network.find_valid_path(info, {invalid_path}).has_value()); -// // Need to get a valid path for subsequent tests -// std::promise>> prom; + // Need to get a valid path for subsequent tests + std::promise>> prom; -// network->establish_connection( -// "Test", -// PathType::standard, -// test_service_node, -// 3s, -// [&prom](connection_info info, std::optional error) { -// prom.set_value({info, error}); -// }); + network.establish_connection( + "Test", + PathType::standard, + test_service_node, + 3s, + [&prom](connection_info conn_info, std::optional error) { + prom.set_value({std::move(conn_info), error}); + }); -// // Wait for the result to be set -// auto result = prom.get_future().get(); -// REQUIRE(result.first.is_valid()); -// auto valid_path = onion_path{ -// std::move(result.first), std::vector{test_service_node}, uint8_t{0}}; - -// // It excludes paths which include the IP of the target -// auto shared_ip_info = request_info::make(test_service_node, 0ms, std::nullopt, std::nullopt); -// CHECK_FALSE(network->find_valid_path(shared_ip_info, {valid_path}).has_value()); - -// // It returns a path when there is a valid one -// CHECK(network->find_valid_path(info, {valid_path}).has_value()); - -// // In 'single_path_mode' it does allow the path to include the IP of the target (so that -// // requests can still be made) -// network.emplace(std::nullopt, true, true, false); -// CHECK(network->find_valid_path(shared_ip_info, {valid_path}).has_value()); -// } + // Wait for the result to be set + auto result = prom.get_future().get(); + REQUIRE(result.first.is_valid()); + auto valid_path = onion_path{ + std::move(result.first), std::vector{test_service_node}, uint8_t{0}}; + + // It excludes paths which include the IP of the target + auto shared_ip_info = request_info::make(test_service_node, 0ms, std::nullopt, std::nullopt); + CHECK_FALSE(network.find_valid_path(shared_ip_info, {valid_path}).has_value()); + + // It returns a path when there is a valid one + CHECK(network.find_valid_path(info, {valid_path}).has_value()); + + // In 'single_path_mode' it does allow the path to include the IP of the target (so that + // requests can still be made) + auto network_single_path = TestNetwork(std::nullopt, true, true, false); + CHECK(network_single_path.find_valid_path(shared_ip_info, {valid_path}).has_value()); +} TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; From b368c47cf8bba741b6994bb9c9ea50ebf84969c4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 8 Aug 2024 11:01:00 +1000 Subject: [PATCH 374/572] Drop 'not found' nodes immediately, additional PR feedback tweaks --- src/network.cpp | 172 ++++++++++++++++++++++------------------- tests/test_network.cpp | 34 +------- 2 files changed, 96 insertions(+), 110 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 30734748..f4b5bcd6 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -531,8 +531,8 @@ void Network::disk_write_thread_loop() { // Save the snode pool to disk if (need_pool_write) { - auto pool_path = cache_path / file_snode_pool, - pool_tmp = pool_path / u8"_new"; + auto pool_path = cache_path / file_snode_pool, pool_tmp = pool_path; + pool_tmp += u8"_new"; { std::stringstream ss; @@ -556,8 +556,8 @@ void Network::disk_write_thread_loop() { // Save the snode failure counts to disk if (need_failure_counts_write) { - auto failure_counts_path = cache_path / file_snode_failure_counts; - auto failure_counts_tmp = failure_counts_path; + auto failure_counts_path = cache_path / file_snode_failure_counts, + failure_counts_tmp = failure_counts_path; failure_counts_tmp += u8"_new"; { @@ -579,8 +579,7 @@ void Network::disk_write_thread_loop() { auto time_now = std::chrono::system_clock::now(); for (auto& [key, swarm] : swarm_cache_write) { - auto swarm_path = swarm_base / key; - auto swarm_tmp = swarm_path; + auto swarm_path = swarm_base / key, swarm_tmp = swarm_path; swarm_tmp += u8"_new"; // Write the timestamp @@ -921,6 +920,9 @@ void Network::resume_queues() { // If there are left over invalid paths in `paths` and we have more than the minimum number of // required paths (including the builds/recoveries started above) then we can just drop the // extra invalid paths + std::vector valid_paths; + std::vector invalid_paths; + for (auto& [path_type, existing_paths] : paths) { size_t pending_count = num_pending_paths[path_type]; size_t total_count = (existing_paths.size() + pending_count); @@ -930,10 +932,8 @@ void Network::resume_queues() { if (total_count <= target_count) continue; - std::vector valid_paths; - std::vector invalid_paths; - valid_paths.reserve(existing_paths.size()); - invalid_paths.reserve(existing_paths.size()); + valid_paths.clear(); + invalid_paths.clear(); for (auto& path : existing_paths) { if (path.is_valid()) @@ -955,27 +955,32 @@ void Network::resume_queues() { // Now that we've scheduled any required path builds we should try to resume any pending // requests in case they now have a valid paths if (!paths.empty()) { + bool need_resume = false; std::unordered_set already_enqueued_paths; for (auto& [path_type, requests] : request_queue) if (!paths[path_type].empty()) - std::erase_if(requests, [this, &already_enqueued_paths](const auto& request) { - // If there are no valid paths to send the request then enqueue a new path build - // (if needed) leave the request in the queue - if (!find_valid_path(request.first, paths[request.first.path_type])) { - if (!already_enqueued_paths.contains(request.first.path_type)) { - already_enqueued_paths.insert(request.first.path_type); - enqueue_path_build_if_needed(request.first.path_type, true); - } - net.call_soon([this]() { resume_queues(); }); - return false; - } + std::erase_if( + requests, + [this, &need_resume, &already_enqueued_paths](const auto& request) { + // If there are no valid paths to send the request then enqueue a new + // path build (if needed) leave the request in the queue + if (!find_valid_path(request.first, paths[request.first.path_type])) { + if (already_enqueued_paths.emplace(request.first.path_type).second) + enqueue_path_build_if_needed(request.first.path_type, true); + need_resume = true; + return false; + } - net.call_soon([this, info = request.first, cb = std::move(request.second)]() { - _send_onion_request(std::move(info), std::move(cb)); - }); - return true; - }); + net.call_soon( + [this, info = request.first, cb = std::move(request.second)]() { + _send_onion_request(std::move(info), std::move(cb)); + }); + return true; + }); + + if (need_resume) + net.call_soon([this]() { resume_queues(); }); } } @@ -1347,9 +1352,11 @@ void Network::build_path(std::optional existing_request_id, PathTyp log::info(cat, "{}", e.what()); // Delay the next path build attempt based on the error we received - auto failure_count = in_progress_path_builds[request_id].second; - in_progress_path_builds[request_id] = {path_type, failure_count + 1}; - auto delay = retry_delay(failure_count + 1); + auto& [_, failure_count] = + in_progress_path_builds.try_emplace(request_id, path_type, 0) + .first->second; + failure_count += 1; + auto delay = retry_delay(failure_count); net.call_later(delay, [this, request_id, path_type]() { build_path(request_id, path_type); }); @@ -2517,53 +2524,57 @@ void Network::handle_errors( if (snode_it != updated_path.nodes.end()) { found_invalid_node = true; - auto failure_count = updated_failure_counts[snode_it->to_string()]; - updated_failure_counts[snode_it->to_string()] = failure_count + 1; - - // If the specific node has failed too many times then we should try to repair - // the existing path by replace the bad node with another one - if (failure_count + 1 >= snode_failure_threshold) { - nodes_to_drop.emplace_back(*snode_it); + // If we get an explicit node failure then we should just immediately drop it and + // try to repair the existing path by replacing the bad node with another one + updated_failure_counts.erase(snode_it->to_string()); + nodes_to_drop.emplace_back(*snode_it); - try { - // If the node that's gone bad is the guard node then we just have to - // drop the path - if (snode_it == updated_path.nodes.begin()) - throw std::runtime_error{"Cannot recover if guard node is bad"}; - - // Try to find an unused node to patch the path - std::vector unused_snodes; - std::vector existing_path_node_ips = all_path_ips(); - - std::copy_if( - snode_cache.begin(), - snode_cache.end(), - std::back_inserter(unused_snodes), - [&existing_path_node_ips](const auto& node) { - return std::find( - existing_path_node_ips.begin(), - existing_path_node_ips.end(), - node.to_ipv4()) == existing_path_node_ips.end(); - }); + try { + // If the node that's gone bad is the guard node then we just have to + // drop the path + if (snode_it == updated_path.nodes.begin()) + throw std::runtime_error{"Cannot recover if guard node is bad"}; + + // Try to find an unused node to patch the path + std::vector unused_snodes; + std::vector existing_path_node_ips = all_path_ips(); + + std::copy_if( + snode_cache.begin(), + snode_cache.end(), + std::back_inserter(unused_snodes), + [&existing_path_node_ips](const auto& node) { + return std::find( + existing_path_node_ips.begin(), + existing_path_node_ips.end(), + node.to_ipv4()) == existing_path_node_ips.end(); + }); - if (unused_snodes.empty()) - throw std::runtime_error{"No remaining nodes"}; + if (unused_snodes.empty()) + throw std::runtime_error{"No remaining nodes"}; - CSRNG rng; - std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); + CSRNG rng; + std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); - std::replace( - updated_path.nodes.begin(), - updated_path.nodes.end(), - *snode_it, - unused_snodes.front()); - log::info(cat, "Found bad node in {} path, replacing node.", path_name); - } catch (...) { - // There aren't enough unused nodes remaining so we need to drop the - // path - updated_path.failure_count = path_failure_threshold; - log::info(cat, "Unable to replace bad node in {} path.", path_name); - } + std::replace( + updated_path.nodes.begin(), + updated_path.nodes.end(), + *snode_it, + unused_snodes.front()); + log::info( + cat, + "Found bad node ({}) in {} path, replacing node.", + *ed25519PublicKey, + path_name); + } catch (...) { + // There aren't enough unused nodes remaining so we need to drop the + // path + updated_path.failure_count = path_failure_threshold; + log::info( + cat, + "Unable to replace bad node ({}) in {} path.", + *ed25519PublicKey, + path_name); } } } @@ -2578,10 +2589,11 @@ void Network::handle_errors( // invalid) and increment the failure count of each node in the path) if (updated_path.failure_count >= path_failure_threshold) { for (auto& it : updated_path.nodes) { - auto failure_count = updated_failure_counts[it.to_string()]; - updated_failure_counts[it.to_string()] = failure_count + 1; + auto& failure_count = + updated_failure_counts.try_emplace(it.to_string(), 0).first->second; + failure_count += 1; - if (failure_count + 1 >= snode_failure_threshold) + if (failure_count >= snode_failure_threshold) nodes_to_drop.emplace_back(it); } @@ -2592,15 +2604,17 @@ void Network::handle_errors( // If the path doesn't have enough nodes then it's likely that this failure was // triggered when trying to establish a new path and, as such, we should increase // the failure count of the guard node since it is probably invalid - auto failure_count = updated_failure_counts[updated_path.nodes[0].to_string()]; - updated_failure_counts[updated_path.nodes[0].to_string()] = failure_count + 1; + auto& failure_count = + updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) + .first->second; + failure_count += 1; - if (failure_count + 1 >= snode_failure_threshold) + if (failure_count >= snode_failure_threshold) nodes_to_drop.emplace_back(updated_path.nodes[0]); } } - // Remove any nodes from 'nodes_to_drop' which don't actually need to be dropped + // Make sure to remove any nodes we want to drop from the swarm cache as well auto updated_swarm_cache = swarm_cache; bool requires_swarm_cache_update = false; diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 7fb2dfea..5b59a46b 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -353,42 +353,13 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(target3) == 1); // Other nodes incremented CHECK(network.get_failure_count(PathType::standard, path) == 0); // Path dropped and reset - // Check general error handling with a path and specific node failure (first failure) + // Check general error handling with a path and specific node failure path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( - mock_request, - {target, nullptr, nullptr}, - false, - 500, - response, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - result = {success, timeout, status_code, response}; - }); - - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 500); - CHECK(result.response == response); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 1); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == - 1); // Incremented because conn_info is invalid - - // Check general error handling with a path and specific node failure (too many failures) network.set_snode_cache({target, target2, target3, target4}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); - network.set_failure_count(target2, 9); + network.set_failure_count(target2, 1); network.set_failure_count(target3, 0); network.handle_errors( mock_request, @@ -411,6 +382,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(target) == 0); CHECK(network.get_failure_count(target2) == 0); // Node will have been dropped CHECK(network.get_failure_count(target3) == 0); + CHECK(network.get_paths(PathType::standard).front().nodes[1] != target2); CHECK(network.get_failure_count(PathType::standard, path) == 1); // Incremented because conn_info is invalid From cae3fb6ea7873477d17c127cf8d1c90a3b9a73f4 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Sun, 11 Aug 2024 15:30:28 +1000 Subject: [PATCH 375/572] fix: make FULL_URL_MAX_LENGTH a constexpr and not such static const --- include/session/config/community.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index d6d9b27f..8f666a55 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -19,9 +19,9 @@ struct community { static constexpr size_t BASE_URL_MAX_LENGTH = 267; static constexpr size_t ROOM_MAX_LENGTH = 64; static constexpr std::string_view qs_pubkey{"?public_key="}; - static const size_t FULL_URL_MAX_LENGTH = BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + - ROOM_MAX_LENGTH + qs_pubkey.size() + - 64 /*pubkey hex*/ + 1 /*null terminator*/; + static constexpr size_t FULL_URL_MAX_LENGTH = BASE_URL_MAX_LENGTH + 3 /* '/r/' */ + + ROOM_MAX_LENGTH + qs_pubkey.size() + + 64 /*pubkey hex*/ + 1 /*null terminator*/; community() = default; From 2966cc39c1ec1fcd9cf85853ad064e248be49299 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 13 Aug 2024 18:10:49 +1000 Subject: [PATCH 376/572] Split the resume_queues function and fixed broken tests --- include/session/network.hpp | 189 ++-- src/network.cpp | 1613 +++++++++++++++++------------------ tests/test_network.cpp | 653 +++++++------- 3 files changed, 1224 insertions(+), 1231 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 6e534a8e..b53ec866 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -98,8 +98,8 @@ struct request_info { std::optional _original_body, std::optional _swarm_pk, PathType _type = PathType::standard, - std::optional endpoint = "onion_req", std::optional _req_id = std::nullopt, + std::optional endpoint = "onion_req", std::optional _body = std::nullopt); enum class RetryReason { @@ -133,6 +133,7 @@ class Network { // Disk thread state std::mutex snode_cache_mutex; // This guards all the below: std::condition_variable snode_cache_cv; + bool has_pending_disk_write = false; bool shut_down_disk_thread = false; bool need_write = false; bool need_pool_write = false; @@ -150,27 +151,33 @@ class Network { // General values bool suspended = false; + bool being_destroyed = false; ConnectionStatus status; oxen::quic::Network net; std::shared_ptr endpoint; std::unordered_map> paths; - // Resume queues throttling - bool has_scheduled_resume_queues = false; - std::chrono::system_clock::time_point last_resume_queues_timestamp{}; - - // Snode refreshing values - bool refreshing_snode_cache = false; + // Snode refresh state int snode_cache_refresh_failure_count; + int in_progress_snode_cache_refresh_count; + std::optional current_snode_cache_refresh_request_id; std::vector> after_snode_cache_refresh; + std::optional> unused_snode_refresh_nodes; + std::shared_ptr>> snode_refresh_results; + + // First hop state + std::optional> unused_connection_and_path_build_nodes; + int connection_failures = 0; + std::deque unused_connections; + std::unordered_set in_progress_connections; - // Path building values - int general_path_build_failures; - std::vector path_build_queue; - std::vector unused_path_build_nodes; - std::unordered_map> in_progress_path_builds; + // Path build state + int path_build_failures = 0; + std::deque path_build_queue; - // Pending requests + // Request state + bool has_scheduled_resume_queues = false; + std::chrono::system_clock::time_point last_resume_queues_timestamp{}; std::unordered_map>> request_queue; @@ -342,11 +349,6 @@ class Network { network_response_callback_t handle_response); private: - /// API: network/_send_onion_request - /// - /// Internal function invoked by ::send_onion_request after request_info construction - void _send_onion_request(request_info info, network_response_callback_t handle_response); - /// API: network/all_path_ips /// /// Internal function to retrieve all of the node ips current used in paths @@ -361,6 +363,15 @@ class Network { return result; }; + /// API: network/update_disk_cache_throttled + /// + /// Function which can be used to notify the disk write thread that a write can be performed. + /// This function has a very basic throttling mechanism where it triggers the write a small + /// delay after it is called, any subsequent calls to the function within the same period will + /// be ignored. This is done to avoid excessive disk writes which probably aren't needed for + /// the cached network data. + virtual void update_disk_cache_throttled(bool force_immediate_write = false); + /// API: network/disk_write_thread_loop /// /// Body of the disk writer which runs until signalled to stop. This is intended to run in its @@ -393,13 +404,22 @@ class Network { /// - 'max_delay' - [in] the maximum amount of time to delay for. virtual std::chrono::milliseconds retry_delay( int num_failures, - std::chrono::milliseconds max_delay = std::chrono::milliseconds{3000}); + std::chrono::milliseconds max_delay = std::chrono::milliseconds{5000}); /// API: network/get_endpoint /// /// Retrieves or creates a new endpoint pointer. std::shared_ptr get_endpoint(); + /// API: network/get_unused_nodes + /// + /// Retrieves a list of all nodes in the cache which are currently unused (ie. not present in an + /// exising or pending path, connection or request). + /// + /// Outputs: + /// - The list of unused nodes. + std::vector get_unused_nodes(); + /// API: network/establish_connection /// /// Establishes a connection to the target node and triggers the callback once the connection is @@ -407,7 +427,6 @@ class Network { /// /// Inputs: /// - 'request_id' - [in] id for the request which triggered the call. - /// - 'path_type' - [in] the type of paths this connection is for. /// - `target` -- [in] the target service node to connect to. /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the /// `quic::DEFAULT_HANDSHAKE_TIMEOUT` will be used. @@ -415,30 +434,52 @@ class Network { /// established or fails. void establish_connection( std::string request_id, - PathType path_type, service_node target, std::optional timeout, std::function error)> callback); - /// API: network/resume_queues + /// API: network/establish_and_store_connection /// - /// This function is the backbone of the Network class, it will: - /// - Build/refresh the snode cache - /// - Try to recover connections to paths - /// - Build any queued path builds - /// - Start any queued requests that are now valid + /// Establishes a connection to a random unused node and stores it in the `unused_connections` + /// list. /// - /// When most of these processes finish they call this function again to move through the next - /// step in the process. Note: Due to this "looping" behaviour there is a built in throttling - /// mechanism to avoid running the logic excessively. - virtual void resume_queues(); + /// Inputs: + /// - 'request_id' - [in] id for the request which triggered the call. + virtual void establish_and_store_connection(std::string request_id); + + /// API: network/refresh_snode_cache_complete + /// + /// This function will be called from either `refresh_snode_cache` or + /// `refresh_snode_cache_from_seed_nodes` and will actually update the state and persist the + /// updated cache to disk. + /// + /// Inputs: + /// - 'nodes' - [in] the nodes to use as the updated cache. + void refresh_snode_cache_complete(std::vector nodes); + + /// API: network/refresh_snode_cache_from_seed_nodes + /// + /// This function refreshes the snode cache for a random seed node. Unlike the + /// `refresh_snode_cache` function this will update the cache with the response from a single + /// seed node since it's a trusted source. + /// + /// Inputs: + /// - 'request_id' - [in] id for an existing refresh_snode_cache request. + /// - 'reset_unused_nodes' - [in] flag to indicate whether this should reset the unused nodes + /// before kicking off the request. + virtual void refresh_snode_cache_from_seed_nodes( + std::string request_id, bool reset_unused_nodes); /// API: network/refresh_snode_cache /// /// This function refreshes the snode cache. If the current cache is to small (or not present) - /// this will fetch the cache from a random seed node, otherwise it will randomly pick a number - /// of nodes and set the cache to the intersection of the results. - virtual void refresh_snode_cache(); + /// this will trigger the above `refresh_snode_cache_from_seed_nodes` function, otherwise it + /// will randomly pick a number of nodes from the existing cache and refresh the cache from the + /// intersection of the results. + /// + /// Inputs: + /// - 'existing_request_id' - [in, optional] id for an existing refresh_snode_cache request. + virtual void refresh_snode_cache(std::optional existing_request_id = std::nullopt); /// API: network/build_path /// @@ -446,21 +487,11 @@ class Network { /// random service nodes in the snode pool. /// /// Inputs: + /// - `path_type` -- [in] the type of path to build. /// - 'existing_request_id' - [in, optional] id for an existing build_path request. Generally /// this will only be set when retrying a path build. - /// - `path_type` -- [in] the type of path to build. - virtual void build_path(std::optional existing_request_id, PathType path_type); - - /// API: network/recover_path - /// - /// Attempt to "recover" an existing onion request path. This will attempt to establish a new - /// connection to the guard node of the path, if unable to establish a new connection the path - /// will be dropped an a new path build will be enqueued. - /// - /// Inputs: - /// - `path_type` -- [in] the type for the provided path. - /// - 'path' - [in] the path to try to reconnect to. - virtual void recover_path(PathType path_type, onion_path path); + virtual void build_path( + PathType path_type, std::optional existing_request_id = std::nullopt); /// API: network/find_valid_path /// @@ -474,39 +505,23 @@ class Network { /// /// Outputs: /// - The possible path, if found. - std::optional find_valid_path(request_info info, std::vector paths); + virtual std::optional find_valid_path( + request_info info, std::vector paths); /// API: network/enqueue_path_build_if_needed /// /// Adds a path build to the path build queue for the specified type if the total current or /// pending paths is below the minimum threshold for the given type. Note: This may result in - /// more paths than the minimum threshold being built but not allowing that behaviour could - /// result in a request that never gets sent due to it's destination being present in the - /// existing path(s) for the type. + /// more paths than the minimum threshold being built in order to avoid a situation where a + /// request may never get sent due to it's destination being present in the existing path(s) for + /// the type. /// /// Inputs: /// - `path_type` -- [in] the type of path to be built. - virtual void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable); - - /// API: network/get_service_nodes_recursive - /// - /// A recursive function that will attempt to retrieve service nodes from a given node until it - /// successfully retrieves nodes or the list is drained. - /// - /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - /// - `target_nodes` -- [in] list of nodes to send requests to until we get a result or it's - /// drained. - /// - `limit` -- [in, optional] the number of service nodes to retrieve. - /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If we drain the - /// `target_nodes` and haven't gotten a successful response then the callback will be invoked - /// with an empty vector and an error string. - void get_service_nodes_recursive( - std::string request_id, - std::vector target_nodes, - std::optional limit, - std::function nodes, std::optional error)> - callback); + /// - `found_path` -- [in, optional] the path which was found for the request by calling + /// `find_valid_path` above. + virtual void enqueue_path_build_if_needed( + PathType path_type, std::optional found_path); /// API: network/get_service_nodes /// @@ -514,38 +529,22 @@ class Network { /// /// Inputs: /// - 'request_id' - [in] id for the request which triggered the call. - /// - `node` -- [in] node to retrieve the service nodes from. + /// - `conn_info` -- [in] the connection info to retrieve service nodes from. /// - `limit` -- [in, optional] the number of service nodes to retrieve. /// - `callback` -- [in] callback to be triggered once we receive nodes. NOTE: If an error /// occurs an empty list and an error will be provided. void get_service_nodes( std::string request_id, - service_node node, + connection_info conn_info, std::optional limit, std::function nodes, std::optional error)> callback); - /// API: network/get_snode_version - /// - /// Retrieves the version information for a given service node. + /// API: network/_send_onion_request /// - /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - /// - 'type' - [in] the type of paths to send the request across. - /// - `node` -- [in] node to retrieve the version from. - /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the - /// `quic::DEFAULT_TIMEOUT` will be used. - /// - `callback` -- [in] callback to be triggered with the result of the request. NOTE: If an - /// error occurs an empty list and an error will be provided. - virtual void get_snode_version( - std::string request_id, - PathType path_type, - service_node node, - std::optional timeout, - std::function< - void(std::vector version, - connection_info info, - std::optional error)> callback); + /// Internal function invoked by ::send_onion_request after request_info construction + virtual void _send_onion_request( + request_info info, network_response_callback_t handle_response); /// API: network/process_v3_onion_response /// @@ -588,6 +587,8 @@ class Network { /// response elsewhere). std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + void drop_path(std::string request_id, PathType path_type, onion_path path); + /// API: network/handle_errors /// /// Processes a non-success response to automatically perform any standard operations based on diff --git a/src/network.cpp b/src/network.cpp index f4b5bcd6..ca539269 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -53,9 +53,6 @@ namespace { constexpr int16_t error_network_suspended = -10001; constexpr int16_t error_building_onion_request = -10002; - // The minimum time which should pass between `resume_queues` executions - constexpr auto resume_queues_throttle_duration = 100ms; - // The amount of time the snode cache can be used before it needs to be refreshed constexpr auto snode_cache_expiration_duration = 2h; @@ -66,10 +63,7 @@ namespace { constexpr size_t min_snode_cache_count = 12; // The number of snodes to use to refresh the cache. - constexpr size_t num_snodes_to_refresh_cache_from = 3; - - // The number of times to retry refreshing the cache from each snode. - constexpr size_t snode_cache_refresh_retries = 3; + constexpr int num_snodes_to_refresh_cache_from = 3; // The smallest size a swarm can get to before we need to fetch it again. constexpr uint16_t min_swarm_snode_count = 3; @@ -267,6 +261,94 @@ namespace detail { throw std::runtime_error{"Invalid destination."}; } + + nlohmann::json get_service_nodes_params(std::optional limit) { + nlohmann::json params{ + {"active_only", true}, + {"fields", + {{"public_ip", true}, + {"pubkey_ed25519", true}, + {"storage_lmq_port", true}, + {"storage_server_version", true}}}}; + + if (limit) + params["limit"] = *limit; + + return params; + } + + std::vector process_get_service_nodes_response( + oxenc::bt_list_consumer result_bencode) { + std::vector result; + result_bencode.skip_value(); // Skip the status code (already validated) + auto response_dict = result_bencode.consume_dict_consumer(); + response_dict.skip_until("result"); + + auto result_dict = response_dict.consume_dict_consumer(); + result_dict.skip_until("service_node_states"); + + // Process the node list + auto node = result_dict.consume_list_consumer(); + + while (!node.is_finished()) { + auto node_consumer = node.consume_dict_consumer(); + auto pubkey_ed25519 = oxenc::from_hex(consume_string(node_consumer, "pubkey_ed25519")); + auto public_ip = consume_string(node_consumer, "public_ip"); + auto storage_lmq_port = consume_integer(node_consumer, "storage_lmq_port"); + + std::vector storage_server_version; + node_consumer.skip_until("storage_server_version"); + auto version_consumer = node_consumer.consume_list_consumer(); + + while (!version_consumer.is_finished()) { + storage_server_version.emplace_back(version_consumer.consume_integer()); + } + + result.emplace_back( + pubkey_ed25519, storage_server_version, public_ip, storage_lmq_port); + } + + return result; + } + + std::vector process_get_service_nodes_response(nlohmann::json response_json) { + if (!response_json.contains("result") || !response_json["result"].is_object()) + throw std::runtime_error{"JSON missing result field."}; + + nlohmann::json result_json = response_json["result"]; + if (!result_json.contains("service_node_states") || + !result_json["service_node_states"].is_array()) + throw std::runtime_error{"JSON missing service_node_states field."}; + + std::vector result; + for (auto& snode : result_json["service_node_states"]) + result.emplace_back(node_from_json(snode)); + + return result; + } + + void log_retry_result_if_needed(request_info info, bool single_path_mode) { + if (!info.retry_reason) + return; + + // For debugging purposes if the error was a redirect retry then + // we want to log that the retry was successful as this will + // help identify how often we are receiving incorrect errors + auto reason = "unknown retry"; + + switch (*info.retry_reason) { + case request_info::RetryReason::redirect: reason = "421 retry"; break; + + case request_info::RetryReason::decryption_failure: reason = "decryption error"; break; + } + + log::info( + cat, + "Received valid response after {} in request {} for {}.", + reason, + info.request_id, + path_type_name(info.path_type, single_path_mode)); + } } // namespace detail request_info request_info::make( @@ -307,20 +389,23 @@ Network::Network( } // Kick off a separate thread to build paths (may as well kick this off early) - if (pre_build_paths) { + if (pre_build_paths) for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) - path_build_queue.emplace_back(PathType::standard); - - net.call_soon([this] { resume_queues(); }); - } + net.call_soon([this] { build_path(PathType::standard); }); } Network::~Network() { + // We need to set a flag to prevent the logic in the `connection_closed_callback` from running + // because if we can get bad memory errors due to trying to use `net.call` while the + // `quic::Network` is in the process of being destroyed + suspended = true; + being_destroyed = true; + { std::lock_guard lock{snode_cache_mutex}; shut_down_disk_thread = true; } - snode_cache_cv.notify_one(); + update_disk_cache_throttled(true); if (disk_write_thread.joinable()) disk_write_thread.join(); } @@ -509,6 +594,25 @@ void Network::load_cache_from_disk() { } } +void Network::update_disk_cache_throttled(bool force_immediate_write) { + // If we are forcing an immediate write then just notify the disk write thread and reset the + // pending write flag + if (force_immediate_write) { + snode_cache_cv.notify_one(); + has_pending_disk_write = false; + return; + } + + if (has_pending_disk_write) + return; + + has_pending_disk_write = true; + net.call_later(1s, [this]() { + snode_cache_cv.notify_one(); + has_pending_disk_write = false; + }); +} + void Network::disk_write_thread_loop() { std::unique_lock lock{snode_cache_mutex}; while (true) { @@ -631,7 +735,7 @@ void Network::clear_cache() { std::lock_guard lock{snode_cache_mutex}; need_clear_cache = true; } - snode_cache_cv.notify_one(); + update_disk_cache_throttled(true); }); } @@ -657,6 +761,12 @@ void Network::close_connections() { // Explicitly reset the endpoint to close all connections endpoint.reset(); + // Cancel any pending requests (they can't succeed once the connection is closed) + for (const auto& [path_type, path_type_requests] : request_queue) + for (const auto& [info, callback] : path_type_requests) + callback(false, false, error_network_suspended, "Network is suspended."); + request_queue.clear(); + // Explicitly reset the connection and stream (just in case) for (auto& [type, paths_for_type] : paths) { for (auto& path : paths_for_type) { @@ -664,6 +774,14 @@ void Network::close_connections() { path.conn_info.stream.reset(); } } + paths.clear(); + + // Reset any spare first hops + for (auto& conn_info : unused_connections) { + conn_info.conn.reset(); + conn_info.stream.reset(); + } + unused_connections.clear(); update_status(ConnectionStatus::disconnected); log::info(cat, "Closed all connections."); @@ -705,9 +823,55 @@ std::shared_ptr Network::get_endpoint() { }); } +// MARK: Request Queues and Path Building + +std::vector Network::get_unused_nodes() { + if (snode_cache.size() < min_snode_cache_count) + return {}; + + // Exclude any IPs that are already in use from existing paths + std::vector node_ips_to_exlude = all_path_ips(); + + // Exclude unused connections + for (const auto& conn_info : unused_connections) + node_ips_to_exlude.emplace_back(conn_info.node.to_ipv4()); + + // Exclude in progress connections + for (const auto& conn_info : unused_connections) + node_ips_to_exlude.emplace_back(conn_info.node.to_ipv4()); + + // Exclude pending requests + for (const auto& [path_type, path_type_requests] : request_queue) + for (const auto& [info, callback] : path_type_requests) + if (auto* dest = std::get_if(&info.destination)) + node_ips_to_exlude.emplace_back(dest->to_ipv4()); + + // Populate the unused nodes with any nodes in the cache which shouldn't be excluded + std::vector result; + + if (node_ips_to_exlude.empty()) + result = snode_cache; + else + std::copy_if( + snode_cache.begin(), + snode_cache.end(), + std::back_inserter(result), + [&node_ips_to_exlude](const auto& node) { + return std::find( + node_ips_to_exlude.begin(), + node_ips_to_exlude.end(), + node.to_ipv4()) == node_ips_to_exlude.end(); + }); + + // Shuffle the `result` so anything that uses it would get random nodes + CSRNG rng; + std::shuffle(result.begin(), result.end(), rng); + + return result; +} + void Network::establish_connection( std::string request_id, - PathType path_type, service_node target, std::optional timeout, std::function error)> callback) { @@ -751,8 +915,14 @@ void Network::establish_connection( }); }); }, - [this, path_type, target, request_id, cb, cb_called, conn_future]( + [this, target, request_id, cb, cb_called, conn_future]( quic::connection_interface& conn, uint64_t error_code) mutable { + // If the instance is in the process of being destroyed then we don't want to + // interact with `net` as it's also likely in the process of being destroyed and + // trigging any of the below logic can result in bad memory access + if (being_destroyed) + return; + log::trace(cat, "Connection closed for {}.", request_id); // Just in case, call it within a `net.call` @@ -766,669 +936,559 @@ void Network::establish_connection( } }); - // When the connection is closed we update the path and reset it's connection - // info so we can recover the path later if desired - auto conn_info = conn_future.get(); - auto current_paths = paths[path_type]; - auto target_path = std::find_if( - current_paths.begin(), - current_paths.end(), - [&target](const auto& path) { - return !path.nodes.empty() && target == path.nodes.front(); - }); + // Remove the connection from `unused_connection` if present + std::erase_if(unused_connections, [&conn, &target](auto& unused_conn) { + if (unused_conn.node != target && + (!unused_conn.conn || + unused_conn.conn->reference_id() != conn.reference_id())) + return false; - if (target_path != current_paths.end() && target_path->conn_info.conn && - conn.reference_id() == target_path->conn_info.conn->reference_id()) { - target_path->conn_info.conn.reset(); - target_path->conn_info.stream.reset(); - - handle_node_error(target, path_type, target_path->conn_info, request_id); - } else if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) - // Depending on the state of the snode pool cache it's possible for certain - // errors to result in being permanently unable to establish a connection, - // to avoid this we handle those error codes and drop - handle_node_error( - target, path_type, {target, nullptr, nullptr}, request_id); - }); - }); + // Just in case reset the connection and pointers + if (unused_conn.conn) + unused_conn.conn->close_connection(); - conn_promise.set_value(c); -} + unused_conn.conn.reset(); + unused_conn.stream.reset(); + return true; + }); -// MARK: Request Queues and Path Building + // If this connection is being used in an existing path then we should drop it + // (as the path is no longer valid) + for (const auto& [path_type, paths_for_type] : paths) { + for (const auto& path : paths_for_type) { + if (!path.nodes.empty() && path.nodes.front() == target && + path.conn_info.conn && + conn.reference_id() == path.conn_info.conn->reference_id()) { + drop_path(request_id, path_type, path); + break; + } + } + } -void Network::resume_queues() { - if (suspended) { - log::info(cat, "Ignoring resume queues as network is suspended."); + // If the connection failed with a handshake timeout then the node is + // unreachable, either due to a device network issue or because the node is down + // - since we frequently refresh the snode cache it's better to assume the + // latter to avoid using a potentially bad node being used in the path so we + // want to drop the snode from the cache and any swarms + if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) { + { + std::lock_guard lock{snode_cache_mutex}; - // If we have any requests in the queue then we should trigger their callbacks - for (auto& [path_type, requests] : request_queue) - for (auto& [info, callback] : requests) - callback(false, false, error_network_suspended, "Network is suspended."); + // Update the snode failure counts + if (snode_failure_counts.erase(target.to_string()) > 0) + need_failure_counts_write = true; - // Clear the map after processing all callbacks - request_queue.clear(); - return; - } + // Remove the node from any swarms + for (auto& [key, nodes] : swarm_cache) { + auto it = std::remove(nodes.begin(), nodes.end(), target); + if (it != nodes.end()) { + nodes.erase(it, nodes.end()); + need_swarm_write = true; + } + } - // Throttle this function as it can get called very frequently and running the logic excessively - // could cause battery drain - auto time_since_last_run = std::chrono::duration_cast( - std::chrono::system_clock::now() - last_resume_queues_timestamp); - if (time_since_last_run < resume_queues_throttle_duration) { - if (!has_scheduled_resume_queues) { - has_scheduled_resume_queues = true; - - auto delay = (resume_queues_throttle_duration - time_since_last_run); - net.call_later(delay, [this]() { - has_scheduled_resume_queues = false; - resume_queues(); - }); - } + // Remove the node from the cache + auto it = std::remove(snode_cache.begin(), snode_cache.end(), target); + if (it != snode_cache.end()) { + snode_cache.erase(it, snode_cache.end()); + need_pool_write = true; + } - return; - } - last_resume_queues_timestamp = std::chrono::system_clock::now(); - - // Only generate the stats if we are actually going to log them - if (log::get_level(cat) == log::Level::trace) { - auto request_count = 0; - std::vector existing_path_type_names; - std::vector pending_path_type_names; - std::vector in_progress_path_type_names; - for (const auto& [path_type, paths_for_type] : paths) - existing_path_type_names.insert( - existing_path_type_names.end(), - paths_for_type.size(), - path_type_name(path_type, single_path_mode)); - std::transform( - path_build_queue.begin(), - path_build_queue.end(), - std::back_inserter(pending_path_type_names), - [this](const PathType& type) { return path_type_name(type, single_path_mode); }); - std::transform( - in_progress_path_builds.begin(), - in_progress_path_builds.end(), - std::back_inserter(in_progress_path_type_names), - [this](const auto& pending_build) { - return path_type_name(pending_build.second.first, single_path_mode); + if (need_failure_counts_write || need_swarm_write || need_pool_write) + need_write = true; + } + update_disk_cache_throttled(); + } }); - auto format_names = [](std::vector names) { - return (names.empty() ? "0" : "{} ({})"_format(names.size(), fmt::join(names, ", "))); - }; - for (const auto& [type, requests] : request_queue) - request_count += requests.size(); + }); - log::trace( - cat, - "Resuming queues snodes: {}, paths: {}, path_builds: " - "{}, in_progress_path_builds: {}, requests: {}.", - snode_cache.size(), - format_names(existing_path_type_names), - format_names(pending_path_type_names), - format_names(in_progress_path_type_names), - request_count); - } + conn_promise.set_value(c); +} + +void Network::establish_and_store_connection(std::string request_id) { + // If we are suspended then done try to establish a new connection + if (suspended) + return; // If we haven't set a connection status yet then do so now if (status == ConnectionStatus::unknown) update_status(ConnectionStatus::connecting); - // If the snode cache is too small then we need to update it before we try to build any paths - if (snode_cache.size() < min_snode_cache_count) { - net.call_soon([this]() { refresh_snode_cache(); }); + // Re-populate the unused nodes if it ends up being empty + if (!unused_connection_and_path_build_nodes || unused_connection_and_path_build_nodes->empty()) + unused_connection_and_path_build_nodes = get_unused_nodes(); + + // If there aren't enough unused nodes then trigger a cache refresh + if (unused_connection_and_path_build_nodes->size() < min_snode_cache_count) { + log::trace( + cat, + "Unable to establish new connection due to lack of unused nodes, refreshing snode " + "cache ({}).", + request_id); + net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); return; } - // Otherwise check if it's been too long since the last update and, if so, trigger a refresh + // Otherwise check if it's been too long since the last cache update and, if so, trigger a + // refresh auto cache_lifetime = std::chrono::duration_cast( std::chrono::system_clock::now() - last_snode_cache_update); if (cache_lifetime < 0s || cache_lifetime > snode_cache_expiration_duration) net.call_soon([this]() { refresh_snode_cache(); }); - // Schedule any path builds (or recoveries) - std::unordered_map num_pending_paths; + // If there are no in progress connections then reset the failure count + if (in_progress_connections.empty()) + connection_failures = 0; + + // Grab a node from the `unused_nodes` list to establish a connection to + auto target_node = unused_connection_and_path_build_nodes->back(); + unused_connection_and_path_build_nodes->pop_back(); - for (const auto& path_type : path_build_queue) { - num_pending_paths[path_type]++; + // Try to establish a new connection to the target (this has a 3s handshake timeout as we + // wouldn't want to use any nodes which take longer than that anyway) + log::info(cat, "Establishing connection to {} for {}.", target_node.to_string(), request_id); + in_progress_connections.emplace(request_id); - // If we have an existing path that is not valid then we should try to recover that instead - // of trying to build a new path (after triggering the recovery we remove it from - // `existing_paths` to ensure we don't try to recover it multiple times) - auto& existing_paths = paths[path_type]; - if (!existing_paths.empty()) { - auto it = std::find_if( - existing_paths.begin(), existing_paths.end(), [](const onion_path& path) { - return !path.is_valid(); + establish_connection( + request_id, + target_node, + 3s, + [this, target_node, request_id](connection_info info, std::optional) { + // If we failed to get a connection then try again after a delay (may as well try + // indefinitely because there is no way to recover from this issue) + if (!info.is_valid()) { + connection_failures++; + auto connection_retry_delay = retry_delay(connection_failures); + log::error( + cat, + "Failed to connect to {}, will try another after {}ms.", + target_node.to_string(), + connection_retry_delay.count()); + return net.call_later(connection_retry_delay, [this, request_id]() { + establish_and_store_connection(request_id); }); + } - if (it != existing_paths.end()) { - recover_path(path_type, *it); - existing_paths.erase(it); - continue; - } - } + // We were able to connect to the node so add it to the unused_connections queue + log::info( + cat, "Connection to {} valid for {}.", target_node.to_string(), request_id); + unused_connections.emplace_back(info); - // Otherwise we need to build a new path - build_path(std::nullopt, path_type); - } + // Kick off the next pending path build since we now have a valid connection + if (!path_build_queue.empty()) { + net.call_soon([this, path_type = path_build_queue.front(), request_id]() { + build_path(path_type, request_id); + }); + path_build_queue.pop_front(); + } - // Now that we've triggered all request path builds/recoveries we can clear the path_build_queue - path_build_queue.clear(); + // If there are still pending path builds but no in progress connections then kick + // off enough additional connections for remaining builds (this shouldn't happen but + // better to be safe and avoid a situation where a path build gets orphaned) + if (!path_build_queue.empty() && in_progress_connections.empty()) + for ([[maybe_unused]] const auto& _ : path_build_queue) + net.call_soon([this]() { + establish_and_store_connection(random::random_base32(4)); + }); - // If there are left over invalid paths in `paths` and we have more than the minimum number of - // required paths (including the builds/recoveries started above) then we can just drop the - // extra invalid paths - std::vector valid_paths; - std::vector invalid_paths; + // If there are no more pending requests, path builds or connections then we should + // reset the `unused_connection_and_path_build_nodes` since it shouldn't be needed + // anymore + if (request_queue.empty() && path_build_queue.empty() && + in_progress_connections.empty()) + unused_connection_and_path_build_nodes = std::nullopt; + }); +} - for (auto& [path_type, existing_paths] : paths) { - size_t pending_count = num_pending_paths[path_type]; - size_t total_count = (existing_paths.size() + pending_count); - size_t target_count = min_path_count(path_type, single_path_mode); +void Network::refresh_snode_cache_complete(std::vector nodes) { + // Shuffle the nodes so we don't have a specific order + CSRNG rng; + std::shuffle(nodes.begin(), nodes.end(), rng); - // If we don't have enough paths then do nothing - if (total_count <= target_count) - continue; + // Update the disk cache if the snode pool was updated + { + std::lock_guard lock{snode_cache_mutex}; + snode_cache = nodes; + last_snode_cache_update = std::chrono::system_clock::now(); + need_pool_write = true; + need_write = true; + } + update_disk_cache_throttled(); + + // Reset the cache refresh state + current_snode_cache_refresh_request_id = std::nullopt; + snode_cache_refresh_failure_count = 0; + in_progress_snode_cache_refresh_count = 0; + unused_snode_refresh_nodes = std::nullopt; + snode_refresh_results.reset(); + + // Run any post-refresh processes + for (const auto& callback : after_snode_cache_refresh) + net.call_soon([cb = std::move(callback)]() { cb(); }); + after_snode_cache_refresh.clear(); + + // Resume any queued path builds + for (const auto& path_type : path_build_queue) + net.call_soon([this, path_type]() { build_path(path_type); }); + path_build_queue.clear(); +} - valid_paths.clear(); - invalid_paths.clear(); +void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool reset_unused_nodes) { + if (suspended) { + log::info(cat, "Ignoring snode cache refresh as network is suspended ({}).", request_id); + return; + } - for (auto& path : existing_paths) { - if (path.is_valid()) - valid_paths.emplace_back(path); - else - invalid_paths.emplace_back(path); - } + // If the unused nodes is empty then reset it (if we are refreshing from seed nodes it means the + // local cache is not usable so we are just going to have to call this endlessly until it works) + if (reset_unused_nodes || !unused_snode_refresh_nodes || unused_snode_refresh_nodes->empty()) { + log::info( + cat, + "Existing cache is insufficient, refreshing from seed nodes ({}).", + request_id); - // Keep all valid paths and only enough invalid paths to meet the minimum requirements - size_t remaining_slots = - std::max(target_count - valid_paths.size(), static_cast(0)); - existing_paths = std::move(valid_paths); - existing_paths.insert( - existing_paths.end(), - invalid_paths.begin(), - invalid_paths.begin() + std::min(remaining_slots, invalid_paths.size())); + // Shuffle to ensure we pick random nodes to fetch from + CSRNG rng; + unused_snode_refresh_nodes = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); + std::shuffle(unused_snode_refresh_nodes->begin(), unused_snode_refresh_nodes->end(), rng); } - // Now that we've scheduled any required path builds we should try to resume any pending - // requests in case they now have a valid paths - if (!paths.empty()) { - bool need_resume = false; - std::unordered_set already_enqueued_paths; - - for (auto& [path_type, requests] : request_queue) - if (!paths[path_type].empty()) - std::erase_if( - requests, - [this, &need_resume, &already_enqueued_paths](const auto& request) { - // If there are no valid paths to send the request then enqueue a new - // path build (if needed) leave the request in the queue - if (!find_valid_path(request.first, paths[request.first.path_type])) { - if (already_enqueued_paths.emplace(request.first.path_type).second) - enqueue_path_build_if_needed(request.first.path_type, true); - need_resume = true; - return false; + auto target_node = unused_snode_refresh_nodes->back(); + unused_snode_refresh_nodes->pop_back(); + + establish_connection( + request_id, + target_node, + 3s, + [this, request_id](connection_info info, std::optional) { + // If we failed to get a connection then try again after a delay (may as well try + // indefinitely because there is no way to recover from this issue) + if (!info.is_valid()) { + snode_cache_refresh_failure_count++; + auto cache_refresh_retry_delay = retry_delay(snode_cache_refresh_failure_count); + log::error( + cat, + "Failed to connect to seed node to refresh snode cache, will retry " + "after {}ms ({}).", + cache_refresh_retry_delay.count(), + request_id); + return net.call_later(cache_refresh_retry_delay, [this, request_id]() { + refresh_snode_cache_from_seed_nodes(request_id, false); + }); + } + + get_service_nodes( + request_id, + info, + std::nullopt, + [this, request_id]( + std::vector nodes, std::optional) { + // If we got no nodes then we will need to try again + if (nodes.empty()) { + snode_cache_refresh_failure_count++; + auto cache_refresh_retry_delay = + retry_delay(snode_cache_refresh_failure_count); + log::error( + cat, + "Failed to retrieve nodes from seen node to refresh snode " + "cache, will retry after {}ms ({}).", + cache_refresh_retry_delay.count(), + request_id); + return net.call_later( + cache_refresh_retry_delay, [this, request_id]() { + refresh_snode_cache_from_seed_nodes(request_id, false); + }); } - net.call_soon( - [this, info = request.first, cb = std::move(request.second)]() { - _send_onion_request(std::move(info), std::move(cb)); - }); - return true; + log::info( + cat, + "Refreshing snode cache from seed nodes completed with {} " + "nodes ({}).", + nodes.size(), + request_id); + refresh_snode_cache_complete(nodes); }); - - if (need_resume) - net.call_soon([this]() { resume_queues(); }); - } + }); } -void Network::refresh_snode_cache() { +void Network::refresh_snode_cache(std::optional existing_request_id) { + auto request_id = existing_request_id.value_or(random::random_base32(4)); + if (suspended) { - log::info(cat, "Ignoring snode cache refresh as network is suspended."); + log::info(cat, "Ignoring snode cache refresh as network is suspended ({}).", request_id); return; } // Only allow a single cache refresh at a time - if (refreshing_snode_cache) { - log::info(cat, "Snode cache refresh ignored due to in progress refresh."); + if (current_snode_cache_refresh_request_id && + current_snode_cache_refresh_request_id != request_id) { + log::info(cat, "Snode cache refresh ignored due to in progress refresh ({}).", request_id); return; } - refreshing_snode_cache = true; - - // If we don't have enough nodes in the current cached then we need to fetch - // from the seed nodes which is a trusted source so we can update the cache - // from a single response - // - // If the current cache is large enough then we want to getch from a number - // of random nodes and use the intersection of their responses as the update - auto use_seed_node = (snode_cache.size() < min_snode_cache_count); - size_t num_requests = (use_seed_node ? 1 : num_snodes_to_refresh_cache_from); - log::info(cat, "Refreshing snode cache{}.", (use_seed_node ? " from seed nodes" : "")); - - // Define a handler function - auto processing_responses = - [this, use_seed_node, num_requests]( - std::shared_ptr>> all_nodes) { - // There are still pending requests so just wait - if (all_nodes->size() != num_requests) - return; + // Reset the snode refresh states when starting a new snode cache refresh + if (!current_snode_cache_refresh_request_id) { + log::info(cat, "Refreshing snode cache ({}).", request_id); + current_snode_cache_refresh_request_id = request_id; - auto any_nodes_request_failed = - std::any_of(all_nodes->begin(), all_nodes->end(), [](const auto& n) { - return n.empty(); - }); + // Shuffle to ensure we pick random nodes to fetch from + CSRNG rng; + unused_snode_refresh_nodes = get_unused_nodes(); + std::shuffle(unused_snode_refresh_nodes->begin(), unused_snode_refresh_nodes->end(), rng); + snode_refresh_results = std::make_shared>>(); + } - if (any_nodes_request_failed) { - // If the current cache is still usable just send a warning and don't bother - // retrying - if (!use_seed_node) { - log::warning( - cat, - "Failed to refresh snode cache due to request failing to retrieve " - "nodes"); - refreshing_snode_cache = false; - return; - } + // If we don't have enough nodes in the unused nodes it likely means we didn't have + // enough nodes in the cache so instead just fetch from the seed nodes (which is a + // trusted source so we can update the cache from a single response) + if (!unused_snode_refresh_nodes || unused_snode_refresh_nodes->size() < min_snode_cache_count) + return refresh_snode_cache_from_seed_nodes(request_id, true); + + // Target an unused node and increment the in progress refresh counter + auto target_node = unused_snode_refresh_nodes->back(); + unused_snode_refresh_nodes->pop_back(); + in_progress_snode_cache_refresh_count++; + + // If there are still more concurrent refresh_snode_cache requests we want to trigger then + // trigger the next one to run in the next run loop + if (in_progress_snode_cache_refresh_count < num_snodes_to_refresh_cache_from) + net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); + + // Prepare and send the request to retrieve service nodes + nlohmann::json payload{ + {"method", "get_service_nodes"}, + {"params", detail::get_service_nodes_params(std::nullopt)}, + }; + auto info = request_info::make( + target_node, + quic::DEFAULT_TIMEOUT, + ustring{quic::to_usv(payload.dump())}, + std::nullopt, + PathType::standard, + request_id); + _send_onion_request( + info, + [this, request_id]( + bool success, bool timeout, int16_t, std::optional response) { + // Update the in progress request count + in_progress_snode_cache_refresh_count--; - // Otherwise schedule a retry after a short delay + try { + if (!success || timeout || !response) + throw std::runtime_error{response.value_or("Unknown error.")}; + if (!snode_refresh_results) + throw std::runtime_error{"Invalid result pointer."}; + + nlohmann::json response_json = nlohmann::json::parse(*response); + std::vector result = + detail::process_get_service_nodes_response(*response); + snode_refresh_results->emplace_back(result); + } catch (...) { + // The request failed so increment the failure counter and retry after a short + // delay snode_cache_refresh_failure_count++; - refreshing_snode_cache = false; auto cache_refresh_retry_delay = retry_delay(snode_cache_refresh_failure_count); log::error( cat, - "Failed to refresh snode cache{} due to request failing to retrieve " - "nodes, will retry after: {}ms", - (use_seed_node ? " from seed nodes" : ""), - cache_refresh_retry_delay.count()); - net.call_later(cache_refresh_retry_delay, [this]() { refresh_snode_cache(); }); + "Failed to retrieve nodes from one target when refresh snode cache " + "due, will try another target after {}ms ({}).", + cache_refresh_retry_delay.count(), + request_id); + net.call_later(cache_refresh_retry_delay, [this, request_id]() { + refresh_snode_cache(request_id); + }); + return; + } + + // If we haven't received all results then do nothing + if (snode_refresh_results->size() != num_snodes_to_refresh_cache_from) + return; + + auto any_nodes_request_failed = std::any_of( + snode_refresh_results->begin(), + snode_refresh_results->end(), + [](const auto& n) { return n.empty(); }); + + // If the current cache is still usable just send a warning and don't bother + // retrying + if (any_nodes_request_failed) { + log::warning(cat, "Failed to refresh snode cache ({}).", request_id); + current_snode_cache_refresh_request_id = std::nullopt; + snode_cache_refresh_failure_count = 0; + in_progress_snode_cache_refresh_count = 0; + unused_snode_refresh_nodes = std::nullopt; + snode_refresh_results.reset(); return; } // Sort the vectors (so make it easier to find the intersection) - for (auto& nodes : *all_nodes) + for (auto& nodes : *snode_refresh_results) std::stable_sort(nodes.begin(), nodes.end()); - auto nodes = (*all_nodes)[0]; + auto nodes = (*snode_refresh_results)[0]; // If we triggered multiple requests then get the intersection of all vectors - if (all_nodes->size() > 1) { - for (size_t i = 1; i < all_nodes->size(); ++i) { + if (snode_refresh_results->size() > 1) { + for (size_t i = 1; i < snode_refresh_results->size(); ++i) { std::vector temp; std::set_intersection( nodes.begin(), nodes.end(), - (*all_nodes)[i].begin(), - (*all_nodes)[i].end(), + (*snode_refresh_results)[i].begin(), + (*snode_refresh_results)[i].end(), std::back_inserter(temp), [](const auto& a, const auto& b) { return a == b; }); nodes = std::move(temp); } } - // Shuffle the nodes so we don't have a specific order - CSRNG rng; - std::shuffle(nodes.begin(), nodes.end(), rng); - - // Update the disk cache if the snode pool was updated - { - std::lock_guard lock{snode_cache_mutex}; - snode_cache = nodes; - last_snode_cache_update = std::chrono::system_clock::now(); - need_pool_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - - // Resume any queued processes - log::info(cat, "Refreshing snode cache completed with {} nodes.", nodes.size()); - refreshing_snode_cache = false; - - for (const auto& callback : after_snode_cache_refresh) - net.call_soon([cb = std::move(callback)]() { cb(); }); - after_snode_cache_refresh.clear(); - net.call_soon([this]() { resume_queues(); }); - }; - auto handle_response = - [processing_responses]( - std::shared_ptr>> all_nodes) { - return [all_nodes, processing_responses]( - std::vector nodes, std::optional) { - all_nodes->emplace_back(nodes); - processing_responses(all_nodes); - }; - }; - - std::vector nodes; - - if (use_seed_node) - nodes = (use_testnet ? seed_nodes_testnet : seed_nodes_mainnet); - else - nodes = snode_cache; - - // Just in case, make sure we actually have nodes to send requests to - if (nodes.empty()) { - log::error(cat, "Failed to refresh snode cache from seed nodes: Insufficient seed nodes."); - refreshing_snode_cache = false; - return; - } - - // Shuffle to ensure we pick random nodes to fetch from - CSRNG rng; - std::shuffle(nodes.begin(), nodes.end(), rng); - - // Kick off the requests concurrently - // - // It's possible, even likely, that some of the nodes could be unavailable so - // we want to try to cycle through a few nodes if any of the requests fail - // so calculate how many we can cycle through - size_t num_attempts = std::min(nodes.size() / num_requests, snode_cache_refresh_retries); - auto all_nodes = std::make_shared>>(); - all_nodes->reserve(num_requests); - - for (size_t i = 0; i < num_requests; ++i) { - std::vector chunk( - nodes.begin() + (i * num_attempts), nodes.begin() + ((i + 1) * num_attempts)); - - get_service_nodes_recursive( - "Refresh Snode Cache (Node {})"_format(i + 1), - chunk, - std::nullopt, - handle_response(all_nodes)); - } + log::info( + cat, + "Refreshing snode cache completed with {} nodes ({}).", + nodes.size(), + request_id); + refresh_snode_cache_complete(nodes); + }); } -void Network::build_path(std::optional existing_request_id, PathType path_type) { +void Network::build_path(PathType path_type, std::optional existing_request_id) { if (suspended) { - log::info(cat, "Ignoring path build request as network is suspended."); + log::info(cat, "Ignoring build_path call as network is suspended."); return; } auto request_id = existing_request_id.value_or(random::random_base32(4)); auto path_name = path_type_name(path_type, single_path_mode); - // Check that we have enough snodes before continuing (shouldn't be an issue but if something - // calls `build_path` in the future then this drive it back into the standard loop) - if (snode_cache.size() < min_snode_cache_count) { - log::warning( + // If we don't have an unused connection for the first hop then enqueue the path build and + // establish a new connection + if (unused_connections.empty()) { + log::info( cat, - "Re-queing {} path build due to insufficient nodes ({}).", + "No unused connections available to build {} path, creating new connection ({}).", path_name, request_id); - in_progress_path_builds.erase(request_id); path_build_queue.emplace_back(path_type); - net.call_soon([this]() { resume_queues(); }); - return; + return net.call_soon([this, request_id]() { establish_and_store_connection(request_id); }); } - CSRNG rng; - log::info(cat, "Building {} path ({}).", path_name, request_id); - - // If we don't have any in-progress path builds then reset the 'unused_path_build_nodes' - // to be the `snode_cache` minus nodes in any current paths - if (in_progress_path_builds.empty()) { - std::vector existing_path_node_ips = all_path_ips(); - std::copy_if( - snode_cache.begin(), - snode_cache.end(), - std::back_inserter(unused_path_build_nodes), - [&existing_path_node_ips](const auto& node) { - return std::find( - existing_path_node_ips.begin(), - existing_path_node_ips.end(), - node.to_ipv4()) == existing_path_node_ips.end(); - }); - - // Shuffle the `unused_path_build_nodes` value so we build paths from random nodes - std::shuffle(unused_path_build_nodes.begin(), unused_path_build_nodes.end(), rng); - general_path_build_failures = 0; - } + // Reset the unused nodes list if it's too small + if ((!unused_connection_and_path_build_nodes || + unused_connection_and_path_build_nodes->size() < path_size)) + unused_connection_and_path_build_nodes = get_unused_nodes(); - // Make sure we have possible guard nodes before continuing - if (unused_path_build_nodes.empty()) { - log::warning( + // If we still don't have enough unused nodes then we need to refresh the cache + if (unused_connection_and_path_build_nodes->size() < path_size) { + std::cout << "RAWR build_path need a snode cache: " + request_id << std::endl; + log::info( cat, - "Unable to build {} path due to lack of possible guard nodes ({}).", + "Re-queing {} path build due to insufficient nodes ({}).", path_name, request_id); - general_path_build_failures++; + path_build_failures = 0; path_build_queue.emplace_back(path_type); - - auto delay = retry_delay(general_path_build_failures); - net.call_later(delay, [this]() { resume_queues(); }); + net.call_soon([this]() { refresh_snode_cache(); }); return; } - // Add this build to the `in_progress_path_builds` map if it doesn't already exist - in_progress_path_builds.try_emplace(request_id, path_type, 0); - - // Exclude nodes targeted in the request_queue from the path (so any queued requests will be - // able to be sent once the path is built) - std::vector nodes_to_exclude; - - for (const auto& [info, callback] : request_queue[path_type]) - if (auto* dest = std::get_if(&info.destination)) - nodes_to_exclude.emplace_back(*dest); - - // Get the possible guard nodes - service_node target_node = unused_path_build_nodes.back(); - - if (nodes_to_exclude.empty()) - unused_path_build_nodes.pop_back(); - else { - auto it = std::find_if( - unused_path_build_nodes.begin(), - unused_path_build_nodes.end(), - [&nodes_to_exclude](const service_node& node) { - return std::find(nodes_to_exclude.begin(), nodes_to_exclude.end(), node) == - nodes_to_exclude.end(); - }); + // Build the path + log::info(cat, "Building {} path ({}).", path_name, request_id); - if (it == unused_path_build_nodes.end()) { - log::warning( + auto conn_info = std::move(unused_connections.front()); + unused_connections.pop_front(); + std::vector path_nodes = {conn_info.node}; + + while (path_nodes.size() < path_size) { + if (unused_connection_and_path_build_nodes->empty()) { + // Log the error and try build again after a slight delay + log::info( cat, - "Unable to build paths due to lack of possible guard nodes ({}).", + "Unable to build {} path due to lack of suitable unused path build nodes ({}).", + path_name, request_id); - general_path_build_failures++; - path_build_queue.emplace_back(path_type); - in_progress_path_builds.erase(request_id); - auto delay = retry_delay(general_path_build_failures); - net.call_later(delay, [this]() { resume_queues(); }); + // Delay the next path build attempt based on the error we received + path_build_failures++; + unused_connections.push_front(std::move(conn_info)); + auto delay = retry_delay(path_build_failures); + net.call_later( + delay, [this, request_id, path_type]() { build_path(path_type, request_id); }); return; } - target_node = *it; - unused_path_build_nodes.erase(it); - } - - // Make a request to the guard node to ensure it's reachable - log::info(cat, "Testing guard snode: {} for {}", target_node.to_string(), request_id); - - get_snode_version( - request_id, - path_type, - target_node, - 3s, - [this, path_name, path_type, target_node, request_id]( - std::vector version, - connection_info info, - std::optional error) { - log::trace(cat, "Got snode version response for {}.", request_id); - - try { - if (version.empty()) - throw std::runtime_error{"Testing {} for {} failed with error: {}"_format( - target_node.to_string(), - request_id, - error.value_or("Unknown Error"))}; - - // Build the new paths - log::info( - cat, - "Guard snode {} valid for {}.", - target_node.to_string(), - request_id); - std::vector path_nodes{info.node}; - - while (path_nodes.size() < path_size) { - if (unused_path_build_nodes.empty()) - throw std::runtime_error{ - "Unable to build {} path due to lack of unused path build nodes ({})."_format( - path_name, request_id)}; - - // Grab the next unused node to continue building the path - auto node = unused_path_build_nodes.back(); - unused_path_build_nodes.pop_back(); - - // Ensure we don't put two nodes with the same IP into the same path - auto snode_with_ip_it = std::find_if( - path_nodes.begin(), - path_nodes.end(), - [&node](const auto& existing_node) { - return existing_node.to_ipv4() == node.to_ipv4(); - }); - - if (snode_with_ip_it == path_nodes.end()) - path_nodes.push_back(node); - } - - // Store the new path - auto path = onion_path{std::move(info), path_nodes, 0}; - paths[path_type].emplace_back(path); - - // Log that a path was built - std::vector node_descriptions; - std::transform( - path_nodes.begin(), - path_nodes.end(), - std::back_inserter(node_descriptions), - [](const service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info( - cat, - "Built new {} onion request path ({}): [{}]", - path_name, - request_id, - path_description); - - // If the connection info is valid and it's a standard path then update the - // connection status to connected - if (path_type == PathType::standard) { - update_status(ConnectionStatus::connected); - - // If a paths_changed callback was provided then call it - if (paths_changed) { - std::vector> raw_paths; - for (auto& path : paths[path_type]) - raw_paths.emplace_back(path.nodes); - - paths_changed(raw_paths); - } - } + // Grab the next unused node to continue building the path + auto node = unused_connection_and_path_build_nodes->back(); + unused_connection_and_path_build_nodes->pop_back(); - // If the network happened to get suspended just before the guard node was - // connected then we may need to reset the connection info so the new path is - // also in a suspended state - if (suspended) { - log::info( - cat, - "Network is suspended, suspending new {} path ({})", - path_name, - request_id); - info.conn.reset(); - info.stream.reset(); - } + // Ensure we don't put two nodes with the same IP into the same path + auto snode_with_ip_it = std::find_if( + path_nodes.begin(), path_nodes.end(), [&node](const auto& existing_node) { + return existing_node.to_ipv4() == node.to_ipv4(); + }); - // Resume any queued requests - in_progress_path_builds.erase(request_id); - net.call_soon([this]() { resume_queues(); }); - } catch (const std::exception& e) { - // Log the error and loop after a slight delay (don't want to drain the pool - // too quickly if the network goes down) - log::info(cat, "{}", e.what()); - - // Delay the next path build attempt based on the error we received - auto& [_, failure_count] = - in_progress_path_builds.try_emplace(request_id, path_type, 0) - .first->second; - failure_count += 1; - auto delay = retry_delay(failure_count); - net.call_later(delay, [this, request_id, path_type]() { - build_path(request_id, path_type); - }); - } - }); -} + if (snode_with_ip_it == path_nodes.end()) + path_nodes.push_back(node); + } -void Network::recover_path(PathType path_type, onion_path path) { - // Try to re-establish a connection to the guard node - auto request_id = random::random_base32(4); - log::trace( + // Store the new path + auto path = onion_path{std::move(conn_info), path_nodes, 0}; + paths[path_type].emplace_back(path); + + // Log that a path was built + std::vector node_descriptions; + std::transform( + path_nodes.begin(), + path_nodes.end(), + std::back_inserter(node_descriptions), + [](const service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + log::info( cat, - "Connection to {} for {} path no longer valid, attempting reconnection ({}).", - path.nodes.front(), - path_type_name(path_type, single_path_mode), - request_id); - - establish_connection( + "Built new onion request path, now have {} {} path(s) ({}): [{}]", + paths[path_type].size(), + path_name, request_id, - path_type, - path.nodes.front(), - 3s, - [this, request_id, path_type, old_path = path]( - connection_info info, std::optional error) { - auto guard_node = old_path.nodes.front(); + path_description); - if (!info.is_valid()) { - log::info( - cat, - "Reconnection to {} for {} path failed ({}) with error: {}.", - guard_node, - path_type_name(path_type, single_path_mode), - request_id, - error.value_or("Unknown error")); - - // If we failed to reconnect to the path, and it was a standard path, then we - // should call 'paths_changed' as it is has now been "officially" dropped - if (path_type == PathType::standard && paths_changed) { - std::vector> raw_paths; - for (auto& path : paths[path_type]) - raw_paths.emplace_back(path.nodes); - - paths_changed(raw_paths); - } + // If the connection info is valid and it's a standard path then update the + // connection status to connected + if (path_type == PathType::standard) { + update_status(ConnectionStatus::connected); - // Now enqueue a new path build (if needed) for the type of path we dropped and - // resume the queues to trigger the path build (if needed) - enqueue_path_build_if_needed(path_type, false); - net.call_soon([this]() { resume_queues(); }); - return; - } + // If a paths_changed callback was provided then call it + if (paths_changed) { + std::vector> raw_paths; + for (auto& path : paths[path_type]) + raw_paths.emplace_back(path.nodes); - // Knowing that the reconnection succeeded is helpful for debugging - log::info( - cat, - "Reconnection to {} for {} path successful ({}).", - guard_node, - path_type_name(path_type, single_path_mode), - request_id); + paths_changed(raw_paths); + } + } - // If the connection info is valid and it's a standard path then update the - // connection status back to connected - if (path_type == PathType::standard) - update_status(ConnectionStatus::connected); + // If there are pending requests which this path is valid for then resume them + std::erase_if(request_queue[path_type], [this, &path](const auto& request) { + if (!find_valid_path(request.first, {path})) + return false; - // No need to call the 'paths_changed' callback as the paths haven't - // actually changed, just their connection info - paths[path_type].emplace_back(onion_path{std::move(info), old_path.nodes, 0}); + net.call_soon([this, info = request.first, cb = std::move(request.second)]() { + _send_onion_request(std::move(info), std::move(cb)); + }); + return true; + }); - // Resume any queued requests - net.call_soon([this]() { resume_queues(); }); - }); + // If there are still pending requests and there are no pending path builds for them then kick + // off a subsequent path build in an effort to resume the remaining requests + if (!request_queue[path_type].empty()) + net.call_soon([this, path_type]() { build_path(path_type); }); + else + request_queue.erase(path_type); + + // If there are no more pending requests, path builds or connections then we should reset the + // `unused_connection_and_path_build_nodes` since it shouldn't be needed anymore + if (request_queue.empty() && path_build_queue.empty() && in_progress_connections.empty()) + unused_connection_and_path_build_nodes = std::nullopt; } std::optional Network::find_valid_path( @@ -1477,217 +1537,83 @@ std::optional Network::find_valid_path( return possible_paths.front(); }; -void Network::enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable) { - auto current_paths = paths[path_type].size(); - - // In `single_path_mode` we never build additional paths - if (current_paths > 0 && single_path_mode) - return; +void Network::enqueue_path_build_if_needed( + PathType path_type, std::optional found_path) { + net.call([this, path_type, found_path]() { + auto current_paths = paths[path_type]; - // Get the number pending paths - auto pending_build_count = - std::count(path_build_queue.begin(), path_build_queue.end(), path_type); - auto in_progress_build_count = std::count_if( - in_progress_path_builds.begin(), - in_progress_path_builds.end(), - [&path_type](const auto& pair) { return pair.second.first == path_type; }); - auto pending_paths = (pending_build_count + in_progress_build_count); - - // We only want to enqueue a new path build if: - // - We don't have the minimum number of paths for the specified type - // - We don't have any pending builds - // - The current paths are unsuitable for the request - auto min_paths = min_path_count(path_type, single_path_mode); - - if ((current_paths + pending_paths) < min_paths || - (existing_paths_unsuitable && pending_paths == 0)) - path_build_queue.emplace_back(path_type); -} + // In `single_path_mode` we never build additional paths + if (current_paths.size() > 0 && single_path_mode) + return; -// MARK: Multi-request logic + // We only want to enqueue a new path build if: + // - We don't have the minimum number of paths for the specified type + // - We don't have any pending builds + // - The current paths are unsuitable for the request + auto min_paths = min_path_count(path_type, single_path_mode); -void Network::get_service_nodes_recursive( - std::string request_id, - std::vector target_nodes, - std::optional limit, - std::function nodes, std::optional error)> - callback) { - if (target_nodes.empty()) - return callback({}, "No nodes to fetch from provided."); + // If we have enough existing paths and found a valid path then no need to build more paths + if (found_path && current_paths.size() >= min_paths) + return; - auto target_node = target_nodes.front(); - get_service_nodes( - request_id, - target_node, - limit, - [this, limit, target_nodes, request_id, cb = std::move(callback)]( - std::vector nodes, std::optional error) { - // If we got nodes then stop looping and return them - if (!nodes.empty()) { - cb(nodes, error); - return; - } + // Get the number pending paths + auto pending_paths = + std::count(path_build_queue.begin(), path_build_queue.end(), path_type); - // Loop if we didn't get any nodes - std::vector remaining_nodes( - target_nodes.begin() + 1, target_nodes.end()); - net.call_soon([this, request_id, remaining_nodes, limit, cb = std::move(cb)]() { - get_service_nodes_recursive(request_id, remaining_nodes, limit, cb); - }); - }); + // If we don't have enough current + pending paths, or the request couldn't be sent then + // kick off a new path build + if ((current_paths.size() + pending_paths) < min_paths || + (!found_path && pending_paths == 0)) + net.call_soon([this, path_type]() { build_path(path_type); }); + }); } -// MARK: Pre-Defined Requests +// MARK: Direct Requests void Network::get_service_nodes( std::string request_id, - service_node node, + connection_info conn_info, std::optional limit, std::function nodes, std::optional error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - establish_connection( - request_id, - PathType::standard, - node, - 3s, - [this, request_id, limit, cb = std::move(callback)]( - connection_info info, std::optional error) { - if (!info.is_valid()) - return cb({}, error.value_or("Unknown error.")); - - nlohmann::json params{ - {"active_only", true}, - {"fields", - {{"public_ip", true}, - {"pubkey_ed25519", true}, - {"storage_lmq_port", true}, - {"storage_server_version", true}}}}; - - if (limit) - params["limit"] = *limit; - - oxenc::bt_dict_producer payload; - payload.append("endpoint", "get_service_nodes"); - payload.append("params", params.dump()); - - info.stream->command( - "oxend_request", - payload.view(), - [this, request_id, cb = std::move(cb)](quic::message resp) { - log::trace( - cat, - "{} got response for {}.", - __PRETTY_FUNCTION__, - request_id); - std::vector result; - - try { - auto [status_code, body] = validate_response(resp, true); - - oxenc::bt_list_consumer result_bencode{body}; - result_bencode - .skip_value(); // Skip the status code (already validated) - auto response_dict = result_bencode.consume_dict_consumer(); - response_dict.skip_until("result"); - - auto result_dict = response_dict.consume_dict_consumer(); - result_dict.skip_until("service_node_states"); - - // Process the node list - auto node = result_dict.consume_list_consumer(); - - while (!node.is_finished()) { - auto node_consumer = node.consume_dict_consumer(); - auto pubkey_ed25519 = oxenc::from_hex( - consume_string(node_consumer, "pubkey_ed25519")); - auto public_ip = consume_string(node_consumer, "public_ip"); - auto storage_lmq_port = consume_integer( - node_consumer, "storage_lmq_port"); - - std::vector storage_server_version; - node_consumer.skip_until("storage_server_version"); - auto version_consumer = node_consumer.consume_list_consumer(); - - while (!version_consumer.is_finished()) { - storage_server_version.emplace_back( - version_consumer.consume_integer()); - } - - result.emplace_back( - pubkey_ed25519, - storage_server_version, - public_ip, - storage_lmq_port); - } - } catch (const std::exception& e) { - return cb({}, e.what()); - } + if (!conn_info.is_valid()) + return callback({}, "Connection is not valid."); - // Output the result - cb(result, std::nullopt); - }); - }); -} + oxenc::bt_dict_producer payload; + payload.append("endpoint", "get_service_nodes"); + payload.append("params", detail::get_service_nodes_params(limit).dump()); -void Network::get_snode_version( - std::string request_id, - PathType path_type, - service_node node, - std::optional timeout, - std::function version, connection_info info, std::optional error)> - callback) { - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - establish_connection( - request_id, - path_type, - node, - timeout, - [this, request_id, timeout, cb = std::move(callback)]( - connection_info info, std::optional error) { - if (!info.is_valid()) - return cb({}, info, error.value_or("Unknown error.")); - - oxenc::bt_dict_producer payload; - info.stream->command( - "info", - payload.view(), - timeout, - [this, info, request_id, cb = std::move(cb)](quic::message resp) { - log::trace( - cat, - "{} got response for {}.", - __PRETTY_FUNCTION__, - request_id); - std::vector version; - - try { - auto [status_code, body] = validate_response(resp, true); - - oxenc::bt_list_consumer result_bencode{body}; - result_bencode - .skip_value(); // Skip the status code (already validated) - auto response_dict = result_bencode.consume_dict_consumer(); - response_dict.skip_until("version"); - auto version_list = response_dict.consume_list_consumer(); - - while (!version_list.is_finished()) - version.emplace_back(version_list.consume_integer()); - } catch (const std::exception& e) { - return cb({}, info, e.what()); - } + conn_info.stream->command( + "oxend_request", + payload.view(), + [this, request_id, cb = std::move(callback)](quic::message resp) { + log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); + std::vector result; - // Output the result - cb(version, info, std::nullopt); - }); + try { + auto [status_code, body] = validate_response(resp, true); + oxenc::bt_list_consumer result_bencode{body}; + result = detail::process_get_service_nodes_response(result_bencode); + } catch (const std::exception& e) { + return cb({}, e.what()); + } + + // Output the result + cb(result, std::nullopt); }); } +// MARK: Node Retrieval + void Network::get_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::function swarm)> callback) { + // If the network is in the process of being shut down just ignore the call + if (being_destroyed) + return; + auto request_id = random::random_base32(4); log::trace(cat, "{} called for {} as {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex(), request_id); @@ -1706,8 +1632,9 @@ void Network::get_swarm( // If we have no snode cache then we need to rebuild the cache and run this request again // once it's rebuild if (snode_cache.empty()) { - after_snode_cache_refresh.emplace_back( - [this, swarm_pubkey, cb = std::move(cb)]() { get_swarm(swarm_pubkey, cb); }); + after_snode_cache_refresh.emplace_back([this, swarm_pubkey, cb = std::move(cb)]() { + get_swarm(swarm_pubkey, std::move(cb)); + }); net.call_soon([this]() { refresh_snode_cache(); }); return; } @@ -1727,18 +1654,17 @@ void Network::get_swarm( ustring{quic::to_usv(payload.dump())}, swarm_pubkey, PathType::standard, - std::nullopt, request_id); _send_onion_request( info, - [this, swarm_pubkey, request_id, cb = std::move(cb)]( + [this, hex_key = swarm_pubkey.hex(), request_id, cb = std::move(cb)]( bool success, bool timeout, int16_t, std::optional response) { log::trace( cat, "{} got response for {} as {}.", __PRETTY_FUNCTION__, - swarm_pubkey.hex(), + hex_key, request_id); if (!success || timeout || !response) return cb({}); @@ -1759,16 +1685,14 @@ void Network::get_swarm( } // Update the cache - log::info(cat, "Retrieved swarm for {} ({}).", swarm_pubkey.hex(), request_id); - net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[hex_key] = swarm; - need_swarm_write = true; - need_write = true; - } - snode_cache_cv.notify_one(); - }); + log::info(cat, "Retrieved swarm for {} ({}).", hex_key, request_id); + { + std::lock_guard lock{snode_cache_mutex}; + swarm_cache[hex_key] = swarm; + need_swarm_write = true; + need_write = true; + } + update_disk_cache_throttled(); cb(swarm); }); @@ -1777,6 +1701,10 @@ void Network::get_swarm( void Network::set_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { + // If the network is in the process of being shut down just ignore the call + if (being_destroyed) + return; + net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; @@ -1784,12 +1712,16 @@ void Network::set_swarm( need_swarm_write = true; need_write = true; } - snode_cache_cv.notify_one(); + update_disk_cache_throttled(); }); } void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { + // If the network is in the process of being shut down just ignore the call + if (being_destroyed) + return; + auto request_id = random::random_base32(4); log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); @@ -1831,14 +1763,18 @@ void Network::send_request( [this, info, conn_info, cb = std::move(handle_response)](quic::message resp) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); + std::pair result; + auto& [status_code, body] = result; + try { - auto [status_code, body] = validate_response(resp, false); - cb(true, false, status_code, body); + result = validate_response(resp, false); } catch (const status_code_exception& e) { - handle_errors(info, conn_info, false, e.status_code, e.what(), cb); + return handle_errors(info, conn_info, false, e.status_code, e.what(), cb); } catch (const std::exception& e) { - handle_errors(info, conn_info, resp.timed_out, -1, e.what(), cb); + return handle_errors(info, conn_info, resp.timed_out, -1, e.what(), cb); } + + cb(true, false, status_code, body); }); } @@ -1860,35 +1796,30 @@ void Network::send_onion_request( } void Network::_send_onion_request(request_info info, network_response_callback_t handle_response) { + // If the network is in the process of being shut down just ignore the call + if (being_destroyed) + return; + auto path_name = path_type_name(info.path_type, single_path_mode); log::trace(cat, "{} called for {} path ({}).", __PRETTY_FUNCTION__, path_name, info.request_id); - // Try to retrieve a valid path for this request - auto [path, callback] = net.call_get( - [this, info, cb = std::move(handle_response)]() mutable - -> std::pair, std::optional> { - // If the network is suspended then fail immediately - if (suspended) { - cb(false, false, error_network_suspended, "Network is suspended."); - return {std::nullopt, std::nullopt}; - } - - // Otherwise try to retrieve a valid path - if (auto path = find_valid_path(info, paths[info.path_type])) - return {path, std::move(cb)}; + // Try to retrieve a valid path for this request, if we can't get one then add the request to + // the queue to be run once a path for it has successfully been built + auto path = net.call_get([this, info]() { + auto result = find_valid_path(info, paths[info.path_type]); + enqueue_path_build_if_needed(info.path_type, result); + return result; + }); - // Currently there are no valid paths so enqueue a new build (if needed) and add the - // request to the queue to be run one the path build completes - request_queue[info.path_type].emplace_back(std::move(info), std::move(cb)); - enqueue_path_build_if_needed(info.path_type, true); - net.call_soon([this]() { resume_queues(); }); - return {std::nullopt, std::nullopt}; - }); + if (!path) { + return net.call([this, info = std::move(info), cb = std::move(handle_response)]() { + // If the network is suspended then fail immediately + if (suspended) + return cb(false, false, error_network_suspended, "Network is suspended."); - // If either of these don't exist then we will have added this request to the queue to be run - // one we have a valid path - if (!path || !callback) - return; + request_queue[info.path_type].emplace_back(std::move(info), std::move(cb)); + }); + } log::trace(cat, "{} got {} path for {}.", __PRETTY_FUNCTION__, path_name, info.request_id); @@ -1907,14 +1838,18 @@ void Network::_send_onion_request(request_info info, network_response_callback_t auto payload = builder.generate_payload(info.original_body); info.body = builder.build(payload); } catch (const std::exception& e) { - return (*callback)(false, false, error_building_onion_request, e.what()); + return handle_response(false, false, error_building_onion_request, e.what()); } // Actually send the request send_request( info, path->conn_info, - [this, builder = std::move(builder), info, path = *path, cb = std::move(*callback)]( + [this, + builder = std::move(builder), + info, + path = *path, + cb = std::move(handle_response)]( bool success, bool timeout, int16_t status_code, @@ -1954,16 +1889,9 @@ void Network::_send_onion_request(request_info info, network_response_callback_t "non-success status " "code.")}; - // For debugging purposes if the error was a redirect retry then - // we want to log that the retry was successful as this will - // help identify how often we are receiving incorrect 421 errors - if (info.retry_reason == request_info::RetryReason::redirect) - log::info( - cat, - "Received valid response after 421 retry in " - "request {} for {}.", - info.request_id, - path_type_name(info.path_type, single_path_mode)); + // For debugging purposes we want to add a log if this was a successful request + // after we did an automatic retry + detail::log_retry_result_if_needed(info, single_path_mode); // Try process the body in case it was a batch request which // failed @@ -2262,6 +2190,38 @@ std::pair Network::validate_response(quic::message resp, return {status_code, response_string}; } +void Network::drop_path(std::string request_id, PathType path_type, onion_path path) { + // Close the connection immediately (just in case there are other requests happening) + if (path.conn_info.conn) + path.conn_info.conn->close_connection(); + + auto path_nodes = path.nodes; + path.conn_info.conn.reset(); + path.conn_info.stream.reset(); + paths[path_type].erase( + std::remove(paths[path_type].begin(), paths[path_type].end(), path), + paths[path_type].end()); + + std::vector node_descriptions; + std::transform( + path_nodes.begin(), + path_nodes.end(), + std::back_inserter(node_descriptions), + [](service_node& node) { return node.to_string(); }); + auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); + log::info( + cat, + "Dropping path, now have {} {} paths(s) ({}): [{}]", + paths[path_type].size(), + path_type_name(path_type, single_path_mode), + request_id, + path_description); + + // Update the network status if we've removed all standard paths + if (paths[PathType::standard].empty()) + update_status(ConnectionStatus::disconnected); +} + void Network::handle_node_error( service_node node, PathType path_type, connection_info conn_info, std::string request_id) { handle_errors( @@ -2300,10 +2260,10 @@ void Network::handle_errors( path_name); auto updated_info = info; updated_info.retry_reason = request_info::RetryReason::decryption_failure; - request_queue[updated_info.path_type].emplace_back( - updated_info, std::move(*handle_response)); - net.call_soon([this]() { resume_queues(); }); - return; + return net.call_soon([this, updated_info, cb = std::move(*handle_response)]() { + _send_onion_request(updated_info, std::move(cb)); + }); + } } // A number of server errors can return HTML data but no status code, we want to extract those @@ -2413,10 +2373,9 @@ void Network::handle_errors( auto updated_info = info; updated_info.destination = *random_node; updated_info.retry_reason = request_info::RetryReason::redirect; - request_queue[updated_info.path_type].emplace_back( - updated_info, std::move(*handle_response)); - net.call_soon([this]() { resume_queues(); }); - return; + return net.call_soon([this, updated_info, cb = std::move(*handle_response)]() { + _send_onion_request(updated_info, std::move(cb)); + }); } if (!response) @@ -2449,7 +2408,7 @@ void Network::handle_errors( need_swarm_write = true; need_write = true; } - snode_cache_cv.notify_one(); + update_disk_cache_throttled(); return (*handle_response)(false, false, status_code, response); } catch (...) { } @@ -2636,32 +2595,12 @@ void Network::handle_errors( }); // Drop the path if invalid - if (updated_path.failure_count >= path_failure_threshold) { - // Close the connection immediately (just in case there are other requests happening) - if (path_it->conn_info.conn) - path_it->conn_info.conn->close_connection(); - - auto path_nodes = path_it->nodes; - path_it->conn_info.conn.reset(); - path_it->conn_info.stream.reset(); - paths[info.path_type].erase(path_it); - - std::vector node_descriptions; - std::transform( - path_nodes.begin(), - path_nodes.end(), - std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); - log::info(cat, "Dropping {} path ({}): [{}]", path_name, info.request_id, path_description); - } else + if (updated_path.failure_count >= path_failure_threshold) + drop_path(info.request_id, info.path_type, *path_it); + else std::replace( paths[info.path_type].begin(), paths[info.path_type].end(), *path_it, updated_path); - // Update the network status if we've removed all standard paths - if (paths[PathType::standard].empty()) - update_status(ConnectionStatus::disconnected); - // Update the snode cache { std::lock_guard lock{snode_cache_mutex}; @@ -2682,7 +2621,7 @@ void Network::handle_errors( need_write = true; } - snode_cache_cv.notify_one(); + update_disk_cache_throttled(); if (handle_response) (*handle_response)(false, false, status_code, response); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 5b59a46b..873bdf1f 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -28,11 +28,9 @@ namespace session::network { class TestNetwork : public Network { public: std::unordered_map call_counts; - bool ignore_resume_queues = false; - bool ignore_get_snode_version = false; + std::vector calls_to_ignore; std::chrono::milliseconds retry_delay_value = 0ms; - std::optional, connection_info, std::optional>> - get_snode_version_response; + std::optional> find_valid_path_response; TestNetwork( std::optional cache_path, @@ -58,6 +56,14 @@ class TestNetwork : public Network { last_snode_cache_update = (std::chrono::system_clock::now() - 10s); } + void set_unused_connections(std::deque unused_connections_) { + unused_connections = unused_connections_; + } + + void set_in_progress_connections(std::unordered_set in_progress_connections_) { + in_progress_connections = in_progress_connections_; + } + void add_path(PathType path_type, std::vector nodes) { paths[path_type].emplace_back(onion_path{{nodes[0], nullptr, nullptr}, nodes, 0}); } @@ -97,33 +103,30 @@ class TestNetwork : public Network { return 0; } - void add_in_progress_path_builds(std::string request_id, std::pair path_build) { - in_progress_path_builds[request_id] = path_build; + void set_path_build_queue(std::deque path_build_queue_) { + path_build_queue = path_build_queue_; } - void clear_in_progress_path_builds() { in_progress_path_builds.clear(); } - - std::unordered_map> get_in_progress_path_builds() { - return in_progress_path_builds; - } + std::deque get_path_build_queue() { return path_build_queue; } - void set_path_build_queue(std::vector path_build_queue_) { - path_build_queue = path_build_queue_; + void set_path_build_failures(int path_build_failures_) { + path_build_failures = path_build_failures_; } - std::vector get_path_build_queue() { return path_build_queue; } + int get_path_build_failures() { return path_build_failures; } - void set_general_path_build_failures(int general_path_build_failures_) { - general_path_build_failures = general_path_build_failures_; + void set_unused_connection_and_path_build_nodes( + std::optional> unused_connection_and_path_build_nodes_) { + unused_connection_and_path_build_nodes = unused_connection_and_path_build_nodes_; } - int get_general_path_build_failures() { return general_path_build_failures; } - - std::vector get_unused_path_build_nodes() { return unused_path_build_nodes; } + std::optional> get_unused_connection_and_path_build_nodes() { + return unused_connection_and_path_build_nodes; + } void add_pending_request(PathType path_type, request_info info) { request_queue[path_type].emplace_back( - info, [](bool, bool, int16_t, std::optional) {}); + std::move(info), [](bool, bool, int16_t, std::optional) {}); } // Overridden Functions @@ -132,60 +135,78 @@ class TestNetwork : public Network { return retry_delay_value; } - void resume_queues() override { - call_counts["resume_queues"]++; + void update_disk_cache_throttled(bool force_immediate_write) override { + const auto func_name = "update_disk_cache_throttled"; - if (ignore_resume_queues) + if (check_should_ignore_and_log_call(func_name)) return; - Network::resume_queues(); + Network::update_disk_cache_throttled(force_immediate_write); } - void build_path(std::optional existing_request_id, PathType path_type) override { - call_counts["build_path"]++; + void establish_and_store_connection(std::string request_id) override { + const auto func_name = "establish_and_store_connection"; + + if (check_should_ignore_and_log_call(func_name)) + return; - Network::build_path(existing_request_id, path_type); + Network::establish_and_store_connection(request_id); } - void get_snode_version( - std::string request_id, - PathType path_type, - service_node node, - std::optional timeout, - std::function< - void(std::vector version, - connection_info info, - std::optional error)> callback) override { - call_counts["get_snode_version"]++; + void refresh_snode_cache(std::optional existing_request_id) override { + const auto func_name = "refresh_snode_cache"; + + if (check_should_ignore_and_log_call(func_name)) + return; - if (ignore_get_snode_version) + Network::refresh_snode_cache(existing_request_id); + } + + void build_path(PathType path_type, std::optional existing_request_id) override { + const auto func_name = "build_path"; + + if (check_should_ignore_and_log_call(func_name)) return; - if (get_snode_version_response) { - const auto& [version, info, error] = *get_snode_version_response; - return callback(version, info, error); - } + Network::build_path(path_type, existing_request_id); + } + + std::optional find_valid_path( + request_info info, std::vector paths) override { + const auto func_name = "find_valid_path"; + + if (check_should_ignore_and_log_call(func_name)) + return std::nullopt; + + if (find_valid_path_response) + return *find_valid_path_response; + + return Network::find_valid_path(info, paths); + } + + void _send_onion_request( + request_info info, network_response_callback_t handle_response) override { + const auto func_name = "_send_onion_request"; + + if (check_should_ignore_and_log_call(func_name)) + return; - Network::get_snode_version(request_id, path_type, node, timeout, callback); + Network::_send_onion_request(std::move(info), std::move(handle_response)); } // Exposing Private Functions void establish_connection( std::string request_id, - PathType path_type, service_node target, std::optional timeout, std::function error)> callback) { - Network::establish_connection(request_id, path_type, target, timeout, std::move(callback)); - } - - std::optional find_valid_path(request_info info, std::vector paths) { - return Network::find_valid_path(info, paths); + Network::establish_connection(request_id, target, timeout, std::move(callback)); } - void enqueue_path_build_if_needed(PathType path_type, bool existing_paths_unsuitable) override { - return Network::enqueue_path_build_if_needed(path_type, existing_paths_unsuitable); + void enqueue_path_build_if_needed( + PathType path_type, std::optional found_path) override { + return Network::enqueue_path_build_if_needed(path_type, found_path); } void send_request( @@ -207,6 +228,19 @@ class TestNetwork : public Network { // Mocking Functions + template + void ignore_calls_to(Strings&&... __args) { + (calls_to_ignore.emplace_back(std::forward(__args)), ...); + } + + bool check_should_ignore_and_log_call(std::string func_name) { + call_counts[func_name]++; + + return std::find(calls_to_ignore.begin(), calls_to_ignore.end(), func_name) != + calls_to_ignore.end(); + } + + void reset_calls() { return call_counts.clear(); } bool called(std::string func_name, int times = 1) { return (call_counts[func_name] >= times); } bool did_not_call(std::string func_name) { return !call_counts.contains(func_name); } @@ -263,7 +297,7 @@ TEST_CASE("Network error handling", "[network]") { Result result; auto network = TestNetwork(std::nullopt, true, true, false); network.set_suspended(true); // Make no requests in this test - network.ignore_resume_queues = true; + network.ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; @@ -488,7 +522,7 @@ TEST_CASE("Network error handling", "[network]") { {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); nlohmann::json swarm_json{{"snodes", snodes}}; response = swarm_json.dump(); - network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target}); + network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target, target2, target3}); network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); @@ -521,6 +555,33 @@ TEST_CASE("Network error handling", "[network]") { .front() .view_remote_key()) == oxenc::to_hex(ed_pk)); + // Check a non redirect 421 with swam data triggers a retry + auto mock_request3 = request_info{ + "BBBB", + target, + "test", + std::nullopt, + std::nullopt, + x25519_pubkey::from_hex(x_pk_hex), + PathType::standard, + 0ms, + std::nullopt, + true}; + network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target, target2, target3}); + network.set_paths(PathType::standard, {path}); + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); + network.reset_calls(); + network.handle_errors( + mock_request3, + {target, nullptr, nullptr}, + false, + 421, + response, + [](bool, bool, int16_t, std::optional) {}); + CHECK(EVENTUALLY(10ms, network.called("_send_onion_request"))); + // Check a timeout with a sever destination doesn't impact the failure counts auto server = ServerDestination{ "https", @@ -531,7 +592,7 @@ TEST_CASE("Network error handling", "[network]") { 443, std::nullopt, "GET"}; - auto mock_request3 = request_info{ + auto mock_request4 = request_info{ "CCCC", server, "test", @@ -542,8 +603,11 @@ TEST_CASE("Network error handling", "[network]") { 0ms, std::nullopt, false}; + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( - mock_request3, + mock_request4, {target, nullptr, nullptr}, true, std::nullopt, @@ -565,8 +629,11 @@ TEST_CASE("Network error handling", "[network]") { // Check a server response starting with '500 Internal Server Error' is reported as a `500` // error and doesn't affect the failure count + network.set_failure_count(target, 0); + network.set_failure_count(target2, 0); + network.set_failure_count(target3, 0); network.handle_errors( - mock_request3, + mock_request4, {target, nullptr, nullptr}, false, std::nullopt, @@ -593,131 +660,100 @@ TEST_CASE("Network Path Building", "[network][build_path]") { std::vector snode_cache; for (uint16_t i = 0; i < 12; ++i) snode_cache.emplace_back(service_node{ed_pk, {2, 8, 0}, fmt::format("0.0.0.{}", i), i}); + auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr}; // Nothing should happen if the network is suspended network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; network->set_suspended(true); - network->build_path(std::nullopt, PathType::standard); - CHECK(ALWAYS(10ms, network->did_not_call("resume_queues"))); + network->build_path(PathType::standard, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); - // If the snode cache is too small it puts the path build in the queue and calls resume_queues + // If there are no unused connections it puts the path build in the queue and calls + // establish_and_store_connection network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; - network->build_path("Test1", PathType::standard); - CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); - CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); - CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); - - // If there are no in progress path builds it refreshes the unused nodes value and adds an - // in-progress build + network->ignore_calls_to("establish_and_store_connection"); + network->build_path(PathType::standard, "Test1"); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + + // If the unused nodes are empty it refreshes them network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; network->set_snode_cache(snode_cache); - network->set_general_path_build_failures(10); - network->build_path("Test1", PathType::standard); - CHECK(network->get_unused_path_build_nodes().size() == snode_cache.size() - 1); - CHECK(network->get_general_path_build_failures() == 0); - CHECK(network->get_in_progress_path_builds().contains("Test1")); + network->set_unused_connections({invalid_info}); + network->set_in_progress_connections({"TestInProgress"}); + network->build_path(PathType::standard, "Test1"); + REQUIRE(network->get_unused_connection_and_path_build_nodes().has_value()); + CHECK(network->get_unused_connection_and_path_build_nodes()->size() == snode_cache.size() - 3); + CHECK(network->get_path_build_queue().empty()); // It should exclude nodes that are already in existing paths network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; network->set_snode_cache(snode_cache); - network->add_path(PathType::standard, {snode_cache.begin(), snode_cache.begin() + 3}); - network->build_path("Test1", PathType::standard); - CHECK(network->get_unused_path_build_nodes().size() == (snode_cache.size() - 1 - 3)); - CHECK(network->get_in_progress_path_builds().contains("Test1")); - - // If there are no unused nodes it increments the failure count and re-queues the path build + network->set_unused_connections({invalid_info}); + network->set_in_progress_connections({"TestInProgress"}); + network->add_path(PathType::standard, {snode_cache.begin() + 1, snode_cache.begin() + 1 + 3}); + network->build_path(PathType::standard, "Test1"); + REQUIRE(network->get_unused_connection_and_path_build_nodes().has_value()); + CHECK(network->get_unused_connection_and_path_build_nodes()->size() == + (snode_cache.size() - 3 - 3)); + CHECK(network->get_path_build_queue().empty()); + + // If there aren't enough unused nodes it resets the failure count, re-queues the path build and + // triggers a snode cache refresh network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; + network->ignore_calls_to("refresh_snode_cache"); network->set_snode_cache(snode_cache); + network->set_unused_connections({invalid_info}); + network->set_path_build_failures(10); network->add_path(PathType::standard, snode_cache); - network->build_path("Test1", PathType::standard); - CHECK(network->get_unused_path_build_nodes().empty()); - CHECK(network->get_general_path_build_failures() == 1); - CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); - CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); - - // If it can't build a path after excluding nodes for existing requests it increments the - // failure count and re-queues the path build - network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; - network->set_snode_cache(snode_cache); - network->add_path(PathType::standard, {snode_cache.begin() + 1, snode_cache.end()}); - network->add_pending_request( - PathType::standard, - request_info::make(snode_cache.front(), 0ms, std::nullopt, std::nullopt)); - network->build_path("Test1", PathType::standard); - CHECK(network->get_unused_path_build_nodes().size() == 1); - CHECK_FALSE(network->get_in_progress_path_builds().contains("Test1")); - CHECK(network->get_general_path_build_failures() == 1); - CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); - CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); - - // It fetches the version of the guard node to check reachability - network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->ignore_get_snode_version = true; - network->set_snode_cache(snode_cache); - network->build_path("Test1", PathType::standard); - CHECK(network->called("get_snode_version")); + network->build_path(PathType::standard, "Test1"); + CHECK(network->get_path_build_failures() == 0); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); + CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); - // If it fails to get the guard node version it increments the pending path build failure count - // and retries again until it runs out of unused nodes + // If it can't build a path after excluding nodes with the same IP it increments the + // failure count and re-tries the path build after a small delay network.emplace(std::nullopt, true, false, false); - network->retry_delay_value = 1ms; // Prevent infinite loop detection - network->ignore_resume_queues = true; - network->get_snode_version_response = {{}, {snode_cache.front(), nullptr, nullptr}, "Error"}; network->set_snode_cache(snode_cache); - network->build_path("Test1", PathType::standard); - CHECK(EVENTUALLY(20ms, network->called("build_path", 12))); - CHECK(network->get_in_progress_path_builds().contains("Test1")); - CHECK(network->get_in_progress_path_builds()["Test1"].second == 12); - CHECK(network->get_unused_path_build_nodes().empty()); - CHECK(network->called("get_snode_version", 12)); - - // If it runs out of unused nodes after getting the guard node version it increments the pending - // path build failure count and retries the build - network.emplace(std::nullopt, true, false, false); - network->retry_delay_value = 1ms; // Prevent infinite loop detection - network->ignore_resume_queues = true; - network->get_snode_version_response = {{}, {snode_cache.front(), nullptr, nullptr}, "Error"}; - network->set_snode_cache(snode_cache); - network->add_path(PathType::standard, {snode_cache.begin() + 3, snode_cache.end()}); - network->build_path("Test1", PathType::standard); - CHECK(EVENTUALLY(20ms, network->called("build_path", 3))); - CHECK(network->get_general_path_build_failures() == 1); - CHECK(network->get_path_build_queue() == std::vector{PathType::standard}); - CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); - - // It stores a successful non-standard path and calls 'resume_queues' but doesn't update the + network->set_unused_connections({invalid_info}); + network->set_unused_connection_and_path_build_nodes(std::vector{ + snode_cache[0], snode_cache[0], snode_cache[0], snode_cache[0]}); + network->build_path(PathType::standard, "Test1"); + network->ignore_calls_to("build_path"); // Ignore the 2nd loop + CHECK(network->get_path_build_failures() == 1); + CHECK(network->get_path_build_queue().empty()); + CHECK(EVENTUALLY(10ms, network->called("build_path", 2))); + + // It stores a successful non-standard path and kicks of queued requests but doesn't update the // status or call the 'paths_changed' hook network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->get_snode_version_response = { - {2, 8, 0}, {snode_cache.front(), nullptr, nullptr}, std::nullopt}; + network->find_valid_path_response = + onion_path{invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; + network->ignore_calls_to("_send_onion_request"); network->set_snode_cache(snode_cache); - network->build_path("Test1", PathType::download); - CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + network->set_unused_connections({invalid_info}); + network->add_pending_request( + PathType::download, + request_info::make( + snode_cache.back(), 1s, std::nullopt, std::nullopt, PathType::download)); + network->build_path(PathType::download, "Test1"); + CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::download).size() == 1); // It stores a successful 'standard' path, updates the status, calls the 'paths_changed' hook - // and calls 'resume_queues' + // and kicks of queued requests network.emplace(std::nullopt, true, false, false); - network->ignore_resume_queues = true; - network->get_snode_version_response = { - {2, 8, 0}, {snode_cache.front(), nullptr, nullptr}, std::nullopt}; + network->find_valid_path_response = + onion_path{invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; + network->ignore_calls_to("_send_onion_request"); network->set_snode_cache(snode_cache); - network->build_path("Test1", PathType::standard); - CHECK(EVENTUALLY(10ms, network->called("resume_queues"))); + network->set_unused_connections({invalid_info}); + network->add_pending_request( + PathType::standard, + request_info::make( + snode_cache.back(), 1s, std::nullopt, std::nullopt, PathType::standard)); + network->build_path(PathType::standard, "Test1"); + CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::standard).size() == 1); CHECK(network->get_status() == ConnectionStatus::connected); CHECK(network->called("paths_changed")); @@ -747,7 +783,6 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { network.establish_connection( "Test", - PathType::standard, test_service_node, 3s, [&prom](connection_info conn_info, std::optional error) { @@ -776,79 +811,95 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; - auto network = TestNetwork(std::nullopt, true, false, false); - auto network_single_path = TestNetwork(std::nullopt, true, true, false); - auto invalid_path = onion_path{{target, nullptr, nullptr}, {target}, uint8_t{0}}; - network.ignore_resume_queues = true; - network_single_path.ignore_resume_queues = true; + std::optional network; + auto invalid_path = onion_path{connection_info{target, nullptr, nullptr}, {target}, uint8_t{0}}; // It does not add additional path builds if there is already a path and it's in // 'single_path_mode' - network_single_path.set_paths(PathType::standard, {invalid_path}); - network_single_path.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network_single_path.get_path_build_queue().empty()); + network.emplace(std::nullopt, true, true, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {invalid_path}); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue().empty()); // Adds a path build to the queue - network.set_paths(PathType::standard, {}); - network.set_path_build_queue({}); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue() == std::vector{PathType::standard}); + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {}); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add two path build to the queue - network.set_paths(PathType::standard, {}); - network.set_path_build_queue({}); - network.enqueue_path_build_if_needed(PathType::standard, false); - network.enqueue_path_build_if_needed(PathType::standard, false); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue() == - std::vector{PathType::standard, PathType::standard}); + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); + network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == + std::deque{PathType::standard, PathType::standard}); // Can add a second 'standard' path build even if there is an active 'standard' path - network.set_paths(PathType::standard, {invalid_path}); - network.set_path_build_queue({}); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue() == std::vector{PathType::standard}); - - // Cannot add more path builds if there are already enough active paths of the same type - network.set_paths(PathType::standard, {invalid_path, invalid_path}); - network.set_path_build_queue({}); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue().empty()); - - // Cannot add more path builds if there are already enough in progress path builds of the same - // type - network.set_paths(PathType::standard, {invalid_path, invalid_path}); - network.set_path_build_queue({}); - network.clear_in_progress_path_builds(); - network.add_in_progress_path_builds("Test1", {PathType::standard, 0}); - network.add_in_progress_path_builds("Test2", {PathType::standard, 0}); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue().empty()); - - // Cannot add more path builds if the combination of active and in progress builds of the same - // time are more than the limit - network.set_paths(PathType::standard, {invalid_path}); - network.set_path_build_queue({}); - network.clear_in_progress_path_builds(); - network.add_in_progress_path_builds("Test1", {PathType::standard, 0}); - network.enqueue_path_build_if_needed(PathType::standard, false); - CHECK(network.get_path_build_queue().empty()); + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {invalid_path}); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); + + // Can add more path builds if there are enough active paths of the same type, no pending paths + // and no `found_path` was provided + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {invalid_path, invalid_path}); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); + + // Cannot add more path builds if there are already enough active paths of the same type and a + // `found_path` was provided + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {invalid_path, invalid_path}); + network->enqueue_path_build_if_needed(PathType::standard, invalid_path); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue().empty()); + + // Cannot add more path builds if there is already a build of the same type in the queue and the + // number of active and pending builds of the same type meet the limit + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::standard, {invalid_path}); + network->set_path_build_queue({PathType::standard}); + network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add a single 'download' path build - network.set_paths(PathType::download, {}); - network.set_path_build_queue({}); - network.clear_in_progress_path_builds(); - network.enqueue_path_build_if_needed(PathType::download, false); - network.enqueue_path_build_if_needed(PathType::download, false); - CHECK(network.get_path_build_queue() == std::vector{PathType::download}); + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::download, {}); + network->enqueue_path_build_if_needed(PathType::download, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued + network->enqueue_path_build_if_needed(PathType::download, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::download}); // Can only add a single 'upload' path build - network.set_paths(PathType::upload, {}); - network.set_path_build_queue({}); - network.clear_in_progress_path_builds(); - network.enqueue_path_build_if_needed(PathType::upload, false); - network.enqueue_path_build_if_needed(PathType::upload, false); - CHECK(network.get_path_build_queue() == std::vector{PathType::upload}); + network.emplace(std::nullopt, true, false, false); + network->ignore_calls_to("establish_and_store_connection"); + network->set_paths(PathType::upload, {}); + network->enqueue_path_build_if_needed(PathType::upload, std::nullopt); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued + network->enqueue_path_build_if_needed(PathType::upload, std::nullopt); + CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->get_path_build_queue() == std::deque{PathType::upload}); } TEST_CASE("Network requests", "[network][establish_connection]") { @@ -862,7 +913,6 @@ TEST_CASE("Network requests", "[network][establish_connection]") { network.establish_connection( "Test", - PathType::standard, test_service_node, 3s, [&prom](connection_info info, std::optional error) { @@ -887,7 +937,6 @@ TEST_CASE("Network requests", "[network][send_request]") { network.establish_connection( "Test", - PathType::standard, test_service_node, 3s, [&prom, &network, test_service_node]( @@ -931,85 +980,89 @@ TEST_CASE("Network requests", "[network][send_request]") { } } -// TEST_CASE("Network onion request", "[network][send_onion_request]") { -// auto test_service_node = service_node{ -// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, -// {2, 8, 0}, -// "144.76.164.202", -// uint16_t{35400}}; -// auto network = Network(std::nullopt, true, true, false); -// std::promise result_promise; - -// network.send_onion_request( -// test_service_node, -// ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, -// std::nullopt, -// oxen::quic::DEFAULT_TIMEOUT, -// [&result_promise]( -// bool success, -// bool timeout, -// int16_t status_code, -// std::optional response) { -// result_promise.set_value({success, timeout, status_code, response}); -// }); - -// // Wait for the result to be set -// auto result = result_promise.get_future().get(); - -// CHECK(result.success); -// CHECK_FALSE(result.timeout); -// CHECK(result.status_code == 200); -// REQUIRE(result.response.has_value()); - -// try { -// auto response = nlohmann::json::parse(*result.response); -// CHECK(response.contains("hf")); -// CHECK(response.contains("t")); -// CHECK(response.contains("version")); -// } catch (...) { -// REQUIRE(*result.response == "{VALID JSON}"); -// } -// } - -// TEST_CASE("Network direct request C API", "[network][network_send_request]") { -// network_object* network; -// REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); -// std::array target_ip = {144, 76, 164, 202}; -// auto test_service_node = network_service_node{}; -// test_service_node.quic_port = 35400; -// std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); -// std::strcpy( -// test_service_node.ed25519_pubkey_hex, -// "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); -// auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; - -// network_send_onion_request_to_snode_destination( -// network, -// test_service_node, -// body.data(), -// body.size(), -// nullptr, -// std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), -// [](bool success, -// bool timeout, -// int16_t status_code, -// const char* c_response, -// size_t response_size, -// void* ctx) { -// CHECK(success); -// CHECK_FALSE(timeout); -// CHECK(status_code == 200); -// REQUIRE(response_size != 0); - -// auto response_str = std::string(c_response, response_size); -// INFO("response_str is: " << response_str); -// REQUIRE_NOTHROW(nlohmann::json::parse(response_str)); - -// auto response = nlohmann::json::parse(response_str); -// CHECK(response.contains("hf")); -// CHECK(response.contains("t")); -// CHECK(response.contains("version")); -// network_free(static_cast(ctx)); -// }, -// network); -// } +TEST_CASE("Network onion request", "[network][send_onion_request]") { + auto test_service_node = service_node{ + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + {2, 8, 0}, + "144.76.164.202", + uint16_t{35400}}; + auto network = Network(std::nullopt, true, true, false); + std::promise result_promise; + + network.send_onion_request( + test_service_node, + ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, + std::nullopt, + oxen::quic::DEFAULT_TIMEOUT, + [&result_promise]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result_promise.set_value({success, timeout, status_code, response}); + }); + + // Wait for the result to be set + auto result = result_promise.get_future().get(); + + CHECK(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 200); + REQUIRE(result.response.has_value()); + INFO("*result.response is: " << *result.response); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); +} + +TEST_CASE("Network direct request C API", "[network][network_send_request]") { + network_object* network; + REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); + std::array target_ip = {144, 76, 164, 202}; + auto test_service_node = network_service_node{}; + test_service_node.quic_port = 35400; + std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); + std::strcpy( + test_service_node.ed25519_pubkey_hex, + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); + auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; + auto result_promise = std::make_shared>(); + + network_send_onion_request_to_snode_destination( + network, + test_service_node, + body.data(), + body.size(), + nullptr, + std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), + [](bool success, + bool timeout, + int16_t status_code, + const char* c_response, + size_t response_size, + void* ctx) { + auto result_promise = static_cast*>(ctx); + auto response_str = std::string(c_response, response_size); + result_promise->set_value({success, timeout, status_code, response_str}); + }, + static_cast(result_promise.get())); + + // Wait for the result to be set + auto result = result_promise->get_future().get(); + + CHECK(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 200); + REQUIRE(result.response.has_value()); + INFO("*result.response is: " << *result.response); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); + network_free(network); +} From bdfa6fd5fb0cc42499fdbbb1641c8386b01a499e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 14 Aug 2024 09:42:00 +1000 Subject: [PATCH 377/572] Updated libquic submodule --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index ef94de7d..91ca79e5 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit ef94de7d9b01b95ccb2684ec44243347ae3c022b +Subproject commit 91ca79e578d7edc4d108e530b6a6e072a212878c From fd218a9eee0cdc18565a597a69739d9f89a9298c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 14 Aug 2024 17:47:33 +1000 Subject: [PATCH 378/572] Fixed a bug and the broken tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an issue causing the tests to crash • Fixed an issue where the handle_errors function may not correctly report a timeout • Removed hacky attempts to fix test crash • Removed some redundant code --- include/session/network.hpp | 11 +++- src/network.cpp | 104 ++++++++++-------------------------- tests/test_network.cpp | 2 +- 3 files changed, 39 insertions(+), 78 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index b53ec866..f0f82504 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -151,7 +151,6 @@ class Network { // General values bool suspended = false; - bool being_destroyed = false; ConnectionStatus status; oxen::quic::Network net; std::shared_ptr endpoint; @@ -183,6 +182,7 @@ class Network { public: friend class TestNetwork; + friend class TestNetworkWrapper; // Hook to be notified whenever the network connection status changes. std::function status_changed; @@ -385,6 +385,13 @@ class Network { /// data exists. void load_cache_from_disk(); + /// API: network/_close_connections + /// + /// Triggered via the close_connections function but actually contains the logic to clear out + /// paths, requests and connections. This function is not thread safe so should should be + /// called with that in mind. + void _close_connections(); + /// API: network/update_status /// /// Internal function to update the connection status and trigger the `status_changed` hook if @@ -506,7 +513,7 @@ class Network { /// Outputs: /// - The possible path, if found. virtual std::optional find_valid_path( - request_info info, std::vector paths); + const request_info info, const std::vector paths); /// API: network/enqueue_path_build_if_needed /// diff --git a/src/network.cpp b/src/network.cpp index ca539269..677ace9a 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -395,11 +395,9 @@ Network::Network( } Network::~Network() { - // We need to set a flag to prevent the logic in the `connection_closed_callback` from running - // because if we can get bad memory errors due to trying to use `net.call` while the - // `quic::Network` is in the process of being destroyed - suspended = true; - being_destroyed = true; + // We need to explicitly close the connections at the start of the destructor to prevent ban + // memory errors due to complex logic with the quic::Network instance + _close_connections(); { std::lock_guard lock{snode_cache_mutex}; @@ -757,35 +755,25 @@ void Network::resume() { } void Network::close_connections() { - net.call([this]() mutable { - // Explicitly reset the endpoint to close all connections - endpoint.reset(); - - // Cancel any pending requests (they can't succeed once the connection is closed) - for (const auto& [path_type, path_type_requests] : request_queue) - for (const auto& [info, callback] : path_type_requests) - callback(false, false, error_network_suspended, "Network is suspended."); - request_queue.clear(); - - // Explicitly reset the connection and stream (just in case) - for (auto& [type, paths_for_type] : paths) { - for (auto& path : paths_for_type) { - path.conn_info.conn.reset(); - path.conn_info.stream.reset(); - } - } - paths.clear(); + net.call([this]() mutable { _close_connections(); }); +} - // Reset any spare first hops - for (auto& conn_info : unused_connections) { - conn_info.conn.reset(); - conn_info.stream.reset(); - } - unused_connections.clear(); +void Network::_close_connections() { + // Explicitly reset the endpoint to close all connections + endpoint.reset(); - update_status(ConnectionStatus::disconnected); - log::info(cat, "Closed all connections."); - }); + // Cancel any pending requests (they can't succeed once the connection is closed) + for (const auto& [path_type, path_type_requests] : request_queue) + for (const auto& [info, callback] : path_type_requests) + callback(false, false, error_network_suspended, "Network is suspended."); + + // Clear all storage of requests, paths and connections + request_queue.clear(); + paths.clear(); + unused_connections.clear(); + + update_status(ConnectionStatus::disconnected); + log::info(cat, "Closed all connections."); } void Network::update_status(ConnectionStatus updated_status) { @@ -917,12 +905,6 @@ void Network::establish_connection( }, [this, target, request_id, cb, cb_called, conn_future]( quic::connection_interface& conn, uint64_t error_code) mutable { - // If the instance is in the process of being destroyed then we don't want to - // interact with `net` as it's also likely in the process of being destroyed and - // trigging any of the below logic can result in bad memory access - if (being_destroyed) - return; - log::trace(cat, "Connection closed for {}.", request_id); // Just in case, call it within a `net.call` @@ -938,18 +920,8 @@ void Network::establish_connection( // Remove the connection from `unused_connection` if present std::erase_if(unused_connections, [&conn, &target](auto& unused_conn) { - if (unused_conn.node != target && - (!unused_conn.conn || - unused_conn.conn->reference_id() != conn.reference_id())) - return false; - - // Just in case reset the connection and pointers - if (unused_conn.conn) - unused_conn.conn->close_connection(); - - unused_conn.conn.reset(); - unused_conn.stream.reset(); - return true; + return (unused_conn.node == target && unused_conn.conn && + unused_conn.conn->reference_id() == conn.reference_id()); }); // If this connection is being used in an existing path then we should drop it @@ -1381,7 +1353,6 @@ void Network::build_path(PathType path_type, std::optional existing // If we still don't have enough unused nodes then we need to refresh the cache if (unused_connection_and_path_build_nodes->size() < path_size) { - std::cout << "RAWR build_path need a snode cache: " + request_id << std::endl; log::info( cat, "Re-queing {} path build due to insufficient nodes ({}).", @@ -1460,7 +1431,7 @@ void Network::build_path(PathType path_type, std::optional existing // If a paths_changed callback was provided then call it if (paths_changed) { std::vector> raw_paths; - for (auto& path : paths[path_type]) + for (const auto& path : paths[path_type]) raw_paths.emplace_back(path.nodes); paths_changed(raw_paths); @@ -1492,7 +1463,7 @@ void Network::build_path(PathType path_type, std::optional existing } std::optional Network::find_valid_path( - request_info info, std::vector paths) { + const request_info info, const std::vector paths) { if (paths.empty()) return std::nullopt; @@ -1540,7 +1511,7 @@ std::optional Network::find_valid_path( void Network::enqueue_path_build_if_needed( PathType path_type, std::optional found_path) { net.call([this, path_type, found_path]() { - auto current_paths = paths[path_type]; + const auto current_paths = paths[path_type]; // In `single_path_mode` we never build additional paths if (current_paths.size() > 0 && single_path_mode) @@ -1610,10 +1581,6 @@ void Network::get_service_nodes( void Network::get_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::function swarm)> callback) { - // If the network is in the process of being shut down just ignore the call - if (being_destroyed) - return; - auto request_id = random::random_base32(4); log::trace(cat, "{} called for {} as {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex(), request_id); @@ -1701,10 +1668,6 @@ void Network::get_swarm( void Network::set_swarm( session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { - // If the network is in the process of being shut down just ignore the call - if (being_destroyed) - return; - net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { { std::lock_guard lock{snode_cache_mutex}; @@ -1718,10 +1681,6 @@ void Network::set_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { - // If the network is in the process of being shut down just ignore the call - if (being_destroyed) - return; - auto request_id = random::random_base32(4); log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); @@ -1769,7 +1728,8 @@ void Network::send_request( try { result = validate_response(resp, false); } catch (const status_code_exception& e) { - return handle_errors(info, conn_info, false, e.status_code, e.what(), cb); + return handle_errors( + info, conn_info, resp.timed_out, e.status_code, e.what(), cb); } catch (const std::exception& e) { return handle_errors(info, conn_info, resp.timed_out, -1, e.what(), cb); } @@ -1796,10 +1756,6 @@ void Network::send_onion_request( } void Network::_send_onion_request(request_info info, network_response_callback_t handle_response) { - // If the network is in the process of being shut down just ignore the call - if (being_destroyed) - return; - auto path_name = path_type_name(info.path_type, single_path_mode); log::trace(cat, "{} called for {} path ({}).", __PRETTY_FUNCTION__, path_name, info.request_id); @@ -2196,8 +2152,6 @@ void Network::drop_path(std::string request_id, PathType path_type, onion_path p path.conn_info.conn->close_connection(); auto path_nodes = path.nodes; - path.conn_info.conn.reset(); - path.conn_info.stream.reset(); paths[path_type].erase( std::remove(paths[path_type].begin(), paths[path_type].end(), path), paths[path_type].end()); @@ -2448,7 +2402,7 @@ void Network::handle_errors( cat, "Request {} failed but {} path already dropped.", info.request_id, path_name); if (handle_response) - (*handle_response)(false, false, status_code, response); + (*handle_response)(false, timeout, status_code, response); return; } @@ -2624,7 +2578,7 @@ void Network::handle_errors( update_disk_cache_throttled(); if (handle_response) - (*handle_response)(false, false, status_code, response); + (*handle_response)(false, timeout, status_code, response); } std::vector convert_service_nodes( diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 873bdf1f..45c79b4b 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -50,7 +50,7 @@ class TestNetwork : public Network { ConnectionStatus get_status() { return status; } void set_snode_cache(std::vector cache) { - // Need to set the `last_snode_cache_update` to `1s` ago because otherwise it'll be + // Need to set the `last_snode_cache_update` to `10s` ago because otherwise it'll be // considered invalid when checking the cache validity snode_cache = cache; last_snode_cache_update = (std::chrono::system_clock::now() - 10s); From 3650fdc843a7cb55912276b41d18bd41866e6ecf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 15 Aug 2024 12:36:04 +1000 Subject: [PATCH 379/572] Fixed a couple of bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an issue where refreshing the snode cache via an onion request had the wrong request structure • Fixed an issue where suspending and resuming the network without a snode cache could result in the snode cache refresh never running --- src/network.cpp | 63 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 677ace9a..4f67aa6c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -767,10 +767,15 @@ void Network::_close_connections() { for (const auto& [info, callback] : path_type_requests) callback(false, false, error_network_suspended, "Network is suspended."); - // Clear all storage of requests, paths and connections + // Clear all storage of requests, paths and connections so that we are in a fresh state on + // relaunch request_queue.clear(); paths.clear(); + path_build_queue.clear(); unused_connections.clear(); + in_progress_connections.clear(); + current_snode_cache_refresh_request_id = std::nullopt; + unused_connection_and_path_build_nodes = std::nullopt; update_status(ConnectionStatus::disconnected); log::info(cat, "Closed all connections."); @@ -978,7 +983,7 @@ void Network::establish_connection( } void Network::establish_and_store_connection(std::string request_id) { - // If we are suspended then done try to establish a new connection + // If we are suspended then don't try to establish a new connection if (suspended) return; @@ -997,8 +1002,7 @@ void Network::establish_and_store_connection(std::string request_id) { "Unable to establish new connection due to lack of unused nodes, refreshing snode " "cache ({}).", request_id); - net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); - return; + return net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); } // Otherwise check if it's been too long since the last cache update and, if so, trigger a @@ -1112,6 +1116,19 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r return; } + // Only allow a single cache refresh at a time (this gets cleared in `_close_connections` so if + // it happens to loop after going to, and returning from, the background a subsequent refresh + // won't be blocked) + if (current_snode_cache_refresh_request_id && + current_snode_cache_refresh_request_id != request_id) { + log::info( + cat, + "Snode cache refresh from seed node ignored as it doesn't match the current " + "refresh id ({}).", + request_id); + return; + } + // If the unused nodes is empty then reset it (if we are refreshing from seed nodes it means the // local cache is not usable so we are just going to have to call this endlessly until it works) if (reset_unused_nodes || !unused_snode_refresh_nodes || unused_snode_refresh_nodes->empty()) { @@ -1155,7 +1172,7 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r info, std::nullopt, [this, request_id]( - std::vector nodes, std::optional) { + std::vector nodes, std::optional error) { // If we got no nodes then we will need to try again if (nodes.empty()) { snode_cache_refresh_failure_count++; @@ -1163,8 +1180,9 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r retry_delay(snode_cache_refresh_failure_count); log::error( cat, - "Failed to retrieve nodes from seen node to refresh snode " - "cache, will retry after {}ms ({}).", + "Failed to retrieve nodes from seed node to refresh cache " + "due to error: {}, will retry after {}ms ({}).", + error.value_or("Unknown Error"), cache_refresh_retry_delay.count(), request_id); return net.call_later( @@ -1192,7 +1210,9 @@ void Network::refresh_snode_cache(std::optional existing_request_id return; } - // Only allow a single cache refresh at a time + // Only allow a single cache refresh at a time (this gets cleared in `_close_connections` so if + // it happens to loop after going to, and returning from, the background a subsequent refresh + // won't be blocked) if (current_snode_cache_refresh_request_id && current_snode_cache_refresh_request_id != request_id) { log::info(cat, "Snode cache refresh ignored due to in progress refresh ({}).", request_id); @@ -1229,8 +1249,10 @@ void Network::refresh_snode_cache(std::optional existing_request_id // Prepare and send the request to retrieve service nodes nlohmann::json payload{ - {"method", "get_service_nodes"}, - {"params", detail::get_service_nodes_params(std::nullopt)}, + {"method", "oxend_request"}, + {"params", + {{"endpoint", "get_service_nodes"}, + {"params", detail::get_service_nodes_params(std::nullopt)}}}, }; auto info = request_info::make( target_node, @@ -1254,9 +1276,9 @@ void Network::refresh_snode_cache(std::optional existing_request_id nlohmann::json response_json = nlohmann::json::parse(*response); std::vector result = - detail::process_get_service_nodes_response(*response); + detail::process_get_service_nodes_response(response_json); snode_refresh_results->emplace_back(result); - } catch (...) { + } catch (const std::exception& e) { // The request failed so increment the failure counter and retry after a short // delay snode_cache_refresh_failure_count++; @@ -1264,14 +1286,14 @@ void Network::refresh_snode_cache(std::optional existing_request_id auto cache_refresh_retry_delay = retry_delay(snode_cache_refresh_failure_count); log::error( cat, - "Failed to retrieve nodes from one target when refresh snode cache " - "due, will try another target after {}ms ({}).", + "Failed to retrieve nodes from one target when refreshing cache due to " + "error: {}, will try another target after {}ms ({}).", + e.what(), cache_refresh_retry_delay.count(), request_id); - net.call_later(cache_refresh_retry_delay, [this, request_id]() { + return net.call_later(cache_refresh_retry_delay, [this, request_id]() { refresh_snode_cache(request_id); }); - return; } // If we haven't received all results then do nothing @@ -1360,8 +1382,7 @@ void Network::build_path(PathType path_type, std::optional existing request_id); path_build_failures = 0; path_build_queue.emplace_back(path_type); - net.call_soon([this]() { refresh_snode_cache(); }); - return; + return net.call_soon([this]() { refresh_snode_cache(); }); } // Build the path @@ -1602,8 +1623,7 @@ void Network::get_swarm( after_snode_cache_refresh.emplace_back([this, swarm_pubkey, cb = std::move(cb)]() { get_swarm(swarm_pubkey, std::move(cb)); }); - net.call_soon([this]() { refresh_snode_cache(); }); - return; + return net.call_soon([this]() { refresh_snode_cache(); }); } CSRNG rng; @@ -1689,8 +1709,7 @@ void Network::get_random_nodes( if (snode_cache.size() < count) { after_snode_cache_refresh.emplace_back( [this, count, cb = std::move(cb)]() { get_random_nodes(count, cb); }); - net.call_soon([this]() { refresh_snode_cache(); }); - return; + return net.call_soon([this]() { refresh_snode_cache(); }); } // Otherwise callback with the requested random number of nodes From 1967646260cdc6af3ab19ecb137293c018d2dd87 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 15 Aug 2024 12:37:51 +1000 Subject: [PATCH 380/572] Fixed a build error --- src/network.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index 4f67aa6c..8f4495b7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2237,7 +2237,6 @@ void Network::handle_errors( _send_onion_request(updated_info, std::move(cb)); }); } - } // A number of server errors can return HTML data but no status code, we want to extract those // cases so they can be handled properly below From ecba02bcda4ef38b91509b3140bb4790c1e42d58 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 16 Aug 2024 09:24:31 +1000 Subject: [PATCH 381/572] Fixed a few bugs found when testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed a JSON snode parsing issue (depending on the source there are different key names) • Fixed an issue where excessive path builds could get incorrectly scheduled between the current run loop and when `call_soon` kicks off the next path build build • Fixed an issue with loading swarm caches from disk • Fixed an issue where the refresh_snode_cache could get excessively called due to the retry mechanism --- include/session/network.hpp | 27 +++--- src/network.cpp | 159 ++++++++++++++++++++++++------------ tests/test_network.cpp | 39 +++++---- 3 files changed, 136 insertions(+), 89 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index f0f82504..038380e3 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -173,6 +173,7 @@ class Network { // Path build state int path_build_failures = 0; std::deque path_build_queue; + std::unordered_map in_progress_path_builds; // Request state bool has_scheduled_resume_queues = false; @@ -490,15 +491,13 @@ class Network { /// API: network/build_path /// - /// Build a new onion request path for the specified type by opening and testing connections to - /// random service nodes in the snode pool. + /// Build a new onion request path for the specified type. If there are no existing connections + /// this will open a new connection to a random service nodes in the snode cache. /// /// Inputs: /// - `path_type` -- [in] the type of path to build. - /// - 'existing_request_id' - [in, optional] id for an existing build_path request. Generally - /// this will only be set when retrying a path build. - virtual void build_path( - PathType path_type, std::optional existing_request_id = std::nullopt); + /// - 'request_id' - [in] id for the build_path request. + virtual void build_path(PathType path_type, std::string request_id); /// API: network/find_valid_path /// @@ -515,20 +514,18 @@ class Network { virtual std::optional find_valid_path( const request_info info, const std::vector paths); - /// API: network/enqueue_path_build_if_needed + /// API: network/build_path_if_needed /// - /// Adds a path build to the path build queue for the specified type if the total current or - /// pending paths is below the minimum threshold for the given type. Note: This may result in - /// more paths than the minimum threshold being built in order to avoid a situation where a - /// request may never get sent due to it's destination being present in the existing path(s) for - /// the type. + /// Triggers a path build for the specified type if the total current or pending paths is below + /// the minimum threshold for the given type. Note: This may result in more paths than the + /// minimum threshold being built in order to avoid a situation where a request may never get + /// sent due to it's destination being present in the existing path(s) for the type. /// /// Inputs: /// - `path_type` -- [in] the type of path to be built. - /// - `found_path` -- [in, optional] the path which was found for the request by calling + /// - `found_path` -- [in] flag indicating whether a valid path was found by calling /// `find_valid_path` above. - virtual void enqueue_path_build_if_needed( - PathType path_type, std::optional found_path); + virtual void build_path_if_needed(PathType path_type, bool found_valid_path); /// API: network/get_service_nodes /// diff --git a/src/network.cpp b/src/network.cpp index 8f4495b7..8e69889a 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -139,14 +139,33 @@ namespace { // error neither of which contain the `storage_server_version` - luckily we don't need the // version for these two cases so can just default it to `0` std::vector storage_server_version = {0}; - if (json.contains("storage_server_version")) - storage_server_version = - parse_version(json["storage_server_version"].get()); - - return {oxenc::from_hex(pk_ed), - storage_server_version, - json["ip"].get(), - json["port_omq"].get()}; + if (json.contains("storage_server_version")) { + if (json["storage_server_version"].is_array()) { + if (json["storage_server_version"].size() > 0) { + // Convert the version to a string and parse it back into a version code to + // ensure the version formats remain consistent throughout + storage_server_version = json["storage_server_version"].get>(); + storage_server_version = + parse_version("{}"_format(fmt::join(storage_server_version, "."))); + } + } else + storage_server_version = + parse_version(json["storage_server_version"].get()); + } + + std::string ip; + if (json.contains("public_ip")) + ip = json["public_ip"].get(); + else + ip = json["ip"].get(); + + uint16_t port; + if (json.contains("storage_lmq_port")) + port = json["storage_lmq_port"].get(); + else + port = json["port_omq"].get(); + + return {oxenc::from_hex(pk_ed), storage_server_version, ip, port}; } service_node node_from_disk(std::string_view str, bool can_ignore_version = false) { @@ -390,8 +409,11 @@ Network::Network( // Kick off a separate thread to build paths (may as well kick this off early) if (pre_build_paths) - for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) - net.call_soon([this] { build_path(PathType::standard); }); + for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) { + auto request_id = random::random_base32(4); + in_progress_path_builds[request_id] = PathType::standard; + net.call_soon([this, request_id] { build_path(PathType::standard, request_id); }); + } } Network::~Network() { @@ -541,18 +563,18 @@ void Network::load_cache_from_disk() { if (swarm_lifetime < swarm_cache_expiration_duration) throw load_cache_exception{"Expired swarm cache."}; + + continue; } // Otherwise try to parse as a node (for the swarm cache we can ignore invalid // versions as the `get_swarm` API doesn't return version info) nodes.push_back(node_from_disk(line, true)); - } catch (const std::exception& e) { // Don't bother logging for expired entries (we include the count separately at // the end) - if (dynamic_cast(&e) == nullptr) { + if (dynamic_cast(&e) == nullptr) ++invalid_swarm_entries; - } // The cache is invalid, we should remove it if (!checked_swarm_expiration) { @@ -774,6 +796,7 @@ void Network::_close_connections() { path_build_queue.clear(); unused_connections.clear(); in_progress_connections.clear(); + snode_refresh_results.reset(); current_snode_cache_refresh_request_id = std::nullopt; unused_connection_and_path_build_nodes = std::nullopt; @@ -1053,6 +1076,7 @@ void Network::establish_and_store_connection(std::string request_id) { // Kick off the next pending path build since we now have a valid connection if (!path_build_queue.empty()) { + in_progress_path_builds[request_id] = path_build_queue.front(); net.call_soon([this, path_type = path_build_queue.front(), request_id]() { build_path(path_type, request_id); }); @@ -1105,8 +1129,11 @@ void Network::refresh_snode_cache_complete(std::vector nodes) { after_snode_cache_refresh.clear(); // Resume any queued path builds - for (const auto& path_type : path_build_queue) - net.call_soon([this, path_type]() { build_path(path_type); }); + for (const auto& path_type : path_build_queue) { + auto request_id = random::random_base32(4); + in_progress_path_builds[request_id] = path_type; + net.call_soon([this, path_type, request_id]() { build_path(path_type, request_id); }); + } path_build_queue.clear(); } @@ -1123,9 +1150,10 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r current_snode_cache_refresh_request_id != request_id) { log::info( cat, - "Snode cache refresh from seed node ignored as it doesn't match the current " + "Snode cache refresh from seed node {} ignored as it doesn't match the current " "refresh id ({}).", - request_id); + request_id, + current_snode_cache_refresh_request_id.value_or("NULL")); return; } @@ -1215,7 +1243,11 @@ void Network::refresh_snode_cache(std::optional existing_request_id // won't be blocked) if (current_snode_cache_refresh_request_id && current_snode_cache_refresh_request_id != request_id) { - log::info(cat, "Snode cache refresh ignored due to in progress refresh ({}).", request_id); + log::info( + cat, + "Snode cache refresh {} ignored due to in progress refresh ({}).", + request_id, + current_snode_cache_refresh_request_id.value_or("NULL")); return; } @@ -1265,19 +1297,29 @@ void Network::refresh_snode_cache(std::optional existing_request_id info, [this, request_id]( bool success, bool timeout, int16_t, std::optional response) { - // Update the in progress request count - in_progress_snode_cache_refresh_count--; + // If the 'snode_refresh_results' value doesn't exist it means we have already + // completed/cancelled this snode cache refresh and have somehow gotten into an + // invalid state, so just ignore this request + if (!snode_refresh_results) { + log::warning( + cat, + "Ignoring snode cache response after cache update already completed " + "({}).", + request_id); + return; + } try { if (!success || timeout || !response) throw std::runtime_error{response.value_or("Unknown error.")}; - if (!snode_refresh_results) - throw std::runtime_error{"Invalid result pointer."}; nlohmann::json response_json = nlohmann::json::parse(*response); std::vector result = detail::process_get_service_nodes_response(response_json); snode_refresh_results->emplace_back(result); + + // Update the in progress request count + in_progress_snode_cache_refresh_count--; } catch (const std::exception& e) { // The request failed so increment the failure counter and retry after a short // delay @@ -1287,7 +1329,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id log::error( cat, "Failed to retrieve nodes from one target when refreshing cache due to " - "error: {}, will try another target after {}ms ({}).", + "error: {} Will try another target after {}ms ({}).", e.what(), cache_refresh_retry_delay.count(), request_id); @@ -1347,13 +1389,12 @@ void Network::refresh_snode_cache(std::optional existing_request_id }); } -void Network::build_path(PathType path_type, std::optional existing_request_id) { +void Network::build_path(PathType path_type, std::string request_id) { if (suspended) { log::info(cat, "Ignoring build_path call as network is suspended."); return; } - auto request_id = existing_request_id.value_or(random::random_base32(4)); auto path_name = path_type_name(path_type, single_path_mode); // If we don't have an unused connection for the first hop then enqueue the path build and @@ -1365,6 +1406,7 @@ void Network::build_path(PathType path_type, std::optional existing path_name, request_id); path_build_queue.emplace_back(path_type); + in_progress_path_builds.erase(request_id); return net.call_soon([this, request_id]() { establish_and_store_connection(request_id); }); } @@ -1382,11 +1424,13 @@ void Network::build_path(PathType path_type, std::optional existing request_id); path_build_failures = 0; path_build_queue.emplace_back(path_type); + in_progress_path_builds.erase(request_id); return net.call_soon([this]() { refresh_snode_cache(); }); } // Build the path log::info(cat, "Building {} path ({}).", path_name, request_id); + in_progress_path_builds[request_id] = path_type; auto conn_info = std::move(unused_connections.front()); unused_connections.pop_front(); @@ -1427,6 +1471,7 @@ void Network::build_path(PathType path_type, std::optional existing // Store the new path auto path = onion_path{std::move(conn_info), path_nodes, 0}; paths[path_type].emplace_back(path); + in_progress_path_builds.erase(request_id); // Log that a path was built std::vector node_descriptions; @@ -1472,9 +1517,13 @@ void Network::build_path(PathType path_type, std::optional existing // If there are still pending requests and there are no pending path builds for them then kick // off a subsequent path build in an effort to resume the remaining requests - if (!request_queue[path_type].empty()) - net.call_soon([this, path_type]() { build_path(path_type); }); - else + if (!request_queue[path_type].empty()) { + auto additional_request_id = random::random_base32(4); + in_progress_path_builds[additional_request_id] = path_type; + net.call_soon([this, path_type, additional_request_id] { + build_path(path_type, additional_request_id); + }); + } else request_queue.erase(path_type); // If there are no more pending requests, path builds or connections then we should reset the @@ -1529,35 +1578,35 @@ std::optional Network::find_valid_path( return possible_paths.front(); }; -void Network::enqueue_path_build_if_needed( - PathType path_type, std::optional found_path) { - net.call([this, path_type, found_path]() { - const auto current_paths = paths[path_type]; - - // In `single_path_mode` we never build additional paths - if (current_paths.size() > 0 && single_path_mode) - return; +void Network::build_path_if_needed(PathType path_type, bool found_path) { + const auto current_paths = paths[path_type]; - // We only want to enqueue a new path build if: - // - We don't have the minimum number of paths for the specified type - // - We don't have any pending builds - // - The current paths are unsuitable for the request - auto min_paths = min_path_count(path_type, single_path_mode); + // In `single_path_mode` we never build additional paths + if (current_paths.size() > 0 && single_path_mode) + return; - // If we have enough existing paths and found a valid path then no need to build more paths - if (found_path && current_paths.size() >= min_paths) - return; + // We only want to enqueue a new path build if: + // - We don't have the minimum number of paths for the specified type + // - We don't have any pending builds + // - The current paths are unsuitable for the request + auto min_paths = min_path_count(path_type, single_path_mode); - // Get the number pending paths - auto pending_paths = - std::count(path_build_queue.begin(), path_build_queue.end(), path_type); + // If we have enough existing paths and found a valid path then no need to build more paths + if (found_path && current_paths.size() >= min_paths) + return; - // If we don't have enough current + pending paths, or the request couldn't be sent then - // kick off a new path build - if ((current_paths.size() + pending_paths) < min_paths || - (!found_path && pending_paths == 0)) - net.call_soon([this, path_type]() { build_path(path_type); }); - }); + // Get the number pending paths + auto queued = std::count(path_build_queue.begin(), path_build_queue.end(), path_type); + auto in_progress = std::count_if( + in_progress_path_builds.begin(), + in_progress_path_builds.end(), + [&path_type](const auto& build) { return build.second == path_type; }); + auto pending_paths = (queued + in_progress); + + // If we don't have enough current + pending paths, or the request couldn't be sent then + // kick off a new path build + if ((current_paths.size() + pending_paths) < min_paths || (!found_path && pending_paths == 0)) + build_path(path_type, random::random_base32(4)); } // MARK: Direct Requests @@ -1782,7 +1831,9 @@ void Network::_send_onion_request(request_info info, network_response_callback_t // the queue to be run once a path for it has successfully been built auto path = net.call_get([this, info]() { auto result = find_valid_path(info, paths[info.path_type]); - enqueue_path_build_if_needed(info.path_type, result); + net.call_soon([this, path_type = info.path_type, found_path = result.has_value()]() { + build_path_if_needed(path_type, found_path); + }); return result; }); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 45c79b4b..1458c46f 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -162,13 +162,13 @@ class TestNetwork : public Network { Network::refresh_snode_cache(existing_request_id); } - void build_path(PathType path_type, std::optional existing_request_id) override { + void build_path(PathType path_type, std::string request_id) override { const auto func_name = "build_path"; if (check_should_ignore_and_log_call(func_name)) return; - Network::build_path(path_type, existing_request_id); + Network::build_path(path_type, request_id); } std::optional find_valid_path( @@ -204,9 +204,8 @@ class TestNetwork : public Network { Network::establish_connection(request_id, target, timeout, std::move(callback)); } - void enqueue_path_build_if_needed( - PathType path_type, std::optional found_path) override { - return Network::enqueue_path_build_if_needed(path_type, found_path); + void build_path_if_needed(PathType path_type, bool found_valid_path) override { + return Network::build_path_if_needed(path_type, found_valid_path); } void send_request( @@ -665,7 +664,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { // Nothing should happen if the network is suspended network.emplace(std::nullopt, true, false, false); network->set_suspended(true); - network->build_path(PathType::standard, std::nullopt); + network->build_path(PathType::standard, "Test1"); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); // If there are no unused connections it puts the path build in the queue and calls @@ -808,7 +807,7 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { CHECK(network_single_path.find_valid_path(shared_ip_info, {valid_path}).has_value()); } -TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed]") { +TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; std::optional network; @@ -819,7 +818,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, true, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue().empty()); @@ -827,18 +826,18 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {}); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add two path build to the queue network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); + network->build_path_if_needed(PathType::standard, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard, PathType::standard}); @@ -847,7 +846,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); @@ -856,7 +855,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); @@ -865,7 +864,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); - network->enqueue_path_build_if_needed(PathType::standard, invalid_path); + network->build_path_if_needed(PathType::standard, true); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue().empty()); @@ -875,7 +874,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); network->set_path_build_queue({PathType::standard}); - network->enqueue_path_build_if_needed(PathType::standard, std::nullopt); + network->build_path_if_needed(PathType::standard, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); @@ -883,10 +882,10 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::download, {}); - network->enqueue_path_build_if_needed(PathType::download, std::nullopt); + network->build_path_if_needed(PathType::download, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued - network->enqueue_path_build_if_needed(PathType::download, std::nullopt); + network->build_path_if_needed(PathType::download, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::download}); @@ -894,10 +893,10 @@ TEST_CASE("Network Enqueue Path Build", "[network][enqueue_path_build_if_needed] network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::upload, {}); - network->enqueue_path_build_if_needed(PathType::upload, std::nullopt); + network->build_path_if_needed(PathType::upload, false); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued - network->enqueue_path_build_if_needed(PathType::upload, std::nullopt); + network->build_path_if_needed(PathType::upload, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::upload}); } From cc539e3830cce5a4385c803324ae3f089d1caad2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 19 Aug 2024 14:07:41 +1000 Subject: [PATCH 382/572] Updated decrypt_ons_response to support decrypting legacy ONS values --- include/session/session_encrypt.h | 10 +++--- include/session/session_encrypt.hpp | 8 +++-- src/session_encrypt.cpp | 51 +++++++++++++++++++++++++---- tests/test_session_encrypt.cpp | 4 +++ 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index b91628e3..4b6399de 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -170,12 +170,11 @@ LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( /// This function attempts to decrypt an ONS response. /// /// Inputs: -/// - `lowercase_name_in` -- [in] Pointer to a buffer containing the lowercase name used to trigger -/// the response. -/// - `name_len` -- [in] Length of `name_in`. +/// - `lowercase_name_in` -- [in] Pointer to a NULL-terminated buffer containing the lowercase name +/// used to trigger the response. /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. /// - `ciphertext_len` -- [in] Length of `ciphertext_in`. -/// - `nonce_in` -- [in] Pointer to a data buffer containing the nonce (24 bytes). +/// - `nonce_in` -- [in, optional] Pointer to a data buffer containing the nonce (24 bytes) or NULL. /// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, /// hex-encoded session_id will be written if decryption was successful. /// @@ -183,10 +182,9 @@ LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( /// - `bool` -- True if the session ID was successfully decrypted, false if decryption failed. LIBSESSION_EXPORT bool session_decrypt_ons_response( const char* lowercase_name_in, - size_t name_len, const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* nonce_in, /* 24 bytes */ + const unsigned char* nonce_in, /* 24 bytes or NULL */ char* session_id_out /* 67 byte output buffer */); /// API: crypto/session_decrypt_push_notification diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 6a185a73..e1381f7b 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "types.hpp" // Helper functions for the "Session Protocol" encryption mechanism. This is the encryption used @@ -235,13 +237,15 @@ std::pair decrypt_from_blinded_recipient( /// Inputs: /// - `lowercase_name` -- the lowercase name which was looked to up to retrieve this response. /// - `ciphertext` -- ciphertext returned from the server. -/// - `nonce` -- the nonce returned from the server +/// - `nonce` -- the nonce returned from the server if provided. /// /// Outputs: /// - `std::string` -- the session ID (in hex) returned from the server, *if* the server returned /// a session ID. Throws on error/failure. std::string decrypt_ons_response( - std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce); + std::string_view lowercase_name, + ustring_view ciphertext, + std::optional nonce); /// API: crypto/decrypt_push_notification /// diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index fdb831f6..6843d886 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -508,10 +510,44 @@ std::pair decrypt_from_blinded_recipient( } std::string decrypt_ons_response( - std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { + std::string_view lowercase_name, + ustring_view ciphertext, + std::optional nonce) { + // Handle old Argon2-based encryption used before HF16 + if (!nonce) { + if (ciphertext.size() < crypto_secretbox_MACBYTES) + throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + + uc32 key; + std::array salt = {0}; + + if (0 != crypto_pwhash( + key.data(), + key.size(), + lowercase_name.data(), + lowercase_name.size(), + salt.data(), + crypto_pwhash_OPSLIMIT_MODERATE, + crypto_pwhash_MEMLIMIT_MODERATE, + crypto_pwhash_ALG_ARGON2ID13)) + throw std::runtime_error{"Failed to generate key"}; + + ustring msg; + msg.resize(ciphertext.size() - crypto_secretbox_MACBYTES); + std::array nonce = {0}; + + if (0 != + crypto_secretbox_open_easy( + msg.data(), ciphertext.data(), ciphertext.size(), nonce.data(), key.data())) + throw std::runtime_error{"Failed to decrypt"}; + + std::string session_id = oxenc::to_hex(msg.begin(), msg.end()); + return session_id; + } + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; - if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) + if (nonce->size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) throw std::invalid_argument{"Invalid nonce: expected to be 24 bytes"}; // Hash the ONS name using BLAKE2b @@ -543,7 +579,7 @@ std::string decrypt_ons_response( ciphertext.size(), nullptr, 0, - nonce.data(), + nonce->data(), key.data())) throw std::runtime_error{"Failed to decrypt"}; @@ -718,16 +754,17 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( LIBSESSION_C_API bool session_decrypt_ons_response( const char* name_in, - size_t name_len, const unsigned char* ciphertext_in, size_t ciphertext_len, const unsigned char* nonce_in, char* session_id_out) { try { + std::optional nonce; + if (nonce_in) + nonce = ustring{*nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + auto session_id = session::decrypt_ons_response( - std::string_view{name_in, name_len}, - ustring_view{ciphertext_in, ciphertext_len}, - ustring_view{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}); + name_in, ustring_view{ciphertext_in, ciphertext_len}, nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 8c286b69..fa71c4cc 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -418,12 +418,16 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { auto ciphertext = "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" "1580d9a8c9b8a64cacfec97"_hexbytes; + auto ciphertext_legacy = + "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; ustring sid_data = "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; CHECK(decrypt_ons_response(name, ciphertext, nonce) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(decrypt_ons_response(name, ciphertext_legacy, std::nullopt) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK_THROWS(decrypt_ons_response(name, to_unsigned_sv("invalid"), nonce)); CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); } From 3e584f82e34c61c5539683e761bf93f9428b704c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 20 Aug 2024 17:53:13 +1000 Subject: [PATCH 383/572] Updated logic to keep dropped paths around until their requests finish --- include/session/network.hpp | 40 ++++---- src/network.cpp | 186 ++++++++++++++++++++++++------------ 2 files changed, 147 insertions(+), 79 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 038380e3..1ee5263a 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -63,10 +63,26 @@ struct service_node : public oxen::quic::RemoteAddress { struct connection_info { service_node node; + std::shared_ptr pending_requests; std::shared_ptr conn; std::shared_ptr stream; bool is_valid() const { return conn && stream && !stream->is_closing(); }; + bool has_pending_requests() const { return !pending_requests || ((*pending_requests) == 0); }; + + void add_pending_request() { + if (!pending_requests) + pending_requests = std::make_shared(0); + (*pending_requests)++; + }; + + // This is weird but since we are modifying the shared_ptr we aren't mutating + // the object so it can be a const function + void remove_pending_request() const { + if (!pending_requests) + return; + (*pending_requests)--; + }; }; struct onion_path { @@ -75,6 +91,7 @@ struct onion_path { uint8_t failure_count; bool is_valid() const { return !nodes.empty() && conn_info.is_valid(); }; + std::string to_string() const; bool operator==(const onion_path& other) const { // The `conn_info` and failure/timeout counts can be reset for a path in a number @@ -155,6 +172,7 @@ class Network { oxen::quic::Network net; std::shared_ptr endpoint; std::unordered_map> paths; + std::vector> paths_pending_drop; // Snode refresh state int snode_cache_refresh_failure_count; @@ -591,7 +609,8 @@ class Network { /// response elsewhere). std::pair validate_response(oxen::quic::message resp, bool is_bencoded); - void drop_path(std::string request_id, PathType path_type, onion_path path); + void drop_path_when_empty(std::string request_id, PathType path_type, onion_path path); + void clear_empty_pending_path_drops(); /// API: network/handle_errors /// @@ -614,25 +633,6 @@ class Network { std::optional status_code, std::optional response, std::optional handle_response); - - /// API: network/handle_node_error - /// - /// Convenience method to increment the failure count for a given node and path (if a node - /// doesn't have an associated path then just create one with the single node). This just calls - /// into the 'handle_errors' function in a way that will trigger an update to the failure - /// counts. - /// - /// Inputs: - /// - `node` -- [in] the node to increment the failure count for. - /// - `path_type` -- [in] type of path the node (or provided path) belong to. - /// - `conn_info` -- [in] the connection info for the request that failed. - /// - `request_id` -- [in] the request id for the original request which resulted in a node - /// error. - void handle_node_error( - service_node node, - PathType path_type, - connection_info conn_info, - std::string request_id); }; } // namespace session::network diff --git a/src/network.cpp b/src/network.cpp index 8e69889a..0250a78b 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -390,6 +390,17 @@ request_info request_info::make( _timeout}; } +std::string onion_path::to_string() const { + std::vector node_descriptions; + std::transform( + nodes.begin(), + nodes.end(), + std::back_inserter(node_descriptions), + [](const service_node& node) { return node.to_string(); }); + + return "{}"_format(fmt::join(node_descriptions, ", ")); +} + // MARK: Initialization Network::Network( @@ -896,7 +907,8 @@ void Network::establish_connection( // If the network is currently suspended then don't try to open a connection if (currently_suspended) - return callback({target, nullptr, nullptr}, "Network is suspended."); + return callback( + {target, std::make_shared(0), nullptr, nullptr}, "Network is suspended."); auto conn_key_pair = ed25519::ed25519_key_pair(); auto creds = quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(conn_key_pair.second)); @@ -924,7 +936,10 @@ void Network::establish_connection( std::call_once(*cb_called, [&]() { if (cb) { auto conn = conn_future.get(); - (*cb)({target, conn, conn->open_stream()}, + (*cb)({target, + std::make_shared(0), + conn, + conn->open_stream()}, std::nullopt); cb.reset(); } @@ -941,7 +956,8 @@ void Network::establish_connection( // triggered when try to establish a connection std::call_once(*cb_called, [&]() { if (cb) { - (*cb)({target, nullptr, nullptr}, std::nullopt); + (*cb)({target, std::make_shared(0), nullptr, nullptr}, + std::nullopt); cb.reset(); } }); @@ -959,12 +975,16 @@ void Network::establish_connection( if (!path.nodes.empty() && path.nodes.front() == target && path.conn_info.conn && conn.reference_id() == path.conn_info.conn->reference_id()) { - drop_path(request_id, path_type, path); + drop_path_when_empty(request_id, path_type, path); break; } } } + // Since a connection was closed we should also clear any pending path drops + // in case this connection was one of those + clear_empty_pending_path_drops(); + // If the connection failed with a handshake timeout then the node is // unreachable, either due to a device network issue or because the node is down // - since we frequently refresh the snode cache it's better to assume the @@ -1474,20 +1494,13 @@ void Network::build_path(PathType path_type, std::string request_id) { in_progress_path_builds.erase(request_id); // Log that a path was built - std::vector node_descriptions; - std::transform( - path_nodes.begin(), - path_nodes.end(), - std::back_inserter(node_descriptions), - [](const service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); log::info( cat, "Built new onion request path, now have {} {} path(s) ({}): [{}]", paths[path_type].size(), path_name, request_id, - path_description); + path.to_string()); // If the connection info is valid and it's a standard path then update the // connection status to connected @@ -1626,12 +1639,14 @@ void Network::get_service_nodes( payload.append("endpoint", "get_service_nodes"); payload.append("params", detail::get_service_nodes_params(limit).dump()); + conn_info.add_pending_request(); conn_info.stream->command( "oxend_request", payload.view(), - [this, request_id, cb = std::move(callback)](quic::message resp) { + [this, request_id, conn_info, cb = std::move(callback)](quic::message resp) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, request_id); std::vector result; + conn_info.remove_pending_request(); try { auto [status_code, body] = validate_response(resp, true); @@ -1643,6 +1658,11 @@ void Network::get_service_nodes( // Output the result cb(result, std::nullopt); + + // After completing a request we should try to clear any pending path drops (just in + // case this request was the final one on a pending path drop) + if (!conn_info.has_pending_requests()) + clear_empty_pending_path_drops(); }); } @@ -1702,8 +1722,14 @@ void Network::get_swarm( __PRETTY_FUNCTION__, hex_key, request_id); - if (!success || timeout || !response) + if (!success || timeout || !response) { + log::info( + cat, + "Failed to retrieve swarm due to error: {} ({}).", + response.value_or("Unknown error"), + request_id); return cb({}); + } std::vector swarm; @@ -1716,7 +1742,12 @@ void Network::get_swarm( for (auto& snode : response_json["snodes"]) swarm.emplace_back(node_from_json(snode)); - } catch (...) { + } catch (const std::exception& e) { + log::info( + cat, + "Failed to parse swarm due to error: {} ({}).", + e.what(), + request_id); return cb({}); } @@ -1783,6 +1814,7 @@ void Network::send_request( if (info.body) payload = convert_sv(*info.body); + conn_info.add_pending_request(); conn_info.stream->command( info.endpoint, payload, @@ -1792,6 +1824,7 @@ void Network::send_request( std::pair result; auto& [status_code, body] = result; + conn_info.remove_pending_request(); try { result = validate_response(resp, false); @@ -1803,6 +1836,11 @@ void Network::send_request( } cb(true, false, status_code, body); + + // After completing a request we should try to clear any pending path drops (just in + // case this request was the final one on a pending path drop) + if (!conn_info.has_pending_requests()) + clear_empty_pending_path_drops(); }); } @@ -2216,46 +2254,51 @@ std::pair Network::validate_response(quic::message resp, return {status_code, response_string}; } -void Network::drop_path(std::string request_id, PathType path_type, onion_path path) { - // Close the connection immediately (just in case there are other requests happening) - if (path.conn_info.conn) - path.conn_info.conn->close_connection(); - - auto path_nodes = path.nodes; +void Network::drop_path_when_empty(std::string request_id, PathType path_type, onion_path path) { + paths_pending_drop.emplace_back(path, path_type); paths[path_type].erase( std::remove(paths[path_type].begin(), paths[path_type].end(), path), paths[path_type].end()); - - std::vector node_descriptions; - std::transform( - path_nodes.begin(), - path_nodes.end(), - std::back_inserter(node_descriptions), - [](service_node& node) { return node.to_string(); }); - auto path_description = "{}"_format(fmt::join(node_descriptions, ", ")); log::info( cat, - "Dropping path, now have {} {} paths(s) ({}): [{}]", + "Flagging path to be dropped, now have {} {} paths(s) ({}): [{}].", paths[path_type].size(), path_type_name(path_type, single_path_mode), request_id, - path_description); + path.to_string()); - // Update the network status if we've removed all standard paths - if (paths[PathType::standard].empty()) - update_status(ConnectionStatus::disconnected); + // Clear any paths which are waiting to be dropped + clear_empty_pending_path_drops(); } -void Network::handle_node_error( - service_node node, PathType path_type, connection_info conn_info, std::string request_id) { - handle_errors( - request_info::make( - std::move(node), 0ms, std::nullopt, std::nullopt, path_type, request_id, ""), - conn_info, - false, - std::nullopt, - "Node Error", - std::nullopt); +void Network::clear_empty_pending_path_drops() { + auto remaining_standard_paths = 0; + std::erase_if(paths_pending_drop, [this, &remaining_standard_paths](const auto& path_info) { + // If the path is no longer valid then we can drop it + if (!path_info.first.is_valid()) { + log::info( + cat, + "Removing flagged {} path: No longer valid: [{}].", + path_type_name(path_info.second, single_path_mode), + path_info.first.to_string()); + return true; + } + + if (!path_info.first.conn_info.has_pending_requests()) { + log::info( + cat, + "Removing flagged {} path: No remaining requests: [{}].", + path_type_name(path_info.second, single_path_mode), + path_info.first.to_string()); + return true; + } + remaining_standard_paths++; + return false; + }); + + // Update the network status if we've removed all standard paths + if (remaining_standard_paths == 0 && paths[PathType::standard].empty()) + update_status(ConnectionStatus::disconnected); } void Network::handle_errors( @@ -2457,6 +2500,9 @@ void Network::handle_errors( // Retrieve the path for the connection_info (no paths share the same guard node so we can use // that to find it) + std::optional path; + auto is_active_path = true; + auto path_it = std::find_if( paths[info.path_type].begin(), paths[info.path_type].end(), @@ -2464,19 +2510,36 @@ void Network::handle_errors( return !path.nodes.empty() && path.nodes.front() == guard_node; }); - // If the path was already dropped then the snode pool would have already been - // updated so just log the failure and call the callback - if (path_it == paths[info.path_type].end()) { - log::info( - cat, "Request {} failed but {} path already dropped.", info.request_id, path_name); + // Try to retrieve the path this request was on, if it's not in an active or pending drop path + // then log a warning (as this shouldn't be possible) and call the callback + if (path_it != paths[info.path_type].end()) + path = *path_it; + else { + auto path_pending_drop_it = std::find_if( + paths_pending_drop.begin(), + paths_pending_drop.end(), + [guard_node = conn_info.node](const auto& path_info) { + return !path_info.first.nodes.empty() && + path_info.first.nodes.front() == guard_node; + }); - if (handle_response) - (*handle_response)(false, timeout, status_code, response); - return; + if (path_pending_drop_it == paths_pending_drop.end()) { + log::warning( + cat, + "Request {} failed but {} path already dropped.", + info.request_id, + path_name); + + if (handle_response) + (*handle_response)(false, timeout, status_code, response); + return; + } + path = path_pending_drop_it->first; + is_active_path = false; } // Update the failure counts and paths - auto updated_path = *path_it; + auto updated_path = *path; auto updated_failure_counts = snode_failure_counts; bool found_invalid_node = false; std::vector nodes_to_drop; @@ -2617,12 +2680,17 @@ void Network::handle_errors( return item.second == 0 || item.second >= snode_failure_threshold; }); - // Drop the path if invalid - if (updated_path.failure_count >= path_failure_threshold) - drop_path(info.request_id, info.path_type, *path_it); - else - std::replace( - paths[info.path_type].begin(), paths[info.path_type].end(), *path_it, updated_path); + // Drop the path if invalid (and currnetly an active path) + if (is_active_path) { + if (updated_path.failure_count >= path_failure_threshold) + drop_path_when_empty(info.request_id, info.path_type, *path_it); + else + std::replace( + paths[info.path_type].begin(), + paths[info.path_type].end(), + *path_it, + updated_path); + } // Update the snode cache { From 0ce9762e75f4bf7df779ca0e023c6605d6e4f0e7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 21 Aug 2024 14:31:33 +1000 Subject: [PATCH 384/572] Fixed a bug with the C API ONS lookup function --- src/session_encrypt.cpp | 2 +- tests/test_session_encrypt.cpp | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 6843d886..24ef71a3 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -761,7 +761,7 @@ LIBSESSION_C_API bool session_decrypt_ons_response( try { std::optional nonce; if (nonce_in) - nonce = ustring{*nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + nonce = ustring{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; auto session_id = session::decrypt_ons_response( name_in, ustring_view{ciphertext_in, ciphertext_len}, nonce); diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index fa71c4cc..392fb85e 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -421,8 +422,6 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { auto ciphertext_legacy = "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; - ustring sid_data = - "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; CHECK(decrypt_ons_response(name, ciphertext, nonce) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); @@ -432,6 +431,26 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); } +TEST_CASE("Session ONS response decryption C API", "[session-ons][session_decrypt_ons_response]") { + using namespace session; + + auto name = "test\0"; + auto ciphertext = + "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" + "1580d9a8c9b8a64cacfec97"_hexbytes; + auto ciphertext_legacy = + "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; + auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + + char ons1[66]; + CHECK(session_decrypt_ons_response(name, ciphertext.data(), ciphertext.size(), nonce.data(), ons1)); + CHECK(ons1 == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"sv); + + char ons2[66]; + CHECK(session_decrypt_ons_response(name, ciphertext_legacy.data(), ciphertext_legacy.size(), nullptr, ons2)); + CHECK(ons2 == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"sv); +} + TEST_CASE("Session push notification decryption", "[session-notification][decrypt]") { using namespace session; From 8c3bd7e861dc506018f46a79c3ee712e7c2ddad2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 21 Aug 2024 15:03:52 +1000 Subject: [PATCH 385/572] Ran formatter, fixed array size issue --- tests/test_session_encrypt.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 392fb85e..2a0f6eca 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,8 +1,8 @@ +#include #include #include #include -#include #include #include @@ -442,12 +442,14 @@ TEST_CASE("Session ONS response decryption C API", "[session-ons][session_decryp "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; - char ons1[66]; - CHECK(session_decrypt_ons_response(name, ciphertext.data(), ciphertext.size(), nonce.data(), ons1)); + char ons1[67]; + CHECK(session_decrypt_ons_response( + name, ciphertext.data(), ciphertext.size(), nonce.data(), ons1)); CHECK(ons1 == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"sv); - char ons2[66]; - CHECK(session_decrypt_ons_response(name, ciphertext_legacy.data(), ciphertext_legacy.size(), nullptr, ons2)); + char ons2[67]; + CHECK(session_decrypt_ons_response( + name, ciphertext_legacy.data(), ciphertext_legacy.size(), nullptr, ons2)); CHECK(ons2 == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"sv); } From faec15a5fc222eb6a72e360b331ce9628cfb8de7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 21 Aug 2024 15:44:06 +1000 Subject: [PATCH 386/572] Fixed issue breaking tests when testnet is busted, fixed some CI warnings --- include/session/network.hpp | 28 ++++++++++++++++++- src/network.cpp | 41 ++++++++++++++++++---------- tests/test_group_info.cpp | 2 -- tests/test_group_members.cpp | 2 -- tests/test_logging.cpp | 2 -- tests/test_network.cpp | 53 ++++++++++++++++++------------------ 6 files changed, 80 insertions(+), 48 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 1ee5263a..a41e88d1 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -68,7 +68,9 @@ struct connection_info { std::shared_ptr stream; bool is_valid() const { return conn && stream && !stream->is_closing(); }; - bool has_pending_requests() const { return !pending_requests || ((*pending_requests) == 0); }; + bool has_pending_requests() const { + return is_valid() && (!pending_requests || ((*pending_requests) == 0)); + }; void add_pending_request() { if (!pending_requests) @@ -91,6 +93,7 @@ struct onion_path { uint8_t failure_count; bool is_valid() const { return !nodes.empty() && conn_info.is_valid(); }; + bool has_pending_requests() const { return conn_info.has_pending_requests(); } std::string to_string() const; bool operator==(const onion_path& other) const { @@ -159,6 +162,7 @@ class Network { bool need_clear_cache = false; // Values persisted to disk + std::optional seed_node_cache_size; std::vector snode_cache; std::unordered_map snode_failure_counts; std::chrono::system_clock::time_point last_snode_cache_update{}; @@ -167,6 +171,7 @@ class Network { std::thread disk_write_thread; // General values + bool destroyed = false; bool suspended = false; ConnectionStatus status; oxen::quic::Network net; @@ -437,6 +442,14 @@ class Network { /// Retrieves or creates a new endpoint pointer. std::shared_ptr get_endpoint(); + /// API: network/min_snode_cache_size + /// + /// When talking to testnet it's occassionally possible for the cache size to be smaller than + /// the `min_snode_cache_count` value (which would result in an endless loop re-fetching the + /// node cache) so instead this function will return the smaller of the two if we've done a + /// fetch from a seed node. + size_t min_snode_cache_size() const; + /// API: network/get_unused_nodes /// /// Retrieves a list of all nodes in the cache which are currently unused (ie. not present in an @@ -609,7 +622,20 @@ class Network { /// response elsewhere). std::pair validate_response(oxen::quic::message resp, bool is_bencoded); + /// API: network/drop_path_when_empty + /// + /// Flags a path to be dropped once all pending requests have finished. + /// + /// Inputs: + /// - `request_id` -- [in] the request_id which triggered the path drop. + /// - `path_type` -- [in] the type of path to build. + /// - `path` -- [in] the path to be dropped. void drop_path_when_empty(std::string request_id, PathType path_type, onion_path path); + + /// API: network/clear_empty_pending_path_drops + /// + /// Iterates through all paths flagged to be dropped and actually drops any which are no longer + /// valid or have no more pending requests. void clear_empty_pending_path_drops(); /// API: network/handle_errors diff --git a/src/network.cpp b/src/network.cpp index 0250a78b..34a313ce 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -430,6 +430,7 @@ Network::Network( Network::~Network() { // We need to explicitly close the connections at the start of the destructor to prevent ban // memory errors due to complex logic with the quic::Network instance + destroyed = true; _close_connections(); { @@ -805,6 +806,7 @@ void Network::_close_connections() { request_queue.clear(); paths.clear(); path_build_queue.clear(); + paths_pending_drop.clear(); unused_connections.clear(); in_progress_connections.clear(); snode_refresh_results.reset(); @@ -852,8 +854,20 @@ std::shared_ptr Network::get_endpoint() { // MARK: Request Queues and Path Building +size_t Network::min_snode_cache_size() const { + if (!seed_node_cache_size) + return min_snode_cache_count; + + // If the seed node cache size is somehow smaller than `min_snode_cache_count` (ie. Testnet + // having issues) then the minimum size should be the full cache size (minus enough to build a + // path) or at least the size of a path + auto min_path_size = static_cast(path_size); + return std::min( + std::max(min_path_size, *seed_node_cache_size - min_path_size), min_snode_cache_count); +} + std::vector Network::get_unused_nodes() { - if (snode_cache.size() < min_snode_cache_count) + if (snode_cache.size() < min_snode_cache_size()) return {}; // Exclude any IPs that are already in use from existing paths @@ -962,6 +976,12 @@ void Network::establish_connection( } }); + // If the Network instance has been `destroyed` (ie. it's destructor has been + // called) then don't do any of the following logic as it'll likely result in + // undefined behaviours and crashes + if (destroyed) + return; + // Remove the connection from `unused_connection` if present std::erase_if(unused_connections, [&conn, &target](auto& unused_conn) { return (unused_conn.node == target && unused_conn.conn && @@ -1039,7 +1059,7 @@ void Network::establish_and_store_connection(std::string request_id) { unused_connection_and_path_build_nodes = get_unused_nodes(); // If there aren't enough unused nodes then trigger a cache refresh - if (unused_connection_and_path_build_nodes->size() < min_snode_cache_count) { + if (unused_connection_and_path_build_nodes->size() < min_snode_cache_size()) { log::trace( cat, "Unable to establish new connection due to lack of unused nodes, refreshing snode " @@ -1245,6 +1265,7 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r "nodes ({}).", nodes.size(), request_id); + seed_node_cache_size = nodes.size(); refresh_snode_cache_complete(nodes); }); }); @@ -1286,7 +1307,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id // If we don't have enough nodes in the unused nodes it likely means we didn't have // enough nodes in the cache so instead just fetch from the seed nodes (which is a // trusted source so we can update the cache from a single response) - if (!unused_snode_refresh_nodes || unused_snode_refresh_nodes->size() < min_snode_cache_count) + if (!unused_snode_refresh_nodes || unused_snode_refresh_nodes->size() < min_snode_cache_size()) return refresh_snode_cache_from_seed_nodes(request_id, true); // Target an unused node and increment the in progress refresh counter @@ -2275,20 +2296,12 @@ void Network::clear_empty_pending_path_drops() { auto remaining_standard_paths = 0; std::erase_if(paths_pending_drop, [this, &remaining_standard_paths](const auto& path_info) { // If the path is no longer valid then we can drop it - if (!path_info.first.is_valid()) { - log::info( - cat, - "Removing flagged {} path: No longer valid: [{}].", - path_type_name(path_info.second, single_path_mode), - path_info.first.to_string()); - return true; - } - - if (!path_info.first.conn_info.has_pending_requests()) { + if (!path_info.first.has_pending_requests()) { log::info( cat, - "Removing flagged {} path: No remaining requests: [{}].", + "Removing flagged {} path: {}: [{}].", path_type_name(path_info.second, single_path_mode), + (path_info.first.is_valid() ? "No remaining requests" : "No longer valid"), path_info.first.to_string()); return true; } diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index df70900a..9d9482db 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -13,8 +13,6 @@ using namespace std::literals; using namespace oxenc::literals; -static constexpr int64_t created_ts = 1680064059; - using namespace session::config; TEST_CASE("Group Info settings", "[config][groups][info]") { diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index e2304351..164f62ae 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -13,8 +13,6 @@ using namespace std::literals; using namespace oxenc::literals; -static constexpr int64_t created_ts = 1680064059; - using namespace session::config; constexpr bool is_prime100(int i) { diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index b7d8b7ed..17230f70 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -56,9 +56,7 @@ TEST_CASE("Logging callbacks", "[logging]") { } log::critical(log::Cat("test.a"), "abc {}", 21 * 2); - int line0 = __LINE__ - 1; log::info(log::Cat("test.b"), "hi"); - int line1 = __LINE__ - 1; oxen::log::clear_sinks(); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 1458c46f..60f41582 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -65,7 +65,7 @@ class TestNetwork : public Network { } void add_path(PathType path_type, std::vector nodes) { - paths[path_type].emplace_back(onion_path{{nodes[0], nullptr, nullptr}, nodes, 0}); + paths[path_type].emplace_back(onion_path{{nodes[0], nullptr, nullptr, nullptr}, nodes, 0}); } void set_paths(PathType path_type, std::vector paths_) { @@ -281,7 +281,7 @@ TEST_CASE("Network error handling", "[network]") { auto target2 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; auto target3 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.2", uint16_t{2}}; auto target4 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.3", uint16_t{3}}; - auto path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; + auto path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", target, @@ -308,7 +308,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, code, std::nullopt, @@ -337,7 +337,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 500, std::nullopt, @@ -358,14 +358,14 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 1); // Check general error handling with no response (too many path failures) - path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 9}; + path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; network.set_paths(PathType::standard, {path}); network.set_failure_count(target, 0); network.set_failure_count(target2, 0); network.set_failure_count(target3, 0); network.handle_errors( mock_request, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 500, std::nullopt, @@ -387,7 +387,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network.get_failure_count(PathType::standard, path) == 0); // Path dropped and reset // Check general error handling with a path and specific node failure - path = onion_path{{{target}, nullptr, nullptr}, {target, target2, target3}, 0}; + path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.set_snode_cache({target, target2, target3, target4}); network.set_paths(PathType::standard, {path}); @@ -396,7 +396,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 500, response, @@ -426,7 +426,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 421, std::nullopt, @@ -463,7 +463,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request2, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 421, std::nullopt, @@ -486,7 +486,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_paths(PathType::standard, {path}); network.handle_errors( mock_request2, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 421, "Test", @@ -528,7 +528,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request2, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 421, response, @@ -574,7 +574,7 @@ TEST_CASE("Network error handling", "[network]") { network.reset_calls(); network.handle_errors( mock_request3, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, 421, response, @@ -607,7 +607,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request4, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, true, std::nullopt, "Test", @@ -633,7 +633,7 @@ TEST_CASE("Network error handling", "[network]") { network.set_failure_count(target3, 0); network.handle_errors( mock_request4, - {target, nullptr, nullptr}, + {target, nullptr, nullptr, nullptr}, false, std::nullopt, "500 Internal Server Error", @@ -659,7 +659,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { std::vector snode_cache; for (uint16_t i = 0; i < 12; ++i) snode_cache.emplace_back(service_node{ed_pk, {2, 8, 0}, fmt::format("0.0.0.{}", i), i}); - auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr}; + auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr, nullptr}; // Nothing should happen if the network is suspended network.emplace(std::nullopt, true, false, false); @@ -768,8 +768,8 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { uint16_t{35400}}; auto network = TestNetwork(std::nullopt, true, false, false); auto info = request_info::make(target, 0ms, std::nullopt, std::nullopt); - auto invalid_path = - onion_path{{test_service_node, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; + auto invalid_path = onion_path{ + {test_service_node, nullptr, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; // It returns nothing when given no path options CHECK_FALSE(network.find_valid_path(info, {}).has_value()); @@ -811,7 +811,8 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; std::optional network; - auto invalid_path = onion_path{connection_info{target, nullptr, nullptr}, {target}, uint8_t{0}}; + auto invalid_path = + onion_path{connection_info{target, nullptr, nullptr, nullptr}, {target}, uint8_t{0}}; // It does not add additional path builds if there is already a path and it's in // 'single_path_mode' @@ -968,15 +969,13 @@ TEST_CASE("Network requests", "[network][send_request]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); + INFO("*result.response is: " << *result.response); + REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); - try { - auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); - CHECK(response.contains("t")); - CHECK(response.contains("version")); - } catch (...) { - REQUIRE(*result.response == "{VALID JSON}"); - } + auto response = nlohmann::json::parse(*result.response); + CHECK(response.contains("hf")); + CHECK(response.contains("t")); + CHECK(response.contains("version")); } TEST_CASE("Network onion request", "[network][send_onion_request]") { From ba7919d304bfe44a75a77d418638bf82ac0b7f93 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 21 Aug 2024 16:29:26 +1000 Subject: [PATCH 387/572] Exposes functions to clear the oxen::log sinks --- include/session/logging.h | 5 +++++ include/session/logging.hpp | 5 +++++ src/logging.cpp | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/include/session/logging.h b/include/session/logging.h index d027a32c..98811934 100644 --- a/include/session/logging.h +++ b/include/session/logging.h @@ -63,6 +63,11 @@ LIBSESSION_EXPORT LOG_LEVEL session_logger_get_level(const char* cat_name); /// correctly LIBSESSION_EXPORT void session_manual_log(const char* msg); +/// API: session/session_clear_loggers +/// +/// Clears all currently set loggers +LIBSESSION_EXPORT void session_clear_loggers(); + #ifdef __cplusplus } #endif diff --git a/include/session/logging.hpp b/include/session/logging.hpp index 14c2a5de..2b949120 100644 --- a/include/session/logging.hpp +++ b/include/session/logging.hpp @@ -104,4 +104,9 @@ LogLevel logger_get_level(std::string cat_name); /// correctly void manual_log(std::string_view msg); +/// API: session/clear_loggers +/// +/// Clears all currently set loggers +void clear_loggers(); + } // namespace session diff --git a/src/logging.cpp b/src/logging.cpp index 28a457b6..779b1f66 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -51,6 +51,10 @@ LogLevel logger_get_level(std::string cat_name) { return log::get_level(std::move(cat_name)); } +void clear_loggers() { + log::clear_sinks(); +} + } // namespace session extern "C" { @@ -95,4 +99,8 @@ LIBSESSION_C_API void session_manual_log(const char* msg) { session::manual_log(msg); } +LIBSESSION_C_API void session_clear_loggers() { + session::clear_loggers(); +} + } // extern "C" \ No newline at end of file From af0ab996da46bcbeeea5e70831cda7e23c955243 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 23 Aug 2024 11:18:32 +1000 Subject: [PATCH 388/572] Updated the docs for legacy group decrypt function to reduce misuse --- include/session/session_encrypt.h | 4 ++-- include/session/session_encrypt.hpp | 2 +- src/session_encrypt.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 4b6399de..2e363050 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -103,7 +103,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming( /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. /// - `ciphertext_len` -- [in] Length of `ciphertext_in` /// - `x25519_pubkey` -- [in] the x25519 public key of the receiver (32 bytes). -/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (32 bytes). +/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (64 bytes). /// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, /// hex-encoded session_id of the message's author will be written if decryption/verification was /// successful. @@ -121,7 +121,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( const unsigned char* ciphertext_in, size_t ciphertext_len, const unsigned char* x25519_pubkey, /* 32 bytes */ - const unsigned char* x25519_seckey, /* 32 bytes */ + const unsigned char* x25519_seckey, /* 64 bytes */ char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index e1381f7b..b20c70a7 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -192,7 +192,7 @@ std::pair decrypt_incoming_session_id( /// /// Inputs: /// - `x25519_pubkey` -- the 32 byte x25519 public key of the recipient. -/// - `x25519_seckey` -- the 32 byte x25519 private key of the recipient. +/// - `x25519_seckey` -- the 64 byte x25519 private key of the recipient. /// - `ciphertext` -- the encrypted data /// /// Outputs: diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 24ef71a3..8b99bad2 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -709,7 +709,7 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( try { auto result = session::decrypt_incoming_session_id( ustring_view{x25519_pubkey, 32}, - ustring_view{x25519_seckey, 32}, + ustring_view{x25519_seckey, 64}, ustring_view{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; From 2bf8c81443494f227a9509ddd95889f196b668d6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 27 Aug 2024 17:39:36 +1000 Subject: [PATCH 389/572] Reverted 64-byte legacy group secret key decryption (only need 32 bytes) --- include/session/session_encrypt.h | 4 ++-- include/session/session_encrypt.hpp | 2 +- src/session_encrypt.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 2e363050..4b6399de 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -103,7 +103,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming( /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. /// - `ciphertext_len` -- [in] Length of `ciphertext_in` /// - `x25519_pubkey` -- [in] the x25519 public key of the receiver (32 bytes). -/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (64 bytes). +/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (32 bytes). /// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, /// hex-encoded session_id of the message's author will be written if decryption/verification was /// successful. @@ -121,7 +121,7 @@ LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( const unsigned char* ciphertext_in, size_t ciphertext_len, const unsigned char* x25519_pubkey, /* 32 bytes */ - const unsigned char* x25519_seckey, /* 64 bytes */ + const unsigned char* x25519_seckey, /* 32 bytes */ char* session_id_out, /* 67 byte output buffer */ unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index b20c70a7..e1381f7b 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -192,7 +192,7 @@ std::pair decrypt_incoming_session_id( /// /// Inputs: /// - `x25519_pubkey` -- the 32 byte x25519 public key of the recipient. -/// - `x25519_seckey` -- the 64 byte x25519 private key of the recipient. +/// - `x25519_seckey` -- the 32 byte x25519 private key of the recipient. /// - `ciphertext` -- the encrypted data /// /// Outputs: diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 8b99bad2..24ef71a3 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -709,7 +709,7 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( try { auto result = session::decrypt_incoming_session_id( ustring_view{x25519_pubkey, 32}, - ustring_view{x25519_seckey, 64}, + ustring_view{x25519_seckey, 32}, ustring_view{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; From de7d8a6580d8317007460d8dcbf4ce821644f80a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 29 Aug 2024 11:43:14 +1000 Subject: [PATCH 390/572] Fixed an incorrect size for the curve25519 secret key --- include/session/curve25519.hpp | 2 +- src/curve25519.cpp | 4 ++-- tests/test_curve25519.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index 34a437ad..21e8de47 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -7,7 +7,7 @@ namespace session::curve25519 { /// Generates a random curve25519 key pair -std::pair, std::array> curve25519_key_pair(); +std::pair, std::array> curve25519_key_pair(); /// API: curve25519/to_curve25519_pubkey /// diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 81870cc3..1c8f11b3 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -10,9 +10,9 @@ namespace session::curve25519 { -std::pair, std::array> curve25519_key_pair() { +std::pair, std::array> curve25519_key_pair() { std::array curve_pk; - std::array curve_sk; + std::array curve_sk; crypto_box_keypair(curve_pk.data(), curve_sk.data()); return {curve_pk, curve_sk}; diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp index 3acf6aa4..275e8c7b 100644 --- a/tests/test_curve25519.cpp +++ b/tests/test_curve25519.cpp @@ -12,7 +12,7 @@ TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { auto kp2 = session::curve25519::curve25519_key_pair(); CHECK(kp1.first.size() == 32); - CHECK(kp1.second.size() == 64); + CHECK(kp1.second.size() == 32); CHECK(kp1.first != kp2.first); CHECK(kp1.second != kp2.second); } From 12df14a6fc4c3276c651dbc612377ff0d80fb323 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 4 Sep 2024 14:19:39 +1000 Subject: [PATCH 391/572] Updated libQuic and cleaned up 'wrap_exceptions' usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated to latest libQuic • Updated all C functions using 'wrap_exceptions' to return a bool --- external/oxen-libquic | 2 +- include/session/config/base.h | 29 ++++++--- include/session/config/contacts.h | 4 +- include/session/config/convo_info_volatile.h | 20 ++++-- include/session/config/groups/members.h | 5 +- include/session/config/user_groups.h | 15 ++++- src/config/base.cpp | 64 ++++++++++++++------ src/config/contacts.cpp | 10 ++- src/config/convo_info_volatile.cpp | 41 ++++++++++--- src/config/groups/members.cpp | 9 ++- src/config/user_groups.cpp | 31 +++++++--- 11 files changed, 174 insertions(+), 56 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 91ca79e5..79d3f89b 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 91ca79e578d7edc4d108e530b6a6e072a212878c +Subproject commit 79d3f89b9260880904c831bf8bebafc916ca7f7d diff --git a/include/session/config/base.h b/include/session/config/base.h index ff1a6540..0fa722a3 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -204,7 +204,10 @@ LIBSESSION_EXPORT void config_confirm_pushed( /// - `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); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool config_dump(config_object* conf, unsigned char** out, size_t* outlen); /// API: base/config_needs_dump /// @@ -311,7 +314,7 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size /// /// Declaration: /// ```cpp -/// VOID config_add_key( +/// BOOL config_add_key( /// [in, out] config_object* conf, /// [in] const unsigned char* key /// ); @@ -321,7 +324,10 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size /// 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); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool config_add_key(config_object* conf, const unsigned char* key); /// API: base/config_add_key_low_prio /// @@ -330,7 +336,7 @@ LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* /// /// Declaration: /// ```cpp -/// VOID config_add_key_low_prio( +/// BOOL config_add_key_low_prio( /// [in, out] config_object* conf, /// [in] const unsigned char* key /// ); @@ -340,7 +346,10 @@ LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* /// 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); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool config_add_key_low_prio(config_object* conf, const unsigned char* key); /// API: base/config_clear_keys /// @@ -486,7 +495,10 @@ LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf /// 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); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool config_set_sig_keys(config_object* conf, const unsigned char* secret); /// API: base/config_set_sig_pubkey /// @@ -496,7 +508,10 @@ LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned c /// /// 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); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey); /// API: base/config_get_sig_pubkey /// diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index ee863074..e2752153 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -152,8 +152,8 @@ LIBSESSION_EXPORT bool contacts_get_or_construct( /// - `contact` -- [in] Pointer containing the contact info data /// /// Output: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void contacts_set(config_object* conf, const contacts_contact* contact); +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool 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 diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index eacecdb9..952b6ff7 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -360,7 +360,10 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_legacy_group( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [in] Pointer to conversation info structure -LIBSESSION_EXPORT void convo_info_volatile_set_1to1( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool convo_info_volatile_set_1to1( config_object* conf, const convo_info_volatile_1to1* convo); /// API: convo_info_volatile/convo_info_volatile_set_community @@ -378,7 +381,10 @@ LIBSESSION_EXPORT void convo_info_volatile_set_1to1( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [in] Pointer to community info structure -LIBSESSION_EXPORT void convo_info_volatile_set_community( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo); /// API: convo_info_volatile/convo_info_volatile_set_group @@ -396,7 +402,10 @@ LIBSESSION_EXPORT void convo_info_volatile_set_community( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [in] Pointer to group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_group( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool convo_info_volatile_set_group( config_object* conf, const convo_info_volatile_group* convo); /// API: convo_info_volatile/convo_info_volatile_set_legacy_group @@ -414,7 +423,10 @@ LIBSESSION_EXPORT void convo_info_volatile_set_group( /// Inputs: /// - `conf` -- [in] Pointer to the config object /// - `convo` -- [in] Pointer to legacy group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_legacy_group( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool convo_info_volatile_set_legacy_group( config_object* conf, const convo_info_volatile_legacy_group* convo); /// API: convo_info_volatile/convo_info_volatile_erase_1to1 diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index eb57b165..9c8fa904 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -107,7 +107,10 @@ LIBSESSION_EXPORT bool groups_members_get_or_construct( /// Inputs: /// - `conf` -- [in, out] Pointer to the config object /// - `member` -- [in] Pointer containing the member info data -LIBSESSION_EXPORT void groups_members_set(config_object* conf, const config_group_member* member); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool groups_members_set(config_object* conf, const config_group_member* member); /// API: groups/groups_members_erase /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 919faa79..cce70309 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -325,7 +325,10 @@ LIBSESSION_EXPORT void user_groups_set_community( /// Inputs: /// - `conf` -- [in] Pointer to config_object object /// - `group` -- [in] Pointer to a group info object -LIBSESSION_EXPORT void user_groups_set_group(config_object* conf, const ugroups_group_info* group); +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool user_groups_set_group(config_object* conf, const ugroups_group_info* group); /// API: user_groups/user_groups_set_legacy_group /// @@ -345,7 +348,10 @@ LIBSESSION_EXPORT void user_groups_set_group(config_object* conf, const ugroups_ /// Inputs: /// - `conf` -- [in] Pointer to config_object object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_legacy_group( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool user_groups_set_legacy_group( config_object* conf, const ugroups_legacy_group_info* group); /// API: user_groups/user_groups_set_free_legacy_group @@ -365,7 +371,10 @@ LIBSESSION_EXPORT void user_groups_set_legacy_group( /// Inputs: /// - `conf` -- [in] Pointer to config_object object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_free_legacy_group( +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool user_groups_set_free_legacy_group( config_object* conf, ugroups_legacy_group_info* group); /// API: user_groups/user_groups_erase_community diff --git a/src/config/base.cpp b/src/config/base.cpp index dcf18bf0..13081f9e 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -728,14 +728,18 @@ LIBSESSION_EXPORT void config_confirm_pushed( unbox(conf)->confirm_pushed(seqno, msg_hash); } -LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen) { - wrap_exceptions(conf, [&] { - 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_dump(config_object* conf, unsigned char** out, size_t* outlen) { + return wrap_exceptions( + conf, + [&] { + 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()); + return true; + }, + false); } LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { @@ -769,12 +773,24 @@ LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size return buf; } -LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - wrap_exceptions(conf, [&] { unbox(conf)->add_key({key, 32}); }); +LIBSESSION_EXPORT bool config_add_key(config_object* conf, const unsigned char* key) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->add_key({key, 32}); + return true; + }, + false); } -LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - wrap_exceptions(conf, [&] { unbox(conf)->add_key({key, 32}, /*high_priority=*/false); }); +LIBSESSION_EXPORT bool config_add_key_low_prio(config_object* conf, const unsigned char* key) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->add_key({key, 32}, /*high_priority=*/false); + return true; + }, + false); } LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { return unbox(conf)->clear_keys(); @@ -800,12 +816,24 @@ 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) { - wrap_exceptions(conf, [&] { unbox(conf)->set_sig_keys({secret, 64}); }); -} - -LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - wrap_exceptions(conf, [&] { unbox(conf)->set_sig_pubkey({pubkey, 32}); }); +LIBSESSION_EXPORT bool config_set_sig_keys(config_object* conf, const unsigned char* secret) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_sig_keys({secret, 64}); + return true; + }, + false); +} + +LIBSESSION_EXPORT bool config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_sig_pubkey({pubkey, 32}); + return true; + }, + false); } LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 0fba7b55..e23f213a 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -249,8 +249,14 @@ void Contacts::set(const contact_info& contact) { set_positive_int(info["j"], contact.created); } -LIBSESSION_C_API void contacts_set(config_object* conf, const contacts_contact* contact) { - wrap_exceptions(conf, [&] { unbox(conf)->set(contact_info{*contact}); }); +LIBSESSION_C_API bool contacts_set(config_object* conf, const contacts_contact* contact) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(contact_info{*contact}); + return true; + }, + false); } void Contacts::set_name(std::string_view session_id, std::string name) { diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 20f2bb06..3355f1dc 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -602,22 +602,45 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_legacy_group( false); } -LIBSESSION_C_API void convo_info_volatile_set_1to1( +LIBSESSION_C_API bool convo_info_volatile_set_1to1( config_object* conf, const convo_info_volatile_1to1* convo) { - wrap_exceptions(conf, [&] { unbox(conf)->set(convo::one_to_one{*convo}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::one_to_one{*convo}); + return true; + }, + false); } -LIBSESSION_C_API void convo_info_volatile_set_community( +LIBSESSION_C_API bool convo_info_volatile_set_community( config_object* conf, const convo_info_volatile_community* convo) { - wrap_exceptions(conf, [&] { unbox(conf)->set(convo::community{*convo}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::community{*convo}); + return true; + }, + false); } -LIBSESSION_C_API void convo_info_volatile_set_group( +LIBSESSION_C_API bool convo_info_volatile_set_group( config_object* conf, const convo_info_volatile_group* convo) { - wrap_exceptions(conf, [&] { unbox(conf)->set(convo::group{*convo}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::group{*convo}); + return true; + }, + false); } -LIBSESSION_C_API void convo_info_volatile_set_legacy_group( +LIBSESSION_C_API bool convo_info_volatile_set_legacy_group( config_object* conf, const convo_info_volatile_legacy_group* convo) { - wrap_exceptions( - conf, [&] { unbox(conf)->set(convo::legacy_group{*convo}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::legacy_group{*convo}); + return true; + }, + false); } LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 96a772d5..cbb88e25 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -219,9 +219,14 @@ LIBSESSION_C_API bool groups_members_get_or_construct( false); } -LIBSESSION_C_API void groups_members_set(config_object* conf, const config_group_member* member) { +LIBSESSION_C_API bool groups_members_set(config_object* conf, const config_group_member* member) { return wrap_exceptions( - conf, [&] { unbox(conf)->set(groups::member{*member}); }); + conf, + [&] { + unbox(conf)->set(groups::member{*member}); + return true; + }, + false); } LIBSESSION_C_API bool groups_members_erase(config_object* conf, const char* session_id) { diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 3c5044b1..74097811 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -708,17 +708,34 @@ 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 user_groups_set_group(config_object* conf, const ugroups_group_info* group) { - wrap_exceptions(conf, [&] { unbox(conf)->set(group_info{*group}); }); +LIBSESSION_C_API bool user_groups_set_group(config_object* conf, const ugroups_group_info* group) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(group_info{*group}); + return true; + }, + false); } -LIBSESSION_C_API void user_groups_set_legacy_group( +LIBSESSION_C_API bool user_groups_set_legacy_group( config_object* conf, const ugroups_legacy_group_info* group) { - wrap_exceptions(conf, [&] { unbox(conf)->set(legacy_group_info{*group}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(legacy_group_info{*group}); + return true; + }, + false); } -LIBSESSION_C_API void user_groups_set_free_legacy_group( +LIBSESSION_C_API bool user_groups_set_free_legacy_group( config_object* conf, ugroups_legacy_group_info* group) { - wrap_exceptions( - conf, [&] { unbox(conf)->set(legacy_group_info{std::move(*group)}); }); + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(legacy_group_info{std::move(*group)}); + return true; + }, + false); } LIBSESSION_C_API bool user_groups_erase_community( From e1a76ebb7b8f90f4dc07e05ccaf88643da2829ad Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 5 Sep 2024 17:46:46 +1000 Subject: [PATCH 392/572] Added a function to compute a message hash --- include/session/session_encrypt.h | 17 +++++ include/session/session_encrypt.hpp | 14 ++++ src/session_encrypt.cpp | 103 ++++++++++++++++++++++++++++ tests/test_session_encrypt.cpp | 16 +++++ 4 files changed, 150 insertions(+) diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 4b6399de..56b5f90f 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -5,6 +5,7 @@ extern "C" { #endif #include +#include #include "export.h" @@ -212,6 +213,22 @@ LIBSESSION_EXPORT bool session_decrypt_push_notification( unsigned char** plaintext_out, size_t* plaintext_len); +/// API: crypto/compute_message_hash +/// +/// Computes the hash for a message. +/// +/// Inputs: +/// - `pubkey_hex_in` -- the pubkey as a 67 character hex string that the message will be stored in. +/// NULL terminated. +/// - `ns` -- the namespace that the message will be stored in. +/// - `data` -- the base64 encoded message data that will be stored for the message. NULL +/// terminated. +/// +/// Outputs: +/// - `std::string` -- a deterministic hash for the message. +LIBSESSION_EXPORT bool session_compute_message_hash( + const char* pubkey_hex_in, int16_t ns, const char* base64_data_in, char* hash_out); + #ifdef __cplusplus } #endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index e1381f7b..8b3e6c9d 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -261,4 +261,18 @@ std::string decrypt_ons_response( /// successful. Throws on error/failure. ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key); +/// API: crypto/compute_message_hash +/// +/// Computes the hash for a message. +/// +/// Inputs: +/// - `pubkey_hex` -- the pubkey as a 66 character hex string that the message will be stored in. +/// - `ns` -- the namespace that the message will be stored in. +/// - `data` -- the base64 encoded message data that will be stored for the message. +/// +/// Outputs: +/// - `std::string` -- a deterministic hash for the message. +std::string compute_message_hash( + const std::string_view pubkey_hex, int16_t ns, std::string_view data); + } // namespace session diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 24ef71a3..50758103 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,10 +1,12 @@ #include "session/session_encrypt.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -25,6 +27,35 @@ using namespace std::literals; namespace session { +namespace detail { + inline int64_t to_epoch_ms(std::chrono::system_clock::time_point t) { + return std::chrono::duration_cast(t.time_since_epoch()).count(); + } + + // detail::to_hashable takes either an integral type, system_clock::time_point, or a string + // type and converts it to a string_view by writing an integer value (using std::to_chars) + // into the buffer space (which should be at least 20 bytes), and returning a string_view + // into the written buffer space. For strings/string_views the string_view is returned + // directly from the argument. system_clock::time_points are converted into integral + // milliseconds since epoch then treated as an integer value. + template , int> = 0> + std::string_view to_hashable(const T& val, char*& buffer) { + auto [p, ec] = std::to_chars(buffer, buffer + 20, val); + std::string_view s(buffer, p - buffer); + buffer = p; + return s; + } + inline std::string_view to_hashable( + const std::chrono::system_clock::time_point& val, char*& buffer) { + return to_hashable(to_epoch_ms(val), buffer); + } + template , int> = 0> + std::string_view to_hashable(const T& value, char*&) { + return value; + } + +} // namespace detail + // Version tag we prepend to encrypted-for-blinded-user messages. This is here so we can detect if // some future version changes the format (and if not even try to load it). inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; @@ -626,6 +657,66 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { return buf; } +template +std::string compute_hash(Func hasher, const T&... args) { + // Allocate a buffer of 20 bytes per integral value (which is the largest the any integral + // value can be when stringified). + std::array< + char, + (0 + ... + + (std::is_integral_v || std::is_same_v + ? 20 + : 0))> + buffer; + auto* b = buffer.data(); + return hasher({detail::to_hashable(args, b)...}); +} + +std::string compute_hash_blake2b_b64(std::vector parts) { + constexpr size_t HASH_SIZE = 32; + crypto_generichash_state state; + crypto_generichash_init(&state, nullptr, 0, HASH_SIZE); + for (const auto& s : parts) + crypto_generichash_update( + &state, reinterpret_cast(s.data()), s.size()); + std::array hash; + crypto_generichash_final(&state, hash.data(), HASH_SIZE); + + std::string b64hash = oxenc::to_base64(hash.begin(), hash.end()); + // Trim padding: + while (!b64hash.empty() && b64hash.back() == '=') + b64hash.pop_back(); + return b64hash; +} + +std::string compute_message_hash( + const std::string_view pubkey_hex, int16_t ns, std::string_view data) { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{ + "Invalid pubkey_hex: Expecting 66 character hex-encoded pubkey"}; + + // This function is based on the `computeMessageHash` function on the storage-server used to + // generate a message hash: + // https://github.com/oxen-io/oxen-storage-server/blob/dev/oxenss/rpc/request_handler.cpp + auto pubkey = oxenc::from_hex(pubkey_hex.substr(2)); + uint8_t netid_raw; + oxenc::from_hex(pubkey_hex.begin(), pubkey_hex.begin() + 2, &netid_raw); + char netid = static_cast(netid_raw); + + std::array ns_buf; + char* ns_buf_ptr = ns_buf.data(); + std::string_view ns_for_hash = ns != 0 ? detail::to_hashable(ns, ns_buf_ptr) : ""sv; + + auto decoded_data = oxenc::from_base64(data); + + return compute_hash( + compute_hash_blake2b_b64, + std::string_view{&netid, 1}, + pubkey, + ns_for_hash, + decoded_data); +} + } // namespace session using namespace session; @@ -791,3 +882,15 @@ LIBSESSION_C_API bool session_decrypt_push_notification( return false; } } + +LIBSESSION_C_API bool session_compute_message_hash( + const char* pubkey_hex_in, int16_t ns, const char* base64_data_in, char* hash_out) { + try { + auto hash = session::compute_message_hash(pubkey_hex_in, ns, base64_data_in); + + std::memcpy(hash_out, hash.c_str(), hash.size() + 1); + return true; + } catch (...) { + return false; + } +} diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 2a0f6eca..a15ef826 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -469,3 +469,19 @@ TEST_CASE("Session push notification decryption", "[session-notification][decryp CHECK_THROWS(decrypt_push_notification(to_unsigned_sv("invalid"), enc_key)); CHECK_THROWS(decrypt_push_notification(payload, to_unsigned_sv("invalid"))); } + +TEST_CASE("Session message hash", "[session][message-hash]") { + using namespace session; + + auto pubkey_hex = "0518981d2822aabc9ba8dbf83f2feac4c70eb737930bc4f254fa71e01f8464a049"sv; + auto base64_data1 = + "CAESpQMKA1BVVBIPL2FwaS92MS9tZXNzYWdlGoEDCAcSQjA1MTg5ODFkMjgyMmFhYmM5YmE4ZGJmODNmMmZlYWM0YzcwZWI3Mzc5MzBiYzRmMjU0ZmE3MWUwMWY4NDY0YTA0OSirlfaFnDI4AUKvAvYMh0I1qhBp9tDOzZhl7vMFuD7a9k/BLvPHMOkTrYsjGj2ri7T6AoJjVm/dDMsXlEP58VaGFSv+mcctCRstYox+3CchbQoVieBi2NGE1bqCeiZeLOMxQxleSZ94vzi7CoC8/NCLmTBzKvw0GBo77Tz37yPGxNLp2QO1xOuDVqM1/+4Sdj+JzMpfsZA8PDMmG3T1o8DJJ/EmwlxsmKM/eAjqtNpdF1G7wtZW5im9fiW11sQgG0/+5EsqxqEoo0xsi5TL6L9DN6zKhjXC9bu/QAfI5ZIpF5+9IHzKashPAjSswBZmlesjbFbNvNgBq4hSeXIxjtg7xDm/hfXao1WRa3TMHgfZs2bY+cNlDGqArjZT9q5XTVxsQYXq+mz/koh0qxiJktAC3C0ixs7CInORFiD18omD4oqX1/IB"sv; + auto base64_data2 = + "CAESpAMKA1BVVBIPL2FwaS92MS9tZXNzYWdlGoEDCAcSQjA1MTg5ODFkMjgyMmFhYmM5YmE4ZGJmODNmMmZlYWM0YzcwZWI3Mzc5MzBiYzRmMjU0ZmE3MWUwMWY4NDY0YTA0OSi/7peInDI4AUKvAsCTN9WMEkajMbC7EA6QOClzdXK3W6MTEElFotQ6PGNa2IKfYb+iu0MRC6ph+1hE5hzfay00v0UfB5Xen3dBgZ2drwToYhYb1zqRlIeesdwT0Yt6ct+Gn47PBL4oXOv7PJo3ys3jlq1t+xbAN/vum/8ART9xVhNIZ+3dOpS62z8pwSqusWECGw9dJDgFN6g0+2R85dco/HP9Z2SiGBaAJulKFUXKaT+jMHab3nPjoqke/lVG544iJAmNbI+KJr61YgtsbVfO02pje1RXeQtQacAtWpCYlin4fNtr6ANTs8aJDb1H1JFOG/r8PZHkPl1Fl/2cDppngZYJJo6/8IH9FpZS64le+mZy2BjP7UKfEx3ulmJIwpfqcqe9qvoTbGtljSf8wRylUkeo1E7Gg2WP8SDrgdXBwIaZp24="sv; + int16_t ns = -10; + + CHECK(compute_message_hash(pubkey_hex, ns, base64_data1) == + "xREbCx9GRzDiuU8GsEK7rR1InU6peC3vp10cBkTUDPg"); + CHECK(compute_message_hash(pubkey_hex, ns, base64_data2) == + "apKu8OMjrbU+YeVWpMSyrr1wHq51K3uKD8WM0F4E1cE"); +} From ac34aa26aa78066aeaa443fe30f384821d4ae36e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 6 Sep 2024 13:50:32 +1000 Subject: [PATCH 393/572] Added code to calculate swarms locally instead of fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added code to calculate swarms locally instead of fetching • Stopped persisting the 'failure_count' to disk (it gets reset when refreshing the snode cache so why bother) • Stopped persisting the swarm cache to disk (it's now calculated locally so no need) --- include/session/network.hpp | 58 ++- src/network.cpp | 903 +++++++++++++----------------------- src/onionreq/builder.cpp | 2 + tests/test_network.cpp | 577 +++++++++++++++-------- 4 files changed, 734 insertions(+), 806 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index a41e88d1..9f2c3e1a 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "onionreq/builder.hpp" @@ -28,27 +29,43 @@ enum class PathType { download, }; +using swarm_id_t = uint64_t; +constexpr swarm_id_t INVALID_SWARM_ID = std::numeric_limits::max(); + struct service_node : public oxen::quic::RemoteAddress { public: std::vector storage_server_version; + swarm_id_t swarm_id; service_node() = delete; template service_node( - std::string_view remote_pk, std::vector storage_server_version, Opt&&... opts) : + std::string_view remote_pk, + std::vector storage_server_version, + swarm_id_t swarm_id, + Opt&&... opts) : oxen::quic::RemoteAddress{remote_pk, std::forward(opts)...}, - storage_server_version{storage_server_version} {} + storage_server_version{storage_server_version}, + swarm_id{swarm_id} {} template - service_node(ustring_view remote_pk, std::vector storage_server_version, Opt&&... opts) : + service_node( + ustring_view remote_pk, + std::vector storage_server_version, + swarm_id_t swarm_id, + Opt&&... opts) : oxen::quic::RemoteAddress{remote_pk, std::forward(opts)...}, - storage_server_version{storage_server_version} {} + storage_server_version{storage_server_version}, + swarm_id{swarm_id} {} service_node(const service_node& obj) : - oxen::quic::RemoteAddress{obj}, storage_server_version{obj.storage_server_version} {} + oxen::quic::RemoteAddress{obj}, + storage_server_version{obj.storage_server_version}, + swarm_id{obj.swarm_id} {} service_node& operator=(const service_node& obj) { storage_server_version = obj.storage_server_version; + swarm_id = obj.swarm_id; oxen::quic::RemoteAddress::operator=(obj); _copy_internals(obj); return *this; @@ -57,7 +74,7 @@ struct service_node : public oxen::quic::RemoteAddress { bool operator==(const service_node& other) const { return static_cast(*this) == static_cast(other) && - storage_server_version == other.storage_server_version; + storage_server_version == other.storage_server_version && swarm_id == other.swarm_id; } }; @@ -104,6 +121,10 @@ struct onion_path { }; namespace detail { + swarm_id_t pubkey_to_swarm_space(const session::onionreq::x25519_pubkey& pk); + std::vector>> generate_swarms( + std::vector nodes); + std::optional node_for_destination(onionreq::network_destination destination); session::onionreq::x25519_pubkey pubkey_for_destination( @@ -123,8 +144,10 @@ struct request_info { std::optional _body = std::nullopt); enum class RetryReason { + none, decryption_failure, redirect, + redirect_swarm_refresh, }; std::string request_id; @@ -156,17 +179,12 @@ class Network { bool has_pending_disk_write = false; bool shut_down_disk_thread = false; bool need_write = false; - bool need_pool_write = false; - bool need_failure_counts_write = false; - bool need_swarm_write = false; bool need_clear_cache = false; // Values persisted to disk std::optional seed_node_cache_size; std::vector snode_cache; - std::unordered_map snode_failure_counts; std::chrono::system_clock::time_point last_snode_cache_update{}; - std::unordered_map> swarm_cache; std::thread disk_write_thread; @@ -178,6 +196,10 @@ class Network { std::shared_ptr endpoint; std::unordered_map> paths; std::vector> paths_pending_drop; + std::vector unused_nodes; + std::unordered_map snode_failure_counts; + std::vector>> all_swarms; + std::unordered_map>> swarm_cache; // Snode refresh state int snode_cache_refresh_failure_count; @@ -188,10 +210,9 @@ class Network { std::shared_ptr>> snode_refresh_results; // First hop state - std::optional> unused_connection_and_path_build_nodes; int connection_failures = 0; std::deque unused_connections; - std::unordered_set in_progress_connections; + std::unordered_map in_progress_connections; // Path build state int path_build_failures = 0; @@ -256,16 +277,7 @@ class Network { /// the callback will be called with an empty list). void get_swarm( session::onionreq::x25519_pubkey swarm_pubkey, - std::function swarm)> callback); - - /// API: network/set_swarm - /// - /// Update the nodes to be used for a swarm. This function should never be called directly. - /// - /// Inputs: - /// - 'swarm_pubkey' - [in] public key for the swarm. - /// - `swarm` -- [in] nodes for the swarm. - void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm); + std::function swarm)> callback); /// API: network/get_random_nodes /// diff --git a/src/network.cpp b/src/network.cpp index 34a313ce..e8ade608 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -56,18 +56,12 @@ namespace { // The amount of time the snode cache can be used before it needs to be refreshed constexpr auto snode_cache_expiration_duration = 2h; - // The amount of time a swarm cache can be used before it needs to be refreshed - constexpr auto swarm_cache_expiration_duration = (24h * 7); - // The smallest size the snode cache can get to before we need to fetch more. constexpr size_t min_snode_cache_count = 12; // The number of snodes to use to refresh the cache. constexpr int num_snodes_to_refresh_cache_from = 3; - // The smallest size a swarm can get to before we need to fetch it again. - constexpr uint16_t min_swarm_snode_count = 3; - // The number of snodes (including the guard snode) in a path. constexpr uint8_t path_size = 3; @@ -77,10 +71,10 @@ namespace { // The number of times a snode can fail before it's replaced. constexpr uint16_t snode_failure_threshold = 3; - // File names - const fs::path file_testnet{u8"testnet"}, file_snode_pool{u8"snode_pool"}, - file_snode_pool_updated{u8"snode_pool_updated"}, swarm_dir{u8"swarm"}, - default_cache_path{u8"."}, file_snode_failure_counts{u8"snode_failure_counts"}; + const fs::path default_cache_path{u8"."}, file_testnet{u8"testnet"}, + file_snode_pool{u8"snode_pool"}; + const std::vector legacy_files{ + u8"snode_pool_updated", u8"swarm", u8"snode_failure_counts"}; constexpr auto node_not_found_prefix = "502 Bad Gateway\n\nNext node not found: "sv; constexpr auto node_not_found_prefix_no_status = "Next node not found: "sv; @@ -165,13 +159,17 @@ namespace { else port = json["port_omq"].get(); - return {oxenc::from_hex(pk_ed), storage_server_version, ip, port}; + swarm_id_t swarm_id = INVALID_SWARM_ID; + if (json.contains("swarm_id")) + swarm_id = json["swarm_id"].get(); + + return {oxenc::from_hex(pk_ed), storage_server_version, swarm_id, ip, port}; } service_node node_from_disk(std::string_view str, bool can_ignore_version = false) { - // Format is "{ip}|{port}|{version}|{ed_pubkey} + // Format is "{ip}|{port}|{version}|{ed_pubkey}|{swarm_id}" auto parts = split(str, "|"); - if (parts.size() != 4) + if (parts.size() != 5) throw std::invalid_argument("Invalid service node serialisation: {}"_format(str)); if (parts[3].size() != 64 || !oxenc::is_hex(parts[3])) throw std::invalid_argument{ @@ -185,9 +183,13 @@ namespace { if (!can_ignore_version && storage_server_version == std::vector{0}) throw std::invalid_argument{"Invalid service node serialization: invalid version"}; + swarm_id_t swarm_id = INVALID_SWARM_ID; + quic::parse_int(parts[4], swarm_id); + return { oxenc::from_hex(parts[3]), // ed25519_pubkey storage_server_version, // storage_server_version + swarm_id, // swarm_id std::string(parts[0]), // ip port, // port }; @@ -195,18 +197,18 @@ namespace { const std::vector seed_nodes_testnet{ node_from_disk("144.76.164.202|35400|2.8.0|" - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"sv)}; + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9|"sv)}; const std::vector seed_nodes_mainnet{ node_from_disk("144.76.164.202|20200|2.8.0|" - "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef"sv), + "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef|"sv), node_from_disk("88.99.102.229|20201|2.8.0|" - "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce"sv), + "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce|"sv), node_from_disk("195.16.73.17|20202|2.8.0|" - "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f"sv), + "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f|"sv), node_from_disk("104.194.11.120|20203|2.8.0|" - "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b"sv), + "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b|"sv), node_from_disk("104.194.8.115|20204|2.8.0|" - "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0"sv)}; + "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0|"sv)}; constexpr auto file_server = "filev2.getsession.org"sv; constexpr auto file_server_pubkey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"sv; @@ -227,15 +229,16 @@ namespace { }; std::string node_to_disk(service_node node) { - // Format is "{ip}|{port}|{version}|{ed_pubkey} + // Format is "{ip}|{port}|{version}|{ed_pubkey}|{swarm_id}" auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); return fmt::format( - "{}|{}|{}|{}", + "{}|{}|{}|{}|{}", node.host(), node.port(), "{}"_format(fmt::join(node.storage_server_version, ".")), - ed25519_pubkey_hex); + ed25519_pubkey_hex, + node.swarm_id); } session::onionreq::x25519_pubkey compute_xpk(ustring_view ed25519_pk) { @@ -264,6 +267,35 @@ namespace { } // namespace namespace detail { + swarm_id_t pubkey_to_swarm_space(const session::onionreq::x25519_pubkey& pk) { + swarm_id_t res = 0; + for (size_t i = 0; i < 4; i++) { + swarm_id_t buf; + std::memcpy(&buf, pk.data() + i * 8, 8); + res ^= buf; + } + oxenc::big_to_host_inplace(res); + + return res; + } + + std::vector>> generate_swarms( + std::vector nodes) { + std::vector>> result; + std::unordered_map> _grouped_nodes; + + for (const auto& node : nodes) + _grouped_nodes[node.swarm_id].push_back(node); + + for (auto& [swarm_id, nodes] : _grouped_nodes) + result.emplace_back(swarm_id, std::move(nodes)); + + std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { + return a.first < b.first; + }); + return result; + } + std::optional node_for_destination(network_destination destination) { if (auto* dest = std::get_if(&destination)) return *dest; @@ -288,7 +320,8 @@ namespace detail { {{"public_ip", true}, {"pubkey_ed25519", true}, {"storage_lmq_port", true}, - {"storage_server_version", true}}}}; + {"storage_server_version", true}, + {"swarm_id", true}}}}; if (limit) params["limit"] = *limit; @@ -318,13 +351,14 @@ namespace detail { std::vector storage_server_version; node_consumer.skip_until("storage_server_version"); auto version_consumer = node_consumer.consume_list_consumer(); + auto swarm_id = consume_integer(node_consumer, "swarm_id"); while (!version_consumer.is_finished()) { storage_server_version.emplace_back(version_consumer.consume_integer()); } result.emplace_back( - pubkey_ed25519, storage_server_version, public_ip, storage_lmq_port); + pubkey_ed25519, storage_server_version, swarm_id, public_ip, storage_lmq_port); } return result; @@ -356,9 +390,12 @@ namespace detail { auto reason = "unknown retry"; switch (*info.retry_reason) { + case request_info::RetryReason::none: reason = "unknown retry"; break; case request_info::RetryReason::redirect: reason = "421 retry"; break; - case request_info::RetryReason::decryption_failure: reason = "decryption error"; break; + case request_info::RetryReason::redirect_swarm_refresh: + reason = "421 swarm refresh retry"; + break; } log::info( @@ -451,38 +488,24 @@ void Network::load_cache_from_disk() { if (use_testnet != fs::exists(testnet_stub) && fs::exists(testnet_stub)) fs::remove_all(cache_path); - // Create the cache directory (and swarm_dir, inside it) if needed - auto swarm_path = cache_path / swarm_dir; - fs::create_directories(swarm_path); + // Remove any legacy files (don't want to leave old data around) + for (const auto& path : legacy_files) { + auto path_to_remove = cache_path / path; + fs::remove_all(path_to_remove); + } // If we are using testnet then create a file to indicate that if (use_testnet) write_whole_file(testnet_stub); - // Load the last time the snode pool was updated - // - // Note: We aren't just reading the write time of the file because Apple consider - // accessing file timestamps a method that can be used to track the user (and we - // want to avoid being flagged as using such) - if (auto last_updated_path = cache_path / file_snode_pool_updated; - fs::exists(last_updated_path)) { - try { - auto timestamp_str = read_whole_file(last_updated_path); - while (timestamp_str.ends_with('\n')) - timestamp_str.pop_back(); - - std::time_t timestamp; - if (!quic::parse_int(timestamp_str, timestamp)) - throw std::runtime_error{"invalid file data: expected timestamp first line"}; - - last_snode_cache_update = std::chrono::system_clock::from_time_t(timestamp); - } catch (const std::exception& e) { - log::error(cat, "Ignoring invalid last update timestamp file: {}", e.what()); - } - } - // Load the snode pool if (auto pool_path = cache_path / file_snode_pool; fs::exists(pool_path)) { + auto ftime = fs::last_write_time(pool_path); + last_snode_cache_update = + std::chrono::time_point_cast( + ftime - fs::file_time_type::clock::now() + + std::chrono::system_clock::now()); + auto file = open_for_reading(pool_path); std::vector loaded_cache; std::string line; @@ -497,127 +520,13 @@ void Network::load_cache_from_disk() { } if (invalid_entries > 0) - log::warning( - cat, "Skipped {} invalid entries in snode pool cache.", invalid_entries); + log::warning(cat, "Skipped {} invalid entries in snode cache.", invalid_entries); snode_cache = loaded_cache; + all_swarms = detail::generate_swarms(loaded_cache); } - // Load the failure counts - if (auto failure_counts_path = cache_path / file_snode_failure_counts; - fs::exists(failure_counts_path)) { - auto file = open_for_reading(failure_counts_path); - std::unordered_map loaded_failure_count; - std::string line; - auto invalid_entries = 0; - - while (std::getline(file, line)) { - try { - auto parts = split(line, "|"); - uint8_t failure_count; - - if (parts.size() != 2) - throw std::invalid_argument( - "Invalid failure count serialisation: {}"_format(line)); - if (!quic::parse_int(parts[1], failure_count)) - throw std::invalid_argument{ - "Invalid failure count serialization: invalid failure count"}; - - // If we somehow already have a value then we should use whichever has the - // larger failure count (want to avoid keeping a bad node around longer than - // needed) - if (loaded_failure_count.try_emplace(std::string(parts[0]), failure_count) - .first->second < failure_count) - loaded_failure_count[std::string(parts[0])] = failure_count; - } catch (...) { - ++invalid_entries; - } - } - - if (invalid_entries > 0) - log::warning( - cat, - "Skipped {} invalid entries in snode failure count cache.", - invalid_entries); - - snode_failure_counts = loaded_failure_count; - } - - // Load the swarm cache - auto time_now = std::chrono::system_clock::now(); - std::unordered_map> loaded_cache; - std::vector caches_to_remove; - auto invalid_swarm_entries = 0; - - for (auto& entry : fs::directory_iterator(swarm_path)) { - // If the pubkey was valid then process the content - const auto& path = entry.path(); - auto file = open_for_reading(path); - std::vector nodes; - std::string line; - bool checked_swarm_expiration = false; - std::chrono::seconds swarm_lifetime = 0s; - auto filename = path.filename().string(); - - while (std::getline(file, line)) { - try { - // If we haven't checked if the swarm cache has expired then do so, removing - // any expired/invalid caches - if (!checked_swarm_expiration) { - std::time_t timestamp; - if (!quic::parse_int(line, timestamp)) - throw std::runtime_error{ - "invalid file data: expected timestamp first line"}; - auto swarm_last_updated = std::chrono::system_clock::from_time_t(timestamp); - swarm_lifetime = std::chrono::duration_cast( - time_now - swarm_last_updated); - checked_swarm_expiration = true; - - if (swarm_lifetime < swarm_cache_expiration_duration) - throw load_cache_exception{"Expired swarm cache."}; - - continue; - } - - // Otherwise try to parse as a node (for the swarm cache we can ignore invalid - // versions as the `get_swarm` API doesn't return version info) - nodes.push_back(node_from_disk(line, true)); - } catch (const std::exception& e) { - // Don't bother logging for expired entries (we include the count separately at - // the end) - if (dynamic_cast(&e) == nullptr) - ++invalid_swarm_entries; - - // The cache is invalid, we should remove it - if (!checked_swarm_expiration) { - caches_to_remove.emplace_back(path); - break; - } - } - } - - // If we got nodes the add it to the cache, otherwise we want to remove it - if (!nodes.empty()) - loaded_cache[filename] = std::move(nodes); - else - caches_to_remove.emplace_back(path); - } - - if (invalid_swarm_entries > 0) - log::warning(cat, "Skipped {} invalid entries in swarm cache.", invalid_swarm_entries); - - swarm_cache = loaded_cache; - - // Remove any expired cache files - for (auto& expired_cache : caches_to_remove) - fs::remove_all(expired_cache); - - log::info( - cat, - "Loaded cache of {} snodes, {} swarms ({} expired swarms).", - snode_cache.size(), - swarm_cache.size(), - caches_to_remove.size()); + log::info(cat, "Loaded cache of {} snodes, {} swarms.", snode_cache.size(), all_swarms.size()); } catch (const std::exception& e) { log::error(cat, "Failed to load snode cache, will rebuild ({}).", e.what()); @@ -648,96 +557,43 @@ void Network::update_disk_cache_throttled(bool force_immediate_write) { void Network::disk_write_thread_loop() { std::unique_lock lock{snode_cache_mutex}; while (true) { - snode_cache_cv.wait(lock, [this] { return need_write || shut_down_disk_thread; }); + snode_cache_cv.wait( + lock, [this] { return need_write || need_clear_cache || shut_down_disk_thread; }); if (need_write) { - // Make local copies so that we can release the lock and not - // worry about other threads wanting to change things: + // Make a local copy so that we can release the lock and not + // worry about other threads wanting to change things auto snode_cache_write = snode_cache; - auto snode_failure_counts_write = snode_failure_counts; - auto last_pool_update_write = last_snode_cache_update; - auto swarm_cache_write = swarm_cache; lock.unlock(); { try { // Create the cache directories if needed - auto swarm_base = cache_path / swarm_dir; - fs::create_directories(swarm_base); - - // Save the snode pool to disk - if (need_pool_write) { - auto pool_path = cache_path / file_snode_pool, pool_tmp = pool_path; - pool_tmp += u8"_new"; - - { - std::stringstream ss; - for (auto& snode : snode_cache_write) - ss << node_to_disk(snode) << '\n'; - - std::ofstream file(pool_tmp, std::ios::binary); - file << ss.rdbuf(); - } - - fs::rename(pool_tmp, pool_path); - - // Write the last update timestamp to disk - write_whole_file( - cache_path / file_snode_pool_updated, - "{}"_format(std::chrono::system_clock::to_time_t( - last_pool_update_write))); - need_pool_write = false; - log::debug(cat, "Finished writing snode pool cache to disk."); - } - - // Save the snode failure counts to disk - if (need_failure_counts_write) { - auto failure_counts_path = cache_path / file_snode_failure_counts, - failure_counts_tmp = failure_counts_path; - failure_counts_tmp += u8"_new"; - - { - std::stringstream ss; - for (auto& [key, count] : snode_failure_counts_write) - ss << fmt::format("{}|{}", key, count) << '\n'; + fs::create_directories(cache_path); - std::ofstream file(failure_counts_tmp, std::ios::binary); - file << ss.rdbuf(); - } - - fs::rename(failure_counts_tmp, failure_counts_path); - need_failure_counts_write = false; - log::debug(cat, "Finished writing snode failure counts to disk."); + // If we are using testnet then create a file to indicate that + if (use_testnet) { + auto testnet_stub = cache_path / file_testnet; + write_whole_file(testnet_stub); } - // Write the swarm cache to disk - if (need_swarm_write) { - auto time_now = std::chrono::system_clock::now(); - - for (auto& [key, swarm] : swarm_cache_write) { - auto swarm_path = swarm_base / key, swarm_tmp = swarm_path; - swarm_tmp += u8"_new"; - - // Write the timestamp - std::stringstream ss; - ss << std::chrono::system_clock::to_time_t(time_now) << '\n'; - - // Write the nodes - for (auto& snode : swarm) - ss << node_to_disk(snode) << '\n'; + // Save the snode pool to disk + auto pool_path = cache_path / file_snode_pool, pool_tmp = pool_path; + pool_tmp += u8"_new"; - // FIXME: In the future we should store the swarm info in the encrypted - // database instead of a plaintext file - std::ofstream swarm_file(swarm_tmp, std::ios::binary); - swarm_file << ss.rdbuf(); + { + std::stringstream ss; + for (auto& snode : snode_cache_write) + ss << node_to_disk(snode) << '\n'; - fs::rename(swarm_tmp, swarm_path); - } - need_swarm_write = false; - log::debug(cat, "Finished writing swarm cache to disk."); + std::ofstream file(pool_tmp, std::ios::binary); + file << ss.rdbuf(); } + fs::rename(pool_tmp, pool_path); need_write = false; + + log::debug(cat, "Finished writing snode cache to disk."); } catch (const std::exception& e) { log::error(cat, "Failed to write snode cache: {}", e.what()); } @@ -746,9 +602,6 @@ void Network::disk_write_thread_loop() { } if (need_clear_cache) { snode_cache = {}; - last_snode_cache_update = {}; - snode_failure_counts = {}; - swarm_cache = {}; lock.unlock(); if (fs::exists(cache_path)) @@ -811,7 +664,6 @@ void Network::_close_connections() { in_progress_connections.clear(); snode_refresh_results.reset(); current_snode_cache_refresh_request_id = std::nullopt; - unused_connection_and_path_build_nodes = std::nullopt; update_status(ConnectionStatus::disconnected); log::info(cat, "Closed all connections."); @@ -878,8 +730,8 @@ std::vector Network::get_unused_nodes() { node_ips_to_exlude.emplace_back(conn_info.node.to_ipv4()); // Exclude in progress connections - for (const auto& conn_info : unused_connections) - node_ips_to_exlude.emplace_back(conn_info.node.to_ipv4()); + for (const auto& [request_id, node] : in_progress_connections) + node_ips_to_exlude.emplace_back(node.to_ipv4()); // Exclude pending requests for (const auto& [path_type, path_type_requests] : request_queue) @@ -887,6 +739,17 @@ std::vector Network::get_unused_nodes() { if (auto* dest = std::get_if(&info.destination)) node_ips_to_exlude.emplace_back(dest->to_ipv4()); + // Exclude any nodes which have surpassed the failure threshold + for (const auto& [node_string, failure_count] : snode_failure_counts) + if (failure_count >= snode_failure_threshold) { + size_t colon_pos = node_string.find(':'); + + if (colon_pos != std::string::npos) + node_ips_to_exlude.emplace_back(quic::ipv4{node_string.substr(0, colon_pos)}); + else + node_ips_to_exlude.emplace_back(quic::ipv4{node_string}); + } + // Populate the unused nodes with any nodes in the cache which shouldn't be excluded std::vector result; @@ -1006,39 +869,11 @@ void Network::establish_connection( clear_empty_pending_path_drops(); // If the connection failed with a handshake timeout then the node is - // unreachable, either due to a device network issue or because the node is down - // - since we frequently refresh the snode cache it's better to assume the - // latter to avoid using a potentially bad node being used in the path so we - // want to drop the snode from the cache and any swarms - if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) { - { - std::lock_guard lock{snode_cache_mutex}; - - // Update the snode failure counts - if (snode_failure_counts.erase(target.to_string()) > 0) - need_failure_counts_write = true; - - // Remove the node from any swarms - for (auto& [key, nodes] : swarm_cache) { - auto it = std::remove(nodes.begin(), nodes.end(), target); - if (it != nodes.end()) { - nodes.erase(it, nodes.end()); - need_swarm_write = true; - } - } - - // Remove the node from the cache - auto it = std::remove(snode_cache.begin(), snode_cache.end(), target); - if (it != snode_cache.end()) { - snode_cache.erase(it, snode_cache.end()); - need_pool_write = true; - } - - if (need_failure_counts_write || need_swarm_write || need_pool_write) - need_write = true; - } - update_disk_cache_throttled(); - } + // unreachable, either due to a device network issue or because the node + // is down so set the failure count to the failure threshold so it won't + // be used for subsequent requests + if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) + snode_failure_counts[target.to_string()] = snode_failure_threshold; }); }); @@ -1055,11 +890,11 @@ void Network::establish_and_store_connection(std::string request_id) { update_status(ConnectionStatus::connecting); // Re-populate the unused nodes if it ends up being empty - if (!unused_connection_and_path_build_nodes || unused_connection_and_path_build_nodes->empty()) - unused_connection_and_path_build_nodes = get_unused_nodes(); + if (unused_nodes.empty()) + unused_nodes = get_unused_nodes(); // If there aren't enough unused nodes then trigger a cache refresh - if (unused_connection_and_path_build_nodes->size() < min_snode_cache_size()) { + if (unused_nodes.size() < min_snode_cache_size()) { log::trace( cat, "Unable to establish new connection due to lack of unused nodes, refreshing snode " @@ -1081,13 +916,13 @@ void Network::establish_and_store_connection(std::string request_id) { connection_failures = 0; // Grab a node from the `unused_nodes` list to establish a connection to - auto target_node = unused_connection_and_path_build_nodes->back(); - unused_connection_and_path_build_nodes->pop_back(); + auto target_node = unused_nodes.back(); + unused_nodes.pop_back(); // Try to establish a new connection to the target (this has a 3s handshake timeout as we // wouldn't want to use any nodes which take longer than that anyway) log::info(cat, "Establishing connection to {} for {}.", target_node.to_string(), request_id); - in_progress_connections.emplace(request_id); + in_progress_connections.emplace(request_id, target_node); establish_connection( request_id, @@ -1131,13 +966,6 @@ void Network::establish_and_store_connection(std::string request_id) { net.call_soon([this]() { establish_and_store_connection(random::random_base32(4)); }); - - // If there are no more pending requests, path builds or connections then we should - // reset the `unused_connection_and_path_build_nodes` since it shouldn't be needed - // anymore - if (request_queue.empty() && path_build_queue.empty() && - in_progress_connections.empty()) - unused_connection_and_path_build_nodes = std::nullopt; }); } @@ -1151,7 +979,6 @@ void Network::refresh_snode_cache_complete(std::vector nodes) { std::lock_guard lock{snode_cache_mutex}; snode_cache = nodes; last_snode_cache_update = std::chrono::system_clock::now(); - need_pool_write = true; need_write = true; } update_disk_cache_throttled(); @@ -1163,6 +990,18 @@ void Network::refresh_snode_cache_complete(std::vector nodes) { unused_snode_refresh_nodes = std::nullopt; snode_refresh_results.reset(); + // Reset the snode failure counts (assume if the snode refresh includes + // nodes then they are valid) + snode_failure_counts.clear(); + + // Since we've updated the snode cache the swarm cache could be invalid + // so we need to regenerate it (the resulting `all_swarms` needs to be + // stored in ascending order as it is required for the logic to find the + // appropriate swarm for a given pubkey) + all_swarms.clear(); + swarm_cache.clear(); + all_swarms = detail::generate_swarms(nodes); + // Run any post-refresh processes for (const auto& callback : after_snode_cache_refresh) net.call_soon([cb = std::move(callback)]() { cb(); }); @@ -1292,27 +1131,25 @@ void Network::refresh_snode_cache(std::optional existing_request_id return; } - // Reset the snode refresh states when starting a new snode cache refresh + // We are starting a new cache refresh so store an identifier for it if (!current_snode_cache_refresh_request_id) { log::info(cat, "Refreshing snode cache ({}).", request_id); current_snode_cache_refresh_request_id = request_id; - - // Shuffle to ensure we pick random nodes to fetch from - CSRNG rng; - unused_snode_refresh_nodes = get_unused_nodes(); - std::shuffle(unused_snode_refresh_nodes->begin(), unused_snode_refresh_nodes->end(), rng); - snode_refresh_results = std::make_shared>>(); } - // If we don't have enough nodes in the unused nodes it likely means we didn't have - // enough nodes in the cache so instead just fetch from the seed nodes (which is a - // trusted source so we can update the cache from a single response) - if (!unused_snode_refresh_nodes || unused_snode_refresh_nodes->size() < min_snode_cache_size()) + // If we don't have enough nodes in the unused nodes then refresh it + if (unused_nodes.size() < min_snode_cache_size()) + unused_nodes = get_unused_nodes(); + + // If we still don't have enough nodes in the unused nodes it likely means we didn't + // have enough nodes in the cache so instead just fetch from the seed nodes (which is + // a trusted source so we can update the cache from a single response) + if (unused_nodes.size() < min_snode_cache_size()) return refresh_snode_cache_from_seed_nodes(request_id, true); // Target an unused node and increment the in progress refresh counter - auto target_node = unused_snode_refresh_nodes->back(); - unused_snode_refresh_nodes->pop_back(); + auto target_node = unused_nodes.back(); + unused_nodes.pop_back(); in_progress_snode_cache_refresh_count++; // If there are still more concurrent refresh_snode_cache requests we want to trigger then @@ -1395,7 +1232,6 @@ void Network::refresh_snode_cache(std::optional existing_request_id current_snode_cache_refresh_request_id = std::nullopt; snode_cache_refresh_failure_count = 0; in_progress_snode_cache_refresh_count = 0; - unused_snode_refresh_nodes = std::nullopt; snode_refresh_results.reset(); return; } @@ -1452,12 +1288,11 @@ void Network::build_path(PathType path_type, std::string request_id) { } // Reset the unused nodes list if it's too small - if ((!unused_connection_and_path_build_nodes || - unused_connection_and_path_build_nodes->size() < path_size)) - unused_connection_and_path_build_nodes = get_unused_nodes(); + if (unused_nodes.size() < path_size) + unused_nodes = get_unused_nodes(); // If we still don't have enough unused nodes then we need to refresh the cache - if (unused_connection_and_path_build_nodes->size() < path_size) { + if (unused_nodes.size() < path_size) { log::info( cat, "Re-queing {} path build due to insufficient nodes ({}).", @@ -1478,11 +1313,11 @@ void Network::build_path(PathType path_type, std::string request_id) { std::vector path_nodes = {conn_info.node}; while (path_nodes.size() < path_size) { - if (unused_connection_and_path_build_nodes->empty()) { + if (unused_nodes.empty()) { // Log the error and try build again after a slight delay log::info( cat, - "Unable to build {} path due to lack of suitable unused path build nodes ({}).", + "Unable to build {} path due to lack of suitable unused nodes ({}).", path_name, request_id); @@ -1496,8 +1331,8 @@ void Network::build_path(PathType path_type, std::string request_id) { } // Grab the next unused node to continue building the path - auto node = unused_connection_and_path_build_nodes->back(); - unused_connection_and_path_build_nodes->pop_back(); + auto node = unused_nodes.back(); + unused_nodes.pop_back(); // Ensure we don't put two nodes with the same IP into the same path auto snode_with_ip_it = std::find_if( @@ -1559,11 +1394,6 @@ void Network::build_path(PathType path_type, std::string request_id) { }); } else request_queue.erase(path_type); - - // If there are no more pending requests, path builds or connections then we should reset the - // `unused_connection_and_path_build_nodes` since it shouldn't be needed anymore - if (request_queue.empty() && path_build_queue.empty() && in_progress_connections.empty()) - unused_connection_and_path_build_nodes = std::nullopt; } std::optional Network::find_valid_path( @@ -1687,118 +1517,65 @@ void Network::get_service_nodes( }); } -// MARK: Node Retrieval +// MARK: Swarm Management void Network::get_swarm( session::onionreq::x25519_pubkey swarm_pubkey, - std::function swarm)> callback) { - auto request_id = random::random_base32(4); - log::trace(cat, "{} called for {} as {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex(), request_id); - - net.call([this, request_id, swarm_pubkey, cb = std::move(callback)]() { - // If we have a cached swarm, and it meets the minimum size requirements, then return it - if (swarm_cache[swarm_pubkey.hex()].size() > min_swarm_snode_count) - return cb(swarm_cache[swarm_pubkey.hex()]); - - // Pick a random node from the snode pool to fetch the swarm from - log::info( - cat, - "Get swarm had no valid cached swarm for {}, fetching from random node ({}).", - swarm_pubkey.hex(), - request_id); - - // If we have no snode cache then we need to rebuild the cache and run this request again - // once it's rebuild - if (snode_cache.empty()) { + std::function swarm)> callback) { + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex()); + + net.call([this, swarm_pubkey, cb = std::move(callback)]() { + // If we have a cached swarm then return it + auto cached_swarm = swarm_cache[swarm_pubkey.hex()]; + if (!cached_swarm.second.empty()) + return cb(cached_swarm.first, cached_swarm.second); + + // If we have no snode cache or no swarms then we need to rebuild the cache (which will also + // rebuild the swarms) and run this request again + if (snode_cache.empty() || all_swarms.empty()) { after_snode_cache_refresh.emplace_back([this, swarm_pubkey, cb = std::move(cb)]() { get_swarm(swarm_pubkey, std::move(cb)); }); return net.call_soon([this]() { refresh_snode_cache(); }); } - CSRNG rng; - auto random_cache = snode_cache; - std::shuffle(random_cache.begin(), random_cache.end(), rng); - - nlohmann::json params{{"pubkey", "05" + swarm_pubkey.hex()}}; - nlohmann::json payload{ - {"method", "get_swarm"}, - {"params", params}, - }; - auto info = request_info::make( - random_cache.front(), - quic::DEFAULT_TIMEOUT, - ustring{quic::to_usv(payload.dump())}, - swarm_pubkey, - PathType::standard, - request_id); - - _send_onion_request( - info, - [this, hex_key = swarm_pubkey.hex(), request_id, cb = std::move(cb)]( - bool success, bool timeout, int16_t, std::optional response) { - log::trace( - cat, - "{} got response for {} as {}.", - __PRETTY_FUNCTION__, - hex_key, - request_id); - if (!success || timeout || !response) { - log::info( - cat, - "Failed to retrieve swarm due to error: {} ({}).", - response.value_or("Unknown error"), - request_id); - return cb({}); - } + // If there is only a single swarm then return it + if (all_swarms.size() == 1) + return cb(all_swarms.front().first, all_swarms.front().second); - std::vector swarm; + // Generate a swarm_id for the pubkey + const swarm_id_t swarm_id = detail::pubkey_to_swarm_space(swarm_pubkey); - try { - nlohmann::json response_json = nlohmann::json::parse(*response); + // Find the right boundary, i.e. first swarm with swarm_id >= res + auto right_it = std::lower_bound( + all_swarms.begin(), all_swarms.end(), swarm_id, [](const auto& s, uint64_t v) { + return s.first < v; + }); - if (!response_json.contains("snodes") || - !response_json["snodes"].is_array()) - throw std::runtime_error{"JSON missing swarm field."}; + if (right_it == all_swarms.end()) + // res is > the top swarm_id, meaning it is big and in the wrapping space between last + // and first elements. + right_it = all_swarms.begin(); - for (auto& snode : response_json["snodes"]) - swarm.emplace_back(node_from_json(snode)); - } catch (const std::exception& e) { - log::info( - cat, - "Failed to parse swarm due to error: {} ({}).", - e.what(), - request_id); - return cb({}); - } + // Our "left" is the one just before that (with wraparound, if right is the first swarm) + auto left_it = std::prev(right_it == all_swarms.begin() ? all_swarms.end() : right_it); - // Update the cache - log::info(cat, "Retrieved swarm for {} ({}).", hex_key, request_id); - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[hex_key] = swarm; - need_swarm_write = true; - need_write = true; - } - update_disk_cache_throttled(); + uint64_t dright = right_it->first - swarm_id; + uint64_t dleft = swarm_id - left_it->first; + auto swarm = &*(dright < dleft ? right_it : left_it); - cb(swarm); - }); + // Update the cache with the result + log::info( + cat, + "Found swarm with {} nodes for {}, adding to cache.", + swarm->second.size(), + swarm_pubkey.hex()); + swarm_cache[swarm_pubkey.hex()] = *swarm; + cb(swarm->first, swarm->second); }); } -void Network::set_swarm( - session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { - net.call([this, hex_key = swarm_pubkey.hex(), swarm]() mutable { - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[hex_key] = swarm; - need_swarm_write = true; - need_write = true; - } - update_disk_cache_throttled(); - }); -} +// MARK: Node Retrieval void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { @@ -1806,18 +1583,22 @@ void Network::get_random_nodes( log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); net.call([this, request_id, count, cb = std::move(callback)]() mutable { - // If we don't have sufficient nodes in the snode cache then add this to - if (snode_cache.size() < count) { + // If we don't have sufficient unused nodes then regenerate it + if (unused_nodes.size() < count) + unused_nodes = get_unused_nodes(); + + // If we still don't have sufficient nodes then we need to refresh the snode cache + if (unused_nodes.size() < count) { after_snode_cache_refresh.emplace_back( [this, count, cb = std::move(cb)]() { get_random_nodes(count, cb); }); return net.call_soon([this]() { refresh_snode_cache(); }); } // Otherwise callback with the requested random number of nodes - CSRNG rng; - auto random_cache = snode_cache; - std::shuffle(random_cache.begin(), random_cache.end(), rng); - cb(std::vector(random_cache.begin(), random_cache.begin() + count)); + auto random_nodes = + std::vector(unused_nodes.begin(), unused_nodes.begin() + count); + unused_nodes.erase(unused_nodes.begin(), unused_nodes.begin() + count); + cb(random_nodes); }); } @@ -2416,79 +2197,118 @@ void Network::handle_errors( if (!handle_response || !info.swarm_pubkey || !target) throw std::invalid_argument{"Unable to handle redirect."}; - // If this was the first 421 then we want to retry using another node in the - // swarm to get confirmation that we should switch to a different swarm - if (!info.retry_reason || - info.retry_reason != request_info::RetryReason::redirect) { - auto cached_swarm = swarm_cache[info.swarm_pubkey->hex()]; + switch (info.retry_reason.value_or(request_info::RetryReason::none)) { + // If this was the first 421 then we want to retry using another node in the + // swarm to get confirmation that we should switch to a different swarm + case request_info::RetryReason::none: + case request_info::RetryReason::decryption_failure: { + auto cached_swarm = swarm_cache[info.swarm_pubkey->hex()]; + + if (cached_swarm.second.empty()) + throw std::invalid_argument{ + "Unable to handle redirect due to lack of swarm."}; + + CSRNG rng; + std::vector swarm_copy; + std::copy_if( + cached_swarm.second.begin(), + cached_swarm.second.end(), + std::back_inserter(swarm_copy), + [&target = *target](const auto& node) { return node != target; }); + std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); + + if (swarm_copy.empty()) + throw std::invalid_argument{"No other nodes in the swarm."}; - if (cached_swarm.empty()) - throw std::invalid_argument{ - "Unable to handle redirect due to lack of swarm."}; - - CSRNG rng; - std::vector swarm_copy = cached_swarm; - std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); - - std::optional random_node; - - for (auto& node : swarm_copy) { - if (node == *target) - continue; - - random_node = node; - break; + log::info( + cat, + "Received 421 error in request {} on {} path, retrying once before " + "updating swarm.", + info.request_id, + path_name); + auto updated_info = info; + updated_info.destination = swarm_copy.front(); + updated_info.retry_reason = request_info::RetryReason::redirect; + return net.call_soon( + [this, updated_info, cb = std::move(*handle_response)]() { + _send_onion_request(updated_info, std::move(cb)); + }); } - if (!random_node) - throw std::invalid_argument{"No other nodes in the swarm."}; - - log::info( - cat, - "Received 421 error in request {} on {} path, retrying once before " - "updating swarm.", - info.request_id, - path_name); - auto updated_info = info; - updated_info.destination = *random_node; - updated_info.retry_reason = request_info::RetryReason::redirect; - return net.call_soon([this, updated_info, cb = std::move(*handle_response)]() { - _send_onion_request(updated_info, std::move(cb)); - }); - } - - if (!response) - throw std::invalid_argument{"No response data."}; - - auto response_json = nlohmann::json::parse(*response); - auto snodes = response_json["snodes"]; - - if (!snodes.is_array()) - throw std::invalid_argument{"Invalid JSON response."}; - - std::vector swarm; - - for (auto snode : snodes) - swarm.emplace_back(node_from_json(snode)); + // If we got a second 421 then it's likely that our cached swarm is out of date + // so we need to refresh our snode cache, regenerate our swarm and try one more + // time + case request_info::RetryReason::redirect: + log::info( + cat, + "Received second 421 error in request {} on {} path, refreshing " + "snode cache before trying one final time.", + info.request_id, + path_name); + after_snode_cache_refresh.emplace_back([this, + swarm_pubkey = info.swarm_pubkey, + info, + status_code, + response, + cb = std::move( + *handle_response)]() { + get_swarm( + *swarm_pubkey, + [this, info, status_code, response, cb = std::move(cb)]( + swarm_id_t, std::vector swarm) { + auto target = + detail::node_for_destination(info.destination); + + CSRNG rng; + std::vector swarm_copy; + std::copy_if( + swarm.begin(), + swarm.end(), + std::back_inserter(swarm_copy), + [&target = *target](const auto& node) { + return node != target; + }); + std::shuffle(swarm_copy.begin(), swarm_copy.end(), rng); + + // If there are no nodes in the swarm then don't bother + // trying again + if (swarm_copy.empty()) { + log::info( + cat, + "Second 421 retry for request {} resulted in " + "another 421 and had no other nodes in the " + "swarm.", + info.request_id); + return cb(false, false, status_code, response); + } + + auto updated_info = info; + updated_info.retry_reason = + request_info::RetryReason::redirect_swarm_refresh; + updated_info.destination = swarm_copy.front(); + net.call_soon([this, updated_info, cb = std::move(cb)]() { + _send_onion_request(updated_info, std::move(cb)); + }); + }); + }); + return net.call_soon([this, request_id = info.request_id]() { + refresh_snode_cache(request_id); + }); - if (swarm.empty()) - throw std::invalid_argument{"No snodes in the response."}; + // If we got a 421 after refreshing the swarm then there is some bigger issue + // (ie. our local swarm generation logic differs from the server or we are + // getting invalid swarm ids back when updating our cache) so the best we can + // do is handle this like any other error + case request_info::RetryReason::redirect_swarm_refresh: + log::info( + cat, + "Received another 421 for request {} after refreshing the snode " + "cache, failing request.", + info.request_id); + break; - log::info( - cat, - "Retry for request {} resulted in another 421 on {} path, updating swarm.", - info.request_id, - path_name); - - // Update the cache - { - std::lock_guard lock{snode_cache_mutex}; - swarm_cache[info.swarm_pubkey->hex()] = swarm; - need_swarm_write = true; - need_write = true; + default: break; // Unhandled case should just behave like any other error } - update_disk_cache_throttled(); - return (*handle_response)(false, false, status_code, response); } catch (...) { } @@ -2553,9 +2373,7 @@ void Network::handle_errors( // Update the failure counts and paths auto updated_path = *path; - auto updated_failure_counts = snode_failure_counts; bool found_invalid_node = false; - std::vector nodes_to_drop; if (response) { std::optional ed25519PublicKey; @@ -2578,14 +2396,12 @@ void Network::handle_errors( updated_path.nodes.end(), [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); - // If we found an invalid node then store it to increment the failure count if (snode_it != updated_path.nodes.end()) { found_invalid_node = true; // If we get an explicit node failure then we should just immediately drop it and // try to repair the existing path by replacing the bad node with another one - updated_failure_counts.erase(snode_it->to_string()); - nodes_to_drop.emplace_back(*snode_it); + snode_failure_counts[snode_it->to_string()] = snode_failure_threshold; try { // If the node that's gone bad is the guard node then we just have to @@ -2593,32 +2409,17 @@ void Network::handle_errors( if (snode_it == updated_path.nodes.begin()) throw std::runtime_error{"Cannot recover if guard node is bad"}; - // Try to find an unused node to patch the path - std::vector unused_snodes; - std::vector existing_path_node_ips = all_path_ips(); - - std::copy_if( - snode_cache.begin(), - snode_cache.end(), - std::back_inserter(unused_snodes), - [&existing_path_node_ips](const auto& node) { - return std::find( - existing_path_node_ips.begin(), - existing_path_node_ips.end(), - node.to_ipv4()) == existing_path_node_ips.end(); - }); - - if (unused_snodes.empty()) + if (unused_nodes.empty()) throw std::runtime_error{"No remaining nodes"}; - CSRNG rng; - std::shuffle(unused_snodes.begin(), unused_snodes.end(), rng); + auto target_node = unused_nodes.back(); + unused_nodes.pop_back(); std::replace( updated_path.nodes.begin(), updated_path.nodes.end(), *snode_it, - unused_snodes.front()); + target_node); log::info( cat, "Found bad node ({}) in {} path, replacing node.", @@ -2646,54 +2447,19 @@ void Network::handle_errors( // If the path has failed too many times we want to drop the guard snode (marking it as // invalid) and increment the failure count of each node in the path) if (updated_path.failure_count >= path_failure_threshold) { - for (auto& it : updated_path.nodes) { - auto& failure_count = - updated_failure_counts.try_emplace(it.to_string(), 0).first->second; - failure_count += 1; - - if (failure_count >= snode_failure_threshold) - nodes_to_drop.emplace_back(it); - } + for (auto& it : updated_path.nodes) + ++snode_failure_counts[it.to_string()]; - // Set the failure count of the guard node to match the threshold so we drop it - updated_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; - nodes_to_drop.emplace_back(updated_path.nodes[0]); - } else if (updated_path.nodes.size() < path_size) { - // If the path doesn't have enough nodes then it's likely that this failure was + // Set the failure count of the guard node to match the threshold so we don't use it + // again until we refresh the cache + snode_failure_counts[updated_path.nodes[0].to_string()] = snode_failure_threshold; + } else if (updated_path.nodes.size() < path_size) // triggered when trying to establish a new path and, as such, we should increase // the failure count of the guard node since it is probably invalid - auto& failure_count = - updated_failure_counts.try_emplace(updated_path.nodes[0].to_string(), 0) - .first->second; - failure_count += 1; - - if (failure_count >= snode_failure_threshold) - nodes_to_drop.emplace_back(updated_path.nodes[0]); - } - } - - // Make sure to remove any nodes we want to drop from the swarm cache as well - auto updated_swarm_cache = swarm_cache; - bool requires_swarm_cache_update = false; - - if (!nodes_to_drop.empty()) { - for (auto& [key, nodes] : updated_swarm_cache) { - for (const auto& drop_node : nodes_to_drop) { - auto it = std::remove(nodes.begin(), nodes.end(), drop_node); - if (it != nodes.end()) { - nodes.erase(it, nodes.end()); - requires_swarm_cache_update = true; - } - } - } + ++snode_failure_counts[updated_path.nodes[0].to_string()]; } - // No need to track the failure counts of nodes which have been dropped, or haven't failed - std::erase_if(updated_failure_counts, [](const auto& item) { - return item.second == 0 || item.second >= snode_failure_threshold; - }); - - // Drop the path if invalid (and currnetly an active path) + // Drop the path if invalid (and currently an active path) if (is_active_path) { if (updated_path.failure_count >= path_failure_threshold) drop_path_when_empty(info.request_id, info.path_type, *path_it); @@ -2705,28 +2471,6 @@ void Network::handle_errors( updated_path); } - // Update the snode cache - { - std::lock_guard lock{snode_cache_mutex}; - - // Update the snode failure counts - snode_failure_counts = updated_failure_counts; - need_failure_counts_write = true; - need_swarm_write = requires_swarm_cache_update; - - if (requires_swarm_cache_update) - swarm_cache = updated_swarm_cache; - - for (const auto& node : nodes_to_drop) { - snode_cache.erase( - std::remove(snode_cache.begin(), snode_cache.end(), node), snode_cache.end()); - need_pool_write = true; - } - - need_write = true; - } - update_disk_cache_throttled(); - if (handle_response) (*handle_response)(false, timeout, status_code, response); } @@ -2893,7 +2637,7 @@ LIBSESSION_C_API void network_get_swarm( assert(swarm_pubkey_hex && callback); unbox(network).get_swarm( x25519_pubkey::from_hex({swarm_pubkey_hex, 64}), - [cb = std::move(callback), ctx](std::vector nodes) { + [cb = std::move(callback), ctx](swarm_id_t, std::vector nodes) { auto c_nodes = session::network::convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); @@ -2938,7 +2682,8 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( unbox(network).send_onion_request( service_node{ oxenc::from_hex({node.ed25519_pubkey_hex, 64}), - {0}, // For a destination node we don't care about the version + {0}, + INVALID_SWARM_ID, "{}"_format(fmt::join(ip, ".")), node.quic_port}, body, diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 270e6248..487b9ab1 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -297,6 +298,7 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( unbox(builder).set_destination(session::network::service_node( oxenc::from_hex({ed25519_pubkey, 64}), {0}, + session::network::INVALID_SWARM_ID, "{}"_format(fmt::join(target_ip, ".")), quic_port)); } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 60f41582..7742917b 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -22,6 +22,17 @@ struct Result { std::optional response; }; +service_node test_node(const ustring ed_pk, const uint16_t index, const bool unique_ip = true) { + return service_node{ed_pk, {2, 8, 0}, INVALID_SWARM_ID, (unique_ip ? fmt::format("0.0.0.{}", index) : "1.1.1.1"), index}; +} + +std::optional node_for_destination(network_destination destination) { + if (auto* dest = std::get_if(&destination)) + return *dest; + + return std::nullopt; +} + } // namespace namespace session::network { @@ -31,6 +42,7 @@ class TestNetwork : public Network { std::vector calls_to_ignore; std::chrono::milliseconds retry_delay_value = 0ms; std::optional> find_valid_path_response; + std::optional last_request_info; TestNetwork( std::optional cache_path, @@ -60,7 +72,7 @@ class TestNetwork : public Network { unused_connections = unused_connections_; } - void set_in_progress_connections(std::unordered_set in_progress_connections_) { + void set_in_progress_connections(std::unordered_map in_progress_connections_) { in_progress_connections = in_progress_connections_; } @@ -74,14 +86,28 @@ class TestNetwork : public Network { std::vector get_paths(PathType path_type) { return paths[path_type]; } - void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, std::vector swarm) { - Network::set_swarm(swarm_pubkey, swarm); + void set_all_swarms(std::vector>> all_swarms_) { + all_swarms = all_swarms_; } - std::vector get_swarm_value(session::onionreq::x25519_pubkey swarm_pubkey) { + void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, swarm_id_t swarm_id, std::vector swarm) { + swarm_cache[swarm_pubkey.hex()] = {swarm_id, swarm}; + } + + std::pair> get_cached_swarm(session::onionreq::x25519_pubkey swarm_pubkey) { return swarm_cache[swarm_pubkey.hex()]; } + swarm_id_t get_swarm_id(std::string swarm_pubkey_hex) { + if (swarm_pubkey_hex.size() == 66) + swarm_pubkey_hex = swarm_pubkey_hex.substr(2); + + auto pk = x25519_pubkey::from_hex(swarm_pubkey_hex); + std::promise prom; + get_swarm(pk, [&prom](swarm_id_t result, std::vector){ prom.set_value(result); }); + return prom.get_future().get(); + } + void set_failure_count(service_node node, uint8_t failure_count) { snode_failure_counts[node.to_string()] = failure_count; } @@ -115,14 +141,11 @@ class TestNetwork : public Network { int get_path_build_failures() { return path_build_failures; } - void set_unused_connection_and_path_build_nodes( - std::optional> unused_connection_and_path_build_nodes_) { - unused_connection_and_path_build_nodes = unused_connection_and_path_build_nodes_; - } + void set_unused_nodes(std::vector unused_nodes_) { unused_nodes = unused_nodes_; } - std::optional> get_unused_connection_and_path_build_nodes() { - return unused_connection_and_path_build_nodes; - } + std::vector get_unused_nodes() { return Network::get_unused_nodes(); } + + std::vector get_unused_nodes_value() { return unused_nodes; } void add_pending_request(PathType path_type, request_info info) { request_queue[path_type].emplace_back( @@ -187,6 +210,7 @@ class TestNetwork : public Network { void _send_onion_request( request_info info, network_response_callback_t handle_response) override { const auto func_name = "_send_onion_request"; + last_request_info = info; if (check_should_ignore_and_log_call(func_name)) return; @@ -277,11 +301,11 @@ TEST_CASE("Network error handling", "[network]") { "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto x_pk_hex = "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"; - auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; - auto target2 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; - auto target3 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.2", uint16_t{2}}; - auto target4 = service_node{ed_pk2, {2, 8, 0}, "0.0.0.3", uint16_t{3}}; - auto path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; + auto target = test_node(ed_pk, 0); + auto target2 = test_node(ed_pk2, 1); + auto target3 = test_node(ed_pk2, 2); + auto target4 = test_node(ed_pk2, 3); + auto path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", target, @@ -294,19 +318,17 @@ TEST_CASE("Network error handling", "[network]") { std::nullopt, true}; Result result; - auto network = TestNetwork(std::nullopt, true, true, false); - network.set_suspended(true); // Make no requests in this test - network.ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + std::optional network; // Check the handling of the codes which make no changes auto codes_with_no_changes = {400, 404, 406, 425}; for (auto code : codes_with_no_changes) { - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request, {target, nullptr, nullptr, nullptr}, false, @@ -324,18 +346,18 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == code); CHECK_FALSE(result.response.has_value()); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 0); + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 0); } // Check general error handling (first failure) - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request, {target, nullptr, nullptr, nullptr}, false, @@ -352,18 +374,18 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 1); - - // Check general error handling with no response (too many path failures) - path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 1); + + // // Check general error handling with no response (too many path failures) + path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request, {target, nullptr, nullptr, nullptr}, false, @@ -381,20 +403,21 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK_FALSE(result.response.has_value()); - CHECK(network.get_failure_count(target) == 0); // Guard node dropped - CHECK(network.get_failure_count(target2) == 1); // Other nodes incremented - CHECK(network.get_failure_count(target3) == 1); // Other nodes incremented - CHECK(network.get_failure_count(PathType::standard, path) == 0); // Path dropped and reset + CHECK(network->get_failure_count(target) == 3); // Guard node dropped + CHECK(network->get_failure_count(target2) == 1); // Other nodes incremented + CHECK(network->get_failure_count(target3) == 1); // Other nodes incremented + CHECK(network->get_failure_count(PathType::standard, path) == 0); // Path dropped and reset - // Check general error handling with a path and specific node failure - path = onion_path{{{target}, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; + // // Check general error handling with a path and specific node failure + path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); - network.set_snode_cache({target, target2, target3, target4}); - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 1); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_snode_cache({target, target2, target3, target4}); + network->set_unused_nodes({target4}); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request, {target, nullptr, nullptr, nullptr}, false, @@ -412,19 +435,19 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); CHECK(result.response == response); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); // Node will have been dropped - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_paths(PathType::standard).front().nodes[1] != target2); - CHECK(network.get_failure_count(PathType::standard, path) == + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 3); // Node will have been dropped + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_paths(PathType::standard).front().nodes[1] != target2); + CHECK(network->get_failure_count(PathType::standard, path) == 1); // Incremented because conn_info is invalid // Check a 421 with no swarm data throws (no good way to handle this case) - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request, {target, nullptr, nullptr, nullptr}, false, @@ -440,12 +463,12 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); CHECK(result.status_code == 421); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 1); + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 1); - // Check the retry request of a 421 with no response data is handled like any other error + // Check a non redirect 421 triggers a retry using a different node auto mock_request2 = request_info{ "BBBB", target, @@ -455,106 +478,27 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, - request_info::RetryReason::redirect, + std::nullopt, true}; - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_swarm(x25519_pubkey::from_hex(x_pk_hex), 1, {target, target2, target3}); + network->set_paths(PathType::standard, {path}); + network->reset_calls(); + network->handle_errors( mock_request2, {target, nullptr, nullptr, nullptr}, false, 421, std::nullopt, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - result = {success, timeout, status_code, response}; - }); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 1); - - // Check the retry request of a 421 with non-swarm response data is handled like any other error - network.set_paths(PathType::standard, {path}); - network.handle_errors( - mock_request2, - {target, nullptr, nullptr, nullptr}, - false, - 421, - "Test", - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - result = {success, timeout, status_code, response}; - }); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 1); - - // Check the retry request of a 421 instructs to replace the swarm - auto snodes = nlohmann::json::array(); - snodes.push_back( - {{"ip", "1.1.1.1"}, - {"port_omq", 1}, - {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); - snodes.push_back( - {{"ip", "2.2.2.2"}, - {"port_omq", 2}, - {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); - snodes.push_back( - {{"ip", "3.3.3.3"}, - {"port_omq", 3}, - {"pubkey_ed25519", ed25519_pubkey::from_bytes(ed_pk).hex()}}); - nlohmann::json swarm_json{{"snodes", snodes}}; - response = swarm_json.dump(); - network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target, target2, target3}); - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( - mock_request2, - {target, nullptr, nullptr, nullptr}, - false, - 421, - response, - [&result]( - bool success, - bool timeout, - int16_t status_code, - std::optional response) { - result = {success, timeout, status_code, response}; - }); + [](bool, bool, int16_t, std::optional) {}); + CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); + REQUIRE(network->last_request_info.has_value()); + CHECK(node_for_destination(network->last_request_info->destination) != node_for_destination(mock_request2.destination)); - CHECK_FALSE(result.success); - CHECK_FALSE(result.timeout); - CHECK(result.status_code == 421); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 0); - REQUIRE(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)).size() == 3); - CHECK(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)).front().to_string() == - "1.1.1.1:1"); - CHECK(oxenc::to_hex(network.get_swarm_value(x25519_pubkey::from_hex(x_pk_hex)) - .front() - .view_remote_key()) == oxenc::to_hex(ed_pk)); - - // Check a non redirect 421 with swam data triggers a retry + // Check that when a retry request of a 421 receives it's own 421 that it tries + // to update the snode cache auto mock_request3 = request_info{ "BBBB", target, @@ -564,22 +508,57 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, - std::nullopt, + request_info::RetryReason::redirect, true}; - network.set_swarm(x25519_pubkey::from_hex(x_pk_hex), {target, target2, target3}); - network.set_paths(PathType::standard, {path}); - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.reset_calls(); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled", "refresh_snode_cache"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( mock_request3, {target, nullptr, nullptr, nullptr}, false, 421, - response, + std::nullopt, [](bool, bool, int16_t, std::optional) {}); - CHECK(EVENTUALLY(10ms, network.called("_send_onion_request"))); + CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); + + // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it is handled like any other error + auto mock_request4 = request_info{ + "BBBB", + target, + "test", + std::nullopt, + std::nullopt, + x25519_pubkey::from_hex(x_pk_hex), + PathType::standard, + 0ms, + request_info::RetryReason::redirect_swarm_refresh, + true}; + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->set_paths(PathType::standard, {path}); + network->handle_errors( + mock_request4, + {target, nullptr, nullptr, nullptr}, + false, + 421, + std::nullopt, + [&result]( + bool success, + bool timeout, + int16_t status_code, + std::optional response) { + result = {success, timeout, status_code, response}; + }); + CHECK_FALSE(result.success); + CHECK_FALSE(result.timeout); + CHECK(result.status_code == 421); + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 1); // Check a timeout with a sever destination doesn't impact the failure counts auto server = ServerDestination{ @@ -591,7 +570,7 @@ TEST_CASE("Network error handling", "[network]") { 443, std::nullopt, "GET"}; - auto mock_request4 = request_info{ + auto mock_request5 = request_info{ "CCCC", server, "test", @@ -602,11 +581,11 @@ TEST_CASE("Network error handling", "[network]") { 0ms, std::nullopt, false}; - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( - mock_request4, + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->handle_errors( + mock_request5, {target, nullptr, nullptr, nullptr}, true, std::nullopt, @@ -621,17 +600,17 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.success); CHECK(result.timeout); CHECK(result.status_code == -1); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 0); + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 0); // Check a server response starting with '500 Internal Server Error' is reported as a `500` // error and doesn't affect the failure count - network.set_failure_count(target, 0); - network.set_failure_count(target2, 0); - network.set_failure_count(target3, 0); - network.handle_errors( + network.emplace(std::nullopt, true, true, false); + network->set_suspended(true); // Make no requests in this test + network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); + network->handle_errors( mock_request4, {target, nullptr, nullptr, nullptr}, false, @@ -647,10 +626,89 @@ TEST_CASE("Network error handling", "[network]") { CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); CHECK(result.status_code == 500); - CHECK(network.get_failure_count(target) == 0); - CHECK(network.get_failure_count(target2) == 0); - CHECK(network.get_failure_count(target3) == 0); - CHECK(network.get_failure_count(PathType::standard, path) == 0); + CHECK(network->get_failure_count(target) == 0); + CHECK(network->get_failure_count(target2) == 0); + CHECK(network->get_failure_count(target3) == 0); + CHECK(network->get_failure_count(PathType::standard, path) == 0); +} + +TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { + const auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + std::optional network; + std::vector snode_cache; + std::vector unused_nodes; + for (uint16_t i = 0; i < 12; ++i) + snode_cache.emplace_back(test_node(ed_pk, i)); + auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr, nullptr}; + auto path = onion_path{invalid_info, {snode_cache[0], snode_cache[1], snode_cache[2]}, 0}; + + // Should shuffle the result + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + CHECK(network->get_unused_nodes() != network->get_unused_nodes()); + + // Should contain the entire snode cache initially + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == snode_cache); + + // Should exclude nodes used in paths + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + network->set_paths(PathType::standard, {path}); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == std::vector{snode_cache.begin() + 3, snode_cache.end()}); + + // Should exclude nodes in unused connections + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + network->set_unused_connections({invalid_info}); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); + + // Should exclude nodes in in-progress connections + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + network->set_in_progress_connections({{"Test", snode_cache.front()}}); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); + + // Should exclude nodes destinations in pending requests + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + network->add_pending_request( + PathType::standard, + request_info::make( + snode_cache.front(), 1s, std::nullopt, std::nullopt, PathType::standard)); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); + + // Should exclude nodes which have passed the failure threshold + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(snode_cache); + network->set_failure_count(snode_cache.front(), 10); + unused_nodes = network->get_unused_nodes(); + std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); + + // Should exclude nodes which have the same IP if one was excluded + std::vector same_ip_snode_cache; + auto unique_node = service_node{ed_pk, {2, 8, 0}, INVALID_SWARM_ID, "0.0.0.20", uint16_t{20}}; + for (uint16_t i = 0; i < 11; ++i) + same_ip_snode_cache.emplace_back(test_node(ed_pk, i, false)); + same_ip_snode_cache.emplace_back(unique_node); + network.emplace(std::nullopt, true, false, false); + network->set_snode_cache(same_ip_snode_cache); + network->set_failure_count(same_ip_snode_cache.front(), 10); + unused_nodes = network->get_unused_nodes(); + REQUIRE(unused_nodes.size() == 1); + CHECK(unused_nodes.front() == unique_node); } TEST_CASE("Network Path Building", "[network][build_path]") { @@ -658,7 +716,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { std::optional network; std::vector snode_cache; for (uint16_t i = 0; i < 12; ++i) - snode_cache.emplace_back(service_node{ed_pk, {2, 8, 0}, fmt::format("0.0.0.{}", i), i}); + snode_cache.emplace_back(test_node(ed_pk, i)); auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr, nullptr}; // Nothing should happen if the network is suspended @@ -679,22 +737,19 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network.emplace(std::nullopt, true, false, false); network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); - network->set_in_progress_connections({"TestInProgress"}); + network->set_in_progress_connections({{"TestInProgress", snode_cache.front()}}); network->build_path(PathType::standard, "Test1"); - REQUIRE(network->get_unused_connection_and_path_build_nodes().has_value()); - CHECK(network->get_unused_connection_and_path_build_nodes()->size() == snode_cache.size() - 3); + CHECK(network->get_unused_nodes_value().size() == snode_cache.size() - 3); CHECK(network->get_path_build_queue().empty()); // It should exclude nodes that are already in existing paths network.emplace(std::nullopt, true, false, false); network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); - network->set_in_progress_connections({"TestInProgress"}); + network->set_in_progress_connections({{"TestInProgress", snode_cache.front()}}); network->add_path(PathType::standard, {snode_cache.begin() + 1, snode_cache.begin() + 1 + 3}); network->build_path(PathType::standard, "Test1"); - REQUIRE(network->get_unused_connection_and_path_build_nodes().has_value()); - CHECK(network->get_unused_connection_and_path_build_nodes()->size() == - (snode_cache.size() - 3 - 3)); + CHECK(network->get_unused_nodes_value().size() == (snode_cache.size() - 3 - 3)); CHECK(network->get_path_build_queue().empty()); // If there aren't enough unused nodes it resets the failure count, re-queues the path build and @@ -715,7 +770,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network.emplace(std::nullopt, true, false, false); network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); - network->set_unused_connection_and_path_build_nodes(std::vector{ + network->set_unused_nodes(std::vector{ snode_cache[0], snode_cache[0], snode_cache[0], snode_cache[0]}); network->build_path(PathType::standard, "Test1"); network->ignore_calls_to("build_path"); // Ignore the 2nd loop @@ -760,10 +815,11 @@ TEST_CASE("Network Path Building", "[network][build_path]") { TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.1", uint16_t{1}}; + auto target = test_node(ed_pk, 1); auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, {2, 8, 0}, + INVALID_SWARM_ID, "144.76.164.202", uint16_t{35400}}; auto network = TestNetwork(std::nullopt, true, false, false); @@ -809,7 +865,8 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto target = service_node{ed_pk, {2, 8, 0}, "0.0.0.0", uint16_t{0}}; + auto target = test_node(ed_pk, 0); + ; std::optional network; auto invalid_path = onion_path{connection_info{target, nullptr, nullptr, nullptr}, {target}, uint8_t{0}}; @@ -906,6 +963,7 @@ TEST_CASE("Network requests", "[network][establish_connection]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, {2, 8, 0}, + INVALID_SWARM_ID, "144.76.164.202", uint16_t{35400}}; auto network = TestNetwork(std::nullopt, true, true, false); @@ -930,6 +988,7 @@ TEST_CASE("Network requests", "[network][send_request]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, {2, 8, 0}, + INVALID_SWARM_ID, "144.76.164.202", uint16_t{35400}}; auto network = TestNetwork(std::nullopt, true, true, false); @@ -982,6 +1041,7 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, {2, 8, 0}, + INVALID_SWARM_ID, "144.76.164.202", uint16_t{35400}}; auto network = Network(std::nullopt, true, true, false); @@ -1064,3 +1124,112 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { CHECK(response.contains("version")); network_free(network); } + +TEST_CASE("Network swarm", "[network][detail][pubkey_to_swarm_space]") { + x25519_pubkey pk; + + pk = x25519_pubkey::from_hex("3506f4a71324b7dd114eddbf4e311f39dde243e1f2cb97c40db1961f70ebaae8"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 17589930838143112648ULL); + pk = x25519_pubkey::from_hex("cf27da303a50ac8c4b2d43d27259505c9bcd73fc21cf2a57902c3d050730b604"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 10370619079776428163ULL); + pk = x25519_pubkey::from_hex("d3511706b8b34f6e8411bf07bd22ba6b2435ca56846fbccf6eb1e166a6cd15cc"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 2144983569669512198ULL); + pk = x25519_pubkey::from_hex("0f06693428fca9102a451e3f28d9cc743d8ea60a89ab6aa69eb119470c11cbd3"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 9690840703409570833ULL); + pk = x25519_pubkey::from_hex("ffba630924aa1224bb930dde21c0d11bf004608f2812217f8ac812d6c7e3ad48"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 4532060000165252872ULL); + pk = x25519_pubkey::from_hex("eeeeeeeeeeeeeeee777777777777777711111111111111118888888888888888"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0); + pk = x25519_pubkey::from_hex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0); + pk = x25519_pubkey::from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 1); + pk = x25519_pubkey::from_hex("ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffff"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 1ULL << 63); + pk = x25519_pubkey::from_hex("000000000000000000000000000000000000000000000000ffffffffffffffff"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == (uint64_t)-1); + pk = x25519_pubkey::from_hex("0000000000000000000000000000000000000000000000000123456789abcdef"); + CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0x0123456789abcdefULL); +} + +TEST_CASE("Network swarm", "[network][get_swarm]") { + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + std::vector>> swarms = {{100, {}}, {200, {}}, {300, {}}, {399, {}}, {498, {}}, {596, {}}, {694, {}}}; + auto network = TestNetwork(std::nullopt, true, true, false); + network.set_snode_cache({test_node(ed_pk, 0)}); + network.set_all_swarms(swarms); + + // Exact matches: + // 0x64 = 100, 0xc8 = 200, 0x1f2 = 498 + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000064") == 100); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000c8") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001f2") == 498); + + // Nearest + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000000") == 100); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000001") == 100); + + // Nearest, with wraparound + // 0x8000... is closest to the top value + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000008000000000000000") == 694); + + // 0xa000... is closest (via wraparound) to the smallest + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000a000000000000000") == 100); + + // This is the invalid swarm id for swarms, but should still work for a client + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000ffffffffffffffff") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffffe") == 100); + + // Midpoint tests; we prefer the lower value when exactly in the middle between two swarms. + // 0x96 = 150 + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000095") == 100); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000096") == 100); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000097") == 200); + + // 0xfa = 250 + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000f9") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000fa") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000fb") == 300); + + // 0x15d = 349 + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015d") == 300); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015e") == 399); + + // 0x1c0 = 448 + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c0") == 399); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c1") == 498); + + // 0x223 = 547 + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000222") == 498); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000223") == 498); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000224") == 596); + + // 0x285 = 645 + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000285") == 596); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000286") == 694); + + // 0x800....d is the midpoint between 694 and 100 (the long way). We always round "down" (which + // in this case, means wrapping to the largest swarm). + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018c") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018d") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018e") == 100); + + // With a swarm at -20 the midpoint is now 40 (=0x28). When our value is the *low* value we + // prefer the *last* swarm in the case of a tie (while consistent with the general case of + // preferring the left edge, it means we're inconsistent with the other wraparound case, above. + // *sigh*). + swarms.push_back({(uint64_t)-20, {}}); + network.set_all_swarms(swarms); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000027") == swarms.back().first); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000028") == swarms.back().first); + CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000029") == swarms.front().first); + + // The code used to have a broken edge case if we have a swarm at zero and a client at max-u64 + // because of an overflow in how the distance is calculated (the first swarm will be calculated + // as max-u64 away, rather than 1 away), and so the id always maps to the highest swarm (even + // though 0xfff...fe maps to the lowest swarm; the first check here, then, would fail. + swarms.insert(swarms.begin(), {0, {}}); + network.set_all_swarms(swarms); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000ffffffffffffffff") == 0); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffffe") == 0); +} From c3aa3b99e05d7e7568cdd1dc2e0f1200e3ec01f1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 6 Sep 2024 16:08:23 +1000 Subject: [PATCH 394/572] Added a timeout mechanism which takes the path build time into account --- include/session/network.h | 55 +++++-- include/session/network.hpp | 104 +++++++++---- src/network.cpp | 299 ++++++++++++++++++++++++++---------- tests/test_network.cpp | 279 +++++++++++++++++++++++++-------- 4 files changed, 556 insertions(+), 181 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index bf29eaa9..c35fcc8f 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -201,7 +201,13 @@ typedef void (*network_onion_response_callback_t)( /// - `node` -- [in] address information about the service node the request should be sent to. /// - `body` -- [in] data to send to the specified node. /// - `body_size` -- [in] size of the `body`. -/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `request_timeout_ms` -- [in] timeout in milliseconds to use for the request. This won't take +/// the path build into account so if the path build takes forever then this request will never +/// timeout. +/// - `request_and_path_build_timeout_ms` -- [in] timeout in milliseconds to use for the request and +/// path build (if required). This value takes presedence over `request_timeout_ms` if provided, +/// the request itself will be given a timeout of this value subtracting however long it took to +/// build the path. A value of `0` will be ignored and `request_timeout_ms` will be used instead. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set to /// NULL if unused. @@ -211,7 +217,8 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( const unsigned char* body, size_t body_size, const char* swarm_pubkey_hex, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx); @@ -224,7 +231,13 @@ LIBSESSION_EXPORT void network_send_onion_request_to_snode_destination( /// - `server` -- [in] struct containing information about the server the request should be sent to. /// - `body` -- [in] data to send to the specified endpoint. /// - `body_size` -- [in] size of the `body`. -/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `request_timeout_ms` -- [in] timeout in milliseconds to use for the request. This won't take +/// the path build into account so if the path build takes forever then this request will never +/// timeout. +/// - `request_and_path_build_timeout_ms` -- [in] timeout in milliseconds to use for the request and +/// path build (if required). This value takes presedence over `request_timeout_ms` if provided, +/// the request itself will be given a timeout of this value subtracting however long it took to +/// build the path. A value of `0` will be ignored and `request_timeout_ms` will be used instead. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set /// to NULL if unused. @@ -233,7 +246,8 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( const network_server_destination server, const unsigned char* body, size_t body_size, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx); @@ -247,7 +261,13 @@ LIBSESSION_EXPORT void network_send_onion_request_to_server_destination( /// - `data` -- [in] data to upload to the file server. /// - `data_len` -- [in] size of the `data`. /// - `file_name` -- [in, optional] name of the file being uploaded. MUST be null terminated. -/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `request_timeout_ms` -- [in] timeout in milliseconds to use for the request. This won't take +/// the path build into account so if the path build takes forever then this request will never +/// timeout. +/// - `request_and_path_build_timeout_ms` -- [in] timeout in milliseconds to use for the request and +/// path build (if required). This value takes presedence over `request_timeout_ms` if provided, +/// the request itself will be given a timeout of this value subtracting however long it took to +/// build the path. A value of `0` will be ignored and `request_timeout_ms` will be used instead. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set /// to NULL if unused. @@ -257,7 +277,8 @@ LIBSESSION_EXPORT void network_upload_to_server( const unsigned char* data, size_t data_len, const char* file_name, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx); @@ -268,14 +289,21 @@ LIBSESSION_EXPORT void network_upload_to_server( /// Inputs: /// - `network` -- [in] Pointer to the network object. /// - `server` -- [in] struct containing information about file to be downloaded. -/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `request_timeout_ms` -- [in] timeout in milliseconds to use for the request. This won't take +/// the path build into account so if the path build takes forever then this request will never +/// timeout. +/// - `request_and_path_build_timeout_ms` -- [in] timeout in milliseconds to use for the request and +/// path build (if required). This value takes presedence over `request_timeout_ms` if provided, +/// the request itself will be given a timeout of this value subtracting however long it took to +/// build the path. A value of `0` will be ignored and `request_timeout_ms` will be used instead. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set /// to NULL if unused. LIBSESSION_EXPORT void network_download_from_server( network_object* network, const network_server_destination server, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx); @@ -287,7 +315,13 @@ LIBSESSION_EXPORT void network_download_from_server( /// - `network` -- [in] Pointer to the network object. /// - `platform` -- [in] the platform to retrieve the client version for. /// - `ed25519_secret` -- [in] the users ed25519 secret key (used for blinded auth - 64 bytes). -/// - `timeout_ms` -- [in] timeout in milliseconds to use for the request. +/// - `request_timeout_ms` -- [in] timeout in milliseconds to use for the request. This won't take +/// the path build into account so if the path build takes forever then this request will never +/// timeout. +/// - `request_and_path_build_timeout_ms` -- [in] timeout in milliseconds to use for the request and +/// path build (if required). This value takes presedence over `request_timeout_ms` if provided, +/// the request itself will be given a timeout of this value subtracting however long it took to +/// build the path. A value of `0` will be ignored and `request_timeout_ms` will be used instead. /// - `callback` -- [in] callback to be called with the result of the request. /// - `ctx` -- [in, optional] Pointer to an optional context to pass through to the callback. Set /// to NULL if unused. @@ -295,7 +329,8 @@ LIBSESSION_EXPORT void network_get_client_version( network_object* network, CLIENT_PLATFORM platform, const unsigned char* ed25519_secret, /* 64 bytes */ - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index 9f2c3e1a..c8195dfc 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -135,9 +135,10 @@ namespace detail { struct request_info { static request_info make( onionreq::network_destination _dest, - std::chrono::milliseconds _timeout, std::optional _original_body, std::optional _swarm_pk, + std::chrono::milliseconds _request_timeout, + std::optional _request_and_path_build_timeout = std::nullopt, PathType _type = PathType::standard, std::optional _req_id = std::nullopt, std::optional endpoint = "onion_req", @@ -157,7 +158,9 @@ struct request_info { std::optional original_body; std::optional swarm_pubkey; PathType path_type; - std::chrono::milliseconds timeout; + std::chrono::milliseconds request_timeout; + std::optional request_and_path_build_timeout; + std::chrono::system_clock::time_point creation_time = std::chrono::system_clock::now(); /// The reason we are retrying the request (if it's a retry). Generally only used for internal /// purposes (like receiving a `421`) in order to prevent subsequent retries. @@ -221,6 +224,7 @@ class Network { // Request state bool has_scheduled_resume_queues = false; + std::optional request_timeout_id; std::chrono::system_clock::time_point last_resume_queues_timestamp{}; std::unordered_map>> request_queue; @@ -291,35 +295,31 @@ class Network { void get_random_nodes( uint16_t count, std::function nodes)> callback); - /// API: network/send_request - /// - /// Send a request via the network. - /// - /// Inputs: - /// - `info` -- [in] wrapper around all of the information required to send a request. - /// - `conn` -- [in] connection information used to send the request. - /// - `handle_response` -- [in] callback to be called with the result of the request. - void send_request( - request_info info, connection_info conn, network_response_callback_t handle_response); - /// API: network/send_onion_request /// /// Sends a request via onion routing to the provided service node or server destination. /// /// Inputs: - /// - 'type' - [in] the type of paths to send the request across. /// - `destination` -- [in] service node or server destination information. /// - `body` -- [in] data to send to the specified destination. /// - `swarm_pubkey` -- [in, optional] pubkey for the swarm the request is associated with. /// Should be NULL if the request is not associated with a swarm. - /// - `timeout` -- [in] timeout in milliseconds to use for the request. /// - `handle_response` -- [in] callback to be called with the result of the request. + /// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take + /// the path build into account so if the path build takes forever then this request will never + /// timeout. + /// - `request_and_path_build_timeout` -- [in] timeout in milliseconds to use for the request + /// and path build (if required). This value takes presedence over `request_timeout` if + /// provided, the request itself will be given a timeout of this value subtracting however long + /// it took to build the path. + /// - 'type' - [in] the type of paths to send the request across. void send_onion_request( onionreq::network_destination destination, std::optional body, std::optional swarm_pubkey, - std::chrono::milliseconds timeout, network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout = std::nullopt, PathType type = PathType::standard); /// API: network/upload_file_to_server @@ -330,14 +330,21 @@ class Network { /// - 'data' - [in] the data to be uploaded to a server. /// - `server` -- [in] the server destination to upload the file to. /// - `file_name` -- [in, optional] optional name to use for the file. - /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take + /// the path build into account so if the path build takes forever then this request will never + /// timeout. + /// - `request_and_path_build_timeout` -- [in] timeout in milliseconds to use for the request + /// and path build (if required). This value takes presedence over `request_timeout` if + /// provided, the request itself will be given a timeout of this value subtracting however long + /// it took to build the path. /// - `handle_response` -- [in] callback to be called with the result of the request. void upload_file_to_server( ustring data, onionreq::ServerDestination server, std::optional file_name, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response); + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout = std::nullopt); /// API: network/download_file /// @@ -345,12 +352,19 @@ class Network { /// /// Inputs: /// - `server` -- [in] the server destination to download the file from. - /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take + /// the path build into account so if the path build takes forever then this request will never + /// timeout. + /// - `request_and_path_build_timeout` -- [in] timeout in milliseconds to use for the request + /// and path build (if required). This value takes presedence over `request_timeout` if + /// provided, the request itself will be given a timeout of this value subtracting however long + /// it took to build the path. /// - `handle_response` -- [in] callback to be called with the result of the request. void download_file( onionreq::ServerDestination server, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response); + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout = std::nullopt); /// API: network/download_file /// @@ -362,12 +376,20 @@ class Network { /// - `download_url` -- [in] the url to download the file from. /// - `x25519_pubkey` -- [in] the server destination to download the file from. /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take + /// the path build into account so if the path build takes forever then this request will never + /// timeout. + /// - `request_and_path_build_timeout` -- [in] timeout in milliseconds to use for the request + /// and path build (if required). This value takes presedence over `request_timeout` if + /// provided, the request itself will be given a timeout of this value subtracting however long + /// it took to build the path. /// - `handle_response` -- [in] callback to be called with the result of the request. void download_file( std::string_view download_url, onionreq::x25519_pubkey x25519_pubkey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response); + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout = std::nullopt); /// API: network/get_client_version /// @@ -376,13 +398,20 @@ class Network { /// Inputs: /// - `platform` -- [in] the platform to retrieve the client version for. /// - `seckey` -- [in] the users ed25519 secret key (to generated blinded auth). - /// - `timeout` -- [in] timeout in milliseconds to use for the request. + /// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take + /// the path build into account so if the path build takes forever then this request will never + /// timeout. + /// - `request_and_path_build_timeout` -- [in] timeout in milliseconds to use for the request + /// and path build (if required). This value takes presedence over `request_timeout` if + /// provided, the request itself will be given a timeout of this value subtracting however long + /// it took to build the path. /// - `handle_response` -- [in] callback to be called with the result of the request. void get_client_version( Platform platform, onionreq::ed25519_seckey seckey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response); + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout = std::nullopt); private: /// API: network/all_path_ips @@ -587,6 +616,27 @@ class Network { std::function nodes, std::optional error)> callback); + /// API: network/check_request_queue_timeouts + /// + /// Checks if any of the requests in the request queue have timed out (and fails them if so). + /// + /// Inputs: + /// - 'request_timeout_id' - [in] id for the timeout loop to prevent multiple loops from being + /// scheduled. + virtual void check_request_queue_timeouts( + std::optional request_timeout_id = std::nullopt); + + /// API: network/send_request + /// + /// Send a request via the network. + /// + /// Inputs: + /// - `info` -- [in] wrapper around all of the information required to send a request. + /// - `conn` -- [in] connection information used to send the request. + /// - `handle_response` -- [in] callback to be called with the result of the request. + void send_request( + request_info info, connection_info conn, network_response_callback_t handle_response); + /// API: network/_send_onion_request /// /// Internal function invoked by ::send_onion_request after request_info construction diff --git a/src/network.cpp b/src/network.cpp index e8ade608..6df6c742 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -52,8 +52,9 @@ namespace { constexpr int16_t error_network_suspended = -10001; constexpr int16_t error_building_onion_request = -10002; + constexpr int16_t error_path_build_timeout = -10003; - // The amount of time the snode cache can be used before it needs to be refreshed + // The amount of time the snode cache can be used before it needs to be refreshed/ constexpr auto snode_cache_expiration_duration = 2h; // The smallest size the snode cache can get to before we need to fetch more. @@ -71,6 +72,9 @@ namespace { // The number of times a snode can fail before it's replaced. constexpr uint16_t snode_failure_threshold = 3; + // The frequency to check if queued requests have timed out due to a pending path build + constexpr auto queued_request_path_build_timeout_frequency = 250ms; + const fs::path default_cache_path{u8"."}, file_testnet{u8"testnet"}, file_snode_pool{u8"snode_pool"}; const std::vector legacy_files{ @@ -405,13 +409,53 @@ namespace detail { info.request_id, path_type_name(info.path_type, single_path_mode)); } + + std::vector convert_service_nodes( + std::vector nodes) { + std::vector converted_nodes; + for (auto& node : nodes) { + auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); + auto ipv4 = node.to_ipv4(); + network_service_node converted_node; + converted_node.ip[0] = (ipv4.addr >> 24) & 0xFF; + converted_node.ip[1] = (ipv4.addr >> 16) & 0xFF; + converted_node.ip[2] = (ipv4.addr >> 8) & 0xFF; + converted_node.ip[3] = ipv4.addr & 0xFF; + strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); + converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination + converted_node.quic_port = node.port(); + converted_nodes.push_back(converted_node); + } + + return converted_nodes; + } + + ServerDestination convert_server_destination(const network_server_destination server) { + std::optional>> headers; + if (server.headers_size > 0) { + headers = std::vector>{}; + + for (size_t i = 0; i < server.headers_size; i++) + headers->emplace_back(server.headers[i], server.header_values[i]); + } + + return ServerDestination{ + server.protocol, + server.host, + server.endpoint, + x25519_pubkey::from_hex({server.x25519_pubkey, 64}), + server.port, + headers, + server.method}; + } } // namespace detail request_info request_info::make( onionreq::network_destination _dest, - std::chrono::milliseconds _timeout, std::optional _original_body, std::optional _swarm_pk, + std::chrono::milliseconds _request_timeout, + std::optional _request_and_path_build_timeout, PathType _type, std::optional _req_id, std::optional _ep, @@ -424,7 +468,8 @@ request_info request_info::make( std::move(_original_body), std::move(_swarm_pk), _type, - _timeout}; + _request_timeout, + _request_and_path_build_timeout}; } std::string onion_path::to_string() const { @@ -526,7 +571,11 @@ void Network::load_cache_from_disk() { all_swarms = detail::generate_swarms(loaded_cache); } - log::info(cat, "Loaded cache of {} snodes, {} swarms.", snode_cache.size(), all_swarms.size()); + log::info( + cat, + "Loaded cache of {} snodes, {} swarms.", + snode_cache.size(), + all_swarms.size()); } catch (const std::exception& e) { log::error(cat, "Failed to load snode cache, will rebuild ({}).", e.what()); @@ -1166,9 +1215,10 @@ void Network::refresh_snode_cache(std::optional existing_request_id }; auto info = request_info::make( target_node, - quic::DEFAULT_TIMEOUT, ustring{quic::to_usv(payload.dump())}, std::nullopt, + quic::DEFAULT_TIMEOUT, + std::nullopt, PathType::standard, request_id); _send_onion_request( @@ -1604,6 +1654,59 @@ void Network::get_random_nodes( // MARK: Request Handling +void Network::check_request_queue_timeouts(std::optional request_timeout_id_) { + // If the network is suspended (or destroyed) then don't bother checking for timeouts + if (suspended || destroyed) + return; + + // If there is an existing timeout checking loop then we don't want to start a second + if (request_timeout_id != request_timeout_id_) + return; + + // If there wasn't an existing loop id then set it here + if (!request_timeout_id) + request_timeout_id = random::random_base32(4); + + // Timeout and remove any pending requests which should timeout based on path build time + auto has_remaining_timeout_requests = false; + auto time_now = std::chrono::system_clock::now(); + + for (auto& [path_type, requests_for_path] : request_queue) + std::erase_if( + requests_for_path, + [&has_remaining_timeout_requests, &time_now](const auto& request) { + // If the request doesn't have a path build timeout then ignore it + if (!request.first.request_and_path_build_timeout) + return false; + + auto duration = std::chrono::duration_cast( + time_now - request.first.creation_time); + + if (duration > *request.first.request_and_path_build_timeout) { + request.second( + false, + true, + error_path_build_timeout, + "Timed out waiting for path build."); + return true; + } + + has_remaining_timeout_requests = true; + return false; + }); + + // If there are no more timeout requests then stop looping here + if (!has_remaining_timeout_requests) { + request_timeout_id = std::nullopt; + return; + } + + // Otherwise schedule the next check + net.call_later(queued_request_path_build_timeout_frequency, [this]() { + check_request_queue_timeouts(request_timeout_id); + }); +} + void Network::send_request( request_info info, connection_info conn_info, network_response_callback_t handle_response) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, info.request_id); @@ -1616,11 +1719,26 @@ void Network::send_request( if (info.body) payload = convert_sv(*info.body); + // Calculate the remaining timeout + std::chrono::milliseconds timeout = info.request_timeout; + + if (info.request_and_path_build_timeout) { + auto elapsed_time = std::chrono::duration_cast( + std::chrono::system_clock::now() - info.creation_time); + + timeout = *info.request_and_path_build_timeout - elapsed_time; + + // If the timeout was somehow negative then just fail the request (no point continuing if + // we have already timed out) + if (timeout < std::chrono::milliseconds(0)) + return handle_response(false, true, error_path_build_timeout, std::nullopt); + } + conn_info.add_pending_request(); conn_info.stream->command( info.endpoint, payload, - info.timeout, + timeout, [this, info, conn_info, cb = std::move(handle_response)](quic::message resp) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); @@ -1650,15 +1768,17 @@ void Network::send_onion_request( onionreq::network_destination destination, std::optional body, std::optional swarm_pubkey, - std::chrono::milliseconds timeout, network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout, PathType type) { _send_onion_request( request_info::make( std::move(destination), - timeout, std::move(body), std::move(swarm_pubkey), + request_timeout, + request_and_path_build_timeout, type), std::move(handle_response)); } @@ -1684,6 +1804,12 @@ void Network::_send_onion_request(request_info info, network_response_callback_t return cb(false, false, error_network_suspended, "Network is suspended."); request_queue[info.path_type].emplace_back(std::move(info), std::move(cb)); + + // If the request has a path_build_timeout then start the timeout check loop + if (info.request_and_path_build_timeout) + net.call_later(queued_request_path_build_timeout_frequency, [this]() { + check_request_queue_timeouts(); + }); }); } @@ -1826,8 +1952,9 @@ void Network::upload_file_to_server( ustring data, onionreq::ServerDestination server, std::optional file_name, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout) { std::vector> headers; std::unordered_set existing_keys; @@ -1857,16 +1984,18 @@ void Network::upload_file_to_server( server.method}, data, std::nullopt, - timeout, handle_response, + request_timeout, + request_and_path_build_timeout, PathType::upload); } void Network::download_file( std::string_view download_url, session::onionreq::x25519_pubkey x25519_pubkey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout) { const auto& [proto, host, port, path] = parse_url(download_url); if (!path) @@ -1874,23 +2003,32 @@ void Network::download_file( download_file( ServerDestination{proto, host, *path, x25519_pubkey, port, std::nullopt, "GET"}, - timeout, - handle_response); + handle_response, + request_timeout, + request_and_path_build_timeout); } void Network::download_file( onionreq::ServerDestination server, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout) { send_onion_request( - server, std::nullopt, std::nullopt, timeout, handle_response, PathType::download); + server, + std::nullopt, + std::nullopt, + handle_response, + request_timeout, + request_and_path_build_timeout, + PathType::download); } void Network::get_client_version( Platform platform, onionreq::ed25519_seckey seckey, - std::chrono::milliseconds timeout, - network_response_callback_t handle_response) { + network_response_callback_t handle_response, + std::chrono::milliseconds request_timeout, + std::optional request_and_path_build_timeout) { std::string endpoint; switch (platform) { @@ -1924,8 +2062,9 @@ void Network::get_client_version( "http", std::string(file_server), endpoint, pubkey, 80, headers, "GET"}, std::nullopt, pubkey, - timeout, handle_response, + request_timeout, + request_and_path_build_timeout, PathType::standard); } @@ -2307,7 +2446,7 @@ void Network::handle_errors( info.request_id); break; - default: break; // Unhandled case should just behave like any other error + default: break; // Unhandled case should just behave like any other error } } catch (...) { } @@ -2475,45 +2614,6 @@ void Network::handle_errors( (*handle_response)(false, timeout, status_code, response); } -std::vector convert_service_nodes( - std::vector nodes) { - std::vector converted_nodes; - for (auto& node : nodes) { - auto ed25519_pubkey_hex = oxenc::to_hex(node.view_remote_key()); - auto ipv4 = node.to_ipv4(); - network_service_node converted_node; - converted_node.ip[0] = (ipv4.addr >> 24) & 0xFF; - converted_node.ip[1] = (ipv4.addr >> 16) & 0xFF; - converted_node.ip[2] = (ipv4.addr >> 8) & 0xFF; - converted_node.ip[3] = ipv4.addr & 0xFF; - strncpy(converted_node.ed25519_pubkey_hex, ed25519_pubkey_hex.c_str(), 64); - converted_node.ed25519_pubkey_hex[64] = '\0'; // Ensure null termination - converted_node.quic_port = node.port(); - converted_nodes.push_back(converted_node); - } - - return converted_nodes; -} - -ServerDestination convert_server_destination(const network_server_destination server) { - std::optional>> headers; - if (server.headers_size > 0) { - headers = std::vector>{}; - - for (size_t i = 0; i < server.headers_size; i++) - headers->emplace_back(server.headers[i], server.header_values[i]); - } - - return ServerDestination{ - server.protocol, - server.host, - server.endpoint, - x25519_pubkey::from_hex({server.x25519_pubkey, 64}), - server.port, - headers, - server.method}; -} - } // namespace session::network // MARK: C API @@ -2615,7 +2715,7 @@ LIBSESSION_C_API void network_set_paths_changed_callback( // Allocate the memory for the onion_request_paths* array auto* c_paths_array = static_cast(std::malloc(paths_mem_size)); for (size_t i = 0; i < paths.size(); ++i) { - auto c_nodes = session::network::convert_service_nodes(paths[i]); + auto c_nodes = network::detail::convert_service_nodes(paths[i]); // Allocate memory that persists outside the loop size_t node_array_size = sizeof(network_service_node) * c_nodes.size(); @@ -2638,7 +2738,7 @@ LIBSESSION_C_API void network_get_swarm( unbox(network).get_swarm( x25519_pubkey::from_hex({swarm_pubkey_hex, 64}), [cb = std::move(callback), ctx](swarm_id_t, std::vector nodes) { - auto c_nodes = session::network::convert_service_nodes(nodes); + auto c_nodes = network::detail::convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); } @@ -2651,7 +2751,7 @@ LIBSESSION_C_API void network_get_random_nodes( assert(callback); unbox(network).get_random_nodes( count, [cb = std::move(callback), ctx](std::vector nodes) { - auto c_nodes = session::network::convert_service_nodes(nodes); + auto c_nodes = network::detail::convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); } @@ -2662,7 +2762,8 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( const unsigned char* body_, size_t body_size, const char* swarm_pubkey_hex, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx) { assert(callback); @@ -2676,6 +2777,11 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( if (swarm_pubkey_hex) swarm_pubkey = x25519_pubkey::from_hex({swarm_pubkey_hex, 64}); + std::optional request_and_path_build_timeout; + if (request_and_path_build_timeout_ms > 0) + request_and_path_build_timeout = + std::chrono::milliseconds{request_and_path_build_timeout_ms}; + std::array ip; std::memcpy(ip.data(), node.ip, ip.size()); @@ -2688,7 +2794,6 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( node.quic_port}, body, swarm_pubkey, - std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2703,7 +2808,9 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); - }); + }, + std::chrono::milliseconds{request_timeout_ms}, + request_and_path_build_timeout); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } @@ -2714,7 +2821,8 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( const network_server_destination server, const unsigned char* body_, size_t body_size, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx) { assert(server.method && server.protocol && server.host && server.endpoint && @@ -2725,11 +2833,15 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( if (body_size > 0) body = {body_, body_size}; + std::optional request_and_path_build_timeout; + if (request_and_path_build_timeout_ms > 0) + request_and_path_build_timeout = + std::chrono::milliseconds{request_and_path_build_timeout_ms}; + unbox(network).send_onion_request( - convert_server_destination(server), + network::detail::convert_server_destination(server), body, std::nullopt, - std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2744,7 +2856,9 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); - }); + }, + std::chrono::milliseconds{request_timeout_ms}, + request_and_path_build_timeout); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } @@ -2756,7 +2870,8 @@ LIBSESSION_C_API void network_upload_to_server( const unsigned char* data, size_t data_len, const char* file_name_, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx) { assert(data && server.method && server.protocol && server.host && server.endpoint && @@ -2767,11 +2882,15 @@ LIBSESSION_C_API void network_upload_to_server( if (file_name_) file_name = file_name_; + std::optional request_and_path_build_timeout; + if (request_and_path_build_timeout_ms > 0) + request_and_path_build_timeout = + std::chrono::milliseconds{request_and_path_build_timeout_ms}; + unbox(network).upload_file_to_server( {data, data_len}, - convert_server_destination(server), + network::detail::convert_server_destination(server), file_name, - std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2786,7 +2905,9 @@ LIBSESSION_C_API void network_upload_to_server( ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); - }); + }, + std::chrono::milliseconds{request_timeout_ms}, + request_and_path_build_timeout); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } @@ -2795,16 +2916,21 @@ LIBSESSION_C_API void network_upload_to_server( LIBSESSION_C_API void network_download_from_server( network_object* network, const network_server_destination server, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx) { assert(server.method && server.protocol && server.host && server.endpoint && server.x25519_pubkey && callback); try { + std::optional request_and_path_build_timeout; + if (request_and_path_build_timeout_ms > 0) + request_and_path_build_timeout = + std::chrono::milliseconds{request_and_path_build_timeout_ms}; + unbox(network).download_file( - convert_server_destination(server), - std::chrono::milliseconds{timeout_ms}, + network::detail::convert_server_destination(server), [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2819,7 +2945,9 @@ LIBSESSION_C_API void network_download_from_server( ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); - }); + }, + std::chrono::milliseconds{request_timeout_ms}, + request_and_path_build_timeout); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } @@ -2829,16 +2957,21 @@ LIBSESSION_C_API void network_get_client_version( network_object* network, CLIENT_PLATFORM platform, const unsigned char* ed25519_secret, - int64_t timeout_ms, + int64_t request_timeout_ms, + int64_t request_and_path_build_timeout_ms, network_onion_response_callback_t callback, void* ctx) { assert(platform && callback); try { + std::optional request_and_path_build_timeout; + if (request_and_path_build_timeout_ms > 0) + request_and_path_build_timeout = + std::chrono::milliseconds{request_and_path_build_timeout_ms}; + unbox(network).get_client_version( static_cast(platform), onionreq::ed25519_seckey::from_bytes({ed25519_secret, 64}), - std::chrono::milliseconds{timeout_ms}, [cb = std::move(callback), ctx]( bool success, bool timeout, @@ -2853,7 +2986,9 @@ LIBSESSION_C_API void network_get_client_version( ctx); else cb(success, timeout, status_code, nullptr, 0, ctx); - }); + }, + std::chrono::milliseconds{request_timeout_ms}, + request_and_path_build_timeout); } catch (const std::exception& e) { callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 7742917b..39fecab3 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -23,7 +23,12 @@ struct Result { }; service_node test_node(const ustring ed_pk, const uint16_t index, const bool unique_ip = true) { - return service_node{ed_pk, {2, 8, 0}, INVALID_SWARM_ID, (unique_ip ? fmt::format("0.0.0.{}", index) : "1.1.1.1"), index}; + return service_node{ + ed_pk, + {2, 8, 0}, + INVALID_SWARM_ID, + (unique_ip ? fmt::format("0.0.0.{}", index) : "1.1.1.1"), + index}; } std::optional node_for_destination(network_destination destination) { @@ -72,7 +77,8 @@ class TestNetwork : public Network { unused_connections = unused_connections_; } - void set_in_progress_connections(std::unordered_map in_progress_connections_) { + void set_in_progress_connections( + std::unordered_map in_progress_connections_) { in_progress_connections = in_progress_connections_; } @@ -90,21 +96,27 @@ class TestNetwork : public Network { all_swarms = all_swarms_; } - void set_swarm(session::onionreq::x25519_pubkey swarm_pubkey, swarm_id_t swarm_id, std::vector swarm) { + void set_swarm( + session::onionreq::x25519_pubkey swarm_pubkey, + swarm_id_t swarm_id, + std::vector swarm) { swarm_cache[swarm_pubkey.hex()] = {swarm_id, swarm}; } - std::pair> get_cached_swarm(session::onionreq::x25519_pubkey swarm_pubkey) { + std::pair> get_cached_swarm( + session::onionreq::x25519_pubkey swarm_pubkey) { return swarm_cache[swarm_pubkey.hex()]; } swarm_id_t get_swarm_id(std::string swarm_pubkey_hex) { if (swarm_pubkey_hex.size() == 66) swarm_pubkey_hex = swarm_pubkey_hex.substr(2); - + auto pk = x25519_pubkey::from_hex(swarm_pubkey_hex); std::promise prom; - get_swarm(pk, [&prom](swarm_id_t result, std::vector){ prom.set_value(result); }); + get_swarm(pk, [&prom](swarm_id_t result, std::vector) { + prom.set_value(result); + }); return prom.get_future().get(); } @@ -207,6 +219,15 @@ class TestNetwork : public Network { return Network::find_valid_path(info, paths); } + void check_request_queue_timeouts(std::optional request_timeout_id) override { + const auto func_name = "check_request_queue_timeouts"; + + if (check_should_ignore_and_log_call(func_name)) + return; + + Network::check_request_queue_timeouts(request_timeout_id); + } + void _send_onion_request( request_info info, network_response_callback_t handle_response) override { const auto func_name = "_send_onion_request"; @@ -316,6 +337,8 @@ TEST_CASE("Network error handling", "[network]") { PathType::standard, 0ms, std::nullopt, + std::chrono::system_clock::now(), + std::nullopt, true}; Result result; std::optional network; @@ -479,6 +502,8 @@ TEST_CASE("Network error handling", "[network]") { PathType::standard, 0ms, std::nullopt, + std::chrono::system_clock::now(), + std::nullopt, true}; network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test @@ -495,7 +520,8 @@ TEST_CASE("Network error handling", "[network]") { [](bool, bool, int16_t, std::optional) {}); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); REQUIRE(network->last_request_info.has_value()); - CHECK(node_for_destination(network->last_request_info->destination) != node_for_destination(mock_request2.destination)); + CHECK(node_for_destination(network->last_request_info->destination) != + node_for_destination(mock_request2.destination)); // Check that when a retry request of a 421 receives it's own 421 that it tries // to update the snode cache @@ -508,11 +534,14 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, + std::nullopt, + std::chrono::system_clock::now(), request_info::RetryReason::redirect, true}; network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test - network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled", "refresh_snode_cache"); + network->ignore_calls_to( + "_send_onion_request", "update_disk_cache_throttled", "refresh_snode_cache"); network->set_paths(PathType::standard, {path}); network->handle_errors( mock_request3, @@ -523,7 +552,8 @@ TEST_CASE("Network error handling", "[network]") { [](bool, bool, int16_t, std::optional) {}); CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); - // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it is handled like any other error + // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it + // is handled like any other error auto mock_request4 = request_info{ "BBBB", target, @@ -533,6 +563,8 @@ TEST_CASE("Network error handling", "[network]") { x25519_pubkey::from_hex(x_pk_hex), PathType::standard, 0ms, + std::nullopt, + std::chrono::system_clock::now(), request_info::RetryReason::redirect_swarm_refresh, true}; network.emplace(std::nullopt, true, true, false); @@ -580,6 +612,8 @@ TEST_CASE("Network error handling", "[network]") { PathType::standard, 0ms, std::nullopt, + std::chrono::system_clock::now(), + std::nullopt, false}; network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test @@ -684,7 +718,12 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network->add_pending_request( PathType::standard, request_info::make( - snode_cache.front(), 1s, std::nullopt, std::nullopt, PathType::standard)); + snode_cache.front(), + std::nullopt, + std::nullopt, + 1s, + std::nullopt, + PathType::standard)); unused_nodes = network->get_unused_nodes(); std::stable_sort(unused_nodes.begin(), unused_nodes.end()); CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); @@ -789,7 +828,12 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->add_pending_request( PathType::download, request_info::make( - snode_cache.back(), 1s, std::nullopt, std::nullopt, PathType::download)); + snode_cache.back(), + std::nullopt, + std::nullopt, + 1s, + std::nullopt, + PathType::download)); network->build_path(PathType::download, "Test1"); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::download).size() == 1); @@ -805,7 +849,12 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->add_pending_request( PathType::standard, request_info::make( - snode_cache.back(), 1s, std::nullopt, std::nullopt, PathType::standard)); + snode_cache.back(), + std::nullopt, + std::nullopt, + 1s, + std::nullopt, + PathType::standard)); network->build_path(PathType::standard, "Test1"); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::standard).size() == 1); @@ -823,7 +872,7 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { "144.76.164.202", uint16_t{35400}}; auto network = TestNetwork(std::nullopt, true, false, false); - auto info = request_info::make(target, 0ms, std::nullopt, std::nullopt); + auto info = request_info::make(target, std::nullopt, std::nullopt, 0ms); auto invalid_path = onion_path{ {test_service_node, nullptr, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; @@ -851,7 +900,7 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { std::move(result.first), std::vector{test_service_node}, uint8_t{0}}; // It excludes paths which include the IP of the target - auto shared_ip_info = request_info::make(test_service_node, 0ms, std::nullopt, std::nullopt); + auto shared_ip_info = request_info::make(test_service_node, std::nullopt, std::nullopt, 0ms); CHECK_FALSE(network.find_valid_path(shared_ip_info, {valid_path}).has_value()); // It returns a path when there is a valid one @@ -984,6 +1033,65 @@ TEST_CASE("Network requests", "[network][establish_connection]") { CHECK_FALSE(result.second.has_value()); } +TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { + auto test_service_node = service_node{ + "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, + {2, 8, 0}, + INVALID_SWARM_ID, + "144.76.164.202", + uint16_t{35400}}; + std::optional network; + std::promise prom; + + // Test that it doesn't start checking for timeouts when the request doesn't have + // a build paths timeout + network.emplace(std::nullopt, true, true, false); + network->send_onion_request( + test_service_node, + ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, + std::nullopt, + [](bool, bool, int16_t, std::optional) {}, + oxen::quic::DEFAULT_TIMEOUT, + std::nullopt); + CHECK(ALWAYS(300ms, network->did_not_call("check_request_queue_timeouts"))); + + // Test that it does start checking for timeouts when the request has a + // paths build timeout + network.emplace(std::nullopt, true, true, false); + network->ignore_calls_to("build_path"); + network->send_onion_request( + test_service_node, + ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, + std::nullopt, + [](bool, bool, int16_t, std::optional) {}, + oxen::quic::DEFAULT_TIMEOUT, + oxen::quic::DEFAULT_TIMEOUT); + CHECK(EVENTUALLY(300ms, network->called("check_request_queue_timeouts"))); + + // Test that it fails the request with a timeout if it has a build path timeout + // and the path build takes too long + network.emplace(std::nullopt, true, true, false); + network->ignore_calls_to("build_path"); + network->send_onion_request( + test_service_node, + ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, + std::nullopt, + [&prom](bool success, + bool timeout, + int16_t status_code, + std::optional response) { + prom.set_value({success, timeout, status_code, response}); + }, + oxen::quic::DEFAULT_TIMEOUT, + 100ms); + + // Wait for the result to be set + auto result = prom.get_future().get(); + + CHECK_FALSE(result.success); + CHECK(result.timeout); +} + TEST_CASE("Network requests", "[network][send_request]") { auto test_service_node = service_node{ "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, @@ -1006,9 +1114,10 @@ TEST_CASE("Network requests", "[network][send_request]") { network.send_request( request_info::make( test_service_node, - 3s, ustring{to_usv("{}")}, std::nullopt, + 3s, + std::nullopt, PathType::standard, std::nullopt, "info"), @@ -1051,14 +1160,15 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { test_service_node, ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, std::nullopt, - oxen::quic::DEFAULT_TIMEOUT, [&result_promise]( bool success, bool timeout, int16_t status_code, std::optional response) { result_promise.set_value({success, timeout, status_code, response}); - }); + }, + oxen::quic::DEFAULT_TIMEOUT, + oxen::quic::DEFAULT_TIMEOUT); // Wait for the result to be set auto result = result_promise.get_future().get(); @@ -1096,6 +1206,7 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { body.size(), nullptr, std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), + std::chrono::milliseconds{oxen::quic::DEFAULT_TIMEOUT}.count(), [](bool success, bool timeout, int16_t status_code, @@ -1128,91 +1239,130 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { TEST_CASE("Network swarm", "[network][detail][pubkey_to_swarm_space]") { x25519_pubkey pk; - pk = x25519_pubkey::from_hex("3506f4a71324b7dd114eddbf4e311f39dde243e1f2cb97c40db1961f70ebaae8"); + pk = x25519_pubkey::from_hex( + "3506f4a71324b7dd114eddbf4e311f39dde243e1f2cb97c40db1961f70ebaae8"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 17589930838143112648ULL); - pk = x25519_pubkey::from_hex("cf27da303a50ac8c4b2d43d27259505c9bcd73fc21cf2a57902c3d050730b604"); + pk = x25519_pubkey::from_hex( + "cf27da303a50ac8c4b2d43d27259505c9bcd73fc21cf2a57902c3d050730b604"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 10370619079776428163ULL); - pk = x25519_pubkey::from_hex("d3511706b8b34f6e8411bf07bd22ba6b2435ca56846fbccf6eb1e166a6cd15cc"); + pk = x25519_pubkey::from_hex( + "d3511706b8b34f6e8411bf07bd22ba6b2435ca56846fbccf6eb1e166a6cd15cc"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 2144983569669512198ULL); - pk = x25519_pubkey::from_hex("0f06693428fca9102a451e3f28d9cc743d8ea60a89ab6aa69eb119470c11cbd3"); + pk = x25519_pubkey::from_hex( + "0f06693428fca9102a451e3f28d9cc743d8ea60a89ab6aa69eb119470c11cbd3"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 9690840703409570833ULL); - pk = x25519_pubkey::from_hex("ffba630924aa1224bb930dde21c0d11bf004608f2812217f8ac812d6c7e3ad48"); + pk = x25519_pubkey::from_hex( + "ffba630924aa1224bb930dde21c0d11bf004608f2812217f8ac812d6c7e3ad48"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 4532060000165252872ULL); - pk = x25519_pubkey::from_hex("eeeeeeeeeeeeeeee777777777777777711111111111111118888888888888888"); + pk = x25519_pubkey::from_hex( + "eeeeeeeeeeeeeeee777777777777777711111111111111118888888888888888"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0); - pk = x25519_pubkey::from_hex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + pk = x25519_pubkey::from_hex( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0); - pk = x25519_pubkey::from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); + pk = x25519_pubkey::from_hex( + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 1); - pk = x25519_pubkey::from_hex("ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffff"); + pk = x25519_pubkey::from_hex( + "ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffff"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 1ULL << 63); - pk = x25519_pubkey::from_hex("000000000000000000000000000000000000000000000000ffffffffffffffff"); + pk = x25519_pubkey::from_hex( + "000000000000000000000000000000000000000000000000ffffffffffffffff"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == (uint64_t)-1); - pk = x25519_pubkey::from_hex("0000000000000000000000000000000000000000000000000123456789abcdef"); + pk = x25519_pubkey::from_hex( + "0000000000000000000000000000000000000000000000000123456789abcdef"); CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0x0123456789abcdefULL); } TEST_CASE("Network swarm", "[network][get_swarm]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - std::vector>> swarms = {{100, {}}, {200, {}}, {300, {}}, {399, {}}, {498, {}}, {596, {}}, {694, {}}}; + std::vector>> swarms = { + {100, {}}, {200, {}}, {300, {}}, {399, {}}, {498, {}}, {596, {}}, {694, {}}}; auto network = TestNetwork(std::nullopt, true, true, false); network.set_snode_cache({test_node(ed_pk, 0)}); network.set_all_swarms(swarms); // Exact matches: // 0x64 = 100, 0xc8 = 200, 0x1f2 = 498 - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000064") == 100); - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000c8") == 200); - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001f2") == 498); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000006" + "4") == 100); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000c" + "8") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001f" + "2") == 498); // Nearest - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000000") == 100); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000001") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000000" + "0") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000000" + "1") == 100); // Nearest, with wraparound // 0x8000... is closest to the top value - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000008000000000000000") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000000" + "0") == 694); // 0xa000... is closest (via wraparound) to the smallest - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000a000000000000000") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000a00000000000000" + "0") == 100); // This is the invalid swarm id for swarms, but should still work for a client - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000ffffffffffffffff") == 100); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffffe") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffff" + "f") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffff" + "e") == 100); // Midpoint tests; we prefer the lower value when exactly in the middle between two swarms. // 0x96 = 150 - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000095") == 100); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000096") == 100); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000097") == 200); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000009" + "5") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000009" + "6") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000009" + "7") == 200); // 0xfa = 250 - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000f9") == 200); - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000fa") == 200); - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000fb") == 300); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000f" + "9") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000f" + "a") == 200); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000000f" + "b") == 300); // 0x15d = 349 - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015d") == 300); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015e") == 399); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015" + "d") == 300); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000015" + "e") == 399); // 0x1c0 = 448 - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c0") == 399); - CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c1") == 498); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c" + "0") == 399); + CHECK(network.get_swarm_id("0500000000000000000000000000000000000000000000000000000000000001c" + "1") == 498); // 0x223 = 547 - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000222") == 498); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000223") == 498); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000224") == 596); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000022" + "2") == 498); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000022" + "3") == 498); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000022" + "4") == 596); // 0x285 = 645 - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000285") == 596); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000286") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000028" + "5") == 596); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000028" + "6") == 694); // 0x800....d is the midpoint between 694 and 100 (the long way). We always round "down" (which // in this case, means wrapping to the largest swarm). - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018c") == 694); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018d") == 694); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018e") == 100); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018" + "c") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018" + "d") == 694); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000800000000000018" + "e") == 100); // With a swarm at -20 the midpoint is now 40 (=0x28). When our value is the *low* value we // prefer the *last* swarm in the case of a tie (while consistent with the general case of @@ -1220,16 +1370,21 @@ TEST_CASE("Network swarm", "[network][get_swarm]") { // *sigh*). swarms.push_back({(uint64_t)-20, {}}); network.set_all_swarms(swarms); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000027") == swarms.back().first); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000028") == swarms.back().first); - CHECK(network.get_swarm_id("050000000000000000000000000000000000000000000000000000000000000029") == swarms.front().first); - + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000002" + "7") == swarms.back().first); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000002" + "8") == swarms.back().first); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000000000000000002" + "9") == swarms.front().first); + // The code used to have a broken edge case if we have a swarm at zero and a client at max-u64 // because of an overflow in how the distance is calculated (the first swarm will be calculated // as max-u64 away, rather than 1 away), and so the id always maps to the highest swarm (even // though 0xfff...fe maps to the lowest swarm; the first check here, then, would fail. swarms.insert(swarms.begin(), {0, {}}); network.set_all_swarms(swarms); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000ffffffffffffffff") == 0); - CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffffe") == 0); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffff" + "f") == 0); + CHECK(network.get_swarm_id("05000000000000000000000000000000000000000000000000fffffffffffffff" + "e") == 0); } From 1d20f5e392ba55847a30431f196d638834a4feb0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 6 Sep 2024 17:08:10 +1000 Subject: [PATCH 395/572] Improvements to attachment upload/download behaviours MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Increased both upload and download min path counts from 1 to 2 • Added a new `PathSelectionBehaviour` which prioritises upload/download path selection based on which has the fewest pending requests • Fixed an issue where you could end up with paths with the same IP because the `unused_nodes` wasn't getting updated after building a path --- include/session/network.hpp | 6 ++++ src/network.cpp | 57 +++++++++++++++++++++++++++++++++++-- tests/test_network.cpp | 22 +++++++------- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index c8195dfc..1e8cc920 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -111,6 +111,12 @@ struct onion_path { bool is_valid() const { return !nodes.empty() && conn_info.is_valid(); }; bool has_pending_requests() const { return conn_info.has_pending_requests(); } + size_t num_pending_requests() const { + if (!conn_info.pending_requests) + return 0; + return (*conn_info.pending_requests); + } + std::string to_string() const; bool operator==(const onion_path& other) const { diff --git a/src/network.cpp b/src/network.cpp index 6df6c742..8678e773 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -85,6 +85,11 @@ namespace { constexpr auto ALPN = "oxenstorage"sv; constexpr auto ONION = "onion_req"; + enum class PathSelectionBehaviour { + random, + new_or_least_busy, + }; + std::string path_type_name(PathType path_type, bool single_path_mode) { if (single_path_mode) return "single_path"; @@ -104,12 +109,21 @@ namespace { switch (path_type) { case PathType::standard: return 2; - case PathType::upload: return 1; - case PathType::download: return 1; + case PathType::upload: return 2; + case PathType::download: return 2; } return 2; // Default } + PathSelectionBehaviour path_selection_behaviour(PathType path_type) { + switch (path_type) { + case PathType::standard: return PathSelectionBehaviour::random; + case PathType::upload: return PathSelectionBehaviour::new_or_least_busy; + case PathType::download: return PathSelectionBehaviour::new_or_least_busy; + } + return PathSelectionBehaviour::random; // Default + } + /// Converts a string such as "1.2.3" to a vector of ints {1,2,3}. Throws if something /// in/around the .'s isn't parseable as an integer. std::vector parse_version(std::string_view vers, bool trim_trailing_zero = true) { @@ -1423,6 +1437,16 @@ void Network::build_path(PathType path_type, std::string request_id) { } } + // Remove the nodes from unused_nodes which have the same IPs as nodes in + // the final path + std::vector path_ips; + for (const auto& node : path_nodes) + path_ips.emplace_back(node.to_ipv4()); + + std::erase_if(unused_nodes, [&path_ips](const auto& node) { + return std::find(path_ips.begin(), path_ips.end(), node.to_ipv4()) != path_ips.end(); + }); + // If there are pending requests which this path is valid for then resume them std::erase_if(request_queue[path_type], [this, &path](const auto& request) { if (!find_valid_path(request.first, {path})) @@ -1486,10 +1510,37 @@ std::optional Network::find_valid_path( if (possible_paths.empty()) return std::nullopt; + // Randomise the possible paths (if all paths are equal for the PathSelectionBehaviour then we + // want a random one to be selected) CSRNG rng; std::shuffle(possible_paths.begin(), possible_paths.end(), rng); - return possible_paths.front(); + // Select from the possible paths based on the desired behaviour + auto behaviour = path_selection_behaviour(info.path_type); + switch (behaviour) { + case PathSelectionBehaviour::new_or_least_busy: { + auto min_num_paths = min_path_count(info.path_type, single_path_mode); + std::sort( + possible_paths.begin(), possible_paths.end(), [](const auto& a, const auto& b) { + return a.num_pending_requests() < b.num_pending_requests(); + }); + + // If we have already have the min number of paths for this path type, or there is + // a path with no pending requests then return the first path + if (paths.size() >= min_num_paths || + possible_paths.front().num_pending_requests() == 0) + return possible_paths.front(); + + // Otherwise we want to build a new path (for this PathSelectionBehaviour the assuption + // is that it'd be faster to build a new path and send the request along that rather + // than use an existing path) + return std::nullopt; + } + + // Random is the default behaviour + case PathSelectionBehaviour::random: return possible_paths.front(); + default: return possible_paths.front(); + } }; void Network::build_path_if_needed(PathType path_type, bool found_path) { diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 39fecab3..9c54950d 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -937,7 +937,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - // Can only add two path build to the queue + // Can only add the correct number of 'standard' path builds to the queue network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::standard, false); @@ -949,7 +949,7 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { CHECK(network->get_path_build_queue() == std::deque{PathType::standard, PathType::standard}); - // Can add a second 'standard' path build even if there is an active 'standard' path + // Can add additional 'standard' path builds if below the minimum threshold network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); @@ -985,27 +985,29 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - // Can only add a single 'download' path build + // Can only add the correct number of 'download' path builds to the queue network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); - network->set_paths(PathType::download, {}); network->build_path_if_needed(PathType::download, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + network->build_path_if_needed(PathType::download, false); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::download, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); - CHECK(network->get_path_build_queue() == std::deque{PathType::download}); + CHECK(network->get_path_build_queue() == + std::deque{PathType::download, PathType::download}); - // Can only add a single 'upload' path build + // Can only add the correct number of 'upload' path builds to the queue network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); - network->set_paths(PathType::upload, {}); network->build_path_if_needed(PathType::upload, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + network->build_path_if_needed(PathType::upload, false); + CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::upload, false); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); - CHECK(network->get_path_build_queue() == std::deque{PathType::upload}); + CHECK(network->get_path_build_queue() == + std::deque{PathType::upload, PathType::upload}); } TEST_CASE("Network requests", "[network][establish_connection]") { From 789f9f6c2c8c8304bf8e6b69716aa618ba39c3da Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 14:44:00 +1000 Subject: [PATCH 396/572] Expose response headers in network request functions --- include/session/network.h | 6 + include/session/network.hpp | 32 ++-- src/network.cpp | 314 ++++++++++++++++++++++++++++++------ tests/test_logging.cpp | 8 + tests/test_network.cpp | 116 ++++++++++--- 5 files changed, 393 insertions(+), 83 deletions(-) diff --git a/include/session/network.h b/include/session/network.h index c35fcc8f..1168437f 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -181,6 +181,9 @@ LIBSESSION_EXPORT void network_get_random_nodes( /// - `success` -- true if the request was successful, false if it failed. /// - `timeout` -- true if the request failed because of a timeout /// - `status_code` -- the HTTP numeric status code of the request, e.g. 200 for OK +/// - `headers` -- the response headers, array of null-terminated C strings +/// - `header_values` -- the response header values, array of null-terminated C strings +/// - `headers_size` -- the number of `headers`/`header_values` /// - `response` -- pointer to the beginning of the response body /// - `response_size` -- length of the response body /// - `ctx` -- the context pointer passed to the function that initiated the request. @@ -188,6 +191,9 @@ typedef void (*network_onion_response_callback_t)( bool success, bool timeout, int16_t status_code, + const char** headers, + const char** header_values, + size_t headers_size, const char* response, size_t response_size, void* ctx); diff --git a/include/session/network.hpp b/include/session/network.hpp index 1e8cc920..5585c245 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -14,7 +14,11 @@ namespace session::network { namespace fs = std::filesystem; using network_response_callback_t = std::function response)>; + bool success, + bool timeout, + int16_t status_code, + std::vector> headers, + std::optional response)>; enum class ConnectionStatus { unknown, @@ -658,9 +662,13 @@ class Network { /// - `response` -- [in] the response data returned from the destination. /// /// Outputs: - /// - A pair containing the status code and body of the decrypted onion request response. - std::pair> process_v3_onion_response( - session::onionreq::Builder builder, std::string response); + /// - A tuple containing the status code, headers and body of the decrypted onion request + /// response. + std::tuple< + int16_t, + std::vector>, + std::optional> + process_v3_onion_response(session::onionreq::Builder builder, std::string response); /// API: network/process_v4_onion_response /// @@ -671,9 +679,13 @@ class Network { /// - `response` -- [in] the response data returned from the destination. /// /// Outputs: - /// - A pair containing the status code and body of the decrypted onion request response. - std::pair> process_v4_onion_response( - session::onionreq::Builder builder, std::string response); + /// - A tuple containing the status code, headers and body of the decrypted onion request + /// response. + std::tuple< + int16_t, + std::vector>, + std::optional> + process_v4_onion_response(session::onionreq::Builder builder, std::string response); /// API: network/validate_response /// @@ -716,7 +728,8 @@ class Network { /// - `info` -- [in] the information for the request that was made. /// - `conn_info` -- [in] the connection info for the request that failed. /// - `timeout` -- [in, optional] flag indicating whether the request timed out. - /// - `status_code` -- [in, optional] the status code returned from the network. + /// - `status_code` -- [in] the status code returned from the network. + /// - `headers` -- [in] the response headers returned from the network. /// - `response` -- [in, optional] response data returned from the network. /// - `handle_response` -- [in, optional] callback to be called with updated response /// information after processing the error. @@ -724,7 +737,8 @@ class Network { request_info info, connection_info conn_info, bool timeout, - std::optional status_code, + int16_t status_code, + std::vector> headers, std::optional response, std::optional handle_response); }; diff --git a/src/network.cpp b/src/network.cpp index 8678e773..0066fb7d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -45,15 +45,24 @@ namespace { class status_code_exception : public std::runtime_error { public: int16_t status_code; + std::vector> headers; - status_code_exception(int16_t status_code, std::string message) : - std::runtime_error(message), status_code{status_code} {} + status_code_exception( + int16_t status_code, + std::vector> headers, + std::string message) : + std::runtime_error(message), status_code{status_code}, headers{headers} {} }; constexpr int16_t error_network_suspended = -10001; constexpr int16_t error_building_onion_request = -10002; constexpr int16_t error_path_build_timeout = -10003; + const std::pair content_type_plain_text = { + "Content-Type", "text/plain; charset=UTF-8"}; + const std::pair content_type_json = { + "Content-Type", "application/json"}; + // The amount of time the snode cache can be used before it needs to be refreshed/ constexpr auto snode_cache_expiration_duration = 2h; @@ -715,7 +724,12 @@ void Network::_close_connections() { // Cancel any pending requests (they can't succeed once the connection is closed) for (const auto& [path_type, path_type_requests] : request_queue) for (const auto& [info, callback] : path_type_requests) - callback(false, false, error_network_suspended, "Network is suspended."); + callback( + false, + false, + error_network_suspended, + {content_type_plain_text}, + "Network is suspended."); // Clear all storage of requests, paths and connections so that we are in a fresh state on // relaunch @@ -1238,7 +1252,11 @@ void Network::refresh_snode_cache(std::optional existing_request_id _send_onion_request( info, [this, request_id]( - bool success, bool timeout, int16_t, std::optional response) { + bool success, + bool timeout, + int16_t, + std::vector>, + std::optional response) { // If the 'snode_refresh_results' value doesn't exist it means we have already // completed/cancelled this snode cache refresh and have somehow gotten into an // invalid state, so just ignore this request @@ -1527,8 +1545,7 @@ std::optional Network::find_valid_path( // If we have already have the min number of paths for this path type, or there is // a path with no pending requests then return the first path - if (paths.size() >= min_num_paths || - possible_paths.front().num_pending_requests() == 0) + if (paths.size() >= min_num_paths || possible_paths.front().num_pending_requests() == 0) return possible_paths.front(); // Otherwise we want to build a new path (for this PathSelectionBehaviour the assuption @@ -1738,6 +1755,7 @@ void Network::check_request_queue_timeouts(std::optional request_ti false, true, error_path_build_timeout, + {content_type_plain_text}, "Timed out waiting for path build."); return true; } @@ -1763,7 +1781,8 @@ void Network::send_request( log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, info.request_id); if (!conn_info.is_valid()) - return handle_response(false, false, -1, "Network is unreachable."); + return handle_response( + false, false, -1, {content_type_plain_text}, "Network is unreachable."); quic::bstring_view payload{}; @@ -1782,7 +1801,12 @@ void Network::send_request( // If the timeout was somehow negative then just fail the request (no point continuing if // we have already timed out) if (timeout < std::chrono::milliseconds(0)) - return handle_response(false, true, error_path_build_timeout, std::nullopt); + return handle_response( + false, + true, + error_path_build_timeout, + {content_type_plain_text}, + "Path Build Timed Out."); } conn_info.add_pending_request(); @@ -1801,12 +1825,25 @@ void Network::send_request( result = validate_response(resp, false); } catch (const status_code_exception& e) { return handle_errors( - info, conn_info, resp.timed_out, e.status_code, e.what(), cb); + info, + conn_info, + resp.timed_out, + e.status_code, + e.headers, + e.what(), + cb); } catch (const std::exception& e) { - return handle_errors(info, conn_info, resp.timed_out, -1, e.what(), cb); + return handle_errors( + info, + conn_info, + resp.timed_out, + -1, + {content_type_plain_text}, + e.what(), + cb); } - cb(true, false, status_code, body); + cb(true, false, status_code, {}, body); // After completing a request we should try to clear any pending path drops (just in // case this request was the final one on a pending path drop) @@ -1852,7 +1889,12 @@ void Network::_send_onion_request(request_info info, network_response_callback_t return net.call([this, info = std::move(info), cb = std::move(handle_response)]() { // If the network is suspended then fail immediately if (suspended) - return cb(false, false, error_network_suspended, "Network is suspended."); + return cb( + false, + false, + error_network_suspended, + {content_type_plain_text}, + "Network is suspended."); request_queue[info.path_type].emplace_back(std::move(info), std::move(cb)); @@ -1881,7 +1923,8 @@ void Network::_send_onion_request(request_info info, network_response_callback_t auto payload = builder.generate_payload(info.original_body); info.body = builder.build(payload); } catch (const std::exception& e) { - return handle_response(false, false, error_building_onion_request, e.what()); + return handle_response( + false, false, error_building_onion_request, {content_type_plain_text}, e.what()); } // Actually send the request @@ -1896,13 +1939,14 @@ void Network::_send_onion_request(request_info info, network_response_callback_t bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { log::trace(cat, "{} got response for {}.", __PRETTY_FUNCTION__, info.request_id); // If the request was reported as a failure or a timeout then we // will have already handled the errors so just trigger the callback if (!success || timeout) - return cb(success, timeout, status_code, response); + return cb(success, timeout, status_code, headers, response); try { // Ensure the response is long enough to be processed, if not @@ -1910,11 +1954,16 @@ void Network::_send_onion_request(request_info info, network_response_callback_t if (!ResponseParser::response_long_enough(builder.enc_type, response->size())) throw status_code_exception{ status_code, + {content_type_plain_text}, "Response is too short to be an onion request response: " + *response}; // Otherwise, process the onion request response - std::pair> processed_response; + std::tuple< + int16_t, + std::vector>, + std::optional> + processed_response; // The SnodeDestination runs via V3 onion requests and the // ServerDestination runs via V4 @@ -1924,10 +1973,12 @@ void Network::_send_onion_request(request_info info, network_response_callback_t processed_response = process_v4_onion_response(builder, *response); // If we got a non 2xx status code, return the error - auto& [processed_status_code, processed_body] = processed_response; + auto& [processed_status_code, processed_headers, processed_body] = + processed_response; if (processed_status_code < 200 || processed_status_code > 299) throw status_code_exception{ processed_status_code, + {content_type_plain_text}, processed_body.value_or("Request returned " "non-success status " "code.")}; @@ -1954,13 +2005,20 @@ void Network::_send_onion_request(request_info info, network_response_callback_t // If there was no 'results' array then it wasn't a batch // request so we can stop here and return if (!results) - return cb(true, false, processed_status_code, processed_body); + return cb( + true, + false, + processed_status_code, + processed_headers, + processed_body); // Otherwise we want to check if all of the results have the // same status code and, if so, handle that failure case // (default the 'error_body' to the 'processed_body' in case we // don't get an explicit error) int16_t single_status_code = -1; + std::vector> single_headers = { + content_type_plain_text}; std::optional error_body = processed_body; for (const auto& result : results->items()) { if (result.value().contains("code") && result.value()["code"].is_number() && @@ -1976,6 +2034,14 @@ void Network::_send_onion_request(request_info info, network_response_callback_t break; } + if (result.value().contains("headers")) { + single_headers = {}; + auto header_vals = result.value()["headers"]; + + for (auto it = header_vals.begin(); it != header_vals.end(); ++it) + single_headers.emplace_back(it.key(), it.value()); + } + if (result.value().contains("body") && result.value()["body"].is_string()) error_body = result.value()["body"].get(); } @@ -1985,16 +2051,26 @@ void Network::_send_onion_request(request_info info, network_response_callback_t if (single_status_code < 200 || single_status_code > 299) throw status_code_exception{ single_status_code, + single_headers, error_body.value_or("Sub-request returned " "non-success status code.")}; // Otherwise some requests succeeded and others failed so // succeed with the processed data - return cb(true, false, processed_status_code, processed_body); + return cb( + true, false, processed_status_code, processed_headers, processed_body); } catch (const status_code_exception& e) { - handle_errors(info, path.conn_info, false, e.status_code, e.what(), cb); + handle_errors( + info, path.conn_info, false, e.status_code, e.headers, e.what(), cb); } catch (const std::exception& e) { - handle_errors(info, path.conn_info, false, -1, e.what(), cb); + handle_errors( + info, + path.conn_info, + false, + -1, + {content_type_plain_text}, + e.what(), + cb); } }); } @@ -2121,8 +2197,8 @@ void Network::get_client_version( // MARK: Response Handling -std::pair> Network::process_v3_onion_response( - Builder builder, std::string response) { +std::tuple>, std::optional> +Network::process_v3_onion_response(Builder builder, std::string response) { std::string base64_iv_and_ciphertext; try { nlohmann::json response_json = nlohmann::json::parse(response); @@ -2147,6 +2223,7 @@ std::pair> Network::process_v3_onion_respons auto result = parser.decrypt(iv_and_ciphertext); auto result_json = nlohmann::json::parse(result); int16_t status_code; + std::vector> headers; std::string body; if (result_json.contains("status_code") && result_json["status_code"].is_number()) @@ -2156,16 +2233,23 @@ std::pair> Network::process_v3_onion_respons else throw std::runtime_error{"Invalid JSON response, missing required status_code field."}; + if (result_json.contains("headers")) { + auto header_vals = result_json["headers"]; + + for (auto it = header_vals.begin(); it != header_vals.end(); ++it) + headers.emplace_back(it.key(), it.value()); + } + if (result_json.contains("body") && result_json["body"].is_string()) body = result_json["body"].get(); else body = result_json.dump(); - return {status_code, body}; + return {status_code, headers, body}; } -std::pair> Network::process_v4_onion_response( - Builder builder, std::string response) { +std::tuple>, std::optional> +Network::process_v4_onion_response(Builder builder, std::string response) { ustring response_data{to_unsigned(response.data()), response.size()}; auto parser = ResponseParser(builder); auto result = parser.decrypt(response_data); @@ -2178,6 +2262,7 @@ std::pair> Network::process_v4_onion_respons auto response_info_string = result_bencode.consume_string(); int16_t status_code; + std::vector> headers; nlohmann::json response_info_json = nlohmann::json::parse(response_info_string); if (response_info_json.contains("code") && response_info_json["code"].is_number()) @@ -2185,10 +2270,17 @@ std::pair> Network::process_v4_onion_respons else throw std::runtime_error{"Invalid JSON response, missing required code field."}; + if (response_info_json.contains("headers")) { + auto header_vals = response_info_json["headers"]; + + for (auto it = header_vals.begin(); it != header_vals.end(); ++it) + headers.emplace_back(it.key(), it.value()); + } + if (result_bencode.is_finished()) - return {status_code, std::nullopt}; + return {status_code, headers, std::nullopt}; - return {status_code, result_bencode.consume_string()}; + return {status_code, headers, result_bencode.consume_string()}; } // MARK: Error Handling @@ -2215,9 +2307,11 @@ std::pair Network::validate_response(quic::message resp, if (result_bencode.is_finished() || !result_bencode.is_string()) throw status_code_exception{ status_code, + {content_type_plain_text}, "Request failed with status code: " + std::to_string(status_code)}; - throw status_code_exception{status_code, result_bencode.consume_string()}; + throw status_code_exception{ + status_code, {content_type_plain_text}, result_bencode.consume_string()}; } // Can't convert the data to a string so just return the response body itself @@ -2226,10 +2320,12 @@ std::pair Network::validate_response(quic::message resp, // Default to a 200 success if the response is empty but didn't timeout or error int16_t status_code = 200; + std::pair content_type; std::string response_string; try { nlohmann::json response_json = nlohmann::json::parse(body); + content_type = content_type_json; if (response_json.is_array() && response_json.size() == 2) { status_code = response_json[0].get(); @@ -2238,10 +2334,11 @@ std::pair Network::validate_response(quic::message resp, response_string = body; } catch (...) { response_string = body; + content_type = content_type_plain_text; } if (status_code < 200 || status_code > 299) - throw status_code_exception{status_code, response_string}; + throw status_code_exception{status_code, {content_type}, response_string}; return {status_code, response_string}; } @@ -2289,11 +2386,13 @@ void Network::handle_errors( request_info info, connection_info conn_info, bool timeout_, - std::optional status_code_, + int16_t status_code_, + std::vector> headers_, std::optional response, std::optional handle_response) { bool timeout = timeout_; - auto status_code = status_code_.value_or(-1); + auto status_code = status_code_; + auto headers = headers_; auto path_name = path_type_name(info.path_type, single_path_mode); // There is an issue which can occur where we get invalid data back and are unable to decrypt @@ -2321,6 +2420,7 @@ void Network::handle_errors( if (status_code == -1 && response) { const std::unordered_map> response_map = { {"400 Bad Request", {400, false}}, + {"403 Forbidden", {403, false}}, {"500 Internal Server Error", {500, false}}, {"502 Bad Gateway", {502, false}}, {"503 Service Unavailable", {503, false}}, @@ -2352,7 +2452,7 @@ void Network::handle_errors( // the server side and don't update the path/snode state if (!info.node_destination && timeout) { if (handle_response) - return (*handle_response)(false, true, status_code, response); + return (*handle_response)(false, true, status_code, headers, response); return; } @@ -2362,7 +2462,7 @@ void Network::handle_errors( case 400: case 404: if (handle_response) - return (*handle_response)(false, false, status_code, response); + return (*handle_response)(false, false, status_code, headers, response); return; // The user's clock is out of sync with the service node network (a @@ -2370,7 +2470,7 @@ void Network::handle_errors( case 406: case 425: if (handle_response) - return (*handle_response)(false, false, status_code, response); + return (*handle_response)(false, false, status_code, headers, response); return; // The snode is reporting that it isn't associated with the given public key anymore. If @@ -2439,12 +2539,18 @@ void Network::handle_errors( swarm_pubkey = info.swarm_pubkey, info, status_code, + headers, response, cb = std::move( *handle_response)]() { get_swarm( *swarm_pubkey, - [this, info, status_code, response, cb = std::move(cb)]( + [this, + info, + status_code, + headers, + response, + cb = std::move(cb)]( swarm_id_t, std::vector swarm) { auto target = detail::node_for_destination(info.destination); @@ -2469,7 +2575,7 @@ void Network::handle_errors( "another 421 and had no other nodes in the " "swarm.", info.request_id); - return cb(false, false, status_code, response); + return cb(false, false, status_code, headers, response); } auto updated_info = info; @@ -2513,7 +2619,7 @@ void Network::handle_errors( // state if (!info.node_destination) { if (handle_response) - return (*handle_response)(false, timeout, status_code, response); + return (*handle_response)(false, timeout, status_code, headers, response); return; } break; @@ -2554,7 +2660,7 @@ void Network::handle_errors( path_name); if (handle_response) - (*handle_response)(false, timeout, status_code, response); + (*handle_response)(false, timeout, status_code, headers, response); return; } path = path_pending_drop_it->first; @@ -2662,7 +2768,7 @@ void Network::handle_errors( } if (handle_response) - (*handle_response)(false, timeout, status_code, response); + (*handle_response)(false, timeout, status_code, headers, response); } } // namespace session::network @@ -2849,21 +2955,43 @@ LIBSESSION_C_API void network_send_onion_request_to_snode_destination( bool success, bool timeout, int status_code, + std::vector> headers, std::optional response) { + std::vector cHeaders; + std::vector cHeaderValues; + cHeaders.reserve(headers.size()); + cHeaderValues.reserve(headers.size()); + + for (const auto& [header, value] : headers) { + cHeaders.push_back(header.c_str()); + cHeaderValues.push_back(value.c_str()); + } + if (response) cb(success, timeout, status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), (*response).c_str(), (*response).size(), ctx); else - cb(success, timeout, status_code, nullptr, 0, ctx); + cb(success, + timeout, + status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), + nullptr, + 0, + ctx); }, std::chrono::milliseconds{request_timeout_ms}, request_and_path_build_timeout); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, nullptr, nullptr, 0, e.what(), std::strlen(e.what()), ctx); } } @@ -2897,21 +3025,43 @@ LIBSESSION_C_API void network_send_onion_request_to_server_destination( bool success, bool timeout, int status_code, + std::vector> headers, std::optional response) { + std::vector cHeaders; + std::vector cHeaderValues; + cHeaders.reserve(headers.size()); + cHeaderValues.reserve(headers.size()); + + for (const auto& [header, value] : headers) { + cHeaders.push_back(header.c_str()); + cHeaderValues.push_back(value.c_str()); + } + if (response) cb(success, timeout, status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), (*response).c_str(), (*response).size(), ctx); else - cb(success, timeout, status_code, nullptr, 0, ctx); + cb(success, + timeout, + status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), + nullptr, + 0, + ctx); }, std::chrono::milliseconds{request_timeout_ms}, request_and_path_build_timeout); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, nullptr, nullptr, 0, e.what(), std::strlen(e.what()), ctx); } } @@ -2946,21 +3096,43 @@ LIBSESSION_C_API void network_upload_to_server( bool success, bool timeout, int status_code, + std::vector> headers, std::optional response) { + std::vector cHeaders; + std::vector cHeaderValues; + cHeaders.reserve(headers.size()); + cHeaderValues.reserve(headers.size()); + + for (const auto& [header, value] : headers) { + cHeaders.push_back(header.c_str()); + cHeaderValues.push_back(value.c_str()); + } + if (response) cb(success, timeout, status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), (*response).c_str(), (*response).size(), ctx); else - cb(success, timeout, status_code, nullptr, 0, ctx); + cb(success, + timeout, + status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), + nullptr, + 0, + ctx); }, std::chrono::milliseconds{request_timeout_ms}, request_and_path_build_timeout); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, nullptr, nullptr, 0, e.what(), std::strlen(e.what()), ctx); } } @@ -2986,21 +3158,43 @@ LIBSESSION_C_API void network_download_from_server( bool success, bool timeout, int status_code, + std::vector> headers, std::optional response) { + std::vector cHeaders; + std::vector cHeaderValues; + cHeaders.reserve(headers.size()); + cHeaderValues.reserve(headers.size()); + + for (const auto& [header, value] : headers) { + cHeaders.push_back(header.c_str()); + cHeaderValues.push_back(value.c_str()); + } + if (response) cb(success, timeout, status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), (*response).c_str(), (*response).size(), ctx); else - cb(success, timeout, status_code, nullptr, 0, ctx); + cb(success, + timeout, + status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), + nullptr, + 0, + ctx); }, std::chrono::milliseconds{request_timeout_ms}, request_and_path_build_timeout); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, nullptr, nullptr, 0, e.what(), std::strlen(e.what()), ctx); } } @@ -3027,21 +3221,43 @@ LIBSESSION_C_API void network_get_client_version( bool success, bool timeout, int status_code, + std::vector> headers, std::optional response) { + std::vector cHeaders; + std::vector cHeaderValues; + cHeaders.reserve(headers.size()); + cHeaderValues.reserve(headers.size()); + + for (const auto& [header, value] : headers) { + cHeaders.push_back(header.c_str()); + cHeaderValues.push_back(value.c_str()); + } + if (response) cb(success, timeout, status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), (*response).c_str(), (*response).size(), ctx); else - cb(success, timeout, status_code, nullptr, 0, ctx); + cb(success, + timeout, + status_code, + cHeaders.data(), + cHeaderValues.data(), + headers.size(), + nullptr, + 0, + ctx); }, std::chrono::milliseconds{request_timeout_ms}, request_and_path_build_timeout); } catch (const std::exception& e) { - callback(false, false, -1, e.what(), std::strlen(e.what()), ctx); + callback(false, false, -1, nullptr, nullptr, 0, e.what(), std::strlen(e.what()), ctx); } } diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 17230f70..87972988 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -56,7 +56,15 @@ TEST_CASE("Logging callbacks", "[logging]") { } log::critical(log::Cat("test.a"), "abc {}", 21 * 2); +#if defined(__APPLE__) && defined(__clang__) +#else + int line0 = __LINE__ - 1; +#endif log::info(log::Cat("test.b"), "hi"); +#if defined(__APPLE__) && defined(__clang__) +#else + int line1 = __LINE__ - 1; +#endif oxen::log::clear_sinks(); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 9c54950d..3a69747e 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -19,6 +19,7 @@ struct Result { bool success; bool timeout; int16_t status_code; + std::vector> headers; std::optional response; }; @@ -161,7 +162,12 @@ class TestNetwork : public Network { void add_pending_request(PathType path_type, request_info info) { request_queue[path_type].emplace_back( - std::move(info), [](bool, bool, int16_t, std::optional) {}); + std::move(info), + [](bool, + bool, + int16_t, + std::vector>, + std::optional) {}); } // Overridden Functions @@ -262,12 +268,19 @@ class TestNetwork : public Network { request_info info, connection_info conn_info, bool timeout, - std::optional status_code, + int16_t status_code, + std::vector> headers, std::optional response, std::optional handle_response) override { call_counts["handle_errors"]++; Network::handle_errors( - info, conn_info, timeout, status_code, response, std::move(handle_response)); + info, + conn_info, + timeout, + status_code, + headers, + response, + std::move(handle_response)); } // Mocking Functions @@ -356,13 +369,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, code, + {}, std::nullopt, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); @@ -385,13 +400,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 500, + {}, std::nullopt, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); @@ -413,13 +430,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 500, + {}, std::nullopt, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); @@ -445,13 +464,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 500, + {}, response, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); @@ -475,13 +496,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 421, + {}, std::nullopt, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); @@ -516,8 +539,13 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 421, + {}, std::nullopt, - [](bool, bool, int16_t, std::optional) {}); + [](bool, + bool, + int16_t, + std::vector>, + std::optional) {}); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); REQUIRE(network->last_request_info.has_value()); CHECK(node_for_destination(network->last_request_info->destination) != @@ -548,8 +576,13 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 421, + {}, std::nullopt, - [](bool, bool, int16_t, std::optional) {}); + [](bool, + bool, + int16_t, + std::vector>, + std::optional) {}); CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it @@ -576,13 +609,15 @@ TEST_CASE("Network error handling", "[network]") { {target, nullptr, nullptr, nullptr}, false, 421, + {}, std::nullopt, [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); @@ -622,14 +657,16 @@ TEST_CASE("Network error handling", "[network]") { mock_request5, {target, nullptr, nullptr, nullptr}, true, - std::nullopt, + -1, + {}, "Test", [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); CHECK(result.timeout); @@ -648,14 +685,16 @@ TEST_CASE("Network error handling", "[network]") { mock_request4, {target, nullptr, nullptr, nullptr}, false, - std::nullopt, + -1, + {}, "500 Internal Server Error", [&result]( bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result = {success, timeout, status_code, response}; + result = {success, timeout, status_code, headers, response}; }); CHECK_FALSE(result.success); CHECK_FALSE(result.timeout); @@ -1052,7 +1091,11 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { test_service_node, ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, std::nullopt, - [](bool, bool, int16_t, std::optional) {}, + [](bool, + bool, + int16_t, + std::vector>, + std::optional) {}, oxen::quic::DEFAULT_TIMEOUT, std::nullopt); CHECK(ALWAYS(300ms, network->did_not_call("check_request_queue_timeouts"))); @@ -1065,7 +1108,11 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { test_service_node, ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}, std::nullopt, - [](bool, bool, int16_t, std::optional) {}, + [](bool, + bool, + int16_t, + std::vector>, + std::optional) {}, oxen::quic::DEFAULT_TIMEOUT, oxen::quic::DEFAULT_TIMEOUT); CHECK(EVENTUALLY(300ms, network->called("check_request_queue_timeouts"))); @@ -1081,8 +1128,9 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { [&prom](bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - prom.set_value({success, timeout, status_code, response}); + prom.set_value({success, timeout, status_code, headers, response}); }, oxen::quic::DEFAULT_TIMEOUT, 100ms); @@ -1111,7 +1159,7 @@ TEST_CASE("Network requests", "[network][send_request]") { [&prom, &network, test_service_node]( connection_info info, std::optional error) { if (!info.is_valid()) - return prom.set_value({false, false, -1, error.value_or("Unknown Error")}); + return prom.set_value({false, false, -1, {}, error.value_or("Unknown Error")}); network.send_request( request_info::make( @@ -1127,8 +1175,9 @@ TEST_CASE("Network requests", "[network][send_request]") { [&prom](bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - prom.set_value({success, timeout, status_code, response}); + prom.set_value({success, timeout, status_code, headers, response}); }); }); @@ -1140,7 +1189,7 @@ TEST_CASE("Network requests", "[network][send_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); @@ -1166,8 +1215,9 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { bool success, bool timeout, int16_t status_code, + std::vector> headers, std::optional response) { - result_promise.set_value({success, timeout, status_code, response}); + result_promise.set_value({success, timeout, status_code, headers, response}); }, oxen::quic::DEFAULT_TIMEOUT, oxen::quic::DEFAULT_TIMEOUT); @@ -1180,7 +1230,7 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); @@ -1212,12 +1262,28 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { [](bool success, bool timeout, int16_t status_code, + const char** headers, + const char** header_values, + size_t headers_size, const char* c_response, size_t response_size, void* ctx) { auto result_promise = static_cast*>(ctx); auto response_str = std::string(c_response, response_size); - result_promise->set_value({success, timeout, status_code, response_str}); + std::vector> header_pairs; + header_pairs.reserve(headers_size); + + for (size_t i = 0; i < headers_size; ++i) { + if (headers[i] == nullptr) + continue; // Skip null entries + if (header_values[i] == nullptr) + continue; // Skip null entries + + header_pairs.emplace_back(headers[i], header_values[i]); + } + + result_promise->set_value( + {success, timeout, status_code, header_pairs, response_str}); }, static_cast(result_promise.get())); @@ -1229,7 +1295,7 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(nlohmann::json::parse(*result.response)); + REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); From 1188aaf8b93bea048b4ecfc88b4fd257c554d2d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 15:10:39 +1000 Subject: [PATCH 397/572] Tweaks to try to fix CI errors --- src/session_encrypt.cpp | 8 ++++++-- tests/test_network.cpp | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 50758103..56fea158 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -40,9 +40,13 @@ namespace detail { // milliseconds since epoch then treated as an integer value. template , int> = 0> std::string_view to_hashable(const T& val, char*& buffer) { + int length = std::snprintf(buffer, 20, "%d", val); + + if (length < 0) length = 0; + if (length > 20) length = 20; auto [p, ec] = std::to_chars(buffer, buffer + 20, val); - std::string_view s(buffer, p - buffer); - buffer = p; + std::string_view s(buffer, length); + buffer += length; return s; } inline std::string_view to_hashable( diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 3a69747e..1f3e48f3 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1189,7 +1189,7 @@ TEST_CASE("Network requests", "[network][send_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); + REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); @@ -1230,7 +1230,7 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); + REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); @@ -1295,7 +1295,7 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { CHECK(result.status_code == 200); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); - REQUIRE_NOTHROW(static_cast(nlohmann::json::parse(*result.response))); + REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); CHECK(response.contains("hf")); From 2f81c6985cae2d5e4347ab5b32ab24790a815043 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 15:57:10 +1000 Subject: [PATCH 398/572] Another tweak for CI errors --- src/session_encrypt.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 56fea158..cc617c86 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "session/blinding.hpp" @@ -40,13 +41,13 @@ namespace detail { // milliseconds since epoch then treated as an integer value. template , int> = 0> std::string_view to_hashable(const T& val, char*& buffer) { - int length = std::snprintf(buffer, 20, "%d", val); + std::ostringstream ss; + ss << val; - if (length < 0) length = 0; - if (length > 20) length = 20; - auto [p, ec] = std::to_chars(buffer, buffer + 20, val); - std::string_view s(buffer, length); - buffer += length; + std::string str = ss.str(); + std::copy(str.begin(), str.end(), buffer); + std::string_view s(buffer, str.length()); + buffer += str.length(); return s; } inline std::string_view to_hashable( From 6bc86ad553470c733d2b3af5ce22e6a7a8ead81d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 17:25:56 +1000 Subject: [PATCH 399/572] Further tweaks to make CI happy --- tests/test_config_userprofile.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 45872b1b..db63c171 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -355,15 +355,15 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Since only one of them set a profile pic there should be no conflict there: pic = user_profile_get_pic(conf); - REQUIRE(pic.url); + REQUIRE(pic.url != nullptr); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); + REQUIRE(pic.key != nullptr); CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); - REQUIRE(pic.url); + REQUIRE(pic.url != nullptr); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); + REQUIRE(pic.key != nullptr); CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); From 6a1ab5168ff2cd2f967d804ea6e1130a939f2189 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 17:37:21 +1000 Subject: [PATCH 400/572] More CI platform specific tweaks --- tests/test_config_userprofile.cpp | 16 ++++++++++++++++ tests/test_logging.cpp | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index db63c171..aabc94af 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -355,15 +355,31 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Since only one of them set a profile pic there should be no conflict there: pic = user_profile_get_pic(conf); +#if defined(__APPLE__) && defined(__clang__) + REQUIRE(pic.url); +#else REQUIRE(pic.url != nullptr); +#endif CHECK(pic.url == "http://new.example.com/pic"sv); +#if defined(__APPLE__) && defined(__clang__) + REQUIRE(pic.key); +#else REQUIRE(pic.key != nullptr); +#endif CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); +#if defined(__APPLE__) && defined(__clang__) + REQUIRE(pic.url); +#else REQUIRE(pic.url != nullptr); +#endif CHECK(pic.url == "http://new.example.com/pic"sv); +#if defined(__APPLE__) && defined(__clang__) + REQUIRE(pic.key); +#else REQUIRE(pic.key != nullptr); +#endif CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 87972988..a52920f4 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -58,12 +58,12 @@ TEST_CASE("Logging callbacks", "[logging]") { log::critical(log::Cat("test.a"), "abc {}", 21 * 2); #if defined(__APPLE__) && defined(__clang__) #else - int line0 = __LINE__ - 1; + int line0 = __LINE__ - 3; #endif log::info(log::Cat("test.b"), "hi"); #if defined(__APPLE__) && defined(__clang__) #else - int line1 = __LINE__ - 1; + int line1 = __LINE__ - 3; #endif oxen::log::clear_sinks(); From 12ed513d4fed91ba2ff1fda07b3ae62101d4baa5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Sep 2024 17:48:00 +1000 Subject: [PATCH 401/572] Another tweak for CI test build issues --- tests/test_config_userprofile.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index aabc94af..49b8126c 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -355,13 +355,13 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Since only one of them set a profile pic there should be no conflict there: pic = user_profile_get_pic(conf); -#if defined(__APPLE__) && defined(__clang__) +#if defined(__APPLE__) REQUIRE(pic.url); #else REQUIRE(pic.url != nullptr); #endif CHECK(pic.url == "http://new.example.com/pic"sv); -#if defined(__APPLE__) && defined(__clang__) +#if defined(__APPLE__) REQUIRE(pic.key); #else REQUIRE(pic.key != nullptr); @@ -369,13 +369,13 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); -#if defined(__APPLE__) && defined(__clang__) +#if defined(__APPLE__) REQUIRE(pic.url); #else REQUIRE(pic.url != nullptr); #endif CHECK(pic.url == "http://new.example.com/pic"sv); -#if defined(__APPLE__) && defined(__clang__) +#if defined(__APPLE__) REQUIRE(pic.key); #else REQUIRE(pic.key != nullptr); From f3f273c6f64450f16f1f3240843a76a8ed86bac5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 10 Sep 2024 12:08:38 +1000 Subject: [PATCH 402/572] Fixed a cache refresh bug, logging tweaks, CI build tweaks --- src/network.cpp | 29 ++++++++++++++++++++--------- tests/test_config_userprofile.cpp | 8 ++++---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index 0066fb7d..30602a22 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1208,10 +1208,12 @@ void Network::refresh_snode_cache(std::optional existing_request_id return; } - // We are starting a new cache refresh so store an identifier for it + // We are starting a new cache refresh so store an identifier for it (we also initialise + // `snode_refresh_results` so we can use it to track the results from the different requests) if (!current_snode_cache_refresh_request_id) { log::info(cat, "Refreshing snode cache ({}).", request_id); current_snode_cache_refresh_request_id = request_id; + snode_refresh_results = std::make_shared>>(); } // If we don't have enough nodes in the unused nodes then refresh it @@ -1299,8 +1301,15 @@ void Network::refresh_snode_cache(std::optional existing_request_id } // If we haven't received all results then do nothing - if (snode_refresh_results->size() != num_snodes_to_refresh_cache_from) + if (snode_refresh_results->size() != num_snodes_to_refresh_cache_from) { + log::info( + cat, + "Received snode cache refresh result {}/{} ({}).", + snode_refresh_results->size(), + num_snodes_to_refresh_cache_from, + request_id); return; + } auto any_nodes_request_failed = std::any_of( snode_refresh_results->begin(), @@ -2350,14 +2359,15 @@ void Network::drop_path_when_empty(std::string request_id, PathType path_type, o paths[path_type].end()); log::info( cat, - "Flagging path to be dropped, now have {} {} paths(s) ({}): [{}].", + "Flagging path to be dropped [{}], now have {} {} paths(s) ({}).", + path.to_string(), paths[path_type].size(), path_type_name(path_type, single_path_mode), - request_id, - path.to_string()); + request_id); - // Clear any paths which are waiting to be dropped - clear_empty_pending_path_drops(); + // Clear any paths which are waiting to be dropped (do this in the next loop to avoid confusing + // logs where the logs from `clear_empty_pending_path_drops` could appear before the above log) + net.call_soon([this]() { clear_empty_pending_path_drops(); }); } void Network::clear_empty_pending_path_drops() { @@ -2367,9 +2377,10 @@ void Network::clear_empty_pending_path_drops() { if (!path_info.first.has_pending_requests()) { log::info( cat, - "Removing flagged {} path: {}: [{}].", + "Removing flagged {} path that {}: [{}].", path_type_name(path_info.second, single_path_mode), - (path_info.first.is_valid() ? "No remaining requests" : "No longer valid"), + (path_info.first.is_valid() ? "has no remaining requests" + : "is no longer valid"), path_info.first.to_string()); return true; } diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 49b8126c..ade57c94 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -355,13 +355,13 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Since only one of them set a profile pic there should be no conflict there: pic = user_profile_get_pic(conf); -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) REQUIRE(pic.url); #else REQUIRE(pic.url != nullptr); #endif CHECK(pic.url == "http://new.example.com/pic"sv); -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) REQUIRE(pic.key); #else REQUIRE(pic.key != nullptr); @@ -369,13 +369,13 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(to_hex(ustring_view{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) REQUIRE(pic.url); #else REQUIRE(pic.url != nullptr); #endif CHECK(pic.url == "http://new.example.com/pic"sv); -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) REQUIRE(pic.key); #else REQUIRE(pic.key != nullptr); From c057cd9af37d7447c7be22ff94c5b8dffed2d1dd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 12 Sep 2024 11:02:12 +1000 Subject: [PATCH 403/572] Fixed a bug with onion_path pending requests func and logging tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed a bug and simplified the 'has_pending_requests' func • Tweaked logs to better distinguish between request ids --- include/session/network.hpp | 22 ++--- src/network.cpp | 161 ++++++++++++++++++++---------------- tests/test_network.cpp | 51 +++++++----- 3 files changed, 130 insertions(+), 104 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 5585c245..386cfd60 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -89,9 +89,7 @@ struct connection_info { std::shared_ptr stream; bool is_valid() const { return conn && stream && !stream->is_closing(); }; - bool has_pending_requests() const { - return is_valid() && (!pending_requests || ((*pending_requests) == 0)); - }; + bool has_pending_requests() const { return (pending_requests && (*pending_requests) > 0); }; void add_pending_request() { if (!pending_requests) @@ -109,6 +107,7 @@ struct connection_info { }; struct onion_path { + std::string id; connection_info conn_info; std::vector nodes; uint8_t failure_count; @@ -516,14 +515,14 @@ class Network { /// established (or closed in case it fails). /// /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. + /// - 'id' - [in] id for the request or path build which triggered the call. /// - `target` -- [in] the target service node to connect to. /// - `timeout` -- [in, optional] optional timeout for the request, if NULL the /// `quic::DEFAULT_HANDSHAKE_TIMEOUT` will be used. /// - `callback` -- [in] callback to be called with connection info once the connection is /// established or fails. void establish_connection( - std::string request_id, + std::string id, service_node target, std::optional timeout, std::function error)> callback); @@ -534,8 +533,8 @@ class Network { /// list. /// /// Inputs: - /// - 'request_id' - [in] id for the request which triggered the call. - virtual void establish_and_store_connection(std::string request_id); + /// - 'path_id' - [in] id for the path build which triggered the call. + virtual void establish_and_store_connection(std::string path_id); /// API: network/refresh_snode_cache_complete /// @@ -577,9 +576,9 @@ class Network { /// this will open a new connection to a random service nodes in the snode cache. /// /// Inputs: + /// - 'path_id' - [in] id for the new path. /// - `path_type` -- [in] the type of path to build. - /// - 'request_id' - [in] id for the build_path request. - virtual void build_path(PathType path_type, std::string request_id); + virtual void build_path(std::string path_id, PathType path_type); /// API: network/find_valid_path /// @@ -707,10 +706,11 @@ class Network { /// Flags a path to be dropped once all pending requests have finished. /// /// Inputs: - /// - `request_id` -- [in] the request_id which triggered the path drop. + /// - `id` -- [in] id the request or path which triggered the path drop (if the id is a path_id + /// then the drop was triggered by the connection being dropped). /// - `path_type` -- [in] the type of path to build. /// - `path` -- [in] the path to be dropped. - void drop_path_when_empty(std::string request_id, PathType path_type, onion_path path); + void drop_path_when_empty(std::string id, PathType path_type, onion_path path); /// API: network/clear_empty_pending_path_drops /// diff --git a/src/network.cpp b/src/network.cpp index 30602a22..b5441637 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -484,7 +484,7 @@ request_info request_info::make( std::optional _ep, std::optional _body) { return request_info{ - _req_id.value_or(random::random_base32(4)), + _req_id.value_or("R-{}"_format(random::random_base32(4))), std::move(_dest), _ep.value_or(ONION), std::move(_body), @@ -526,9 +526,9 @@ Network::Network( // Kick off a separate thread to build paths (may as well kick this off early) if (pre_build_paths) for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) { - auto request_id = random::random_base32(4); - in_progress_path_builds[request_id] = PathType::standard; - net.call_soon([this, request_id] { build_path(PathType::standard, request_id); }); + auto path_id = "P-{}"_format(random::random_base32(4)); + in_progress_path_builds[path_id] = PathType::standard; + net.call_soon([this, path_id] { build_path(path_id, PathType::standard); }); } } @@ -852,11 +852,11 @@ std::vector Network::get_unused_nodes() { } void Network::establish_connection( - std::string request_id, + std::string id, service_node target, std::optional timeout, std::function error)> callback) { - log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); + log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, id); auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); // If the network is currently suspended then don't try to open a connection @@ -881,9 +881,8 @@ void Network::establish_connection( creds, quic::opt::keep_alive{10s}, handshake_timeout, - [this, request_id, target, cb, cb_called, conn_future]( - quic::connection_interface&) mutable { - log::trace(cat, "Connection established for {}.", request_id); + [this, id, target, cb, cb_called, conn_future](quic::connection_interface&) mutable { + log::trace(cat, "Connection established for {}.", id); // Just in case, call it within a `net.call` net.call([&] { @@ -900,9 +899,16 @@ void Network::establish_connection( }); }); }, - [this, target, request_id, cb, cb_called, conn_future]( + [this, target, id, cb, cb_called, conn_future]( quic::connection_interface& conn, uint64_t error_code) mutable { - log::trace(cat, "Connection closed for {}.", request_id); + if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) + log::info( + cat, + "Unable to establish connection to {} for {}.", + target.to_string(), + id); + else + log::info(cat, "Connection to {} closed for {}.", target.to_string(), id); // Just in case, call it within a `net.call` net.call([&] { @@ -935,7 +941,7 @@ void Network::establish_connection( if (!path.nodes.empty() && path.nodes.front() == target && path.conn_info.conn && conn.reference_id() == path.conn_info.conn->reference_id()) { - drop_path_when_empty(request_id, path_type, path); + drop_path_when_empty(id, path_type, path); break; } } @@ -957,7 +963,7 @@ void Network::establish_connection( conn_promise.set_value(c); } -void Network::establish_and_store_connection(std::string request_id) { +void Network::establish_and_store_connection(std::string path_id) { // If we are suspended then don't try to establish a new connection if (suspended) return; @@ -976,8 +982,8 @@ void Network::establish_and_store_connection(std::string request_id) { cat, "Unable to establish new connection due to lack of unused nodes, refreshing snode " "cache ({}).", - request_id); - return net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); + path_id); + return net.call_soon([this, path_id]() { refresh_snode_cache(path_id); }); } // Otherwise check if it's been too long since the last cache update and, if so, trigger a @@ -998,14 +1004,14 @@ void Network::establish_and_store_connection(std::string request_id) { // Try to establish a new connection to the target (this has a 3s handshake timeout as we // wouldn't want to use any nodes which take longer than that anyway) - log::info(cat, "Establishing connection to {} for {}.", target_node.to_string(), request_id); - in_progress_connections.emplace(request_id, target_node); + log::info(cat, "Establishing connection to {} for {}.", target_node.to_string(), path_id); + in_progress_connections.emplace(path_id, target_node); establish_connection( - request_id, + path_id, target_node, 3s, - [this, target_node, request_id](connection_info info, std::optional) { + [this, target_node, path_id](connection_info info, std::optional) { // If we failed to get a connection then try again after a delay (may as well try // indefinitely because there is no way to recover from this issue) if (!info.is_valid()) { @@ -1016,21 +1022,20 @@ void Network::establish_and_store_connection(std::string request_id) { "Failed to connect to {}, will try another after {}ms.", target_node.to_string(), connection_retry_delay.count()); - return net.call_later(connection_retry_delay, [this, request_id]() { - establish_and_store_connection(request_id); + return net.call_later(connection_retry_delay, [this, path_id]() { + establish_and_store_connection(path_id); }); } // We were able to connect to the node so add it to the unused_connections queue - log::info( - cat, "Connection to {} valid for {}.", target_node.to_string(), request_id); + log::info(cat, "Connection to {} valid for {}.", target_node.to_string(), path_id); unused_connections.emplace_back(info); // Kick off the next pending path build since we now have a valid connection if (!path_build_queue.empty()) { - in_progress_path_builds[request_id] = path_build_queue.front(); - net.call_soon([this, path_type = path_build_queue.front(), request_id]() { - build_path(path_type, request_id); + in_progress_path_builds[path_id] = path_build_queue.front(); + net.call_soon([this, path_type = path_build_queue.front(), path_id]() { + build_path(path_id, path_type); }); path_build_queue.pop_front(); } @@ -1041,7 +1046,8 @@ void Network::establish_and_store_connection(std::string request_id) { if (!path_build_queue.empty() && in_progress_connections.empty()) for ([[maybe_unused]] const auto& _ : path_build_queue) net.call_soon([this]() { - establish_and_store_connection(random::random_base32(4)); + auto conn_id = "EC-{}"_format(random::random_base32(4)); + establish_and_store_connection(conn_id); }); }); } @@ -1086,9 +1092,9 @@ void Network::refresh_snode_cache_complete(std::vector nodes) { // Resume any queued path builds for (const auto& path_type : path_build_queue) { - auto request_id = random::random_base32(4); - in_progress_path_builds[request_id] = path_type; - net.call_soon([this, path_type, request_id]() { build_path(path_type, request_id); }); + auto path_id = "P-{}"_format(random::random_base32(4)); + in_progress_path_builds[path_id] = path_type; + net.call_soon([this, path_type, path_id]() { build_path(path_id, path_type); }); } path_build_queue.clear(); } @@ -1188,7 +1194,7 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r } void Network::refresh_snode_cache(std::optional existing_request_id) { - auto request_id = existing_request_id.value_or(random::random_base32(4)); + auto request_id = existing_request_id.value_or("RSC-{}"_format(random::random_base32(4))); if (suspended) { log::info(cat, "Ignoring snode cache refresh as network is suspended ({}).", request_id); @@ -1357,7 +1363,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id }); } -void Network::build_path(PathType path_type, std::string request_id) { +void Network::build_path(std::string path_id, PathType path_type) { if (suspended) { log::info(cat, "Ignoring build_path call as network is suspended."); return; @@ -1370,12 +1376,12 @@ void Network::build_path(PathType path_type, std::string request_id) { if (unused_connections.empty()) { log::info( cat, - "No unused connections available to build {} path, creating new connection ({}).", + "No unused connections available to build {} path, creating new connection for {}.", path_name, - request_id); + path_id); path_build_queue.emplace_back(path_type); - in_progress_path_builds.erase(request_id); - return net.call_soon([this, request_id]() { establish_and_store_connection(request_id); }); + in_progress_path_builds.erase(path_id); + return net.call_soon([this, path_id]() { establish_and_store_connection(path_id); }); } // Reset the unused nodes list if it's too small @@ -1385,19 +1391,16 @@ void Network::build_path(PathType path_type, std::string request_id) { // If we still don't have enough unused nodes then we need to refresh the cache if (unused_nodes.size() < path_size) { log::info( - cat, - "Re-queing {} path build due to insufficient nodes ({}).", - path_name, - request_id); + cat, "Re-queing {} path build due to insufficient nodes ({}).", path_name, path_id); path_build_failures = 0; path_build_queue.emplace_back(path_type); - in_progress_path_builds.erase(request_id); + in_progress_path_builds.erase(path_id); return net.call_soon([this]() { refresh_snode_cache(); }); } // Build the path - log::info(cat, "Building {} path ({}).", path_name, request_id); - in_progress_path_builds[request_id] = path_type; + log::info(cat, "Building {} path ({}).", path_name, path_id); + in_progress_path_builds[path_id] = path_type; auto conn_info = std::move(unused_connections.front()); unused_connections.pop_front(); @@ -1410,14 +1413,13 @@ void Network::build_path(PathType path_type, std::string request_id) { cat, "Unable to build {} path due to lack of suitable unused nodes ({}).", path_name, - request_id); + path_id); // Delay the next path build attempt based on the error we received path_build_failures++; unused_connections.push_front(std::move(conn_info)); auto delay = retry_delay(path_build_failures); - net.call_later( - delay, [this, request_id, path_type]() { build_path(path_type, request_id); }); + net.call_later(delay, [this, path_id, path_type]() { build_path(path_id, path_type); }); return; } @@ -1436,18 +1438,18 @@ void Network::build_path(PathType path_type, std::string request_id) { } // Store the new path - auto path = onion_path{std::move(conn_info), path_nodes, 0}; + auto path = onion_path{path_id, std::move(conn_info), path_nodes, 0}; paths[path_type].emplace_back(path); - in_progress_path_builds.erase(request_id); + in_progress_path_builds.erase(path_id); // Log that a path was built log::info( cat, - "Built new onion request path, now have {} {} path(s) ({}): [{}]", + "Built new onion request path [{}], now have {} {} path(s) ({}).", + path.to_string(), paths[path_type].size(), path_name, - request_id, - path.to_string()); + path_id); // If the connection info is valid and it's a standard path then update the // connection status to connected @@ -1488,10 +1490,10 @@ void Network::build_path(PathType path_type, std::string request_id) { // If there are still pending requests and there are no pending path builds for them then kick // off a subsequent path build in an effort to resume the remaining requests if (!request_queue[path_type].empty()) { - auto additional_request_id = random::random_base32(4); - in_progress_path_builds[additional_request_id] = path_type; - net.call_soon([this, path_type, additional_request_id] { - build_path(path_type, additional_request_id); + auto additional_path_id = "P-{}"_format(random::random_base32(4)); + in_progress_path_builds[additional_path_id] = path_type; + net.call_soon([this, path_type, additional_path_id] { + build_path(additional_path_id, path_type); }); } else request_queue.erase(path_type); @@ -1596,8 +1598,10 @@ void Network::build_path_if_needed(PathType path_type, bool found_path) { // If we don't have enough current + pending paths, or the request couldn't be sent then // kick off a new path build - if ((current_paths.size() + pending_paths) < min_paths || (!found_path && pending_paths == 0)) - build_path(path_type, random::random_base32(4)); + if ((current_paths.size() + pending_paths) < min_paths || (!found_path && pending_paths == 0)) { + auto path_id = "P-{}"_format(random::random_base32(4)); + build_path(path_id, path_type); + } } // MARK: Direct Requests @@ -1706,7 +1710,7 @@ void Network::get_swarm( void Network::get_random_nodes( uint16_t count, std::function nodes)> callback) { - auto request_id = random::random_base32(4); + auto request_id = "R-{}"_format(random::random_base32(4)); log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); net.call([this, request_id, count, cb = std::move(callback)]() mutable { @@ -1742,7 +1746,7 @@ void Network::check_request_queue_timeouts(std::optional request_ti // If there wasn't an existing loop id then set it here if (!request_timeout_id) - request_timeout_id = random::random_base32(4); + request_timeout_id = "RT-{}"_format(random::random_base32(4)); // Timeout and remove any pending requests which should timeout based on path build time auto has_remaining_timeout_requests = false; @@ -2352,21 +2356,30 @@ std::pair Network::validate_response(quic::message resp, return {status_code, response_string}; } -void Network::drop_path_when_empty(std::string request_id, PathType path_type, onion_path path) { +void Network::drop_path_when_empty(std::string id, PathType path_type, onion_path path) { paths_pending_drop.emplace_back(path, path_type); paths[path_type].erase( std::remove(paths[path_type].begin(), paths[path_type].end(), path), paths[path_type].end()); + + std::string reason; + if (id == path.id) + reason = "connection being closed"; + else + reason = "failure threshold passed with {} failure"_format(id); + log::info( cat, - "Flagging path to be dropped [{}], now have {} {} paths(s) ({}).", + "Flagging path {} [{}] to be dropped due to {}, now have {} {} paths(s).", + path.id, path.to_string(), + reason, paths[path_type].size(), - path_type_name(path_type, single_path_mode), - request_id); + path_type_name(path_type, single_path_mode)); - // Clear any paths which are waiting to be dropped (do this in the next loop to avoid confusing - // logs where the logs from `clear_empty_pending_path_drops` could appear before the above log) + // Clear any paths which are waiting to be dropped (do this after a short delay to avoid + // confusing logs where the logs from `clear_empty_pending_path_drops` could appear before the + // above log) net.call_soon([this]() { clear_empty_pending_path_drops(); }); } @@ -2377,8 +2390,9 @@ void Network::clear_empty_pending_path_drops() { if (!path_info.first.has_pending_requests()) { log::info( cat, - "Removing flagged {} path that {}: [{}].", + "Removing flagged {} path {} that {}: [{}].", path_type_name(path_info.second, single_path_mode), + path_info.first.id, (path_info.first.is_valid() ? "has no remaining requests" : "is no longer valid"), path_info.first.to_string()); @@ -2666,9 +2680,10 @@ void Network::handle_errors( if (path_pending_drop_it == paths_pending_drop.end()) { log::warning( cat, - "Request {} failed but {} path already dropped.", + "Request {} failed but {} path with guard {} already dropped.", info.request_id, - path_name); + path_name, + conn_info.node.to_string()); if (handle_response) (*handle_response)(false, timeout, status_code, headers, response); @@ -2729,18 +2744,20 @@ void Network::handle_errors( target_node); log::info( cat, - "Found bad node ({}) in {} path, replacing node.", + "Found bad node ({}) in {} path, replacing node ({}).", *ed25519PublicKey, - path_name); + path_name, + updated_path.id); } catch (...) { // There aren't enough unused nodes remaining so we need to drop the // path updated_path.failure_count = path_failure_threshold; log::info( cat, - "Unable to replace bad node ({}) in {} path.", + "Unable to replace bad node ({}) in {} path ({}).", *ed25519PublicKey, - path_name); + path_name, + updated_path.id); } } } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 1f3e48f3..f038fb4a 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -84,7 +84,8 @@ class TestNetwork : public Network { } void add_path(PathType path_type, std::vector nodes) { - paths[path_type].emplace_back(onion_path{{nodes[0], nullptr, nullptr, nullptr}, nodes, 0}); + paths[path_type].emplace_back( + onion_path{"Test", {nodes[0], nullptr, nullptr, nullptr}, nodes, 0}); } void set_paths(PathType path_type, std::vector paths_) { @@ -203,13 +204,13 @@ class TestNetwork : public Network { Network::refresh_snode_cache(existing_request_id); } - void build_path(PathType path_type, std::string request_id) override { + void build_path(std::string path_id, PathType path_type) override { const auto func_name = "build_path"; if (check_should_ignore_and_log_call(func_name)) return; - Network::build_path(path_type, request_id); + Network::build_path(path_id, path_type); } std::optional find_valid_path( @@ -339,7 +340,8 @@ TEST_CASE("Network error handling", "[network]") { auto target2 = test_node(ed_pk2, 1); auto target3 = test_node(ed_pk2, 2); auto target4 = test_node(ed_pk2, 3); - auto path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; + auto path = + onion_path{"Test", {target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto mock_request = request_info{ "AAAA", target, @@ -420,7 +422,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network->get_failure_count(PathType::standard, path) == 1); // // Check general error handling with no response (too many path failures) - path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; + path = onion_path{"Test", {target, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test network->ignore_calls_to("_send_onion_request", "update_disk_cache_throttled"); @@ -451,7 +453,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network->get_failure_count(PathType::standard, path) == 0); // Path dropped and reset // // Check general error handling with a path and specific node failure - path = onion_path{{target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; + path = onion_path{"Test", {target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test @@ -713,7 +715,8 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { for (uint16_t i = 0; i < 12; ++i) snode_cache.emplace_back(test_node(ed_pk, i)); auto invalid_info = connection_info{snode_cache[0], nullptr, nullptr, nullptr}; - auto path = onion_path{invalid_info, {snode_cache[0], snode_cache[1], snode_cache[2]}, 0}; + auto path = + onion_path{"Test", invalid_info, {snode_cache[0], snode_cache[1], snode_cache[2]}, 0}; // Should shuffle the result network.emplace(std::nullopt, true, false, false); @@ -800,14 +803,14 @@ TEST_CASE("Network Path Building", "[network][build_path]") { // Nothing should happen if the network is suspended network.emplace(std::nullopt, true, false, false); network->set_suspended(true); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); // If there are no unused connections it puts the path build in the queue and calls // establish_and_store_connection network.emplace(std::nullopt, true, false, false); network->ignore_calls_to("establish_and_store_connection"); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); @@ -816,7 +819,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); network->set_in_progress_connections({{"TestInProgress", snode_cache.front()}}); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(network->get_unused_nodes_value().size() == snode_cache.size() - 3); CHECK(network->get_path_build_queue().empty()); @@ -826,7 +829,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->set_unused_connections({invalid_info}); network->set_in_progress_connections({{"TestInProgress", snode_cache.front()}}); network->add_path(PathType::standard, {snode_cache.begin() + 1, snode_cache.begin() + 1 + 3}); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(network->get_unused_nodes_value().size() == (snode_cache.size() - 3 - 3)); CHECK(network->get_path_build_queue().empty()); @@ -838,7 +841,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->set_unused_connections({invalid_info}); network->set_path_build_failures(10); network->add_path(PathType::standard, snode_cache); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_failures() == 0); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); @@ -850,7 +853,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { network->set_unused_connections({invalid_info}); network->set_unused_nodes(std::vector{ snode_cache[0], snode_cache[0], snode_cache[0], snode_cache[0]}); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); network->ignore_calls_to("build_path"); // Ignore the 2nd loop CHECK(network->get_path_build_failures() == 1); CHECK(network->get_path_build_queue().empty()); @@ -860,7 +863,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { // status or call the 'paths_changed' hook network.emplace(std::nullopt, true, false, false); network->find_valid_path_response = - onion_path{invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; + onion_path{"Test", invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; network->ignore_calls_to("_send_onion_request"); network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); @@ -873,7 +876,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { 1s, std::nullopt, PathType::download)); - network->build_path(PathType::download, "Test1"); + network->build_path("Test1", PathType::download); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::download).size() == 1); @@ -881,7 +884,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { // and kicks of queued requests network.emplace(std::nullopt, true, false, false); network->find_valid_path_response = - onion_path{invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; + onion_path{"Test", invalid_info, {snode_cache.begin(), snode_cache.begin() + 3}, 0}; network->ignore_calls_to("_send_onion_request"); network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); @@ -894,7 +897,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { 1s, std::nullopt, PathType::standard)); - network->build_path(PathType::standard, "Test1"); + network->build_path("Test1", PathType::standard); CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::standard).size() == 1); CHECK(network->get_status() == ConnectionStatus::connected); @@ -913,7 +916,10 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { auto network = TestNetwork(std::nullopt, true, false, false); auto info = request_info::make(target, std::nullopt, std::nullopt, 0ms); auto invalid_path = onion_path{ - {test_service_node, nullptr, nullptr, nullptr}, {test_service_node}, uint8_t{0}}; + "Test", + {test_service_node, nullptr, nullptr, nullptr}, + {test_service_node}, + uint8_t{0}}; // It returns nothing when given no path options CHECK_FALSE(network.find_valid_path(info, {}).has_value()); @@ -936,7 +942,10 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { auto result = prom.get_future().get(); REQUIRE(result.first.is_valid()); auto valid_path = onion_path{ - std::move(result.first), std::vector{test_service_node}, uint8_t{0}}; + "Test", + std::move(result.first), + std::vector{test_service_node}, + uint8_t{0}}; // It excludes paths which include the IP of the target auto shared_ip_info = request_info::make(test_service_node, std::nullopt, std::nullopt, 0ms); @@ -956,8 +965,8 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { auto target = test_node(ed_pk, 0); ; std::optional network; - auto invalid_path = - onion_path{connection_info{target, nullptr, nullptr, nullptr}, {target}, uint8_t{0}}; + auto invalid_path = onion_path{ + "Test", connection_info{target, nullptr, nullptr, nullptr}, {target}, uint8_t{0}}; // It does not add additional path builds if there is already a path and it's in // 'single_path_mode' From f19df114e5f4f6c29112f49c0b4897d7b93b78f1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 12 Sep 2024 11:58:41 +1000 Subject: [PATCH 404/572] Reverted change causing test crashes on CI, tweak for 32-bit CI error --- src/network.cpp | 6 ++---- tests/test_blinding.cpp | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/network.cpp b/src/network.cpp index b5441637..c1af2aeb 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2377,10 +2377,8 @@ void Network::drop_path_when_empty(std::string id, PathType path_type, onion_pat paths[path_type].size(), path_type_name(path_type, single_path_mode)); - // Clear any paths which are waiting to be dropped (do this after a short delay to avoid - // confusing logs where the logs from `clear_empty_pending_path_drops` could appear before the - // above log) - net.call_soon([this]() { clear_empty_pending_path_drops(); }); + // Clear any paths which are waiting to be dropped + clear_empty_pending_path_drops(); } void Network::clear_empty_pending_path_drops() { diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 1fe320e6..4cd19198 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -329,8 +329,8 @@ TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") CHECK(session_id_matches_blinded_id(session_id2, b25_5, server_pks[4])); CHECK(session_id_matches_blinded_id(session_id1, b25_6, server_pks[5])); - auto invalid_session_id = "9" + session_id1.substr(1, 65); - auto invalid_blinded_id = "9" + b15_1.substr(1, 65); + auto invalid_session_id = "9"s + session_id1.substr(1); + auto invalid_blinded_id = "9"s + b15_1.substr(1); auto invalid_server_pk = server_pks[0].substr(0, 60); CHECK_THROWS(session_id_matches_blinded_id(invalid_session_id, b15_1, server_pks[0])); CHECK_THROWS(session_id_matches_blinded_id(session_id1, invalid_blinded_id, server_pks[0])); From 776bac0b7aeba37a5d610759c6b05e3149b048fc Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 20 Sep 2024 12:51:19 +1000 Subject: [PATCH 405/572] Fixed up the iOS build script --- utils/ios.sh | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/utils/ios.sh b/utils/ios.sh index b3f63085..1ff2eede 100755 --- a/utils/ios.sh +++ b/utils/ios.sh @@ -22,6 +22,7 @@ VALID_DEVICE_ARCH_PLATFORMS=(OS64) OUTPUT_DIR="${TARGET_BUILD_DIR:-build-ios}" IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:-13} ENABLE_BITCODE=${ENABLE_BITCODE:-OFF} +CONFIGURATION=${CONFIGURATION:-App_Store_Release} SHOULD_ACHIVE=${2:-true} # Parameter 2 is a flag indicating whether we want to archive the result # We want to customise the env variable so can't just default the value @@ -97,6 +98,14 @@ if [ -z $PLATFORM_NAME ] || [ $PLATFORM_NAME = "iphoneos" ]; then fi # Build the individual architectures +submodule_check=ON +build_type="Release" + +if [ "$CONFIGURATION" == "Debug" || "$CONFIGURATION" == "Debug_Compile_LibSession" ]; then + submodule_check=OFF + build_type="Debug" +fi + for i in "${!TARGET_ARCHS[@]}"; do build="${BUILD_DIR}/${TARGET_ARCHS[$i]}" platform="${TARGET_PLATFORMS[$i]}" @@ -106,7 +115,12 @@ for i in "${!TARGET_ARCHS[@]}"; do -DCMAKE_TOOLCHAIN_FILE="${projdir}/external/ios-cmake/ios.toolchain.cmake" \ -DPLATFORM=$platform \ -DDEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET \ - -DENABLE_BITCODE=$ENABLE_BITCODE + -DENABLE_BITCODE=$ENABLE_BITCODE \ + -DBUILD_STATIC_DEPS=ON \ + -DENABLE_VISIBILITY=ON \ + -DSUBMODULE_CHECK=$submodule_check \ + -DCMAKE_BUILD_TYPE=$build_type \ + -DLOCAL_MIRROR=https://oxen.rocks/deps done # If needed combine simulator builds into a multi-arch lib @@ -140,42 +154,39 @@ rm -rf "${OUTPUT_DIR}/libsession-util.xcframework" if [ "${#TARGET_SIM_ARCHS}" -gt "0" ] && [ "${#TARGET_DEVICE_ARCHS}" -gt "0" ]; then xcodebuild -create-xcframework \ -library "${BUILD_DIR}/ios/libsession-util.a" \ + -headers "include" \ -library "${BUILD_DIR}/sim/libsession-util.a" \ + -headers "include" \ -output "${OUTPUT_DIR}/libsession-util.xcframework" elif [ "${#TARGET_DEVICE_ARCHS}" -gt "0" ]; then xcodebuild -create-xcframework \ -library "${BUILD_DIR}/ios/libsession-util.a" \ + -headers "include" \ -output "${OUTPUT_DIR}/libsession-util.xcframework" else xcodebuild -create-xcframework \ -library "${BUILD_DIR}/sim/libsession-util.a" \ + -headers "include" \ -output "${OUTPUT_DIR}/libsession-util.xcframework" fi -# Copy the headers over -cp -rv include/session "${OUTPUT_DIR}/libsession-util.xcframework" - # The 'module.modulemap' is needed for XCode to be able to find the headers -modmap="${OUTPUT_DIR}/libsession-util.xcframework/module.modulemap" +modmap="${OUTPUT_DIR}/module.modulemap" echo "module SessionUtil {" >"$modmap" echo " module capi {" >>"$modmap" for x in $(cd include && find session -name '*.h'); do echo " header \"$x\"" >>"$modmap" done echo -e " export *\n }" >>"$modmap" -if false; then - # If we include the cpp headers like this then Xcode will try to load them as C headers (which - # of course breaks) and doesn't provide any way to only load the ones you need (because this is - # Apple land, why would anything useful be available?). So we include the headers in the - # archive but can't let xcode discover them because it will do it wrong. - echo -e "\n module cppapi {" >>"$modmap" - for x in $(cd include && find session -name '*.hpp'); do - echo " header \"$x\"" >>"$modmap" - done - echo -e " export *\n }" >>"$modmap" -fi echo "}" >>"$modmap" +# Need to add the module.modulemap into each architecture directory in the xcframework +for dir in "${OUTPUT_DIR}/libsession-util.xcframework"/*/; do + cp "${modmap}" "${dir}/Headers/module.modulemap" +done + +rm -rf "${modmap}" + if [ $SHOULD_ACHIVE = true ]; then (cd "${OUTPUT_DIR}/.." && tar cvJf "${UNIQUE_NAME}.tar.xz" "${UNIQUE_NAME}") fi From 71024083b521099c3effff64a0745b79e8657cef Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Oct 2024 17:26:01 +1000 Subject: [PATCH 406/572] Fixed a C API bug with the INVITE_NOT_SENT member status --- src/config/groups/members.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 4444d1b5..c86017e7 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -141,8 +141,14 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { profile_picture.key = {m.profile_pic.key, 32}; } admin = m.admin; - invite_status = (m.invited == INVITE_SENT || m.invited == INVITE_FAILED) ? m.invited : 0; - promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED) ? m.promoted : 0; + invite_status = + (m.invited == INVITE_SENT || m.invited == INVITE_FAILED || m.invited == INVITE_NOT_SENT) + ? m.invited + : 0; + promotion_status = (m.promoted == INVITE_SENT || m.promoted == INVITE_FAILED || + m.invited == INVITE_NOT_SENT) + ? m.promoted + : 0; removed_status = (m.removed == REMOVED_MEMBER || m.removed == REMOVED_MEMBER_AND_MESSAGES) ? m.removed : 0; From 10f15aeb1f9712452adf55a8d0826be6271c10e4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 8 Oct 2024 09:50:24 +1100 Subject: [PATCH 407/572] Added a function to retrieve the current snode cache size --- include/session/network.h | 6 ++++++ include/session/network.hpp | 6 ++++++ src/network.cpp | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/include/session/network.h b/include/session/network.h index 1168437f..3d9af0d7 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -109,6 +109,12 @@ LIBSESSION_EXPORT void network_close_connections(network_object* network); /// initialization). LIBSESSION_EXPORT void network_clear_cache(network_object* network); +/// API: network/network_get_cache_size +/// +/// Retrieves the current size of the snode cache from memory (if a cache doesn't exist or +/// hasn't been loaded then this will return 0). +LIBSESSION_EXPORT size_t network_get_snode_cache_size(network_object* network); + /// API: network/network_set_status_changed_callback /// /// Registers a callback to be called whenever the network connection status changes. diff --git a/include/session/network.hpp b/include/session/network.hpp index 386cfd60..c3f7fc52 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -278,6 +278,12 @@ class Network { /// initialization). void clear_cache(); + /// API: network/snode_cache_size + /// + /// Retrieves the current size of the snode cache from memory (if a cache doesn't exist or + /// hasn't been loaded then this will return 0). + size_t snode_cache_size(); + /// API: network/get_swarm /// /// Retrieves the swarm for the given pubkey. If there is already an entry in the cache for the diff --git a/src/network.cpp b/src/network.cpp index c1af2aeb..852386c6 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -696,6 +696,10 @@ void Network::clear_cache() { }); } +size_t Network::snode_cache_size() { + return net.call_get([this]() -> size_t { return snode_cache.size(); }); +} + // MARK: Connection void Network::suspend() { @@ -2871,6 +2875,10 @@ LIBSESSION_C_API void network_clear_cache(network_object* network) { unbox(network).clear_cache(); } +LIBSESSION_C_API size_t network_get_snode_cache_size(network_object* network) { + return unbox(network).snode_cache_size(); +} + LIBSESSION_C_API void network_set_status_changed_callback( network_object* network, void (*callback)(CONNECTION_STATUS status, void* ctx), void* ctx) { if (!callback) From 5ce7e4d5be325dc958cdb06ce85f522005d8e041 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 24 Oct 2024 11:53:31 +1100 Subject: [PATCH 408/572] Add blind version signing for a generic request --- src/blinding.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/blinding.cpp b/src/blinding.cpp index 44c1549d..87c5efdf 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -456,6 +456,22 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } +ustring blind_version_sign_request(ustring_view ed25519_sk, uint64_t timestamp, ustring_view method, ustring_view path, std::optional body) { + auto [pk, sk] = blind_version_key_pair(ed25519_sk); + + // Signature should be on `TIMESTAMP || METHOD || PATH || BODY_HASH` + ustring buf; + buf.reserve(10 + 6 + path.size() + body.value_or({}).size()); + buf += to_unsigned_sv(std::to_string(timestamp)); + buf += to_unsigned_sv(method); + buf += to_unsigned_sv(path); + + if (body) + buf += blake2b(*body, 64); + + return ed25519::sign({sk.data(), sk.size()}, buf); +} + ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); From 3a4eb57b028d34c6b071fda8c68aa2ccd9e75070 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 25 Oct 2024 10:53:57 +1100 Subject: [PATCH 409/572] Change body hash to body Co-authored-by: Thomas Winget --- src/blinding.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blinding.cpp b/src/blinding.cpp index 87c5efdf..5ce5cb39 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -459,15 +459,15 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust ustring blind_version_sign_request(ustring_view ed25519_sk, uint64_t timestamp, ustring_view method, ustring_view path, std::optional body) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); - // Signature should be on `TIMESTAMP || METHOD || PATH || BODY_HASH` + // Signature should be on `TIMESTAMP || METHOD || PATH || BODY` ustring buf; - buf.reserve(10 + 6 + path.size() + body.value_or({}).size()); + buf.reserve(10 + 6 + path.size() + (body ? body->size() : 0)); buf += to_unsigned_sv(std::to_string(timestamp)); buf += to_unsigned_sv(method); buf += to_unsigned_sv(path); if (body) - buf += blake2b(*body, 64); + buf += *body; return ed25519::sign({sk.data(), sk.size()}, buf); } From 7f5faca0eeb48c1c0e98c3b68c87e0688e467ea3 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 28 Oct 2024 14:03:27 +1100 Subject: [PATCH 410/572] Create headers for blind version signing --- include/session/blinding.h | 13 +++++++++++++ include/session/blinding.hpp | 13 +++++++++++++ src/blinding.cpp | 7 ++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index a2f39b2d..ac0d6a45 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -113,6 +113,19 @@ LIBSESSION_EXPORT bool session_blind25_sign( size_t msg_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes), unix timestamp, method, path, and optional body. +/// Returns a version-blinded signature. +LIBSESSION_EXPORT bool blind_version_sign_request( + const unsigned char* ed25519_seckey, /* 64 bytes */ + size_t timestamp, + const unsigned char* method, /* 6 bytes */ + const unsigned char* path, /* 6 bytes */ + const unsigned char* body, /* optional */ + unsigned char* blinded_sig_out /* 64 byte output buffer */); + /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index c6cc1ee3..d42e3a7b 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -168,6 +168,19 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +/// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey +/// that would be returned from blind_version_key_pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), unix timestamp, method, path, and +/// optional body. +/// Returns the version-blinded signature. +ustring blind_version_sign_request( + ustring_view ed25519_sk, + uint64_t timestamp, + ustring_view method, + ustring_view path, + std::optional body); + /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// diff --git a/src/blinding.cpp b/src/blinding.cpp index 5ce5cb39..7c5260af 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -456,7 +456,12 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } -ustring blind_version_sign_request(ustring_view ed25519_sk, uint64_t timestamp, ustring_view method, ustring_view path, std::optional body) { +ustring blind_version_sign_request( + ustring_view ed25519_sk, + uint64_t timestamp, + ustring_view method, + ustring_view path, + std::optional body) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); // Signature should be on `TIMESTAMP || METHOD || PATH || BODY` From e73c72a771a0580f4d57b704c4e6b1c93a5d13de Mon Sep 17 00:00:00 2001 From: Thomas Winget Date: Mon, 28 Oct 2024 15:45:47 -0400 Subject: [PATCH 411/572] blind_version_sign_request C API and CPP test --- include/session/blinding.h | 9 ++++++--- include/session/blinding.hpp | 1 + src/blinding.cpp | 35 +++++++++++++++++++++++++++++++++-- tests/test_blinding.cpp | 17 +++++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index ac0d6a45..d2679271 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -118,12 +118,15 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// /// Takes the Ed25519 secret key (64 bytes), unix timestamp, method, path, and optional body. /// Returns a version-blinded signature. -LIBSESSION_EXPORT bool blind_version_sign_request( +LIBSESSION_EXPORT bool session_blind_version_sign_request( const unsigned char* ed25519_seckey, /* 64 bytes */ size_t timestamp, - const unsigned char* method, /* 6 bytes */ - const unsigned char* path, /* 6 bytes */ + const unsigned char* method, + size_t method_len, + const unsigned char* path, + size_t path_len, const unsigned char* body, /* optional */ + size_t body_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index d42e3a7b..9d0fa1ec 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/src/blinding.cpp b/src/blinding.cpp index 7c5260af..de375692 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -468,8 +468,8 @@ ustring blind_version_sign_request( ustring buf; buf.reserve(10 + 6 + path.size() + (body ? body->size() : 0)); buf += to_unsigned_sv(std::to_string(timestamp)); - buf += to_unsigned_sv(method); - buf += to_unsigned_sv(path); + buf += method; + buf += path; if (body) buf += *body; @@ -604,6 +604,37 @@ LIBSESSION_C_API bool session_blind25_sign( } } +LIBSESSION_C_API bool session_blind_version_sign_request( + const unsigned char* ed25519_seckey, + size_t timestamp, + const unsigned char* method, + size_t method_len, + const unsigned char* path, + size_t path_len, + const unsigned char* body, + size_t body_len, + unsigned char* blinded_sig_out) { + ustring_view method_sv{method, method_len}; + ustring_view path_sv{path, path_len}; + + std::optional body_sv{std::nullopt}; + if (body) + body_sv = ustring_view{body, body_len}; + + try { + auto sig = session::blind_version_sign_request( + {ed25519_seckey, 64}, + timestamp, + method_sv, + path_sv, + body_sv); + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + LIBSESSION_C_API bool session_blind_version_sign( const unsigned char* ed25519_seckey, CLIENT_PLATFORM platform, diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 56a7bc90..aafdf4fd 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -302,6 +302,23 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { CHECK(oxenc::to_hex(signature.begin(), signature.end()) == "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e92" "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); + + auto [pk, sk] = blind_version_key_pair(to_usv(seed1)); + auto method = to_unsigned_sv("GET"); + auto path = to_unsigned_sv("/path/to/somewhere"); + auto body = to_unsigned_sv("some body (once told me)"); + + uint64_t timestamp = 1234567890; + ustring full_message; + full_message += to_unsigned_sv(std::to_string(timestamp)); + full_message += method; + full_message += path; + auto req_sig_no_body = blind_version_sign_request(to_usv(seed1), timestamp, method, path, std::nullopt); + CHECK(crypto_sign_verify_detached(req_sig_no_body.data(), full_message.data(), full_message.size(), pk.data()) == 0); + + full_message += body; + auto req_sig = blind_version_sign_request(to_usv(seed1), timestamp, method, path, body); + CHECK(crypto_sign_verify_detached(req_sig.data(), full_message.data(), full_message.size(), pk.data()) == 0); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From 01b97f7329b3ff7296ff555f38ab0b605ac5a88b Mon Sep 17 00:00:00 2001 From: Thomas Winget Date: Tue, 29 Oct 2024 18:48:12 -0400 Subject: [PATCH 412/572] more correct string reserve, also lint --- include/session/blinding.h | 2 +- src/blinding.cpp | 8 ++------ tests/test_blinding.cpp | 10 +++++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index d2679271..208ebdb3 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -125,7 +125,7 @@ LIBSESSION_EXPORT bool session_blind_version_sign_request( size_t method_len, const unsigned char* path, size_t path_len, - const unsigned char* body, /* optional */ + const unsigned char* body, /* optional */ size_t body_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); diff --git a/src/blinding.cpp b/src/blinding.cpp index de375692..eeb2eb1a 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -466,7 +466,7 @@ ustring blind_version_sign_request( // Signature should be on `TIMESTAMP || METHOD || PATH || BODY` ustring buf; - buf.reserve(10 + 6 + path.size() + (body ? body->size() : 0)); + buf.reserve(10 /* timestamp */ + method.size() + path.size() + (body ? body->size() : 0)); buf += to_unsigned_sv(std::to_string(timestamp)); buf += method; buf += path; @@ -623,11 +623,7 @@ LIBSESSION_C_API bool session_blind_version_sign_request( try { auto sig = session::blind_version_sign_request( - {ed25519_seckey, 64}, - timestamp, - method_sv, - path_sv, - body_sv); + {ed25519_seckey, 64}, timestamp, method_sv, path_sv, body_sv); std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index aafdf4fd..23e3cdce 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -313,12 +313,16 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { full_message += to_unsigned_sv(std::to_string(timestamp)); full_message += method; full_message += path; - auto req_sig_no_body = blind_version_sign_request(to_usv(seed1), timestamp, method, path, std::nullopt); - CHECK(crypto_sign_verify_detached(req_sig_no_body.data(), full_message.data(), full_message.size(), pk.data()) == 0); + auto req_sig_no_body = + blind_version_sign_request(to_usv(seed1), timestamp, method, path, std::nullopt); + CHECK(crypto_sign_verify_detached( + req_sig_no_body.data(), full_message.data(), full_message.size(), pk.data()) == + 0); full_message += body; auto req_sig = blind_version_sign_request(to_usv(seed1), timestamp, method, path, body); - CHECK(crypto_sign_verify_detached(req_sig.data(), full_message.data(), full_message.size(), pk.data()) == 0); + CHECK(crypto_sign_verify_detached( + req_sig.data(), full_message.data(), full_message.size(), pk.data()) == 0); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { From 5685948927d96a6d675cb9072b549c970f63d509 Mon Sep 17 00:00:00 2001 From: Thomas Winget Date: Wed, 30 Oct 2024 21:53:22 -0400 Subject: [PATCH 413/572] use string_view instead of ustring_view where reasonable --- include/session/blinding.h | 6 ++---- include/session/blinding.hpp | 4 ++-- src/blinding.cpp | 18 ++++++++---------- tests/test_blinding.cpp | 8 ++++---- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 208ebdb3..7f4c8ba0 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -121,10 +121,8 @@ LIBSESSION_EXPORT bool session_blind25_sign( LIBSESSION_EXPORT bool session_blind_version_sign_request( const unsigned char* ed25519_seckey, /* 64 bytes */ size_t timestamp, - const unsigned char* method, - size_t method_len, - const unsigned char* path, - size_t path_len, + const char* method, + const char* path, const unsigned char* body, /* optional */ size_t body_len, unsigned char* blinded_sig_out /* 64 byte output buffer */); diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 9d0fa1ec..99032043 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -178,8 +178,8 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin ustring blind_version_sign_request( ustring_view ed25519_sk, uint64_t timestamp, - ustring_view method, - ustring_view path, + std::string_view method, + std::string_view path, std::optional body); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey diff --git a/src/blinding.cpp b/src/blinding.cpp index eeb2eb1a..6bdf7bd9 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -459,8 +459,8 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust ustring blind_version_sign_request( ustring_view ed25519_sk, uint64_t timestamp, - ustring_view method, - ustring_view path, + std::string_view method, + std::string_view path, std::optional body) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); @@ -468,8 +468,8 @@ ustring blind_version_sign_request( ustring buf; buf.reserve(10 /* timestamp */ + method.size() + path.size() + (body ? body->size() : 0)); buf += to_unsigned_sv(std::to_string(timestamp)); - buf += method; - buf += path; + buf += to_unsigned_sv(method); + buf += to_unsigned_sv(path); if (body) buf += *body; @@ -607,15 +607,13 @@ LIBSESSION_C_API bool session_blind25_sign( LIBSESSION_C_API bool session_blind_version_sign_request( const unsigned char* ed25519_seckey, size_t timestamp, - const unsigned char* method, - size_t method_len, - const unsigned char* path, - size_t path_len, + const char* method, + const char* path, const unsigned char* body, size_t body_len, unsigned char* blinded_sig_out) { - ustring_view method_sv{method, method_len}; - ustring_view path_sv{path, path_len}; + std::string_view method_sv{method}; + std::string_view path_sv{path}; std::optional body_sv{std::nullopt}; if (body) diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 23e3cdce..5804bbcb 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -304,15 +304,15 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); auto [pk, sk] = blind_version_key_pair(to_usv(seed1)); - auto method = to_unsigned_sv("GET"); - auto path = to_unsigned_sv("/path/to/somewhere"); + auto method = "GET"sv; + auto path = "/path/to/somewhere"sv; auto body = to_unsigned_sv("some body (once told me)"); uint64_t timestamp = 1234567890; ustring full_message; full_message += to_unsigned_sv(std::to_string(timestamp)); - full_message += method; - full_message += path; + full_message += to_unsigned_sv(method); + full_message += to_unsigned_sv(path); auto req_sig_no_body = blind_version_sign_request(to_usv(seed1), timestamp, method, path, std::nullopt); CHECK(crypto_sign_verify_detached( From a5ea05738553cc972c0634fbcf7de11159d00326 Mon Sep 17 00:00:00 2001 From: Thomas Winget Date: Wed, 30 Oct 2024 21:56:47 -0400 Subject: [PATCH 414/572] size_t -> uint64_t in C API in a couple places (timestamp) --- include/session/blinding.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/blinding.h b/include/session/blinding.h index 7f4c8ba0..e3724883 100644 --- a/include/session/blinding.h +++ b/include/session/blinding.h @@ -120,7 +120,7 @@ LIBSESSION_EXPORT bool session_blind25_sign( /// Returns a version-blinded signature. LIBSESSION_EXPORT bool session_blind_version_sign_request( const unsigned char* ed25519_seckey, /* 64 bytes */ - size_t timestamp, + uint64_t timestamp, const char* method, const char* path, const unsigned char* body, /* optional */ @@ -135,7 +135,7 @@ LIBSESSION_EXPORT bool session_blind_version_sign_request( LIBSESSION_EXPORT bool session_blind_version_sign( const unsigned char* ed25519_seckey, /* 64 bytes */ CLIENT_PLATFORM platform, - size_t timestamp, + uint64_t timestamp, unsigned char* blinded_sig_out /* 64 byte output buffer */); /// API: crypto/session_blind25_sign From 259321467d5afe6c92f2e979d5782d1a7a608e53 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 6 Nov 2024 14:17:27 +1100 Subject: [PATCH 415/572] feat: add is_destroyed flag to UserGroupsConfig --- include/session/config/user_groups.h | 2 ++ include/session/config/user_groups.hpp | 11 +++++++++++ src/config/user_groups.cpp | 25 ++++++++++++++++++++++++- tests/test_config_user_groups.cpp | 10 ++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 919faa79..3c6a07a0 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -67,6 +67,8 @@ typedef struct ugroups_group_info { bool invited; // True if this is in the invite-but-not-accepted state. + bool is_destroyed; // True if this group was marked as permanently destroyed + } ugroups_group_info; typedef struct ugroups_community_info { diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index d35855dd..ed1735fd 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -50,6 +50,7 @@ namespace session::config { /// non-empty. /// n - the room name, from a the group invitation; this is intended to be removed once the /// invitation has been accepted, as the name contained in the group info supercedes this). +/// d - true if the group was marked as destroyed /// @, !, +, i, j -- see common values, above. /// /// o - dict of communities (AKA open groups); within this dict (which deliberately has the same @@ -198,6 +199,10 @@ struct group_info : base_group_info { /// Producing and using this value is done with the groups::Keys `swarm` methods. ustring auth_data; + /// Flag indicating if this group was marked as permanently deleted. + /// You should only use `destroyGroup` and `isDestroyed` to interact with this field. + bool is_destroyed = false; + /// Constructs a new group info from an hex id (03 + pubkey). Throws if id is invalid. explicit group_info(std::string gid); @@ -213,6 +218,12 @@ struct group_info : base_group_info { /// auth_data are empty. bool kicked() const; + /// Mark the group as permanently destroyed. This cannot be unset once set. + void destroyGroup(); + + /// Returns true if the group was destroyed by one of the admin. + bool isDestroyed() const; + private: friend class UserGroups; diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 9b08aa0c..e432e97d 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -206,6 +206,7 @@ group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { base_from(*this, c); name = c.name; + is_destroyed = c.is_destroyed; assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up if (c.have_secretkey) @@ -219,6 +220,7 @@ void group_info::into(ugroups_group_info& c) const { base_into(*this, c); copy_c_str(c.id, id); copy_c_str(c.name, name); + c.is_destroyed = is_destroyed; if ((c.have_secretkey = secretkey.size() == 64)) std::memcpy(c.secretkey, secretkey.data(), 64); if ((c.have_auth_data = auth_data.size() == 100)) @@ -243,6 +245,8 @@ void group_info::load(const dict& info_dict) { } if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); + + is_destroyed = maybe_int(info_dict, "d").value_or(0); } void group_info::setKicked() { @@ -251,7 +255,16 @@ void group_info::setKicked() { } bool group_info::kicked() const { - return secretkey.empty() && auth_data.empty(); + return secretkey.empty() && auth_data.empty() && !isDestroyed(); +} + +void group_info::destroyGroup() { + setKicked(); + is_destroyed = true; +} + +bool group_info::isDestroyed() const { + return is_destroyed; } void community_info::load(const dict& info_dict) { @@ -420,6 +433,7 @@ void UserGroups::set(const group_info& g) { set_nonempty_str( info["n"], std::string_view{g.name}.substr(0, legacy_group_info::NAME_MAX_LENGTH)); + set_flag(info["d"], g.is_destroyed); if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: @@ -765,6 +779,15 @@ LIBSESSION_C_API bool ugroups_group_is_kicked(const ugroups_group_info* group) { return !(group->have_auth_data || group->have_secretkey); } +LIBSESSION_C_API void ugroups_group_set_destroyed(ugroups_group_info* group) { + assert(group); + ugroups_group_set_kicked(group); + group->is_destroyed = true; +} +LIBSESSION_C_API bool ugroups_group_is_destroyed(const ugroups_group_info* group) { + return group->is_destroyed; +} + struct ugroups_legacy_members_iterator { using map_t = std::map; map_t& members; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 25425a4e..c8e7783b 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -541,13 +541,23 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK_FALSE(c3b->kicked()); c3b->auth_data.resize(100); CHECK_FALSE(c3b->kicked()); + // mark ourselves as kicked c3b->setKicked(); CHECK(c3b->kicked()); CHECK(c3b->secretkey.empty()); CHECK(c3b->auth_data.empty()); + // add a non empty auth_data, we shouldn't be kicked anymore c3b->auth_data.resize(100); CHECK_FALSE(c3b->kicked()); c3b->auth_data.clear(); + CHECK(c3b->kicked()); + // we are not kicked, mark the group as destroyed + c3b->auth_data.resize(100); + c3b->destroyGroup(); + CHECK(c3b->isDestroyed()); + // the group was destroyed, so we are not `kicked` from it. + // We keep the states separate as `kicked` is not permanent but `destroyed` is. + CHECK_FALSE(c3b->kicked()); auto gg = groups.get_or_construct_group( "030303030303030303030303030303030303030303030303030303030303030303"); From 82526fcf2dbb906a41ce03586b6d3b3abc6b4320 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 7 Nov 2024 11:43:53 +1100 Subject: [PATCH 416/572] fix: merge kicked & destroyed group status into removed_status --- include/session/config/user_groups.h | 22 +++++++------- include/session/config/user_groups.hpp | 26 +++++++++++------ src/config/user_groups.cpp | 40 +++++++++++++++++--------- tests/test_config_user_groups.cpp | 24 ++++++++++++---- 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index 3c6a07a0..c7cedfa3 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -28,9 +28,9 @@ typedef struct ugroups_legacy_group_info { // terminator). int64_t disappearing_timer; // Seconds. 0 == disabled. - int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned - // (with higher meaning pinned higher). - int64_t joined_at; // unix timestamp when joined (or re-joined) + int priority; // pinned conversation priority; 0 = unpinned, negative = hidden, positive = + // pinned (with higher meaning pinned higher). + int64_t joined_at; // unix timestamp when joined (or re-joined) CONVO_NOTIFY_MODE notifications; // When the user wants notifications int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) @@ -58,17 +58,17 @@ typedef struct ugroups_group_info { // signing value that can be used to produce signature values to // access the swarm. - int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned - // (with higher meaning pinned higher). - int64_t joined_at; // unix timestamp when joined (or re-joined) + int priority; // pinned conversation priority; 0 = unpinned, negative = hidden, positive = + // pinned (with higher meaning pinned higher). + int64_t joined_at; // unix timestamp when joined (or re-joined) CONVO_NOTIFY_MODE notifications; // When the user wants notifications int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) bool invited; // True if this is in the invite-but-not-accepted state. - bool is_destroyed; // True if this group was marked as permanently destroyed - + int removed_status; // Tracks why we were removed from the group. Values are 0:NOT_REMOVED, + // 1:KICKED_FROM_GROUP or 2:GROUP_DESTROYED } ugroups_group_info; typedef struct ugroups_community_info { @@ -79,9 +79,9 @@ typedef struct ugroups_community_info { // info (that one is always forced lower-cased). unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls) - int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned - // (with higher meaning pinned higher). - int64_t joined_at; // unix timestamp when joined (or re-joined) + int priority; // pinned conversation priority; 0 = unpinned, negative = hidden, positive = + // pinned (with higher meaning pinned higher). + int64_t joined_at; // unix timestamp when joined (or re-joined) CONVO_NOTIFY_MODE notifications; // When the user wants notifications int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications` // setting until the timestamp) diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index ed1735fd..c03b933f 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -50,7 +50,7 @@ namespace session::config { /// non-empty. /// n - the room name, from a the group invitation; this is intended to be removed once the /// invitation has been accepted, as the name contained in the group info supercedes this). -/// d - true if the group was marked as destroyed +/// r - removed_status, tracks why we were removed from the group. /// @, !, +, i, j -- see common values, above. /// /// o - dict of communities (AKA open groups); within this dict (which deliberately has the same @@ -183,6 +183,8 @@ struct legacy_group_info : base_group_info { void load(const dict& info_dict); }; +constexpr int NOT_REMOVED = 0, KICKED_FROM_GROUP = 1, GROUP_DESTROYED = 2; + /// Struct containing new group info (aka "closed groups v2"). struct group_info : base_group_info { std::string id; // The group pubkey (66 hex digits); this is an ed25519 key, prefixed with "03" @@ -199,9 +201,11 @@ struct group_info : base_group_info { /// Producing and using this value is done with the groups::Keys `swarm` methods. ustring auth_data; - /// Flag indicating if this group was marked as permanently deleted. - /// You should only use `destroyGroup` and `isDestroyed` to interact with this field. - bool is_destroyed = false; + /// Tracks why we were removed from the group. Values are: + /// - NOT_REMOVED: that we haven't been removed, + /// - KICKED_FROM_GROUP: we have been kicked from the group, + /// - GROUP_DESTROYED: the group was permanently destroyed so everyone got removed. + int removed_status = NOT_REMOVED; /// Constructs a new group info from an hex id (03 + pubkey). Throws if id is invalid. explicit group_info(std::string gid); @@ -210,16 +214,20 @@ struct group_info : base_group_info { group_info(const struct ugroups_group_info& c); // From c struct void into(struct ugroups_group_info& c) const; // Into c struct - /// Shortcut for clearing both secretkey and auth_data, which indicates that we were kicked from - /// the group. - void setKicked(); + /// Marks the group as kicked and clears auth_data & secret_key + void markKicked(); + + /// Marks the group as reinvited (i.e. revert a `markKicked` call) + /// Note: this only works when the group was not permanently deleted. + void markInvited(); /// Returns true if we don't have room access, i.e. we were kicked and both secretkey and /// auth_data are empty. bool kicked() const; - /// Mark the group as permanently destroyed. This cannot be unset once set. - void destroyGroup(); + /// Mark the group as permanently destroyed and clears auth_data & secret_key. This cannot be + /// unset once set. + void markDestroyed(); /// Returns true if the group was destroyed by one of the admin. bool isDestroyed() const; diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index e432e97d..76435e0b 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -206,7 +206,7 @@ group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { base_from(*this, c); name = c.name; - is_destroyed = c.is_destroyed; + removed_status = c.removed_status; assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up if (c.have_secretkey) @@ -220,7 +220,7 @@ void group_info::into(ugroups_group_info& c) const { base_into(*this, c); copy_c_str(c.id, id); copy_c_str(c.name, name); - c.is_destroyed = is_destroyed; + c.removed_status = removed_status; if ((c.have_secretkey = secretkey.size() == 64)) std::memcpy(c.secretkey, secretkey.data(), 64); if ((c.have_auth_data = auth_data.size() == 100)) @@ -246,25 +246,35 @@ void group_info::load(const dict& info_dict) { if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); - is_destroyed = maybe_int(info_dict, "d").value_or(0); + removed_status = maybe_int(info_dict, "r").value_or(0); } -void group_info::setKicked() { +void group_info::markKicked() { secretkey.clear(); auth_data.clear(); + if (removed_status != GROUP_DESTROYED) { + removed_status = KICKED_FROM_GROUP; + } +} + +void group_info::markInvited() { + if (removed_status == KICKED_FROM_GROUP) { + removed_status = NOT_REMOVED; + } } bool group_info::kicked() const { - return secretkey.empty() && auth_data.empty() && !isDestroyed(); + return removed_status == KICKED_FROM_GROUP; } -void group_info::destroyGroup() { - setKicked(); - is_destroyed = true; +void group_info::markDestroyed() { + secretkey.clear(); + auth_data.clear(); + removed_status = GROUP_DESTROYED; } bool group_info::isDestroyed() const { - return is_destroyed; + return removed_status == GROUP_DESTROYED; } void community_info::load(const dict& info_dict) { @@ -433,7 +443,7 @@ void UserGroups::set(const group_info& g) { set_nonempty_str( info["n"], std::string_view{g.name}.substr(0, legacy_group_info::NAME_MAX_LENGTH)); - set_flag(info["d"], g.is_destroyed); + set_positive_int(info["r"], g.removed_status); if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: @@ -774,18 +784,20 @@ LIBSESSION_C_API void ugroups_group_set_kicked(ugroups_group_info* group) { assert(group); group->have_auth_data = false; group->have_secretkey = false; + group->removed_status = KICKED_FROM_GROUP; } LIBSESSION_C_API bool ugroups_group_is_kicked(const ugroups_group_info* group) { - return !(group->have_auth_data || group->have_secretkey); + return group->removed_status == KICKED_FROM_GROUP; } LIBSESSION_C_API void ugroups_group_set_destroyed(ugroups_group_info* group) { assert(group); - ugroups_group_set_kicked(group); - group->is_destroyed = true; + group->have_auth_data = false; + group->have_secretkey = false; + group->removed_status = GROUP_DESTROYED; } LIBSESSION_C_API bool ugroups_group_is_destroyed(const ugroups_group_info* group) { - return group->is_destroyed; + return group->removed_status == GROUP_DESTROYED; } struct ugroups_legacy_members_iterator { diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index c8e7783b..936a1dbc 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -542,23 +542,35 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { c3b->auth_data.resize(100); CHECK_FALSE(c3b->kicked()); // mark ourselves as kicked - c3b->setKicked(); + c3b->markKicked(); CHECK(c3b->kicked()); CHECK(c3b->secretkey.empty()); CHECK(c3b->auth_data.empty()); - // add a non empty auth_data, we shouldn't be kicked anymore + // add a non empty auth_data, and reset the removed_status: we shouldn't be kicked anymore c3b->auth_data.resize(100); + c3b->markInvited(); CHECK_FALSE(c3b->kicked()); - c3b->auth_data.clear(); - CHECK(c3b->kicked()); // we are not kicked, mark the group as destroyed - c3b->auth_data.resize(100); - c3b->destroyGroup(); + c3b->markDestroyed(); CHECK(c3b->isDestroyed()); // the group was destroyed, so we are not `kicked` from it. // We keep the states separate as `kicked` is not permanent but `destroyed` is. CHECK_FALSE(c3b->kicked()); + // reset the state to test that the transition kicked->destroyed works only in that direction + c3b->auth_data.resize(100); + c3b->removed_status = 0; + CHECK_FALSE(c3b->kicked()); + // kicked->destroyed works + c3b->markKicked(); + CHECK(c3b->kicked()); + c3b->markDestroyed(); + CHECK(c3b->isDestroyed()); + // destroyed->kicked doesn't work + c3b->markKicked(); + CHECK_FALSE(c3b->kicked()); + CHECK(c3b->isDestroyed()); + auto gg = groups.get_or_construct_group( "030303030303030303030303030303030303030303030303030303030303030303"); groups.set(gg); From c422ccbba849169d87f1260427f852e5a4dc4258 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 15 Nov 2024 11:48:31 +1100 Subject: [PATCH 417/572] chore: fix comment for blind_version_key_pair returning a seed --- include/session/blinding.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 99032043..ff611535 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -143,7 +143,7 @@ std::pair blind25_key_pair( /// Computes a version-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key and -/// private key (NOT a seed). +/// blinded libsodium seed value. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. From 48f769e3dd460160a1567f63a95acc7ae0f27776 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 14 Nov 2024 21:23:00 -0400 Subject: [PATCH 418/572] Make windows-x86 static "allow fail" It will take more investigation to coax this build into working, and this is not an important release target. --- .drone.jsonnet | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index a411141f..51e444f5 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -276,6 +276,7 @@ local static_build(name, oxen_repo=false, kitware_repo=''/* ubuntu codename, if wanted */, cmake_extra='', + allow_fail=false, jobs=6) = debian_pipeline( name, @@ -283,6 +284,7 @@ local static_build(name, arch=arch, deps=deps, oxen_repo=oxen_repo, + allow_fail=allow_fail, build=[ 'export JOBS=' + jobs, './utils/static-bundle.sh build ' + archive_name + ' -DSTATIC_LIBSTD=ON ' + cmake_extra, @@ -366,6 +368,7 @@ local static_build(name, docker_base + 'debian-win32-cross', 'libsession-util-windows-x86-TAG.zip', deps=['g++-mingw-w64-i686-posix'], + allow_fail=true, cmake_extra='-DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw-i686-toolchain.cmake'), debian_pipeline( 'Static Android', From 43c2d44a0e0d1b00504666f786b9686bf6f5c8d0 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 13 Nov 2024 15:22:21 +1100 Subject: [PATCH 419/572] chore: replace oxen-io urls with session-foundation --- .gitmodules | 4 ++-- docs/api/make-docs.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 3b5ab0d4..22049b33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ [submodule "external/oxen-encoding"] path = external/oxen-encoding - url = https://github.com/oxen-io/oxen-encoding.git + url = https://github.com/session-foundation/oxen-encoding.git [submodule "external/libsodium-internal"] path = external/libsodium-internal - url = https://github.com/jagerman/libsodium-internal.git + url = https://github.com/session-foundation/libsodium-internal.git [submodule "tests/Catch2"] path = tests/Catch2 url = https://github.com/catchorg/Catch2 diff --git a/docs/api/make-docs.sh b/docs/api/make-docs.sh index 7acd5b15..db25a8c0 100755 --- a/docs/api/make-docs.sh +++ b/docs/api/make-docs.sh @@ -60,7 +60,7 @@ if (m{^\s*\s*$}) { \s*$}) { - if (not $first) { - $first = false; - print qq{ - \n}; - } -} else { - s{.*}{Libsession Utils API}; - s{(name="description" content=)"[^"]*"}{$1"libsession-util function documentation"}; - s{^\s*\s*$}{}; - if (m{^\s*}) { - print qq{ - - - - - - - - - - - - \n}; - } - print; -}' "$destdir"/index.html +cd $destdir && mkdocs build && cd - diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml new file mode 100644 index 00000000..2eb2db24 --- /dev/null +++ b/docs/api/mkdocs.yml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json +site_name: Libsession Utils API Functions +theme: + name: material From a81cb1db47b85e130b381559d49d280d830fede0 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 17:02:27 +1100 Subject: [PATCH 489/572] feat: restructured make commands to build, serve, and dev within the dist folder added running a dev server with hot reloading --- docs/api/Makefile | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/api/Makefile b/docs/api/Makefile index 2f9f8352..053a8fe9 100644 --- a/docs/api/Makefile +++ b/docs/api/Makefile @@ -1,26 +1,37 @@ H_FILES = $(wildcard ../../include/session/config/*.h ../../include/session/config/*/*.h) HPP_FILES = $(wildcard ../../include/session/config/*.hpp ../../include/session/config/*/*.hpp) -.PHONY: all -all: h-docs hpp-docs +.PHONY: clean +clean: + rm -rf ./dist -.PHONY: hpp-docs -hpp-docs: - ./make-docs.sh libsession-util-cpp $(HPP_FILES) +.PHONY: build-hpp +build-hpp: + ./make-docs.sh dist/libsession-util-cpp $(HPP_FILES) -.PHONY: h-docs -h-docs: - ./make-docs.sh libsession-util-c $(H_FILES) +.PHONY: build-h +build-h: + ./make-docs.sh dist/libsession-util-c $(H_FILES) -.PHONY: run-c -run-c: - docsify serve libsession-util-c +.PHONY: build-all +all: + build-h build-hpp -.PHONY: run-cpp -run-cpp: - docsify serve libsession-util-cpp +.PHONY: serve-c +serve-c: + cd dist/libsession-util-c/site && python -m http.server 8000 && cd - +.PHONY: serve-cpp +serve-cpp: + cd dist/libsession-util-cpp/site && python -m http.server 8001 && cd - -.PHONY: clean -clean: - rm -rf ./libsession-util-c ./libsession-util-cpp +# Developer commands: Build and serve a site inside of dist/ using the mkdocs development server. +# NOTE: Any changes to dist/ will be hot-reloaded but are not tracked by git. + +.PHONY: dev-c +dev-c: build-c + cd dist/libsession-util-c && mkdocs serve && cd - + +.PHONY: dev-cpp +dev-cpp: build-hpp + cd dist/libsession-util-cpp && mkdocs serve && cd - From 968115a1af01815d9b25403c46b78bf44a0f11c2 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 18:46:58 +1100 Subject: [PATCH 490/572] feat: update mkdocs configuration and add KaTeX support for math rendering --- docs/api/docs/javascripts/katex.js | 10 ++++ docs/api/docs/sidebar.md | 10 ---- docs/api/docs/stylesheets/extra.css | 8 +++ docs/api/mkdocs.yml | 79 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 docs/api/docs/javascripts/katex.js delete mode 100644 docs/api/docs/sidebar.md create mode 100644 docs/api/docs/stylesheets/extra.css diff --git a/docs/api/docs/javascripts/katex.js b/docs/api/docs/javascripts/katex.js new file mode 100644 index 00000000..8c642137 --- /dev/null +++ b/docs/api/docs/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true } + ], + }) +}) diff --git a/docs/api/docs/sidebar.md b/docs/api/docs/sidebar.md deleted file mode 100644 index 569af33b..00000000 --- a/docs/api/docs/sidebar.md +++ /dev/null @@ -1,10 +0,0 @@ -- [Base](base.md) -- [Community](community.md) -- [Contacts](contacts.md) -- [Convo Info Volatile](convo_info_volatile.md) -- [Encrypt](encrypt.md) -- [Error](error.md) -- [Groups](groups.md) -- [User Groups](user_groups.md) -- [User Profile](user_profile.md) -- [Utils](util.md) diff --git a/docs/api/docs/stylesheets/extra.css b/docs/api/docs/stylesheets/extra.css new file mode 100644 index 00000000..ab47b65a --- /dev/null +++ b/docs/api/docs/stylesheets/extra.css @@ -0,0 +1,8 @@ +:root > * { + --md-accent-fg-color: #00aa59; + --md-accent-fg-color--light: #00f782; + --md-accemt-fg-color--dark: #00aa59; + --md-primary-fg-color: #00aa59; + --md-primary-fg-color--light: #00f782; + --md-primary-fg-color--dark: #00aa59; +} diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml index 2eb2db24..e79668a8 100644 --- a/docs/api/mkdocs.yml +++ b/docs/api/mkdocs.yml @@ -1,4 +1,83 @@ # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json site_name: Libsession Utils API Functions +repo_url: https://github.com/session-foundation/libsession-util +nav: + - Getting Started: index.md + - Base: base.md + - Community: community.md + - Config: config.md + - Contacts: contacts.md + - Convo: convo_info_volatile.md + - Encrypt: encrypt.md + # TODO Currently doesn't have the new API comments + # - Error: error.md + - Groups: groups.md + - Profile Pictures: profile_pic.md + - User Groups: user_groups.md + - User Profile: user_profile.md + # TODO Currently doesn't have the new API comments + # - Utils: util.md +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/session-foundation/libsession-util +extra_css: + - stylesheets/extra.css + - https://unpkg.com/katex@0/dist/katex.min.css +extra_javascript: + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js +markdown_extensions: + - pymdownx.arithmatex: + generic: true + - pymdownx.critic + - pymdownx.caret + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: python/name:pymdownx.superfences.fence_code_format + - pymdownx.tilde +plugins: + - privacy theme: name: material + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + # Content + - content.code.copy + # Navigation + - navigation.footer + - navigation.instant + - navigation.instant.progress + - navigation.path + - navigation.top + - navigation.tracking + # Table of Contents + - toc.follow From 9b4e93c384d78cc684c00da775cae7d3840aa1a5 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 18:56:47 +1100 Subject: [PATCH 491/572] feat: moved and updated README --- docs/README.md | 17 --------------- docs/api/README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) delete mode 100644 docs/README.md create mode 100644 docs/api/README.md diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index a3185cfc..00000000 --- a/docs/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Libsession Utils API Functions - -## Prerequisites - -- [Material for MkDocs](https://github.com/squidfunk/mkdocs-material) - ```sh - pip install mkdocs-material - ``` -- [Requests](https://github.com/psf/requests) - ```sh - pip install requests - ``` - -# Resources - -- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) -- [MkDocs](https://www.mkdocs.org/) diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000..bb0f2908 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,52 @@ +# Libsession Utils Documentation Websites + +# Getting Started + +## Install dependencies + +```sh +pip install -r requirements.txt +``` + +## Building + +Building happens in the `dist` directory. + +```sh +# Build libsession-util C API functions +make build-h + +# Build libsession-util C++ API functions +make build-cpp + +# Build both C and C++ API functions +make build-all +``` + +## Hosting + +```sh +# Serve libsession-util C API functions on localhost:8000 +make serve-c + +# Serve libsession-util C++ API functions on localhost:8001 +make serve-cpp +``` + +## Developing + +> [!WARNING] +> Any changes to `dist` directory will be hot-reloaded but are not tracked by git. + +```sh +# Serve libsession-util C API functions on localhost:8000 +make dev-c + +# Serve libsession-util C++ API functions on localhost:8000 +make dev-cpp +``` + +# Resources + +- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +- [MkDocs](https://www.mkdocs.org/) From f125f80b7bd79ae470c07ef18fd9dfcb9f100e5f Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 18:57:08 +1100 Subject: [PATCH 492/572] feat: added requirements.txt --- docs/api/requirements.txt | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/api/requirements.txt diff --git a/docs/api/requirements.txt b/docs/api/requirements.txt new file mode 100644 index 00000000..c2abdaa3 --- /dev/null +++ b/docs/api/requirements.txt @@ -0,0 +1,38 @@ +babel==2.17.0 +backrefs==5.8 +cairocffi==1.7.1 +CairoSVG==2.7.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +click==8.1.8 +colorama==0.4.6 +cssselect2==0.8.0 +defusedxml==0.7.1 +ghp-import==2.1.0 +idna==3.10 +Jinja2==3.1.6 +Markdown==3.7 +MarkupSafe==3.0.2 +mergedeep==1.3.4 +mkdocs==1.6.1 +mkdocs-get-deps==0.2.0 +mkdocs-material==9.6.8 +mkdocs-material-extensions==1.3.1 +packaging==24.2 +paginate==0.5.7 +pathspec==0.12.1 +pillow==10.4.0 +platformdirs==4.3.6 +pycparser==2.22 +Pygments==2.19.1 +pymdown-extensions==10.14.3 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +pyyaml_env_tag==0.1 +requests==2.32.3 +six==1.17.0 +tinycss2==1.4.0 +urllib3==2.3.0 +watchdog==6.0.0 +webencodings==0.5.1 From 931df6138c1587ba096071a30ee41fc35e9778c4 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 19:03:56 +1100 Subject: [PATCH 493/572] feat: add config merge logic doc to api websites updated any reference comments to the file --- docs/{ => api/docs}/config-merge-logic.md | 34 ++++++++++++----------- docs/api/mkdocs.yml | 1 + tests/test_configdata.cpp | 8 +++--- 3 files changed, 23 insertions(+), 20 deletions(-) rename docs/{ => api/docs}/config-merge-logic.md (98%) diff --git a/docs/config-merge-logic.md b/docs/api/docs/config-merge-logic.md similarity index 98% rename from docs/config-merge-logic.md rename to docs/api/docs/config-merge-logic.md index 8a0d586e..c3dbb1b6 100644 --- a/docs/config-merge-logic.md +++ b/docs/api/docs/config-merge-logic.md @@ -1,3 +1,5 @@ +## Config Merge Logic + When we have competing config message, we need completely consistent logic for merging them, that is both forwards and backwards compatible (that is: old clients with new data, and new clients with old data, need to produce an agreeable result). @@ -16,7 +18,7 @@ The way this is implemented here is as follows: - whichever one wins, any client that observes a conflict (i.e. same seqno) needs to merge the two messages, increment the seqno, and push the new one to the swarm. -# Structure of a config message +## Structure of a config message A decrypted config message consists of outer data (which contains generic information about the config message), and the inner message data, which contains the actual application-specific @@ -27,7 +29,7 @@ needs to decrypt it. (How to obtain that key is use-case dependent and outside document). The specifics of encryption are covered in the [Message Encryption](#message-encryption) section, below. -## Application-side config data +### Application-side config data To start with the inner config data (which will be under the `"&"` key of the outermost data message; see description in the [outer metadata](#config-message-outer-metadata), this is a dict @@ -52,7 +54,7 @@ allows arbitrary values inside lists, doesn't impose list ordering or uniqueness lists/dicts. All of those are excluded from config data messages so as to make merging of messages feasible and deterministic. -## Config message outer metadata +### Config message outer metadata - The top-level structure of a config message is always a dict, with keys as follows (note that the restrictions described above for the application data do *not* apply to the outer config message @@ -75,7 +77,7 @@ feasible and deterministic. where authentication of the message creator is required. This field, when present, *must* be the last key (i.e. no top-level keys that sort after `~` are permitted). -# Config diffs +## Config diffs To enable clients to merge config messages (even of potential content from future clients it does not understand) any config changes are included in the "diff" section of the config (keys `"<"` and @@ -99,7 +101,7 @@ not understand) any config changes are included in the "diff" section of the con the main application config data, but with placeholder values to indicate changes (rather than duplicating values). -## Config diff updates +### Config diff updates A config diff itself (i.e. the third element of a `"<"` tuple, or the `"="` value) is a dict that largely mirrors the inner config data structure (i.e. under the `"&"` outer config key) but that @@ -125,11 +127,11 @@ A diff update itself is a dict such that: empty if there were only additions or only removals. Similarly to dict handling, removing *all* values implicitly removes the set itself, and adding to a non-existent set autovivifies the set. -# Client update behaviour +## Client update behaviour When there are multiple conflicting config messages clients resolve according to several rules. -## Ignored updates +### Ignored updates Clients ignore updates according to two criteria: @@ -154,7 +156,7 @@ Clients ignore updates according to two criteria: - missing or invalid signature (in contexts where signatures are required) - etc. -## Non-conflicting updates +### Non-conflicting updates If a client is making a configuration change and there is only a single current valid config message (i.e. no conflicts to resolve) then the update procedure is straightforward: @@ -169,7 +171,7 @@ If a client is making a configuration change and there is only a single current 4. A current diff is constructed for any values being assigned, and stored under the `"="` key of the config message. -## Conflict resolution +### Conflict resolution Conflict resolution logic kicks in if, after removing ignored messages from consideration, there are still multiple valid config messages: these messages must be merged and the merge update published. @@ -205,7 +207,7 @@ Merging is performed as follows: constructed and written into the `"="` key. - Otherwise (i.e. only merge changes) the current diff key `"="` is set to an empty dict. -# Examples +## Examples The following depict several examples showing how update rules work. Values are shown in not-quite-JSON (i.e. we add comments) for human readability, but in reality will be bt-encoded. @@ -218,7 +220,7 @@ order of the hashes with the same XXX seqno value (lower letters = earlier-sorti All examples use a "within 5" rule for determining how the seqno cutoff for conflict resolution, and are not using signatures. -## Ordinary update +### Ordinary update Suppose an update begins from the following data (with seqno 122), and updates have all been linear and orderly (i.e. there have been no recent config conflicts): @@ -286,7 +288,7 @@ like this: ``` -## Large, but still ordinary, update +### Large, but still ordinary, update Suppose an update begins from the following data (with seqno 123), and updates have all been linear and orderly (i.e. there have been no recent config conflicts): @@ -400,7 +402,7 @@ The overall record of this change looks as follows. } ``` -## Simple conflict resolution +### Simple conflict resolution Suppose two clients now push update with `seqno=125` where one client sets `["int1"]` to `5` and another removes element `["dictB"]["foo"]`. @@ -464,7 +466,7 @@ Since all clients will produce an identical message, even if multiple clients pu at once, it will simply be de-duplicated and stored only once. -## Stale messages +### Stale messages If a message arrives with a seqno that is not within the most recent five seqno values of the largest-seqno message then it is simply dropped. For instance supposed we have seqno 126 (from the @@ -473,7 +475,7 @@ out of date and has a delayed update still to go out). This message neither com current seqno (126) nor any of the historic ones (122 through 125), and so it is discarded. -## Complex conflict resolution +### Complex conflict resolution Suppose that while the resolution from the previous example is happening there is another client that is somewhat out of date and submitting a configuration update with `seqno=124` (we'll label @@ -621,7 +623,7 @@ This final 127 update thus becomes: } ``` -# Message Encryption +## Message Encryption All messages are stored in encrypted form; we select XChaCha20-Poly1305 encryption for its excellent properties. diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml index e79668a8..f6140974 100644 --- a/docs/api/mkdocs.yml +++ b/docs/api/mkdocs.yml @@ -6,6 +6,7 @@ nav: - Base: base.md - Community: community.md - Config: config.md + - Config Merge Logic: config-merge-logic.md - Contacts: contacts.md - Convo: convo_info_volatile.md - Encrypt: encrypt.md diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 433be62f..30acb8c3 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -494,7 +494,7 @@ const auto m123_expected = const auto h123 = "d9398c597b058ac7e28e3febb76ed68eb8c5b6c369610562ab5f2b596775d73c"_hexbytes; TEST_CASE("config message example 1", "[config][example]") { - /// This is the "Ordinary update" example described in docs/config-merge-logic.md + /// This is the "Ordinary update" example described in docs/api/docs/config-merge-logic.md MutableConfigMessage m118{118, 5}; CHECK(m118.seqno() == 118); CHECK(m118.lag == 5); @@ -725,7 +725,7 @@ const auto h124 = "8b73f316178765b9b3b37168e865c84bb5a78610cbb59b84d0fa4d3b4b3c1 TEST_CASE("config message example 2", "[config][example]") { /// This is the "Large, but still ordinary, update" example described in - /// docs/config-merge-logic.md + /// docs/api/docs/config-merge-logic.md MutableConfigMessage m{m123_expected}; REQUIRE(m.seqno() == 124); @@ -902,7 +902,7 @@ const auto m126_expected = // clang-format on TEST_CASE("config message example 3 - simple conflict", "[config][example][conflict]") { - /// This is the "Simple conflict resolution" example described in docs/config-merge-logic.md + /// This is the "Simple conflict resolution" example described in docs/api/docs/config-merge-logic.md MutableConfigMessage m124{m123_expected}; REQUIRE(m124.seqno() == 124); @@ -961,7 +961,7 @@ TEST_CASE("config message example 3 - simple conflict", "[config][example][confl TEST_CASE("config message example 4 - complex conflict resolution", "[config][example][conflict]") { /// This is the "Complex conflict resolution" example described in - /// docs/config-merge-logic.md + /// docs/api/docs/config-merge-logic.md ConfigMessage m123{m123_expected}; From 083214530e8957013ecb7898cfb78d1d1e3d04ad Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 19:11:46 +1100 Subject: [PATCH 494/572] feat: added back search bar --- docs/api/mkdocs.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml index f6140974..c30374a4 100644 --- a/docs/api/mkdocs.yml +++ b/docs/api/mkdocs.yml @@ -50,6 +50,7 @@ markdown_extensions: - pymdownx.tilde plugins: - privacy + - search theme: name: material palette: @@ -71,14 +72,18 @@ theme: icon: material/brightness-4 name: Switch to light mode features: - # Content + # Content - content.code.copy - # Navigation + # Navigation - navigation.footer - navigation.instant - navigation.instant.progress - navigation.path - navigation.top - navigation.tracking - # Table of Contents + # Search + - search.highlight + - search.share + - search.suggest + # Table of Contents - toc.follow From cf4d4f4ef9a8956f64d05d0ea34ea8682187da00 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Fri, 14 Mar 2025 19:21:05 +1100 Subject: [PATCH 495/572] feat: disable mdx files from being committed --- docs/api/.gitignore | 1 + docs/api/docs/config-merge-logic.md | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api/.gitignore b/docs/api/.gitignore index 19561937..851229e6 100644 --- a/docs/api/.gitignore +++ b/docs/api/.gitignore @@ -1,3 +1,4 @@ api/* api dist +*.mdx diff --git a/docs/api/docs/config-merge-logic.md b/docs/api/docs/config-merge-logic.md index c3dbb1b6..83fda0f1 100644 --- a/docs/api/docs/config-merge-logic.md +++ b/docs/api/docs/config-merge-logic.md @@ -1,5 +1,3 @@ -## Config Merge Logic - When we have competing config message, we need completely consistent logic for merging them, that is both forwards and backwards compatible (that is: old clients with new data, and new clients with old data, need to produce an agreeable result). From f3ead39602dd124957bfcf2913c6210ab21a5d1b Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:03:35 +1100 Subject: [PATCH 496/572] fix: remove arguments from api comments in group info the parser only wants the function name and considers anything else trailing garbage --- include/session/config/groups/info.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 4e6a85af..3b253831 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -211,7 +211,7 @@ LIBSESSION_EXPORT int64_t groups_info_get_attach_delete_before(const config_obje /// - `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); -/// API: groups_info/groups_info_is_destroyed(const config_object* conf); +/// API: groups_info/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. @@ -223,7 +223,7 @@ LIBSESSION_EXPORT void groups_info_set_attach_delete_before(config_object* conf, /// - `true` if the group has been nuked, `false` otherwise. LIBSESSION_EXPORT bool groups_info_is_destroyed(const config_object* conf); -/// API: groups_info/groups_info_destroy_group(const config_object* conf); +/// API: groups_info/groups_info_destroy_group /// /// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). /// From b17462f601fe2aad54e25e103b576584f6999e9b Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:03:52 +1100 Subject: [PATCH 497/572] fix: build-all command --- docs/api/Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api/Makefile b/docs/api/Makefile index 053a8fe9..571a2141 100644 --- a/docs/api/Makefile +++ b/docs/api/Makefile @@ -14,8 +14,7 @@ build-h: ./make-docs.sh dist/libsession-util-c $(H_FILES) .PHONY: build-all -all: - build-h build-hpp +build-all: build-h build-hpp .PHONY: serve-c serve-c: From c6dc5170bb3671410349d7e93b344d50482e4150 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:30:12 +1100 Subject: [PATCH 498/572] feat: auto generate table of contents note make sure that the file names use snakecase --- docs/api/api-to-markdown.py | 2 +- ...fig-merge-logic.md => config_merge_logic.md} | 2 ++ docs/api/mkdocs.yml | 17 ----------------- 3 files changed, 3 insertions(+), 18 deletions(-) rename docs/api/docs/{config-merge-logic.md => config_merge_logic.md} (99%) diff --git a/docs/api/api-to-markdown.py b/docs/api/api-to-markdown.py index ddbefd3c..f8f96c08 100755 --- a/docs/api/api-to-markdown.py +++ b/docs/api/api-to-markdown.py @@ -401,7 +401,7 @@ class Parsing(Enum): f.write("\n\n") else: print(f"Warning: {preamble} doesn't exist, writing generic preamble for {cat}", file=sys.stderr) - f.write(f"# {cat} endpoints\n\n") + f.write(f"# {cat.replace('_', ' ').title()}\n\n") for _, md in eps: f.write(md) diff --git a/docs/api/docs/config-merge-logic.md b/docs/api/docs/config_merge_logic.md similarity index 99% rename from docs/api/docs/config-merge-logic.md rename to docs/api/docs/config_merge_logic.md index 83fda0f1..2ea265fa 100644 --- a/docs/api/docs/config-merge-logic.md +++ b/docs/api/docs/config_merge_logic.md @@ -1,3 +1,5 @@ +# Config Merge Logic + When we have competing config message, we need completely consistent logic for merging them, that is both forwards and backwards compatible (that is: old clients with new data, and new clients with old data, need to produce an agreeable result). diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml index c30374a4..28082e8c 100644 --- a/docs/api/mkdocs.yml +++ b/docs/api/mkdocs.yml @@ -1,23 +1,6 @@ # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json site_name: Libsession Utils API Functions repo_url: https://github.com/session-foundation/libsession-util -nav: - - Getting Started: index.md - - Base: base.md - - Community: community.md - - Config: config.md - - Config Merge Logic: config-merge-logic.md - - Contacts: contacts.md - - Convo: convo_info_volatile.md - - Encrypt: encrypt.md - # TODO Currently doesn't have the new API comments - # - Error: error.md - - Groups: groups.md - - Profile Pictures: profile_pic.md - - User Groups: user_groups.md - - User Profile: user_profile.md - # TODO Currently doesn't have the new API comments - # - Utils: util.md extra: social: - icon: fontawesome/brands/github From 0a4e3e4f717c0e74d63282fe31ffeb2a3fa7e118 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:30:43 +1100 Subject: [PATCH 499/572] feat: updated drone api task --- .drone.jsonnet | 13 +++++++------ utils/ci/drone-docs-upload.sh | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index a7a19663..66565e9d 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -331,21 +331,22 @@ local static_build(name, type: 'docker', steps: [{ name: 'build', - image: 'node:19-bullseye', + image: docker_base + 'debian-stable', pull: 'always', environment: { SSH_KEY: { from_secret: 'SSH_KEY' } }, commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', - apt_get_quiet + ' install -y python3-requests rsync', - 'npm i docsify-cli docsify-themeable docsify-katex@1.4.4 katex marked@4', + apt_get_quiet + ' install -y rsync', + 'python3 -m pip install --upgrade pip', + 'pip install -r requirements.txt', 'cd docs/api/', - 'export NODE_PATH=node_modules', - 'make', + 'make build-all', '../../utils/ci/drone-docs-upload.sh', ], }], - trigger: { branch: ['dev'], event: ['push'] }, + // FIXME DO NOT MERGE + // trigger: { branch: ['dev'], event: ['push'] }, }, // Various debian builds diff --git a/utils/ci/drone-docs-upload.sh b/utils/ci/drone-docs-upload.sh index cb144a73..3edebe18 100755 --- a/utils/ci/drone-docs-upload.sh +++ b/utils/ci/drone-docs-upload.sh @@ -18,8 +18,8 @@ chmod 600 ~/ssh_key sftp -i ~/ssh_key -b - -o StrictHostKeyChecking=off apidocs@chianina.oxen.io < Date: Mon, 17 Mar 2025 11:50:36 +1100 Subject: [PATCH 500/572] feat: adjusted content and right sidebar widths --- docs/api/docs/stylesheets/extra.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/api/docs/stylesheets/extra.css b/docs/api/docs/stylesheets/extra.css index ab47b65a..0401811c 100644 --- a/docs/api/docs/stylesheets/extra.css +++ b/docs/api/docs/stylesheets/extra.css @@ -6,3 +6,12 @@ --md-primary-fg-color--light: #00f782; --md-primary-fg-color--dark: #00aa59; } + +.md-sidebar.md-sidebar--secondary { + width: unset; + max-width: 500px; +} + +.md-content { + min-width: 40vw; +} From 36f3b2884666009288509b5ebdc53e59e162c5fe Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:50:49 +1100 Subject: [PATCH 501/572] feat: added copyright --- docs/api/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/mkdocs.yml b/docs/api/mkdocs.yml index 28082e8c..297b4359 100644 --- a/docs/api/mkdocs.yml +++ b/docs/api/mkdocs.yml @@ -1,6 +1,7 @@ # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json site_name: Libsession Utils API Functions repo_url: https://github.com/session-foundation/libsession-util +copyright: Copyright © 2025 - Session Technology Foundation (Session Technology Stiftung) extra: social: - icon: fontawesome/brands/github From 1faf516fe7f168b40c19399cc083dbb6913e5520 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 11:59:10 +1100 Subject: [PATCH 502/572] fix: using virtual environment for installing deps --- .drone.jsonnet | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index 66565e9d..e7ae5d5f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -339,6 +339,8 @@ local static_build(name, apt_get_quiet + ' update', apt_get_quiet + ' install -y rsync', 'python3 -m pip install --upgrade pip', + 'python -m venv .venv', + 'source .venv/bin/activate', 'pip install -r requirements.txt', 'cd docs/api/', 'make build-all', From 3e8b6f154d10d552de4d85a8522ac86d4fe6df31 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 12:00:05 +1100 Subject: [PATCH 503/572] fix: remove pip upgrade step --- .drone.jsonnet | 1 - 1 file changed, 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index e7ae5d5f..4405590a 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -338,7 +338,6 @@ local static_build(name, 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', apt_get_quiet + ' install -y rsync', - 'python3 -m pip install --upgrade pip', 'python -m venv .venv', 'source .venv/bin/activate', 'pip install -r requirements.txt', From 9e2306b44addb7ba4dce1a84914c5714504a94c1 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 12:01:02 +1100 Subject: [PATCH 504/572] fix: use python3 alias --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 4405590a..abe2240b 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -338,7 +338,7 @@ local static_build(name, 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', apt_get_quiet + ' install -y rsync', - 'python -m venv .venv', + 'python3 -m venv .venv', 'source .venv/bin/activate', 'pip install -r requirements.txt', 'cd docs/api/', From 60233a9932ac020422acdfa62fd2daa75ce7afb0 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 12:04:02 +1100 Subject: [PATCH 505/572] feat: install system wide python venv package --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index abe2240b..892c4b9c 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -337,7 +337,7 @@ local static_build(name, commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', - apt_get_quiet + ' install -y rsync', + apt_get_quiet + ' install -y rsync python3-venv', 'python3 -m venv .venv', 'source .venv/bin/activate', 'pip install -r requirements.txt', From aa30d07c1c358417f3314a2180eaa81c7c842525 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Mar 2025 12:09:46 +1100 Subject: [PATCH 506/572] Updated to the latest libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 4fd48c90..e1ca1879 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 4fd48c908618be22d4a5b1744f6f0fb9e4ac9dcc +Subproject commit e1ca18798f37192f2a89a380c6d42b4980e58397 From 9b066d1798394856fe17eb3a16a80890388501c5 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 13:08:36 +1100 Subject: [PATCH 507/572] fix: make sure to make the venv inside the correct directory --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 892c4b9c..b27828a1 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -338,10 +338,10 @@ local static_build(name, 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', apt_get_quiet + ' install -y rsync python3-venv', + 'cd docs/api/', 'python3 -m venv .venv', 'source .venv/bin/activate', 'pip install -r requirements.txt', - 'cd docs/api/', 'make build-all', '../../utils/ci/drone-docs-upload.sh', ], From d3b42b3632d59d7c6358b2230ef02ae8233a7de1 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 13:14:56 +1100 Subject: [PATCH 508/572] fix: source venv activiate using dot which is POSIX-compliant added venv to gitignore --- .drone.jsonnet | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index b27828a1..56058933 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -340,7 +340,7 @@ local static_build(name, apt_get_quiet + ' install -y rsync python3-venv', 'cd docs/api/', 'python3 -m venv .venv', - 'source .venv/bin/activate', + '. .venv/bin/activate', 'pip install -r requirements.txt', 'make build-all', '../../utils/ci/drone-docs-upload.sh', diff --git a/.gitignore b/.gitignore index 1fe18bd5..2ab5aea9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.cache/ /.vscode/ .DS_STORE +/.venv/ From 14b294d25dcf70fee1c2bfe44a2e793fb405930a Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 13:18:13 +1100 Subject: [PATCH 509/572] feat: move venv ignore into docs api folder --- .gitignore | 1 - docs/api/.gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2ab5aea9..1fe18bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ /.cache/ /.vscode/ .DS_STORE -/.venv/ diff --git a/docs/api/.gitignore b/docs/api/.gitignore index 851229e6..f1734b39 100644 --- a/docs/api/.gitignore +++ b/docs/api/.gitignore @@ -2,3 +2,4 @@ api/* api dist *.mdx +.venv From 5f96d860cdeac0e8c4ba64d3505902052ebf5ade Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 13:42:48 +1100 Subject: [PATCH 510/572] fix: dev-c make command --- docs/api/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/Makefile b/docs/api/Makefile index 571a2141..c2463f91 100644 --- a/docs/api/Makefile +++ b/docs/api/Makefile @@ -28,7 +28,7 @@ serve-cpp: # NOTE: Any changes to dist/ will be hot-reloaded but are not tracked by git. .PHONY: dev-c -dev-c: build-c +dev-c: build-h cd dist/libsession-util-c && mkdocs serve && cd - .PHONY: dev-cpp From 099e351e292ca58524aee9fe7a8ebc8df052641b Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 17 Mar 2025 13:46:36 +1100 Subject: [PATCH 511/572] fix: revert triggering api docs in drone unless we are pushing to dev testing is finished and we confirm it works --- .drone.jsonnet | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 56058933..84346154 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -346,8 +346,7 @@ local static_build(name, '../../utils/ci/drone-docs-upload.sh', ], }], - // FIXME DO NOT MERGE - // trigger: { branch: ['dev'], event: ['push'] }, + trigger: { branch: ['dev'], event: ['push'] }, }, // Various debian builds From b9823cccc6ca6c9a80ec765ec636401172ea075e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 17 Mar 2025 19:06:10 -0300 Subject: [PATCH 512/572] Bump libquic to silence gcc stringop warnings --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index e1ca1879..001e4e8d 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit e1ca18798f37192f2a89a380c6d42b4980e58397 +Subproject commit 001e4e8d36dc9b7b423798b13cda9ce952bfb7c7 From 44d2b7f4fd23450391298c39e1c2fb51cc3ae69c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 17 Mar 2025 19:06:37 -0300 Subject: [PATCH 513/572] Fix inconsistency with == and inequalities service_nodes have an overridden == that compares other fields, but then *doesn't* have an override for <=>, which means inequalities would fall back to the parent RemoteAddress class which knows nothing about the extra fields, and means `==` and `<=` (and similar) could be inconsistent. This adds a operator<=> so that that doesn't happen. --- include/session/network.hpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index e67ea22b..66ca2e60 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include "onionreq/builder.hpp" @@ -76,11 +76,14 @@ struct service_node : public oxen::quic::RemoteAddress { return *this; } - bool operator==(const service_node& other) const { - return static_cast(*this) == - static_cast(other) && - storage_server_version == other.storage_server_version && swarm_id == other.swarm_id; + auto operator<=>(const service_node& other) const { + auto cmp = oxen::quic::RemoteAddress::operator<=>(other); + if (cmp == 0) + cmp = std::tie(storage_server_version, swarm_id) <=> + std::tie(other.storage_server_version, other.swarm_id); + return cmp; } + bool operator==(const service_node& other) const { return (*this <=> other) == 0; } }; struct connection_info { From 086a310f9303bf1c1394b6b77d59ea30c4bfb60f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 18 Mar 2025 10:53:30 +1100 Subject: [PATCH 514/572] Fixed broken logging test --- tests/test_logging.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 43ded9f1..8bae88d6 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -113,12 +113,8 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); -#if defined(__APPLE__) && defined(__clang__) && defined(RELEASE_BUILD) - CHECK(simple_logs.front().find("Started libevent") != std::string::npos); -#else + //CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); -#endif CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); } #endif \ No newline at end of file From 54a11f7779dcec6fc83a30cd8ecab9dd768f1bb0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 18 Mar 2025 11:01:33 +1100 Subject: [PATCH 515/572] Debug macOS release logging issue on CI --- tests/test_logging.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 8bae88d6..6613da08 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -113,7 +113,7 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - //CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); } From 9636d5abc46658fe3ac344e66099c989096b378f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 18 Mar 2025 11:25:30 +1100 Subject: [PATCH 516/572] Updated logs for Apple Release builds (as they are different for some reason) --- tests/test_logging.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 6613da08..bea03acb 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -113,8 +113,12 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + //CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); +#if defined(__APPLE__) && defined(__clang__) && defined(RELEASE_BUILD) + CHECK(simple_logs.front().find("libevent loop is started") != std::string::npos); +#else CHECK(simple_logs.front().find("Starting libevent") != std::string::npos); +#endif CHECK(simple_logs.back().find("Loop shutdown complete") != std::string::npos); } #endif \ No newline at end of file From 328e2018c996b8ae72787b1bc5f1bf67f1081bdf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 16:38:57 +1100 Subject: [PATCH 517/572] Retyped code to use vector & span instead of ustring & ustring_view --- include/session/blinding.hpp | 52 ++-- include/session/config.hpp | 21 +- include/session/config/base.hpp | 98 ++++--- include/session/config/community.hpp | 36 ++- include/session/config/contacts.hpp | 4 +- .../session/config/convo_info_volatile.hpp | 20 +- include/session/config/encrypt.hpp | 26 +- include/session/config/groups/info.hpp | 10 +- include/session/config/groups/keys.hpp | 87 +++--- include/session/config/groups/members.hpp | 6 +- include/session/config/profile_pic.hpp | 14 +- include/session/config/protos.hpp | 12 +- include/session/config/user_groups.hpp | 21 +- include/session/config/user_profile.hpp | 8 +- include/session/curve25519.hpp | 4 +- include/session/ed25519.hpp | 12 +- include/session/hash.hpp | 5 +- include/session/multi_encrypt.hpp | 148 +++++----- include/session/network.hpp | 2 +- include/session/onionreq/key_types.hpp | 6 +- include/session/onionreq/parser.hpp | 4 +- include/session/random.hpp | 2 +- include/session/session_encrypt.hpp | 84 +++--- include/session/types.hpp | 11 +- include/session/util.hpp | 164 ++++++----- include/session/xed25519.hpp | 15 +- src/blinding.cpp | 136 +++++---- src/config.cpp | 49 ++-- src/config/base.cpp | 108 +++---- src/config/community.cpp | 22 +- src/config/contacts.cpp | 10 +- src/config/convo_info_volatile.cpp | 23 +- src/config/encrypt.cpp | 45 +-- src/config/groups/info.cpp | 14 +- src/config/groups/keys.cpp | 265 ++++++++++-------- src/config/groups/members.cpp | 10 +- src/config/internal.cpp | 27 +- src/config/internal.hpp | 26 +- src/config/protos.cpp | 33 ++- src/config/user_groups.cpp | 48 ++-- src/config/user_profile.cpp | 13 +- src/curve25519.cpp | 10 +- src/ed25519.cpp | 27 +- src/hash.cpp | 14 +- src/multi_encrypt.cpp | 132 +++++---- src/network.cpp | 24 +- src/onionreq/builder.cpp | 13 +- src/onionreq/hop_encryption.cpp | 12 +- src/onionreq/parser.cpp | 13 +- src/random.cpp | 4 +- src/session_encrypt.cpp | 201 +++++++------ src/xed25519.cpp | 30 +- tests/static_bundle.cpp | 2 +- tests/swarm-auth-test.cpp | 48 ++-- tests/test_blinding.cpp | 101 +++---- tests/test_bugs.cpp | 32 ++- tests/test_compression.cpp | 18 +- tests/test_config_contacts.cpp | 10 +- tests/test_config_convo_info_volatile.cpp | 11 +- tests/test_config_user_groups.cpp | 36 +-- tests/test_config_userprofile.cpp | 22 +- tests/test_configdata.cpp | 135 +++++---- tests/test_curve25519.cpp | 8 +- tests/test_ed25519.cpp | 22 +- tests/test_encrypt.cpp | 6 +- tests/test_group_info.cpp | 29 +- tests/test_group_keys.cpp | 98 ++++--- tests/test_group_members.cpp | 12 +- tests/test_hash.cpp | 43 +-- tests/test_logging.cpp | 2 +- tests/test_multi_encrypt.cpp | 226 +++++++-------- tests/test_network.cpp | 17 +- tests/test_onionreq.cpp | 50 ++-- tests/test_proto.cpp | 6 +- tests/test_session_encrypt.cpp | 237 ++++++++-------- tests/test_xed25519.cpp | 46 ++- tests/utils.hpp | 44 +-- 77 files changed, 1867 insertions(+), 1575 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index ff611535..e243ce4f 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -59,12 +59,13 @@ namespace session { /// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes server pk in bytes, not hex. -uc32 blind15_factor(ustring_view server_pk); +uc32 blind15_factor(std::span server_pk); /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). -uc32 blind25_factor(ustring_view session_id, ustring_view server_pk); +uc32 blind25_factor( + std::span session_id, std::span server_pk); /// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted /// and returned are hex-encoded. @@ -75,7 +76,8 @@ std::array blind15_id(std::string_view session_id, std::string_v /// session_id here may be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). Only /// the *positive* possible ID is returned: the alternative can be computed by flipping the highest /// bit of byte 32, i.e.: `result[32] ^= 0x80`. -ustring blind15_id(ustring_view session_id, ustring_view server_pk); +std::vector blind15_id( + std::span session_id, std::span server_pk); /// Computes the 25-blinded id from a session id and server pubkey. Values accepted and /// returned are hex-encoded. @@ -84,7 +86,8 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk); /// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a /// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). -ustring blind25_id(ustring_view session_id, ustring_view server_pk); +std::vector blind25_id( + std::span session_id, std::span server_pk); /// Computes the 15-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 /// pubkey behind a (X25519) Session ID. Unlike blind15_id, knowing the true Ed25519 pubkey allows @@ -96,8 +99,10 @@ ustring blind25_id(ustring_view session_id, ustring_view server_pk); /// `blinded25_id_from_ed`, but unlike the 25 version, this value is not read if non-empty, and is /// not an optimization (that is: it is purely for convenience and is no more efficient to use this /// than it is to compute it yourself). -ustring blinded15_id_from_ed( - ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); +std::vector blinded15_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::vector* session_id = nullptr); /// Computes the 25-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 /// pubkey behind a (X25519) Session ID. This will be the same as blind25_id (if given the X25519 @@ -109,8 +114,10 @@ ustring blinded15_id_from_ed( /// containing the precomputed value (to avoid needing to compute it again). If unknown but needed /// then a pointer to an empty string can be given to computed and stored the value here. Otherwise /// (if omitted or nullptr) then the value will temporarily computed within the function. -ustring blinded25_id_from_ed( - ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); +std::vector blinded25_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::vector* session_id = nullptr); /// Computes a 15-blinded key pair. /// @@ -123,7 +130,9 @@ ustring blinded25_id_from_ed( /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. std::pair blind15_key_pair( - ustring_view ed25519_sk, ustring_view server_pk, uc32* k = nullptr); + std::span ed25519_sk, + std::span server_pk, + uc32* k = nullptr); /// Computes a 25-blinded key pair. /// @@ -138,7 +147,9 @@ std::pair blind15_key_pair( /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. std::pair blind25_key_pair( - ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); + std::span ed25519_sk, + std::span server_pk, + uc32* k_prime = nullptr); /// Computes a version-blinded key pair. /// @@ -147,7 +158,7 @@ std::pair blind25_key_pair( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind_version_key_pair(ustring_view ed25519_sk); +std::pair blind_version_key_pair(std::span ed25519_sk); /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). @@ -157,7 +168,10 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk); /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message); +std::vector blind15_sign( + std::span ed25519_sk, + std::string_view server_pk_in, + std::span message); /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). @@ -167,7 +181,10 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +std::vector blind25_sign( + std::span ed25519_sk, + std::string_view server_pk, + std::span message); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. @@ -175,19 +192,20 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustrin /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), unix timestamp, method, path, and /// optional body. /// Returns the version-blinded signature. -ustring blind_version_sign_request( - ustring_view ed25519_sk, +std::vector blind_version_sign_request( + std::span ed25519_sk, uint64_t timestamp, std::string_view method, std::string_view path, - std::optional body); + std::optional> body); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), current platform and unix timestamp. /// Returns the version-blinded signature. -ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp); +std::vector blind_version_sign( + std::span ed25519_sk, Platform platform, uint64_t timestamp); /// Takes in a standard session_id and returns a flag indicating whether it matches the given /// blinded_id for a given server_pk. diff --git a/include/session/config.hpp b/include/session/config.hpp index eaf418e5..36308893 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -108,10 +108,12 @@ class ConfigMessage { /// message. It can also throw to abort message construction (that is: returning false skips /// the message when loading multiple messages, but can still continue with other messages; /// throwing aborts the entire construction). - using verify_callable = std::function; + using verify_callable = std::function data, std::span signature)>; /// Signing function: this is passed the data to be signed and returns the 64-byte signature. - using sign_callable = std::function; + using sign_callable = + std::function(std::span data)>; ConfigMessage(); ConfigMessage(const ConfigMessage&) = default; @@ -124,7 +126,7 @@ class ConfigMessage { /// Initializes a config message by parsing a serialized message. Throws on any error. See the /// vector version below for argument descriptions. explicit ConfigMessage( - ustring_view serialized, + std::span serialized, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -160,7 +162,7 @@ class ConfigMessage { /// `[](size_t, const auto& e) { throw e; }` can be used to make any parse error of any message /// fatal. explicit ConfigMessage( - const std::vector& configs, + const std::vector>& configs, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -231,10 +233,11 @@ class ConfigMessage { /// typically for a local serialization value that isn't being pushed to the server). Note that /// signing is always disabled if there is no signing callback set, regardless of the value of /// this argument. - virtual ustring serialize(bool enable_signing = true); + virtual std::vector serialize(bool enable_signing = true); protected: - ustring serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true); + std::vector serialize_impl( + const oxenc::bt_dict& diff, bool enable_signing = true); }; // Constructor tag @@ -282,7 +285,7 @@ class MutableConfigMessage : public ConfigMessage { /// constructor only increments seqno once while the indirect version would increment twice in /// the case of a required merge conflict resolution. explicit MutableConfigMessage( - const std::vector& configs, + const std::vector>& configs, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -292,7 +295,7 @@ class MutableConfigMessage : public ConfigMessage { /// take an error handler and instead always throws on parse errors (the above also throws for /// an erroneous single message, but with a less specific "no valid config messages" error). explicit MutableConfigMessage( - ustring_view config, + std::span config, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS); @@ -339,7 +342,7 @@ class MutableConfigMessage : public ConfigMessage { const hash_t& hash() override; protected: - const hash_t& hash(ustring_view serialized); + const hash_t& hash(std::span serialized); void increment_impl(); }; diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 92eab8d7..2821ee83 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -78,8 +78,8 @@ class ConfigSig { // // Throws if given invalid data (i.e. wrong key size, or mismatched pubkey/secretkey). void init_sig_keys( - std::optional ed25519_pubkey, - std::optional ed25519_secretkey); + std::optional> ed25519_pubkey, + std::optional> ed25519_secretkey); public: virtual ~ConfigSig() = default; @@ -107,7 +107,7 @@ class ConfigSig { /// Inputs: /// - `secret` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey /// concatenated together) that sets both the secret key and public key. - void set_sig_keys(ustring_view secret); + void set_sig_keys(std::span secret); /// API: base/ConfigSig::set_sig_pubkey /// @@ -117,7 +117,7 @@ class ConfigSig { /// /// Inputs: /// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages - void set_sig_pubkey(ustring_view pubkey); + void set_sig_pubkey(std::span pubkey); /// API: base/ConfigSig::get_sig_pubkey /// @@ -176,9 +176,9 @@ 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 dump = std::nullopt, - std::optional ed25519_pubkey = std::nullopt, - std::optional ed25519_secretkey = std::nullopt); + std::optional> dump = std::nullopt, + std::optional> ed25519_pubkey = std::nullopt, + std::optional> ed25519_secretkey = std::nullopt); // Initializes the base config object with dump data and keys; this is typically invoked by the // constructor, but is exposed to subclasses so that they can delay initial processing by @@ -189,9 +189,9 @@ class ConfigBase : public ConfigSig { // // This method must not be called outside derived class construction! void init( - std::optional dump = std::nullopt, - std::optional ed25519_pubkey = std::nullopt, - std::optional ed25519_secretkey = std::nullopt); + std::optional> dump = std::nullopt, + std::optional> ed25519_pubkey = std::nullopt, + std::optional> ed25519_secretkey = std::nullopt); // Tracks whether we need to dump again; most mutating methods should set this to true (unless // calling set_state, which sets to to true implicitly). @@ -459,15 +459,18 @@ class ConfigBase : public ConfigSig { /// API: base/ConfigBase::DictFieldProxy::uview /// - /// Returns the value as a ustring_view, if it exists and is a string; nullopt otherwise. + /// Returns the value as a std::span, if it exists and is a string; + /// nullopt otherwise. /// /// Inputs: None /// /// Outputs: - /// - `std::optional` -- Returns a value as a view if it exists - std::optional uview() const { + /// - `std::optional>` -- Returns a value as a view if it + /// exists + std::optional> uview() const { if (auto* s = get_clean()) - return ustring_view{reinterpret_cast(s->data()), s->size()}; + return std::span{ + reinterpret_cast(s->data()), s->size()}; return std::nullopt; } @@ -574,17 +577,17 @@ class ConfigBase : public ConfigSig { /// - `value` -- replaces current value with given string view void operator=(std::string_view value) { *this = std::string{value}; } - /// API: base/ConfigBase::DictFieldProxy::operator=(ustring_view) + /// API: base/ConfigBase::DictFieldProxy::operator=(std::span) /// - /// Replaces the current value with the given ustring_view. This also auto-vivifies any - /// intermediate dicts needed to reach the given key, including replacing non-dict values if - /// they currently exist along the path (this makes a copy). + /// Replaces the current value with the given std::span. This also + /// auto-vivifies any intermediate dicts needed to reach the given key, including replacing + /// non-dict values if they currently exist along the path (this makes a copy). /// /// Inputs: - /// - `value` -- replaces current value with given ustring_view + /// - `value` -- replaces current value with given std::span /// - /// Same as above, but takes a ustring_view - void operator=(ustring_view value) { + /// Same as above, but takes a std::span + void operator=(std::span value) { *this = std::string{reinterpret_cast(value.data()), value.size()}; } @@ -786,7 +789,7 @@ class ConfigBase : public ConfigSig { /// 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( - const std::vector>& configs); + const std::vector>>& configs); /// API: base/ConfigBase::extra_data /// @@ -828,7 +831,7 @@ class ConfigBase : public ConfigSig { /// /// Inputs: /// - `ed25519_secret_key` -- key is loaded for encryption - void load_key(ustring_view ed25519_secretkey); + void load_key(std::span ed25519_secretkey); public: virtual ~ConfigBase() = default; @@ -917,9 +920,9 @@ class ConfigBase : public ConfigSig { /// Declaration: /// ```cpp /// std::vector merge( - /// const std::vector>& configs); + /// const std::vector>>& configs); /// std::vector merge( - /// const std::vector>& configs); + /// const std::vector>>& configs); /// ``` /// /// Inputs: @@ -931,11 +934,13 @@ class ConfigBase : public ConfigSig { /// 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(const std::vector>& configs); + std::vector merge( + const std::vector>>& configs); - // Same as above, but takes values as ustrings (because sometimes that is more convenient). + // Same as above, but takes values as std::spans (because sometimes that is + // more convenient). std::vector merge( - const std::vector>& configs); + const std::vector>>& configs); /// API: base/ConfigBase::is_dirty /// @@ -1076,11 +1081,12 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::tuple>` - Returns a tuple containing + /// - `std::tuple, std::vector>` - Returns a + /// tuple containing /// - `seqno_t` -- sequence number - /// - `ustring` -- data message to push to the server + /// - `std::vector` -- data message to push to the server /// - `std::vector` -- list of known message hashes - virtual std::tuple> push(); + virtual std::tuple, std::vector> push(); /// API: base/ConfigBase::confirm_pushed /// @@ -1114,8 +1120,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `ustring` -- Returns binary data of the state dump - ustring dump(); + /// - `std::vector` -- Returns binary data of the state dump + std::vector dump(); /// API: base/ConfigBase::make_dump /// @@ -1126,8 +1132,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `ustring` -- Returns binary data of the state dump - ustring make_dump() const; + /// - `std::vector` -- Returns binary data of the state dump + std::vector make_dump() const; /// API: base/ConfigBase::needs_dump /// @@ -1163,13 +1169,16 @@ class ConfigBase : public ConfigSig { /// Will throw a std::invalid_argument if the key is not 32 bytes. /// /// Inputs: - /// - `ustring_view key` -- 32 byte binary key + /// - `std::span key` -- 32 byte binary key /// - `high_priority` -- Whether to add to front or back of key list. If true then key is added /// to beginning and replace highest-priority key for encryption /// - `dirty_config` -- if true then mark the config as dirty (incrementing seqno and needing a /// push) if the first key (i.e. the key used for encryption) is changed as a result of this /// call. Ignored if the config is not modifiable. - void add_key(ustring_view key, bool high_priority = true, bool dirty_config = false); + void add_key( + std::span key, + bool high_priority = true, + bool dirty_config = false); /// API: base/ConfigBase::clear_keys /// @@ -1201,7 +1210,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if found and removed - bool remove_key(ustring_view key, size_t from = 0, bool dirty_config = false); + bool remove_key(std::span key, size_t from = 0, bool dirty_config = false); /// API: base/ConfigBase::replace_keys /// @@ -1214,7 +1223,8 @@ class ConfigBase : public ConfigSig { /// - `dirty_config` -- if true then set the config status to dirty (incrementing seqno and /// requiring a repush) if the old and new first key are not the same. Ignored if the config /// is not modifiable. - void replace_keys(const std::vector& new_keys, bool dirty_config = false); + void replace_keys( + const std::vector>& new_keys, bool dirty_config = false); /// API: base/ConfigBase::get_keys /// @@ -1228,8 +1238,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::vector` -- Returns vector of encryption keys - std::vector get_keys() const; + /// - `std::vector>` -- Returns vector of encryption keys + std::vector> get_keys() const; /// API: base/ConfigBase::key_count /// @@ -1250,7 +1260,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if it does exist - bool has_key(ustring_view key) const; + bool has_key(std::span key) const; /// API: base/ConfigBase::key /// @@ -1262,8 +1272,8 @@ class ConfigBase : public ConfigSig { /// - `i` -- keys position in key list /// /// Outputs: - /// - `ustring_view` -- binary data of the key - ustring_view key(size_t i = 0) const { + /// - `std::span` -- binary data of the key + std::span key(size_t i = 0) const { assert(i < _keys.size()); return {_keys[i].data(), _keys[i].size()}; } diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index 8f666a55..232e53e3 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -27,7 +27,10 @@ struct community { // Constructs an empty community struct from url, room, and pubkey. `base_url` will be // normalized if not already. pubkey is 32 bytes. - community(std::string_view base_url, std::string_view room, ustring_view pubkey); + community( + std::string_view base_url, + std::string_view room, + std::span pubkey); // Same as above, but takes pubkey as an encoded (hex or base32z or base64) string. community(std::string_view base_url, std::string_view room, std::string_view pubkey_encoded); @@ -89,13 +92,13 @@ struct community { /// /// Declaration: /// ```cpp - /// void set_pubkey(ustring_view pubkey); + /// void set_pubkey(std::span pubkey); /// void set_pubkey(std::string_view pubkey); /// ``` /// /// Inputs: /// - `pubkey` -- Pubkey to be stored - void set_pubkey(ustring_view pubkey); + void set_pubkey(std::span pubkey); void set_pubkey(std::string_view pubkey); /// API: community/community::base_url @@ -137,8 +140,8 @@ struct community { /// Inputs: None /// /// Outputs: - /// - `const ustring&` -- Returns the pubkey - const ustring& pubkey() const { return pubkey_; } + /// - `const std::vector&` -- Returns the pubkey + const std::vector& pubkey() const { return pubkey_; } /// API: community/community::pubkey_hex /// @@ -180,7 +183,8 @@ struct community { /// - `std::string` -- Returns the Full URL std::string full_url() const; - /// API: community/community::full_url(std::string_view,std::string_view,ustring_view) + /// API: community/community::full_url(std::string_view,std::string_view,std::span) /// /// Constructs and returns the full URL for a given base, room, and pubkey. Currently this /// returns it in a Session-compatibility form (https://server.com/RoomName?public_key=....), @@ -195,7 +199,9 @@ struct community { /// Outputs: /// - `std::string` -- Returns the Full URL static std::string full_url( - std::string_view base_url, std::string_view room, ustring_view pubkey); + std::string_view base_url, + std::string_view room, + std::span pubkey); /// API: community/community::canonical_url /// @@ -263,8 +269,9 @@ struct community { /// - `std::tuple` -- Tuple of 3 components of the url /// - `std::string` -- canonical url, normalized /// - `std::string` -- room name, *not* normalized - /// - `ustring` -- binary of the server pubkey - static std::tuple parse_full_url(std::string_view full_url); + /// - `std::vector` -- binary of the server pubkey + static std::tuple> parse_full_url( + std::string_view full_url); /// API: community/community::parse_partial_url /// @@ -278,9 +285,10 @@ struct community { /// - `std::tuple` -- Tuple of 3 components of the url /// - `std::string` -- canonical url, normalized /// - `std::string` -- room name, *not* normalized - /// - `std::optional` -- optional binary of the server pubkey if present - static std::tuple> parse_partial_url( - std::string_view url); + /// - `std::optional>` -- optional binary of the server pubkey if + /// present + static std::tuple>> + parse_partial_url(std::string_view url); protected: // The canonical base url and room (i.e. lower-cased, URL cleaned up): @@ -289,7 +297,7 @@ struct community { // `someroom` and this could `SomeRoom`). Omitted if not available. std::optional localized_room_; // server pubkey - ustring pubkey_; + std::vector pubkey_; // Construction without a pubkey for when pubkey isn't known yet but will be set shortly // after constructing (or when isn't needed, such as when deleting). @@ -341,7 +349,7 @@ struct comm_iterator_helper { continue; } - ustring_view pubkey{ + std::span pubkey{ reinterpret_cast(pubkey_raw->data()), pubkey_raw->size()}; if (!it_room) { diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index aae34fba..757e6cd0 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -119,7 +119,9 @@ class Contacts : public ConfigBase { /// /// Outputs: /// - `Contact` - Constructor - Contacts(ustring_view ed25519_secretkey, std::optional dumped); + Contacts( + std::span ed25519_secretkey, + std::optional> dumped); /// API: contacts/Contacts::storage_namespace /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index b9884e68..4e2e0b12 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -171,7 +171,9 @@ 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( + std::span ed25519_secretkey, + std::optional> dumped); /// API: convo_info_volatile/ConvoInfoVolatile::storage_namespace /// @@ -224,11 +226,12 @@ class ConvoInfoVolatile : public ConfigBase { /// Inputs: None /// /// Outputs: - /// - `std::tuple>` - Returns a tuple containing + /// - `std::tuple, std::vector>` - Returns a + /// tuple containing /// - `seqno_t` -- sequence number - /// - `ustring` -- data message to push to the server + /// - `std::vector` -- data message to push to the server /// - `std::vector` -- list of known message hashes - std::tuple> push() override; + std::tuple, std::vector> push() override; /// API: convo_info_volatile/ConvoInfoVolatile::get_1to1 /// @@ -351,7 +354,8 @@ class ConvoInfoVolatile : public ConfigBase { /// std::string_view base_url, std::string_view room, std::string_view pubkey_hex) /// const; /// convo::community get_or_construct_community( - /// std::string_view base_url, std::string_view room, ustring_view pubkey) const; + /// std::string_view base_url, std::string_view room, std::span + /// pubkey) const; /// ``` /// /// Inputs: @@ -364,7 +368,9 @@ class ConvoInfoVolatile : public ConfigBase { convo::community get_or_construct_community( std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const; convo::community get_or_construct_community( - std::string_view base_url, std::string_view room, ustring_view pubkey) const; + std::string_view base_url, + std::string_view room, + std::span pubkey) const; /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_community(full_url) /// @@ -412,7 +418,7 @@ class ConvoInfoVolatile : public ConfigBase { // Drills into the nested dicts to access community details; if the second argument is // non-nullptr then it will be set to the community's pubkey, if it exists. DictFieldProxy community_field( - const convo::community& og, ustring_view* get_pubkey = nullptr) const; + const convo::community& og, std::span* get_pubkey = nullptr) const; public: /// API: convo_info_volatile/ConvoInfoVolatile::erase_1to1 diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index 1fd9c771..662edb22 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -33,8 +33,11 @@ namespace session::config { /// - `domain` -- short string for the keyed hash /// /// Outputs: -/// - `ustring` -- Returns the encrypted message bytes -ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain); +/// - `std::vector` -- Returns the encrypted message bytes +std::vector encrypt( + std::span message, + std::span key_base, + std::string_view domain); /// API: encrypt/encrypt_inplace /// @@ -45,7 +48,10 @@ ustring encrypt(ustring_view message, ustring_view key_base, std::string_view do /// - `message` -- message to encrypt /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash -void encrypt_inplace(ustring& message, ustring_view key_base, std::string_view domain); +void encrypt_inplace( + std::vector& message, + std::span key_base, + std::string_view domain); /// Constant amount of extra bytes required to be appended when encrypting. constexpr size_t ENCRYPT_DATA_OVERHEAD = 40; // ABYTES + NPUBBYTES @@ -67,8 +73,11 @@ struct decrypt_error : std::runtime_error { /// - `domain` -- short string for the keyed hash /// /// Outputs: -/// - `ustring` -- Returns the decrypt message bytes -ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain); +/// - `std::vector` -- Returns the decrypt message bytes +std::vector decrypt( + std::span ciphertext, + std::span key_base, + std::string_view domain); /// API: encrypt/decrypt_inplace /// @@ -79,7 +88,10 @@ ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view /// - `ciphertext` -- message to decrypt /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash -void decrypt_inplace(ustring& ciphertext, ustring_view key_base, std::string_view domain); +void decrypt_inplace( + std::vector& ciphertext, + std::span key_base, + std::string_view domain); /// Returns the target size of the message with padding, assuming an additional `overhead` bytes of /// overhead (e.g. from encrypt() overhead) will be appended. Will always return a value >= s + @@ -105,6 +117,6 @@ inline constexpr size_t padded_size(size_t s, size_t overhead = ENCRYPT_DATA_OVE /// - `data` -- the data; this is modified in place /// - `overhead` -- encryption overhead to account for to reach the desired padded size. The /// default, if omitted, is the space used by the `encrypt()` function defined above. -void pad_message(ustring& data, size_t overhead = ENCRYPT_DATA_OVERHEAD); +void pad_message(std::vector& data, size_t overhead = ENCRYPT_DATA_OVERHEAD); } // namespace session::config diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 2aabf458..cc1c21b8 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -55,9 +55,9 @@ class Info : public ConfigBase { /// push config changes. /// - `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()`. - Info(ustring_view ed25519_pubkey, - std::optional ed25519_secretkey, - std::optional dumped); + Info(std::span ed25519_pubkey, + std::optional> ed25519_secretkey, + std::optional> dumped); /// API: groups/Info::storage_namespace /// @@ -174,7 +174,7 @@ class Info : public ConfigBase { /// /// Declaration: /// ```cpp - /// void set_profile_pic(std::string_view url, ustring_view key); + /// void set_profile_pic(std::string_view url, std::span key); /// void set_profile_pic(profile_pic pic); /// ``` /// @@ -184,7 +184,7 @@ class Info : public ConfigBase { /// - `key` -- Decryption key /// - Second function: /// - `pic` -- Profile pic object - void set_profile_pic(std::string_view url, ustring_view key); + void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); /// API: groups/Info::set_expiry_timer diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index fbe5e773..b2f1b5af 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -120,13 +120,13 @@ class Keys : public ConfigSig { void set_verifier(ConfigMessage::verify_callable v) override { verifier_ = std::move(v); } void set_signer(ConfigMessage::sign_callable s) override { signer_ = std::move(s); } - ustring sign(ustring_view data) const; + std::vector sign(std::span data) const; // Checks for and drops expired keys. void remove_expired(); // Loads existing state from a previous dump of keys data - void load_dump(ustring_view dump); + void load_dump(std::span dump); // Inserts a key into the correct place in `keys_`. void insert_key(std::string_view message_hash, key_info&& key); @@ -192,10 +192,10 @@ class Keys : public ConfigSig { /// - `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()`. /// - `info` and `members` -- will be loaded with the group keys, if present in the dump. - Keys(ustring_view user_ed25519_secretkey, - ustring_view group_ed25519_pubkey, - std::optional group_ed25519_secretkey, - std::optional dumped, + Keys(std::span user_ed25519_secretkey, + std::span group_ed25519_pubkey, + std::optional> group_ed25519_secretkey, + std::optional> dumped, Info& info, Members& members); @@ -232,8 +232,8 @@ class Keys : public ConfigSig { /// Inputs: none. /// /// Outputs: - /// - `std::vector` - vector of encryption keys. - std::vector group_keys() const; + /// - `std::vector>` - vector of encryption keys. + std::vector> group_keys() const; /// API: groups/Keys::size /// @@ -258,8 +258,8 @@ class Keys : public ConfigSig { /// Inputs: none. /// /// Outputs: - /// - `ustring_view` of the most current group encryption key. - ustring_view group_enc_key() const; + /// - `std::span` of the most current group encryption key. + std::span group_enc_key() const; /// API: groups/Keys::is_admin /// @@ -292,7 +292,7 @@ class Keys : public ConfigSig { /// /// 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_admin_key(ustring_view secret, Info& info, Members& members); + void load_admin_key(std::span secret, Info& info, Members& members); /// API: groups/Keys::rekey /// @@ -325,11 +325,12 @@ class Keys : public ConfigSig { /// config will be dirtied after the rekey and will require a push. /// /// Outputs: - /// - `ustring_view` containing the data that needs to be pushed to the config keys namespace + /// - `std::span` containing the data that needs to be pushed to the config + /// keys namespace /// for the group. (This can be re-obtained from `pending_config()` if needed until it has /// been confirmed or superceded). This data must be consumed or copied from the returned /// string_view immediately: it will not be valid past other calls on the Keys config object. - ustring_view rekey(Info& info, Members& members); + std::span rekey(Info& info, Members& members); /// API: groups/Keys::key_supplement /// @@ -351,10 +352,11 @@ class Keys : public ConfigSig { /// Session IDs are specified in hex. /// /// Outputs: - /// - `ustring` containing the message that should be pushed to the swarm containing encrypted + /// - `std::vector` containing the message that should be pushed to the swarm + /// containing encrypted /// keys for the given user(s). - ustring key_supplement(const std::vector& sids) const; - ustring key_supplement(std::string sid) const { + std::vector key_supplement(const std::vector& sids) const; + std::vector key_supplement(std::string sid) const { return key_supplement(std::vector{{std::move(sid)}}); } @@ -382,7 +384,8 @@ class Keys : public ConfigSig { /// delete messages without having the full admin group keys. /// /// Outputs: - /// - `ustring` -- contains a subaccount swarm signing value; this can be passed (by the user) + /// - `std::vector` -- contains a subaccount swarm signing value; this can be + /// passed (by the user) /// into `swarm_subaccount_sign` to sign a value suitable for swarm authentication. /// (Internally this packs the flags, blinding factor, and group admin signature together and /// will be 4 + 32 + 64 = 100 bytes long). @@ -393,7 +396,7 @@ class Keys : public ConfigSig { /// /// The signing value produced will be the same (for a given `session_id`/`write`/`del` /// values) when constructed by any admin of the group. - ustring swarm_make_subaccount( + std::vector swarm_make_subaccount( std::string_view session_id, bool write = true, bool del = false) const; /// API: groups/Keys::swarm_verify_subaccount @@ -427,12 +430,14 @@ class Keys : public ConfigSig { /// not validate or does not meet the requirements. static bool swarm_verify_subaccount( std::string group_id, - ustring_view session_ed25519_secretkey, - ustring_view signing_value, + std::span session_ed25519_secretkey, + std::span signing_value, bool write = false, bool del = false); bool swarm_verify_subaccount( - ustring_view signing_value, bool write = false, bool del = false) const; + std::span signing_value, + bool write = false, + bool del = false) const; /// API: groups/Keys::swarm_auth /// @@ -480,7 +485,9 @@ class Keys : public ConfigSig { /// - struct containing three binary values enabling swarm authentication (see description /// above). swarm_auth swarm_subaccount_sign( - ustring_view msg, ustring_view signing_value, bool binary = false) const; + std::span msg, + std::span signing_value, + bool binary = false) const; /// API: groups/Keys::swarm_subaccount_token /// @@ -501,22 +508,23 @@ class Keys : public ConfigSig { /// /// Outputs: /// - 36 byte token that can be used for swarm token revocation. - ustring swarm_subaccount_token( + std::vector swarm_subaccount_token( std::string_view session_id, bool write = true, bool del = false) const; /// API: groups/Keys::pending_config /// /// If a rekey has been performed but not yet confirmed then this will contain the config /// message to be pushed to the swarm. If there is no push current pending then this returns - /// nullopt. The value should be used immediately (i.e. the ustring_view may not remain valid - /// if other calls to the config object are made). + /// nullopt. The value should be used immediately (i.e. the std::span may + /// not remain valid if other calls to the config object are made). /// /// Inputs: None /// /// Outputs: - /// - `std::optional` -- returns a populated config message that should be pushed, + /// - `std::optional>` -- returns a populated config message that + /// should be pushed, /// if not yet confirmed, otherwise when no pending update is present this returns nullopt. - std::optional pending_config() const; + std::optional> pending_config() const; /// API: groups/Keys::pending_key /// @@ -530,10 +538,11 @@ class Keys : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::optional` the encryption key generated by the last `rekey()` call. + /// - `std::optional>` the encryption key generated by the last + /// `rekey()` call. /// This is set to a new key when `rekey()` is called, and is cleared when any config message /// is successfully loaded by `load_key`. - std::optional pending_key() const; + std::optional> pending_key() const; /// API: groups/Keys::load_key /// @@ -568,7 +577,7 @@ class Keys : public ConfigSig { /// it could mean we decrypted one for us, but already had it. bool load_key_message( std::string_view hash, - ustring_view data, + std::span data, int64_t timestamp_ms, Info& info, Members& members); @@ -637,7 +646,7 @@ class Keys : public ConfigSig { /// Outputs: /// - opaque binary data containing the group keys and other Keys config data that can be passed /// to the `Keys` constructor to reinitialize a Keys object with the current state. - ustring dump(); + std::vector dump(); /// API: groups/Keys::make_dump /// @@ -648,8 +657,8 @@ class Keys : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `ustring` -- Returns binary data of the state dump - ustring make_dump() const; + /// - `std::vector` -- Returns binary data of the state dump + std::vector make_dump() const; /// API: groups/Keys::encrypt_message /// @@ -710,8 +719,10 @@ class Keys : public ConfigSig { /// /// Outputs: /// - `ciphertext` -- the encrypted, etc. value to send to the swarm - ustring encrypt_message( - ustring_view plaintext, bool compress = true, size_t padding = 256) const; + std::vector encrypt_message( + std::span plaintext, + bool compress = true, + size_t padding = 256) const; /// API: groups/Keys::decrypt_message /// @@ -727,14 +738,16 @@ class Keys : public ConfigSig { /// by `encrypt_message()`. /// /// Outputs: - /// - `std::pair` -- the session ID (in hex) and the plaintext binary + /// - `std::pair>` -- the session ID (in hex) and the + /// plaintext binary /// data that was encrypted. /// /// On failure this throws a std::exception-derived exception with a `.what()` string containing /// some diagnostic info on what part failed. Typically a production session client would catch /// (and possibly log) but otherwise ignore such exceptions and just not process the message if /// it throws. - std::pair decrypt_message(ustring_view ciphertext) const; + std::pair> decrypt_message( + std::span ciphertext) const; }; } // namespace session::config::groups diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index fc11ca49..039f3fb0 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -282,9 +282,9 @@ class Members : public ConfigBase { /// push config changes. /// - `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()`. - Members(ustring_view ed25519_pubkey, - std::optional ed25519_secretkey, - std::optional dumped); + Members(std::span ed25519_pubkey, + std::optional> ed25519_secretkey, + std::optional> dumped); /// API: groups/Members::storage_namespace /// diff --git a/include/session/config/profile_pic.hpp b/include/session/config/profile_pic.hpp index 93259c95..1eb55a9c 100644 --- a/include/session/config/profile_pic.hpp +++ b/include/session/config/profile_pic.hpp @@ -10,9 +10,9 @@ struct profile_pic { static constexpr size_t MAX_URL_LENGTH = 223; std::string url; - ustring key; + std::vector key; - static void check_key(ustring_view key) { + static void check_key(std::span key) { if (!(key.empty() || key.size() == 32)) throw std::invalid_argument{"Invalid profile pic key: 32 bytes required"}; } @@ -21,12 +21,14 @@ struct profile_pic { profile_pic() = default; // Constructs from a URL and key. Key must be empty or 32 bytes. - profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} { + profile_pic(std::string_view url, std::span key) : + url{url}, key{to_vector(key)} { check_key(this->key); } - // Constructs from a string/ustring pair moved into the constructor - profile_pic(std::string&& url, ustring&& key) : url{std::move(url)}, key{std::move(key)} { + // Constructs from a string/std::vector pair moved into the constructor + profile_pic(std::string&& url, std::vector&& key) : + url{std::move(url)}, key{std::move(key)} { check_key(this->key); } @@ -63,7 +65,7 @@ struct profile_pic { /// /// Inputs: /// - `new_key` -- binary data of a new key to be set. Must be 32 bytes - void set_key(ustring new_key) { + void set_key(std::vector new_key) { check_key(new_key); key = std::move(new_key); } diff --git a/include/session/config/protos.hpp b/include/session/config/protos.hpp index f7d1371d..2880aed3 100644 --- a/include/session/config/protos.hpp +++ b/include/session/config/protos.hpp @@ -21,8 +21,11 @@ namespace session::config::protos { /// Outputs: /// Returns the wrapped config. Will throw on serious errors (e.g. `ed25519_sk` or `ns` are /// invalid). -ustring wrap_config( - ustring_view ed25519_sk, ustring_view data, int64_t seqno, config::Namespace ns); +std::vector wrap_config( + std::span ed25519_sk, + std::span data, + int64_t seqno, + config::Namespace ns); /// API: config/protos::unwrap_config /// @@ -41,6 +44,9 @@ ustring wrap_config( /// Throws a std::invalid_argument if the given ed25519_sk is invalid. (It is recommended that only /// the std::runtime_error is caught for detecting non-wrapped input as the invalid secret key is /// more serious). -ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namespace ns); +std::vector unwrap_config( + std::span ed25519_sk, + std::span data, + config::Namespace ns); } // namespace session::config::protos diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 077130a5..55febbc9 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -100,8 +100,8 @@ struct base_group_info { /// Struct containing legacy group info (aka "closed groups"). struct legacy_group_info : base_group_info { std::string session_id; // The legacy group "session id" (33 bytes). - ustring enc_pubkey; // bytes (32 or empty) - ustring enc_seckey; // bytes (32 or empty) + std::vector enc_pubkey; // bytes (32 or empty) + std::vector enc_seckey; // bytes (32 or empty) std::chrono::seconds disappearing_timer{0}; // 0 == disabled. /// Constructs a new legacy group info from an id (which must look like a session_id). Throws @@ -191,7 +191,7 @@ struct group_info : base_group_info { // (to distinguish it from a 05 x25519 pubkey session id). /// Group secret key (64 bytes); this is only possessed by admins. - ustring secretkey; + std::vector secretkey; /// Group authentication signing value (100 bytes); this is used by non-admins to authenticate /// (using the swarm key generation functions in config::groups::Keys). This value will be @@ -199,7 +199,7 @@ struct group_info : base_group_info { /// is an admin), and so does not need to be explicitly cleared when being promoted to admin. /// /// Producing and using this value is done with the groups::Keys `swarm` methods. - ustring auth_data; + std::vector auth_data; /// Tracks why we were removed from the group. Values are: /// - NOT_REMOVED: that we haven't been removed, @@ -281,7 +281,9 @@ class UserGroups : public ConfigBase { /// /// Outputs: /// - `UserGroups` - Constructor - UserGroups(ustring_view ed25519_secretkey, std::optional dumped); + UserGroups( + std::span ed25519_secretkey, + std::optional> dumped); /// API: user_groups/UserGroups::storage_namespace /// @@ -366,7 +368,8 @@ class UserGroups : public ConfigBase { /// std::string_view room, /// std::string_view pubkey_encoded) const; /// community_info get_or_construct_community( - /// std::string_view base_url, std::string_view room, ustring_view pubkey) const; + /// std::string_view base_url, std::string_view room, std::span + /// pubkey) const; /// ``` /// /// Inputs: @@ -389,7 +392,9 @@ class UserGroups : public ConfigBase { std::string_view room, std::string_view pubkey_encoded) const; community_info get_or_construct_community( - std::string_view base_url, std::string_view room, ustring_view pubkey) const; + std::string_view base_url, + std::string_view room, + std::span pubkey) const; /// API: user_groups/UserGroups::get_or_construct_community(string_view) /// @@ -465,7 +470,7 @@ class UserGroups : public ConfigBase { protected: // Drills into the nested dicts to access open group details DictFieldProxy community_field( - const community_info& og, ustring_view* get_pubkey = nullptr) const; + const community_info& og, std::span* get_pubkey = nullptr) const; void set_base(const base_group_info& bg, DictFieldProxy& info) const; diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 2f4b3c45..6b9dfa8e 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -47,7 +47,9 @@ class UserProfile : public ConfigBase { /// /// Outputs: /// - `UserProfile` - Constructor - UserProfile(ustring_view ed25519_secretkey, std::optional dumped); + UserProfile( + std::span ed25519_secretkey, + std::optional> dumped); /// API: user_profile/UserProfile::storage_namespace /// @@ -114,7 +116,7 @@ class UserProfile : public ConfigBase { /// /// Declaration: /// ```cpp - /// void set_profile_pic(std::string_view url, ustring_view key); + /// void set_profile_pic(std::string_view url, std::span key); /// void set_profile_pic(profile_pic pic); /// ``` /// @@ -124,7 +126,7 @@ class UserProfile : public ConfigBase { /// - `key` -- Decryption key /// - Second function: /// - `pic` -- Profile pic object - void set_profile_pic(std::string_view url, ustring_view key); + void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); /// API: user_profile/UserProfile::get_nts_priority diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index 21e8de47..d818916f 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -18,7 +18,7 @@ std::pair, std::array> curve255 /// /// Outputs: /// - The curve25519 public key -std::array to_curve25519_pubkey(ustring_view ed25519_pubkey); +std::array to_curve25519_pubkey(std::span ed25519_pubkey); /// API: curve25519/to_curve25519_seckey /// @@ -30,6 +30,6 @@ std::array to_curve25519_pubkey(ustring_view ed25519_pubkey); /// /// Outputs: /// - The curve25519 secret key -std::array to_curve25519_seckey(ustring_view ed25519_seckey); +std::array to_curve25519_seckey(std::span ed25519_seckey); } // namespace session::curve25519 diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index e40144af..9623b45e 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -11,7 +11,7 @@ std::pair, std::array> ed25519_ /// Given an Ed25519 seed this returns the associated Ed25519 key pair std::pair, std::array> ed25519_key_pair( - ustring_view ed25519_seed); + std::span ed25519_seed); /// API: ed25519/seed_for_ed_privkey /// @@ -25,7 +25,7 @@ std::pair, std::array> ed25519_ /// /// Outputs: /// - The ed25519 seed -std::array seed_for_ed_privkey(ustring_view ed25519_privkey); +std::array seed_for_ed_privkey(std::span ed25519_privkey); /// API: ed25519/sign /// @@ -37,7 +37,8 @@ std::array seed_for_ed_privkey(ustring_view ed25519_privkey); /// /// Outputs: /// - The ed25519 signature -ustring sign(ustring_view ed25519_privkey, ustring_view msg); +std::vector sign( + std::span ed25519_privkey, std::span msg); /// API: ed25519/verify /// @@ -50,6 +51,9 @@ ustring sign(ustring_view ed25519_privkey, ustring_view msg); /// /// Outputs: /// - A flag indicating whether the signature is valid -bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg); +bool verify( + std::span sig, + std::span pubkey, + std::span msg); } // namespace session::ed25519 diff --git a/include/session/hash.hpp b/include/session/hash.hpp index f9dce034..8729a5d5 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -18,6 +18,9 @@ namespace session::hash { /// /// Outputs: /// - a `size` byte hash. -ustring hash(const size_t size, ustring_view msg, std::optional key = std::nullopt); +std::vector hash( + const size_t size, + std::span msg, + std::optional> key = std::nullopt); } // namespace session::hash diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 533f055e..fc738e79 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -41,19 +41,21 @@ namespace detail { std::string_view domain); void encrypt_multi_impl( - ustring& out, - ustring_view message, + std::vector& out, + std::span message, const unsigned char* key, const unsigned char* nonce); bool decrypt_multi_impl( - ustring& out, - ustring_view ciphertext, + std::vector& out, + std::span ciphertext, const unsigned char* key, const unsigned char* nonce); inline void validate_multi_fields( - ustring_view nonce, ustring_view privkey, ustring_view pubkey) { + std::span nonce, + std::span privkey, + std::span pubkey) { if (nonce.size() < 24) throw std::logic_error{"nonce must be 24 bytes"}; if (privkey.size() != 32) @@ -74,7 +76,7 @@ extern const size_t encrypt_multiple_message_overhead; /// API: crypto/encrypt_for_multiple /// /// Encrypts a message multiple times for multiple recipients. `callable` is invoked once per -/// encrypted (or junk) value, passed as a `ustring_view`. +/// encrypted (or junk) value, passed as a `std::span`. /// /// Inputs: /// - `messages` -- a vector of message bodies to encrypt. Must be either size 1, or of the same @@ -95,19 +97,20 @@ extern const size_t encrypt_multiple_message_overhead; /// used to generate individual keys for domain separation, and so should ideally have a different /// value in different contexts (i.e. group keys uses one value, kicked messages use another, /// etc.). *Can* be empty, but should be set to something. -/// - `call` -- this is invoked for each different encrypted value with a ustring_view; the caller -/// must copy as needed as the ustring_view doesn't remain valid past the call. +/// - `call` -- this is invoked for each different encrypted value with a std::span; the caller +/// must copy as needed as the std::span doesn't remain valid past the call. /// - `ignore_invalid_recipient` -- if given and true then any recipients that appear to have /// invalid public keys (i.e. the shared key multiplication fails) will be silently ignored (the /// callback will not be called). If not given (or false) then such a failure for any recipient /// will raise an exception. template void encrypt_for_multiple( - const std::vector messages, - const std::vector recipients, - ustring_view nonce, - ustring_view privkey, - ustring_view pubkey, + const std::vector> messages, + const std::vector> recipients, + std::span nonce, + std::span privkey, + std::span pubkey, std::string_view domain, F&& call, bool ignore_invalid_recipient = false) { @@ -126,7 +129,7 @@ void encrypt_for_multiple( if (auto sz = m.size(); sz > max_msg_size) max_msg_size = sz; - ustring encrypted; + std::vector encrypted; encrypted.reserve(max_msg_size + encrypt_multiple_message_overhead); sodium_cleared> key; @@ -144,35 +147,36 @@ void encrypt_for_multiple( throw; } detail::encrypt_multi_impl(encrypted, m, key.data(), nonce.data()); - call(ustring_view{encrypted}); + call(to_span(encrypted)); } } /// Wrapper for passing a single message for all recipients; all arguments other than the first are /// identical. template -void encrypt_for_multiple(ustring_view message, Args&&... args) { +void encrypt_for_multiple(std::span message, Args&&... args) { return encrypt_for_multiple( to_view_vector(&message, &message + 1), std::forward(args)...); } template void encrypt_for_multiple(std::string_view message, Args&&... args) { - return encrypt_for_multiple(to_unsigned_sv(message), std::forward(args)...); + return encrypt_for_multiple(to_span(message), std::forward(args)...); } template void encrypt_for_multiple(std::basic_string_view message, Args&&... args) { - return encrypt_for_multiple(to_unsigned_sv(message), std::forward(args)...); + return encrypt_for_multiple(to_span(message), std::forward(args)...); } /// API: crypto/decrypt_for_multiple /// -/// Decryption via a lambda: we call the lambda (which must return a std::optional) -/// repeatedly until we get back a nullopt, and attempt to decrypt each returned value. When -/// decryption succeeds, we return the plaintext to the caller. If none of the fed-in values can be -/// decrypt, we return std::nullopt. +/// Decryption via a lambda: we call the lambda (which must return a std::optional>) repeatedly until we get back a nullopt, and attempt to decrypt each returned +/// value. When decryption succeeds, we return the plaintext to the caller. If none of the fed-in +/// values can be decrypt, we return std::nullopt. /// /// Inputs: -/// - `ciphertext` -- callback that returns a std::optional or std::optional +/// - `ciphertext` -- callback that returns a std::optional> or +/// std::optional> /// when called, containing the next ciphertext; should return std::nullopt when finished. /// - `nonce` -- the nonce used for encryption/decryption (which must have been provided by the /// sender alongside the encrypted messages, and is the same as the `nonce` value given to @@ -187,20 +191,22 @@ void encrypt_for_multiple(std::basic_string_view message, Args&&... a template < typename NextCiphertext, typename = std::enable_if_t< - std::is_invocable_r_v, NextCiphertext> || - std::is_invocable_r_v, NextCiphertext> || + std::is_invocable_r_v< + std::optional>, + NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v< std::optional>, NextCiphertext> || std::is_invocable_r_v>, NextCiphertext>>> -std::optional decrypt_for_multiple( +std::optional> decrypt_for_multiple( NextCiphertext next_ciphertext, - ustring_view nonce, - ustring_view privkey, - ustring_view pubkey, - ustring_view sender_pubkey, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { detail::validate_multi_fields(nonce, privkey, pubkey); @@ -211,7 +217,7 @@ std::optional decrypt_for_multiple( detail::encrypt_multi_key( key, privkey.data(), pubkey.data(), sender_pubkey.data(), false, domain); - auto decrypted = std::make_optional(); + auto decrypted = std::make_optional>(); for (auto ciphertext = next_ciphertext(); ciphertext; ciphertext = next_ciphertext()) if (detail::decrypt_multi_impl(*decrypted, *ciphertext, key.data(), nonce.data())) @@ -239,12 +245,12 @@ std::optional decrypt_for_multiple( /// - `domain` -- the encryption domain; this is typically a hard-coded string, and must be the same /// as the one used for encryption. /// -std::optional decrypt_for_multiple( - const std::vector& ciphertexts, - ustring_view nonce, - ustring_view privkey, - ustring_view pubkey, - ustring_view sender_pubkey, +std::optional> decrypt_for_multiple( + const std::vector>& ciphertexts, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain); /// API: crypto/encrypt_for_multiple_simple @@ -288,15 +294,15 @@ std::optional decrypt_for_multiple( /// entries will be somewhat identifiable. /// /// Outputs: -/// ustring containing bytes that contains the nonce and encoded encrypted messages, suitable for -/// decryption by the recipients with `decrypt_for_multiple_simple`. -ustring encrypt_for_multiple_simple( - const std::vector& messages, - const std::vector& recipients, - ustring_view privkey, - ustring_view pubkey, +/// std::vector containing bytes that contains the nonce and encoded encrypted +/// messages, suitable for decryption by the recipients with `decrypt_for_multiple_simple`. +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span privkey, + std::span pubkey, std::string_view domain, - std::optional nonce = std::nullopt, + std::optional> nonce = std::nullopt, int pad = 0); /// API: crypto/encrypt_for_multiple_simple @@ -304,12 +310,12 @@ ustring encrypt_for_multiple_simple( /// This function is the same as the above, except that instead of taking the sender private and /// public X25519 keys, it takes the single, 64-byte libsodium Ed25519 secret key (which is then /// converted into the required X25519 keys). -ustring encrypt_for_multiple_simple( - const std::vector& messages, - const std::vector& recipients, - ustring_view ed25519_secret_key, +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span ed25519_secret_key, std::string_view domain, - ustring_view nonce = {}, + std::span nonce = {}, int pad = 0); /// API: crypto/encrypt_for_multiple_simple @@ -319,17 +325,19 @@ ustring encrypt_for_multiple_simple( /// the first are identical. /// template -ustring encrypt_for_multiple_simple(ustring_view message, Args&&... args) { +std::vector encrypt_for_multiple_simple( + std::span message, Args&&... args) { return encrypt_for_multiple_simple( to_view_vector(&message, &message + 1), std::forward(args)...); } template -ustring encrypt_for_multiple_simple(std::string_view message, Args&&... args) { - return encrypt_for_multiple_simple(to_unsigned_sv(message), std::forward(args)...); +std::vector encrypt_for_multiple_simple(std::string_view message, Args&&... args) { + return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); } template -ustring encrypt_for_multiple_simple(std::basic_string_view message, Args&&... args) { - return encrypt_for_multiple_simple(to_unsigned_sv(message), std::forward(args)...); +std::vector encrypt_for_multiple_simple( + std::basic_string_view message, Args&&... args) { + return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); } /// API: crypto/decrypt_for_multiple_simple @@ -350,13 +358,13 @@ ustring encrypt_for_multiple_simple(std::basic_string_view message, A /// `encrypt_for_multiple_simple`. /// /// Outputs: -/// If decryption succeeds, returns a ustring containing the decrypted message, in bytes. If -/// parsing or decryption fails, returns std::nullopt. -std::optional decrypt_for_multiple_simple( - ustring_view encoded, - ustring_view privkey, - ustring_view pubkey, - ustring_view sender_pubkey, +/// If decryption succeeds, returns a std::vector containing the decrypted message, +/// in bytes. If parsing or decryption fails, returns std::nullopt. +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain); /// API: crypto/decrypt_for_multiple_simple @@ -366,20 +374,20 @@ std::optional decrypt_for_multiple_simple( /// the decryption. /// /// Note that `sender_pubkey` is still an X25519 pubkey for this version of the function. -std::optional decrypt_for_multiple_simple( - ustring_view encoded, - ustring_view ed25519_secret_key, - ustring_view sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span ed25519_secret_key, + std::span sender_pubkey, std::string_view domain); /// API: crypto/decrypt_for_multiple_simple_ed25519 /// /// This is the same as the above, except that it takes both the sender and recipient as Ed25519 /// keys, converting them on the fly to attempt the decryption. -std::optional decrypt_for_multiple_simple_ed25519( - ustring_view encoded, - ustring_view ed25519_secret_key, - ustring_view sender_ed25519_pubkey, +std::optional> decrypt_for_multiple_simple_ed25519( + std::span encoded, + std::span ed25519_secret_key, + std::span sender_ed25519_pubkey, std::string_view domain); } // namespace session diff --git a/include/session/network.hpp b/include/session/network.hpp index 66ca2e60..af290397 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -56,7 +56,7 @@ struct service_node : public oxen::quic::RemoteAddress { template service_node( - ustring_view remote_pk, + std::span remote_pk, std::vector storage_server_version, swarm_id_t swarm_id, Opt&&... opts) : diff --git a/include/session/onionreq/key_types.hpp b/include/session/onionreq/key_types.hpp index 6d862268..b03e2ee0 100644 --- a/include/session/onionreq/key_types.hpp +++ b/include/session/onionreq/key_types.hpp @@ -55,9 +55,11 @@ struct alignas(size_t) key_base : std::array { return d; } static Derived from_bytes(std::vector bytes) { - return from_bytes(vec_to_str(bytes)); + return from_bytes(to_string(bytes)); + } + static Derived from_bytes(std::span bytes) { + return from_bytes(to_string(bytes)); } - static Derived from_bytes(std::span bytes) { return from_bytes(from_const_unsigned_span(bytes)); } }; template diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index ff165a9b..233a97e0 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -27,7 +27,7 @@ class OnionReqParser { size_t max_size = DEFAULT_MAX_SIZE); /// plaintext payload, decrypted from the incoming request during construction. - std::span payload() const { return vec_to_span(payload_); } + std::span payload() const { return to_span(payload_); } /// Extracts payload from this object (via a std::move); after the call the object's payload /// will be empty. @@ -37,7 +37,7 @@ class OnionReqParser { return ret; } - std::span remote_pubkey() const { return to_const_unsigned_span(remote_pk.view()); } + std::span remote_pubkey() const { return to_span(remote_pk.view()); } /// Encrypts a reply using the appropriate encryption as determined when parsing the /// request. diff --git a/include/session/random.hpp b/include/session/random.hpp index 4d07139b..54b33fba 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -37,7 +37,7 @@ namespace session::random { /// /// Outputs: /// - random bytes of the specified length. -ustring random(size_t size); +std::vector random(size_t size); /// API: random/random_base32 /// diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 8b3e6c9d..5e346e88 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -60,8 +60,10 @@ namespace session { /// Outputs: /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) -ustring encrypt_for_recipient( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +std::vector encrypt_for_recipient( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/encrypt_for_recipient_deterministic /// @@ -79,8 +81,10 @@ ustring encrypt_for_recipient( /// /// Outputs: /// Identical to `encrypt_for_recipient`. -ustring encrypt_for_recipient_deterministic( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +std::vector encrypt_for_recipient_deterministic( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/session_encrypt_for_blinded_recipient /// @@ -97,11 +101,11 @@ ustring encrypt_for_recipient_deterministic( /// Outputs: /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) -ustring encrypt_for_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view recipient_blinded_id, - ustring_view message); +std::vector encrypt_for_blinded_recipient( + std::span ed25519_privkey, + std::span server_pk, + std::span recipient_blinded_id, + std::span message); /// API: crypto/sign_for_recipient /// @@ -126,8 +130,10 @@ ustring encrypt_for_blinded_recipient( /// - `recipient_pubkey` -- the recipient X25519 pubkey, which may or may not be prefixed with the /// 0x05 session id prefix (33 bytes if prefixed, 32 if not prefixed). /// - `message` -- the message to embed and sign. -ustring sign_for_recipient( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +std::vector sign_for_recipient( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/decrypt_incoming /// @@ -141,10 +147,12 @@ ustring sign_for_recipient( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// - `std::pair, std::vector>` -- the plaintext binary +/// data that was encrypted and the /// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on /// error. -std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); +std::pair, std::vector> decrypt_incoming( + std::span ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -160,11 +168,14 @@ std::pair decrypt_incoming(ustring_view ed25519_privkey, ustri /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// - `std::pair, std::vector>` -- the plaintext binary +/// data that was encrypted and the /// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on /// error. -std::pair decrypt_incoming( - ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); +std::pair, std::vector> decrypt_incoming( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -178,10 +189,11 @@ std::pair decrypt_incoming( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// - `std::pair, std::string>` -- the plaintext binary data that was +/// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_incoming_session_id( - ustring_view ed25519_privkey, ustring_view ciphertext); +std::pair, std::string> decrypt_incoming_session_id( + std::span ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -196,10 +208,13 @@ std::pair decrypt_incoming_session_id( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// - `std::pair, std::string>` -- the plaintext binary data that was +/// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_incoming_session_id( - ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); +std::pair, std::string> decrypt_incoming_session_id( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext); /// API: crypto/decrypt_from_blinded_recipient /// @@ -221,14 +236,15 @@ std::pair decrypt_incoming_session_id( /// - `ciphertext` -- Pointer to a data buffer containing the encrypted data. /// /// Outputs: -/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// - `std::pair, std::string>` -- the plaintext binary data that was +/// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair decrypt_from_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view sender_id, - ustring_view recipient_id, - ustring_view ciphertext); +std::pair, std::string> decrypt_from_blinded_recipient( + std::span ed25519_privkey, + std::span server_pk, + std::span sender_id, + std::span recipient_id, + std::span ciphertext); /// API: crypto/decrypt_ons_response /// @@ -244,8 +260,8 @@ std::pair decrypt_from_blinded_recipient( /// a session ID. Throws on error/failure. std::string decrypt_ons_response( std::string_view lowercase_name, - ustring_view ciphertext, - std::optional nonce); + std::span ciphertext, + std::optional> nonce); /// API: crypto/decrypt_push_notification /// @@ -257,9 +273,11 @@ std::string decrypt_ons_response( /// bytes). /// /// Outputs: -/// - `ustring` -- the decrypted push notification payload, *if* the decryption was +/// - `std::vector` -- the decrypted push notification payload, *if* the decryption +/// was /// successful. Throws on error/failure. -ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key); +std::vector decrypt_push_notification( + std::span payload, std::span enc_key); /// API: crypto/compute_message_hash /// diff --git a/include/session/types.hpp b/include/session/types.hpp index a4ee8725..40505908 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -6,15 +6,8 @@ #include #include -namespace session { - -using ustring = std::basic_string; -using ustring_view = std::basic_string_view; - -namespace config { +namespace session { namespace config { using seqno_t = std::int64_t; -} // namespace config - -} // namespace session +}} // namespace session::config diff --git a/include/session/util.hpp b/include/session/util.hpp index 7244b863..d0a1c751 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -20,6 +20,77 @@ namespace session { using namespace oxenc; +// Helper functions to convert to/from spans +template +inline std::span as_span(const std::span& sp) { + return {reinterpret_cast(sp.data()), sp.size()}; +} + +template +inline std::span to_span(const T& c) { + return {reinterpret_cast(c.data()), c.size()}; +} + +template +inline std::span to_span(const char (&literal)[N]) { + return {reinterpret_cast(literal), N - 1}; +} + +template + requires(!oxenc::string_like) +inline std::span to_span(const Container& c) { + return {reinterpret_cast(c.data()), c.size()}; +} + +// Helper functions to convert container types +template +inline OutContainer convert(const InContainer& in) { + using out_value_type = typename OutContainer::value_type; + auto begin = reinterpret_cast(in.data()); + return OutContainer(begin, begin + in.size()); +} + +template +inline std::vector to_vector(std::span sp) { + return convert>(sp); +} + +template +inline std::vector to_vector(const T& c) { + return convert>(to_span(c)); +} + +template +inline std::vector to_vector(const std::array& arr) { + return convert>(arr); +} + +template + requires(!oxenc::string_like) +inline std::vector to_vector(const Container& c) { + return convert>(to_span(c)); +} + +template +inline std::array to_array(std::span sp) { + std::array result{}; + std::copy_n( + reinterpret_cast(sp.data()), + std::min(N, sp.size()), + result.begin()); + return result; +} + +template +inline std::string to_string(const Container& c) { + return convert(c); +} + +template +inline std::string_view to_string_view(const Container& c) { + return {reinterpret_cast(c.data()), c.size()}; +} + // Helper function to go to/from char pointers to unsigned char pointers: inline const unsigned char* to_unsigned(const char* x) { return reinterpret_cast(x); @@ -40,84 +111,6 @@ inline const unsigned char* to_unsigned(const unsigned char* x) { inline unsigned char* to_unsigned(unsigned char* x) { return x; } -inline const char* from_unsigned(const unsigned char* x) { - return reinterpret_cast(x); -} -inline char* from_unsigned(unsigned char* x) { - return reinterpret_cast(x); -} -// Helper to switch from basic_string_view to basic_string_view. Both CFrom and CTo -// must be primitive, one-byte types. -template -inline std::basic_string_view convert_sv(std::basic_string_view from) { - return {reinterpret_cast(from.data()), from.size()}; -} -// Same as above, but with a const basic_string& argument (to allow deduction of CFrom when -// using a basic_string). -template -inline std::basic_string_view convert_sv(const std::basic_string& from) { - return {reinterpret_cast(from.data()), from.size()}; -} -// Helper function to switch between basic_string_view and ustring_view -inline ustring_view to_unsigned_sv(std::string_view v) { - return {to_unsigned(v.data()), v.size()}; -} -inline ustring_view to_unsigned_sv(std::basic_string_view v) { - return {to_unsigned(v.data()), v.size()}; -} -inline ustring_view to_unsigned_sv(ustring_view v) { - return v; // no-op, but helps with template metaprogramming -} -inline std::string_view from_unsigned_sv(ustring_view v) { - return {from_unsigned(v.data()), v.size()}; -} -template -inline std::string_view from_unsigned_sv(const std::array& v) { - return {from_unsigned(v.data()), v.size()}; -} -template -inline std::string_view from_unsigned_sv(const std::vector& v) { - return {from_unsigned(v.data()), v.size()}; -} -template -inline std::basic_string_view to_sv(const std::array& v) { - return {v.data(), N}; -} -// Helper function to switch between spans -template -inline std::span to_const_unsigned_span(const T& sv) { - return {reinterpret_cast(sv.data()), sv.size()}; -} - -template -inline std::span to_const_unsigned_span(const char (&literal)[N]) { - return {reinterpret_cast(literal), N - 1}; -} - -template -std::string from_const_unsigned_span(const std::span& sp) { - return std::string(reinterpret_cast(sp.data()), sp.size()); -} - -template -inline std::span vec_to_span(const std::vector& v) { - return {reinterpret_cast(v.data()), v.size()}; -} - -template -inline std::vector span_to_vec(const std::span& sp) { - auto begin = reinterpret_cast(sp.data()); - return {begin, begin + sp.size()}; -} - -inline std::vector str_to_vec(std::string_view v) { - auto begin = reinterpret_cast(v.data()); - return {begin, begin + v.size()}; -} - -inline std::string vec_to_str(std::vector v) { - return std::string(reinterpret_cast(v.data()), v.size()); -} /// 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) { @@ -131,10 +124,11 @@ using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; -/// Takes a container of string-like binary values and returns a vector of ustring_views viewing -/// those values. This can be used on a container of any type with a `.data()` and a `.size()` -/// where `.data()` is a one-byte value pointer; std::string, std::string_view, ustring, -/// ustring_view, etc. apply, as does std::array of 1-byte char types. +/// Takes a container of string-like binary values and returns a vector of unsigned char spans +/// viewing those values. This can be used on a container of any type with a `.data()` and a +/// `.size()` where `.data()` is a one-byte value pointer; std::string, std::string_view, +/// std::vector, std::span, etc. apply, as does std::array +/// of 1-byte char types. /// /// This is useful in various libsession functions that require such a vector. Note that the /// returned vector's views are valid only as the original container remains alive; this is @@ -145,8 +139,8 @@ using uc64 = std::array; /// There are two versions of this: the first takes a generic iterator pair; the second takes a /// single container. template -std::vector to_view_vector(It begin, It end) { - std::vector vec; +std::vector> to_view_vector(It begin, It end) { + std::vector> vec; vec.reserve(std::distance(begin, end)); for (; begin != end; ++begin) { if constexpr (std::is_same_v, char*>) // C strings @@ -163,7 +157,7 @@ std::vector to_view_vector(It begin, It end) { } template -std::vector to_view_vector(const Container& c) { +std::vector> to_view_vector(const Container& c) { return to_view_vector(c.begin(), c.end()); } diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index e389e9d0..d037fa07 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -1,24 +1,25 @@ #pragma once + #include +#include #include #include namespace session::xed25519 { -using ustring_view = std::basic_string_view; - /// XEd25519-signs a message given the curve25519 privkey and message. std::array sign( - ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg); + std::span curve25519_privkey /* 32 bytes */, + std::span msg); /// "Softer" version that takes and returns strings of regular chars std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg); /// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey [[nodiscard]] bool verify( - ustring_view signature /* 64 bytes */, - ustring_view curve25519_pubkey /* 32 bytes */, - ustring_view msg); + std::span signature /* 64 bytes */, + std::span curve25519_pubkey /* 32 bytes */, + std::span msg); /// "Softer" version that takes strings of regular chars [[nodiscard]] bool verify( @@ -30,7 +31,7 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string /// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519 /// pubkey: this always returns the positive value. You can get the other possibility (the /// negative) by setting the sign bit, i.e. `returned_pubkey[31] |= 0x80`. -std::array pubkey(ustring_view curve25519_pubkey); +std::array pubkey(std::span curve25519_pubkey); /// "Softer" version that takes/returns strings of regular chars std::string pubkey(std::string_view curve25519_pubkey); diff --git a/src/blinding.cpp b/src/blinding.cpp index 6bdf7bd9..927aa5ea 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -23,7 +23,7 @@ using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; -std::array blind15_factor(ustring_view server_pk) { +std::array blind15_factor(std::span server_pk) { assert(server_pk.size() == 32); crypto_generichash_blake2b_state st; @@ -37,7 +37,8 @@ std::array blind15_factor(ustring_view server_pk) { return k; } -std::array blind25_factor(ustring_view session_id, ustring_view server_pk) { +std::array blind25_factor( + std::span session_id, std::span server_pk) { assert(session_id.size() == 32 || session_id.size() == 33); assert(server_pk.size() == 32); @@ -59,20 +60,26 @@ std::array blind25_factor(ustring_view session_id, ustring_vi namespace { - void blind15_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { + void blind15_id_impl( + std::span session_id, + std::span server_pk, + unsigned char* out) { auto k = blind15_factor(server_pk); if (session_id.size() == 33) - session_id.remove_prefix(1); + session_id = session_id.subspan(1); auto ed_pk = xed25519::pubkey(session_id); if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; out[0] = 0x15; } - void blind25_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { + void blind25_id_impl( + std::span session_id, + std::span server_pk, + unsigned char* out) { auto k = blind25_factor(session_id, server_pk); if (session_id.size() == 33) - session_id.remove_prefix(1); + session_id = session_id.subspan(1); auto ed_pk = xed25519::pubkey(session_id); if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; @@ -81,18 +88,19 @@ namespace { } // namespace -ustring blind15_id(ustring_view session_id, ustring_view server_pk) { +std::vector blind15_id( + std::span session_id, std::span server_pk) { if (session_id.size() == 33) { if (session_id[0] != 0x05) throw std::invalid_argument{"blind15_id: session_id must start with 0x05"}; - session_id.remove_prefix(1); + session_id = session_id.subspan(1); } else if (session_id.size() != 32) { throw std::invalid_argument{"blind15_id: session_id must be 32 or 33 bytes"}; } if (server_pk.size() != 32) throw std::invalid_argument{"blind15_id: server_pk must be 32 bytes"}; - ustring result; + std::vector result; result.resize(33); blind15_id_impl(session_id, server_pk, result.data()); return result; @@ -112,7 +120,7 @@ std::array blind15_id(std::string_view session_id, std::string_v oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); uc33 blinded; - blind15_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); + blind15_id_impl(to_span(raw_sid), to_span(raw_server_pk), blinded.data()); std::array result; result[0] = oxenc::to_hex(blinded.begin(), blinded.end()); blinded.back() ^= 0x80; @@ -120,7 +128,8 @@ std::array blind15_id(std::string_view session_id, std::string_v return result; } -ustring blind25_id(ustring_view session_id, ustring_view server_pk) { +std::vector blind25_id( + std::span session_id, std::span server_pk) { if (session_id.size() == 33) { if (session_id[0] != 0x05) throw std::invalid_argument{"blind25_id: session_id must start with 0x05"}; @@ -130,7 +139,7 @@ ustring blind25_id(ustring_view session_id, ustring_view server_pk) { if (server_pk.size() != 32) throw std::invalid_argument{"blind25_id: server_pk must be 32 bytes"}; - ustring result; + std::vector result; result.resize(33); blind25_id_impl(session_id, server_pk, result.data()); return result; @@ -150,11 +159,14 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); uc33 blinded; - blind25_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); + blind25_id_impl(to_span(raw_sid), to_span(raw_server_pk), blinded.data()); return oxenc::to_hex(blinded.begin(), blinded.end()); } -ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { +std::vector blinded15_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::vector* session_id) { if (ed_pubkey.size() != 32) throw std::invalid_argument{"blind15_id_from_ed: ed_pubkey must be 32 bytes"}; if (server_pk.size() != 32) @@ -170,7 +182,7 @@ ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; } - ustring result; + std::vector result; result.resize(33); auto k = blind15_factor(server_pk); if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pubkey.data())) @@ -179,7 +191,10 @@ ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust return result; } -ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { +std::vector blinded25_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::vector* session_id) { if (ed_pubkey.size() != 32) throw std::invalid_argument{"blind25_id_from_ed: ed_pubkey must be 32 bytes"}; if (server_pk.size() != 32) @@ -187,7 +202,7 @@ ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust if (session_id && session_id->size() != 0 && session_id->size() != 33) throw std::invalid_argument{"blind25_id_from_ed: session_id pointer must be 0 or 33 bytes"}; - ustring tmp_session_id; + std::vector tmp_session_id; if (!session_id) session_id = &tmp_session_id; if (session_id->size() == 0) { @@ -199,7 +214,7 @@ ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust auto k = blind25_factor(*session_id, server_pk); - ustring result; + std::vector result; result.resize(33); // Blinded25 ids are always constructed using the absolute value of the ed pubkey, so if // negative we need to clear the sign bit to make it positive before computing the blinded @@ -215,7 +230,9 @@ ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ust } std::pair blind15_key_pair( - ustring_view ed25519_sk, ustring_view server_pk, uc32* k) { + std::span ed25519_sk, + std::span server_pk, + uc32* k) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -251,7 +268,9 @@ std::pair blind15_key_pair( } std::pair blind25_key_pair( - ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime) { + std::span ed25519_sk, + std::span server_pk, + uc32* k_prime) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -271,7 +290,7 @@ std::pair blind25_key_pair( throw std::runtime_error{ "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; - ustring_view X{session_id.data() + 1, 32}; + std::span X{session_id.data() + 1, 32}; /// Generate the blinding factor (storing into `*k`, if a pointer was provided) uc32 k_tmp; @@ -301,9 +320,9 @@ std::pair blind25_key_pair( return result; } -static const auto version_blinding_hash_key_sig = to_unsigned_sv("VersionCheckKey_sig"sv); +static const auto version_blinding_hash_key_sig = to_span("VersionCheckKey_sig"); -std::pair blind_version_key_pair(ustring_view ed25519_sk) { +std::pair blind_version_key_pair(std::span ed25519_sk) { if (ed25519_sk.size() != 32 && ed25519_sk.size() != 64) throw std::invalid_argument{ "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " @@ -327,10 +346,13 @@ std::pair blind_version_key_pair(ustring_view ed25519_sk) { return result; } -static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); -static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); +static const auto hash_key_seed = to_span("SessCommBlind25_seed"); +static const auto hash_key_sig = to_span("SessCommBlind25_sig"); -ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { +std::vector blind25_sign( + std::span ed25519_sk, + std::string_view server_pk_in, + std::span message) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -348,7 +370,7 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust else throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; - auto [A, a] = blind25_key_pair(ed25519_sk, to_sv(server_pk)); + auto [A, a] = blind25_key_pair(ed25519_sk, to_span(server_pk)); uc32 seedhash; crypto_generichash_blake2b( @@ -370,7 +392,7 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust uc32 r; crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); - ustring result; + std::vector result; result.resize(64); auto* sig_R = result.data(); auto* sig_S = result.data() + 32; @@ -392,7 +414,10 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } -ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { +std::vector blind15_sign( + std::span ed25519_sk, + std::string_view server_pk_in, + std::span message) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -433,7 +458,7 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); // sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) - ustring result; + std::vector result; result.resize(64); auto* sig_R = result.data(); auto* sig_S = result.data() + 32; @@ -456,41 +481,48 @@ ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } -ustring blind_version_sign_request( - ustring_view ed25519_sk, +std::vector blind_version_sign_request( + std::span ed25519_sk, uint64_t timestamp, std::string_view method, std::string_view path, - std::optional body) { + std::optional> body) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); // Signature should be on `TIMESTAMP || METHOD || PATH || BODY` - ustring buf; + std::vector ts = to_vector(std::to_string(timestamp)); + std::vector buf; buf.reserve(10 /* timestamp */ + method.size() + path.size() + (body ? body->size() : 0)); - buf += to_unsigned_sv(std::to_string(timestamp)); - buf += to_unsigned_sv(method); - buf += to_unsigned_sv(path); + buf.insert(buf.end(), ts.begin(), ts.end()); + buf.insert(buf.end(), method.begin(), method.end()); + buf.insert(buf.end(), path.begin(), path.end()); if (body) - buf += *body; + buf.insert(buf.end(), body->begin(), body->end()); return ed25519::sign({sk.data(), sk.size()}, buf); } -ustring blind_version_sign(ustring_view ed25519_sk, Platform platform, uint64_t timestamp) { +std::vector blind_version_sign( + std::span ed25519_sk, Platform platform, uint64_t timestamp) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); // Signature should be on `TIMESTAMP || METHOD || PATH` - ustring buf; + std::vector ts = to_vector(std::to_string(timestamp)); + std::vector method = to_vector("GET"); + std::vector buf; buf.reserve(10 + 6 + 33); - buf += to_unsigned_sv(std::to_string(timestamp)); - buf += to_unsigned("GET"); + buf.insert(buf.end(), ts.begin(), ts.end()); + buf.insert(buf.end(), method.begin(), method.end()); + std::vector url; switch (platform) { - case Platform::android: buf += to_unsigned("/session_version?platform=android"); break; - case Platform::desktop: buf += to_unsigned("/session_version?platform=desktop"); break; - case Platform::ios: buf += to_unsigned("/session_version?platform=ios"); break; + case Platform::android: url = to_vector("/session_version?platform=android"); break; + case Platform::desktop: url = to_vector("/session_version?platform=desktop"); break; + case Platform::ios: url = to_vector("/session_version?platform=ios"); break; + default: url = to_vector("/session_version?platform=desktop"); break; } + buf.insert(buf.end(), url.begin(), url.end()); return ed25519::sign({sk.data(), sk.size()}, buf); } @@ -510,7 +542,7 @@ bool session_id_matches_blinded_id( "session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; std::string converted_blind_id1, converted_blind_id2; - ustring converted_blind_id1_raw; + std::vector converted_blind_id1_raw; switch (blinded_id[0]) { case '1': { @@ -580,7 +612,9 @@ LIBSESSION_C_API bool session_blind15_sign( unsigned char* blinded_sig_out) { try { auto sig = session::blind15_sign( - {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); + {ed25519_seckey, 64}, + {reinterpret_cast(server_pk), 32}, + {msg, msg_len}); std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -596,7 +630,9 @@ LIBSESSION_C_API bool session_blind25_sign( unsigned char* blinded_sig_out) { try { auto sig = session::blind25_sign( - {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); + {ed25519_seckey, 64}, + {reinterpret_cast(server_pk), 32}, + {msg, msg_len}); std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -615,9 +651,9 @@ LIBSESSION_C_API bool session_blind_version_sign_request( std::string_view method_sv{method}; std::string_view path_sv{path}; - std::optional body_sv{std::nullopt}; + std::optional> body_sv{std::nullopt}; if (body) - body_sv = ustring_view{body, body_len}; + body_sv = std::span{body, body_len}; try { auto sig = session::blind_version_sign_request( diff --git a/src/config.cpp b/src/config.cpp index 2972d9fb..4dfdc93c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -346,7 +346,7 @@ namespace { return std::string_view{reinterpret_cast(hash.data()), hash.size()}; } - hash_t& hash_msg(hash_t& into, ustring_view serialized) { + hash_t& hash_msg(hash_t& into, std::span serialized) { crypto_generichash_blake2b( into.data(), into.size(), serialized.data(), serialized.size(), nullptr, 0); return into; @@ -433,17 +433,18 @@ void verify_config_sig( std::optional>* verified_signature, bool trust_signature) { if (dict.skip_until("~")) { - dict.consume_signature([&](ustring_view to_verify, ustring_view sig) { - if (sig.size() != 64) - throw signature_error{"Config signature is invalid (not 64B)"}; - if (verifier && !verifier(to_verify, sig)) - throw signature_error{"Config signature failed verification"}; - if (verified_signature && (verifier || trust_signature)) { - if (!*verified_signature) - verified_signature->emplace(); - std::memcpy((*verified_signature)->data(), sig.data(), 64); - } - }); + dict.consume_signature( + [&](std::span to_verify, std::span sig) { + if (sig.size() != 64) + throw signature_error{"Config signature is invalid (not 64B)"}; + if (verifier && !verifier(to_verify, sig)) + throw signature_error{"Config signature failed verification"}; + if (verified_signature && (verifier || trust_signature)) { + if (!*verified_signature) + verified_signature->emplace(); + std::memcpy((*verified_signature)->data(), sig.data(), 64); + } + }); } else if (verifier) { throw missing_signature{"Config signature is missing"}; } @@ -523,14 +524,14 @@ ConfigMessage::ConfigMessage() { } ConfigMessage::ConfigMessage( - ustring_view serialized, + std::span serialized, verify_callable verifier_, sign_callable signer_, int lag, bool trust_signature) : verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} { - oxenc::bt_dict_consumer dict{from_unsigned_sv(serialized)}; + oxenc::bt_dict_consumer dict{serialized}; try { hash_msg(seqno_hash_.second, serialized); @@ -561,7 +562,7 @@ ConfigMessage::ConfigMessage( } ConfigMessage::ConfigMessage( - const std::vector& serialized_confs, + const std::vector>& serialized_confs, verify_callable verifier_, sign_callable signer_, int lag, @@ -690,7 +691,7 @@ ConfigMessage::ConfigMessage( } MutableConfigMessage::MutableConfigMessage( - const std::vector& serialized_confs, + const std::vector>& serialized_confs, verify_callable verifier, sign_callable signer, int lag, @@ -706,7 +707,10 @@ MutableConfigMessage::MutableConfigMessage( } MutableConfigMessage::MutableConfigMessage( - ustring_view config, verify_callable verifier, sign_callable signer, int lag) : + std::span config, + verify_callable verifier, + sign_callable signer, + int lag) : MutableConfigMessage{ std::vector{{config}}, std::move(verifier), @@ -725,13 +729,14 @@ const oxenc::bt_dict& MutableConfigMessage::diff() { return diff_; } -ustring ConfigMessage::serialize(bool enable_signing) { +std::vector ConfigMessage::serialize(bool enable_signing) { return serialize_impl( diff(), // implicitly prunes (if actually a mutable instance) enable_signing); } -ustring ConfigMessage::serialize_impl(const oxenc::bt_dict& curr_diff, bool enable_signing) { +std::vector ConfigMessage::serialize_impl( + const oxenc::bt_dict& curr_diff, bool enable_signing) { oxenc::bt_dict_producer outer{}; outer.append("#", seqno()); @@ -771,7 +776,7 @@ ustring ConfigMessage::serialize_impl(const oxenc::bt_dict& curr_diff, bool enab reinterpret_cast(verified_signature_->data()), verified_signature_->size()}); } else if (signer && enable_signing) { - outer.append_signature("~", [this](ustring_view to_sign) { + outer.append_signature("~", [this](std::span to_sign) { auto sig = signer(to_sign); if (sig.size() != 64) throw std::logic_error{ @@ -779,13 +784,13 @@ ustring ConfigMessage::serialize_impl(const oxenc::bt_dict& curr_diff, bool enab return sig; }); } - return ustring{to_unsigned_sv(outer.view())}; + return to_vector(outer.view()); } const hash_t& MutableConfigMessage::hash() { return hash(serialize()); } -const hash_t& MutableConfigMessage::hash(ustring_view serialized) { +const hash_t& MutableConfigMessage::hash(std::span serialized) { return hash_msg(seqno_hash_.second, serialized); } diff --git a/src/config/base.cpp b/src/config/base.cpp index 2ce68c37..9a7583ff 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -63,8 +63,8 @@ std::unique_ptr make_config_message(bool from_dirty, Args&&... ar } std::vector ConfigBase::merge( - const std::vector>& configs) { - std::vector> config_views; + const std::vector>>& configs) { + std::vector>> config_views; config_views.reserve(configs.size()); for (auto& [hash, data] : configs) config_views.emplace_back(hash, data); @@ -72,16 +72,16 @@ std::vector ConfigBase::merge( } std::vector ConfigBase::merge( - const std::vector>& configs) { + const std::vector>>& configs) { if (accepts_protobuf() && !_keys.empty()) { - std::list keep_alive; - std::vector> parsed; + std::list> keep_alive; + std::vector>> parsed; parsed.reserve(configs.size()); for (auto& [h, c] : configs) { try { auto unwrapped = protos::unwrap_config( - ustring_view{_keys.front().data(), _keys.front().size()}, + std::span{_keys.front().data(), _keys.front().size()}, c, storage_namespace()); @@ -90,7 +90,8 @@ std::vector ConfigBase::merge( // support multi-device for users running those old versions try { auto unwrapped2 = protos::unwrap_config( - ustring_view{_keys.front().data(), _keys.front().size()}, + std::span{ + _keys.front().data(), _keys.front().size()}, unwrapped, storage_namespace()); log::warning( @@ -113,14 +114,14 @@ std::vector ConfigBase::merge( } std::vector ConfigBase::_merge( - const std::vector>& configs) { + const std::vector>>& configs) { if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; const auto old_seqno = _config->seqno(); std::vector all_hashes; - std::vector all_confs; + std::vector> all_confs; all_hashes.reserve(configs.size() + 1); all_confs.reserve(configs.size() + 1); @@ -145,7 +146,7 @@ std::vector ConfigBase::_merge( // at the end (rather than the beginning) so that it is identical to one of the incoming // messages, *that* one becomes the config superset rather than our current, hash-unknown value. - ustring mine; + std::vector mine; bool mine_last = false; if (old_seqno != 0 || is_dirty()) { mine = _config->serialize(); @@ -157,7 +158,7 @@ std::vector ConfigBase::_merge( } } - std::vector> plaintexts; + std::vector>> plaintexts; // TODO: // - handle multipart messages. Each part of a multipart message starts with `m` and then is @@ -193,7 +194,10 @@ std::vector ConfigBase::_merge( for (auto& [hash, plain] : plaintexts) { // Remove prefix padding: - if (auto p = plain.find_first_not_of((unsigned char)0); p > 0 && p != std::string::npos) { + if (auto it = std::find_if( + plain.begin(), plain.end(), [](unsigned char c) { return c != 0; }); + it != plain.begin() && it != plain.end()) { + auto p = std::distance(plain.begin(), it); std::memmove(plain.data(), plain.data() + p, plain.size() - p); plain.resize(plain.size() - p); } @@ -213,8 +217,8 @@ std::vector ConfigBase::_merge( bool was_compressed = plain[0] == 'z'; // 'z' prefix indicates zstd-compressed data: if (was_compressed) { - if (auto decompressed = - zstd_decompress(ustring_view{plain.data() + 1, plain.size() - 1}); + if (auto decompressed = zstd_decompress( + std::span{plain.data() + 1, plain.size() - 1}); decompressed && !decompressed->empty()) plain = std::move(*decompressed); else { @@ -391,22 +395,23 @@ bool ConfigBase::needs_push() const { // Tries to compresses the message; if the compressed version (including the 'z' prefix tag) is // smaller than the source message then we modify `msg` to contain the 'z'-prefixed compressed // message, otherwise we leave it as-is. -void compress_message(ustring& msg, int level) { +void compress_message(std::vector& msg, int level) { if (!level) return; // "z" is our zstd compression marker prefix byte - ustring compressed = zstd_compress(msg, level, to_unsigned_sv("z"sv)); + std::vector compressed = zstd_compress(msg, level, to_span("z")); if (compressed.size() < msg.size()) msg = std::move(compressed); } -std::tuple> ConfigBase::push() { +std::tuple, std::vector> ConfigBase::push() { if (_keys.empty()) throw std::logic_error{"Cannot push data without an encryption key!"}; auto s = _config->seqno(); - std::tuple> ret{s, _config->serialize(), {}}; + std::tuple, std::vector> ret{ + s, _config->serialize(), {}}; auto& [seqno, msg, obs] = ret; if (auto lvl = compression_level()) @@ -417,7 +422,7 @@ std::tuple> ConfigBase::push() { if (accepts_protobuf() && !_keys.empty()) msg = protos::wrap_config( - ustring_view{_keys.front().data(), _keys.front().size()}, + std::span{_keys.front().data(), _keys.front().size()}, msg, s, storage_namespace()); @@ -445,7 +450,7 @@ void ConfigBase::confirm_pushed(seqno_t seqno, std::string msg_hash) { } } -ustring ConfigBase::dump() { +std::vector ConfigBase::dump() { if (is_readonly()) _old_hashes.clear(); @@ -454,9 +459,9 @@ ustring ConfigBase::dump() { return d; } -ustring ConfigBase::make_dump() const { +std::vector ConfigBase::make_dump() const { auto data = _config->serialize(false /* disable signing for local storage */); - auto data_sv = from_unsigned_sv(data); + auto data_sv = to_string_view(data); oxenc::bt_list old_hashes; oxenc::bt_dict_producer d; @@ -468,13 +473,13 @@ ustring ConfigBase::make_dump() const { extra_data(d.append_dict("+")); - return ustring{to_unsigned_sv(d.view())}; + return to_vector(d.view()); } ConfigBase::ConfigBase( - std::optional dump, - std::optional ed25519_pubkey, - std::optional ed25519_secretkey) { + std::optional> dump, + std::optional> ed25519_pubkey, + std::optional> ed25519_secretkey) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; @@ -483,9 +488,10 @@ ConfigBase::ConfigBase( } void ConfigSig::init_sig_keys( - std::optional ed25519_pubkey, std::optional ed25519_secretkey) { + std::optional> ed25519_pubkey, + std::optional> ed25519_secretkey) { if (ed25519_secretkey) { - if (ed25519_pubkey && *ed25519_pubkey != ed25519_secretkey->substr(32)) + if (ed25519_pubkey && *ed25519_pubkey != ed25519_secretkey->subspan(32)) throw std::invalid_argument{"Invalid signing keys: secret key and pubkey do not match"}; set_sig_keys(*ed25519_secretkey); } else if (ed25519_pubkey) { @@ -496,15 +502,14 @@ void ConfigSig::init_sig_keys( } void ConfigBase::init( - std::optional dump, - std::optional ed25519_pubkey, - std::optional ed25519_secretkey) { + std::optional> dump, + std::optional> ed25519_pubkey, + std::optional> ed25519_secretkey) { if (!dump) { _state = ConfigState::Clean; _config = std::make_unique(); } else { - - oxenc::bt_dict_consumer d{from_unsigned_sv(*dump)}; + oxenc::bt_dict_consumer d{*dump}; if (!d.skip_until("!")) throw std::runtime_error{ "Unable to parse dumped config data: did not find '!' state key"}; @@ -513,7 +518,7 @@ void ConfigBase::init( if (!d.skip_until("$")) throw std::runtime_error{ "Unable to parse dumped config data: did not find '$' data key"}; - auto data = to_unsigned_sv(d.consume_string_view()); + auto data = to_span(d.consume_string_view()); if (_state == ConfigState::Dirty) // If we dumped dirty data then we need to reload it as a mutable config message so that // the seqno gets incremented. This "wastes" one seqno value (since we didn't send the @@ -554,7 +559,7 @@ int ConfigBase::key_count() const { return _keys.size(); } -bool ConfigBase::has_key(ustring_view key) const { +bool ConfigBase::has_key(std::span key) const { if (key.size() != 32) throw std::invalid_argument{"invalid key given to has_key(): not 32-bytes"}; @@ -565,15 +570,16 @@ bool ConfigBase::has_key(ustring_view key) const { return false; } -std::vector ConfigBase::get_keys() const { - std::vector ret; +std::vector> ConfigBase::get_keys() const { + std::vector> ret; ret.reserve(_keys.size()); for (const auto& key : _keys) ret.emplace_back(key.data(), key.size()); return ret; } -void ConfigBase::add_key(ustring_view key, bool high_priority, bool dirty_config) { +void ConfigBase::add_key( + std::span key, bool high_priority, bool dirty_config) { static_assert( sizeof(Key) == KEY_SIZE, "std::array appears to have some overhead which seems bad"); @@ -611,7 +617,8 @@ int ConfigBase::clear_keys(bool dirty_config) { return ret; } -void ConfigBase::replace_keys(const std::vector& new_keys, bool dirty_config) { +void ConfigBase::replace_keys( + const std::vector>& new_keys, bool dirty_config) { if (new_keys.empty()) { if (_keys.empty()) return; @@ -636,7 +643,7 @@ void ConfigBase::replace_keys(const std::vector& new_keys, bool di dirty(); } -bool ConfigBase::remove_key(ustring_view key, size_t from, bool dirty_config) { +bool ConfigBase::remove_key(std::span key, size_t from, bool dirty_config) { auto starting_size = _keys.size(); if (from >= starting_size) return false; @@ -659,15 +666,15 @@ bool ConfigBase::remove_key(ustring_view key, size_t from, bool dirty_config) { return _keys.size() < starting_size; } -void ConfigBase::load_key(ustring_view ed25519_secretkey) { +void ConfigBase::load_key(std::span ed25519_secretkey) { if (!(ed25519_secretkey.size() == 64 || ed25519_secretkey.size() == 32)) throw std::invalid_argument{ encryption_domain() + " requires an Ed25519 64-byte secret key or 32-byte seed"s}; - add_key(ed25519_secretkey.substr(0, 32)); + add_key(ed25519_secretkey.subspan(0, 32)); } -void ConfigSig::set_sig_keys(ustring_view secret) { +void ConfigSig::set_sig_keys(std::span secret) { if (secret.size() != 64) throw std::invalid_argument{"Invalid sodium secret: expected 64 bytes"}; clear_sig_keys(); @@ -676,12 +683,12 @@ void ConfigSig::set_sig_keys(ustring_view secret) { _sign_pk.emplace(); crypto_sign_ed25519_sk_to_pk(_sign_pk->data(), _sign_sk.data()); - set_verifier([this](ustring_view data, ustring_view sig) { + set_verifier([this](std::span data, std::span sig) { return 0 == crypto_sign_ed25519_verify_detached( sig.data(), data.data(), data.size(), _sign_pk->data()); }); - set_signer([this](ustring_view data) { - ustring sig; + set_signer([this](std::span data) { + std::vector sig; sig.resize(64); if (0 != crypto_sign_ed25519_detached( sig.data(), nullptr, data.data(), data.size(), _sign_sk.data())) @@ -690,13 +697,13 @@ void ConfigSig::set_sig_keys(ustring_view secret) { }); } -void ConfigSig::set_sig_pubkey(ustring_view pubkey) { +void ConfigSig::set_sig_pubkey(std::span pubkey) { if (pubkey.size() != 32) throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; _sign_pk.emplace(); std::memcpy(_sign_pk->data(), pubkey.data(), 32); - set_verifier([this](ustring_view data, ustring_view sig) { + set_verifier([this](std::span data, std::span sig) { return 0 == crypto_sign_ed25519_verify_detached( sig.data(), data.data(), data.size(), _sign_pk->data()); }); @@ -761,10 +768,11 @@ LIBSESSION_EXPORT config_string_list* config_merge( size_t count) { return wrap_exceptions(conf, [&] { auto& config = *unbox(conf); - std::vector> confs; + 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]}); + confs.emplace_back( + msg_hashes[i], std::span{configs[i], lengths[i]}); return make_string_list(config.merge(confs)); }); diff --git a/src/config/community.cpp b/src/config/community.cpp index 8a4f1351..1832f6f6 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -23,7 +23,8 @@ community::community(std::string_view base_url_, std::string_view room_) { set_room(std::move(room_)); } -community::community(std::string_view base_url, std::string_view room, ustring_view pubkey_) : +community::community( + std::string_view base_url, std::string_view room, std::span pubkey_) : community{base_url, room} { set_pubkey(pubkey_); } @@ -45,10 +46,10 @@ void community::set_base_url(std::string_view new_url) { base_url_ = canonical_url(new_url); } -void community::set_pubkey(ustring_view pubkey) { +void community::set_pubkey(std::span pubkey) { if (pubkey.size() != 32) throw std::invalid_argument{"Invalid pubkey: expected a 32-byte pubkey"}; - pubkey_ = pubkey; + pubkey_.assign(pubkey.begin(), pubkey.end()); } void community::set_pubkey(std::string_view pubkey) { pubkey_ = decode_pubkey(pubkey); @@ -79,7 +80,7 @@ std::string community::full_url() const { } std::string community::full_url( - std::string_view base_url, std::string_view room, ustring_view pubkey) { + std::string_view base_url, std::string_view room, std::span pubkey) { std::string url{base_url}; url += '/'; url += room; @@ -129,9 +130,9 @@ std::string community::canonical_room(std::string_view room) { return r; } -std::tuple> community::parse_partial_url( - std::string_view url) { - std::tuple> result; +std::tuple>> +community::parse_partial_url(std::string_view url) { + std::tuple>> result; auto& [base_url, room_token, maybe_pubkey] = result; // Consume the URL from back to front; first the public key: @@ -155,7 +156,8 @@ std::tuple> community::parse_pa return result; } -std::tuple community::parse_full_url(std::string_view full_url) { +std::tuple> community::parse_full_url( + std::string_view full_url) { auto [base, rm, maybe_pk] = parse_partial_url(full_url); if (!maybe_pk) throw std::invalid_argument{"Invalid community URL: no valid server pubkey"}; @@ -213,8 +215,8 @@ LIBSESSION_C_API bool community_parse_partial_url( LIBSESSION_C_API void community_make_full_url( const char* base_url, const char* room, const unsigned char* pubkey, char* full_url) { - auto full = - session::config::community::full_url(base_url, room, session::ustring_view{pubkey, 32}); + auto full = session::config::community::full_url( + base_url, room, std::span{pubkey, 32}); assert(full.size() <= COMMUNITY_FULL_URL_MAX_LENGTH); std::memcpy(full_url, full.data(), full.size() + 1); } diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 0473b8f9..093c0a9c 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -14,8 +14,6 @@ using namespace std::literals; using namespace session::config; -using session::ustring; -using session::ustring_view; LIBSESSION_C_API const size_t CONTACT_MAX_NAME_LENGTH = contact_info::MAX_NAME_LENGTH; @@ -56,7 +54,9 @@ void contact_info::set_nickname_truncated(std::string n) { set_nickname(utf8_truncate(std::move(n), MAX_NAME_LENGTH)); } -Contacts::Contacts(ustring_view ed25519_secretkey, std::optional dumped) : +Contacts::Contacts( + std::span ed25519_secretkey, + std::optional> dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -75,7 +75,7 @@ void contact_info::load(const dict& info_dict) { nickname = maybe_string(info_dict, "N").value_or(""); auto url = maybe_string(info_dict, "p"); - auto key = maybe_ustring(info_dict, "q"); + auto key = maybe_vector(info_dict, "q"); if (url && key && !url->empty() && key->size() == 32) { profile_picture.url = std::move(*url); profile_picture.key = std::move(*key); @@ -152,7 +152,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); if (std::strlen(c.profile_pic.url)) { profile_picture.url = c.profile_pic.url; - profile_picture.key = {c.profile_pic.key, 32}; + profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); } approved = c.approved; approved_me = c.approved_me; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 3355f1dc..174bcb12 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -19,7 +19,6 @@ #include "session/util.hpp" using namespace std::literals; -using session::ustring_view; namespace session::config { @@ -41,7 +40,7 @@ namespace convo { } community::community(const convo_info_volatile_community& c) : - config::community{c.base_url, c.room, ustring_view{c.pubkey, 32}}, + config::community{c.base_url, c.room, std::span{c.pubkey, 32}}, base{c.last_read, c.unread} {} void community::into(convo_info_volatile_community& c) const { @@ -92,7 +91,8 @@ namespace convo { } // namespace convo ConvoInfoVolatile::ConvoInfoVolatile( - ustring_view ed25519_secretkey, std::optional dumped) : + std::span ed25519_secretkey, + std::optional> dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -117,13 +117,12 @@ convo::one_to_one ConvoInfoVolatile::get_or_construct_1to1(std::string_view pubk } ConfigBase::DictFieldProxy ConvoInfoVolatile::community_field( - const convo::community& comm, ustring_view* get_pubkey) const { + const convo::community& comm, std::span* get_pubkey) const { auto record = data["o"][comm.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) - *get_pubkey = - ustring_view{reinterpret_cast(pk.data()), pk.size()}; + *get_pubkey = to_span(pk); } return record["R"][comm.room_norm()]; } @@ -132,7 +131,7 @@ std::optional ConvoInfoVolatile::get_community( std::string_view base_url, std::string_view room) const { convo::community og{base_url, community::canonical_room(room)}; - ustring_view pubkey; + std::span pubkey; if (auto* info_dict = community_field(og, &pubkey).dict()) { og.load(*info_dict); if (!pubkey.empty()) @@ -149,7 +148,9 @@ std::optional ConvoInfoVolatile::get_community( } convo::community ConvoInfoVolatile::get_or_construct_community( - std::string_view base_url, std::string_view room, ustring_view pubkey) const { + std::string_view base_url, + std::string_view room, + std::span pubkey) const { convo::community result{base_url, community::canonical_room(room), pubkey}; if (auto* info_dict = community_field(result).dict()) @@ -261,7 +262,8 @@ void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { erase_community(base, room); } -std::tuple> ConvoInfoVolatile::push() { +std::tuple, std::vector> +ConvoInfoVolatile::push() { // Prune off any conversations with last_read timestamps more than PRUNE_HIGH ago (unless they // also have a `unread` flag set, in which case we keep them indefinitely). prune_stale(); @@ -545,7 +547,8 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( conf, [&] { unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + ->get_or_construct_community( + base_url, room, std::span{pubkey, 32}) .into(*convo); return true; }, diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 8729131b..538cf620 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -8,6 +8,7 @@ #include #include "session/export.h" +#include "session/util.hpp" using namespace std::literals; @@ -20,10 +21,6 @@ namespace { return reinterpret_cast(x); } - ustring_view to_unsigned_sv(std::string_view v) { - return {to_unsigned(v.data()), v.size()}; - } - } // namespace static constexpr size_t DOMAIN_MAX_SIZE = 24; @@ -31,7 +28,7 @@ static constexpr auto NONCE_KEY_PREFIX = "libsessionutil-config-encrypted-"sv; static_assert(NONCE_KEY_PREFIX.size() + DOMAIN_MAX_SIZE < crypto_generichash_blake2b_KEYBYTES_MAX); static std::array make_encrypt_key( - ustring_view key_base, uint64_t message_size, std::string_view domain) { + std::span key_base, uint64_t message_size, std::string_view domain) { if (key_base.size() != 32) throw std::invalid_argument{"encrypt called with key_base != 32 bytes"}; if (domain.size() < 1 || domain.size() > DOMAIN_MAX_SIZE) @@ -54,16 +51,22 @@ static std::array ma return key; } -ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain) { - ustring msg; +std::vector encrypt( + std::span message, + std::span key_base, + std::string_view domain) { + std::vector msg; msg.reserve( message.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - msg.assign(message); + msg.assign(message.begin(), message.end()); encrypt_inplace(msg, key_base, domain); return msg; } -void encrypt_inplace(ustring& message, ustring_view key_base, std::string_view domain) { +void encrypt_inplace( + std::vector& message, + std::span key_base, + std::string_view domain) { auto key = make_encrypt_key(key_base, message.size(), domain); std::string nonce_key{NONCE_KEY_PREFIX}; @@ -103,18 +106,24 @@ static_assert( ENCRYPT_DATA_OVERHEAD == crypto_aead_xchacha20poly1305_IETF_ABYTES + crypto_aead_xchacha20poly1305_IETF_NPUBBYTES); -ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain) { - ustring x{ciphertext}; +std::vector decrypt( + std::span ciphertext, + std::span key_base, + std::string_view domain) { + std::vector x = session::to_vector(ciphertext); decrypt_inplace(x, key_base, domain); return x; } -void decrypt_inplace(ustring& ciphertext, ustring_view key_base, std::string_view domain) { +void decrypt_inplace( + std::vector& ciphertext, + std::span key_base, + std::string_view domain) { size_t message_len = ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; if (message_len > ciphertext.size()) // overflow throw decrypt_error{"Decryption failed: ciphertext is too short"}; - ustring_view nonce = ustring_view{ciphertext}.substr( + std::span nonce = std::span{ciphertext}.subspan( ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); auto key = make_encrypt_key(key_base, message_len, domain); @@ -135,18 +144,16 @@ void decrypt_inplace(ustring& ciphertext, ustring_view key_base, std::string_vie ciphertext.resize(mlen_wrote); } -void pad_message(ustring& data, size_t overhead) { +void pad_message(std::vector& data, size_t overhead) { size_t target_size = padded_size(data.size(), overhead); if (target_size > data.size()) - data.insert(0, target_size - data.size(), '\0'); + data.insert(data.begin(), target_size - data.size(), 0); } } // namespace session::config extern "C" { -using session::ustring; - LIBSESSION_EXPORT unsigned char* config_encrypt( const unsigned char* plaintext, size_t len, @@ -154,7 +161,7 @@ LIBSESSION_EXPORT unsigned char* config_encrypt( const char* domain, size_t* ciphertext_size) { - ustring ciphertext; + std::vector ciphertext; try { ciphertext = session::config::encrypt({plaintext, len}, {key_base, 32}, domain); } catch (...) { @@ -174,7 +181,7 @@ LIBSESSION_EXPORT unsigned char* config_decrypt( const char* domain, size_t* plaintext_size) { - ustring plaintext; + std::vector plaintext; try { plaintext = session::config::decrypt({ciphertext, clen}, {key_base, 32}, domain); } catch (const std::exception& e) { diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 5c1e0031..2025ca0a 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -17,9 +17,9 @@ using namespace std::literals; namespace session::config::groups { Info::Info( - ustring_view ed25519_pubkey, - std::optional ed25519_secretkey, - std::optional dumped) : + std::span ed25519_pubkey, + std::optional> ed25519_secretkey, + std::optional> dumped) : ConfigBase{dumped, ed25519_pubkey, ed25519_secretkey}, id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} {} @@ -60,11 +60,13 @@ profile_pic Info::get_profile_pic() const { if (auto* url = data["p"].string(); url && !url->empty()) pic.url = *url; if (auto* key = data["q"].string(); key && key->size() == 32) - pic.key = {reinterpret_cast(key->data()), 32}; + pic.key.assign( + reinterpret_cast(key->data()), + reinterpret_cast(key->data()) + 32); return pic; } -void Info::set_profile_pic(std::string_view url, ustring_view key) { +void Info::set_profile_pic(std::string_view url, std::span key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); } @@ -254,7 +256,7 @@ LIBSESSION_C_API user_profile_pic groups_info_get_pic(const config_object* conf) /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic pic) { std::string_view url{pic.url}; - ustring_view key; + std::span key; if (!url.empty()) key = {pic.key, 32}; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 76c75969..dddf4f37 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -33,10 +33,10 @@ static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { } Keys::Keys( - ustring_view user_ed25519_secretkey, - ustring_view group_ed25519_pubkey, - std::optional group_ed25519_secretkey, - std::optional dumped, + std::span user_ed25519_secretkey, + std::span group_ed25519_pubkey, + std::optional> group_ed25519_secretkey, + std::optional> dumped, Info& info, Members& members) { @@ -66,14 +66,14 @@ bool Keys::needs_dump() const { return needs_dump_; } -ustring Keys::dump() { +std::vector Keys::dump() { auto dumped = make_dump(); needs_dump_ = false; return dumped; } -ustring Keys::make_dump() const { +std::vector Keys::make_dump() const { oxenc::bt_dict_producer d; { auto active = d.append_list("A"); @@ -91,7 +91,7 @@ ustring Keys::make_dump() const { auto ki = keys.append_dict(); // NB: Keys must be in sorted order ki.append("g", k.generation); - ki.append("k", from_unsigned_sv(k.key)); + ki.append("k", to_string_view(k.key)); ki.append( "t", std::chrono::duration_cast( @@ -103,16 +103,16 @@ ustring Keys::make_dump() const { if (!pending_key_config_.empty()) { auto pending = d.append_dict("P"); // NB: Keys must be in sorted order - pending.append("c", from_unsigned_sv(pending_key_config_)); + pending.append("c", to_string_view(pending_key_config_)); pending.append("g", pending_gen_); - pending.append("k", from_unsigned_sv(pending_key_)); + pending.append("k", to_string_view(pending_key_)); } - return ustring{to_unsigned_sv(d.view())}; + return to_vector(d.view()); } -void Keys::load_dump(ustring_view dump) { - oxenc::bt_dict_consumer d{from_unsigned_sv(dump)}; +void Keys::load_dump(std::span dump) { + oxenc::bt_dict_consumer d{dump}; if (d.skip_until("A")) { auto active = d.consume_list_consumer(); @@ -185,8 +185,8 @@ size_t Keys::size() const { return keys_.size() + !pending_key_config_.empty(); } -std::vector Keys::group_keys() const { - std::vector ret; +std::vector> Keys::group_keys() const { + std::vector> ret; ret.reserve(size()); if (!pending_key_config_.empty()) @@ -198,7 +198,7 @@ std::vector Keys::group_keys() const { return ret; } -ustring_view Keys::group_enc_key() const { +std::span Keys::group_enc_key() const { if (!pending_key_config_.empty()) return {pending_key_.data(), 32}; if (keys_.empty()) @@ -208,12 +208,12 @@ ustring_view Keys::group_enc_key() const { return {key.data(), key.size()}; } -void Keys::load_admin_key(ustring_view seed, Info& info, Members& members) { +void Keys::load_admin_key(std::span seed, Info& info, Members& members) { if (admin()) return; if (seed.size() == 64) - seed.remove_suffix(32); + seed = seed.subspan(0, seed.size() - 32); else if (seed.size() != 32) throw std::invalid_argument{ "Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"}; @@ -226,7 +226,7 @@ void Keys::load_admin_key(ustring_view seed, Info& info, Members& members) { throw std::runtime_error{ "Failed to load admin key: given secret key does not match group pubkey"}; - auto seckey = to_sv(sk); + auto seckey = to_span(sk); set_sig_keys(seckey); info.set_sig_keys(seckey); members.set_sig_keys(seckey); @@ -244,14 +244,14 @@ namespace { } constexpr auto seed_hash_key = "SessionGroupKeySeed"sv; - const ustring_view enc_key_hash_key = to_unsigned_sv("SessionGroupKeyGen"sv); + const std::span enc_key_hash_key = to_span("SessionGroupKeyGen"); constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; constexpr auto enc_key_member_hash_key = "SessionGroupKeyMemberKey"sv; - const ustring_view junk_seed_hash_key = to_unsigned_sv("SessionGroupJunkMembers"sv); + const std::span junk_seed_hash_key = to_span("SessionGroupJunkMembers"); } // namespace -ustring_view Keys::rekey(Info& info, Members& members) { +std::span Keys::rekey(Info& info, Members& members) { if (!admin()) throw std::logic_error{ "Unable to issue a new group encryption key without the main group keys"}; @@ -310,12 +310,12 @@ ustring_view Keys::rekey(Info& info, Members& members) { crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); - ustring_view enc_key{h1.data(), 32}; - ustring_view nonce{h1.data() + 32, 24}; + std::span enc_key{h1.data(), 32}; + std::span nonce{h1.data() + 32, 24}; oxenc::bt_dict_producer d{}; - d.append("#", from_unsigned_sv(nonce)); + d.append("#", to_string_view(nonce)); static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); static_assert(crypto_aead_xchacha20poly1305_ietf_ABYTES == 16); @@ -323,7 +323,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { unsigned char, crypto_aead_xchacha20poly1305_ietf_KEYBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES> encrypted; - std::string_view enc_sv = from_unsigned_sv(encrypted); + std::string_view enc_sv = to_string_view(encrypted); // Shared key for admins auto member_k = seed_hash(enc_key_admin_hash_key); @@ -346,7 +346,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { auto member_keys = d.append_list("k"); int member_count = 0; std::vector> member_xpk_raw; - std::vector member_xpks; + std::vector> member_xpks; member_xpk_raw.reserve(members.size()); member_xpks.reserve(members.size()); for (const auto& m : members) { @@ -358,10 +358,10 @@ ustring_view Keys::rekey(Info& info, Members& members) { enc_key, member_xpks, nonce, - to_sv(group_xsk), - to_sv(group_xpk), + to_span(group_xsk), + to_span(group_xpk), enc_key_member_hash_key, - [&](ustring_view enc_sv) { + [&](std::span enc_sv) { member_keys.append(enc_sv); member_count++; }, @@ -382,7 +382,7 @@ ustring_view Keys::rekey(Info& info, Members& members) { crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); - std::string_view junk_view = from_unsigned_sv(junk_data); + std::string_view junk_view = to_string_view(junk_data); while (!junk_view.empty()) { member_keys.append(junk_view.substr(0, encrypted.size())); junk_view.remove_prefix(encrypted.size()); @@ -392,7 +392,8 @@ ustring_view Keys::rekey(Info& info, Members& members) { // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). - d.append_signature("~", [this](ustring_view to_sign) { return sign(to_sign); }); + d.append_signature( + "~", [this](std::span to_sign) { return sign(to_sign); }); // Load this key/config/gen into our pending variables pending_gen_ = gen; @@ -410,17 +411,17 @@ ustring_view Keys::rekey(Info& info, Members& members) { needs_dump_ = true; - return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; + return std::span{pending_key_config_.data(), pending_key_config_.size()}; } -ustring Keys::sign(ustring_view data) const { +std::vector Keys::sign(std::span data) const { auto sig = signer_(data); if (sig.size() != 64) throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; return sig; } -ustring Keys::key_supplement(const std::vector& sids) const { +std::vector Keys::key_supplement(const std::vector& sids) const { if (!admin()) throw std::logic_error{ "Unable to issue supplemental group encryption keys without the main group keys"}; @@ -459,7 +460,7 @@ ustring Keys::key_supplement(const std::vector& sids) const { for (auto& ki : keys_) { auto d = supp.append_dict(); d.append("g", ki.generation); - d.append("k", from_unsigned_sv(ki.key)); + d.append("k", to_string_view(ki.key)); d.append( "t", std::chrono::duration_cast( @@ -486,11 +487,11 @@ ustring Keys::key_supplement(const std::vector& sids) const { crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); - ustring_view nonce{h1.data(), h1.size()}; + std::span nonce{h1.data(), h1.size()}; oxenc::bt_dict_producer d{}; - d.append("#", from_unsigned_sv(nonce)); + d.append("#", to_string_view(nonce)); { auto list = d.append_list("+"); @@ -500,7 +501,7 @@ ustring Keys::key_supplement(const std::vector& sids) const { size_t member_count = 0; std::vector> member_xpk_raw; - std::vector member_xpks; + std::vector> member_xpks; member_xpk_raw.reserve(sids.size()); member_xpks.reserve(sids.size()); for (const auto& sid : sids) { @@ -512,10 +513,10 @@ ustring Keys::key_supplement(const std::vector& sids) const { supp_keys, member_xpks, nonce, - to_sv(group_xsk), - to_sv(group_xpk), + to_span(group_xsk), + to_span(group_xpk), enc_key_member_hash_key, - [&](ustring_view encrypted) { + [&](std::span encrypted) { list.append(encrypted); member_count++; }, @@ -531,9 +532,10 @@ ustring Keys::key_supplement(const std::vector& sids) const { // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). - d.append_signature("~", [this](ustring_view to_sign) { return sign(to_sign); }); + d.append_signature( + "~", [this](std::span to_sign) { return sign(to_sign); }); - return ustring{to_unsigned_sv(d.view())}; + return to_vector(d.view()); } // Blinding factor for subaccounts: H(sessionid || groupid) mod L, where H is 64-byte blake2b, using @@ -572,7 +574,8 @@ namespace { } // namespace -ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, bool del) const { +std::vector Keys::swarm_make_subaccount( + std::string_view session_id, bool write, bool del) const { if (!admin()) throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; @@ -600,7 +603,7 @@ ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, boo auto k = subaccount_blind_factor(X); // T = |S| - auto T = xed25519::pubkey(ustring_view{X.data(), X.size()}); + auto T = xed25519::pubkey(std::span{X.data(), X.size()}); // kT is the user's Ed25519 blinded pubkey: std::array kT; @@ -608,7 +611,7 @@ ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, boo if (0 != crypto_scalarmult_ed25519_noclamp(kT.data(), k.data(), T.data())) throw std::runtime_error{"scalarmult failed: perhaps an invalid session id?"}; - ustring out; + std::vector out; out.resize(4 + 32 + 64); out[0] = 0x03; // network prefix out[1] = subacc_flags(write, del); // permission flags @@ -626,7 +629,8 @@ ustring Keys::swarm_make_subaccount(std::string_view session_id, bool write, boo return out; } -ustring Keys::swarm_subaccount_token(std::string_view session_id, bool write, bool del) const { +std::vector Keys::swarm_subaccount_token( + std::string_view session_id, bool write, bool del) const { if (!admin()) throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; @@ -636,9 +640,9 @@ ustring Keys::swarm_subaccount_token(std::string_view session_id, bool write, bo auto k = subaccount_blind_factor(X); // T = |S| - auto T = xed25519::pubkey(ustring_view{X.data(), X.size()}); + auto T = xed25519::pubkey(std::span{X.data(), X.size()}); - ustring out; + std::vector out; out.resize(4 + 32); out[0] = 0x03; // network prefix out[1] = subacc_flags(write, del); // permission flags @@ -650,7 +654,9 @@ ustring Keys::swarm_subaccount_token(std::string_view session_id, bool write, bo } Keys::swarm_auth Keys::swarm_subaccount_sign( - ustring_view msg, ustring_view sign_val, bool binary) const { + std::span msg, + std::span sign_val, + bool binary) const { if (sign_val.size() != 100) throw std::logic_error{"Invalid signing value: size is wrong"}; @@ -662,7 +668,7 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // (see above for variable/crypto notation) - ustring_view k = sign_val.substr(4, 32); + std::span k = sign_val.subspan(4, 32); // our token is the first 4 bytes of `sign_val` (flags, etc.), followed by kT which we have to // compute: @@ -678,10 +684,10 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( throw std::runtime_error{"scalarmult failed: perhaps an invalid session id or seed?"}; // token is now set: flags || kT - ustring_view kT{to_unsigned(token.data() + 4), 32}; + std::span kT{to_unsigned(token.data() + 4), 32}; // sub_sig is just the admin's signature, sitting at the end of sign_val (after 4f || k): - sub_sig = from_unsigned_sv(sign_val.substr(36)); + sub_sig = to_string_view(sign_val.subspan(36)); // Our signing private scalar is kt, where t = ±s according to whether we had to negate S to // make T @@ -769,12 +775,13 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( return result; } -bool Keys::swarm_verify_subaccount(ustring_view sign_val, bool write, bool del) const { +bool Keys::swarm_verify_subaccount( + std::span sign_val, bool write, bool del) const { if (!_sign_pk) return false; return swarm_verify_subaccount( "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()), - ustring_view{user_ed25519_sk.data(), user_ed25519_sk.size()}, + std::span{user_ed25519_sk.data(), user_ed25519_sk.size()}, sign_val, write, del); @@ -782,8 +789,8 @@ bool Keys::swarm_verify_subaccount(ustring_view sign_val, bool write, bool del) bool Keys::swarm_verify_subaccount( std::string group_id, - ustring_view user_ed_sk, - ustring_view sign_val, + std::span user_ed_sk, + std::span sign_val, bool write, bool del) { auto group_pk = session_id_pk(group_id, "03"); @@ -791,7 +798,7 @@ bool Keys::swarm_verify_subaccount( if (sign_val.size() != 100) return false; - ustring_view prefix = sign_val.substr(0, 4); + std::span prefix = sign_val.subspan(0, 4); if (prefix[0] != 0x03 && !(prefix[1] & SUBACC_FLAG_ANY_PREFIX)) return false; // require either 03 prefix match, or the "any prefix" flag @@ -804,8 +811,8 @@ bool Keys::swarm_verify_subaccount( if (del && !(prefix[1] & SUBACC_FLAG_DEL)) return false; // we require delete, but it isn't set - ustring_view k = sign_val.substr(4, 32); - ustring_view sig = sign_val.substr(36); + std::span k = sign_val.subspan(4, 32); + std::span sig = sign_val.subspan(36); // T = |S|, i.e. we have to clear the sign bit from our pubkey std::array T; @@ -827,10 +834,10 @@ bool Keys::swarm_verify_subaccount( sig.data(), to_verify.data(), to_verify.size(), group_pk.data()); } -std::optional Keys::pending_config() const { +std::optional> Keys::pending_config() const { if (pending_key_config_.empty()) return std::nullopt; - return ustring_view{pending_key_config_.data(), pending_key_config_.size()}; + return std::span{pending_key_config_.data(), pending_key_config_.size()}; } void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { @@ -875,7 +882,10 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { // Returns true (after writing to `out`) if decryption succeeds, false if it fails. namespace { bool try_decrypting( - unsigned char* out, ustring_view encrypted, ustring_view nonce, ustring_view key) { + unsigned char* out, + std::span encrypted, + std::span nonce, + std::span key) { assert(encrypted.size() >= crypto_aead_xchacha20poly1305_ietf_ABYTES); assert(nonce.size() == crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); assert(key.size() == crypto_aead_xchacha20poly1305_ietf_KEYBYTES); @@ -893,22 +903,23 @@ namespace { } bool try_decrypting( unsigned char* out, - ustring_view encrypted, - ustring_view nonce, + std::span encrypted, + std::span nonce, const std::array& key) { - return try_decrypting(out, encrypted, nonce, ustring_view{key.data(), key.size()}); + return try_decrypting( + out, encrypted, nonce, std::span{key.data(), key.size()}); } } // namespace bool Keys::load_key_message( std::string_view hash, - ustring_view data, + std::span data, int64_t timestamp_ms, Info& info, Members& members) { - oxenc::bt_dict_consumer d{from_unsigned_sv(data)}; + oxenc::bt_dict_consumer d{data}; if (!_sign_pk || !verifier_) throw std::logic_error{"Group pubkey is not set; unable to load config message"}; @@ -917,7 +928,7 @@ bool Keys::load_key_message( if (!d.skip_until("#")) throw config_value_error{"Key message has no nonce"}; - auto nonce = to_unsigned_sv(d.consume_string_view()); + auto nonce = to_span(d.consume_string_view()); sodium_vector new_keys; std::optional max_gen; // If set then associate the message with this generation @@ -937,10 +948,10 @@ bool Keys::load_key_message( int member_key_pos = -1; - auto next_ciphertext = [&]() -> std::optional { + auto next_ciphertext = [&]() -> std::optional> { while (!supp.is_finished()) { member_key_pos++; - auto encrypted = to_unsigned_sv(supp.consume_string_view()); + auto encrypted = to_span(supp.consume_string_view()); // Expect an encrypted message like this, which has a minimum valid size (if both g // and t are 0 for some reason) of: // d -- 1 @@ -967,14 +978,14 @@ bool Keys::load_key_message( if (auto plaintext = decrypt_for_multiple( next_ciphertext, nonce, - to_sv(member_xsk), - to_sv(member_xpk), - to_sv(group_xpk), + to_span(member_xsk), + to_span(member_xpk), + to_span(group_xpk), enc_key_member_hash_key)) { // Decryption success, we found our key list! - oxenc::bt_list_consumer key_infos{from_unsigned_sv(*plaintext)}; + oxenc::bt_list_consumer key_infos{to_string_view(*plaintext)}; while (!key_infos.is_finished()) { auto& new_key = new_keys.emplace_back(); auto keyinf = key_infos.consume_dict_consumer(); @@ -1021,7 +1032,7 @@ bool Keys::load_key_message( throw config_value_error{ "Non-supplemental key message is missing required admin key (K)"}; - auto admin_key = to_unsigned_sv(d.consume_string_view()); + auto admin_key = to_span(d.consume_string_view()); if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw config_value_error{"Key message has invalid admin key length"}; @@ -1042,10 +1053,10 @@ bool Keys::load_key_message( auto key_list = d.consume_list_consumer(); int member_key_pos = -1; - auto next_ciphertext = [&]() -> std::optional { + auto next_ciphertext = [&]() -> std::optional> { while (!key_list.is_finished()) { member_key_pos++; - auto member_key = to_unsigned_sv(key_list.consume_string_view()); + auto member_key = to_span(key_list.consume_string_view()); if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw config_value_error{ "Key message has invalid member key length at index " + @@ -1062,9 +1073,9 @@ bool Keys::load_key_message( if (auto plaintext = decrypt_for_multiple( next_ciphertext, nonce, - to_sv(member_xsk), - to_sv(member_xpk), - to_sv(group_xpk), + to_span(member_xsk), + to_span(member_xpk), + to_span(group_xpk), enc_key_member_hash_key)) { // Decryption success, we found our key! assert(plaintext->size() == 32); @@ -1179,19 +1190,20 @@ bool Keys::needs_rekey() const { return last_it->generation == second_it->generation; } -std::optional Keys::pending_key() const { +std::optional> Keys::pending_key() const { if (!pending_key_config_.empty()) - return ustring_view{pending_key_.data(), pending_key_.size()}; + return std::span{pending_key_.data(), pending_key_.size()}; return std::nullopt; } static constexpr size_t ENCRYPT_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; -ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padding) const { +std::vector Keys::encrypt_message( + std::span plaintext, bool compress, size_t padding) const { if (plaintext.size() > MAX_PLAINTEXT_MESSAGE_SIZE) throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; - ustring _compressed; + std::vector _compressed; if (compress) { _compressed = zstd_compress(plaintext); if (_compressed.size() < plaintext.size()) @@ -1213,10 +1225,11 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padd // components to this validation: first the regular signature validation of the "s" signature we // add below, but then also validation that this Ed25519 converts to the Session ID of the // claimed sender of the message inside the encoded message data. - dict.append("a", std::string_view{from_unsigned(user_ed25519_sk.data()) + 32, 32}); + dict.append( + "a", std::string_view{reinterpret_cast(user_ed25519_sk.data()) + 32, 32}); if (!compress) - dict.append("d", from_unsigned_sv(plaintext)); + dict.append("d", to_string_view(plaintext)); // We sign `plaintext || group_ed25519_pubkey` rather than just `plaintext` so that if this // encrypted data will not validate if cross-posted to any other group. We don't actually @@ -1230,10 +1243,10 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padd std::array signature; crypto_sign_ed25519_detached( signature.data(), nullptr, to_sign.data(), to_sign.size(), user_ed25519_sk.data()); - dict.append("s", from_unsigned_sv(signature)); + dict.append("s", to_string_view(signature)); if (compress) - dict.append("z", from_unsigned_sv(plaintext)); + dict.append("z", to_string_view(plaintext)); auto encoded = std::move(dict).str(); @@ -1247,10 +1260,11 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padd encoded.resize(encoded.size() + to_append); } - ustring ciphertext; + std::vector ciphertext; ciphertext.resize(ENCRYPT_OVERHEAD + encoded.size()); randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - ustring_view nonce{ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + std::span nonce{ + ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, nullptr, @@ -1266,14 +1280,15 @@ ustring Keys::encrypt_message(ustring_view plaintext, bool compress, size_t padd return ciphertext; } -std::pair Keys::decrypt_message(ustring_view ciphertext) const { +std::pair> Keys::decrypt_message( + std::span ciphertext) const { if (ciphertext.size() < ENCRYPT_OVERHEAD) throw std::runtime_error{"ciphertext is too small to be encrypted data"}; - ustring plain; + std::vector plain; - auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - ciphertext.remove_prefix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext = ciphertext.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); plain.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); // @@ -1298,8 +1313,10 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c // // Removing any null padding bytes from the end // - if (auto pos = plain.find_last_not_of((unsigned char)0); pos != std::string::npos) - plain.resize(pos + 1); + if (auto it = + std::find_if(plain.rbegin(), plain.rend(), [](unsigned char c) { return c != 0; }); + it != plain.rend()) + plain.resize(plain.size() - std::distance(plain.rbegin(), it)); // // Now what we have less should be a bt_dict @@ -1307,7 +1324,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c if (plain.empty() || plain.front() != 'd' || plain.back() != 'e') throw std::runtime_error{"decrypted data is not a bencoded dict"}; - oxenc::bt_dict_consumer dict{from_unsigned_sv(plain)}; + oxenc::bt_dict_consumer dict{to_string_view(plain)}; if (!dict.skip_until("")) throw std::runtime_error{"group message version tag (\"\") is missing"}; @@ -1318,7 +1335,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c if (!dict.skip_until("a")) throw std::runtime_error{"missing message author pubkey"}; - auto ed_pk = to_unsigned_sv(dict.consume_string_view()); + auto ed_pk = to_span(dict.consume_string_view()); if (ed_pk.size() != 32) throw std::runtime_error{ "message author pubkey size (" + std::to_string(ed_pk.size()) + ") is invalid"}; @@ -1328,22 +1345,22 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c throw std::runtime_error{ "author ed25519 pubkey is invalid (unable to convert it to a session id)"}; - std::pair result; + std::pair> result; auto& [session_id, data] = result; session_id.reserve(66); session_id += "05"; oxenc::to_hex(x_pk.begin(), x_pk.end(), std::back_inserter(session_id)); - ustring_view raw_data; + std::span raw_data; if (dict.skip_until("d")) { - raw_data = to_unsigned_sv(dict.consume_string_view()); + raw_data = to_span(dict.consume_string_view()); if (raw_data.empty()) throw std::runtime_error{"uncompressed message data (\"d\") cannot be empty"}; } if (!dict.skip_until("s")) throw std::runtime_error{"message signature is missing"}; - auto ed_sig = to_unsigned_sv(dict.consume_string_view()); + auto ed_sig = to_span(dict.consume_string_view()); if (ed_sig.size() != 64) throw std::runtime_error{ "message signature size (" + std::to_string(ed_sig.size()) + ") is invalid"}; @@ -1354,7 +1371,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c throw std::runtime_error{ "message signature cannot contain both compressed (z) and uncompressed (d) " "data"}; - raw_data = to_unsigned_sv(dict.consume_string_view()); + raw_data = to_span(dict.consume_string_view()); if (raw_data.empty()) throw std::runtime_error{"compressed message data (\"z\") cannot be empty"}; @@ -1378,7 +1395,7 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c } else throw std::runtime_error{"message decompression failed"}; } else - data = raw_data; + data.assign(raw_data.begin(), raw_data.end()); return result; } @@ -1447,12 +1464,12 @@ LIBSESSION_C_API int groups_keys_init( 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; + std::span user_sk{user_ed25519_secretkey, 64}; + std::span group_pk{group_ed25519_pubkey, 32}; + std::optional> group_sk; if (group_ed25519_secretkey) group_sk.emplace(group_ed25519_secretkey, 64); - std::optional dumped; + std::optional> dumped; if (dump && dumplen) dumped.emplace(dump, dumplen); @@ -1510,7 +1527,7 @@ LIBSESSION_C_API bool groups_keys_load_admin_key( conf, [&] { unbox(conf).load_admin_key( - ustring_view{secret, 32}, + std::span{secret, 32}, *unbox(info), *unbox(members)); return true; @@ -1526,7 +1543,7 @@ LIBSESSION_C_API bool groups_keys_rekey( size_t* outlen) { assert(info && members); auto& keys = unbox(conf); - ustring_view to_push; + std::span to_push; return wrap_exceptions( conf, @@ -1566,7 +1583,7 @@ LIBSESSION_C_API bool groups_keys_load_message( [&] { unbox(conf).load_key_message( msg_hash, - ustring_view{data, datalen}, + std::span{data, datalen}, timestamp_ms, *unbox(info), *unbox(members)); @@ -1604,9 +1621,10 @@ LIBSESSION_C_API void groups_keys_encrypt_message( size_t* ciphertext_len) { assert(plaintext_in && ciphertext_out && ciphertext_len); - ustring ciphertext; + std::vector ciphertext; try { - ciphertext = unbox(conf).encrypt_message(ustring_view{plaintext_in, plaintext_len}); + ciphertext = unbox(conf).encrypt_message( + std::span{plaintext_in, plaintext_len}); *ciphertext_out = static_cast(std::malloc(ciphertext.size())); std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); *ciphertext_len = ciphertext.size(); @@ -1628,8 +1646,8 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( return wrap_exceptions( conf, [&] { - auto [sid, plaintext] = - unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); + auto [sid, plaintext] = unbox(conf).decrypt_message( + std::span{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()); @@ -1699,8 +1717,8 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( try { return groups::Keys::swarm_verify_subaccount( group_id, - ustring_view{session_ed25519_secretkey, 64}, - ustring_view{signing_value, 100}, + std::span{session_ed25519_secretkey, 64}, + std::span{signing_value, 100}, write, del); } catch (...) { @@ -1715,8 +1733,8 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( try { return groups::Keys::swarm_verify_subaccount( group_id, - ustring_view{session_ed25519_secretkey, 64}, - ustring_view{signing_value, 100}); + std::span{session_ed25519_secretkey, 64}, + std::span{signing_value, 100}); } catch (...) { return false; } @@ -1736,7 +1754,8 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); + std::span{msg, msg_len}, + std::span{signing_value, 100}); assert(auth.subaccount.size() == 48); assert(auth.subaccount_sig.size() == 88); assert(auth.signature.size() == 88); @@ -1765,7 +1784,9 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - ustring_view{msg, msg_len}, ustring_view{signing_value, 100}, true); + std::span{msg, msg_len}, + std::span{signing_value, 100}, + true); assert(auth.subaccount.size() == 36); assert(auth.subaccount_sig.size() == 64); assert(auth.signature.size() == 64); diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 0aadcbfc..ca515e66 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -8,9 +8,9 @@ namespace session::config::groups { Members::Members( - ustring_view ed25519_pubkey, - std::optional ed25519_secretkey, - std::optional dumped) { + std::span ed25519_pubkey, + std::optional> ed25519_secretkey, + std::optional> dumped) { init(dumped, ed25519_pubkey, ed25519_secretkey); } @@ -87,7 +87,7 @@ void member::load(const dict& info_dict) { name = maybe_string(info_dict, "n").value_or(""); auto url = maybe_string(info_dict, "p"); - auto key = maybe_ustring(info_dict, "q"); + auto key = maybe_vector(info_dict, "q"); if (url && key && !url->empty() && key->size() == 32) { profile_picture.url = std::move(*url); profile_picture.key = std::move(*key); @@ -185,7 +185,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { assert(std::strlen(m.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); if (std::strlen(m.profile_pic.url)) { profile_picture.url = m.profile_pic.url; - profile_picture.key = {m.profile_pic.key, 32}; + profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32); } admin = m.admin; invite_status = diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 9ad61eba..a81446ef 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -39,8 +39,8 @@ void check_encoded_pubkey(std::string_view pk) { throw std::invalid_argument{"Invalid encoded pubkey: expected hex, base32z or base64"}; } -ustring decode_pubkey(std::string_view pk) { - session::ustring pubkey; +std::vector decode_pubkey(std::string_view pk) { + std::vector pubkey; pubkey.reserve(32); if (pk.size() == 64 && oxenc::is_hex(pk)) oxenc::from_hex(pk.begin(), pk.end(), std::back_inserter(pubkey)); @@ -93,10 +93,13 @@ std::optional maybe_sv(const session::config::dict& d, const c return std::nullopt; } -std::optional maybe_ustring(const session::config::dict& d, const char* key) { - std::optional result; +std::optional> maybe_vector( + const session::config::dict& d, const char* key) { + std::optional> result; if (auto* s = maybe_scalar(d, key)) - result.emplace(reinterpret_cast(s->data()), s->size()); + result.emplace( + reinterpret_cast(s->data()), + reinterpret_cast(s->data()) + s->size()); return result; } @@ -183,13 +186,14 @@ namespace { using zstd_decomp_ptr = std::unique_ptr; } // namespace -ustring zstd_compress(ustring_view data, int level, ustring_view prefix) { - ustring compressed; +std::vector zstd_compress( + std::span data, int level, std::span prefix) { + std::vector compressed; if (prefix.empty()) compressed.resize(ZSTD_compressBound(data.size())); else { compressed.resize(prefix.size() + ZSTD_compressBound(data.size())); - compressed.replace(0, prefix.size(), prefix); + std::copy(prefix.begin(), prefix.end(), compressed.begin()); } auto size = ZSTD_compress( compressed.data() + prefix.size(), @@ -204,7 +208,8 @@ ustring zstd_compress(ustring_view data, int level, ustring_view prefix) { return compressed; } -std::optional zstd_decompress(ustring_view data, size_t max_size) { +std::optional> zstd_decompress( + std::span data, size_t max_size) { zstd_decomp_ptr z_decompressor{ZSTD_createDStream()}; auto* zds = z_decompressor.get(); @@ -213,7 +218,7 @@ std::optional zstd_decompress(ustring_view data, size_t max_size) { std::array out_buf; ZSTD_outBuffer output{/*.dst=*/out_buf.data(), /*.size=*/out_buf.size(), /*.pos=*/0}; - ustring decompressed; + std::vector decompressed; size_t ret; do { @@ -224,7 +229,7 @@ std::optional zstd_decompress(ustring_view data, size_t max_size) { if (max_size > 0 && decompressed.size() + output.pos > max_size) return std::nullopt; - decompressed.append(out_buf.data(), output.pos); + decompressed.insert(decompressed.end(), out_buf.begin(), out_buf.begin() + output.pos); } while (ret > 0 || input.pos < input.size); return decompressed; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index c338cbc3..74cc31fd 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -45,8 +45,8 @@ template size_t dumplen, char* error) { assert(ed25519_secretkey_bytes); - ustring_view ed25519_secretkey{ed25519_secretkey_bytes, 64}; - std::optional dump; + std::span 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); @@ -63,11 +63,11 @@ template assert(ed25519_pubkey_bytes); - ustring_view ed25519_pubkey{ed25519_pubkey_bytes, 32}; - std::optional ed25519_secretkey; + std::span ed25519_pubkey{ed25519_pubkey_bytes, 32}; + std::optional> ed25519_secretkey; if (ed25519_secretkey_bytes) ed25519_secretkey.emplace(ed25519_secretkey_bytes, 64); - std::optional dump; + std::optional> dump; if (dump_bytes && dumplen) dump.emplace(dump_bytes, dumplen); @@ -136,7 +136,7 @@ void check_encoded_pubkey(std::string_view pk); // Takes a 32-byte pubkey value encoded as hex, base32z, or base64 and returns the decoded 32 bytes. // Throws if invalid. -ustring decode_pubkey(std::string_view pk); +std::vector decode_pubkey(std::string_view pk); // Modifies a string to be (ascii) lowercase. void make_lc(std::string& s); @@ -150,8 +150,10 @@ std::optional maybe_int(const session::config::dict& d, const char* key // Digs into a config `dict` to get out a string; nullopt if not there (or not string) std::optional maybe_string(const session::config::dict& d, const char* key); -// Digs into a config `dict` to get out a ustring; nullopt if not there (or not string) -std::optional maybe_ustring(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not +// string) +std::optional> maybe_vector( + const session::config::dict& d, const char* key); // Digs into a config `dict` to get out a string view; nullopt if not there (or not string). The // string view is only valid as long as the dict stays unchanged. @@ -203,10 +205,14 @@ void load_unknowns( /// ZSTD-compresses a value. `prefix` can be prepended on the returned value, if needed. Throws on /// serious error. -ustring zstd_compress(ustring_view data, int level = 1, ustring_view prefix = {}); +std::vector zstd_compress( + std::span data, + int level = 1, + std::span prefix = {}); /// ZSTD-decompresses a value. Returns nullopt if decompression fails. If max_size is non-zero /// then this returns nullopt if the decompressed size would exceed that limit. -std::optional zstd_decompress(ustring_view data, size_t max_size = 0); +std::optional> zstd_decompress( + std::span data, size_t max_size = 0); } // namespace session::config diff --git a/src/config/protos.cpp b/src/config/protos.cpp index ef1bcbdc..44009257 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -32,8 +32,11 @@ namespace { } // namespace -ustring wrap_config( - ustring_view ed25519_sk, ustring_view data, int64_t seqno, config::Namespace t) { +std::vector wrap_config( + std::span ed25519_sk, + std::span data, + int64_t seqno, + config::Namespace t) { std::array tmp_sk; if (ed25519_sk.size() == 32) { std::array ignore_pk; @@ -56,7 +59,7 @@ ustring wrap_config( auto& shconf = *config.mutable_sharedconfigmessage(); shconf.set_kind(encode_namespace(t)); shconf.set_seqno(seqno); - *shconf.mutable_data() = from_unsigned_sv(data); + *shconf.mutable_data() = to_string_view(data); // Then we serialize that, pad it, and encrypt it. Copying this relevant comment from the // Session codebase (the comment itself git blames to Signal): @@ -87,7 +90,7 @@ ustring wrap_config( // derived from our private key, but old Session clients expect this. // NOTE: This is dumb. auto enc_shared_conf = encrypt_for_recipient_deterministic( - ed25519_sk, {my_xpk.data(), my_xpk.size()}, to_unsigned_sv(shared_conf)); + ed25519_sk, {my_xpk.data(), my_xpk.size()}, to_span(shared_conf)); // This is the point in session client code where this value got base64-encoded, passed to // another function, which then base64-decoded that value to put into the envelope. We're going @@ -97,7 +100,7 @@ ustring wrap_config( // Now we just keep on trucking with more protobuf: auto envelope = SessionProtos::Envelope(); - *envelope.mutable_content() = from_unsigned_sv(enc_shared_conf); + *envelope.mutable_content() = to_string_view(enc_shared_conf); envelope.set_timestamp(1); // Old session clients with their own unwrapping require this > 0 envelope.set_type(SessionProtos::Envelope_Type::Envelope_Type_SESSION_MESSAGE); @@ -117,10 +120,13 @@ ustring wrap_config( msg.set_type(WebSocketProtos::WebSocketMessage_Type_REQUEST); *msg.mutable_request() = webreq; - return ustring{to_unsigned_sv(msg.SerializeAsString())}; + return to_vector(msg.SerializeAsString()); } -ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namespace ns) { +std::vector unwrap_config( + std::span ed25519_sk, + std::span data, + config::Namespace ns) { // Hurray, we get to undo everything from the above! std::array tmp_sk; @@ -131,7 +137,7 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp } else if (ed25519_sk.size() != 64) throw std::invalid_argument{ "Error: ed25519_sk is not the expected 64-byte Ed25519 secret key"}; - auto ed25519_pk = ed25519_sk.substr(32); + auto ed25519_pk = ed25519_sk.subspan(32); WebSocketProtos::WebSocketMessage req{}; @@ -145,7 +151,7 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp if (!envelope.ParseFromString(req.request().body())) throw std::runtime_error{"Failed to parse Envelope"}; - auto [content, sender] = decrypt_incoming(ed25519_sk, to_unsigned_sv(envelope.content())); + auto [content, sender] = decrypt_incoming(ed25519_sk, to_span(envelope.content())); if (sender != ed25519_pk) throw std::runtime_error{"Incoming config data was not from us; ignoring"}; @@ -155,9 +161,10 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp if (!(content.back() == 0x00 || content.back() == 0x80)) throw std::runtime_error{"Incoming config data doesn't have required padding"}; - if (auto pos = content.find_last_not_of((unsigned char)0); - pos != std::string::npos && content[pos] == 0x80) - content.resize(pos); + if (auto it = std::find_if( + content.rbegin(), content.rend(), [](unsigned char c) { return c != 0; }); + it != content.rend() && *it == 0x80) + content.resize(content.size() - std::distance(content.rbegin(), it) - 1); else throw std::runtime_error{"Incoming config data has invalid padding"}; @@ -172,7 +179,7 @@ ustring unwrap_config(ustring_view ed25519_sk, ustring_view data, config::Namesp throw std::runtime_error{"SharedConfig has wrong kind for config namespace"}; // if ParseFromString fails, we have a raw (not protobuf encoded) message - return ustring{to_unsigned_sv(shconf.data())}; + return to_vector(shconf.data()); } } // namespace session::config::protos diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index ccb7bab3..b7485408 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -20,7 +20,6 @@ #include "session/util.hpp" using namespace std::literals; -using session::ustring_view; LIBSESSION_C_API const size_t GROUP_NAME_MAX_LENGTH = session::config::legacy_group_info::NAME_MAX_LENGTH; @@ -60,7 +59,7 @@ legacy_group_info::legacy_group_info(std::string sid) : session_id{std::move(sid } community_info::community_info(const ugroups_community_info& c) : - community_info{c.base_url, c.room, ustring_view{c.pubkey, 32}} { + community_info{c.base_url, c.room, std::span{c.pubkey, 32}} { base_from(*this, c); } @@ -82,8 +81,8 @@ legacy_group_info::legacy_group_info(const ugroups_legacy_group_info& c, impl_t) assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up base_from(*this, c); if (c.have_enc_keys) { - enc_pubkey.assign(c.enc_pubkey, 32); - enc_seckey.assign(c.enc_seckey, 32); + enc_pubkey.assign(c.enc_pubkey, c.enc_pubkey + 32); + enc_seckey.assign(c.enc_seckey, c.enc_seckey + 32); } } @@ -150,8 +149,8 @@ void legacy_group_info::load(const dict& info_dict) { else name.clear(); - auto enc_pub = maybe_ustring(info_dict, "k"); - auto enc_sec = maybe_ustring(info_dict, "K"); + auto enc_pub = maybe_vector(info_dict, "k"); + auto enc_sec = maybe_vector(info_dict, "K"); if (enc_pub && enc_sec && enc_pub->size() == 32 && enc_sec->size() == 32) { enc_pubkey = std::move(*enc_pub); enc_seckey = std::move(*enc_sec); @@ -210,9 +209,9 @@ group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up if (c.have_secretkey) - secretkey.assign(c.secretkey, 64); + secretkey.assign(c.secretkey, c.secretkey + 64); if (c.have_auth_data) - auth_data.assign(c.auth_data, sizeof(c.auth_data)); + auth_data.assign(c.auth_data, c.auth_data + sizeof(c.auth_data)); } void group_info::into(ugroups_group_info& c) const { @@ -235,7 +234,7 @@ void group_info::load(const dict& info_dict) { else name.clear(); - if (auto seed = maybe_ustring(info_dict, "K"); seed && seed->size() == 32) { + if (auto seed = maybe_vector(info_dict, "K"); seed && seed->size() == 32) { std::array pk; pk[0] = 0x03; secretkey.resize(64); @@ -243,7 +242,7 @@ void group_info::load(const dict& info_dict) { if (id != oxenc::to_hex(pk.begin(), pk.end())) secretkey.clear(); } - if (auto sig = maybe_ustring(info_dict, "s"); sig && sig->size() == 100) + if (auto sig = maybe_vector(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); removed_status = maybe_int(info_dict, "r").value_or(0); @@ -284,19 +283,21 @@ void community_info::load(const dict& info_dict) { set_room(std::move(*n)); } -UserGroups::UserGroups(ustring_view ed25519_secretkey, std::optional dumped) : +UserGroups::UserGroups( + std::span ed25519_secretkey, + std::optional> dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); } ConfigBase::DictFieldProxy UserGroups::community_field( - const community_info& og, ustring_view* get_pubkey) const { + const community_info& og, std::span* get_pubkey) const { auto record = data["o"][og.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) - *get_pubkey = - ustring_view{reinterpret_cast(pk.data()), pk.size()}; + *get_pubkey = std::span{ + reinterpret_cast(pk.data()), pk.size()}; } return record["R"][og.room_norm()]; } @@ -305,7 +306,7 @@ std::optional UserGroups::get_community( std::string_view base_url, std::string_view room) const { community_info og{base_url, room}; - ustring_view pubkey; + std::span pubkey; if (auto* info_dict = community_field(og, &pubkey).dict()) { og.load(*info_dict); if (!pubkey.empty()) @@ -321,7 +322,9 @@ std::optional UserGroups::get_community(std::string_view partial } community_info UserGroups::get_or_construct_community( - std::string_view base_url, std::string_view room, ustring_view pubkey) const { + std::string_view base_url, + std::string_view room, + std::span pubkey) const { community_info result{base_url, room, pubkey}; if (auto* info_dict = community_field(result).dict()) @@ -385,7 +388,7 @@ group_info UserGroups::get_or_construct_group(std::string_view pubkey_hex) const group_info UserGroups::create_group() const { std::array pk; - ustring sk; + std::vector sk; sk.resize(64); crypto_sign_keypair(pk.data(), sk.data()); std::string pk_hex; @@ -447,13 +450,13 @@ void UserGroups::set(const group_info& g) { if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: - ustring_view{g.secretkey.data() + 32, 32} == - ustring_view{ + std::span{g.secretkey.data() + 32, 32} == + std::span{ reinterpret_cast(pk_bytes.data() + 1), pk_bytes.size() - 1}) - info["K"] = ustring_view{g.secretkey.data(), 32}; + info["K"] = std::span{g.secretkey.data(), 32}; else { - info["K"] = ustring_view{}; + info["K"] = std::span{}; if (g.auth_data.size() == 100) info["s"] = g.auth_data; else @@ -668,7 +671,8 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( conf, [&] { unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + ->get_or_construct_community( + base_url, room, std::span{pubkey, 32}) .into(*comm); return true; }, diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index f01cded6..4547386d 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -10,11 +10,12 @@ #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) : +UserProfile::UserProfile( + std::span ed25519_secretkey, + std::optional> dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); } @@ -62,7 +63,9 @@ profile_pic UserProfile::get_profile_pic() const { if (auto* url = data["p"].string(); url && !url->empty()) pic.url = *url; if (auto* key = data["q"].string(); key && key->size() == 32) - pic.key = {reinterpret_cast(key->data()), 32}; + pic.key.assign( + reinterpret_cast(key->data()), + reinterpret_cast(key->data()) + 32); return pic; } @@ -77,7 +80,7 @@ LIBSESSION_C_API user_profile_pic user_profile_get_pic(const config_object* conf return p; } -void UserProfile::set_profile_pic(std::string_view url, ustring_view key) { +void UserProfile::set_profile_pic(std::string_view url, std::span key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); } @@ -87,7 +90,7 @@ void UserProfile::set_profile_pic(profile_pic pic) { LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic pic) { std::string_view url{pic.url}; - ustring_view key; + std::span key; if (!url.empty()) key = {pic.key, 32}; diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 1c8f11b3..a9daea6c 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -18,7 +18,7 @@ std::pair, std::array> curve255 return {curve_pk, curve_sk}; } -std::array to_curve25519_pubkey(ustring_view ed25519_pubkey) { +std::array to_curve25519_pubkey(std::span ed25519_pubkey) { if (ed25519_pubkey.size() != 32) { throw std::invalid_argument{"Invalid ed25519_pubkey: expected 32 bytes"}; } @@ -33,7 +33,7 @@ std::array to_curve25519_pubkey(ustring_view ed25519_pubkey) return curve_pk; } -std::array to_curve25519_seckey(ustring_view ed25519_seckey) { +std::array to_curve25519_seckey(std::span ed25519_seckey) { if (ed25519_seckey.size() != 64) { throw std::invalid_argument{"Invalid ed25519_seckey: expected 64 bytes"}; } @@ -67,7 +67,8 @@ LIBSESSION_C_API bool session_curve25519_key_pair( LIBSESSION_C_API bool session_to_curve25519_pubkey( const unsigned char* ed25519_pubkey, unsigned char* curve25519_pk_out) { try { - auto curve_pk = session::curve25519::to_curve25519_pubkey(ustring_view{ed25519_pubkey, 32}); + auto curve_pk = session::curve25519::to_curve25519_pubkey( + std::span{ed25519_pubkey, 32}); std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); return true; } catch (...) { @@ -78,7 +79,8 @@ LIBSESSION_C_API bool session_to_curve25519_pubkey( LIBSESSION_C_API bool session_to_curve25519_seckey( const unsigned char* ed25519_seckey, unsigned char* curve25519_sk_out) { try { - auto curve_sk = session::curve25519::to_curve25519_seckey(ustring_view{ed25519_seckey, 64}); + auto curve_sk = session::curve25519::to_curve25519_seckey( + std::span{ed25519_seckey, 64}); std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); return true; } catch (...) { diff --git a/src/ed25519.cpp b/src/ed25519.cpp index a4483a2f..df6e6e41 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -25,7 +25,7 @@ std::pair, std::array> ed25519_ } std::pair, std::array> ed25519_key_pair( - ustring_view ed25519_seed) { + std::span ed25519_seed) { if (ed25519_seed.size() != 32) { throw std::invalid_argument{"Invalid ed25519_seed: expected 32 bytes"}; } @@ -38,7 +38,7 @@ std::pair, std::array> ed25519_ return {ed_pk, ed_sk}; } -std::array seed_for_ed_privkey(ustring_view ed25519_privkey) { +std::array seed_for_ed_privkey(std::span ed25519_privkey) { std::array seed; if (ed25519_privkey.size() == 32 || ed25519_privkey.size() == 64) @@ -51,7 +51,8 @@ std::array seed_for_ed_privkey(ustring_view ed25519_privkey) return seed; } -ustring sign(ustring_view ed25519_privkey, ustring_view msg) { +std::vector sign( + std::span ed25519_privkey, std::span msg) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -67,10 +68,13 @@ ustring sign(ustring_view ed25519_privkey, ustring_view msg) { sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; - return {sig.data(), sig.size()}; + return {sig.data(), sig.data() + sig.size()}; } -bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { +bool verify( + std::span sig, + std::span pubkey, + std::span msg) { if (sig.size() != 64) throw std::invalid_argument{"Invalid sig: expected 64 bytes"}; if (pubkey.size() != 32) @@ -102,7 +106,8 @@ LIBSESSION_C_API bool session_ed25519_key_pair_seed( unsigned char* ed25519_pk_out, unsigned char* ed25519_sk_out) { try { - auto result = session::ed25519::ed25519_key_pair(ustring_view{ed25519_seed, 32}); + auto result = session::ed25519::ed25519_key_pair( + std::span{ed25519_seed, 32}); auto [ed_pk, ed_sk] = result; std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); @@ -115,7 +120,8 @@ LIBSESSION_C_API bool session_ed25519_key_pair_seed( LIBSESSION_C_API bool session_seed_for_ed_privkey( const unsigned char* ed25519_privkey, unsigned char* ed25519_seed_out) { try { - auto result = session::ed25519::seed_for_ed_privkey(ustring_view{ed25519_privkey, 64}); + auto result = session::ed25519::seed_for_ed_privkey( + std::span{ed25519_privkey, 64}); std::memcpy(ed25519_seed_out, result.data(), result.size()); return true; } catch (...) { @@ -130,7 +136,8 @@ LIBSESSION_C_API bool session_ed25519_sign( unsigned char* ed25519_sig_out) { try { auto result = session::ed25519::sign( - ustring_view{ed25519_privkey, 64}, ustring_view{msg, msg_len}); + std::span{ed25519_privkey, 64}, + std::span{msg, msg_len}); std::memcpy(ed25519_sig_out, result.data(), result.size()); return true; } catch (...) { @@ -144,5 +151,7 @@ LIBSESSION_C_API bool session_ed25519_verify( const unsigned char* msg, size_t msg_len) { return session::ed25519::verify( - ustring_view{sig, 64}, ustring_view{pubkey, 32}, ustring_view{msg, msg_len}); + std::span{sig, 64}, + std::span{pubkey, 32}, + std::span{msg, msg_len}); } diff --git a/src/hash.cpp b/src/hash.cpp index 0b87599c..ce49f405 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -7,7 +7,10 @@ namespace session::hash { -ustring hash(const size_t size, ustring_view msg, std::optional key) { +std::vector hash( + const size_t size, + std::span msg, + std::optional> key) { if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; @@ -15,7 +18,7 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; auto result_code = 0; - ustring result; + std::vector result; result.resize(size); result_code = crypto_generichash_blake2b( @@ -34,9 +37,6 @@ ustring hash(const size_t size, ustring_view msg, std::optional ke } // namespace session::hash -using session::ustring; -using session::ustring_view; - extern "C" { LIBSESSION_C_API bool session_hash( @@ -47,12 +47,12 @@ LIBSESSION_C_API bool session_hash( size_t key_len, unsigned char* hash_out) { try { - std::optional key; + std::optional> key; if (key_in && key_len) key = {key_in, key_len}; - ustring result = session::hash::hash(size, {msg_in, msg_len}, key); + std::vector result = session::hash::hash(size, {msg_in, msg_len}, key); std::memcpy(hash_out, result.data(), size); return true; } catch (...) { diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index d64e5158..28cddaa0 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -52,7 +52,10 @@ namespace detail { } void encrypt_multi_impl( - ustring& out, ustring_view msg, const unsigned char* key, const unsigned char* nonce) { + std::vector& out, + std::span msg, + const unsigned char* key, + const unsigned char* nonce) { // auto key = encrypt_multi_key(a, A, B, true, domain); @@ -64,8 +67,8 @@ namespace detail { } bool decrypt_multi_impl( - ustring& out, - ustring_view ciphertext, + std::vector& out, + std::span ciphertext, const unsigned char* key, const unsigned char* nonce) { @@ -86,7 +89,7 @@ namespace detail { } std::pair>, std::array> x_keys( - ustring_view ed25519_secret_key) { + std::span ed25519_secret_key) { if (ed25519_secret_key.size() != 64) throw std::invalid_argument{"Ed25519 secret key is not the expected 64 bytes"}; @@ -102,17 +105,17 @@ namespace detail { } // namespace detail -std::optional decrypt_for_multiple( - const std::vector& ciphertexts, - ustring_view nonce, - ustring_view privkey, - ustring_view pubkey, - ustring_view sender_pubkey, +std::optional> decrypt_for_multiple( + const std::vector>& ciphertexts, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { auto it = ciphertexts.begin(); return decrypt_for_multiple( - [&]() -> std::optional { + [&]() -> std::optional> { if (it == ciphertexts.end()) return std::nullopt; return *it++; @@ -124,13 +127,13 @@ std::optional decrypt_for_multiple( domain); } -ustring encrypt_for_multiple_simple( - const std::vector& messages, - const std::vector& recipients, - ustring_view privkey, - ustring_view pubkey, +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span privkey, + std::span pubkey, std::string_view domain, - std::optional nonce, + std::optional> nonce, int pad) { oxenc::bt_dict_producer d; @@ -149,56 +152,62 @@ ustring encrypt_for_multiple_simple( int msg_count = 0; encrypt_for_multiple( - messages, recipients, *nonce, privkey, pubkey, domain, [&](ustring_view encrypted) { + messages, + recipients, + *nonce, + privkey, + pubkey, + domain, + [&](std::span encrypted) { enc_list.append(encrypted); msg_count++; }); if (int pad_size = pad > 1 && !messages.empty() ? messages.front().size() : 0) { - ustring junk; + std::vector junk; junk.resize(pad_size); for (; msg_count % pad != 0; msg_count++) { randombytes_buf(junk.data(), pad_size); - enc_list.append(junk); + enc_list.append(to_string(junk)); } } } - return ustring{d.view()}; + return to_vector(d.span()); } -ustring encrypt_for_multiple_simple( - const std::vector& messages, - const std::vector& recipients, - ustring_view ed25519_secret_key, +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span ed25519_secret_key, std::string_view domain, - ustring_view nonce, + std::span nonce, int pad) { auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); return encrypt_for_multiple_simple( - messages, recipients, to_sv(x_privkey), to_sv(x_pubkey), domain, nonce, pad); + messages, recipients, to_span(x_privkey), to_span(x_pubkey), domain, nonce, pad); } -std::optional decrypt_for_multiple_simple( - ustring_view encoded, - ustring_view privkey, - ustring_view pubkey, - ustring_view sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { try { oxenc::bt_dict_consumer d{encoded}; - auto nonce = d.require("#"); + auto nonce = d.require>("#"); if (nonce.size() != 24) return std::nullopt; auto enc_list = d.require("e"); return decrypt_for_multiple( - [&]() -> std::optional { + [&]() -> std::optional> { if (enc_list.is_finished()) return std::nullopt; - return enc_list.consume(); + return enc_list.consume>(); }, nonce, privkey, @@ -210,22 +219,22 @@ std::optional decrypt_for_multiple_simple( } } -std::optional decrypt_for_multiple_simple( - ustring_view encoded, - ustring_view ed25519_secret_key, - ustring_view sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span ed25519_secret_key, + std::span sender_pubkey, std::string_view domain) { auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); return decrypt_for_multiple_simple( - encoded, to_sv(x_privkey), to_sv(x_pubkey), sender_pubkey, domain); + encoded, to_span(x_privkey), to_span(x_pubkey), sender_pubkey, domain); } -std::optional decrypt_for_multiple_simple_ed25519( - ustring_view encoded, - ustring_view ed25519_secret_key, - ustring_view sender_ed25519_pubkey, +std::optional> decrypt_for_multiple_simple_ed25519( + std::span encoded, + std::span ed25519_secret_key, + std::span sender_ed25519_pubkey, std::string_view domain) { std::array sender_pub; @@ -234,14 +243,14 @@ std::optional decrypt_for_multiple_simple_ed25519( if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_pub.data(), sender_ed25519_pubkey.data())) throw std::runtime_error{"Failed to convert Ed25519 key to X25519: invalid secret key"}; - return decrypt_for_multiple_simple(encoded, ed25519_secret_key, to_sv(sender_pub), domain); + return decrypt_for_multiple_simple(encoded, ed25519_secret_key, to_span(sender_pub), domain); } } // namespace session using namespace session; -static unsigned char* to_c_buffer(ustring_view x, size_t* out_len) { +static unsigned char* to_c_buffer(std::span x, size_t* out_len) { auto* ret = static_cast(malloc(x.size())); *out_len = x.size(); std::memcpy(ret, x.data(), x.size()); @@ -261,14 +270,14 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( const unsigned char* nonce, int pad) { - std::vector msgs, recips; + std::vector> msgs, recips; msgs.reserve(n_messages); recips.reserve(n_recipients); for (size_t i = 0; i < n_messages; i++) msgs.emplace_back(messages[i], message_lengths[i]); for (size_t i = 0; i < n_recipients; i++) recips.emplace_back(recipients[i], 32); - std::optional maybe_nonce; + std::optional> maybe_nonce; if (nonce) maybe_nonce.emplace(nonce, 24); @@ -276,8 +285,8 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( auto encoded = session::encrypt_for_multiple_simple( msgs, recips, - ustring_view{x25519_privkey, 32}, - ustring_view{x25519_pubkey, 32}, + std::span{x25519_privkey, 32}, + std::span{x25519_pubkey, 32}, domain, std::move(maybe_nonce), pad); @@ -300,7 +309,8 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( int pad) { try { - auto [priv, pub] = session::detail::x_keys(ustring_view{ed25519_secret_key, 64}); + auto [priv, pub] = + session::detail::x_keys(std::span{ed25519_secret_key, 64}); return session_encrypt_for_multiple_simple( out_len, messages, @@ -329,10 +339,10 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple( try { if (auto decrypted = session::decrypt_for_multiple_simple( - ustring_view{encoded, encoded_len}, - ustring_view{x25519_privkey, 32}, - ustring_view{x25519_pubkey, 32}, - ustring_view{sender_x25519_pubkey, 32}, + std::span{encoded, encoded_len}, + std::span{x25519_privkey, 32}, + std::span{x25519_pubkey, 32}, + std::span{sender_x25519_pubkey, 32}, domain)) { return to_c_buffer(*decrypted, out_len); } @@ -352,9 +362,9 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519_from try { if (auto decrypted = session::decrypt_for_multiple_simple( - ustring_view{encoded, encoded_len}, - ustring_view{ed25519_secret, 64}, - ustring_view{sender_x25519_pubkey, 32}, + std::span{encoded, encoded_len}, + std::span{ed25519_secret, 64}, + std::span{sender_x25519_pubkey, 32}, domain)) { return to_c_buffer(*decrypted, out_len); } @@ -374,9 +384,9 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519( try { if (auto decrypted = session::decrypt_for_multiple_simple_ed25519( - ustring_view{encoded, encoded_len}, - ustring_view{ed25519_secret, 64}, - ustring_view{sender_ed25519_pubkey, 32}, + std::span{encoded, encoded_len}, + std::span{ed25519_secret, 64}, + std::span{sender_ed25519_pubkey, 32}, domain)) { return to_c_buffer(*decrypted, out_len); } diff --git a/src/network.cpp b/src/network.cpp index b25b65ca..91f1c9bc 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,8 +1,8 @@ #include "session/network.hpp" -#include #include #include +#include #include #include #include @@ -12,9 +12,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -256,7 +256,7 @@ namespace { node.swarm_id); } - session::onionreq::x25519_pubkey compute_xpk(ustring_view ed25519_pk) { + session::onionreq::x25519_pubkey compute_xpk(std::span ed25519_pk) { std::array xpk; if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk.data())) throw std::runtime_error{ @@ -855,7 +855,7 @@ void Network::establish_connection( {target, std::make_shared(0), nullptr, nullptr}, "Network is suspended."); auto conn_key_pair = ed25519::ed25519_key_pair(); - auto creds = quic::GNUTLSCreds::make_from_ed_seckey(from_unsigned_sv(conn_key_pair.second)); + auto creds = quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(conn_key_pair.second)); auto cb_called = std::make_shared(); auto cb = std::make_shared)>>( std::move(callback)); @@ -1239,7 +1239,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id }; auto info = request_info::make( target_node, - str_to_vec(payload.dump()), + to_vector(payload.dump()), std::nullopt, quic::DEFAULT_TIMEOUT, std::nullopt, @@ -1782,7 +1782,7 @@ void Network::send_request( std::span payload{}; if (info.body) - payload = vec_to_span(*info.body); + payload = to_span(*info.body); // Calculate the remaining timeout std::chrono::milliseconds timeout = info.request_timeout; @@ -2151,11 +2151,11 @@ void Network::get_client_version( } // Generate the auth signature - auto blinded_keys = blind_version_key_pair(to_unsigned_sv(seckey.view())); + auto blinded_keys = blind_version_key_pair(to_span(seckey.view())); auto timestamp = std::chrono::duration_cast( (std::chrono::system_clock::now()).time_since_epoch()) .count(); - auto signature = blind_version_sign(to_unsigned_sv(seckey.view()), platform, timestamp); + auto signature = blind_version_sign(to_span(seckey.view()), platform, timestamp); auto pubkey = x25519_pubkey::from_hex(file_server_pubkey); std::string blinded_pk_hex; blinded_pk_hex.reserve(66); @@ -2168,7 +2168,7 @@ void Network::get_client_version( auto headers = std::vector>{}; headers.emplace_back("X-FS-Pubkey", blinded_pk_hex); headers.emplace_back("X-FS-Timestamp", "{}"_format(timestamp)); - headers.emplace_back("X-FS-Signature", oxenc::to_base64(signature)); + headers.emplace_back("X-FS-Signature", oxenc::to_base64(signature.begin(), signature.end())); send_onion_request( ServerDestination{ @@ -2236,12 +2236,12 @@ Network::process_v3_onion_response(Builder builder, std::string response) { std::tuple>, std::optional> Network::process_v4_onion_response(Builder builder, std::string response) { - auto response_data = str_to_vec(response); + auto response_data = to_vector(response); auto parser = ResponseParser(builder); auto result = parser.decrypt(response_data); // Process the bencoded response - oxenc::bt_list_consumer result_bencode{vec_to_span(result)}; + oxenc::bt_list_consumer result_bencode{to_span(result)}; if (result_bencode.is_finished() || !result_bencode.is_string()) throw std::runtime_error{"Invalid bencoded response"}; @@ -2680,7 +2680,7 @@ void Network::handle_errors( oxenc::is_hex(*ed25519PublicKey)) { session::onionreq::ed25519_pubkey edpk = session::onionreq::ed25519_pubkey::from_hex(*ed25519PublicKey); - auto edpk_view = to_unsigned_sv(edpk.view()); + auto edpk_view = to_span(edpk.view()); auto snode_it = std::find_if( updated_path.nodes.begin(), diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index f5986385..bcd40005 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -31,7 +31,6 @@ using namespace std::literals; using namespace oxen::log::literals; -using session::ustring_view; namespace session::onionreq { @@ -149,10 +148,10 @@ std::vector Builder::_generate_payload( // If we were given a body, add it to the payload if (body.has_value()) - payload.emplace_back(vec_to_str(*body)); + payload.emplace_back(session::to_string(*body)); auto result = oxenc::bt_serialize(payload); - return str_to_vec(result); + return to_vector(result); } std::vector Builder::build(std::vector payload) { @@ -229,7 +228,7 @@ std::vector Builder::build(std::vector payload) { }; auto control_dump = control.dump(); - auto control_span = to_const_unsigned_span(control_dump); + auto control_span = to_span(control_dump); auto data = encode_size(payload.size()); data.insert(data.end(), payload.begin(), payload.end()); data.insert(data.end(), control_span.begin(), control_span.end()); @@ -265,7 +264,7 @@ std::vector Builder::build(std::vector payload) { } auto routing_dump = routing.dump(); - auto routing_span = to_const_unsigned_span(routing_dump); + auto routing_span = to_span(routing_dump); auto data = encode_size(blob.size()); data.insert(data.end(), blob.begin(), blob.end()); data.insert(data.end(), routing_span.begin(), routing_span.end()); @@ -280,7 +279,7 @@ std::vector Builder::build(std::vector payload) { // how to decrypt the initial payload: auto wrapper_dump = nlohmann::json{{"ephemeral_key", A.hex()}, {"enc_type", to_string(enc_type)}}.dump(); - auto wrapper_span = to_const_unsigned_span(wrapper_dump); + auto wrapper_span = to_span(wrapper_dump); auto result = encode_size(blob.size()); result.insert(result.end(), blob.begin(), blob.end()); result.insert(result.end(), wrapper_span.begin(), wrapper_span.end()); @@ -300,8 +299,6 @@ session::onionreq::Builder& unbox(onion_request_builder_object* builder) { extern "C" { -using session::ustring; - LIBSESSION_C_API void onion_request_builder_init(onion_request_builder_object** builder) { auto c_builder = std::make_unique(); c_builder->internals = new session::onionreq::Builder{}; diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index da6fd6e8..611ce0bd 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -41,7 +41,7 @@ namespace { const x25519_seckey& seckey, const x25519_pubkey& pubkey) { auto key = calculate_shared_secret(seckey, pubkey); - auto usalt = to_unsigned_sv(salt); + auto usalt = to_span(salt); crypto_auth_hmacsha256_state state; @@ -142,10 +142,11 @@ std::vector HopEncryption::encrypt_aesgcm( std::vector HopEncryption::decrypt_aesgcm( std::vector ciphertext_, const x25519_pubkey& pubKey) const { - std::span ciphertext = vec_to_span(ciphertext_); + std::span ciphertext = to_span(ciphertext_); if (!response_long_enough(EncryptType::aes_gcm, ciphertext_.size())) - throw std::invalid_argument{"Ciphertext data is too short: " + vec_to_str(ciphertext_)}; + throw std::invalid_argument{ + "Ciphertext data is too short: " + session::to_string(ciphertext_)}; auto key = derive_symmetric_key(private_key_, pubKey); @@ -208,7 +209,7 @@ std::vector HopEncryption::encrypt_xchacha20( std::vector HopEncryption::decrypt_xchacha20( std::vector ciphertext_, const x25519_pubkey& pubKey) const { - std::span ciphertext = vec_to_span(ciphertext_); + std::span ciphertext = to_span(ciphertext_); // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -216,7 +217,8 @@ std::vector HopEncryption::decrypt_xchacha20( if (!response_long_enough(EncryptType::xchacha20, ciphertext_.size())) throw std::invalid_argument{ - "Ciphertext data is too short: " + std::string(from_unsigned(ciphertext_.data()))}; + "Ciphertext data is too short: " + + std::string(reinterpret_cast(ciphertext_.data()))}; const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index d58a29c8..766150db 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -8,7 +8,11 @@ namespace session::onionreq { -OnionReqParser::OnionReqParser(std::span x25519_pk, std::span x25519_sk, std::span req, size_t max_size) : +OnionReqParser::OnionReqParser( + std::span x25519_pk, + std::span x25519_sk, + std::span req, + size_t max_size) : keys{x25519_pubkey::from_bytes(x25519_pk), x25519_seckey::from_bytes(x25519_sk)}, enc{keys.second, keys.first} { if (sodium_init() == -1) @@ -35,11 +39,12 @@ OnionReqParser::OnionReqParser(std::span x25519_pk, std::sp else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - payload_ = enc.decrypt(enc_type, span_to_vec(ciphertext), remote_pk); + payload_ = enc.decrypt(enc_type, to_vector(ciphertext), remote_pk); } -std::vector OnionReqParser::encrypt_reply(std::span reply) const { - return enc.encrypt(enc_type, span_to_vec(reply), remote_pk); +std::vector OnionReqParser::encrypt_reply( + std::span reply) const { + return enc.encrypt(enc_type, to_vector(reply), remote_pk); } } // namespace session::onionreq diff --git a/src/random.cpp b/src/random.cpp index caeff02b..ac06bb2f 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -16,8 +16,8 @@ namespace session::random { constexpr char base32_charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; -ustring random(size_t size) { - ustring result; +std::vector random(size_t size) { + std::vector result; result.resize(size); randombytes_buf(result.data(), size); diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 1da385d5..89ecbff8 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -65,8 +65,10 @@ namespace detail { // some future version changes the format (and if not even try to load it). inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; -ustring sign_for_recipient( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { +std::vector sign_for_recipient( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -79,16 +81,19 @@ ustring sign_for_recipient( // If prefixed, drop it (and do this for the caller, too) so that everything after this // doesn't need to worry about whether it is prefixed or not. if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) - recipient_pubkey.remove_prefix(1); + recipient_pubkey = recipient_pubkey.subspan(1); else if (recipient_pubkey.size() != 32) throw std::invalid_argument{ "Invalid recipient_pubkey: expected 32 bytes (33 with 05 prefix)"}; - ustring buf; + std::vector buf; buf.reserve(message.size() + 96); // 32+32 now, but 32+64 when we reuse it for the sealed box - buf += message; - buf += ed25519_privkey.substr(32); // [32:] of a libsodium full seed value is the *pubkey* - buf += recipient_pubkey; + buf.insert(buf.end(), message.begin(), message.end()); + buf.insert( + buf.end(), + ed25519_privkey.begin() + 32, + ed25519_privkey.end()); // [32:] of a libsodium full seed value is the *pubkey* + buf.insert(buf.end(), recipient_pubkey.begin(), recipient_pubkey.end()); uc64 sig; if (0 != crypto_sign_ed25519_detached( @@ -97,23 +102,26 @@ ustring sign_for_recipient( // We have M||A||Y for the sig, but now we want M||A||SIG so drop Y then append SIG: buf.resize(buf.size() - 32); - buf += ustring_view{sig.data(), sig.size()}; + buf.insert(buf.end(), sig.begin(), sig.end()); return buf; } -static const ustring_view BOX_HASHKEY = to_unsigned_sv("SessionBoxEphemeralHashKey"sv); +static const std::span BOX_HASHKEY = to_span("SessionBoxEphemeralHashKey"); -ustring encrypt_for_recipient( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { +std::vector encrypt_for_recipient( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message) { auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); if (recipient_pubkey.size() == 33) - recipient_pubkey.remove_prefix(1); // sign_for_recipient already checked that this is the - // proper 0x05 prefix when present. + recipient_pubkey = + recipient_pubkey.subspan(1); // sign_for_recipient already checked that this is the + // proper 0x05 prefix when present. - ustring result; + std::vector result; result.resize(signed_msg.size() + crypto_box_SEALBYTES); if (0 != crypto_box_seal( result.data(), signed_msg.data(), signed_msg.size(), recipient_pubkey.data())) @@ -122,14 +130,16 @@ ustring encrypt_for_recipient( return result; } -ustring encrypt_for_recipient_deterministic( - ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { +std::vector encrypt_for_recipient_deterministic( + std::span ed25519_privkey, + std::span recipient_pubkey, + std::span message) { auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); if (recipient_pubkey.size() == 33) - recipient_pubkey.remove_prefix(1); // sign_for_recipient already checked that this is the - // proper 0x05 when present. + recipient_pubkey = recipient_pubkey.subspan(1); // sign_for_recipient already checked that + // this is the proper 0x05 when present. // To make our ephemeral seed we're going to hash: SENDER_SEED || RECIPIENT_PK || MESSAGE with a // keyed blake2b hash. @@ -159,7 +169,7 @@ ustring encrypt_for_recipient_deterministic( // pubkey prepended: static_assert(crypto_box_SEALBYTES == crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES); - ustring result; + std::vector result; result.resize(crypto_box_SEALBYTES + signed_msg.size()); std::memcpy(result.data(), eph_pk.data(), crypto_box_PUBLICKEYBYTES); if (0 != crypto_box_easy( @@ -203,7 +213,11 @@ ustring encrypt_for_recipient_deterministic( // server_pk -- the server's pubkey (needed to compute A's `k` value) // sending -- true if this for a message from A to B, false if this is from B to A. static cleared_uc32 blinded_shared_secret( - ustring_view seed, ustring_view kA, ustring_view jB, ustring_view server_pk, bool sending) { + std::span seed, + std::span kA, + std::span jB, + std::span server_pk, + bool sending) { // Because we're doing this generically, we use notation a/A/k for ourselves and b/jB for the // other person; this notion keeps everything exactly as above *except* for the concatenation in @@ -230,8 +244,8 @@ static cleared_uc32 blinded_shared_secret( bool blind25 = kA[0] == 0x25; - kA.remove_prefix(1); - jB.remove_prefix(1); + kA = kA.subspan(1); + jB = jB.subspan(1); cleared_uc32 ka; // Not really switching to x25519 here, this is just an easy way to compute `a` @@ -262,11 +276,11 @@ static cleared_uc32 blinded_shared_secret( return shared_secret; } -ustring encrypt_for_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view recipient_blinded_id, - ustring_view message) { +std::vector encrypt_for_blinded_recipient( + std::span ed25519_privkey, + std::span server_pk, + std::span recipient_blinded_id, + std::span message) { if (ed25519_privkey.size() != 64 && ed25519_privkey.size() != 32) throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; if (server_pk.size() != 32) @@ -285,35 +299,37 @@ ustring encrypt_for_blinded_recipient( throw std::invalid_argument{ "Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; } - ustring blinded_id; + std::vector blinded_id; blinded_id.reserve(33); - blinded_id += recipient_blinded_id[0]; - blinded_id.append(blinded_key_pair.first.begin(), blinded_key_pair.first.end()); + blinded_id.insert( + blinded_id.end(), recipient_blinded_id.begin(), recipient_blinded_id.begin() + 1); + blinded_id.insert( + blinded_id.end(), blinded_key_pair.first.begin(), blinded_key_pair.first.end()); auto enc_key = blinded_shared_secret( ed25519_privkey, blinded_id, recipient_blinded_id, server_pk, true); // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) - ustring buf; + std::vector buf; buf.reserve(message.size() + 32); - buf += message; + buf.insert(buf.end(), message.begin(), message.end()); // append A (pubkey) if (ed25519_privkey.size() == 64) { - buf += ed25519_privkey.substr(32); + buf.insert(buf.end(), ed25519_privkey.begin() + 32, ed25519_privkey.end()); } else { cleared_uc64 ed_sk_from_seed; uc32 ed_pk_buf; crypto_sign_ed25519_seed_keypair( ed_pk_buf.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - buf += to_sv(ed_pk_buf); + buf.insert(buf.end(), ed_pk_buf.begin(), ed_pk_buf.end()); } // Encrypt using xchacha20-poly1305 cleared_array nonce; randombytes_buf(nonce.data(), nonce.size()); - ustring ciphertext; + std::vector ciphertext; unsigned long long outlen = 0; ciphertext.resize( 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + @@ -343,8 +359,8 @@ ustring encrypt_for_blinded_recipient( return ciphertext; } -std::pair decrypt_incoming_session_id( - ustring_view ed25519_privkey, ustring_view ciphertext) { +std::pair, std::string> decrypt_incoming_session_id( + std::span ed25519_privkey, std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(ed25519_privkey, ciphertext); // Convert the sender_ed_pk to the sender's session ID @@ -363,8 +379,10 @@ std::pair decrypt_incoming_session_id( return {buf, sender_session_id}; } -std::pair decrypt_incoming_session_id( - ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { +std::pair, std::string> decrypt_incoming_session_id( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(x25519_pubkey, x25519_seckey, ciphertext); // Convert the sender_ed_pk to the sender's session ID @@ -383,8 +401,8 @@ std::pair decrypt_incoming_session_id( return {buf, sender_session_id}; } -std::pair decrypt_incoming( - ustring_view ed25519_privkey, ustring_view ciphertext) { +std::pair, std::vector> decrypt_incoming( + std::span ed25519_privkey, std::span ciphertext) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -403,15 +421,17 @@ std::pair decrypt_incoming( return decrypt_incoming({x_pub.data(), 32}, {x_sec.data(), 32}, ciphertext); } -std::pair decrypt_incoming( - ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { +std::pair, std::vector> decrypt_incoming( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext) { if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; const size_t msg_size = outer_size - 32 - 64; - std::pair result; + std::pair, std::vector> result; auto& [buf, sender_ed_pk] = result; buf.resize(outer_size); @@ -424,10 +444,10 @@ std::pair decrypt_incoming( throw std::runtime_error{"Decryption failed"}; uc64 sig; - sender_ed_pk = buf.substr(msg_size, 32); + sender_ed_pk.assign(buf.begin() + msg_size, buf.begin() + msg_size + 32); std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify - buf += ustring_view{x25519_pubkey.data(), 32}; + buf.insert(buf.end(), x25519_pubkey.begin(), x25519_pubkey.begin() + 32); if (0 != crypto_sign_ed25519_verify_detached( sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) @@ -439,12 +459,12 @@ std::pair decrypt_incoming( return result; } -std::pair decrypt_from_blinded_recipient( - ustring_view ed25519_privkey, - ustring_view server_pk, - ustring_view sender_id, - ustring_view recipient_id, - ustring_view ciphertext) { +std::pair, std::string> decrypt_from_blinded_recipient( + std::span ed25519_privkey, + std::span server_pk, + std::span sender_id, + std::span recipient_id, + std::span ciphertext) { uc32 ed_pk_from_seed; cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { @@ -462,15 +482,15 @@ std::pair decrypt_from_blinded_recipient( cleared_uc32 dec_key; auto blinded_id = recipient_id[0] == 0x25 - ? blinded25_id_from_ed(to_sv(ed_pk_from_seed), server_pk) - : blinded15_id_from_ed(to_sv(ed_pk_from_seed), server_pk); + ? blinded25_id_from_ed(to_span(ed_pk_from_seed), server_pk) + : blinded15_id_from_ed(to_span(ed_pk_from_seed), server_pk); if (sender_id == blinded_id) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); else dec_key = blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); - std::pair result; + std::pair, std::string> result; auto& [buf, sender_session_id] = result; // v, ct, nc = data[0], data[1:-24], data[-24:] @@ -478,7 +498,7 @@ std::pair decrypt_from_blinded_recipient( throw std::invalid_argument{ "Invalid ciphertext: version is not " + std::to_string(BLINDED_ENCRYPT_VERSION)}; - ustring nonce; + std::vector nonce; const size_t msg_size = (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -518,13 +538,13 @@ std::pair decrypt_from_blinded_recipient( if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - ustring session_id; // Gets populated by the following ..._from_ed calls + std::vector session_id; // Gets populated by the following ..._from_ed calls // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message auto extracted_sender = recipient_id[0] == 0x25 - ? blinded25_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id) - : blinded15_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id); + ? blinded25_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id) + : blinded15_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id); bool matched = sender_id == extracted_sender; if (!matched && extracted_sender[0] == 0x15) { @@ -547,8 +567,8 @@ std::pair decrypt_from_blinded_recipient( std::string decrypt_ons_response( std::string_view lowercase_name, - ustring_view ciphertext, - std::optional nonce) { + std::span ciphertext, + std::optional> nonce) { // Handle old Argon2-based encryption used before HF16 if (!nonce) { if (ciphertext.size() < crypto_secretbox_MACBYTES) @@ -568,7 +588,7 @@ std::string decrypt_ons_response( crypto_pwhash_ALG_ARGON2ID13)) throw std::runtime_error{"Failed to generate key"}; - ustring msg; + std::vector msg; msg.resize(ciphertext.size() - crypto_secretbox_MACBYTES); std::array nonce = {0}; @@ -603,7 +623,7 @@ std::string decrypt_ons_response( name_hash.data(), name_hash.size()); - ustring buf; + std::vector buf; unsigned long long buf_len = 0; buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); @@ -626,15 +646,16 @@ std::string decrypt_ons_response( return session_id; } -ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { +std::vector decrypt_push_notification( + std::span payload, std::span enc_key) { if (payload.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; if (enc_key.size() != 32) throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; - ustring buf; - ustring nonce; + std::vector buf; + std::vector nonce; const size_t msg_size = (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -656,8 +677,9 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; // Removing any null padding bytes from the end - if (auto pos = buf.find_last_not_of((unsigned char)0); pos != std::string::npos) - buf.resize(pos + 1); + if (auto it = std::find_if(buf.rbegin(), buf.rend(), [](unsigned char c) { return c != 0; }); + it != buf.rend()) + buf.resize(buf.size() - std::distance(buf.rbegin(), it)); return buf; } @@ -735,9 +757,9 @@ LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_recipient_deterministic( - ustring_view{ed25519_privkey, 64}, - ustring_view{recipient_pubkey, 32}, - ustring_view{plaintext_in, plaintext_len}); + std::span{ed25519_privkey, 64}, + std::span{recipient_pubkey, 32}, + std::span{plaintext_in, plaintext_len}); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -758,10 +780,10 @@ LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_blinded_recipient( - ustring_view{ed25519_privkey, 64}, - ustring_view{open_group_pubkey, 32}, - ustring_view{recipient_blinded_id, 33}, - ustring_view{plaintext_in, plaintext_len}); + std::span{ed25519_privkey, 64}, + std::span{open_group_pubkey, 32}, + std::span{recipient_blinded_id, 33}, + std::span{plaintext_in, plaintext_len}); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -781,7 +803,8 @@ LIBSESSION_C_API bool session_decrypt_incoming( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - ustring_view{ed25519_privkey, 64}, ustring_view{ciphertext_in, ciphertext_len}); + std::span{ed25519_privkey, 64}, + std::span{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -804,9 +827,9 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - ustring_view{x25519_pubkey, 32}, - ustring_view{x25519_seckey, 32}, - ustring_view{ciphertext_in, ciphertext_len}); + std::span{x25519_pubkey, 32}, + std::span{x25519_seckey, 32}, + std::span{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -831,11 +854,11 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( size_t* plaintext_len) { try { auto result = session::decrypt_from_blinded_recipient( - ustring_view{ed25519_privkey, 64}, - ustring_view{open_group_pubkey, 32}, - ustring_view{sender_id, 33}, - ustring_view{recipient_id, 33}, - ustring_view{ciphertext_in, ciphertext_len}); + std::span{ed25519_privkey, 64}, + std::span{open_group_pubkey, 32}, + std::span{sender_id, 33}, + std::span{recipient_id, 33}, + std::span{ciphertext_in, ciphertext_len}); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -855,12 +878,13 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* nonce_in, char* session_id_out) { try { - std::optional nonce; + std::optional> nonce; if (nonce_in) - nonce = ustring{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + nonce = std::span{ + nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; auto session_id = session::decrypt_ons_response( - name_in, ustring_view{ciphertext_in, ciphertext_len}, nonce); + name_in, std::span{ciphertext_in, ciphertext_len}, nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -877,7 +901,8 @@ LIBSESSION_C_API bool session_decrypt_push_notification( size_t* plaintext_len) { try { auto plaintext = session::decrypt_push_notification( - ustring_view{payload_in, payload_len}, ustring_view{enc_key_in, 32}); + std::span{payload_in, payload_len}, + std::span{enc_key_in, 32}); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/xed25519.cpp b/src/xed25519.cpp index a311e258..9d23390a 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -38,7 +38,7 @@ namespace { // This deviates from Signal's XEd25519 specified derivation of r in that we use a personalized // Black2b hash (for better performance and cryptographic properties), rather than a // custom-prefixed SHA-512 hash. - bytes<32> xed25519_compute_r(const bytes<32>& a, ustring_view msg) { + bytes<32> xed25519_compute_r(const bytes<32>& a, std::span msg) { bytes<64> random; randombytes_buf(random.data(), random.size()); @@ -62,7 +62,10 @@ namespace { // Assigns S = H(R || A || M) mod L void ed25519_hram( - unsigned char* S, const unsigned char* R, const bytes<32>& A, ustring_view msg) { + unsigned char* S, + const unsigned char* R, + const bytes<32>& A, + std::span msg) { bytes<64> hram; crypto_hash_sha512_state st; crypto_hash_sha512_init(&st); @@ -74,13 +77,10 @@ namespace { crypto_core_ed25519_scalar_reduce(S, hram.data()); } - ustring_view as_unsigned_sv(std::string_view x) { - return {reinterpret_cast(x.data()), x.size()}; - } - } // namespace -bytes<64> sign(ustring_view curve25519_privkey, ustring_view msg) { +bytes<64> sign( + std::span curve25519_privkey, std::span msg) { assert(curve25519_privkey.size() == 32); @@ -117,11 +117,14 @@ bytes<64> sign(ustring_view curve25519_privkey, ustring_view msg) { } std::string sign(std::string_view curve25519_privkey, std::string_view msg) { - auto sig = sign(as_unsigned_sv(curve25519_privkey), as_unsigned_sv(msg)); + auto sig = sign(to_span(curve25519_privkey), to_span(msg)); return std::string{reinterpret_cast(sig.data()), sig.size()}; } -bool verify(ustring_view signature, ustring_view curve25519_pubkey, ustring_view msg) { +bool verify( + std::span signature, + std::span curve25519_pubkey, + std::span msg) { assert(signature.size() == crypto_sign_ed25519_BYTES); assert(curve25519_pubkey.size() == 32); auto ed_pubkey = pubkey(curve25519_pubkey); @@ -130,11 +133,10 @@ bool verify(ustring_view signature, ustring_view curve25519_pubkey, ustring_view } bool verify(std::string_view signature, std::string_view curve25519_pubkey, std::string_view msg) { - return verify( - as_unsigned_sv(signature), as_unsigned_sv(curve25519_pubkey), as_unsigned_sv(msg)); + return verify(to_span(signature), to_span(curve25519_pubkey), to_span(msg)); } -std::array pubkey(ustring_view curve25519_pubkey) { +std::array pubkey(std::span curve25519_pubkey) { fe25519 u, y; crypto_internal_fe25519_frombytes(u, curve25519_pubkey.data()); fe25519_montx_to_edy(y, u); @@ -146,14 +148,12 @@ std::array pubkey(ustring_view curve25519_pubkey) { } std::string pubkey(std::string_view curve25519_pubkey) { - auto ed_pk = pubkey(as_unsigned_sv(curve25519_pubkey)); + auto ed_pk = pubkey(to_span(curve25519_pubkey)); return std::string{reinterpret_cast(ed_pk.data()), ed_pk.size()}; } } // namespace session::xed25519 -using session::xed25519::ustring_view; - extern "C" { LIBSESSION_C_API bool session_xed25519_sign( diff --git a/tests/static_bundle.cpp b/tests/static_bundle.cpp index beb158ad..a760a178 100644 --- a/tests/static_bundle.cpp +++ b/tests/static_bundle.cpp @@ -7,6 +7,6 @@ int main() { if (std::mt19937_64{}() == 123) { auto& k = *reinterpret_cast(12345); - k.encrypt_message(session::ustring_view{}); + k.encrypt_message(std::span{}); } } diff --git a/tests/swarm-auth-test.cpp b/tests/swarm-auth-test.cpp index 07d2335f..d524d616 100644 --- a/tests/swarm-auth-test.cpp +++ b/tests/swarm-auth-test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "utils.hpp" @@ -28,14 +29,14 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -static std::array sk_from_seed(ustring_view seed) { +static std::array sk_from_seed(std::span seed) { std::array ignore; std::array sk; crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); return sk; } -static std::string session_id_from_ed(ustring_view ed_pk) { +static std::string session_id_from_ed(std::span ed_pk) { std::string sid; std::array xpk; int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); @@ -48,7 +49,7 @@ static std::string session_id_from_ed(ustring_view ed_pk) { struct pseudo_client { std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; + const std::span public_key{secret_key.data() + 32, 32}; std::string session_id{session_id_from_ed(public_key)}; groups::Info info; @@ -56,20 +57,23 @@ struct pseudo_client { groups::Keys keys; pseudo_client( - ustring_view seed, + std::span seed, bool admin, const unsigned char* gpk, std::optional gsk) : secret_key{sk_from_seed(seed)}, - info{ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + info{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, std::nullopt}, - members{ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + members{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, std::nullopt}, keys{to_usv(secret_key), - ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, std::nullopt, info, members} {} @@ -77,11 +81,11 @@ struct pseudo_client { int main() { - const ustring group_seed = + const std::vector group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const ustring admin_seed = + const std::vector admin_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const ustring member_seed = + const std::vector member_seed = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; std::array group_pk; @@ -103,7 +107,7 @@ int main() { session::config::UserGroups member_gr2{member_seed, std::nullopt}; auto [seqno, push, obs] = member_groups.push(); - std::vector> gr_conf; + std::vector>> gr_conf; gr_conf.emplace_back("fakehash1", push); member_gr2.merge(gr_conf); @@ -111,12 +115,14 @@ int main() { auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); + auto now_vec = session::str_to_vec(std::to_string(now)); auto msg = to_usv("hello world"); std::array store_sig; - ustring store_to_sign; - store_to_sign += to_usv("store999"); - store_to_sign += to_usv(std::to_string(now)); + std::vector store_to_sign; + auto store_vec = session::str_to_vec("store999"); + store_to_sign.insert(store_to_sign.end(), store_vec.begin(), store_vec.end()); + store_to_sign.insert(store_to_sign.end(), now_vec.begin(), now_vec.end()); crypto_sign_ed25519_detached( store_sig.data(), nullptr, store_to_sign.data(), store_to_sign.size(), group_sk.data()); @@ -132,9 +138,11 @@ int main() { std::cout << "STORE:\n\n" << store.dump() << "\n\n"; - ustring retrieve_to_sign; - retrieve_to_sign += to_usv("retrieve999"); - retrieve_to_sign += to_usv(std::to_string(now)); + std::vector retrieve_to_sign; + auto retrieve_vec = session::str_to_vec("retrieve999"); + auto now_vec = session::str_to_vec(std::to_string(now)); + retrieve_to_sign.insert(retrieve_to_sign.end(), retrieve_vec.begin(), retrieve_vec.end()); + retrieve_to_sign.insert(retrieve_to_sign.end(), now_vec.begin(), now_vec.end()); auto subauth = member.keys.swarm_subaccount_sign(retrieve_to_sign, auth_data); nlohmann::json retrieve{ diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 07d033f8..423a3348 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -56,14 +56,14 @@ TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == "25a69cc6884530bf8498d22892e563716c4742f2845a7eb608de2aecbe7b6b5996"); - ustring session_id1_raw; + std::vector session_id1_raw; oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); - CHECK(oxenc::to_hex(blind25_id( + CHECK(to_hex(blind25_id( session_id1_raw, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); - CHECK(oxenc::to_hex(blind25_id( - session_id1_raw.substr(1), + CHECK(to_hex(blind25_id( + {session_id1_raw.begin() + 1, session_id1_raw.end()}, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); } @@ -84,8 +84,8 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { auto b25_5 = blind25_id(session_id2, server_pks[4]); auto b25_6 = blind25_id(session_id1, server_pks[5]); - auto sig1 = blind25_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); - CHECK(oxenc::to_hex(sig1) == + auto sig1 = blind25_sign(to_span(seed1), server_pks[0], to_span("hello")); + CHECK(to_hex(sig1) == "e6c57de4ac0cd278abbeef815bd88b163a037085deae789ecaaf4805884c4c3d3db25f3afa856241366cb341" "a3a4c9bbaa2cda81d028079c956fab16a7fe6206"); CHECK(0 == crypto_sign_verify_detached( @@ -94,8 +94,8 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 5, to_unsigned(oxenc::from_hex(b25_1).data()) + 1)); - auto sig2 = blind25_sign(to_usv(seed1), server_pks[1], to_unsigned_sv("world")); - CHECK(oxenc::to_hex(sig2) == + auto sig2 = blind25_sign(to_span(seed1), server_pks[1], to_span("world")); + CHECK(to_hex(sig2) == "4460b606e9f55a7cba0bbe24207fe2859c3422783373788b6b070b2fa62ceba4f2a50749a6cee68e095747a3" "69927f9f4afa86edaf055cad68110e35e8b06607"); CHECK(0 == crypto_sign_verify_detached( @@ -104,8 +104,8 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 5, to_unsigned(oxenc::from_hex(b25_2).data()) + 1)); - auto sig3 = blind25_sign(to_usv(seed2), server_pks[2], to_unsigned_sv("this")); - CHECK(oxenc::to_hex(sig3) == + auto sig3 = blind25_sign(to_span(seed2), server_pks[2], to_span("this")); + CHECK(to_hex(sig3) == "57bb2f80c88ce2f677902ee58e02cbd83e4e1ec9e06e1c72a34b4ab76d0f5219cfd141ac5ce7016c73c8382d" "b99df9f317f2bc0af6ca68edac2a9a7670938902"); CHECK(0 == crypto_sign_verify_detached( @@ -114,8 +114,8 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 4, to_unsigned(oxenc::from_hex(b25_3).data()) + 1)); - auto sig4 = blind25_sign(to_usv(seed2), server_pks[3], to_unsigned_sv("is")); - CHECK(oxenc::to_hex(sig4) == + auto sig4 = blind25_sign(to_span(seed2), server_pks[3], to_span("is")); + CHECK(to_hex(sig4) == "ecce032b27b09d2d3d6df4ebab8cae86656c64fd1e3e70d6f020cd7e1a8058c57e3df7b6b01e90ccd592ac4a" "845dde7a2fdceb1a328a6690686851583133ea0c"); CHECK(0 == crypto_sign_verify_detached( @@ -124,16 +124,16 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 2, to_unsigned(oxenc::from_hex(b25_4).data()) + 1)); - auto sig5 = blind25_sign(to_usv(seed2), server_pks[4], to_unsigned_sv("")); - CHECK(oxenc::to_hex(sig5) == + auto sig5 = blind25_sign(to_span(seed2), server_pks[4], to_span("")); + CHECK(to_hex(sig5) == "bf2fb9a511adbf5827e2e3bcf09f0a1cff80f85556fb76d8001aa8483b5f22e14539b170eaa0dbfa1489d1b8" "618ce8b48d7512cb5602c7eb8a05ce330a68350b"); CHECK(0 == crypto_sign_verify_detached( sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b25_5).data()) + 1)); - auto sig6 = blind25_sign(to_usv(seed1), server_pks[5], to_unsigned_sv("omg!")); - CHECK(oxenc::to_hex(sig6) == + auto sig6 = blind25_sign(to_span(seed1), server_pks[5], to_span("omg!")); + CHECK(to_hex(sig6) == "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); CHECK(0 == crypto_sign_verify_detached( @@ -143,8 +143,8 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); // Test that it works when given just the seed instead of the whole sk: - auto sig6b = blind25_sign(to_usv(seed1).substr(0, 32), server_pks[5], to_unsigned_sv("omg!")); - CHECK(oxenc::to_hex(sig6b) == + auto sig6b = blind25_sign(to_span(seed1).subspan(0, 32), server_pks[5], to_span("omg!")); + CHECK(to_hex(sig6b) == "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); CHECK(0 == crypto_sign_verify_detached( @@ -157,19 +157,19 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { TEST_CASE("Communities 15xxx-blinded pubkey derivation", "[blinding15][pubkey]") { REQUIRE(sodium_init() >= 0); - ustring session_id1_raw, session_id2_raw; + std::vector session_id1_raw, session_id2_raw; oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); oxenc::from_hex(session_id2.begin(), session_id2.end(), std::back_inserter(session_id2_raw)); - CHECK(oxenc::to_hex(blind15_id( + CHECK(to_hex(blind15_id( session_id1_raw, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); - CHECK(oxenc::to_hex(blind15_id( + CHECK(to_hex(blind15_id( session_id2_raw, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == "1561e070286ff7a71f167e92b18c709882b148d8238c8872caf414b301ba0564fd"); - CHECK(oxenc::to_hex(blind15_id( - session_id1_raw.substr(1), + CHECK(to_hex(blind15_id( + {session_id1_raw.begin() + 1, session_id1_raw.end()}, "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); } @@ -192,8 +192,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; - auto sig1 = blind15_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); - CHECK(oxenc::to_hex(sig1) == + auto sig1 = blind15_sign(to_span(seed1), server_pks[0], to_span("hello")); + CHECK(to_hex(sig1) == "1a5ade20b43af0e16b3e591d6f86303938d7557c0ac54469dd4f5aea759f82d22cafa42587251756e133acdd" "dd8cbec2f707a9ce09a49f2193f46a91502c5006"); CHECK(0 == crypto_sign_verify_detached( @@ -202,8 +202,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { 5, to_unsigned(oxenc::from_hex(b15_1).data()) + 1)); - auto sig2 = blind15_sign(to_usv(seed1), server_pks[1], to_unsigned_sv("world")); - CHECK(oxenc::to_hex(sig2) == + auto sig2 = blind15_sign(to_span(seed1), server_pks[1], to_span("world")); + CHECK(to_hex(sig2) == "d357f74c5ec5536840aec575051f71fdb22d70f35ef31db1715f5f694842de3b39aa647c84aa8e28ec56eb76" "2d237c9e030639c83f429826d419ac719cd4df03"); CHECK(0 == crypto_sign_verify_detached( @@ -212,8 +212,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { 5, to_unsigned(oxenc::from_hex(b15_2).data()) + 1)); - auto sig3 = blind15_sign(to_usv(seed2), server_pks[2], to_unsigned_sv("this")); - CHECK(oxenc::to_hex(sig3) == + auto sig3 = blind15_sign(to_span(seed2), server_pks[2], to_span("this")); + CHECK(to_hex(sig3) == "dacf91dfb411e99cd8ef4cb07b195b49289cf1a724fef122c73462818560bc29832a98d870ec4feb79dedca5" "b59aba6a466d3ce8f3e35adf25a1813f6989fd0a"); CHECK(0 == crypto_sign_verify_detached( @@ -222,8 +222,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { 4, to_unsigned(oxenc::from_hex(b15_3).data()) + 1)); - auto sig4 = blind15_sign(to_usv(seed2), server_pks[3], to_unsigned_sv("is")); - CHECK(oxenc::to_hex(sig4) == + auto sig4 = blind15_sign(to_span(seed2), server_pks[3], to_span("is")); + CHECK(to_hex(sig4) == "8339ea9887d3e44131e33403df160539cdc7a0a8107772172c311e95773660a0d39ed0a6c2b2c794dde1fdc6" "40943e403497aa02c4d1a21a7d9030742beabb05"); CHECK(0 == crypto_sign_verify_detached( @@ -232,16 +232,16 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { 2, to_unsigned(oxenc::from_hex(b15_4).data()) + 1)); - auto sig5 = blind15_sign(to_usv(seed2), server_pks[4], to_unsigned_sv("")); - CHECK(oxenc::to_hex(sig5) == + auto sig5 = blind15_sign(to_span(seed2), server_pks[4], to_span("")); + CHECK(to_hex(sig5) == "8b0d6447decff3a21ec1809141580139c4a51e24977b0605fe7984439993f5377ebc9681e4962593108d03cc" "8b6873c5c5ba8c30287188137d2dee9ab10afd0f"); CHECK(0 == crypto_sign_verify_detached( sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b15_5).data()) + 1)); - auto sig6 = blind15_sign(to_usv(seed1), server_pks[5], to_unsigned_sv("omg!")); - CHECK(oxenc::to_hex(sig6) == + auto sig6 = blind15_sign(to_span(seed1), server_pks[5], to_span("omg!")); + CHECK(to_hex(sig6) == "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); CHECK(0 == crypto_sign_verify_detached( @@ -251,8 +251,8 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); // Test that it works when given just the seed instead of the whole sk: - auto sig6b = blind15_sign(to_usv(seed1).substr(0, 32), server_pks[5], to_unsigned_sv("omg!")); - CHECK(oxenc::to_hex(sig6b) == + auto sig6b = blind15_sign(to_span(seed1).subspan(0, 32), server_pks[5], to_span("omg!")); + CHECK(to_hex(sig6b) == "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); CHECK(0 == crypto_sign_verify_detached( @@ -265,7 +265,7 @@ TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { REQUIRE(sodium_init() >= 0); - auto [pubkey, seckey] = blind_version_key_pair(to_usv(seed1)); + auto [pubkey, seckey] = blind_version_key_pair(to_span(seed1)); CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); @@ -274,7 +274,7 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { // Hash ourselves just to make sure we get what we expect for the seed part of the secret key: cleared_uc32 expect_seed; - static const auto hash_key = to_unsigned_sv("VersionCheckKey_sig"sv); + static const auto hash_key = to_span("VersionCheckKey_sig"sv); crypto_generichash_blake2b( expect_seed.data(), 32, seed1.data(), 32, hash_key.data(), hash_key.size()); @@ -289,29 +289,32 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { REQUIRE(sodium_init() >= 0); - auto signature = blind_version_sign(to_usv(seed1), Platform::desktop, 1234567890); + auto signature = blind_version_sign(to_span(seed1), Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e92" "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); - auto [pk, sk] = blind_version_key_pair(to_usv(seed1)); + auto [pk, sk] = blind_version_key_pair(to_span(seed1)); auto method = "GET"sv; + auto method_span = to_span(method); auto path = "/path/to/somewhere"sv; - auto body = to_unsigned_sv("some body (once told me)"); + auto path_span = to_span(path); + auto body = to_span("some body (once told me)"); uint64_t timestamp = 1234567890; - ustring full_message; - full_message += to_unsigned_sv(std::to_string(timestamp)); - full_message += to_unsigned_sv(method); - full_message += to_unsigned_sv(path); + std::vector ts = to_vector(std::to_string(timestamp)); + std::vector full_message; + full_message.insert(full_message.end(), ts.begin(), ts.end()); + full_message.insert(full_message.end(), method_span.begin(), method_span.end()); + full_message.insert(full_message.end(), path_span.begin(), path_span.end()); auto req_sig_no_body = - blind_version_sign_request(to_usv(seed1), timestamp, method, path, std::nullopt); + blind_version_sign_request(to_span(seed1), timestamp, method, path, std::nullopt); CHECK(crypto_sign_verify_detached( req_sig_no_body.data(), full_message.data(), full_message.size(), pk.data()) == 0); - full_message += body; - auto req_sig = blind_version_sign_request(to_usv(seed1), timestamp, method, path, body); + full_message.insert(full_message.end(), body.begin(), body.end()); + auto req_sig = blind_version_sign_request(to_span(seed1), timestamp, method, path, body); CHECK(crypto_sign_verify_detached( req_sig.data(), full_message.data(), full_message.size(), pk.data()) == 0); } diff --git a/tests/test_bugs.cpp b/tests/test_bugs.cpp index b4cc0818..754f4425 100644 --- a/tests/test_bugs.cpp +++ b/tests/test_bugs.cpp @@ -25,14 +25,14 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{ustring_view{seed}, std::nullopt}; + session::config::Contacts c1{session::to_span(seed), std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); c1.confirm_pushed(seqno, "fakehash1"); - session::config::Contacts c2{ustring_view{seed}, c1.dump()}; - session::config::Contacts c3{ustring_view{seed}, c1.dump()}; + session::config::Contacts c2{session::to_span(seed), c1.dump()}; + session::config::Contacts c3{session::to_span(seed), c1.dump()}; CHECK_FALSE(c2.needs_dump()); CHECK_FALSE(c2.needs_push()); @@ -47,13 +47,13 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { auto [seqno3, data3, obs3] = c3.push(); REQUIRE(seqno2 == 2); - CHECK(obs2 == std::vector{"fakehash1"s}); + CHECK(as_set(obs2) == make_set("fakehash1"s)); REQUIRE(seqno3 == 2); - CHECK(obs2 == std::vector{"fakehash1"s}); + CHECK(as_set(obs3) == make_set("fakehash1"s)); - auto r = c1.merge(std::vector>{ + auto r = c1.merge(std::vector>>{ {{"fakehash2", data2}, {"fakehash3", data3}}}); - CHECK(r == std::vector{{"fakehash2"s, "fakehash3"s}}); + CHECK(as_set(r) == make_set("fakehash2"s, "fakehash3"s)); CHECK(c1.needs_dump()); CHECK(c1.needs_push()); // because we have the merge conflict to push CHECK(c1.is_dirty()); @@ -90,7 +90,7 @@ TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{ustring_view{seed}, std::nullopt}; + session::config::Contacts c1{std::span{seed}, std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); @@ -99,7 +99,8 @@ TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") CHECK(!c1.needs_dump()); CHECK(!c1.needs_push()); - auto r = c1.merge(std::vector>{{{"fakehash1", data}}}); + auto r = c1.merge(std::vector>>{ + {{"fakehash1", data}}}); CHECK(as_set(r) == make_set("fakehash1"s)); auto old_hashes = c1.old_hashes(); @@ -126,13 +127,13 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{ustring_view{seed}, std::nullopt}; + session::config::Contacts c1{std::span{seed}, std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); c1.confirm_pushed(seqno, "fakehash1"); - session::config::Contacts c2{ustring_view{seed}, c1.dump()}; + session::config::Contacts c2{std::span{seed}, c1.dump()}; CHECK_FALSE(c2.needs_dump()); CHECK_FALSE(c2.needs_push()); @@ -148,7 +149,8 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c2.confirm_pushed(seqno2, "fakehash2"); CHECK(c1.is_dirty()); // already dirty before the merge - auto r = c1.merge(std::vector>{{{"fakehash2", data2}}}); + auto r = c1.merge(std::vector>>{ + {{"fakehash2", data2}}}); CHECK(r == std::vector{{"fakehash2"s}}); CHECK(c1.needs_dump()); @@ -167,7 +169,8 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c2.confirm_pushed(seqno3, "fakehash3"); CHECK(c1.is_dirty()); // already dirty before the merge - auto r2 = c1.merge(std::vector>{{{"fakehash3", data3}}}); + auto r2 = c1.merge(std::vector>>{ + {{"fakehash3", data3}}}); CHECK(r2 == std::vector{{"fakehash3"s}}); CHECK(c1.needs_dump()); @@ -198,7 +201,8 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c1.set_name("051111111111111111111111111111111111111111111111111111111111111140", "barney40"); auto size_before_merge = c1.size(); // retrieve size before trying to merge CHECK(c1.is_dirty()); // already dirty before the merge - auto r4 = c1.merge(std::vector>{{{"fakehash21", data4}}}); + auto r4 = c1.merge(std::vector>>{ + {{"fakehash21", data4}}}); CHECK(r4 == std::vector{{"fakehash21"s}}); CHECK(c1.needs_dump()); diff --git a/tests/test_compression.cpp b/tests/test_compression.cpp index 24892fbd..a17e7371 100644 --- a/tests/test_compression.cpp +++ b/tests/test_compression.cpp @@ -10,7 +10,7 @@ #include "utils.hpp" namespace session::config { -void compress_message(ustring& msg, int level); +void compress_message(std::vector& msg, int level); } using namespace std::literals; @@ -40,7 +40,7 @@ TEST_CASE("compression", "[config][compression]") { // This message (from the user profile test case) doesn't compress any better than plaintext // with zstd compression, so the compress_message call shouldn't change it. // clang-format off - data = + data = session::to_vector( "d" "1:#" "i1e" "1:&" "d" @@ -51,8 +51,8 @@ TEST_CASE("compression", "[config][compression]") { "1:<" "l" "l" "i0e" - "32:"_bytes + - "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes + + "32:" + + session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes) + "de" "e" "e" @@ -61,10 +61,10 @@ TEST_CASE("compression", "[config][compression]") { "1:p" "0:" "1:q" "0:" "e" - "e"_bytes; + "e"); // // If we add some more repetition in it, though, it will: - auto data2 = + auto data2 = session::to_vector( "d" "1:#" "i1e" "1:&" "d" @@ -75,8 +75,8 @@ TEST_CASE("compression", "[config][compression]") { "1:<" "l" "l" "i0e" - "32:"_bytes + - "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes + + "32:" + + session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes) + "de" "e" "e" @@ -85,7 +85,7 @@ TEST_CASE("compression", "[config][compression]") { "1:p" "0:" "1:q" "0:" "e" - "e"_bytes; + "e"); // clang-format on d = data; diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 8f506cfe..9faa4a00 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -31,7 +31,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts contacts{ustring_view{seed}, std::nullopt}; + session::config::Contacts contacts{std::span{seed}, std::nullopt}; constexpr auto definitely_real_id = "050000000000000000000000000000000000000000000000000000000000000000"sv; @@ -126,7 +126,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(seqno == 2); - std::vector> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash2", to_push); contacts.merge(merge_configs); contacts2.confirm_pushed(seqno, "fakehash2"); @@ -165,7 +165,7 @@ TEST_CASE("Contacts", "[config][contacts]") { 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::vector key = "qwerty78901234567890123456789012"_bytes; std::string url = "http://example.com/huge.bmp"; p.set_key(std::move(key)); p.url = std::move(url); @@ -422,7 +422,7 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - session::config::Contacts contacts{ustring_view{seed}, std::nullopt}; + session::config::Contacts contacts{std::span{seed}, std::nullopt}; for (uint16_t i = 0; i < 10000; i++) { char buf[2]; @@ -453,7 +453,7 @@ TEST_CASE("needs_dump bug", "[config][needs_dump]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - session::config::Contacts contacts{ustring_view{seed}, std::nullopt}; + session::config::Contacts contacts{std::span{seed}, std::nullopt}; CHECK_FALSE(contacts.needs_dump()); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index bdac72e4..c80f08c6 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -30,7 +30,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::ConvoInfoVolatile convos{ustring_view{seed}, std::nullopt}; + session::config::ConvoInfoVolatile convos{std::span{seed}, std::nullopt}; constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"sv; @@ -146,7 +146,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(seqno == 2); - std::vector> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("hash2", to_push); convos.merge(merge_configs); convos2.confirm_pushed(seqno, "hash2"); @@ -481,10 +481,11 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::ConvoInfoVolatile convos{ustring_view{seed}, std::nullopt}; + session::config::ConvoInfoVolatile convos{std::span{seed}, std::nullopt}; - auto some_pubkey = [](unsigned char x) -> ustring { - ustring s = "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes; + auto some_pubkey = [](unsigned char x) -> std::vector { + std::vector s = + "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes; s[31] = x; return s; }; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index e0af2a98..67b23617 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -97,7 +97,7 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups groups{ustring_view{seed}, std::nullopt}; + session::config::UserGroups groups{std::span{seed}, std::nullopt}; constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"sv; @@ -169,8 +169,8 @@ TEST_CASE("User Groups", "[config][groups]") { lg_pk.data(), lg_sk.data(), reinterpret_cast(lgroup_seed.data())); // Note: this isn't exactly what Session actually does here for legacy closed groups (rather it // uses X25519 keys) but for this test the distinction doesn't matter. - c.enc_pubkey.assign(lg_pk.data(), lg_pk.size()); - c.enc_seckey.assign(lg_sk.data(), 32); + c.enc_pubkey.assign(lg_pk.data(), lg_pk.data() + lg_pk.size()); + c.enc_seckey.assign(lg_sk.data(), lg_sk.data() + 32); c.priority = 3; CHECK(to_hex(c.enc_pubkey) == oxenc::to_hex(lg_pk.begin(), lg_pk.end())); @@ -313,7 +313,7 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(std::get(g2.push()) == 2); CHECK_FALSE(g2.needs_dump()); - std::vector> to_merge; + std::vector>> to_merge; to_merge.emplace_back("fakehash2", to_push); groups.merge(to_merge); auto x3 = groups.get_community("http://example.org:5678", "SudokuRoom"); @@ -441,7 +441,7 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups groups{ustring_view{seed}, std::nullopt}; + session::config::UserGroups groups{std::span{seed}, std::nullopt}; constexpr auto definitely_real_id = "035000000000000000000000000000000000000000000000000000000000000000"sv; @@ -460,8 +460,8 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c.notifications == session::config::notify_mode::defaulted); CHECK(c.mute_until == 0); - c.secretkey = to_usv(ed_sk); // This *isn't* the right secret key for the group, so won't - // propagate, and so auth data will: + c.secretkey = session::to_vector(ed_sk); // This *isn't* the right secret key for the group, so + // won't propagate, and so auth data will: c.auth_data = "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" @@ -477,7 +477,7 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { auto d1 = groups.dump(); - session::config::UserGroups g2{ustring_view{seed}, d1}; + session::config::UserGroups g2{std::span{seed}, d1}; auto c2 = g2.get_group(definitely_real_id); REQUIRE(c2.has_value()); @@ -500,8 +500,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { g2.set(*c2); auto c2b = g2.get_or_construct_group("03" + oxenc::to_hex(ed_pk.begin(), ed_pk.end())); - c2b.secretkey = to_usv(ed_sk); // This one does match the group ID, so should propagate - c2b.auth_data = // should get ignored, since we have a valid secret key set: + c2b.secretkey = + session::to_vector(ed_sk); // This one does match the group ID, so should propagate + c2b.auth_data = // should get ignored, since we have a valid secret key set: "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" "0000000000000000000000000000"_hexbytes; @@ -510,7 +511,7 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { std::tie(seqno, to_push, obs) = g2.push(); g2.confirm_pushed(seqno, "fakehash2"); - std::vector> to_merge; + std::vector>> to_merge; to_merge.emplace_back("fakehash2", to_push); groups.merge(to_merge); @@ -714,10 +715,11 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { REQUIRE(keys); REQUIRE(key_len == 1); - session::config::UserGroups c2{ustring_view{seed}, std::nullopt}; + session::config::UserGroups c2{std::span{seed}, std::nullopt}; - std::vector> to_merge; - to_merge.emplace_back("fakehash1", ustring_view{to_push->config, to_push->config_len}); + std::vector>> to_merge; + to_merge.emplace_back( + "fakehash1", std::span{to_push->config, to_push->config_len}); CHECK(c2.merge(to_merge) == std::vector{{"fakehash1"}}); auto grp = c2.get_legacy_group(definitely_real_id); @@ -742,7 +744,7 @@ TEST_CASE("User groups empty member bug", "[config][groups][bug]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups c{ustring_view{seed}, std::nullopt}; + session::config::UserGroups c{std::span{seed}, std::nullopt}; CHECK_FALSE(c.needs_push()); @@ -828,7 +830,7 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups c{ustring_view{seed}, std::nullopt}; + session::config::UserGroups c{std::span{seed}, std::nullopt}; CHECK_FALSE(c.needs_push()); @@ -894,7 +896,7 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef64313a21303a313a4b" "303a" "313a6a303a65656565313a28303a313a296c6565"_hexbytes; - session::config::UserGroups c2{ustring_view{seed}, dump_with_not_seconds}; + session::config::UserGroups c2{std::span{seed}, dump_with_not_seconds}; auto gr = c2.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 6ae899dd..58627250 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "utils.hpp" @@ -32,7 +33,7 @@ TEST_CASE("UserProfile", "[config][user_profile]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserProfile profile{ustring_view{seed}, std::nullopt}; + session::config::UserProfile profile{std::span{seed}, std::nullopt}; CHECK_THROWS( profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789" @@ -139,9 +140,10 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { pic = user_profile_get_pic(conf); REQUIRE(pic.url != ""s); - REQUIRE(pic.key != to_usv(""s)); + REQUIRE(pic.key != session::to_vector("").data()); CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv); - CHECK(ustring_view{pic.key, 32} == "secret78901234567890123456789012"_bytes); + CHECK(session::to_vector(std::span{pic.key, 32}) == + "secret78901234567890123456789012"_bytes); CHECK(user_profile_get_nts_priority(conf) == 9); @@ -159,7 +161,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // The data to be actually pushed, expanded like this to make it somewhat human-readable: // clang-format off - auto exp_push1_decrypted = + auto exp_push1_decrypted = session::to_vector( "d" "1:#" "i1e" "1:&" "d" @@ -169,7 +171,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:q" "32:secret78901234567890123456789012" "e" "1:<" "l" - "l" "i0e" "32:"_bytes + exp_hash0 + "de" "e" + "l" "i0e" "32:" + session::to_string(exp_hash0) + "de" "e" "e" "1:=" "d" "1:+" "0:" @@ -177,7 +179,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:p" "0:" "1:q" "0:" "e" - "e"_bytes; + "e"); // clang-format on auto exp_push1_encrypted = "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f" @@ -211,7 +213,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(printable(dump1, dump1len) == printable( "d" "1:!" "i2e" - "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" + "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + session::to_string(exp_push1_decrypted) + "" "1:(" "0:" "1:)" "le" + empty_extra_data + "e")); @@ -230,7 +232,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(printable(dump1, dump1len) == printable( "d" "1:!" "i0e" - "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" + "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + session::to_string(exp_push1_decrypted) + "" "1:(" "9:fakehash1" "1:)" "le" + empty_extra_data + "e")); @@ -368,7 +370,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(to_hex(std::span{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); #if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) @@ -382,7 +384,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(to_hex(std::span{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); CHECK(user_profile_get_nts_priority(conf) == 9); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 433be62f..4883b05c 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "session/bt_merge.hpp" #include "session/version.h" @@ -107,17 +108,8 @@ auto& s(config::dict_value& v) { return var::get(v); } -template -ustring_view view(const std::array& data) { - return ustring_view{data.data(), data.size()}; -} -template -std::string view_hex(const std::array& data) { - return oxenc::to_hex(data.begin(), data.end()); -} - -ustring blake2b(ustring_view data) { - ustring result; +std::vector blake2b(std::span data) { + std::vector result; result.resize(32); crypto_generichash_blake2b(result.data(), 32, data.data(), data.size(), nullptr, 0); return result; @@ -203,12 +195,12 @@ TEST_CASE("config message serialization", "[config][serialization]") { // clang-format on const std::string hash0{"d65738bba88b0f3455cef20fe09a7b4b10f25f9db82be24a6ce1bd06da197526"_hex}; - CHECK(view_hex(m.hash()) == oxenc::to_hex(hash0)); + CHECK(to_hex(m.hash()) == oxenc::to_hex(hash0)); auto m1 = m.increment(); m1.data().erase("foo"); const std::string hash1{"5b30b4abf4cba71db25dbc0d977cc25df1d0a8a87cad7f561cdec2b8caf65f5e"_hex}; - CHECK(view_hex(m1.hash()) == oxenc::to_hex(hash1)); + CHECK(to_hex(m1.hash()) == oxenc::to_hex(hash1)); auto m2 = m1.increment(); @@ -221,7 +213,7 @@ TEST_CASE("config message serialization", "[config][serialization]") { s(d(m2.data()["bar"])[""]).insert(42); // already present const std::string hash2{"027552203cf669070d3ecbeecfa65c65497d59aa4da490e0f68f8131ce081320"_hex}; - CHECK(view_hex(m2.hash()) == oxenc::to_hex(hash2)); + CHECK(to_hex(m2.hash()) == oxenc::to_hex(hash2)); // clang-format off CHECK(printable(m2.serialize()) == printable( @@ -273,9 +265,9 @@ TEST_CASE("config message serialization", "[config][serialization]") { hash4{"c30e2cfa7ec93c64a1ab6420c9bccfb63da8e4c2940ed6509ffb64f3f0131860"_hex}, hash5{"3234eb7da8cf4b79b9eec2a144247279d10f6f118184f82429a42c5996bea60c"_hex}; - CHECK(view_hex(m2.increment().hash()) == oxenc::to_hex(hash3)); - CHECK(view_hex(m2.increment().increment().hash()) == oxenc::to_hex(hash4)); - CHECK(view_hex(m5.hash()) == oxenc::to_hex(hash5)); + CHECK(to_hex(m2.increment().hash()) == oxenc::to_hex(hash3)); + CHECK(to_hex(m2.increment().increment().hash()) == oxenc::to_hex(hash4)); + CHECK(to_hex(m5.hash()) == oxenc::to_hex(hash5)); // clang-format off CHECK(printable(m5.serialize()) == printable( @@ -339,14 +331,16 @@ TEST_CASE("config message signature", "[config][signing]") { "4384261cdd338f5820ca9cbbe3fc72ac8944ee60d3b795b797fbbf5597b09f17"sv; std::array secretkey; oxenc::from_hex(skey_hex.begin(), skey_hex.end(), secretkey.begin()); - auto signer = [&secretkey](ustring_view data) { - ustring result; + auto signer = [&secretkey](std::span data) { + std::vector result; result.resize(64); crypto_sign_ed25519_detached( result.data(), nullptr, data.data(), data.size(), secretkey.data()); return result; }; - auto verifier = [&secretkey](ustring_view data, ustring_view signature) { + auto verifier = [&secretkey]( + std::span data, + std::span signature) { return 0 == crypto_sign_verify_detached( signature.data(), data.data(), data.size(), secretkey.data() + 32); }; @@ -383,15 +377,17 @@ TEST_CASE("config message signature", "[config][signing]") { auto expected_sig = "77267f4de7701ae348eba0ef73175281512ba3f1051cfed22dc3e31b9c699330" "2938863e09bc8b33638161071bd8dc397d5c1d3f674120d08fbb9c64dde2e907"_hexbytes; - ustring sig(64, '\0'); + std::vector sig(64, '\0'); // Sign it ourselves, and check what we get: crypto_sign_ed25519_detached( sig.data(), nullptr, m_signing_value.data(), m_signing_value.size(), secretkey.data()); CHECK(to_hex(sig) == to_hex(expected_sig)); + auto key_bytes = "1:~64:"_bytes; + auto end_bytes = "e"_bytes; auto m_expected = m_signing_value; - m_expected += "1:~64:"_bytes; - m_expected += expected_sig; - m_expected += 'e'; + m_expected.insert(m_expected.end(), key_bytes.begin(), key_bytes.end()); + m_expected.insert(m_expected.end(), expected_sig.begin(), expected_sig.end()); + m_expected.insert(m_expected.end(), end_bytes.begin(), end_bytes.end()); CHECK(printable(m.serialize()) == printable(m_expected)); ConfigMessage msg{m_expected, verifier, signer}; @@ -417,7 +413,7 @@ TEST_CASE("config message signature", "[config][signing]") { ConfigMessage m2{{m_broken, m_expected}, verifier, signer}; CHECK_FALSE(m2.merged()); CHECK(m2.seqno() == 10); - CHECK(view_hex(m2.hash()) == view_hex(m.hash())); + CHECK(to_hex(m2.hash()) == to_hex(m.hash())); CHECK_THROWS_MATCHES( ConfigMessage( @@ -429,7 +425,8 @@ TEST_CASE("config message signature", "[config][signing]") { config::config_error, Message("Config signature failed verification")); - auto m_unsigned = m_signing_value + "e"_bytes; + auto m_unsigned = m_signing_value; + m_unsigned.insert(m_unsigned.end(), end_bytes.begin(), end_bytes.end()); CHECK_THROWS_MATCHES( ConfigMessage(m_unsigned, verifier), config::missing_signature, @@ -451,7 +448,7 @@ const auto h119 = "43094f68c1faa37eff79e1c2f3973ffd5f9d6423b00ccda306fc6e7dac5f0 const auto h120 = "e3a237f91014d31e4d30569c4a8bfcd72157804f99b8732c611c48bf126432b5"_hexbytes; const auto h121 = "1a7f602055124deaf21175ef3f32983dee7c9de570e5d9c9a0bbc2db71dcb97f"_hexbytes; const auto h122 = "46560604fe352101bb869435260d7100ccfe007be5f741c7e96303f02f394e8a"_hexbytes; -const auto m123_expected = +const auto m123_expected = to_vector( // clang-format off "d" "1:#" "i123e" @@ -479,17 +476,17 @@ const auto m123_expected = "7:string2" "7:goodbye" "e" "1:<" "l" - "l" "i119e" "32:"_bytes+h119+ "de" "e" - "l" "i120e" "32:"_bytes+h120+ "de" "e" - "l" "i121e" "32:"_bytes+h121+ "de" "e" - "l" "i122e" "32:"_bytes+h122+ "de" "e" + "l" "i119e" "32:"+to_string(h119)+ "de" "e" + "l" "i120e" "32:"+to_string(h120)+ "de" "e" + "l" "i121e" "32:"+to_string(h121)+ "de" "e" + "l" "i122e" "32:"+to_string(h122)+ "de" "e" "e" "1:=" "d" "4:int0" "1:-" "4:int1" "0:" "4:int2" "0:" "e" - "e"_bytes; + "e"); // clang-format on const auto h123 = "d9398c597b058ac7e28e3febb76ed68eb8c5b6c369610562ab5f2b596775d73c"_hexbytes; @@ -558,24 +555,24 @@ TEST_CASE("config message example 1", "[config][example]") { CHECK(printable(m118.serialize()) == printable(m118_expected)); - CHECK(view_hex(m118.hash()) == to_hex(blake2b(m118_expected))); + CHECK(to_hex(m118.hash()) == to_hex(blake2b(m118_expected))); // Increment 5 times so that our diffs will be empty. auto m123 = m118.increment(); CHECK(m123.seqno() == 119); - CHECK(view_hex(m123.hash()) == to_hex(h119)); + CHECK(to_hex(m123.hash()) == to_hex(h119)); m123 = m123.increment(); CHECK(m123.seqno() == 120); - CHECK(view_hex(m123.hash()) == to_hex(h120)); + CHECK(to_hex(m123.hash()) == to_hex(h120)); m123 = m123.increment(); CHECK(m123.seqno() == 121); - CHECK(view_hex(m123.hash()) == to_hex(h121)); + CHECK(to_hex(m123.hash()) == to_hex(h121)); m123 = m123.increment(); CHECK(m123.seqno() == 122); - CHECK(view_hex(m123.hash()) == to_hex(h122)); + CHECK(to_hex(m123.hash()) == to_hex(h122)); m123 = m123.increment(); @@ -591,7 +588,7 @@ TEST_CASE("config message deserialization", "[config][deserialization]") { ConfigMessage m{m123_expected}; CHECK(m.seqno() == 123); - CHECK(view_hex(m.hash()) == to_hex(h123)); + CHECK(to_hex(m.hash()) == to_hex(h123)); CHECK(m.diff() == oxenc::bt_dict{ {"int0"s, "-"s}, {"int1"s, ""s}, @@ -609,7 +606,7 @@ TEST_CASE("config message deserialization", "[config][deserialization]") { // increment() MutableConfigMessage mut{m123_expected}; CHECK(mut.seqno() == 124); - CHECK(view_hex(mut.hash()) == "3ea36410cf7086ce816eb193b0c94e88632abfb75771d82f8ddb3a909124c580"); + CHECK(to_hex(mut.hash()) == "3ea36410cf7086ce816eb193b0c94e88632abfb75771d82f8ddb3a909124c580"); CHECK(mut.diff() == oxenc::bt_dict{}); CHECK_FALSE(mut.merged()); CHECK_FALSE(mut.verified_signature()); @@ -642,12 +639,12 @@ TEST_CASE("config message deserialization", "[config][deserialization]") { "7:string2" "7:goodbye" "e" "1:<" "l" - "l" "i120e" "32:"_bytes+h120+ "de" "e" - "l" "i121e" "32:"_bytes+h121+ "de" "e" - "l" "i122e" "32:"_bytes+h122+ "de" "e" + "l" "i120e" "32:"+to_string(h120)+ "de" "e" + "l" "i121e" "32:"+to_string(h121)+ "de" "e" + "l" "i122e" "32:"+to_string(h122)+ "de" "e" "l" "i123e" - "32:"_bytes+h123+ + "32:"+to_string(h123)+ "d" "4:int0" "1:-" "4:int1" "0:" @@ -656,7 +653,7 @@ TEST_CASE("config message deserialization", "[config][deserialization]") { "e" "e" "1:=" "de" - "e"_bytes)); + "e")); // clang-format on } @@ -757,12 +754,12 @@ TEST_CASE("config message example 2", "[config][example]") { "7:string3" "3:omg" "e" "1:<" "l" - "l" "i120e" "32:"_bytes+h120+ "de" "e" - "l" "i121e" "32:"_bytes+h121+ "de" "e" - "l" "i122e" "32:"_bytes+h122+ "de" "e" + "l" "i120e" "32:"+to_string(h120)+ "de" "e" + "l" "i121e" "32:"+to_string(h121)+ "de" "e" + "l" "i122e" "32:"+to_string(h122)+ "de" "e" "l" "i123e" - "32:"_bytes+blake2b(m123_expected)+ + "32:"+to_string(blake2b(m123_expected))+ "d" "4:int0" "1:-" "4:int1" "0:" @@ -802,17 +799,17 @@ TEST_CASE("config message example 2", "[config][example]") { "7:string2" "0:" "7:string3" "0:" "e" - "e"_bytes)); + "e")); // clang-format on - CHECK(view_hex(m.hash()) == to_hex(h124)); + CHECK(to_hex(m.hash()) == to_hex(h124)); } const auto h125a = "80f229c3667de6d0fa6f96b53118e097fbda82db3ca1aea221a3db91ea9c45fb"_hexbytes; const auto h125b = "ab12f0efe9a9ed00db6b17b44ae0ff36b9f49094077fb114f415522f2a0e98de"_hexbytes; // clang-format off -const auto m126_expected = +const auto m126_expected = to_vector( "d" "1:#" "i126e" "1:&" "d" @@ -836,10 +833,10 @@ const auto m126_expected = "7:string3" "3:omg" "e" "1:<" "l" - "l" "i122e" "32:"_bytes+h122+ "de" "e" + "l" "i122e" "32:"+to_string(h122)+ "de" "e" "l" "i123e" - "32:"_bytes+h123+ + "32:"+to_string(h123)+ "d" "4:int0" "1:-" "4:int1" "0:" @@ -848,7 +845,7 @@ const auto m126_expected = "e" "l" "i124e" - "32:"_bytes+h124+ + "32:"+to_string(h124)+ "d" "5:dictA" "d" "7:goodbye" "l" "l" "i123e" "i456e" "e" "le" "e" @@ -884,7 +881,7 @@ const auto m126_expected = "e" "l" "i125e" - "32:"_bytes+h125a+ + "32:"+to_string(h125a)+ "d" "5:dictB" "d" "3:foo" "1:-" @@ -893,12 +890,12 @@ const auto m126_expected = "e" "l" "i125e" - "32:"_bytes+h125b+ + "32:"+to_string(h125b)+ "d" "4:int1" "0:" "e" "e" "e" "1:=" "de" - "e"_bytes; + "e"); // clang-format on TEST_CASE("config message example 3 - simple conflict", "[config][example][conflict]") { @@ -908,7 +905,7 @@ TEST_CASE("config message example 3 - simple conflict", "[config][example][confl updates_124(m124); - REQUIRE(view_hex(m124.hash()) == to_hex(h124)); + REQUIRE(to_hex(m124.hash()) == to_hex(h124)); auto m125_a = m124.increment(); REQUIRE(m125_a.seqno() == 125); @@ -918,8 +915,8 @@ TEST_CASE("config message example 3 - simple conflict", "[config][example][confl REQUIRE(m125_b.seqno() == 125); m125_b.data()["int1"] = 5; - REQUIRE(view_hex(m125_a.hash()) == to_hex(h125a)); - REQUIRE(view_hex(m125_b.hash()) == to_hex(h125b)); + REQUIRE(to_hex(m125_a.hash()) == to_hex(h125a)); + REQUIRE(to_hex(m125_b.hash()) == to_hex(h125b)); REQUIRE(m125_a.hash() < m125_b.hash()); ConfigMessage m{{m125_a.serialize(), m125_b.serialize()}}; @@ -973,7 +970,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex auto m124b = m123.increment(); updates_124(m124b); - REQUIRE(view_hex(m124b.hash()) == to_hex(h124)); + REQUIRE(to_hex(m124b.hash()) == to_hex(h124)); auto m125a = m124b.increment(); d(m125a.data()["dictB"]).erase("foo"); @@ -1049,7 +1046,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex "1:<" "l" "l" "i123e" - "32:"_bytes+h123+ + "32:"+to_string(h123)+ "d" "4:int0" "1:-" "4:int1" "0:" @@ -1058,7 +1055,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex "e" "l" "i124e" - "32:"_bytes+ustring{view(m124a.hash())}+ + "32:"+to_string(m124a.hash())+ "d" "5:dictB" "d" "6:answer" "0:" @@ -1068,7 +1065,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex "e" "l" "i124e" - "32:"_bytes+h124+ + "32:"+to_string(h124)+ "d" "5:dictA" "d" "7:goodbye" "l" "l" "i123e" "i456e" "e" "le" "e" @@ -1104,7 +1101,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex "e" "l" "i125e" - "32:"_bytes+h125a+ + "32:"+to_string(h125a)+ "d" "5:dictB" "d" "3:foo" "1:-" @@ -1113,18 +1110,18 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex "e" "l" "i125e" - "32:"_bytes+h125b+ + "32:"+to_string(h125b)+ "d" "4:int1" "0:" "e" "e" - "l" "i126e" "32:"_bytes+ustring{view(m126a.hash())}+ "de" "e" - "l" "i126e" "32:"_bytes+ustring{view(m126b.hash())}+ "de" "e" + "l" "i126e" "32:"+to_string(m126a.hash())+ "de" "e" + "l" "i126e" "32:"+to_string(m126b.hash())+ "de" "e" "e" "1:=" "d" "5:dictA" "d" "7:goodbye" "l" "li789ee" "le" "e" "e" "e" - "e"_bytes)); + "e")); // clang-format on ConfigMessage m_alt1{{m127.serialize(), m125a.serialize(), m126b.serialize()}}; diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp index 275e8c7b..14323dd2 100644 --- a/tests/test_curve25519.cpp +++ b/tests/test_curve25519.cpp @@ -23,8 +23,8 @@ TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - auto x_pk1 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk1)); - auto x_pk2 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk2)); + auto x_pk1 = curve25519::to_curve25519_pubkey(to_span(ed_pk1)); + auto x_pk2 = curve25519::to_curve25519_pubkey(to_span(ed_pk2)); CHECK(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); @@ -41,8 +41,8 @@ TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; - auto x_sk1 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk1)); - auto x_sk2 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk2)); + auto x_sk1 = curve25519::to_curve25519_seckey(to_span(ed_sk1)); + auto x_sk2 = curve25519::to_curve25519_seckey(to_span(ed_sk2)); CHECK(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index 31ecb520..fa715b27 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -25,9 +25,9 @@ TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { auto ed_seed2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; auto ed_seed_invalid = "010203040506070809"_hexbytes; - auto kp1 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed1)); - auto kp2 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed2)); - CHECK_THROWS(session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed_invalid))); + auto kp1 = session::ed25519::ed25519_key_pair(to_span(ed_seed1)); + auto kp2 = session::ed25519::ed25519_key_pair(to_span(ed_seed2)); + CHECK_THROWS(session::ed25519::ed25519_key_pair(to_span(ed_seed_invalid))); CHECK(kp1.first.size() == 32); CHECK(kp1.second.size() == 64); @@ -57,9 +57,9 @@ TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; auto ed_sk_invalid = "010203040506070809"_hexbytes; - auto seed1 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk1)); - auto seed2 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk2)); - CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk_invalid))); + auto seed1 = session::ed25519::seed_for_ed_privkey(to_span(ed_sk1)); + auto seed2 = session::ed25519::seed_for_ed_privkey(to_span(ed_sk2)); + CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_span(ed_sk_invalid))); CHECK(oxenc::to_hex(seed1.begin(), seed1.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -74,15 +74,15 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { auto ed_pk = "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; auto ed_invalid = "010203040506070809"_hexbytes; - auto sig1 = session::ed25519::sign(to_unsigned_sv(ed_seed), to_unsigned_sv("hello")); - CHECK_THROWS(session::ed25519::sign(to_unsigned_sv(ed_invalid), to_unsigned_sv("hello"))); + auto sig1 = session::ed25519::sign(to_span(ed_seed), to_span("hello")); + CHECK_THROWS(session::ed25519::sign(to_span(ed_invalid), to_span("hello"))); auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; CHECK(oxenc::to_hex(sig1.begin(), sig1.end()) == expected_sig_hex); - CHECK(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); - CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_unsigned_sv("hello"))); - CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_unsigned_sv("hello"))); + CHECK(session::ed25519::verify(sig1, ed_pk, to_span("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_span("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_span("hello"))); } diff --git a/tests/test_encrypt.cpp b/tests/test_encrypt.cpp index 56cca3c3..a094a0a4 100644 --- a/tests/test_encrypt.cpp +++ b/tests/test_encrypt.cpp @@ -27,9 +27,9 @@ TEST_CASE("config message encryption", "[config][encrypt]") { CHECK(to_hex(enc2) != to_hex(enc1)); auto enc3 = config::encrypt(message1, key2, "test-suite1"); CHECK(to_hex(enc3) != to_hex(enc1)); - auto nonce = enc1.substr(enc1.size() - 24); - auto nonce2 = enc2.substr(enc2.size() - 24); - auto nonce3 = enc3.substr(enc3.size() - 24); + auto nonce = std::vector{enc1.begin() + (enc1.size() - 24), enc1.end()}; + auto nonce2 = std::vector{enc2.begin() + (enc2.size() - 24), enc2.end()}; + auto nonce3 = std::vector{enc3.begin() + (enc3.size() - 24), enc3.end()}; CHECK(to_hex(nonce) == "af2f4860cb4d0f8ba7e09d29e31f5e4a18f65847287a54a0"); CHECK(to_hex(nonce2) == "277e639d36ba46470dfff509a68cb73d9a96386c51739bdd"); CHECK(to_hex(nonce3) == to_hex(nonce)); diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 3eb38106..0ee6128b 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "utils.hpp" @@ -28,10 +29,10 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector enc_keys{ + std::vector> enc_keys{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; - groups::Info ginfo1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. @@ -43,7 +44,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Info ginfo2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. ginfo2.add_key(k, false); @@ -63,7 +64,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.needs_dump()); CHECK_FALSE(ginfo1.needs_push()); - std::vector> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", p1); CHECK(ginfo2.merge(merge_configs) == std::vector{{"fakehash1"s}}); CHECK_FALSE(ginfo2.needs_push()); @@ -154,17 +155,17 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector enc_keys1; + std::vector> enc_keys1; enc_keys1.push_back( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); - std::vector enc_keys2; + std::vector> enc_keys2; enc_keys2.push_back( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys2.push_back( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); // This Info object has only the public key, not the priv key, and so cannot modify things: - groups::Info ginfo{to_usv(ed_pk), std::nullopt, std::nullopt}; + groups::Info ginfo{session::to_span(ed_pk), std::nullopt, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo.add_key(k, false); @@ -176,7 +177,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: - groups::Info ginfo_rw{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw.add_key(k, false); @@ -194,12 +195,12 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo_rw.needs_dump()); CHECK_FALSE(ginfo_rw.needs_push()); - std::vector> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", to_push); CHECK(ginfo.merge(merge_configs) == std::vector{{"fakehash1"s}}); CHECK_FALSE(ginfo.needs_push()); - groups::Info ginfo_rw2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw2.add_key(k, false); @@ -226,13 +227,13 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { ed_sk_bad1.data(), reinterpret_cast(seed_bad1.data())); - groups::Info ginfo_bad1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_bad1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_bad1.add_key(k, false); ginfo_bad1.merge(merge_configs); - ginfo_bad1.set_sig_keys(to_usv(ed_sk_bad1)); + ginfo_bad1.set_sig_keys(session::to_span(ed_sk_bad1)); ginfo_bad1.set_name("Bad name, BAD!"); auto [s_bad, p_bad, o_bad] = ginfo_bad1.push(); @@ -309,7 +310,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.needs_dump()); auto dump = ginfo.dump(); - groups::Info ginfo2{to_usv(ed_pk), std::nullopt, dump}; + groups::Info ginfo2{session::to_span(ed_pk), std::nullopt, dump}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo2.add_key(k, false); @@ -327,7 +328,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(o5.empty()); // This account has a different primary decryption key - groups::Info ginfo_rw3{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Info ginfo_rw3{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys2) // Just for testing, as above. ginfo_rw3.add_key(k, false); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 580772dc..22a08e46 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -25,14 +25,14 @@ using namespace oxenc::literals; using namespace session::config; -static std::array sk_from_seed(ustring_view seed) { +static std::array sk_from_seed(std::span seed) { std::array ignore; std::array sk; crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); return sk; } -static std::string session_id_from_ed(ustring_view ed_pk) { +static std::string session_id_from_ed(std::span ed_pk) { std::string sid; std::array xpk; int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); @@ -54,7 +54,7 @@ struct hacky_list : std::list { struct pseudo_client { std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; + const std::span public_key{secret_key.data() + 32, 32}; std::string session_id{session_id_from_ed(public_key)}; groups::Info info; @@ -62,23 +62,26 @@ struct pseudo_client { groups::Keys keys; pseudo_client( - ustring_view seed, + std::span seed, bool admin, const unsigned char* gpk, std::optional gsk, - std::optional info_dump = std::nullopt, - std::optional members_dump = std::nullopt, - std::optional keys_dump = std::nullopt) : + std::optional> info_dump = std::nullopt, + std::optional> members_dump = std::nullopt, + std::optional> keys_dump = std::nullopt) : secret_key{sk_from_seed(seed)}, - info{ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + info{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, info_dump}, - members{ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + members{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, members_dump}, - keys{to_usv(secret_key), - ustring_view{gpk, 32}, - admin ? std::make_optional({*gsk, 64}) : std::nullopt, + keys{session::to_span(secret_key), + std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) + : std::nullopt, keys_dump, info, members} { @@ -89,11 +92,11 @@ struct pseudo_client { TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { - const ustring group_seed = + const std::vector group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const ustring admin1_seed = + const std::vector admin1_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const ustring admin2_seed = + const std::vector admin2_seed = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; const std::array member_seeds = { "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 @@ -142,8 +145,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (const auto& m : members) REQUIRE(m.members.size() == 0); - std::vector> info_configs; - std::vector> mem_configs; + std::vector>> info_configs; + std::vector>> mem_configs; // add admin account, re-key, distribute auto& admin1 = admins[0]; @@ -288,11 +291,11 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(admin1.members.needs_push()); - ustring old_key{admin1.keys.group_enc_key()}; + std::vector old_key = session::to_vector(admin1.keys.group_enc_key()); auto new_keys_config4 = admin1.keys.rekey(admin1.info, admin1.members); CHECK(not new_keys_config4.empty()); - CHECK(old_key != admin1.keys.group_enc_key()); + CHECK(old_key != session::to_vector(admin1.keys.group_enc_key())); auto [iseq4, new_info_config4, iobs4] = admin1.info.push(); admin1.info.confirm_pushed(iseq4, "fakehash4"); @@ -344,9 +347,9 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (int i = 0; i < 5; ++i) msg += msg; - auto compressed = admin1.keys.encrypt_message(to_usv(msg)); + auto compressed = admin1.keys.encrypt_message(session::to_span(msg)); CHECK(compressed.size() == 256); - auto uncompressed = admin1.keys.encrypt_message(to_usv(msg), false); + auto uncompressed = admin1.keys.encrypt_message(session::to_span(msg), false); CHECK(uncompressed.size() == 2048); CHECK(compressed.size() < msg.size()); @@ -420,14 +423,14 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash5"s}}); } - std::pair decrypted1, decrypted2; + std::pair> decrypted1, decrypted2; REQUIRE_NOTHROW(decrypted1 = members.back().keys.decrypt_message(compressed)); CHECK(decrypted1.first == admin1.session_id); - CHECK(to_sv(decrypted1.second) == msg); + CHECK(session::to_string(decrypted1.second) == msg); REQUIRE_NOTHROW(decrypted2 = members.back().keys.decrypt_message(uncompressed)); CHECK(decrypted2.first == admin1.session_id); - CHECK(to_sv(decrypted2.second) == msg); + CHECK(session::to_string(decrypted2.second) == msg); auto bad_compressed = compressed; bad_compressed.back() ^= 0b100; @@ -458,7 +461,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // get dropped as stale. info_configs.clear(); mem_configs.clear(); - ustring new_keys_config6{admin1.keys.rekey(admin1.info, admin1.members)}; + std::vector new_keys_config6 = + session::to_vector(admin1.keys.rekey(admin1.info, admin1.members)); auto [iseq6, ipush6, iobs6] = admin1.info.push(); info_configs.emplace_back("ifakehash6", ipush6); admin1.info.confirm_pushed(iseq6, "ifakehash6"); @@ -485,10 +489,12 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { "keyhash6"s}}); } - ustring new_keys_config7{admin1.keys.rekey(admin1.info, admin1.members)}; + std::vector new_keys_config7 = + session::to_vector(admin1.keys.rekey(admin1.info, admin1.members)); // Make sure we can encrypt & decrypt even if the rekey is still pending: - CHECK_NOTHROW(admin1.keys.decrypt_message(admin1.keys.encrypt_message(to_usv("abc")))); + CHECK_NOTHROW( + admin1.keys.decrypt_message(admin1.keys.encrypt_message(session::to_span("abc")))); auto [iseq7, ipush7, iobs7] = admin1.info.push(); info_configs.emplace_back("ifakehash7", ipush7); @@ -560,7 +566,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { struct pseudo_client { std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; + const std::span public_key{secret_key.data() + 32, 32}; std::string session_id{session_id_from_ed(public_key)}; config_group_keys* keys; @@ -568,7 +574,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { config_object* members; pseudo_client( - ustring seed, + std::vector seed, bool is_admin, unsigned char* gpk, std::optional gsk) : @@ -601,11 +607,11 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { } }; - const ustring group_seed = + const std::vector group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const ustring admin1_seed = + const std::vector admin1_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const ustring admin2_seed = + const std::vector admin2_seed = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; const std::array member_seeds = { "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 @@ -803,11 +809,11 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { - const ustring group_seed = + const std::vector group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const ustring admin_seed = + const std::vector admin_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const ustring member_seed = + const std::vector member_seed = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; std::array group_pk; @@ -843,7 +849,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") session::config::UserGroups member_gr2{member_seed, std::nullopt}; auto [seqno, push, obs] = member_groups.push(); - std::vector> gr_conf; + std::vector>> gr_conf; gr_conf.emplace_back("fakehash1", push); member_gr2.merge(gr_conf); @@ -853,7 +859,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") CHECK(g->id == member.info.id); CHECK(g->auth_data == auth_data); - auto to_sign = to_usv("retrieve9991693340111000"); + auto to_sign = session::to_span("retrieve9991693340111000"); auto subauth_b64 = member.keys.swarm_subaccount_sign(to_sign, auth_data); CHECK(subauth_b64.subaccount == "AwMAAIWvMR2nJXCFnK5+hNahNecWqMC39/TVVLjaR3imNug5"); @@ -878,7 +884,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") CHECK(member.keys.swarm_verify_subaccount(auth_data)); CHECK(session::config::groups::Keys::swarm_verify_subaccount( - member.info.id, to_usv(member.secret_key), auth_data)); + member.info.id, session::to_span(member.secret_key), auth_data)); // Try flipping a bit in each position of the auth data and make sure it fails to validate: for (size_t i = 0; i < auth_data.size(); i++) { @@ -890,18 +896,18 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") auto auth_data2 = auth_data; auth_data2[i] ^= 1 << b; CHECK_FALSE(session::config::groups::Keys::swarm_verify_subaccount( - member.info.id, to_usv(member.secret_key), auth_data2)); + member.info.id, session::to_span(member.secret_key), auth_data2)); } } } TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { - const ustring group_seed = + const std::vector group_seed = "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const ustring admin1_seed = + const std::vector admin1_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const ustring member1_seed = + const std::vector member1_seed = "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes; std::array group_pk; @@ -914,7 +920,7 @@ TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { pseudo_client admin{admin1_seed, true, group_pk.data(), group_sk.data()}; pseudo_client member{member1_seed, false, group_pk.data(), std::nullopt}; - std::vector> configs; + std::vector>> configs; { auto m = admin.members.get_or_construct(admin.session_id); m.admin = true; @@ -966,7 +972,7 @@ TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { REQUIRE(member.info.is_readonly()); REQUIRE(member.members.is_readonly()); - member.keys.load_admin_key(to_usv(group_sk), member.info, member.members); + member.keys.load_admin_key(session::to_span(group_sk), member.info, member.members); CHECK(member.keys.admin()); CHECK_FALSE(member.members.is_readonly()); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index d5b566ff..ee6159b7 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -37,10 +37,10 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector enc_keys{ + std::vector> enc_keys{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; - groups::Members gmem1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Members gmem1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. @@ -52,7 +52,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Members gmem2{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Members gmem2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. gmem2.add_key(k, false); @@ -101,7 +101,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(gmem1.needs_dump()); CHECK_FALSE(gmem1.needs_push()); - std::vector> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", p1); CHECK(gmem2.merge(merge_configs) == std::vector{{"fakehash1"}}); CHECK_FALSE(gmem2.needs_push()); @@ -346,7 +346,7 @@ TEST_CASE("Group Members restores extra data", "[config][groups][members]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - groups::Members gmem1{to_usv(ed_pk), to_usv(ed_sk), std::nullopt}; + groups::Members gmem1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; auto memberId1 = "050000000000000000000000000000000000000000000000000000000000000000"; auto memberId2 = "051111111111111111111111111111111111111111111111111111111111111111"; @@ -365,7 +365,7 @@ TEST_CASE("Group Members restores extra data", "[config][groups][members]") { auto dumped = gmem1.dump(); - groups::Members gmem2{to_usv(ed_pk), to_usv(ed_sk), dumped}; + groups::Members gmem2{session::to_span(ed_pk), session::to_span(ed_sk), dumped}; CHECK(gmem2.get_status(gmem1.get_or_construct(memberId1)) == groups::member::Status::invite_sending); diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index 626cd57d..ab1e2bc1 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -2,22 +2,27 @@ #include "session/hash.h" #include "session/hash.hpp" +#include "session/util.hpp" #include "utils.hpp" TEST_CASE("Hash generation", "[hash][hash]") { - auto hash1 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); - auto hash2 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); - auto hash3 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); - auto hash4 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); - auto hash5 = session::hash::hash(64, to_usv("TestMessage"), std::nullopt); - auto hash6 = session::hash::hash(64, to_usv("TestMessage"), to_usv("TestKey")); - CHECK_THROWS(session::hash::hash(10, to_usv("TestMessage"), std::nullopt)); - CHECK_THROWS(session::hash::hash(100, to_usv("TestMessage"), std::nullopt)); + auto hash1 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); + auto hash2 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); + auto hash3 = + session::hash::hash(32, session::to_span("TestMessage"), session::to_span("TestKey")); + auto hash4 = + session::hash::hash(32, session::to_span("TestMessage"), session::to_span("TestKey")); + auto hash5 = session::hash::hash(64, session::to_span("TestMessage"), std::nullopt); + auto hash6 = + session::hash::hash(64, session::to_span("TestMessage"), session::to_span("TestKey")); + CHECK_THROWS(session::hash::hash(10, session::to_span("TestMessage"), std::nullopt)); + CHECK_THROWS(session::hash::hash(100, session::to_span("TestMessage"), std::nullopt)); CHECK_THROWS(session::hash::hash( 32, - to_usv("TestMessage"), - to_usv("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLon" - "g"))); + session::to_span("TestMessage"), + session::to_span("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyTh" + "atIsTooLon" + "g"))); CHECK(hash1.size() == 32); CHECK(hash2.size() == 32); @@ -30,14 +35,10 @@ TEST_CASE("Hash generation", "[hash][hash]") { CHECK(hash3 == hash4); CHECK(hash1 != hash5); CHECK(hash3 != hash6); - CHECK(oxenc::to_hex(hash1) == - "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); - CHECK(oxenc::to_hex(hash2) == - "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); - CHECK(oxenc::to_hex(hash3) == - "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); - CHECK(oxenc::to_hex(hash4) == - "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(to_hex(hash1) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(to_hex(hash2) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(to_hex(hash3) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(to_hex(hash4) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); auto expected_hash5 = "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a39c02db69c44" @@ -45,6 +46,6 @@ TEST_CASE("Hash generation", "[hash][hash]") { auto expected_hash6 = "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e877874c9059edf5" "3d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"; - CHECK(oxenc::to_hex(hash5) == expected_hash5); - CHECK(oxenc::to_hex(hash6) == expected_hash6); + CHECK(to_hex(hash5) == expected_hash5); + CHECK(to_hex(hash6) == expected_hash6); } diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index bea03acb..a807bd8a 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -113,7 +113,7 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); CHECK(simple_logs.size() >= 2); - //CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); + // CHECK(simple_logs == std::vector{"uncomment me to fail showing all log lines"}); #if defined(__APPLE__) && defined(__clang__) && defined(RELEASE_BUILD) CHECK(simple_logs.front().find("libevent loop is started") != std::string::npos); #else diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index c84cb1dc..87d8cb3a 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -14,7 +14,7 @@ using namespace oxenc::literals; using x_pair = std::pair, std::array>; // Returns X25519 privkey, pubkey from an Ed25519 seed -x_pair to_x_keys(ustring_view ed_seed) { +x_pair to_x_keys(std::span ed_seed) { std::array ed_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed_seed.data()); @@ -39,73 +39,75 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); - CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[0].second)) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(to_usv(x_keys[1].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[1].second)) == "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); - CHECK(oxenc::to_hex(to_usv(x_keys[2].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[2].second)) == "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); - CHECK(oxenc::to_hex(to_usv(x_keys[3].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[3].second)) == "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); - CHECK(oxenc::to_hex(to_usv(x_keys[4].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[4].second)) == "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; - std::vector recipients; + std::vector> recipients; for (auto& [_, pubkey] : x_keys) recipients.emplace_back(pubkey.data(), pubkey.size()); std::vector msgs{{"hello", "cruel", "world"}}; - std::vector encrypted; + std::vector> encrypted; session::encrypt_for_multiple( msgs[0], session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite", - [&](ustring_view enc) { encrypted.emplace_back(enc); }); + [&](std::span enc) { + encrypted.emplace_back(session::to_vector(enc)); + }); REQUIRE(encrypted.size() == 3); - CHECK(oxenc::to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); - CHECK(oxenc::to_hex(encrypted[1]) == "b7a15bcd9f7b09445defcae2f1dc5085dd75cb085b"); - CHECK(oxenc::to_hex(encrypted[2]) == "01c4fc2156327735f3fb5063b11ea95f6ebcc5b6cc"); + CHECK(to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); + CHECK(to_hex(encrypted[1]) == "b7a15bcd9f7b09445defcae2f1dc5085dd75cb085b"); + CHECK(to_hex(encrypted[2]) == "01c4fc2156327735f3fb5063b11ea95f6ebcc5b6cc"); auto m1 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[1].first), - to_usv(x_keys[1].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[1].first), + session::to_span(x_keys[1].second), + session::to_span(x_keys[0].second), "test suite"); auto m2 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[2].first), - to_usv(x_keys[2].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[2].first), + session::to_span(x_keys[2].second), + session::to_span(x_keys[0].second), "test suite"); auto m3 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "test suite"); auto m3b = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "not test suite"); auto m4 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[4].first), - to_usv(x_keys[4].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[4].first), + session::to_span(x_keys[4].second), + session::to_span(x_keys[0].second), "test suite"); REQUIRE(m1); @@ -114,59 +116,61 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { CHECK_FALSE(m3b); CHECK_FALSE(m4); - CHECK(to_sv(*m1) == "hello"); - CHECK(to_sv(*m2) == "hello"); - CHECK(to_sv(*m3) == "hello"); + CHECK(session::to_string(*m1) == "hello"); + CHECK(session::to_string(*m2) == "hello"); + CHECK(session::to_string(*m3) == "hello"); encrypted.clear(); session::encrypt_for_multiple( session::to_view_vector(msgs.begin(), msgs.end()), session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite", - [&](ustring_view enc) { encrypted.emplace_back(enc); }); + [&](std::span enc) { + encrypted.emplace_back(session::to_vector(enc)); + }); REQUIRE(encrypted.size() == 3); - CHECK(oxenc::to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); - CHECK(oxenc::to_hex(encrypted[1]) == "bcb642c49c6da03f70cdaab2ed6666721318afd631"); - CHECK(oxenc::to_hex(encrypted[2]) == "1ecee2215d226817edfdb097f05037eb799309103a"); + CHECK(to_hex(encrypted[0]) == "e64937e5ea201b84f4e88a976dad900d91caaf6a17"); + CHECK(to_hex(encrypted[1]) == "bcb642c49c6da03f70cdaab2ed6666721318afd631"); + CHECK(to_hex(encrypted[2]) == "1ecee2215d226817edfdb097f05037eb799309103a"); m1 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[1].first), - to_usv(x_keys[1].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[1].first), + session::to_span(x_keys[1].second), + session::to_span(x_keys[0].second), "test suite"); m2 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[2].first), - to_usv(x_keys[2].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[2].first), + session::to_span(x_keys[2].second), + session::to_span(x_keys[0].second), "test suite"); m3 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "test suite"); m3b = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "not test suite"); m4 = session::decrypt_for_multiple( session::to_view_vector(encrypted), nonce, - to_usv(x_keys[4].first), - to_usv(x_keys[4].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[4].first), + session::to_span(x_keys[4].second), + session::to_span(x_keys[0].second), "test suite"); REQUIRE(m1); @@ -175,19 +179,21 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { CHECK_FALSE(m3b); CHECK_FALSE(m4); - CHECK(to_sv(*m1) == "hello"); - CHECK(to_sv(*m2) == "cruel"); - CHECK(to_sv(*m3) == "world"); + CHECK(session::to_string(*m1) == "hello"); + CHECK(session::to_string(*m2) == "cruel"); + CHECK(session::to_string(*m3) == "world"); // Mismatch messages & recipients size throws: CHECK_THROWS(session::encrypt_for_multiple( session::to_view_vector(msgs.begin(), std::prev(msgs.end())), session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite", - [&](ustring_view enc) { encrypted.emplace_back(enc); })); + [&](std::span enc) { + encrypted.emplace_back(session::to_vector(enc)); + })); } TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][simple]") { @@ -203,29 +209,29 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); - CHECK(oxenc::to_hex(to_usv(x_keys[0].second)) == + CHECK(to_hex(session::to_span(x_keys[0].second)) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(to_usv(x_keys[1].second)) == + CHECK(to_hex(session::to_span(x_keys[1].second)) == "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); - CHECK(oxenc::to_hex(to_usv(x_keys[2].second)) == + CHECK(to_hex(session::to_span(x_keys[2].second)) == "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); - CHECK(oxenc::to_hex(to_usv(x_keys[3].second)) == + CHECK(to_hex(session::to_span(x_keys[3].second)) == "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); - CHECK(oxenc::to_hex(to_usv(x_keys[4].second)) == + CHECK(to_hex(session::to_span(x_keys[4].second)) == "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; - std::vector recipients; + std::vector> recipients; for (auto& [_, pubkey] : x_keys) recipients.emplace_back(pubkey.data(), pubkey.size()); std::vector msgs{{"hello", "cruel", "world"}}; - ustring encrypted = session::encrypt_for_multiple_simple( + std::vector encrypted = session::encrypt_for_multiple_simple( msgs[0], session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite"); REQUIRE(encrypted.size() == @@ -240,39 +246,39 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim msgs[0], session::to_view_vector( std::next(recipients.begin()), std::prev(recipients.end())), - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite")); auto m1 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[1].first), - to_usv(x_keys[1].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[1].first), + session::to_span(x_keys[1].second), + session::to_span(x_keys[0].second), "test suite"); auto m2 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[2].first), - to_usv(x_keys[2].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[2].first), + session::to_span(x_keys[2].second), + session::to_span(x_keys[0].second), "test suite"); auto m3 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "test suite"); auto m3b = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "not test suite"); auto m4 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[4].first), - to_usv(x_keys[4].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[4].first), + session::to_span(x_keys[4].second), + session::to_span(x_keys[0].second), "test suite"); REQUIRE(m1); @@ -281,15 +287,15 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim CHECK_FALSE(m3b); CHECK_FALSE(m4); - CHECK(to_sv(*m1) == "hello"); - CHECK(to_sv(*m2) == "hello"); - CHECK(to_sv(*m3) == "hello"); + CHECK(session::to_string(*m1) == "hello"); + CHECK(session::to_string(*m2) == "hello"); + CHECK(session::to_string(*m3) == "hello"); encrypted = session::encrypt_for_multiple_simple( session::to_view_vector(msgs), session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite", nonce); @@ -305,33 +311,33 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim m1 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[1].first), - to_usv(x_keys[1].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[1].first), + session::to_span(x_keys[1].second), + session::to_span(x_keys[0].second), "test suite"); m2 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[2].first), - to_usv(x_keys[2].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[2].first), + session::to_span(x_keys[2].second), + session::to_span(x_keys[0].second), "test suite"); m3 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "test suite"); m3b = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[3].first), - to_usv(x_keys[3].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[3].first), + session::to_span(x_keys[3].second), + session::to_span(x_keys[0].second), "not test suite"); m4 = session::decrypt_for_multiple_simple( encrypted, - to_usv(x_keys[4].first), - to_usv(x_keys[4].second), - to_usv(x_keys[0].second), + session::to_span(x_keys[4].first), + session::to_span(x_keys[4].second), + session::to_span(x_keys[0].second), "test suite"); REQUIRE(m1); @@ -340,14 +346,14 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim CHECK_FALSE(m3b); CHECK_FALSE(m4); - CHECK(to_sv(*m1) == "hello"); - CHECK(to_sv(*m2) == "cruel"); - CHECK(to_sv(*m3) == "world"); + CHECK(session::to_string(*m1) == "hello"); + CHECK(session::to_string(*m2) == "cruel"); + CHECK(session::to_string(*m3) == "world"); CHECK_THROWS(session::encrypt_for_multiple_simple( session::to_view_vector(msgs.begin(), std::prev(msgs.end())), session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - to_usv(x_keys[0].first), - to_usv(x_keys[0].second), + session::to_span(x_keys[0].first), + session::to_span(x_keys[0].second), "test suite")); } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index db7a3c23..36efcf04 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1,9 +1,9 @@ +#include #include #include #include #include -#include #include #include #include @@ -24,7 +24,8 @@ struct Result { std::optional response; }; -service_node test_node(const ustring ed_pk, const uint16_t index, const bool unique_ip = true) { +service_node test_node( + const std::vector ed_pk, const uint16_t index, const bool unique_ip = true) { return service_node{ ed_pk, {2, 8, 0}, @@ -1099,7 +1100,7 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { network.emplace(std::nullopt, true, true, false); network->send_onion_request( test_service_node, - str_to_vec("{\"method\":\"info\",\"params\":{}}"), + to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, bool, @@ -1116,7 +1117,7 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { network->ignore_calls_to("build_path"); network->send_onion_request( test_service_node, - str_to_vec("{\"method\":\"info\",\"params\":{}}"), + to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, bool, @@ -1133,7 +1134,7 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { network->ignore_calls_to("build_path"); network->send_onion_request( test_service_node, - str_to_vec("{\"method\":\"info\",\"params\":{}}"), + to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [&prom](bool success, bool timeout, @@ -1174,7 +1175,7 @@ TEST_CASE("Network requests", "[network][send_request]") { network.send_request( request_info::make( test_service_node, - str_to_vec("{}"), + to_vector("{}"), std::nullopt, 3s, std::nullopt, @@ -1219,7 +1220,7 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { network.send_onion_request( test_service_node, - str_to_vec("{\"method\":\"info\",\"params\":{}}"), + to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [&result_promise]( bool success, @@ -1258,7 +1259,7 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { std::strcpy( test_service_node.ed25519_pubkey_hex, "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); - auto body = ustring{to_usv("{\"method\":\"info\",\"params\":{}}")}; + auto body = to_vector("{\"method\":\"info\",\"params\":{}}"); auto result_promise = std::make_shared>(); network_send_onion_request_to_snode_destination( diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 37f3d318..23280bf1 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -10,33 +10,33 @@ using namespace session::onionreq; TEST_CASE("Onion request encryption", "[encryption][onionreq]") { - auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytesv; - auto B = "caea52c5b0c316d85ffb53ea536826618b13dee40685f166f632653114526a78"_hexbytesv; - auto b = "8fcd8ad3a15c76f76f1c56dff0c529999f8c59b4acda79e05666e54d5727dca1"_hexbytesv; + auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytes; + auto B = "caea52c5b0c316d85ffb53ea536826618b13dee40685f166f632653114526a78"_hexbytes; + auto b = "8fcd8ad3a15c76f76f1c56dff0c529999f8c59b4acda79e05666e54d5727dca1"_hexbytes; auto enc_gcm = "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" - ""_hexbytesv; + ""_hexbytes; auto enc_gcm_broken1 = "1eb6ae1cd72f60999486365759bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" - ""_hexbytesv; + ""_hexbytes; auto enc_gcm_broken2 = "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b69" - ""_hexbytesv; + ""_hexbytes; auto enc_xchacha20 = "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b032"_hexbytesv; + "7d1638d765db75de02b032"_hexbytes; auto enc_xchacha20_broken1 = "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b033"_hexbytesv; + "7d1638d765db75de02b033"_hexbytes; auto enc_xchacha20_broken2 = "9e1a3abe60eff3ea5c23556ccfe225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b032"_hexbytesv; + "7d1638d765db75de02b032"_hexbytes; HopEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; - CHECK(vec_to_str(e.decrypt_aesgcm(enc_gcm, x25519_pubkey::from_bytes(A))) == "Hello world"); - CHECK(vec_to_str(e.decrypt_xchacha20(enc_xchacha20, x25519_pubkey::from_bytes(A))) == + CHECK(to_string(e.decrypt_aesgcm(enc_gcm, x25519_pubkey::from_bytes(A))) == "Hello world"); + CHECK(to_string(e.decrypt_xchacha20(enc_xchacha20, x25519_pubkey::from_bytes(A))) == "Hello world"); CHECK_THROWS(e.decrypt_aesgcm(enc_xchacha20_broken1, x25519_pubkey::from_bytes(A))); CHECK_THROWS(e.decrypt_aesgcm(enc_xchacha20_broken2, x25519_pubkey::from_bytes(A))); @@ -46,36 +46,36 @@ TEST_CASE("Onion request encryption", "[encryption][onionreq]") { TEST_CASE("Onion request parser", "[onionreq][parser]") { - auto A = "8167e97672005c669a48858c69895f395ca235219ac3f7a4210022b1f910e652"_hexbytesv; - auto a = "d2ee09e1a557a077d385fcb69a11ffb6909ecdcc8348def3e0e4172c8a1431c1"_hexbytesv; - auto B = "8388de69bc0d4b6196133233ad9a46ba0473474bc67718aad96a3a33c257f726"_hexbytesv; - auto b = "2f4d1c0d28e137777ec0a316e9f4f763e3e66662a6c51994c6315c9ef34b6deb"_hexbytesv; + auto A = "8167e97672005c669a48858c69895f395ca235219ac3f7a4210022b1f910e652"_hexbytes; + auto a = "d2ee09e1a557a077d385fcb69a11ffb6909ecdcc8348def3e0e4172c8a1431c1"_hexbytes; + auto B = "8388de69bc0d4b6196133233ad9a46ba0473474bc67718aad96a3a33c257f726"_hexbytes; + auto b = "2f4d1c0d28e137777ec0a316e9f4f763e3e66662a6c51994c6315c9ef34b6deb"_hexbytes; auto enc_gcm = "270000009525d587d188c92a966eef0e7162bef99a6171a124575b998072a8ee7eb265e0b6f0930ed96504" "7b22656e635f74797065223a20226165732d67636d222c2022657068656d6572616c5f6b6579223a202238" "31363765393736373230303563363639613438383538633639383935663339356361323335323139616333" - "6637613432313030323262316639313065363532227d"_hexbytesv; + "6637613432313030323262316639313065363532227d"_hexbytes; auto enc_xchacha20 = "33000000e440bc244ddcafd947b86fc5a964aa58de54a6d75cc0f0f3840db14b6c1176a8e2e0a04d5fbdf9" "8f23adee1edc8362ab99b10b7b22656e635f74797065223a2022786368616368613230222c202265706865" "6d6572616c5f6b6579223a2022383136376539373637323030356336363961343838353863363938393566" - "33393563613233353231396163336637613432313030323262316639313065363532227d"_hexbytesv; + "33393563613233353231396163336637613432313030323262316639313065363532227d"_hexbytes; OnionReqParser parser_gcm{B, b, enc_gcm}; - CHECK(from_const_unsigned_span(parser_gcm.payload()) == "Hello world"); - CHECK(sp_to_sv(parser_gcm.remote_pubkey()) == vec_to_sv(A)); - auto aes_reply = parser_gcm.encrypt_reply(to_const_unsigned_span("Goodbye world")); + CHECK(to_string(parser_gcm.payload()) == "Hello world"); + CHECK(sp_to_sv(parser_gcm.remote_pubkey()) == to_string_view(A)); + auto aes_reply = parser_gcm.encrypt_reply(to_span("Goodbye world")); CHECK(aes_reply.size() == 12 + 13 + 16); HopEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; - CHECK(vec_to_str(e.decrypt_aesgcm(aes_reply, x25519_pubkey::from_bytes(B))) == "Goodbye world"); + CHECK(to_string(e.decrypt_aesgcm(aes_reply, x25519_pubkey::from_bytes(B))) == "Goodbye world"); OnionReqParser parser_xchacha20{B, b, enc_xchacha20}; - CHECK(from_const_unsigned_span(parser_xchacha20.payload()) == "Hello world"); - CHECK(sp_to_sv(parser_xchacha20.remote_pubkey()) == vec_to_sv(A)); - auto xcha_reply = parser_xchacha20.encrypt_reply(to_const_unsigned_span("Goodbye world")); + CHECK(to_string(parser_xchacha20.payload()) == "Hello world"); + CHECK(sp_to_sv(parser_xchacha20.remote_pubkey()) == to_string_view(A)); + auto xcha_reply = parser_xchacha20.encrypt_reply(to_span("Goodbye world")); CHECK(xcha_reply.size() == 16 + 13 + 24); - CHECK(vec_to_str(e.decrypt_xchacha20(xcha_reply, x25519_pubkey::from_bytes(B))) == + CHECK(to_string(e.decrypt_xchacha20(xcha_reply, x25519_pubkey::from_bytes(B))) == "Goodbye world"); } diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp index 54abccdc..f79706fb 100644 --- a/tests/test_proto.cpp +++ b/tests/test_proto.cpp @@ -19,7 +19,7 @@ const std::vector groups{ const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk_raw; std::array ed_sk_raw; -ustring_view load_seed() { +std::span load_seed() { crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); return {ed_sk_raw.data(), ed_sk_raw.size()}; } @@ -71,7 +71,7 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { for (auto& p : positions) { auto msg_copy = user_profile_msg; - msg_copy.insert(p, addendum); + msg_copy.insert(msg_copy.begin() + p, addendum.begin(), addendum.end()); REQUIRE_THROWS(protos::unwrap_config(ed_sk, msg_copy, Namespace::UserProfile)); } @@ -83,7 +83,7 @@ TEST_CASE("Protobuf old config loading test", "[config][proto][old]") { std::array ed_pk_raw; std::array ed_sk_raw; crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); - ustring_view ed_sk{ed_sk_raw.data(), ed_sk_raw.size()}; + std::span ed_sk{ed_sk_raw.data(), ed_sk_raw.size()}; auto old_conf = "080112c2060a03505554120f2f6170692f76312f6d6573736167651a9f060806120028e1c5a0beaf313801" diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index a15ef826..376ab942 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -25,7 +25,7 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - ustring sid_raw; + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); REQUIRE(sid_raw == @@ -42,24 +42,24 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - ustring sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); REQUIRE(sid_raw2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); SECTION("full secret, prefixed sid") { - auto enc = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); + auto enc = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); + CHECK(to_string(enc) != "hello"); - CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); + CHECK_THROWS(decrypt_incoming(to_span(ed_sk), enc)); - auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc); - CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); - CHECK(from_unsigned_sv(msg) == "hello"); + auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc); + CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(to_string(msg) == "hello"); auto broken = enc; broken[2] ^= 0x02; - CHECK_THROWS(decrypt_incoming(to_sv(ed_sk2), broken)); + CHECK_THROWS(decrypt_incoming(to_span(ed_sk2), broken)); } SECTION("only seed, unprefixed sid") { constexpr auto lorem_ipsum = @@ -69,19 +69,23 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_recipient( - {to_sv(ed_sk).data(), 32}, sid_raw2, to_unsigned_sv(lorem_ipsum)); - CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + auto enc = + encrypt_for_recipient({to_span(ed_sk).data(), 32}, sid_raw2, to_span(lorem_ipsum)); + CHECK(std::search( + enc.begin(), + enc.end(), + to_unsigned("dolore magna"), + to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); - CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); + CHECK_THROWS(decrypt_incoming(to_span(ed_sk), enc)); - auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc); - CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); - CHECK(from_unsigned_sv(msg) == lorem_ipsum); + auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc); + CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; broken[14] ^= 0x80; - CHECK_THROWS(decrypt_incoming(to_sv(ed_sk2), broken)); + CHECK_THROWS(decrypt_incoming(to_span(ed_sk2), broken)); } } @@ -99,7 +103,7 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - ustring sid_raw; + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); REQUIRE(sid_raw == @@ -116,29 +120,28 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - ustring sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); REQUIRE(sid_raw2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); - auto enc1 = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); - auto enc2 = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + auto enc1 = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); + auto enc2 = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); REQUIRE(enc1 != enc2); - auto enc_det = - encrypt_for_recipient_deterministic(to_sv(ed_sk), sid_raw2, to_unsigned_sv("hello")); + auto enc_det = encrypt_for_recipient_deterministic(to_span(ed_sk), sid_raw2, to_span("hello")); CHECK(enc_det != enc1); CHECK(enc_det != enc2); CHECK(enc_det.size() == enc1.size()); - CHECK(oxenc::to_hex(enc_det) == + CHECK(to_hex(enc_det) == "208f96785db92319bc7a14afecc01e17bde912d17bbb32834c03ea63b1862c2a1b730e0725ef75b2f1a276db" "584c59a0ed9b5497bcb9f4effa893b5cb8b04dbe7a6ab457ebf972f03b006dd4572980a725399616d40184b8" "6aa3b7b218bdc6dd7c1adccda8ef4897f0f458492240b39079c27a6c791067ab26a03067a7602b50f0434639" "906f93e548f909d5286edde365ebddc146"); - auto [msg, sender] = decrypt_incoming(to_sv(ed_sk2), enc_det); - CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); - CHECK(from_unsigned_sv(msg) == "hello"); + auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc_det); + CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + CHECK(to_string(msg) == "hello"); } static std::array prefixed(unsigned char prefix, const session::uc32& pubkey) { @@ -164,13 +167,13 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - ustring sid_raw; + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); REQUIRE(sid_raw == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); - auto [blind15_pk, blind15_sk] = blind15_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); - auto [blind25_pk, blind25_sk] = blind25_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); + auto [blind15_pk, blind15_sk] = blind15_key_pair(to_span(ed_sk), to_span(server_pk)); + auto [blind25_pk, blind25_sk] = blind25_key_pair(to_span(ed_sk), to_span(server_pk)); auto blind15_pk_prefixed = prefixed(0x15, blind15_pk); auto blind25_pk_prefixed = prefixed(0x25, blind25_pk); @@ -185,50 +188,50 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - ustring sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); REQUIRE(sid_raw2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); - auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); - auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); + auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_span(ed_sk2), to_span(server_pk)); + auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_span(ed_sk2), to_span(server_pk)); auto blind15_pk2_prefixed = prefixed(0x15, blind15_pk2); auto blind25_pk2_prefixed = prefixed(0x25, blind25_pk2); SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind15_pk2_prefixed.data(), 33}, - to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); + to_span("hello")); + CHECK(to_string(enc) != "hello"); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), - to_sv(blind15_pk), + to_span(ed_sk2), + to_span(server_pk), + to_span(blind15_pk), {blind15_pk2_prefixed.data(), 33}, enc)); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, - to_sv(blind15_pk2), + to_span(blind15_pk2), enc)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == "hello"); + CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); @@ -242,26 +245,30 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_sv(ed_sk).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk).data(), 32}, + to_span(server_pk), {blind15_pk2_prefixed.data(), 33}, - to_unsigned_sv(lorem_ipsum)); - CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + to_span(lorem_ipsum)); + CHECK(std::search( + enc.begin(), + enc.end(), + to_unsigned("dolore magna"), + to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_sv(ed_sk).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk).data(), 32}, + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == lorem_ipsum); + CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_sv(ed_sk).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk).data(), 32}, + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); @@ -275,104 +282,108 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_sv(ed_sk).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk).data(), 32}, + to_span(server_pk), {blind15_pk2_prefixed.data(), 33}, - to_unsigned_sv(lorem_ipsum)); - CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + to_span(lorem_ipsum)); + CHECK(std::search( + enc.begin(), + enc.end(), + to_unsigned("dolore magna"), + to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_sv(ed_sk2).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk2).data(), 32}, + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == lorem_ipsum); + CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_sv(ed_sk2).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk2).data(), 32}, + to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, broken)); } SECTION("blind25, full secret, sender decrypt") { auto enc = encrypt_for_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind25_pk2_prefixed.data(), 33}, - to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); + to_span("hello")); + CHECK(to_string(enc) != "hello"); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), - to_sv(blind25_pk), + to_span(ed_sk), + to_span(server_pk), + to_span(blind25_pk), {blind25_pk2_prefixed.data(), 33}, enc)); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, - to_sv(blind25_pk2), + to_span(blind25_pk2), enc)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == "hello"); + CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); } SECTION("blind25, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_sv(ed_sk), - to_unsigned_sv(server_pk), + to_span(ed_sk), + to_span(server_pk), {blind25_pk2_prefixed.data(), 33}, - to_unsigned_sv("hello")); - CHECK(from_unsigned_sv(enc) != "hello"); + to_span("hello")); + CHECK(to_string(enc) != "hello"); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), - to_sv(blind25_pk), + to_span(ed_sk2), + to_span(server_pk), + to_span(blind25_pk), {blind25_pk2_prefixed.data(), 33}, enc)); CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, - to_sv(blind25_pk2), + to_span(blind25_pk2), enc)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == "hello"); + CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_sv(ed_sk2), - to_unsigned_sv(server_pk), + to_span(ed_sk2), + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); @@ -386,26 +397,30 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_sv(ed_sk).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk).data(), 32}, + to_span(server_pk), {blind25_pk2_prefixed.data(), 33}, - to_unsigned_sv(lorem_ipsum)); - CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + to_span(lorem_ipsum)); + CHECK(std::search( + enc.begin(), + enc.end(), + to_unsigned("dolore magna"), + to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_sv(ed_sk2).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk2).data(), 32}, + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, enc); CHECK(sender == sid); - CHECK(from_unsigned_sv(msg) == lorem_ipsum); + CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_sv(ed_sk2).data(), 32}, - to_unsigned_sv(server_pk), + {to_span(ed_sk2).data(), 32}, + to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, broken)); @@ -427,8 +442,8 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK(decrypt_ons_response(name, ciphertext_legacy, std::nullopt) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK_THROWS(decrypt_ons_response(name, to_unsigned_sv("invalid"), nonce)); - CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); + CHECK_THROWS(decrypt_ons_response(name, to_span("invalid"), nonce)); + CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_span("invalid"))); } TEST_CASE("Session ONS response decryption C API", "[session-ons][session_decrypt_ons_response]") { @@ -464,10 +479,10 @@ TEST_CASE("Session push notification decryption", "[session-notification][decryp "ab4aca5f0991e99eb0344ceeafa"_hexbytes; auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - CHECK(decrypt_push_notification(payload, enc_key) == to_unsigned("TestMessage")); - CHECK(decrypt_push_notification(payload_padded, enc_key) == to_unsigned("TestMessage")); - CHECK_THROWS(decrypt_push_notification(to_unsigned_sv("invalid"), enc_key)); - CHECK_THROWS(decrypt_push_notification(payload, to_unsigned_sv("invalid"))); + CHECK(decrypt_push_notification(payload, enc_key) == to_vector("TestMessage")); + CHECK(decrypt_push_notification(payload_padded, enc_key) == to_vector("TestMessage")); + CHECK_THROWS(decrypt_push_notification(to_span("invalid"), enc_key)); + CHECK_THROWS(decrypt_push_notification(payload, to_span("invalid"))); } TEST_CASE("Session message hash", "[session][message-hash]") { diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 337ad46f..143ba82b 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -4,11 +4,10 @@ #include +#include "session/util.hpp" #include "session/xed25519.h" #include "session/xed25519.hpp" -using session::xed25519::ustring_view; - constexpr std::array seed1{ 0xfe, 0xcd, 0x9a, 0x60, 0x34, 0xbc, 0x9a, 0xba, 0x27, 0x39, 0x25, 0xde, 0xe7, 0x06, 0x2b, 0x12, 0x33, 0x34, 0x58, 0x7c, 0x3c, 0x62, 0x57, 0x34, 0x1a, 0xfa, @@ -22,8 +21,8 @@ constexpr std::array seed2{ 0x45, 0x44, 0xc1, 0xc5, 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, 0xe8, 0xaa, 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x81}; -constexpr ustring_view pub1{seed1.data() + 32, 32}; -constexpr ustring_view pub2{seed2.data() + 32, 32}; +constexpr std::span pub1{seed1.data() + 32, 32}; +constexpr std::span pub2{seed2.data() + 32, 32}; constexpr std::array xpub1{ 0xfe, 0x94, 0xb7, 0xad, 0x4b, 0x7f, 0x1c, 0xc1, 0xbb, 0x92, 0x67, @@ -42,16 +41,9 @@ constexpr std::array pub2_abs{ 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x01, }; -template -static ustring_view view(const std::array& x) { - return {x.data(), x.size()}; -} -static ustring_view view(const std::string_view x) { - return ustring_view{reinterpret_cast(x.data()), x.size()}; -} template static std::string view_hex(const std::array& x) { - return oxenc::to_hex(view(x)); + return oxenc::to_hex(session::to_span(x)); } TEST_CASE("XEd25519 pubkey conversion", "[xed25519][pubkey]") { @@ -65,11 +57,11 @@ TEST_CASE("XEd25519 pubkey conversion", "[xed25519][pubkey]") { REQUIRE(rc == 0); REQUIRE(view_hex(xpk2) == view_hex(xpub2)); - auto xed1 = session::xed25519::pubkey(view(xpub1)); + auto xed1 = session::xed25519::pubkey(session::to_span(xpub1)); REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(view(xpub2)); + auto xed2 = session::xed25519::pubkey(session::to_span(xpub2)); REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: xed2[31] |= 0x80; @@ -89,14 +81,14 @@ TEST_CASE("XEd25519 signing", "[xed25519][sign]") { std::array xpk2; rc = crypto_sign_ed25519_pk_to_curve25519(xpk2.data(), pub2.data()); - const auto msg = view("hello world"); + const auto msg = session::to_span("hello world"); - auto xed_sig1 = session::xed25519::sign(view(xsk1), msg); + auto xed_sig1 = session::xed25519::sign(session::to_span(xsk1), msg); rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); REQUIRE(rc == 0); - auto xed_sig2 = session::xed25519::sign(view(xsk2), msg); + auto xed_sig2 = session::xed25519::sign(session::to_span(xsk2), msg); // This one will fail, because Xed signing always uses the positive but our actual pub2 is the // negative: @@ -118,26 +110,26 @@ TEST_CASE("XEd25519 verification", "[xed25519][verify]") { rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); REQUIRE(rc == 0); - const auto msg = view("hello world"); + const auto msg = session::to_span("hello world"); - auto xed_sig1 = session::xed25519::sign(view(xsk1), msg); - auto xed_sig2 = session::xed25519::sign(view(xsk2), msg); + auto xed_sig1 = session::xed25519::sign(session::to_span(xsk1), msg); + auto xed_sig2 = session::xed25519::sign(session::to_span(xsk2), msg); - REQUIRE(session::xed25519::verify(view(xed_sig1), view(xpub1), msg)); - REQUIRE(session::xed25519::verify(view(xed_sig2), view(xpub2), msg)); + REQUIRE(session::xed25519::verify(session::to_span(xed_sig1), session::to_span(xpub1), msg)); + REQUIRE(session::xed25519::verify(session::to_span(xed_sig2), session::to_span(xpub2), msg)); // Unlike regular Ed25519, XEd25519 uses randomness in the signature, so signing the same value // a second should give us a different signature: - auto xed_sig1b = session::xed25519::sign(view(xsk1), msg); + auto xed_sig1b = session::xed25519::sign(session::to_span(xsk1), msg); REQUIRE(view_hex(xed_sig1b) != view_hex(xed_sig1)); } TEST_CASE("XEd25519 pubkey conversion (C wrapper)", "[xed25519][pubkey][c]") { - auto xed1 = session::xed25519::pubkey(view(xpub1)); + auto xed1 = session::xed25519::pubkey(session::to_span(xpub1)); REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(view(xpub2)); + auto xed2 = session::xed25519::pubkey(session::to_span(xpub2)); REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: xed2[31] |= 0x80; @@ -156,7 +148,7 @@ TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { std::array xpk2; rc = crypto_sign_ed25519_pk_to_curve25519(xpk2.data(), pub2.data()); - const auto msg = view("hello world"); + const auto msg = session::to_span("hello world"); std::array xed_sig1, xed_sig2; REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); @@ -181,7 +173,7 @@ TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); REQUIRE(rc == 0); - const auto msg = view("hello world"); + const auto msg = session::to_span("hello world"); std::array xed_sig1, xed_sig2; REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); diff --git a/tests/utils.hpp b/tests/utils.hpp index 035732eb..ac033373 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -15,9 +15,7 @@ #include "session/config/base.h" #include "session/types.hpp" - -using ustring = std::basic_string; -using ustring_view = std::basic_string_view; +#include "session/util.hpp" namespace session { @@ -48,21 +46,22 @@ struct log_level_lowerer : log_level_override { }; } // namespace session -inline ustring operator""_bytes(const char* x, size_t n) { - return {reinterpret_cast(x), n}; +inline std::vector operator""_bytes(const char* x, size_t n) { + auto begin = reinterpret_cast(x); + return {begin, begin + n}; } -inline ustring operator""_hexbytes(const char* x, size_t n) { - ustring bytes; +inline std::vector operator""_hexbytes(const char* x, size_t n) { + std::vector bytes; oxenc::from_hex(x, x + n, std::back_inserter(bytes)); return bytes; } -inline std::vector operator""_hexbytesv(const char* x, size_t n) { - auto bytes = oxenc::from_hex(x, x + n); - auto begin = reinterpret_cast(bytes.data()); - return {begin, begin + bytes.size()}; -} -inline std::string to_hex(ustring_view bytes) { +inline std::string to_hex(std::vector bytes) { + std::string hex; + oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); + return hex; +} +inline std::string to_hex(std::span bytes) { std::string hex; oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); return hex; @@ -77,10 +76,6 @@ inline std::string_view sp_to_sv(const T& sp) { return {reinterpret_cast(sp.data()), sp.size()}; } -inline std::string_view vec_to_sv(const std::vector& vec) { - return {reinterpret_cast(vec.data()), vec.size()}; -} - // Returns the current timestamp in milliseconds inline int64_t get_timestamp_ms() { return std::chrono::duration_cast( @@ -103,18 +98,7 @@ inline int64_t get_timestamp_us() { .count(); } -inline std::string_view to_sv(ustring_view x) { - return {reinterpret_cast(x.data()), x.size()}; -} -inline ustring_view to_usv(std::string_view x) { - return {reinterpret_cast(x.data()), x.size()}; -} -template -ustring_view to_usv(const std::array& data) { - return {data.data(), N}; -} - -inline std::string printable(ustring_view x) { +inline std::string printable(std::span x) { std::string p; for (auto c : x) { if (c >= 0x20 && c <= 0x7e) @@ -125,7 +109,7 @@ inline std::string printable(ustring_view x) { return p; } inline std::string printable(std::string_view x) { - return printable(to_usv(x)); + return printable(session::to_span(x)); } std::string printable(const unsigned char* x) = delete; inline std::string printable(const unsigned char* x, size_t n) { From 74287241ce75454331b63bd0b738e6d46ea507e2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 16:52:20 +1100 Subject: [PATCH 518/572] Fixed some CI errors due to missing imports --- include/session/ed25519.hpp | 1 + include/session/hash.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index 9623b45e..a0670c02 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 8729a5d5..cef74f37 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" From 99b931c88021894770d831f13ae801a054562b06 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 17:01:23 +1100 Subject: [PATCH 519/572] Moved the vector include into types.hpp --- include/session/ed25519.hpp | 1 - include/session/hash.hpp | 1 - include/session/types.hpp | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index a0670c02..9623b45e 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include "types.hpp" diff --git a/include/session/hash.hpp b/include/session/hash.hpp index cef74f37..8729a5d5 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include "types.hpp" diff --git a/include/session/types.hpp b/include/session/types.hpp index 40505908..b2e4ccd0 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace session { namespace config { From 382fc9cab1a602370c2f36fa051092b1caed5a71 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 17:14:43 +1100 Subject: [PATCH 520/572] Shifted imports around again --- include/session/ed25519.hpp | 1 + include/session/hash.hpp | 1 + include/session/session_encrypt.hpp | 1 + include/session/types.hpp | 1 - 4 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index 9623b45e..a0670c02 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 8729a5d5..cef74f37 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 5e346e88..6dd941cd 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" diff --git a/include/session/types.hpp b/include/session/types.hpp index b2e4ccd0..40505908 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -5,7 +5,6 @@ #include #include #include -#include namespace session { namespace config { From db030ec3360f4085f0607b83eecef2c79ecf7931 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 17:22:37 +1100 Subject: [PATCH 521/572] Disable -Werror for buggy GCC stringop warnings --- src/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ff7163d4..b6c53f22 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -128,6 +128,14 @@ if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND target_compile_options(config PUBLIC -Wno-error=stringop-overread) endif() +# GCC's stringop overflow and overread warnings are very buggy, producing false positives for all +# sorts of valid things, so make sure we don't have -Werror for them. We cap this check for now at +# 15 (where these still occur in the latest gcc snapshot) with the vain hope that maybe they'll get +# fixed for gcc 16. +if(WARNINGS_AS_ERRORS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND + CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 12.0.0 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0.0) + target_compile_options(crypto PUBLIC -Wno-error=stringop-overflow -Wno-error=stringop-overread) +endif() if(LIBSESSION_UTIL_VERSIONTAG) set(PROJECT_VERSION_TAG "${LIBSESSION_UTIL_VERSIONTAG}") From 01d82ad1fad47a1e6ad015e288de82ce1db47cdf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 17:34:06 +1100 Subject: [PATCH 522/572] Got fed up and included vector everywhere we include array --- include/session/curve25519.hpp | 1 + include/session/fields.hpp | 1 + include/session/onionreq/key_types.hpp | 1 + include/session/xed25519.hpp | 1 + src/config/protos.cpp | 1 + src/session_encrypt.cpp | 1 + 6 files changed, 6 insertions(+) diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index d818916f..c83d00ab 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "types.hpp" diff --git a/include/session/fields.hpp b/include/session/fields.hpp index 6ca71a24..b70980d1 100644 --- a/include/session/fields.hpp +++ b/include/session/fields.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace session { diff --git a/include/session/onionreq/key_types.hpp b/include/session/onionreq/key_types.hpp index b03e2ee0..5f22c71c 100644 --- a/include/session/onionreq/key_types.hpp +++ b/include/session/onionreq/key_types.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "../types.hpp" #include "../util.hpp" diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index d037fa07..1cedd9ca 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace session::xed25519 { diff --git a/src/config/protos.cpp b/src/config/protos.cpp index 44009257..01587c75 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 89ecbff8..90872942 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "session/blinding.hpp" #include "session/sodium_array.hpp" From 0ccec223fc7a8c602266e8aad7859dbb28dbb6ee Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Mar 2025 17:38:27 +1100 Subject: [PATCH 523/572] Another include --- include/session/config/encrypt.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index 662edb22..431136e9 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "../types.hpp" From a0714ccf6464ee3542adfbb18d8cbd63d8ae8551 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 21 Mar 2025 08:49:18 +1100 Subject: [PATCH 524/572] Fixed a couple more CI issues --- include/session/multi_encrypt.hpp | 2 +- tests/utils.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index fc738e79..a6b480ca 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -163,7 +163,7 @@ void encrypt_for_multiple(std::string_view message, Args&&... args) { return encrypt_for_multiple(to_span(message), std::forward(args)...); } template -void encrypt_for_multiple(std::basic_string_view message, Args&&... args) { +void encrypt_for_multiple(std::span message, Args&&... args) { return encrypt_for_multiple(to_span(message), std::forward(args)...); } diff --git a/tests/utils.hpp b/tests/utils.hpp index ac033373..b3b97356 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -56,7 +56,7 @@ inline std::vector operator""_hexbytes(const char* x, size_t n) { return bytes; } -inline std::string to_hex(std::vector bytes) { +inline std::string to_hex(std::vector bytes) { std::string hex; oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); return hex; From d1e08b23d5b0dcf1961bb106eaebf86c6a25ea42 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 21 Mar 2025 09:00:05 +1100 Subject: [PATCH 525/572] Fixed a couple more basic_string_view uses --- include/session/multi_encrypt.hpp | 6 +++--- tests/utils.hpp | 10 ---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index a6b480ca..8083d1cf 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -198,9 +198,9 @@ template < std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v< - std::optional>, + std::optional>, NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext>>> + std::is_invocable_r_v>, NextCiphertext>>> std::optional> decrypt_for_multiple( NextCiphertext next_ciphertext, std::span nonce, @@ -336,7 +336,7 @@ std::vector encrypt_for_multiple_simple(std::string_view message, } template std::vector encrypt_for_multiple_simple( - std::basic_string_view message, Args&&... args) { + std::span message, Args&&... args) { return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); } diff --git a/tests/utils.hpp b/tests/utils.hpp index b3b97356..03dc8a08 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -126,16 +126,6 @@ std::set> make_set(T&&... args) { return {std::forward(args)...}; } -template -std::vector> view_vec(std::vector>&& v) = delete; -template -std::vector> view_vec(const std::vector>& v) { - std::vector> vv; - vv.reserve(v.size()); - std::copy(v.begin(), v.end(), std::back_inserter(vv)); - return vv; -} - template > Validator> auto eventually_impl(std::chrono::milliseconds timeout, Call&& f, Validator&& isValid) -> std::invoke_result_t { From 765753f9c0d79f474567027d966bf0455dc2b2de Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 21 Mar 2025 10:05:04 +1100 Subject: [PATCH 526/572] Fixed an error due to static analysis of a test --- tests/test_blinding.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 423a3348..90c98bc0 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -304,6 +304,7 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { uint64_t timestamp = 1234567890; std::vector ts = to_vector(std::to_string(timestamp)); std::vector full_message; + full_message.reserve(10 /* timestamp */ + method.size() + path.size() + body.size()); full_message.insert(full_message.end(), ts.begin(), ts.end()); full_message.insert(full_message.end(), method_span.begin(), method_span.end()); full_message.insert(full_message.end(), path_span.begin(), path_span.end()); From d94496b6dfd55aa18747aac05f3145ce9218460f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 25 Mar 2025 09:16:37 +1100 Subject: [PATCH 527/572] Updated includes to fix build error due to new version of fmt --- src/config/base.cpp | 1 + src/network.cpp | 1 + src/onionreq/builder.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/src/config/base.cpp b/src/config/base.cpp index 9a7583ff..e33dd9e6 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -1,5 +1,6 @@ #include "session/config/base.hpp" +#include #include #include #include diff --git a/src/network.cpp b/src/network.cpp index 91f1c9bc..5ff0155c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,5 +1,6 @@ #include "session/network.hpp" +#include #include #include #include diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index bcd40005..7048f402 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -1,6 +1,7 @@ #include "session/onionreq/builder.hpp" #include +#include #include #include #include From f207b8dbed332067f48f2dded37b77528372a97f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 25 Mar 2025 12:55:11 +1100 Subject: [PATCH 528/572] Updated to the latest libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 001e4e8d..b3468b4f 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 001e4e8d36dc9b7b423798b13cda9ce952bfb7c7 +Subproject commit b3468b4fa43395213efab5e8e02eeb7d40141e1a From e877f086ea240ddba81f0876f90f9f60987dd9d8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 27 Mar 2025 11:50:20 +1100 Subject: [PATCH 529/572] Updates to support the latest oxen-encoding changes --- include/session/blinding.hpp | 15 ++++++++------- include/session/config/encrypt.hpp | 1 + include/session/curve25519.hpp | 1 + include/session/ed25519.hpp | 1 + include/session/hash.hpp | 1 + include/session/multi_encrypt.hpp | 4 +--- include/session/network.hpp | 27 ++++++++++++++++++++++++--- include/session/session_encrypt.hpp | 1 + include/session/types.hpp | 2 -- include/session/util.hpp | 9 ++++----- src/config/base.cpp | 3 ++- src/config/protos.cpp | 2 +- src/config/user_groups.cpp | 6 +++--- src/network.cpp | 4 +++- src/session_encrypt.cpp | 6 +++--- tests/test_config_userprofile.cpp | 4 ++-- tests/test_configdata.cpp | 1 + tests/test_group_info.cpp | 4 ++-- tests/test_logging.cpp | 4 +++- tests/test_multi_encrypt.cpp | 10 +++++----- tests/test_onionreq.cpp | 4 ++-- tests/utils.hpp | 11 ----------- 22 files changed, 69 insertions(+), 52 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index e243ce4f..fed3d8ff 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -59,12 +59,12 @@ namespace session { /// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes server pk in bytes, not hex. -uc32 blind15_factor(std::span server_pk); +std::array blind15_factor(std::span server_pk); /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). -uc32 blind25_factor( +std::array blind25_factor( std::span session_id, std::span server_pk); /// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted @@ -129,10 +129,10 @@ std::vector blinded25_id_from_ed( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind15_key_pair( +std::pair, cleared_uc32> blind15_key_pair( std::span ed25519_sk, std::span server_pk, - uc32* k = nullptr); + std::array* k = nullptr); /// Computes a 25-blinded key pair. /// @@ -146,10 +146,10 @@ std::pair blind15_key_pair( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind25_key_pair( +std::pair, cleared_uc32> blind25_key_pair( std::span ed25519_sk, std::span server_pk, - uc32* k_prime = nullptr); + std::array* k_prime = nullptr); /// Computes a version-blinded key pair. /// @@ -158,7 +158,8 @@ std::pair blind25_key_pair( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair blind_version_key_pair(std::span ed25519_sk); +std::pair, cleared_uc64> blind_version_key_pair( + std::span ed25519_sk); /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index 431136e9..ded62b70 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp index c83d00ab..b476f5ac 100644 --- a/include/session/curve25519.hpp +++ b/include/session/curve25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "types.hpp" diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index a0670c02..264b2000 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "types.hpp" diff --git a/include/session/hash.hpp b/include/session/hash.hpp index cef74f37..b4e706e7 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "types.hpp" diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 8083d1cf..08c78a4b 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -197,9 +197,7 @@ template < std::is_invocable_r_v>, NextCiphertext> || std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v, NextCiphertext> || - std::is_invocable_r_v< - std::optional>, - NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || std::is_invocable_r_v>, NextCiphertext>>> std::optional> decrypt_for_multiple( NextCiphertext next_ciphertext, diff --git a/include/session/network.hpp b/include/session/network.hpp index af290397..ef8c9471 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -78,9 +78,30 @@ struct service_node : public oxen::quic::RemoteAddress { auto operator<=>(const service_node& other) const { auto cmp = oxen::quic::RemoteAddress::operator<=>(other); - if (cmp == 0) - cmp = std::tie(storage_server_version, swarm_id) <=> - std::tie(other.storage_server_version, other.swarm_id); + if (cmp == 0) { + if (storage_server_version != other.storage_server_version) { +#if defined(__ANDROID__) && __NDK_MAJOR__ < 27 + if (storage_server_version.size() != other.storage_server_version.size()) + return storage_server_version.size() < other.storage_server_version.size() + ? std::strong_ordering::less + : std::strong_ordering::greater; + + for (size_t i = 0; i < storage_server_version.size(); ++i) { + if (storage_server_version[i] != other.storage_server_version[i]) + return storage_server_version[i] < other.storage_server_version[i] + ? std::strong_ordering::less + : std::strong_ordering::greater; + } + + return std::strong_ordering::equal; +#else + return storage_server_version <=> other.storage_server_version; +#endif + } + + return swarm_id <=> other.swarm_id; + } + return cmp; } bool operator==(const service_node& other) const { return (*this <=> other) == 0; } diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 6dd941cd..3a15a015 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "types.hpp" diff --git a/include/session/types.hpp b/include/session/types.hpp index 40505908..6c3e1580 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include diff --git a/include/session/util.hpp b/include/session/util.hpp index d0a1c751..303974d7 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -26,7 +25,7 @@ inline std::span as_span(const std::span& sp) { return {reinterpret_cast(sp.data()), sp.size()}; } -template +template inline std::span to_span(const T& c) { return {reinterpret_cast(c.data()), c.size()}; } @@ -37,7 +36,7 @@ inline std::span to_span(const char (&literal)[N]) { } template - requires(!oxenc::string_like) + requires(!oxenc::bt_input_string) inline std::span to_span(const Container& c) { return {reinterpret_cast(c.data()), c.size()}; } @@ -55,7 +54,7 @@ inline std::vector to_vector(std::span sp) { return convert>(sp); } -template +template inline std::vector to_vector(const T& c) { return convert>(to_span(c)); } @@ -66,7 +65,7 @@ inline std::vector to_vector(const std::array& arr) { } template - requires(!oxenc::string_like) + requires(!oxenc::bt_input_string) inline std::vector to_vector(const Container& c) { return convert>(to_span(c)); } diff --git a/src/config/base.cpp b/src/config/base.cpp index e33dd9e6..2011ae1a 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -492,7 +492,8 @@ void ConfigSig::init_sig_keys( std::optional> ed25519_pubkey, std::optional> ed25519_secretkey) { if (ed25519_secretkey) { - if (ed25519_pubkey && *ed25519_pubkey != ed25519_secretkey->subspan(32)) + if (ed25519_pubkey && + to_string_view(*ed25519_pubkey) != to_string_view(ed25519_secretkey->subspan(32))) throw std::invalid_argument{"Invalid signing keys: secret key and pubkey do not match"}; set_sig_keys(*ed25519_secretkey); } else if (ed25519_pubkey) { diff --git a/src/config/protos.cpp b/src/config/protos.cpp index 01587c75..bfd5d7af 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -153,7 +153,7 @@ std::vector unwrap_config( throw std::runtime_error{"Failed to parse Envelope"}; auto [content, sender] = decrypt_incoming(ed25519_sk, to_span(envelope.content())); - if (sender != ed25519_pk) + if (to_string_view(sender) != to_string_view(ed25519_pk)) throw std::runtime_error{"Incoming config data was not from us; ignoring"}; if (content.empty()) diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index b7485408..55d2b380 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -450,10 +450,10 @@ void UserGroups::set(const group_info& g) { if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: - std::span{g.secretkey.data() + 32, 32} == - std::span{ + to_string_view(std::span{g.secretkey.data() + 32, 32}) == + to_string_view(std::span{ reinterpret_cast(pk_bytes.data() + 1), - pk_bytes.size() - 1}) + pk_bytes.size() - 1})) info["K"] = std::span{g.secretkey.data(), 32}; else { info["K"] = std::span{}; diff --git a/src/network.cpp b/src/network.cpp index 5ff0155c..c6d713c8 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2686,7 +2686,9 @@ void Network::handle_errors( auto snode_it = std::find_if( updated_path.nodes.begin(), updated_path.nodes.end(), - [&edpk_view](const auto& node) { return node.view_remote_key() == edpk_view; }); + [&edpk_view](const auto& node) { + return to_string_view(node.view_remote_key()) == to_string_view(edpk_view); + }); if (snode_it != updated_path.nodes.end()) { found_invalid_node = true; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 90872942..b938c889 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -486,7 +486,7 @@ std::pair, std::string> decrypt_from_blinded_recipien ? blinded25_id_from_ed(to_span(ed_pk_from_seed), server_pk) : blinded15_id_from_ed(to_span(ed_pk_from_seed), server_pk); - if (sender_id == blinded_id) + if (to_string_view(sender_id) == to_string_view(blinded_id)) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); else dec_key = blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); @@ -547,11 +547,11 @@ std::pair, std::string> decrypt_from_blinded_recipien ? blinded25_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id) : blinded15_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id); - bool matched = sender_id == extracted_sender; + bool matched = to_string_view(sender_id) == to_string_view(extracted_sender); if (!matched && extracted_sender[0] == 0x15) { // With 15-blinding we might need the negative instead: extracted_sender[31] ^= 0x80; - matched = sender_id == extracted_sender; + matched = to_string_view(sender_id) == to_string_view(extracted_sender); } if (!matched) throw std::runtime_error{"Blinded sender id does not match the actual sender"}; diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 58627250..c1961e83 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -370,7 +370,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(to_hex(std::span{pic.key, 32}) == + CHECK(oxenc::to_hex(std::span{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); #if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) @@ -384,7 +384,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(to_hex(std::span{pic.key, 32}) == + CHECK(oxenc::to_hex(std::span{pic.key, 32}) == "7177657274007975696f31323334353637383930313233343536373839303132"); CHECK(user_profile_get_nts_priority(conf) == 9); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 4883b05c..1ebf8d8f 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 0ee6128b..b1947bd4 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -337,10 +337,10 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo_rw3.get_name() == "Super Group 2"); auto [s6, t6, o6] = ginfo_rw3.push(); - CHECK(to_hex(ginfo_rw3.key(0)) == + CHECK(oxenc::to_hex(ginfo_rw3.key(0)) == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); REQUIRE(ginfo_rw3.key_count() == 2); - CHECK(to_hex(ginfo_rw3.key(1)) == + CHECK(oxenc::to_hex(ginfo_rw3.key(1)) == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); CHECK(s6 == s5); CHECK(t6.size() == t23.size()); diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index a807bd8a..8b24a769 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -108,7 +108,9 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { session::add_logger([&](std::string_view msg) { simple_logs.emplace_back(msg); }); - { quic::Network net; } + { + quic::Network net; + } oxen::log::clear_sinks(); diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 87d8cb3a..3fa96949 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -209,15 +209,15 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); - CHECK(to_hex(session::to_span(x_keys[0].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[0].second)) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(to_hex(session::to_span(x_keys[1].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[1].second)) == "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); - CHECK(to_hex(session::to_span(x_keys[2].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[2].second)) == "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); - CHECK(to_hex(session::to_span(x_keys[3].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[3].second)) == "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); - CHECK(to_hex(session::to_span(x_keys[4].second)) == + CHECK(oxenc::to_hex(session::to_span(x_keys[4].second)) == "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 23280bf1..dba54503 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -64,7 +64,7 @@ TEST_CASE("Onion request parser", "[onionreq][parser]") { OnionReqParser parser_gcm{B, b, enc_gcm}; CHECK(to_string(parser_gcm.payload()) == "Hello world"); - CHECK(sp_to_sv(parser_gcm.remote_pubkey()) == to_string_view(A)); + CHECK(to_string_view(parser_gcm.remote_pubkey()) == to_string_view(A)); auto aes_reply = parser_gcm.encrypt_reply(to_span("Goodbye world")); CHECK(aes_reply.size() == 12 + 13 + 16); @@ -73,7 +73,7 @@ TEST_CASE("Onion request parser", "[onionreq][parser]") { OnionReqParser parser_xchacha20{B, b, enc_xchacha20}; CHECK(to_string(parser_xchacha20.payload()) == "Hello world"); - CHECK(sp_to_sv(parser_xchacha20.remote_pubkey()) == to_string_view(A)); + CHECK(to_string_view(parser_xchacha20.remote_pubkey()) == to_string_view(A)); auto xcha_reply = parser_xchacha20.encrypt_reply(to_span("Goodbye world")); CHECK(xcha_reply.size() == 16 + 13 + 24); CHECK(to_string(e.decrypt_xchacha20(xcha_reply, x25519_pubkey::from_bytes(B))) == diff --git a/tests/utils.hpp b/tests/utils.hpp index 03dc8a08..4a4aceb0 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -61,21 +60,11 @@ inline std::string to_hex(std::vector bytes) { oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); return hex; } -inline std::string to_hex(std::span bytes) { - std::string hex; - oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); - return hex; -} inline constexpr auto operator""_kiB(unsigned long long kiB) { return kiB * 1024; } -template -inline std::string_view sp_to_sv(const T& sp) { - return {reinterpret_cast(sp.data()), sp.size()}; -} - // Returns the current timestamp in milliseconds inline int64_t get_timestamp_ms() { return std::chrono::duration_cast( From 2b22fe5c83fa43834433fccecc282de8f350b577 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 27 Mar 2025 11:50:31 +1100 Subject: [PATCH 530/572] Updated formatter to clang-19 --- .drone.jsonnet | 2 +- utils/ci/drone-format-verify.sh | 2 +- utils/format.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index a7a19663..2d71d508 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -319,7 +319,7 @@ local static_build(name, 'echo "Building on ${DRONE_STAGE_MACHINE}"', apt_get_quiet + ' update', apt_get_quiet + ' install -y eatmydata', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y git clang-format-15 jsonnet', + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y git clang-format-19 jsonnet', './utils/ci/drone-format-verify.sh', ], }], diff --git a/utils/ci/drone-format-verify.sh b/utils/ci/drone-format-verify.sh index 829a3a21..d6658a3f 100755 --- a/utils/ci/drone-format-verify.sh +++ b/utils/ci/drone-format-verify.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test "x$IGNORE" != "x" && exit 0 repo=$(readlink -e $(dirname $0)/../../) -clang-format-17 -i $(find $repo/src $repo/include $repo/tests | grep -E '\.[hc](pp)?$') +clang-format-19 -i $(find $repo/src $repo/include $repo/tests | grep -E '\.[hc](pp)?$') jsonnetfmt -i $repo/.drone.jsonnet git --no-pager diff --exit-code --color || (echo -ne '\n\n\e[31;1mLint check failed; please run ./utils/format.sh\e[0m\n\n' ; exit 1) diff --git a/utils/format.sh b/utils/format.sh index 15ca8dcf..f565ad5f 100755 --- a/utils/format.sh +++ b/utils/format.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -CLANG_FORMAT_DESIRED_VERSION=17 +CLANG_FORMAT_DESIRED_VERSION=19 binary=$(command -v clang-format-$CLANG_FORMAT_DESIRED_VERSION 2>/dev/null) if [ $? -ne 0 ]; then From bfdff66cc893a1e85bc0e8e776fbd787f33e65b8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 27 Mar 2025 12:02:56 +1100 Subject: [PATCH 531/572] Added a header causing Desktop build issues --- include/session/config/profile_pic.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/session/config/profile_pic.hpp b/include/session/config/profile_pic.hpp index 1eb55a9c..7c82b457 100644 --- a/include/session/config/profile_pic.hpp +++ b/include/session/config/profile_pic.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace session::config { From 8f40960f84350b8eed206598adc04b0ad39d8952 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Apr 2025 08:36:59 +1100 Subject: [PATCH 532/572] Removed the <=> operator from service_node --- include/session/network.hpp | 31 +++---------------------------- src/network.cpp | 12 ++++++++++-- tests/test_network.cpp | 20 ++++++++++++++------ 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index ef8c9471..530ca6c5 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -76,35 +76,10 @@ struct service_node : public oxen::quic::RemoteAddress { return *this; } - auto operator<=>(const service_node& other) const { - auto cmp = oxen::quic::RemoteAddress::operator<=>(other); - if (cmp == 0) { - if (storage_server_version != other.storage_server_version) { -#if defined(__ANDROID__) && __NDK_MAJOR__ < 27 - if (storage_server_version.size() != other.storage_server_version.size()) - return storage_server_version.size() < other.storage_server_version.size() - ? std::strong_ordering::less - : std::strong_ordering::greater; - - for (size_t i = 0; i < storage_server_version.size(); ++i) { - if (storage_server_version[i] != other.storage_server_version[i]) - return storage_server_version[i] < other.storage_server_version[i] - ? std::strong_ordering::less - : std::strong_ordering::greater; - } - - return std::strong_ordering::equal; -#else - return storage_server_version <=> other.storage_server_version; -#endif - } - - return swarm_id <=> other.swarm_id; - } - - return cmp; + auto operator<=>(const service_node& other) const = delete; + bool operator==(const service_node& other) const { + return RemoteAddress::operator==(other) && storage_server_version == other.storage_server_version && swarm_id == other.swarm_id; } - bool operator==(const service_node& other) const { return (*this <=> other) == 0; } }; struct connection_info { diff --git a/src/network.cpp b/src/network.cpp index c6d713c8..521dc21f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1323,8 +1323,16 @@ void Network::refresh_snode_cache(std::optional existing_request_id } // Sort the vectors (so make it easier to find the intersection) + auto compare_service_nodes = [](const service_node& a, const service_node& b) { + if (auto cmp = quic::Address(a) <=> quic::Address(b); cmp != 0) + return cmp < 0; + + return std::tie(a.get_remote_key(), a.swarm_id, a.storage_server_version) < + std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); + }; + for (auto& nodes : *snode_refresh_results) - std::stable_sort(nodes.begin(), nodes.end()); + std::stable_sort(nodes.begin(), nodes.end(), compare_service_nodes); auto nodes = (*snode_refresh_results)[0]; @@ -1338,7 +1346,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id (*snode_refresh_results)[i].begin(), (*snode_refresh_results)[i].end(), std::back_inserter(temp), - [](const auto& a, const auto& b) { return a == b; }); + compare_service_nodes); nodes = std::move(temp); } } diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 36efcf04..6e8fac40 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -720,6 +720,14 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { auto path = onion_path{"Test", invalid_info, {snode_cache[0], snode_cache[1], snode_cache[2]}, 0}; + auto compare_service_nodes = [](const service_node& a, const service_node& b) { + if (auto cmp = oxen::quic::Address(a) <=> oxen::quic::Address(b); cmp != 0) + return cmp < 0; + + return std::tie(a.get_remote_key(), a.swarm_id, a.storage_server_version) < + std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); + }; + // Should shuffle the result network.emplace(std::nullopt, true, false, false); network->set_snode_cache(snode_cache); @@ -729,7 +737,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network.emplace(std::nullopt, true, false, false); network->set_snode_cache(snode_cache); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == snode_cache); // Should exclude nodes used in paths @@ -737,7 +745,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network->set_snode_cache(snode_cache); network->set_paths(PathType::standard, {path}); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == std::vector{snode_cache.begin() + 3, snode_cache.end()}); // Should exclude nodes in unused connections @@ -745,7 +753,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network->set_snode_cache(snode_cache); network->set_unused_connections({invalid_info}); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); // Should exclude nodes in in-progress connections @@ -753,7 +761,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network->set_snode_cache(snode_cache); network->set_in_progress_connections({{"Test", snode_cache.front()}}); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); // Should exclude nodes destinations in pending requests @@ -769,7 +777,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { std::nullopt, PathType::standard)); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); // Should exclude nodes which have passed the failure threshold @@ -777,7 +785,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { network->set_snode_cache(snode_cache); network->set_failure_count(snode_cache.front(), 10); unused_nodes = network->get_unused_nodes(); - std::stable_sort(unused_nodes.begin(), unused_nodes.end()); + std::stable_sort(unused_nodes.begin(), unused_nodes.end(), compare_service_nodes); CHECK(unused_nodes == std::vector{snode_cache.begin() + 1, snode_cache.end()}); // Should exclude nodes which have the same IP if one was excluded From 90720c7ce3bd57f6055f4916e4d1a5721685ab3e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 2 Apr 2025 09:08:46 +1100 Subject: [PATCH 533/572] fix: zstd cmake_minimum_required is 2.8, but not supported anymore we don't want to bump zstd to latest just yet as it changes the compressed hash of a message, and we are not sure what would happen with libsession --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 79cfcd56..dbf1c2ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) set(default_static_libstd OFF) if(WIN32) From a17b49f352a73008edda79683c03922008aa3113 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Apr 2025 10:22:18 +1100 Subject: [PATCH 534/572] Updated to the latest libQuic, ran the formatter --- external/oxen-libquic | 2 +- include/session/network.hpp | 3 ++- src/network.cpp | 4 ++-- tests/test_network.cpp | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index b3468b4f..1b1627bb 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit b3468b4fa43395213efab5e8e02eeb7d40141e1a +Subproject commit 1b1627bbd6113824d134f4d0365dc2c37b120554 diff --git a/include/session/network.hpp b/include/session/network.hpp index 530ca6c5..6baae493 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -78,7 +78,8 @@ struct service_node : public oxen::quic::RemoteAddress { auto operator<=>(const service_node& other) const = delete; bool operator==(const service_node& other) const { - return RemoteAddress::operator==(other) && storage_server_version == other.storage_server_version && swarm_id == other.swarm_id; + return RemoteAddress::operator==(other) && + storage_server_version == other.storage_server_version && swarm_id == other.swarm_id; } }; diff --git a/src/network.cpp b/src/network.cpp index 521dc21f..84b32d84 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1326,9 +1326,9 @@ void Network::refresh_snode_cache(std::optional existing_request_id auto compare_service_nodes = [](const service_node& a, const service_node& b) { if (auto cmp = quic::Address(a) <=> quic::Address(b); cmp != 0) return cmp < 0; - + return std::tie(a.get_remote_key(), a.swarm_id, a.storage_server_version) < - std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); + std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); }; for (auto& nodes : *snode_refresh_results) diff --git a/tests/test_network.cpp b/tests/test_network.cpp index 6e8fac40..c56d020c 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -723,9 +723,9 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { auto compare_service_nodes = [](const service_node& a, const service_node& b) { if (auto cmp = oxen::quic::Address(a) <=> oxen::quic::Address(b); cmp != 0) return cmp < 0; - + return std::tie(a.get_remote_key(), a.swarm_id, a.storage_server_version) < - std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); + std::tie(b.get_remote_key(), b.swarm_id, b.storage_server_version); }; // Should shuffle the result From c6b12b2f4f286cebaa800dca8ae420ca69cb1bae Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Apr 2025 10:46:19 +1100 Subject: [PATCH 535/572] Replaced oxenc::variant usages with stl --- src/config.cpp | 37 +++++++++++++++--------------- src/config/convo_info_volatile.cpp | 3 +-- src/config/user_groups.cpp | 3 +-- src/onionreq/builder.cpp | 1 - tests/catch2_bt_format.hpp | 4 ++-- tests/test_bt_merge.cpp | 3 ++- tests/test_configdata.cpp | 5 ++-- 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 4dfdc93c..ee232910 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -51,7 +50,7 @@ namespace { return {s.empty(), false}; } std::pair prune_(dict_value& v) { - return var::visit([](auto& x) { return prune_(x); }, unwrap(v)); + return std::visit([](auto& x) { return prune_(x); }, unwrap(v)); } // diff helper functions @@ -79,9 +78,9 @@ namespace { oxenc::bt_list additions, removals; for (auto& a : added) - var::visit([&additions](auto& x) { additions.emplace_back(std::move(x)); }, a); + std::visit([&additions](auto& x) { additions.emplace_back(std::move(x)); }, a); for (auto& r : removed) - var::visit([&removals](auto& x) { removals.emplace_back(std::move(x)); }, r); + std::visit([&removals](auto& x) { removals.emplace_back(std::move(x)); }, r); return oxenc::bt_list{{std::move(additions)}, {std::move(removals)}}; } @@ -122,12 +121,12 @@ namespace { } else { const auto& key = newit->first; if (auto* ov = std::get_if(&o)) { - if (*ov != var::get(n)) + if (*ov != std::get(n)) df[key] = ""sv; } else if (auto* dv = std::get_if(&o)) { - if (auto subdiff = diff_impl(*dv, var::get(n))) + if (auto subdiff = diff_impl(*dv, std::get(n))) df[key] = std::move(*subdiff); - } else if (auto subdiff = diff_impl(var::get(o), var::get(n))) { + } else if (auto subdiff = diff_impl(std::get(o), std::get(n))) { df[key] = std::move(*subdiff); } ++oldit; @@ -163,9 +162,9 @@ namespace { // Gets a string_view if the type is a string or string_view, nullopt otherwise. constexpr std::optional get_bt_str(const oxenc::bt_value& v) { if (std::holds_alternative(v)) - return var::get(v); + return std::get(v); if (std::holds_alternative(v)) - return var::get(v); + return std::get(v); return std::nullopt; } @@ -246,7 +245,7 @@ namespace { void serialize_data(oxenc::bt_list_producer&& out, const set& s); void serialize_data(oxenc::bt_dict_producer&& out, const dict& d) { for (const auto& pair : d) { - var::visit( + std::visit( [&](const auto& v) { auto& k = pair.first; using T = std::remove_cv_t>; @@ -255,7 +254,7 @@ namespace { else if constexpr (std::is_same_v) serialize_data(out.append_list(k), v); else - var::visit( + std::visit( [&](const auto& scalar) { out.append(pair.first, scalar); }, v); }, unwrap(pair.second)); @@ -263,7 +262,7 @@ namespace { } void serialize_data(oxenc::bt_list_producer&& out, const set& s) { for (auto& val : s) - var::visit([&](const auto& scalar) { out.append(scalar); }, val); + std::visit([&](const auto& scalar) { out.append(scalar); }, val); } void parse_data(set& s, oxenc::bt_list_consumer in); @@ -280,10 +279,10 @@ namespace { d.emplace_hint(d.end(), std::move(key), in.consume_integer()); else if (in.is_dict()) { auto it = d.emplace_hint(d.end(), std::move(key), dict{}); - parse_data(var::get(it->second), in.consume_dict_consumer()); + parse_data(std::get(it->second), in.consume_dict_consumer()); } else if (in.is_list()) { auto it = d.emplace_hint(d.end(), std::move(key), set{}); - parse_data(var::get(it->second), in.consume_list_consumer()); + parse_data(std::get(it->second), in.consume_list_consumer()); } else { throw oxenc::bt_deserialize_invalid{"Data contains invalid bencoded value type"}; } @@ -386,7 +385,7 @@ namespace { if (*scalar_diff == "-") data.erase(k); else if (*scalar_diff == "") - data[k] = var::get(source_val); + data[k] = std::get(source_val); else throw config_error{ "Invalid diff value to apply at key " + k + ": expected '' or '-'"}; @@ -397,23 +396,23 @@ namespace { // with an empty dict. subdict = dict{}; - apply_diff(var::get(subdict), *dict_diff, var::get(source_val)); + apply_diff(std::get(subdict), *dict_diff, std::get(source_val)); } else if (set_diff) { auto& elem = data[k]; if (!std::holds_alternative(elem)) // If not a list (or new) replace with a new empty list elem = set{}; - auto& subset = var::get(elem); + auto& subset = std::get(elem); - for (const auto& added : var::get(*set_diff->begin())) { + for (const auto& added : std::get(*set_diff->begin())) { if (auto s = get_bt_scalar(added)) subset.insert(std::move(*s)); else throw config_error{"Invalid set diff added value: expected int or scalar"}; } for (const auto& removed : - var::get(*std::next(set_diff->begin()))) { + std::get(*std::next(set_diff->begin()))) { if (auto s = get_bt_scalar(removed)) subset.erase(*s); else diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 174bcb12..79c1539c 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -318,7 +317,7 @@ bool ConvoInfoVolatile::erase(const convo::legacy_group& c) { } bool ConvoInfoVolatile::erase(const convo::any& c) { - return var::visit([this](const auto& c) { return erase(c); }, c); + return std::visit([this](const auto& c) { return erase(c); }, c); } bool ConvoInfoVolatile::erase_1to1(std::string_view session_id) { return erase(convo::one_to_one{session_id}); diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 55d2b380..a93da2a5 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -493,7 +492,7 @@ bool UserGroups::erase(const legacy_group_info& c) { } bool UserGroups::erase(const any_group_info& c) { - return var::visit([this](const auto& c) { return erase(c); }, c); + return std::visit([this](const auto& c) { return erase(c); }, c); } bool UserGroups::erase_community(std::string_view base_url, std::string_view room) { return erase(community_info{base_url, room}); diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 7048f402..ad657bd4 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include diff --git a/tests/catch2_bt_format.hpp b/tests/catch2_bt_format.hpp index ef6942c2..9cc00690 100644 --- a/tests/catch2_bt_format.hpp +++ b/tests/catch2_bt_format.hpp @@ -1,9 +1,9 @@ #pragma once #include -#include #include +#include namespace Catch { @@ -22,7 +22,7 @@ struct StringMaker { }; inline std::string StringMaker::convert(const oxenc::bt_value& value) { - return var::visit( + return std::visit( [](const auto& x) { return StringMaker>{}.convert(x); }, diff --git a/tests/test_bt_merge.cpp b/tests/test_bt_merge.cpp index 03af93df..c937e472 100644 --- a/tests/test_bt_merge.cpp +++ b/tests/test_bt_merge.cpp @@ -1,4 +1,5 @@ #include +#include #include "catch2_bt_format.hpp" #include "session/bt_merge.hpp" @@ -29,7 +30,7 @@ TEST_CASE("bt_list sorted merge", "[bt_list][merge]") { bt_list y{2, 4, 8, 16}; auto compare = [](const auto& a, const auto& b) { - return var::get(a) < var::get(b); + return std::get(a) < std::get(b); }; CHECK(session::bt::merge_sorted(x, y, compare) == bt_list{1, 2, 3, 4, 5, 8, 13, 16, 21}); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 1ebf8d8f..152049d6 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -102,11 +101,11 @@ TEST_CASE("config pruning", "[config][prune]") { // shortcut to access a nested dict auto& d(config::dict_value& v) { - return var::get(v); + return std::get(v); } // or set auto& s(config::dict_value& v) { - return var::get(v); + return std::get(v); } std::vector blake2b(std::span data) { From 7aa6eb4655d846ce38cb034921d5fd564a332a99 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Apr 2025 10:58:03 +1100 Subject: [PATCH 536/572] Fix the logging tests --- tests/test_logging.cpp | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 8b24a769..12f5e6cf 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -76,28 +76,17 @@ TEST_CASE("Logging callbacks", "[logging]") { REQUIRE(simple_logs.size() == 2); REQUIRE(full_logs.size() == 2); - -#if defined(__APPLE__) && defined(__clang__) && (__clang_major__ <= 15) CHECK(fixup_log(simple_logs[0]) == - "[] [] [test.a:critical|log.hpp:177] abc 42\n"); - CHECK(fixup_log(simple_logs[1]) == "[] [] [test.b:info|log.hpp:98] hi\n"); - CHECK(fixup_log(full_logs[0]) == - "test.a|critical|[] [] [test.a:critical|log.hpp:177] abc 42\n"); - CHECK(fixup_log(full_logs[1]) == - "test.b|info|[] [] [test.b:info|log.hpp:98] hi\n"); -#else - CHECK(fixup_log(simple_logs[0]) == - "[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( + "[] [] [test.a:critical|test_logging.cpp:{}] abc 42\n"_format( line0)); CHECK(fixup_log(simple_logs[1]) == - "[] [] [test.b:info|tests/test_logging.cpp:{}] hi\n"_format(line1)); + "[] [] [test.b:info|test_logging.cpp:{}] hi\n"_format(line1)); CHECK(fixup_log(full_logs[0]) == - "test.a|critical|[] [] [test.a:critical|tests/test_logging.cpp:{}] abc 42\n"_format( + "test.a|critical|[] [] [test.a:critical|test_logging.cpp:{}] abc 42\n"_format( line0)); CHECK(fixup_log(full_logs[1]) == - "test.b|info|[] [] [test.b:info|tests/test_logging.cpp:{}] hi\n"_format( + "test.b|info|[] [] [test.b:info|test_logging.cpp:{}] hi\n"_format( line1)); -#endif } #ifndef DISABLE_ONIONREQ From 16cf02ede24b12e89505b927c69eba9c3ad78087 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Apr 2025 10:59:54 +1100 Subject: [PATCH 537/572] Ran the formatter... --- tests/test_logging.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 12f5e6cf..5fdde5ed 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -77,8 +77,7 @@ TEST_CASE("Logging callbacks", "[logging]") { REQUIRE(simple_logs.size() == 2); REQUIRE(full_logs.size() == 2); CHECK(fixup_log(simple_logs[0]) == - "[] [] [test.a:critical|test_logging.cpp:{}] abc 42\n"_format( - line0)); + "[] [] [test.a:critical|test_logging.cpp:{}] abc 42\n"_format(line0)); CHECK(fixup_log(simple_logs[1]) == "[] [] [test.b:info|test_logging.cpp:{}] hi\n"_format(line1)); CHECK(fixup_log(full_logs[0]) == From e5f5efb7a0ff038514270b08d294e7acd0c1741a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 3 Apr 2025 12:18:42 +1100 Subject: [PATCH 538/572] Updated to the latest libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 1b1627bb..627f8c88 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 1b1627bbd6113824d134f4d0365dc2c37b120554 +Subproject commit 627f8c8844b33447f6ec884d432c14f381c5d940 From dddc537750104301fae7d3a3dbe990944b7c443e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 3 Apr 2025 13:13:37 +1100 Subject: [PATCH 539/572] Updated version number to `1.3.0` --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 79cfcd56..7f722c79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.2.1 + VERSION 1.3.0 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) From b52ebfe5524b43e99580f587b0834fcd75826c45 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 3 Apr 2025 13:17:53 +1100 Subject: [PATCH 540/572] Fixed a couple of incorrect 'LIBSESSION_WARN_UNUSED' usages --- include/session/config/base.h | 6 +----- include/session/network.h | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/include/session/config/base.h b/include/session/config/base.h index 0fa722a3..eadf4ba8 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -274,11 +274,7 @@ LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* /// /// Outputs: /// - `config_string_list*` -- pointer to the list of hashes; the pointer belongs to the caller -LIBSESSION_EXPORT config_string_list* config_old_hashes(config_object* conf) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; +LIBSESSION_EXPORT config_string_list* config_old_hashes(config_object* conf) LIBSESSION_WARN_UNUSED; /// API: base/config_get_keys /// diff --git a/include/session/network.h b/include/session/network.h index 3d9af0d7..a5e67338 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -77,7 +77,7 @@ LIBSESSION_EXPORT bool network_init( bool use_testnet, bool single_path_mode, bool pre_build_paths, - char* error) __attribute__((warn_unused_result)); + char* error) LIBSESSION_WARN_UNUSED; /// API: network/network_free /// From 96d0d77a99094151c6b1454634e9e279d6768f8d Mon Sep 17 00:00:00 2001 From: yougotwill Date: Thu, 3 Apr 2025 17:35:06 +1100 Subject: [PATCH 541/572] docs: update README to include documentation writing guidelines --- docs/api/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/api/README.md b/docs/api/README.md index bb0f2908..561bc36f 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1,7 +1,11 @@ -# Libsession Utils Documentation Websites +# Documentation Website # Getting Started +## Writing Documentation + +The [api-to-markdown.py](./api-to-markdown.py) script parses specially formatted docstring comments to generate API documentation. We parse the file looking for `///` comment blocks beginning with `/// API: /`. You can read more about the format in the script itself. + ## Install dependencies ```sh From 369adf785810458763b66ffe5ec04fdadf61ab63 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Mar 2025 13:41:20 +1100 Subject: [PATCH 542/572] Updated the network tests to talk to a local callback --- include/session/network.hpp | 4 +- tests/test_network.cpp | 214 +++++++++++++++++++++++++++--------- 2 files changed, 164 insertions(+), 54 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 6baae493..56d3796d 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -672,7 +672,7 @@ class Network { /// Outputs: /// - A tuple containing the status code, headers and body of the decrypted onion request /// response. - std::tuple< + virtual std::tuple< int16_t, std::vector>, std::optional> @@ -689,7 +689,7 @@ class Network { /// Outputs: /// - A tuple containing the status code, headers and body of the decrypted onion request /// response. - std::tuple< + virtual std::tuple< int16_t, std::vector>, std::optional> diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c56d020c..c84ec513 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -5,7 +5,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -51,6 +54,7 @@ class TestNetwork : public Network { std::chrono::milliseconds retry_delay_value = 0ms; std::optional> find_valid_path_response; std::optional last_request_info; + bool handle_onion_requests_as_plaintext = false; TestNetwork( std::optional cache_path, @@ -173,6 +177,75 @@ class TestNetwork : public Network { std::optional) {}); } + std::pair, service_node> create_test_node(uint16_t port) { + oxen::quic::opt::inbound_alpns server_alpns{"oxenstorage"}; + auto server_key_pair = + session::ed25519::ed25519_key_pair(to_unsigned_sv(fmt::format("{:032}", port))); + auto server_x25519_pubkey = session::curve25519::to_curve25519_pubkey( + {server_key_pair.first.data(), server_key_pair.first.size()}); + auto server_x25519_seckey = session::curve25519::to_curve25519_seckey( + {server_key_pair.second.data(), server_key_pair.second.size()}); + auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( + from_unsigned_sv(server_key_pair.second)); + oxen::quic::Address server_local{port}; + session::onionreq::HopEncryption decryptor{ + x25519_seckey::from_bytes(to_usv(server_x25519_seckey)), + x25519_pubkey::from_bytes(to_usv(server_x25519_pubkey)), + true}; + + auto server_cb = [&](oxen::quic::message m) { + nlohmann::json response{{"hf", {1, 0, 0}}, {"t", 1234567890}, {"version", {2, 8, 0}}}; + m.respond(response.dump(), false); + }; + + auto onion_cb = [&](oxen::quic::message m) { + nlohmann::json response{{"hf", {2, 0, 0}}, {"t", 1234567890}, {"version", {2, 8, 0}}}; + m.respond(response.dump(), false); + }; + + oxen::quic::stream_constructor_callback server_constructor = + [&](oxen::quic::Connection& c, oxen::quic::Endpoint& e, std::optional) { + auto s = e.make_shared(c, e); + s->register_handler("info", server_cb); + s->register_handler("onion_req", onion_cb); + return s; + }; + + auto endpoint = net.endpoint(server_local, server_alpns); + endpoint->listen(creds, server_constructor); + + auto node = service_node{ + from_unsigned_sv(server_key_pair.first), + {2, 8, 0}, + INVALID_SWARM_ID, + "127.0.0.1"s, + endpoint->local().port()}; + + return {endpoint, node}; + } + + onion_path create_test_path() { + std::vector path_nodes; + path_nodes.reserve(3); + + for (auto i = 0; i < 3; ++i) + path_nodes.emplace_back(create_test_node(static_cast(1000 + i)).second); + + std::promise>> prom; + establish_connection( + "Test", + path_nodes[0], + 3s, + [&prom](connection_info conn_info, std::optional error) { + prom.set_value({std::move(conn_info), error}); + }); + + // Wait for the result to be set + auto result = prom.get_future().get(); + REQUIRE(result.first.is_valid()); + return onion_path{"Test", std::move(result.first), path_nodes, uint8_t{0}}; + } + // Overridden Functions std::chrono::milliseconds retry_delay(int, std::chrono::milliseconds) override { @@ -286,6 +359,32 @@ class TestNetwork : public Network { std::move(handle_response)); } + std::tuple< + int16_t, + std::vector>, + std::optional> + process_v3_onion_response(session::onionreq::Builder builder, std::string response) override { + call_counts["process_v3_onion_response"]++; + + if (handle_onion_requests_as_plaintext) + return {200, {}, response}; + + return Network::process_v3_onion_response(builder, response); + } + + std::tuple< + int16_t, + std::vector>, + std::optional> + process_v4_onion_response(session::onionreq::Builder builder, std::string response) override { + call_counts["process_v4_onion_response"]++; + + if (handle_onion_requests_as_plaintext) + return {200, {}, response}; + + return Network::process_v4_onion_response(builder, response); + } + // Mocking Functions template @@ -307,7 +406,7 @@ class TestNetwork : public Network { }; } // namespace session::network -TEST_CASE("Network Url Parsing", "[network][parse_url]") { +TEST_CASE("Network", "[network][parse_url]") { auto [proto1, host1, port1, path1] = parse_url("HTTPS://example.com/test"); auto [proto2, host2, port2, path2] = parse_url("http://example2.com:1234/test/123456"); auto [proto3, host3, port3, path3] = parse_url("https://example3.com"); @@ -331,7 +430,7 @@ TEST_CASE("Network Url Parsing", "[network][parse_url]") { CHECK(path4.value_or("NULL") == "/test?value=test"); } -TEST_CASE("Network error handling", "[network]") { +TEST_CASE("Network", "[network][handle_errors]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; auto ed_sk = @@ -709,7 +808,7 @@ TEST_CASE("Network error handling", "[network]") { CHECK(network->get_failure_count(PathType::standard, path) == 0); } -TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { +TEST_CASE("Network", "[network][get_unused_nodes]") { const auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; std::optional network; std::vector snode_cache; @@ -802,7 +901,7 @@ TEST_CASE("Network Path Building", "[network][get_unused_nodes]") { CHECK(unused_nodes.front() == unique_node); } -TEST_CASE("Network Path Building", "[network][build_path]") { +TEST_CASE("Network", "[network][build_path]") { const auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; std::optional network; std::vector snode_cache; @@ -914,7 +1013,7 @@ TEST_CASE("Network Path Building", "[network][build_path]") { CHECK(network->called("paths_changed")); } -TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { +TEST_CASE("Network", "[network][find_valid_path]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto target = test_node(ed_pk, 1); auto test_service_node = service_node{ @@ -970,7 +1069,7 @@ TEST_CASE("Network Find Valid Path", "[network][find_valid_path]") { CHECK(network_single_path.find_valid_path(shared_ip_info, {valid_path}).has_value()); } -TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { +TEST_CASE("Network", "[network][build_path_if_needed]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; auto target = test_node(ed_pk, 0); ; @@ -1068,14 +1167,9 @@ TEST_CASE("Network Enqueue Path Build", "[network][build_path_if_needed]") { std::deque{PathType::upload, PathType::upload}); } -TEST_CASE("Network requests", "[network][establish_connection]") { - auto test_service_node = service_node{ - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, - {2, 8, 0}, - INVALID_SWARM_ID, - "144.76.164.202", - uint16_t{35400}}; +TEST_CASE("Network", "[network][establish_connection]") { auto network = TestNetwork(std::nullopt, true, true, false); + auto test_service_node = network.create_test_node(500).second; std::promise>> prom; network.establish_connection( @@ -1093,21 +1187,17 @@ TEST_CASE("Network requests", "[network][establish_connection]") { CHECK_FALSE(result.second.has_value()); } -TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { - auto test_service_node = service_node{ - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, - {2, 8, 0}, - INVALID_SWARM_ID, - "144.76.164.202", - uint16_t{35400}}; +TEST_CASE("Network", "[network][check_request_queue_timeouts]") { std::optional network; + std::optional test_service_node; std::promise prom; // Test that it doesn't start checking for timeouts when the request doesn't have // a build paths timeout network.emplace(std::nullopt, true, true, false); + test_service_node.emplace(network->create_test_node(500).second); network->send_onion_request( - test_service_node, + *test_service_node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, @@ -1122,9 +1212,10 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { // Test that it does start checking for timeouts when the request has a // paths build timeout network.emplace(std::nullopt, true, true, false); + test_service_node.emplace(network->create_test_node(500).second); network->ignore_calls_to("build_path"); network->send_onion_request( - test_service_node, + *test_service_node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, @@ -1139,9 +1230,10 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { // Test that it fails the request with a timeout if it has a build path timeout // and the path build takes too long network.emplace(std::nullopt, true, true, false); + test_service_node.emplace(network->create_test_node(500).second); network->ignore_calls_to("build_path"); network->send_onion_request( - test_service_node, + *test_service_node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [&prom](bool success, @@ -1161,21 +1253,16 @@ TEST_CASE("Network requests", "[network][check_request_queue_timeouts]") { CHECK(result.timeout); } -TEST_CASE("Network requests", "[network][send_request]") { - auto test_service_node = service_node{ - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, - {2, 8, 0}, - INVALID_SWARM_ID, - "144.76.164.202", - uint16_t{35400}}; +TEST_CASE("Network", "[network][send_request]") { auto network = TestNetwork(std::nullopt, true, true, false); + auto test_service_node = network.create_test_node(500).second; std::promise prom; network.establish_connection( "Test", test_service_node, 3s, - [&prom, &network, test_service_node]( + [&prom, &network, &test_service_node]( connection_info info, std::optional error) { if (!info.is_valid()) return prom.set_value({false, false, -1, {}, error.value_or("Unknown Error")}); @@ -1211,19 +1298,20 @@ TEST_CASE("Network requests", "[network][send_request]") { REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); + REQUIRE(response.contains("hf")); + auto hf = response["hf"].get>(); + CHECK(hf.size() == 3); + CHECK(hf[0] == 1); // Called the info callback CHECK(response.contains("t")); CHECK(response.contains("version")); } -TEST_CASE("Network onion request", "[network][send_onion_request]") { - auto test_service_node = service_node{ - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"_hexbytes, - {2, 8, 0}, - INVALID_SWARM_ID, - "144.76.164.202", - uint16_t{35400}}; - auto network = Network(std::nullopt, true, true, false); +TEST_CASE("Network", "[network][send_onion_request]") { + auto network = TestNetwork(std::nullopt, true, true, false); + auto test_service_node = network.create_test_node(500).second; + auto test_path = network.create_test_path(); + network.handle_onion_requests_as_plaintext = true; + network.set_paths(PathType::standard, {test_path}); std::promise result_promise; network.send_onion_request( @@ -1252,21 +1340,40 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); + REQUIRE(response.contains("hf")); + auto hf = response["hf"].get>(); + CHECK(hf.size() == 3); + CHECK(hf[0] == 2); // Called the onion_req callback CHECK(response.contains("t")); CHECK(response.contains("version")); } -TEST_CASE("Network direct request C API", "[network][network_send_request]") { - network_object* network; - REQUIRE(network_init(&network, nullptr, true, true, false, nullptr)); - std::array target_ip = {144, 76, 164, 202}; +TEST_CASE("Network", "[network][c][network_send_onion_request]") { + auto test_network = std::make_unique(std::nullopt, true, true, false); + auto test_service_node_cpp = test_network->create_test_node(500).second; + auto test_path = test_network->create_test_path(); + test_network->handle_onion_requests_as_plaintext = true; + test_network->set_paths(PathType::standard, {test_path}); + + // Convert TestNetwork to network_object to pass to C API + auto n_object = std::make_unique(); + n_object->internals = test_network.release(); + network_object* network = n_object.release(); + + // Convert test_service_node_cpp to network_service_node to pass to C API + auto ip_v4 = test_service_node_cpp.to_ipv4(); + std::array target_ip = { + static_cast(ip_v4.addr >> 24), + static_cast((ip_v4.addr >> 16) & 0xFF), + static_cast((ip_v4.addr >> 8) & 0xFF), + static_cast(ip_v4.addr & 0xFF)}; auto test_service_node = network_service_node{}; - test_service_node.quic_port = 35400; + test_service_node.quic_port = test_service_node_cpp.port(); std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); - std::strcpy( - test_service_node.ed25519_pubkey_hex, - "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9"); + auto test_pubkey_hex = oxenc::to_hex(test_service_node_cpp.view_remote_key()); + std::strcpy(test_service_node.ed25519_pubkey_hex, test_pubkey_hex.c_str()); + + // Make the request auto body = to_vector("{\"method\":\"info\",\"params\":{}}"); auto result_promise = std::make_shared>(); @@ -1317,13 +1424,16 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); auto response = nlohmann::json::parse(*result.response); - CHECK(response.contains("hf")); + REQUIRE(response.contains("hf")); + auto hf = response["hf"].get>(); + CHECK(hf.size() == 3); + CHECK(hf[0] == 2); // Called the onion_req callback CHECK(response.contains("t")); CHECK(response.contains("version")); network_free(network); } -TEST_CASE("Network swarm", "[network][detail][pubkey_to_swarm_space]") { +TEST_CASE("Network", "[network][detail][pubkey_to_swarm_space]") { x25519_pubkey pk; pk = x25519_pubkey::from_hex( @@ -1361,7 +1471,7 @@ TEST_CASE("Network swarm", "[network][detail][pubkey_to_swarm_space]") { CHECK(session::network::detail::pubkey_to_swarm_space(pk) == 0x0123456789abcdefULL); } -TEST_CASE("Network swarm", "[network][get_swarm]") { +TEST_CASE("Network", "[network][get_swarm]") { auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; std::vector>> swarms = { {100, {}}, {200, {}}, {300, {}}, {399, {}}, {498, {}}, {596, {}}, {694, {}}}; From e7f0cbf26b20ca70e978f4f3dcfb1181e77f7908 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 24 Mar 2025 09:41:08 +1100 Subject: [PATCH 543/572] Updates for the de-network changes in libQuic --- include/session/network.hpp | 4 +- src/network.cpp | 102 +++++++++++++++++------------------- tests/test_network.cpp | 10 ++-- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 56d3796d..9f9c911f 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -204,10 +204,10 @@ class Network { std::thread disk_write_thread; // General values - bool destroyed = false; bool suspended = false; ConnectionStatus status; - oxen::quic::Network net; + + std::shared_ptr loop; std::shared_ptr endpoint; std::unordered_map> paths; std::vector> paths_pending_drop; diff --git a/src/network.cpp b/src/network.cpp index 84b32d84..d3866501 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -505,6 +505,8 @@ Network::Network( should_cache_to_disk{cache_path}, single_path_mode{single_path_mode}, cache_path{cache_path.value_or(default_cache_path)} { + loop = std::make_shared(); + // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { load_cache_from_disk(); @@ -516,14 +518,11 @@ Network::Network( for (int i = 0; i < min_path_count(PathType::standard, single_path_mode); ++i) { auto path_id = "P-{}"_format(random::random_base32(4)); in_progress_path_builds[path_id] = PathType::standard; - net.call_soon([this, path_id] { build_path(path_id, PathType::standard); }); + loop->call_soon([this, path_id] { build_path(path_id, PathType::standard); }); } } Network::~Network() { - // We need to explicitly close the connections at the start of the destructor to prevent ban - // memory errors due to complex logic with the quic::Network instance - destroyed = true; _close_connections(); { @@ -608,7 +607,7 @@ void Network::update_disk_cache_throttled(bool force_immediate_write) { return; has_pending_disk_write = true; - net.call_later(1s, [this]() { + loop->call_later(1s, [this]() { snode_cache_cv.notify_one(); has_pending_disk_write = false; }); @@ -675,7 +674,7 @@ void Network::disk_write_thread_loop() { } void Network::clear_cache() { - net.call([this]() mutable { + loop->call([this]() mutable { { std::lock_guard lock{snode_cache_mutex}; need_clear_cache = true; @@ -685,13 +684,13 @@ void Network::clear_cache() { } size_t Network::snode_cache_size() { - return net.call_get([this]() -> size_t { return snode_cache.size(); }); + return loop->call_get([this]() -> size_t { return snode_cache.size(); }); } // MARK: Connection void Network::suspend() { - net.call([this]() mutable { + loop->call([this]() mutable { suspended = true; close_connections(); log::info(cat, "Suspended."); @@ -699,18 +698,19 @@ void Network::suspend() { } void Network::resume() { - net.call([this]() mutable { + loop->call([this]() mutable { suspended = false; log::info(cat, "Resumed."); }); } void Network::close_connections() { - net.call([this]() mutable { _close_connections(); }); + loop->call([this]() mutable { _close_connections(); }); } void Network::_close_connections() { // Explicitly reset the endpoint to close all connections + endpoint->close_conns(); endpoint.reset(); // Cancel any pending requests (they can't succeed once the connection is closed) @@ -765,9 +765,9 @@ std::chrono::milliseconds Network::retry_delay( } std::shared_ptr Network::get_endpoint() { - return net.call_get([this]() mutable { + return loop->call_get([this]() mutable { if (!endpoint) - endpoint = net.endpoint(quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); + endpoint = quic::Endpoint::endpoint(*loop, quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); return endpoint; }); @@ -848,7 +848,7 @@ void Network::establish_connection( std::optional timeout, std::function error)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, id); - auto currently_suspended = net.call_get([this]() -> bool { return suspended; }); + auto currently_suspended = loop->call_get([this]() -> bool { return suspended; }); // If the network is currently suspended then don't try to open a connection if (currently_suspended) @@ -875,8 +875,8 @@ void Network::establish_connection( [this, id, target, cb, cb_called, conn_future](quic::connection_interface&) mutable { log::trace(cat, "Connection established for {}.", id); - // Just in case, call it within a `net.call` - net.call([&] { + // Just in case, call it within a `loop->call` + loop->call([&] { std::call_once(*cb_called, [&]() { if (cb) { auto conn = conn_future.get(); @@ -901,8 +901,8 @@ void Network::establish_connection( else log::info(cat, "Connection to {} closed for {}.", target.to_string(), id); - // Just in case, call it within a `net.call` - net.call([&] { + // Just in case, call it within a `loop->call` + loop->call([&] { // Trigger the callback first before updating the paths in case this was // triggered when try to establish a connection std::call_once(*cb_called, [&]() { @@ -913,12 +913,6 @@ void Network::establish_connection( } }); - // If the Network instance has been `destroyed` (ie. it's destructor has been - // called) then don't do any of the following logic as it'll likely result in - // undefined behaviours and crashes - if (destroyed) - return; - // Remove the connection from `unused_connection` if present std::erase_if(unused_connections, [&conn, &target](auto& unused_conn) { return (unused_conn.node == target && unused_conn.conn && @@ -974,7 +968,7 @@ void Network::establish_and_store_connection(std::string path_id) { "Unable to establish new connection due to lack of unused nodes, refreshing snode " "cache ({}).", path_id); - return net.call_soon([this, path_id]() { refresh_snode_cache(path_id); }); + return loop->call_soon([this, path_id]() { refresh_snode_cache(path_id); }); } // Otherwise check if it's been too long since the last cache update and, if so, trigger a @@ -983,7 +977,7 @@ void Network::establish_and_store_connection(std::string path_id) { std::chrono::system_clock::now() - last_snode_cache_update); if (cache_lifetime < 0s || cache_lifetime > snode_cache_expiration_duration) - net.call_soon([this]() { refresh_snode_cache(); }); + loop->call_soon([this]() { refresh_snode_cache(); }); // If there are no in progress connections then reset the failure count if (in_progress_connections.empty()) @@ -1013,7 +1007,7 @@ void Network::establish_and_store_connection(std::string path_id) { "Failed to connect to {}, will try another after {}ms.", target_node.to_string(), connection_retry_delay.count()); - return net.call_later(connection_retry_delay, [this, path_id]() { + return loop->call_later(connection_retry_delay, [this, path_id]() { establish_and_store_connection(path_id); }); } @@ -1025,7 +1019,7 @@ void Network::establish_and_store_connection(std::string path_id) { // Kick off the next pending path build since we now have a valid connection if (!path_build_queue.empty()) { in_progress_path_builds[path_id] = path_build_queue.front(); - net.call_soon([this, path_type = path_build_queue.front(), path_id]() { + loop->call_soon([this, path_type = path_build_queue.front(), path_id]() { build_path(path_id, path_type); }); path_build_queue.pop_front(); @@ -1036,7 +1030,7 @@ void Network::establish_and_store_connection(std::string path_id) { // better to be safe and avoid a situation where a path build gets orphaned) if (!path_build_queue.empty() && in_progress_connections.empty()) for ([[maybe_unused]] const auto& _ : path_build_queue) - net.call_soon([this]() { + loop->call_soon([this]() { auto conn_id = "EC-{}"_format(random::random_base32(4)); establish_and_store_connection(conn_id); }); @@ -1077,14 +1071,14 @@ void Network::refresh_snode_cache_complete(std::vector nodes) { // Run any post-refresh processes for (const auto& callback : after_snode_cache_refresh) - net.call_soon([cb = std::move(callback)]() { cb(); }); + loop->call_soon([cb = std::move(callback)]() { cb(); }); after_snode_cache_refresh.clear(); // Resume any queued path builds for (const auto& path_type : path_build_queue) { auto path_id = "P-{}"_format(random::random_base32(4)); in_progress_path_builds[path_id] = path_type; - net.call_soon([this, path_type, path_id]() { build_path(path_id, path_type); }); + loop->call_soon([this, path_type, path_id]() { build_path(path_id, path_type); }); } path_build_queue.clear(); } @@ -1141,7 +1135,7 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r "after {}ms ({}).", cache_refresh_retry_delay.count(), request_id); - return net.call_later(cache_refresh_retry_delay, [this, request_id]() { + return loop->call_later(cache_refresh_retry_delay, [this, request_id]() { refresh_snode_cache_from_seed_nodes(request_id, false); }); } @@ -1164,7 +1158,7 @@ void Network::refresh_snode_cache_from_seed_nodes(std::string request_id, bool r error.value_or("Unknown Error"), cache_refresh_retry_delay.count(), request_id); - return net.call_later( + return loop->call_later( cache_refresh_retry_delay, [this, request_id]() { refresh_snode_cache_from_seed_nodes(request_id, false); }); @@ -1229,7 +1223,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id // If there are still more concurrent refresh_snode_cache requests we want to trigger then // trigger the next one to run in the next run loop if (in_progress_snode_cache_refresh_count < num_snodes_to_refresh_cache_from) - net.call_soon([this, request_id]() { refresh_snode_cache(request_id); }); + loop->call_soon([this, request_id]() { refresh_snode_cache(request_id); }); // Prepare and send the request to retrieve service nodes nlohmann::json payload{ @@ -1290,7 +1284,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id e.what(), cache_refresh_retry_delay.count(), request_id); - return net.call_later(cache_refresh_retry_delay, [this, request_id]() { + return loop->call_later(cache_refresh_retry_delay, [this, request_id]() { refresh_snode_cache(request_id); }); } @@ -1378,7 +1372,7 @@ void Network::build_path(std::string path_id, PathType path_type) { path_id); path_build_queue.emplace_back(path_type); in_progress_path_builds.erase(path_id); - return net.call_soon([this, path_id]() { establish_and_store_connection(path_id); }); + return loop->call_soon([this, path_id]() { establish_and_store_connection(path_id); }); } // Reset the unused nodes list if it's too small @@ -1392,7 +1386,7 @@ void Network::build_path(std::string path_id, PathType path_type) { path_build_failures = 0; path_build_queue.emplace_back(path_type); in_progress_path_builds.erase(path_id); - return net.call_soon([this]() { refresh_snode_cache(); }); + return loop->call_soon([this]() { refresh_snode_cache(); }); } // Build the path @@ -1416,7 +1410,7 @@ void Network::build_path(std::string path_id, PathType path_type) { path_build_failures++; unused_connections.push_front(std::move(conn_info)); auto delay = retry_delay(path_build_failures); - net.call_later(delay, [this, path_id, path_type]() { build_path(path_id, path_type); }); + loop->call_later(delay, [this, path_id, path_type]() { build_path(path_id, path_type); }); return; } @@ -1478,7 +1472,7 @@ void Network::build_path(std::string path_id, PathType path_type) { if (!find_valid_path(request.first, {path})) return false; - net.call_soon([this, info = request.first, cb = std::move(request.second)]() { + loop->call_soon([this, info = request.first, cb = std::move(request.second)]() { _send_onion_request(std::move(info), std::move(cb)); }); return true; @@ -1489,7 +1483,7 @@ void Network::build_path(std::string path_id, PathType path_type) { if (!request_queue[path_type].empty()) { auto additional_path_id = "P-{}"_format(random::random_base32(4)); in_progress_path_builds[additional_path_id] = path_type; - net.call_soon([this, path_type, additional_path_id] { + loop->call_soon([this, path_type, additional_path_id] { build_path(additional_path_id, path_type); }); } else @@ -1646,7 +1640,7 @@ void Network::get_swarm( std::function swarm)> callback) { log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, swarm_pubkey.hex()); - net.call([this, swarm_pubkey, cb = std::move(callback)]() { + loop->call([this, swarm_pubkey, cb = std::move(callback)]() { // If we have a cached swarm then return it auto cached_swarm = swarm_cache[swarm_pubkey.hex()]; if (!cached_swarm.second.empty()) @@ -1658,7 +1652,7 @@ void Network::get_swarm( after_snode_cache_refresh.emplace_back([this, swarm_pubkey, cb = std::move(cb)]() { get_swarm(swarm_pubkey, std::move(cb)); }); - return net.call_soon([this]() { refresh_snode_cache(); }); + return loop->call_soon([this]() { refresh_snode_cache(); }); } // If there is only a single swarm then return it @@ -1704,7 +1698,7 @@ void Network::get_random_nodes( auto request_id = "R-{}"_format(random::random_base32(4)); log::trace(cat, "{} called for {}.", __PRETTY_FUNCTION__, request_id); - net.call([this, request_id, count, cb = std::move(callback)]() mutable { + loop->call([this, request_id, count, cb = std::move(callback)]() mutable { // If we don't have sufficient unused nodes then regenerate it if (unused_nodes.size() < count) unused_nodes = get_unused_nodes(); @@ -1713,7 +1707,7 @@ void Network::get_random_nodes( if (unused_nodes.size() < count) { after_snode_cache_refresh.emplace_back( [this, count, cb = std::move(cb)]() { get_random_nodes(count, cb); }); - return net.call_soon([this]() { refresh_snode_cache(); }); + return loop->call_soon([this]() { refresh_snode_cache(); }); } // Otherwise callback with the requested random number of nodes @@ -1727,8 +1721,8 @@ void Network::get_random_nodes( // MARK: Request Handling void Network::check_request_queue_timeouts(std::optional request_timeout_id_) { - // If the network is suspended (or destroyed) then don't bother checking for timeouts - if (suspended || destroyed) + // If the network is suspended then don't bother checking for timeouts + if (suspended) return; // If there is an existing timeout checking loop then we don't want to start a second @@ -1775,7 +1769,7 @@ void Network::check_request_queue_timeouts(std::optional request_ti } // Otherwise schedule the next check - net.call_later(queued_request_path_build_timeout_frequency, [this]() { + loop->call_later(queued_request_path_build_timeout_frequency, [this]() { check_request_queue_timeouts(request_timeout_id); }); } @@ -1881,16 +1875,16 @@ void Network::_send_onion_request(request_info info, network_response_callback_t // Try to retrieve a valid path for this request, if we can't get one then add the request to // the queue to be run once a path for it has successfully been built - auto path = net.call_get([this, info]() { + auto path = loop->call_get([this, info]() { auto result = find_valid_path(info, paths[info.path_type]); - net.call_soon([this, path_type = info.path_type, found_path = result.has_value()]() { + loop->call_soon([this, path_type = info.path_type, found_path = result.has_value()]() { build_path_if_needed(path_type, found_path); }); return result; }); if (!path) { - return net.call([this, info = std::move(info), cb = std::move(handle_response)]() { + return loop->call([this, info = std::move(info), cb = std::move(handle_response)]() { // If the network is suspended then fail immediately if (suspended) return cb( @@ -1904,7 +1898,7 @@ void Network::_send_onion_request(request_info info, network_response_callback_t // If the request has a path_build_timeout then start the timeout check loop if (info.request_and_path_build_timeout) - net.call_later(queued_request_path_build_timeout_frequency, [this]() { + loop->call_later(queued_request_path_build_timeout_frequency, [this]() { check_request_queue_timeouts(); }); }); @@ -2415,7 +2409,7 @@ void Network::handle_errors( path_name); auto updated_info = info; updated_info.retry_reason = request_info::RetryReason::decryption_failure; - return net.call_soon([this, updated_info, cb = std::move(*handle_response)]() { + return loop->call_soon([this, updated_info, cb = std::move(*handle_response)]() { _send_onion_request(updated_info, std::move(cb)); }); } @@ -2523,7 +2517,7 @@ void Network::handle_errors( auto updated_info = info; updated_info.destination = swarm_copy.front(); updated_info.retry_reason = request_info::RetryReason::redirect; - return net.call_soon( + return loop->call_soon( [this, updated_info, cb = std::move(*handle_response)]() { _send_onion_request(updated_info, std::move(cb)); }); @@ -2585,12 +2579,12 @@ void Network::handle_errors( updated_info.retry_reason = request_info::RetryReason::redirect_swarm_refresh; updated_info.destination = swarm_copy.front(); - net.call_soon([this, updated_info, cb = std::move(cb)]() { + loop->call_soon([this, updated_info, cb = std::move(cb)]() { _send_onion_request(updated_info, std::move(cb)); }); }); }); - return net.call_soon([this, request_id = info.request_id]() { + return loop->call_soon([this, request_id = info.request_id]() { refresh_snode_cache(request_id); }); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c84ec513..6078edb0 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -177,7 +178,7 @@ class TestNetwork : public Network { std::optional) {}); } - std::pair, service_node> create_test_node(uint16_t port) { + std::pair, std::shared_ptr>, service_node> create_test_node(uint16_t port) { oxen::quic::opt::inbound_alpns server_alpns{"oxenstorage"}; auto server_key_pair = session::ed25519::ed25519_key_pair(to_unsigned_sv(fmt::format("{:032}", port))); @@ -205,13 +206,14 @@ class TestNetwork : public Network { oxen::quic::stream_constructor_callback server_constructor = [&](oxen::quic::Connection& c, oxen::quic::Endpoint& e, std::optional) { - auto s = e.make_shared(c, e); + auto s = e.loop.make_shared(c, e); s->register_handler("info", server_cb); s->register_handler("onion_req", onion_cb); return s; }; - auto endpoint = net.endpoint(server_local, server_alpns); + auto loop = std::make_shared(); + auto endpoint = oxen::quic::Endpoint::endpoint(*loop, server_local, server_alpns); endpoint->listen(creds, server_constructor); auto node = service_node{ @@ -221,7 +223,7 @@ class TestNetwork : public Network { "127.0.0.1"s, endpoint->local().port()}; - return {endpoint, node}; + return {{loop, endpoint}, node}; } onion_path create_test_path() { From c487e40a27aa6400e322c94f56f503c9f39b2856 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 28 Mar 2025 13:30:14 +1100 Subject: [PATCH 544/572] Updates for the latest de-network branch changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Renamed `network` to `session_network` to prevent a naming collision that prevented debugging via Xcode • Added a `TestServer` type to the network tests to more easily manage the relevant objects (and their memory) • Fixed build errors --- .../session/{network.h => session_network.h} | 0 .../{network.hpp => session_network.hpp} | 4 +- src/CMakeLists.txt | 2 +- src/onionreq/builder.cpp | 2 +- src/{network.cpp => session_network.cpp} | 33 ++++--- tests/CMakeLists.txt | 3 +- tests/test_onionreq.cpp | 2 +- ...t_network.cpp => test_session_network.cpp} | 93 +++++++++++-------- 8 files changed, 82 insertions(+), 57 deletions(-) rename include/session/{network.h => session_network.h} (100%) rename include/session/{network.hpp => session_network.hpp} (99%) rename src/{network.cpp => session_network.cpp} (99%) rename tests/{test_network.cpp => test_session_network.cpp} (96%) diff --git a/include/session/network.h b/include/session/session_network.h similarity index 100% rename from include/session/network.h rename to include/session/session_network.h diff --git a/include/session/network.hpp b/include/session/session_network.hpp similarity index 99% rename from include/session/network.hpp rename to include/session/session_network.hpp index 9f9c911f..7208c0cf 100644 --- a/include/session/network.hpp +++ b/include/session/session_network.hpp @@ -86,7 +86,7 @@ struct service_node : public oxen::quic::RemoteAddress { struct connection_info { service_node node; std::shared_ptr pending_requests; - std::shared_ptr conn; + std::shared_ptr conn; std::shared_ptr stream; bool is_valid() const { return conn && stream && !stream->is_closing(); }; @@ -206,7 +206,7 @@ class Network { // General values bool suspended = false; ConnectionStatus status; - + std::shared_ptr loop; std::shared_ptr endpoint; std::unordered_map> paths; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b6c53f22..6c88bb61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,7 +104,7 @@ if(ENABLE_ONIONREQ) onionreq/key_types.cpp onionreq/parser.cpp onionreq/response_parser.cpp - network.cpp + session_network.cpp ) target_link_libraries(onionreq diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index ad657bd4..bea6688c 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -22,10 +22,10 @@ #include #include "session/export.h" -#include "session/network.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/hop_encryption.hpp" #include "session/onionreq/key_types.hpp" +#include "session/session_network.hpp" #include "session/util.hpp" #include "session/xed25519.hpp" diff --git a/src/network.cpp b/src/session_network.cpp similarity index 99% rename from src/network.cpp rename to src/session_network.cpp index d3866501..62110ca4 100644 --- a/src/network.cpp +++ b/src/session_network.cpp @@ -1,4 +1,4 @@ -#include "session/network.hpp" +#include "session/session_network.hpp" #include #include @@ -24,11 +24,11 @@ #include "session/ed25519.hpp" #include "session/export.h" #include "session/file.hpp" -#include "session/network.h" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/onionreq/response_parser.hpp" +#include "session/session_network.h" #include "session/util.hpp" using namespace oxen; @@ -506,7 +506,7 @@ Network::Network( single_path_mode{single_path_mode}, cache_path{cache_path.value_or(default_cache_path)} { loop = std::make_shared(); - + // Load the cache from disk and start the disk write thread if (should_cache_to_disk) { load_cache_from_disk(); @@ -523,7 +523,13 @@ Network::Network( } Network::~Network() { - _close_connections(); + // Flag the network as suspended when we start destroying to ensure no new requests get started + // (which could result in additional calls being added to the `loop` incorrectly and cause bad + // memory crashes) + suspended = true; + + // Trigger a 'call_get' to block until the endpoint has been destroyed + loop->call_get([this]() mutable { _close_connections(); }); { std::lock_guard lock{snode_cache_mutex}; @@ -709,8 +715,9 @@ void Network::close_connections() { } void Network::_close_connections() { - // Explicitly reset the endpoint to close all connections - endpoint->close_conns(); + // Explicitly close all connections then reset the endpoint + if (endpoint) + endpoint->close_conns(); endpoint.reset(); // Cancel any pending requests (they can't succeed once the connection is closed) @@ -767,7 +774,8 @@ std::chrono::milliseconds Network::retry_delay( std::shared_ptr Network::get_endpoint() { return loop->call_get([this]() mutable { if (!endpoint) - endpoint = quic::Endpoint::endpoint(*loop, quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); + endpoint = quic::Endpoint::endpoint( + *loop, quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); return endpoint; }); @@ -860,7 +868,7 @@ void Network::establish_connection( auto cb_called = std::make_shared(); auto cb = std::make_shared)>>( std::move(callback)); - auto conn_promise = std::promise>(); + auto conn_promise = std::promise>(); auto conn_future = conn_promise.get_future().share(); auto handshake_timeout = timeout ? std::optional{quic::opt::handshake_timeout{ @@ -872,7 +880,7 @@ void Network::establish_connection( creds, quic::opt::keep_alive{10s}, handshake_timeout, - [this, id, target, cb, cb_called, conn_future](quic::connection_interface&) mutable { + [this, id, target, cb, cb_called, conn_future](quic::Connection&) mutable { log::trace(cat, "Connection established for {}.", id); // Just in case, call it within a `loop->call` @@ -891,7 +899,7 @@ void Network::establish_connection( }); }, [this, target, id, cb, cb_called, conn_future]( - quic::connection_interface& conn, uint64_t error_code) mutable { + quic::Connection& conn, uint64_t error_code) mutable { if (error_code == static_cast(NGTCP2_ERR_HANDSHAKE_TIMEOUT)) log::info( cat, @@ -1410,7 +1418,8 @@ void Network::build_path(std::string path_id, PathType path_type) { path_build_failures++; unused_connections.push_front(std::move(conn_info)); auto delay = retry_delay(path_build_failures); - loop->call_later(delay, [this, path_id, path_type]() { build_path(path_id, path_type); }); + loop->call_later( + delay, [this, path_id, path_type]() { build_path(path_id, path_type); }); return; } @@ -2275,7 +2284,7 @@ Network::process_v4_onion_response(Builder builder, std::string response) { // MARK: Error Handling std::pair Network::validate_response(quic::message resp, bool is_bencoded) { - std::string body = resp.body_str(); + std::string body = std::string(resp.body()); if (resp.timed_out) throw std::runtime_error{"Timed out"}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ef80633..a8eea027 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,7 +30,7 @@ set(LIB_SESSION_UTESTS_SOURCES ) if (ENABLE_ONIONREQ) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_network.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_session_network.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) endif() @@ -44,6 +44,7 @@ target_link_libraries(test_libs INTERFACE if (ENABLE_ONIONREQ) target_link_libraries(test_libs INTERFACE libsession::onionreq) + target_include_directories(test_libs INTERFACE ${CMAKE_BINARY_DIR}/static-deps/include) else() target_compile_definitions(test_libs INTERFACE DISABLE_ONIONREQ) endif() diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index dba54503..c73935c5 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,7 +1,7 @@ #include -#include #include #include +#include #include "utils.hpp" diff --git a/tests/test_network.cpp b/tests/test_session_network.cpp similarity index 96% rename from tests/test_network.cpp rename to tests/test_session_network.cpp index 6078edb0..9df77619 100644 --- a/tests/test_network.cpp +++ b/tests/test_session_network.cpp @@ -1,16 +1,16 @@ #include -#include +#include #include -#include #include #include #include +#include #include #include -#include #include #include +#include #include #include "utils.hpp" @@ -20,6 +20,18 @@ using namespace session::onionreq; using namespace session::network; namespace { +struct TestServer { + std::shared_ptr loop; + std::shared_ptr endpoint; + service_node node; + + ~TestServer() { + loop->call_get([&]() { endpoint->close_conns(); }); + endpoint.reset(); + loop.reset(); + } +}; + struct Result { bool success; bool timeout; @@ -178,20 +190,20 @@ class TestNetwork : public Network { std::optional) {}); } - std::pair, std::shared_ptr>, service_node> create_test_node(uint16_t port) { + std::shared_ptr create_test_node(uint16_t port) { oxen::quic::opt::inbound_alpns server_alpns{"oxenstorage"}; auto server_key_pair = - session::ed25519::ed25519_key_pair(to_unsigned_sv(fmt::format("{:032}", port))); + session::ed25519::ed25519_key_pair(to_span(fmt::format("{:032}", port))); auto server_x25519_pubkey = session::curve25519::to_curve25519_pubkey( {server_key_pair.first.data(), server_key_pair.first.size()}); auto server_x25519_seckey = session::curve25519::to_curve25519_seckey( {server_key_pair.second.data(), server_key_pair.second.size()}); auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( - from_unsigned_sv(server_key_pair.second)); + to_string_view(server_key_pair.second)); oxen::quic::Address server_local{port}; session::onionreq::HopEncryption decryptor{ - x25519_seckey::from_bytes(to_usv(server_x25519_seckey)), - x25519_pubkey::from_bytes(to_usv(server_x25519_pubkey)), + x25519_seckey::from_bytes(to_span(server_x25519_seckey)), + x25519_pubkey::from_bytes(to_span(server_x25519_pubkey)), true}; auto server_cb = [&](oxen::quic::message m) { @@ -217,21 +229,24 @@ class TestNetwork : public Network { endpoint->listen(creds, server_constructor); auto node = service_node{ - from_unsigned_sv(server_key_pair.first), + to_string_view(server_key_pair.first), {2, 8, 0}, INVALID_SWARM_ID, "127.0.0.1"s, endpoint->local().port()}; - return {{loop, endpoint}, node}; + return std::make_shared(loop, endpoint, node); } - onion_path create_test_path() { + std::pair>, onion_path> create_test_path() { + std::vector> path_servers; std::vector path_nodes; path_nodes.reserve(3); - for (auto i = 0; i < 3; ++i) - path_nodes.emplace_back(create_test_node(static_cast(1000 + i)).second); + for (auto i = 0; i < 3; ++i) { + path_servers.emplace_back(create_test_node(static_cast(1000 + i))); + path_nodes.emplace_back(path_servers[i]->node); + } std::promise>> prom; establish_connection( @@ -245,7 +260,7 @@ class TestNetwork : public Network { // Wait for the result to be set auto result = prom.get_future().get(); REQUIRE(result.first.is_valid()); - return onion_path{"Test", std::move(result.first), path_nodes, uint8_t{0}}; + return {path_servers, onion_path{"Test", std::move(result.first), path_nodes, uint8_t{0}}}; } // Overridden Functions @@ -524,7 +539,7 @@ TEST_CASE("Network", "[network][handle_errors]") { CHECK(network->get_failure_count(target3) == 0); CHECK(network->get_failure_count(PathType::standard, path) == 1); - // // Check general error handling with no response (too many path failures) + // Check general error handling with no response (too many path failures) path = onion_path{"Test", {target, nullptr, nullptr, nullptr}, {target, target2, target3}, 9}; network.emplace(std::nullopt, true, true, false); network->set_suspended(true); // Make no requests in this test @@ -555,7 +570,7 @@ TEST_CASE("Network", "[network][handle_errors]") { CHECK(network->get_failure_count(target3) == 1); // Other nodes incremented CHECK(network->get_failure_count(PathType::standard, path) == 0); // Path dropped and reset - // // Check general error handling with a path and specific node failure + // Check general error handling with a path and specific node failure path = onion_path{"Test", {target, nullptr, nullptr, nullptr}, {target, target2, target3}, 0}; auto response = std::string{"Next node not found: "} + ed25519_pubkey::from_bytes(ed_pk2).hex(); network.emplace(std::nullopt, true, true, false); @@ -1171,12 +1186,12 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { TEST_CASE("Network", "[network][establish_connection]") { auto network = TestNetwork(std::nullopt, true, true, false); - auto test_service_node = network.create_test_node(500).second; + auto test_server = network.create_test_node(500); std::promise>> prom; network.establish_connection( "Test", - test_service_node, + test_server->node, 3s, [&prom](connection_info info, std::optional error) { prom.set_value({info, error}); @@ -1191,15 +1206,15 @@ TEST_CASE("Network", "[network][establish_connection]") { TEST_CASE("Network", "[network][check_request_queue_timeouts]") { std::optional network; - std::optional test_service_node; + std::optional> test_server; std::promise prom; // Test that it doesn't start checking for timeouts when the request doesn't have // a build paths timeout network.emplace(std::nullopt, true, true, false); - test_service_node.emplace(network->create_test_node(500).second); + test_server.emplace(network->create_test_node(500)); network->send_onion_request( - *test_service_node, + (*test_server)->node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, @@ -1214,10 +1229,10 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { // Test that it does start checking for timeouts when the request has a // paths build timeout network.emplace(std::nullopt, true, true, false); - test_service_node.emplace(network->create_test_node(500).second); + test_server.emplace(network->create_test_node(500)); network->ignore_calls_to("build_path"); network->send_onion_request( - *test_service_node, + (*test_server)->node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [](bool, @@ -1232,10 +1247,10 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { // Test that it fails the request with a timeout if it has a build path timeout // and the path build takes too long network.emplace(std::nullopt, true, true, false); - test_service_node.emplace(network->create_test_node(500).second); + test_server.emplace(network->create_test_node(500)); network->ignore_calls_to("build_path"); network->send_onion_request( - *test_service_node, + (*test_server)->node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [&prom](bool success, @@ -1257,21 +1272,21 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { TEST_CASE("Network", "[network][send_request]") { auto network = TestNetwork(std::nullopt, true, true, false); - auto test_service_node = network.create_test_node(500).second; + auto test_server = network.create_test_node(500); std::promise prom; network.establish_connection( "Test", - test_service_node, + test_server->node, 3s, - [&prom, &network, &test_service_node]( + [&prom, &network, &test_server]( connection_info info, std::optional error) { if (!info.is_valid()) return prom.set_value({false, false, -1, {}, error.value_or("Unknown Error")}); network.send_request( request_info::make( - test_service_node, + test_server->node, to_vector("{}"), std::nullopt, 3s, @@ -1310,14 +1325,14 @@ TEST_CASE("Network", "[network][send_request]") { TEST_CASE("Network", "[network][send_onion_request]") { auto network = TestNetwork(std::nullopt, true, true, false); - auto test_service_node = network.create_test_node(500).second; - auto test_path = network.create_test_path(); + auto test_server = network.create_test_node(500); + auto [test_path_servers, test_path] = network.create_test_path(); network.handle_onion_requests_as_plaintext = true; network.set_paths(PathType::standard, {test_path}); std::promise result_promise; network.send_onion_request( - test_service_node, + test_server->node, to_vector("{\"method\":\"info\",\"params\":{}}"), std::nullopt, [&result_promise]( @@ -1352,27 +1367,27 @@ TEST_CASE("Network", "[network][send_onion_request]") { TEST_CASE("Network", "[network][c][network_send_onion_request]") { auto test_network = std::make_unique(std::nullopt, true, true, false); - auto test_service_node_cpp = test_network->create_test_node(500).second; - auto test_path = test_network->create_test_path(); + auto test_server_cpp = test_network->create_test_node(500); + auto [test_path_servers_cpp, test_path_cpp] = test_network->create_test_path(); test_network->handle_onion_requests_as_plaintext = true; - test_network->set_paths(PathType::standard, {test_path}); + test_network->set_paths(PathType::standard, {test_path_cpp}); // Convert TestNetwork to network_object to pass to C API auto n_object = std::make_unique(); n_object->internals = test_network.release(); network_object* network = n_object.release(); - // Convert test_service_node_cpp to network_service_node to pass to C API - auto ip_v4 = test_service_node_cpp.to_ipv4(); + // Convert test_server_cpp->node to network_service_node to pass to C API + auto ip_v4 = test_server_cpp->node.to_ipv4(); std::array target_ip = { static_cast(ip_v4.addr >> 24), static_cast((ip_v4.addr >> 16) & 0xFF), static_cast((ip_v4.addr >> 8) & 0xFF), static_cast(ip_v4.addr & 0xFF)}; auto test_service_node = network_service_node{}; - test_service_node.quic_port = test_service_node_cpp.port(); + test_service_node.quic_port = test_server_cpp->node.port(); std::copy(target_ip.begin(), target_ip.end(), test_service_node.ip); - auto test_pubkey_hex = oxenc::to_hex(test_service_node_cpp.view_remote_key()); + auto test_pubkey_hex = oxenc::to_hex(test_server_cpp->node.view_remote_key()); std::strcpy(test_service_node.ed25519_pubkey_hex, test_pubkey_hex.c_str()); // Make the request From 969b008e0c6cac17bb770190d16dda5f8951c9c0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 4 Apr 2025 13:09:31 +1100 Subject: [PATCH 545/572] Fixed an infinite loop when refreshing the snode cache --- include/session/network.hpp | 4 ++-- src/network.cpp | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/session/network.hpp b/include/session/network.hpp index 6baae493..3fae806f 100644 --- a/include/session/network.hpp +++ b/include/session/network.hpp @@ -217,8 +217,8 @@ class Network { std::unordered_map>> swarm_cache; // Snode refresh state - int snode_cache_refresh_failure_count; - int in_progress_snode_cache_refresh_count; + int snode_cache_refresh_failure_count = 0; + int in_progress_snode_cache_refresh_count = 0; std::optional current_snode_cache_refresh_request_id; std::vector> after_snode_cache_refresh; std::optional> unused_snode_refresh_nodes; diff --git a/src/network.cpp b/src/network.cpp index 84b32d84..5cde3490 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1207,6 +1207,8 @@ void Network::refresh_snode_cache(std::optional existing_request_id // `snode_refresh_results` so we can use it to track the results from the different requests) if (!current_snode_cache_refresh_request_id) { log::info(cat, "Refreshing snode cache ({}).", request_id); + snode_cache_refresh_failure_count = 0; + in_progress_snode_cache_refresh_count = 0; current_snode_cache_refresh_request_id = request_id; snode_refresh_results = std::make_shared>>(); } @@ -1286,7 +1288,7 @@ void Network::refresh_snode_cache(std::optional existing_request_id log::error( cat, "Failed to retrieve nodes from one target when refreshing cache due to " - "error: {} Will try another target after {}ms ({}).", + "error: {}, Will try another target after {}ms ({}).", e.what(), cache_refresh_retry_delay.count(), request_id); From 5766c7a84399eac1fbef064fe753e793eabb1db7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 4 Apr 2025 15:59:12 +1100 Subject: [PATCH 546/572] Updated the version number to 1.3.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 00800884..5c810e5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.3.0 + VERSION 1.3.1 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) From 77c4845e683df740932ffe86e3a18090ca4ae58e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 4 Apr 2025 16:53:45 +1100 Subject: [PATCH 547/572] Changed the 'static macOS' build to 'arm64' --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 2d71d508..97c0285e 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -397,7 +397,7 @@ local static_build(name, ] ), - mac_pipeline('Static macOS', build=[ + mac_pipeline('Static macOS', arch='arm64', build=[ 'export JOBS=6', './utils/macos.sh', 'cd build-macos && ../utils/ci/drone-static-upload.sh', From 795b47a81c3442487ebe3ff55beeadb0a1ab2b15 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 7 Apr 2025 11:05:16 +1000 Subject: [PATCH 548/572] Disabled MTU discovery & C API tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated some C types from `const char**` to `const char* const*` to fix up the Swift interface • Updated to the latest libQuic and disabled MTU discovery (caused issues with some networks) --- external/oxen-libquic | 2 +- include/session/config/base.h | 4 ++-- include/session/config/groups/keys.h | 2 +- include/session/multi_encrypt.h | 8 ++++---- include/session/network.h | 8 ++++---- src/config/base.cpp | 4 ++-- src/config/groups/keys.cpp | 2 +- src/multi_encrypt.cpp | 8 ++++---- src/network.cpp | 2 +- tests/test_network.cpp | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 627f8c88..8d0b584e 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 627f8c8844b33447f6ec884d432c14f381c5d940 +Subproject commit 8d0b584edf8ed6a21450b7aea64ad47a55cc97b3 diff --git a/include/session/config/base.h b/include/session/config/base.h index eadf4ba8..b7d4638c 100644 --- a/include/session/config/base.h +++ b/include/session/config/base.h @@ -95,8 +95,8 @@ typedef struct config_string_list { LIBSESSION_EXPORT config_string_list* config_merge( config_object* conf, - const char** msg_hashes, - const unsigned char** configs, + const char* const* msg_hashes, + const unsigned char* const* configs, const size_t* lengths, size_t count) LIBSESSION_WARN_UNUSED; diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 6c50f23f..4fff03ec 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -333,7 +333,7 @@ LIBSESSION_EXPORT void groups_keys_dump( /// them on failure. LIBSESSION_EXPORT bool groups_keys_key_supplement( config_group_keys* conf, - const char** sids, + const char* const* sids, size_t sids_len, unsigned char** message, size_t* message_len); diff --git a/include/session/multi_encrypt.h b/include/session/multi_encrypt.h index f6a39bb4..1e7f313a 100644 --- a/include/session/multi_encrypt.h +++ b/include/session/multi_encrypt.h @@ -53,10 +53,10 @@ extern "C" { /// responsibility to `free()` this buffer (if non-NULL) when done with it! LIBSESSION_EXPORT unsigned char* session_encrypt_for_multiple_simple( size_t* out_len, - const unsigned char** messages, + const unsigned char* const* messages, const size_t* message_lengths, size_t n_messages, - const unsigned char** recipients, + const unsigned char* const* recipients, size_t n_recipients, const unsigned char* x25519_privkey, const unsigned char* x25519_pubkey, @@ -69,10 +69,10 @@ LIBSESSION_EXPORT unsigned char* session_encrypt_for_multiple_simple( /// from the Ed25519 key on the fly. LIBSESSION_EXPORT unsigned char* session_encrypt_for_multiple_simple_ed25519( size_t* out_len, - const unsigned char** messages, + const unsigned char* const* messages, const size_t* message_lengths, size_t n_messages, - const unsigned char** recipients, + const unsigned char* const* recipients, size_t n_recipients, const unsigned char* ed25519_secret_key, const char* domain, diff --git a/include/session/network.h b/include/session/network.h index a5e67338..534d93b2 100644 --- a/include/session/network.h +++ b/include/session/network.h @@ -37,8 +37,8 @@ typedef struct network_server_destination { const char* endpoint; uint16_t port; const char* x25519_pubkey; - const char** headers; - const char** header_values; + const char* const* headers; + const char* const* header_values; size_t headers_size; } network_server_destination; @@ -197,8 +197,8 @@ typedef void (*network_onion_response_callback_t)( bool success, bool timeout, int16_t status_code, - const char** headers, - const char** header_values, + const char* const* headers, + const char* const* header_values, size_t headers_size, const char* response, size_t response_size, diff --git a/src/config/base.cpp b/src/config/base.cpp index 2011ae1a..d2ec981b 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -764,8 +764,8 @@ LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf) { LIBSESSION_EXPORT config_string_list* config_merge( config_object* conf, - const char** msg_hashes, - const unsigned char** configs, + const char* const* msg_hashes, + const unsigned char* const* configs, const size_t* lengths, size_t count) { return wrap_exceptions(conf, [&] { diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index dddf4f37..c9917075 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1659,7 +1659,7 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( LIBSESSION_C_API bool groups_keys_key_supplement( config_group_keys* conf, - const char** sids, + const char* const* sids, size_t sids_len, unsigned char** message, size_t* message_len) { diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 28cddaa0..93c7f72c 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -259,10 +259,10 @@ static unsigned char* to_c_buffer(std::span x, size_t* out_ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( size_t* out_len, - const unsigned char** messages, + const unsigned char* const* messages, const size_t* message_lengths, size_t n_messages, - const unsigned char** recipients, + const unsigned char* const* recipients, size_t n_recipients, const unsigned char* x25519_privkey, const unsigned char* x25519_pubkey, @@ -298,10 +298,10 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( size_t* out_len, - const unsigned char** messages, + const unsigned char* const* messages, const size_t* message_lengths, size_t n_messages, - const unsigned char** recipients, + const unsigned char* const* recipients, size_t n_recipients, const unsigned char* ed25519_secret_key, const char* domain, diff --git a/src/network.cpp b/src/network.cpp index 5cde3490..aad5fd1d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -767,7 +767,7 @@ std::chrono::milliseconds Network::retry_delay( std::shared_ptr Network::get_endpoint() { return net.call_get([this]() mutable { if (!endpoint) - endpoint = net.endpoint(quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}); + endpoint = net.endpoint(quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}, quic::opt::disable_mtu_discovery{}); return endpoint; }); diff --git a/tests/test_network.cpp b/tests/test_network.cpp index c56d020c..9905e8b5 100644 --- a/tests/test_network.cpp +++ b/tests/test_network.cpp @@ -1281,8 +1281,8 @@ TEST_CASE("Network direct request C API", "[network][network_send_request]") { [](bool success, bool timeout, int16_t status_code, - const char** headers, - const char** header_values, + const char* const* headers, + const char* const* header_values, size_t headers_size, const char* c_response, size_t response_size, From 25beeed49ded0d7770b67230272bfc56c66c1dfe Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 7 Apr 2025 11:11:38 +1000 Subject: [PATCH 549/572] Ran the formatter... --- src/network.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/network.cpp b/src/network.cpp index aad5fd1d..d49f536c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -767,7 +767,10 @@ std::chrono::milliseconds Network::retry_delay( std::shared_ptr Network::get_endpoint() { return net.call_get([this]() mutable { if (!endpoint) - endpoint = net.endpoint(quic::Address{"0.0.0.0", 0}, quic::opt::alpns{ALPN}, quic::opt::disable_mtu_discovery{}); + endpoint = net.endpoint( + quic::Address{"0.0.0.0", 0}, + quic::opt::alpns{ALPN}, + quic::opt::disable_mtu_discovery{}); return endpoint; }); From 1309b3231c27e7c33f3280a452ecb5b6f9a77a01 Mon Sep 17 00:00:00 2001 From: yougotwill Date: Mon, 7 Apr 2025 18:51:05 +1000 Subject: [PATCH 550/572] fix: update comment for clarity in config message example test --- tests/test_configdata.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 30acb8c3..31819cb3 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -902,7 +902,8 @@ const auto m126_expected = // clang-format on TEST_CASE("config message example 3 - simple conflict", "[config][example][conflict]") { - /// This is the "Simple conflict resolution" example described in docs/api/docs/config-merge-logic.md + /// This is the "Simple conflict resolution" example described in docs/api/ + /// docs/config-merge-logic.md MutableConfigMessage m124{m123_expected}; REQUIRE(m124.seqno() == 124); From faf931cf9fc8a81edf776562f2815651d04256e3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 9 Apr 2025 15:59:32 +1000 Subject: [PATCH 551/572] Renaming 'network' to 'session_network' because xcode can't debug --- include/session/{network.h => session_network.h} | 0 include/session/{network.hpp => session_network.hpp} | 0 src/CMakeLists.txt | 2 +- src/onionreq/builder.cpp | 2 +- src/{network.cpp => session_network.cpp} | 4 ++-- tests/CMakeLists.txt | 2 +- tests/test_onionreq.cpp | 2 +- tests/{test_network.cpp => test_session_network.cpp} | 4 ++-- 8 files changed, 8 insertions(+), 8 deletions(-) rename include/session/{network.h => session_network.h} (100%) rename include/session/{network.hpp => session_network.hpp} (100%) rename src/{network.cpp => session_network.cpp} (99%) rename tests/{test_network.cpp => test_session_network.cpp} (99%) diff --git a/include/session/network.h b/include/session/session_network.h similarity index 100% rename from include/session/network.h rename to include/session/session_network.h diff --git a/include/session/network.hpp b/include/session/session_network.hpp similarity index 100% rename from include/session/network.hpp rename to include/session/session_network.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b6c53f22..6c88bb61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,7 +104,7 @@ if(ENABLE_ONIONREQ) onionreq/key_types.cpp onionreq/parser.cpp onionreq/response_parser.cpp - network.cpp + session_network.cpp ) target_link_libraries(onionreq diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index ad657bd4..a0374ba2 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -22,7 +22,7 @@ #include #include "session/export.h" -#include "session/network.hpp" +#include "session/session_network.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/hop_encryption.hpp" #include "session/onionreq/key_types.hpp" diff --git a/src/network.cpp b/src/session_network.cpp similarity index 99% rename from src/network.cpp rename to src/session_network.cpp index d49f536c..8d080a11 100644 --- a/src/network.cpp +++ b/src/session_network.cpp @@ -1,4 +1,4 @@ -#include "session/network.hpp" +#include "session/session_network.hpp" #include #include @@ -24,7 +24,7 @@ #include "session/ed25519.hpp" #include "session/export.h" #include "session/file.hpp" -#include "session/network.h" +#include "session/session_network.h" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ef80633..07338de6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,7 +30,7 @@ set(LIB_SESSION_UTESTS_SOURCES ) if (ENABLE_ONIONREQ) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_network.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_session_network.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) endif() diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index dba54503..4b752a37 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/tests/test_network.cpp b/tests/test_session_network.cpp similarity index 99% rename from tests/test_network.cpp rename to tests/test_session_network.cpp index 9905e8b5..efb968d2 100644 --- a/tests/test_network.cpp +++ b/tests/test_session_network.cpp @@ -1,11 +1,11 @@ #include -#include +#include #include #include #include #include -#include +#include #include #include From c7b6ff761cb2f4a5d0c5de2a76051defdd28261a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 9 Apr 2025 16:02:42 +1000 Subject: [PATCH 552/572] Added general xchacha20 encrypt/decrypt functions, doc tweaks --- include/session/config/groups/info.hpp | 2 +- include/session/random.h | 5 +- include/session/session_encrypt.h | 48 +++++++++++ include/session/session_encrypt.hpp | 26 ++++++ src/session_encrypt.cpp | 109 +++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 3 deletions(-) diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index cc1c21b8..64c2e48f 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -124,7 +124,7 @@ class Info : public ConfigBase { /// /// Returns the group description, or std::nullopt if there is no group description set. /// - /// If given a description longer than `Info::DESCRIPTION_MAX_LENGTH` (2000) bytes it will be + /// If given a description longer than `Info::DESCRIPTION_MAX_LENGTH` bytes it will be /// truncated. /// /// Inputs: None diff --git a/include/session/random.h b/include/session/random.h index 2d560e38..70136315 100644 --- a/include/session/random.h +++ b/include/session/random.h @@ -16,9 +16,10 @@ extern "C" { /// - `size` -- [in] number of bytes to be generated. /// /// Outputs: -/// - `unsigned char*` -- pointer to random bytes of `size` bytes. +/// - `unsigned char*` -- pointer to random bytes of `size` bytes. The caller is responsible for +/// freeing the data when done! LIBSESSION_EXPORT unsigned char* session_random(size_t size); #ifdef __cplusplus } -#endif +#endif \ No newline at end of file diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 56b5f90f..f5a1c2b6 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -229,6 +229,54 @@ LIBSESSION_EXPORT bool session_decrypt_push_notification( LIBSESSION_EXPORT bool session_compute_message_hash( const char* pubkey_hex_in, int16_t ns, const char* base64_data_in, char* hash_out); +/// API: crypto/session_encrypt_xchacha20 +/// +/// Encrypts a value with a given key using xchacha20. +/// +/// Inputs: +/// - `plaintext_in` -- [in] the data to encrypt. +/// - `plaintext_len` -- [in] the length of `plaintext_in`. +/// - `enc_key_in` -- [in] the key to use for encryption (32 bytes). +/// - `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 *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the encryption was successful, false if encryption failed. +LIBSESSION_EXPORT bool session_encrypt_xchacha20( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* enc_key_in, /* 32 bytes */ + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: crypto/session_decrypt_xchacha20 +/// +/// Decrypts a value that was encrypted with the `encrypt_xchacha20` function. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] the data to decrypt. +/// - `ciphertext_len` -- [in] the length of `ciphertext_in`. +/// - `enc_key_in` -- [in] the key to use for decryption (32 bytes). +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted 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 *unless* the function returns +/// 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. +/// +/// Outputs: +/// - `bool` -- True if the decryption was successful, false if decryption failed. +LIBSESSION_EXPORT bool session_decrypt_xchacha20( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* enc_key_in, /* 32 bytes */ + unsigned char** plaintext_out, + size_t* plaintext_len); + #ifdef __cplusplus } #endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 3a15a015..c357a2f0 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -295,4 +295,30 @@ std::vector decrypt_push_notification( std::string compute_message_hash( const std::string_view pubkey_hex, int16_t ns, std::string_view data); +/// API: crypto/encrypt_xchacha20 +/// +/// Encrypts a value with a given key using xchacha20. +/// +/// Inputs: +/// - `plaintext` -- the data to encrypt. +/// - `enc_key` -- the key to use for encryption (32 bytes). +/// +/// Outputs: +/// - `std::vector` -- the resulting ciphertext. +std::vector encrypt_xchacha20( + std::span plaintext, std::span enc_key); + +/// API: crypto/decrypt_xchacha20 +/// +/// Decrypts a value that was encrypted with the `encrypt_xchacha20` function. +/// +/// Inputs: +/// - `ciphertext` -- the data to decrypt. +/// - `enc_key` -- the key to use for decryption (32 bytes). +/// +/// Outputs: +/// - `std::vector` -- the resulting plaintext. +std::vector decrypt_xchacha20( + std::span ciphertext, std::span enc_key); + } // namespace session diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index b938c889..9500452c 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -745,8 +745,75 @@ std::string compute_message_hash( decoded_data); } +std::vector encrypt_xchacha20( + std::span plaintext, std::span enc_key) { + if (enc_key.size() != 32) + throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; + + std::vector ciphertext; + ciphertext.resize( + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + // Generate random nonce, and stash it at the beginning of ciphertext: + randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + auto* c = reinterpret_cast(ciphertext.data()) + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + unsigned long long clen; + + crypto_aead_xchacha20poly1305_ietf_encrypt( + c, + &clen, + plaintext.data(), + plaintext.size(), + nullptr, + 0, // additional data + nullptr, // nsec (always unused) + reinterpret_cast(ciphertext.data()), + enc_key.data()); + assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); + ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); + return ciphertext; +} + +std::vector decrypt_xchacha20( + std::span ciphertext, std::span enc_key) { + if (ciphertext.size() < + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{ + "Invalid ciphertext: too short to contain valid encrypted data"}; + if (enc_key.size() != 32) + throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; + + // Extract nonce from the beginning of the ciphertext: + auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + ciphertext = ciphertext.subspan(nonce.size()); + + std::vector plaintext; + plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + auto* m = reinterpret_cast(plaintext.data()); + unsigned long long mlen; + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + m, + &mlen, + nullptr, // nsec (always unused) + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, // additional data + nonce.data(), + enc_key.data())) + throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; + assert(mlen <= plaintext.size()); + plaintext.resize(mlen); + return plaintext; +} + } // namespace session +extern "C" { + using namespace session; LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( @@ -925,3 +992,45 @@ LIBSESSION_C_API bool session_compute_message_hash( return false; } } + +LIBSESSION_C_API bool session_encrypt_xchacha20( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* enc_key_in, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { + try { + auto ciphertext = session::encrypt_xchacha20( + std::span{plaintext_in, plaintext_len}, + std::span{enc_key_in, 32}); + + *ciphertext_out = static_cast(malloc(ciphertext.size())); + *ciphertext_len = ciphertext.size(); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_xchacha20( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* enc_key_in, + unsigned char** plaintext_out, + size_t* plaintext_len) { + try { + auto plaintext = session::decrypt_xchacha20( + std::span{ciphertext_in, ciphertext_len}, + std::span{enc_key_in, 32}); + + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +} // extern "C" \ No newline at end of file From 820c82f4a259ca56d6322e213f2289a15050177c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 9 Apr 2025 16:09:36 +1000 Subject: [PATCH 553/572] Added a unit test --- tests/test_session_encrypt.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 376ab942..883bde9b 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -500,3 +500,18 @@ TEST_CASE("Session message hash", "[session][message-hash]") { CHECK(compute_message_hash(pubkey_hex, ns, base64_data2) == "apKu8OMjrbU+YeVWpMSyrr1wHq51K3uKD8WM0F4E1cE"); } + +TEST_CASE("xchacha20", "[session][xchacha20]") { + using namespace session; + + auto payload = "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; + auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + + CHECK(decrypt_xchacha20(payload, enc_key) == to_vector("TestMessage")); + CHECK_THROWS(decrypt_xchacha20(to_span("invalid"), enc_key)); + CHECK_THROWS(decrypt_xchacha20(payload, to_span("invalid"))); + + auto ciphertext = encrypt_xchacha20(to_span("TestMessage"), enc_key); + CHECK(decrypt_xchacha20(ciphertext, enc_key) == to_vector("TestMessage")); + CHECK_THROWS(encrypt_xchacha20(payload, to_span("invalid"))); +} From 77d412fe3a2deb6659e38cbadbde565d965d0c02 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Apr 2025 09:44:59 +1000 Subject: [PATCH 554/572] Updated to the latest libQuic --- external/oxen-libquic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/oxen-libquic b/external/oxen-libquic index 627f8c88..24dc67a5 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 627f8c8844b33447f6ec884d432c14f381c5d940 +Subproject commit 24dc67a5c2bde856abc5223216c0dd1683b8a838 From 79c7eaacc5ad12681befbef197c2f39155464604 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Apr 2025 09:51:45 +1000 Subject: [PATCH 555/572] Ran the formatter --- src/onionreq/builder.cpp | 2 +- src/session_network.cpp | 2 +- tests/test_onionreq.cpp | 2 +- tests/test_session_network.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index a0374ba2..bea6688c 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -22,10 +22,10 @@ #include #include "session/export.h" -#include "session/session_network.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/hop_encryption.hpp" #include "session/onionreq/key_types.hpp" +#include "session/session_network.hpp" #include "session/util.hpp" #include "session/xed25519.hpp" diff --git a/src/session_network.cpp b/src/session_network.cpp index 8d080a11..2e82a415 100644 --- a/src/session_network.cpp +++ b/src/session_network.cpp @@ -24,11 +24,11 @@ #include "session/ed25519.hpp" #include "session/export.h" #include "session/file.hpp" -#include "session/session_network.h" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/key_types.hpp" #include "session/onionreq/response_parser.hpp" +#include "session/session_network.h" #include "session/util.hpp" using namespace oxen; diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 4b752a37..c73935c5 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,7 +1,7 @@ #include -#include #include #include +#include #include "utils.hpp" diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index efb968d2..596e1fc1 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include #include "utils.hpp" From 80d894b6706bb1e625cb352f748da14cec034ec7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Apr 2025 10:41:51 +1000 Subject: [PATCH 556/572] Ran the formatter --- tests/test_session_encrypt.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 883bde9b..4eae0d37 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -504,7 +504,8 @@ TEST_CASE("Session message hash", "[session][message-hash]") { TEST_CASE("xchacha20", "[session][xchacha20]") { using namespace session; - auto payload = "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; + auto payload = + "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; CHECK(decrypt_xchacha20(payload, enc_key) == to_vector("TestMessage")); From a4d65ca77803332fdf90df630e4df0dc48e3773e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Apr 2025 14:08:37 +1000 Subject: [PATCH 557/572] Extended durations of eventually/always tests, fixed crashing test --- tests/test_session_network.cpp | 52 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index 9df77619..80539c33 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -666,7 +666,7 @@ TEST_CASE("Network", "[network][handle_errors]") { int16_t, std::vector>, std::optional) {}); - CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); + CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); REQUIRE(network->last_request_info.has_value()); CHECK(node_for_destination(network->last_request_info->destination) != node_for_destination(mock_request2.destination)); @@ -703,7 +703,7 @@ TEST_CASE("Network", "[network][handle_errors]") { int16_t, std::vector>, std::optional) {}); - CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); + CHECK(EVENTUALLY(100ms, network->called("refresh_snode_cache"))); // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it // is handled like any other error @@ -930,7 +930,7 @@ TEST_CASE("Network", "[network][build_path]") { network.emplace(std::nullopt, true, false, false); network->set_suspended(true); network->build_path("Test1", PathType::standard); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); // If there are no unused connections it puts the path build in the queue and calls // establish_and_store_connection @@ -938,7 +938,7 @@ TEST_CASE("Network", "[network][build_path]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); // If the unused nodes are empty it refreshes them network.emplace(std::nullopt, true, false, false); @@ -970,7 +970,7 @@ TEST_CASE("Network", "[network][build_path]") { network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_failures() == 0); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - CHECK(EVENTUALLY(10ms, network->called("refresh_snode_cache"))); + CHECK(EVENTUALLY(100ms, network->called("refresh_snode_cache"))); // If it can't build a path after excluding nodes with the same IP it increments the // failure count and re-tries the path build after a small delay @@ -983,7 +983,7 @@ TEST_CASE("Network", "[network][build_path]") { network->ignore_calls_to("build_path"); // Ignore the 2nd loop CHECK(network->get_path_build_failures() == 1); CHECK(network->get_path_build_queue().empty()); - CHECK(EVENTUALLY(10ms, network->called("build_path", 2))); + CHECK(EVENTUALLY(100ms, network->called("build_path", 2))); // It stores a successful non-standard path and kicks of queued requests but doesn't update the // status or call the 'paths_changed' hook @@ -1003,7 +1003,7 @@ TEST_CASE("Network", "[network][build_path]") { std::nullopt, PathType::download)); network->build_path("Test1", PathType::download); - CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); + CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::download).size() == 1); // It stores a successful 'standard' path, updates the status, calls the 'paths_changed' hook @@ -1024,7 +1024,7 @@ TEST_CASE("Network", "[network][build_path]") { std::nullopt, PathType::standard)); network->build_path("Test1", PathType::standard); - CHECK(EVENTUALLY(10ms, network->called("_send_onion_request"))); + CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); CHECK(network->get_paths(PathType::standard).size() == 1); CHECK(network->get_status() == ConnectionStatus::connected); CHECK(network->called("paths_changed")); @@ -1100,7 +1100,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue().empty()); // Adds a path build to the queue @@ -1108,7 +1108,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add the correct number of 'standard' path builds to the queue @@ -1116,10 +1116,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::standard, false); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard, PathType::standard}); @@ -1128,7 +1128,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can add more path builds if there are enough active paths of the same type, no pending paths @@ -1137,7 +1137,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection"))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Cannot add more path builds if there are already enough active paths of the same type and a @@ -1146,7 +1146,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); network->build_path_if_needed(PathType::standard, true); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue().empty()); // Cannot add more path builds if there is already a build of the same type in the queue and the @@ -1156,7 +1156,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->set_paths(PathType::standard, {invalid_path}); network->set_path_build_queue({PathType::standard}); network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add the correct number of 'download' path builds to the queue @@ -1164,10 +1164,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::download, false); network->build_path_if_needed(PathType::download, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::download, false); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::download, PathType::download}); @@ -1176,10 +1176,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::upload, false); network->build_path_if_needed(PathType::upload, false); - CHECK(EVENTUALLY(10ms, network->called("establish_and_store_connection", 2))); + CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::upload, false); - CHECK(ALWAYS(10ms, network->did_not_call("establish_and_store_connection"))); + CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); CHECK(network->get_path_build_queue() == std::deque{PathType::upload, PathType::upload}); } @@ -1212,7 +1212,7 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { // Test that it doesn't start checking for timeouts when the request doesn't have // a build paths timeout network.emplace(std::nullopt, true, true, false); - test_server.emplace(network->create_test_node(500)); + test_server.emplace(network->create_test_node(501)); network->send_onion_request( (*test_server)->node, to_vector("{\"method\":\"info\",\"params\":{}}"), @@ -1229,7 +1229,7 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { // Test that it does start checking for timeouts when the request has a // paths build timeout network.emplace(std::nullopt, true, true, false); - test_server.emplace(network->create_test_node(500)); + test_server.emplace(network->create_test_node(502)); network->ignore_calls_to("build_path"); network->send_onion_request( (*test_server)->node, @@ -1247,7 +1247,7 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { // Test that it fails the request with a timeout if it has a build path timeout // and the path build takes too long network.emplace(std::nullopt, true, true, false); - test_server.emplace(network->create_test_node(500)); + test_server.emplace(network->create_test_node(503)); network->ignore_calls_to("build_path"); network->send_onion_request( (*test_server)->node, @@ -1368,9 +1368,10 @@ TEST_CASE("Network", "[network][send_onion_request]") { TEST_CASE("Network", "[network][c][network_send_onion_request]") { auto test_network = std::make_unique(std::nullopt, true, true, false); auto test_server_cpp = test_network->create_test_node(500); - auto [test_path_servers_cpp, test_path_cpp] = test_network->create_test_path(); + std::optional>, onion_path>> test_path_data; + test_path_data.emplace(test_network->create_test_path()); test_network->handle_onion_requests_as_plaintext = true; - test_network->set_paths(PathType::standard, {test_path_cpp}); + test_network->set_paths(PathType::standard, {test_path_data->second}); // Convert TestNetwork to network_object to pass to C API auto n_object = std::make_unique(); @@ -1447,6 +1448,7 @@ TEST_CASE("Network", "[network][c][network_send_onion_request]") { CHECK(hf[0] == 2); // Called the onion_req callback CHECK(response.contains("t")); CHECK(response.contains("version")); + test_path_data.reset(); network_free(network); } From 5d3e100656dca9182ee894cedf347e0e4fbcb5bb Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Apr 2025 16:36:04 +1000 Subject: [PATCH 558/572] Fixed C API size calculation (Jason amended) rewrote the comment describing the struct layout as well. --- src/config/base.cpp | 48 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 777d2c2e..65705ddc 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -1181,27 +1181,43 @@ 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. We pack it as follows: - // - the returned struct - // - data pointers: [*configdata1][*configdata2]... <-- `config` points to the beginning of - // this - // - size_t [size1][size2]... <-- `config_lens` points to the beginning of this - // - obsolete hash pointers: [*obs1][*obs2]... <-- `obsolete` points to the beginning of - // this - // - data: [configdata1][configdata2]...[obs1\0][obs2\0]... + // We need to do one alloc here that holds everything. We overallocate the struct, using + // the extra allocated space beyond the end of the struct to store all the values the struct + // points at. + // + // In particular, in the beyond-the-end space, for N configdata and M obsolete hashes, we + // lay it out as follows: + // + // - N data pointers; `ret->config` points to the beginning of this: + // [*configdata1][*configdata2]...[*configdataN] + // - N size_t values; `ret->config_lens` points to the beginning of this: + // [size1][size2]...[sizeN] + // - M obsolete hash c string pointers; `ret->obsolete` points to the beginning of this: + // [*obs1][*obs2]...[*obsM] + // - packed data containing all the N config data and M obsolete hash null-terminated c + // strings pointed at in the above layout: + // [configdata1][configdata2]...[configdataN][obs1\0][obs2\0]...[obsM\0] + // + // For example: + // - `ret->config[1]` is the pointer `*configdata2`, which points at the beginning of + // [configdata2] in the packed data. `ret->config_lens[1]` is the length of that + // [configdata2]. + // - `ret->obs[0]` is the c string pointer containing the first obsolete hash, `obs1`; it + // points at the actual `[obs1\0]` value in the final packed data. + // static_assert(alignof(config_push_data) >= alignof(char*)); static_assert(sizeof(config_push_data) % alignof(char*) == 0); static_assert(alignof(char*) == alignof(size_t*)); static_assert(alignof(size_t) == alignof(char*)); - size_t buffer_size = sizeof(config_push_data) // struct data - + data.size() * sizeof(char**) // data pointer array - + data.size() * sizeof(size_t) // data sizes - + obs.size() * sizeof(char**); // obsolete pointer array + size_t buffer_size = sizeof(config_push_data) // struct data + + data.size() * sizeof(unsigned char*) // data pointer array + + data.size() * sizeof(size_t) // data sizes + + obs.size() * sizeof(char*); // obsolete pointer array - // + configdata array data: + // + configdata array data off the end: for (auto& d : data) buffer_size += d.size(); - // + obsolete hash data (including null terminator for each): + // + obsolete hash data (including null terminator for each) off the end: for (auto& o : obs) buffer_size += o.size() + 1; @@ -1213,9 +1229,9 @@ LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { ret->seqno = seqno; ret->config = reinterpret_cast(ret + 1); - ret->config_lens = reinterpret_cast(ret->config + 1); + ret->config_lens = reinterpret_cast(ret->config + data.size()); ret->n_configs = data.size(); - ret->obsolete = reinterpret_cast(ret->config_lens + 1); + ret->obsolete = reinterpret_cast(ret->config_lens + data.size()); ret->obsolete_len = obs.size(); unsigned char* pos = reinterpret_cast(ret->obsolete + ret->obsolete_len); From 8760192ca2252eb201648a72dd4f72e2ce3cf4ab Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 14 Apr 2025 13:40:27 -0300 Subject: [PATCH 559/572] Add test case trace logger from libquic This can be very helpful to identify abnormally slow or hanging tests by trace logging every test as it runs. --- tests/CMakeLists.txt | 2 + tests/case_logger.cpp | 93 +++++++++++++++++++++++++++++++++++++++++++ tests/main.cpp | 4 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/case_logger.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 07338de6..1dc8ef63 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,8 @@ set(LIB_SESSION_UTESTS_SOURCES test_random.cpp test_session_encrypt.cpp test_xed25519.cpp + + case_logger.cpp ) if (ENABLE_ONIONREQ) diff --git a/tests/case_logger.cpp b/tests/case_logger.cpp new file mode 100644 index 00000000..4dec6240 --- /dev/null +++ b/tests/case_logger.cpp @@ -0,0 +1,93 @@ +// This file contains a Catch2 listener than add oxen logging statements tracing the entry of cases +// and sections in the test suite. +// +// It runs in its own log level; to activate it, run alltests with `-T`/`--test-tracing`. +// +#include + +#include +#include +#include +#include +#include + +namespace fmt { +template <> +struct formatter : formatter { + template + auto format(const Catch::StringRef& val, FormatContext& ctx) const { + return formatter::format({val.data(), val.size()}, ctx); + } +}; +} // namespace fmt + +namespace session::test { +using namespace Catch; + +static auto cat = oxen::log::Cat("testcase"); + +static std::string_view sv(const Catch::StringRef& s) { + return {s.data(), s.size()}; +} + +// Bypass the usual log::trace(...) because we want to fake the source location, and want this +// even in non-debug builds. +template +static void test_trace( + const Catch::SourceLineInfo& sli, fmt::format_string fmt, T&&... args) { + + std::string_view filename{sli.file}; + if (auto pos = filename.rfind('/'); pos != std::string_view::npos) + filename.remove_prefix(pos + 1); + + spdlog::source_loc sloc{filename.data(), static_cast(sli.line), /*function name=*/""}; + + cat->log(sloc, oxen::log::Level::trace, fmt, std::forward(args)...); +} + +class CaseLogger : public Catch::EventListenerBase { + public: + using Catch::EventListenerBase::EventListenerBase; + + static std::string getDescription() { + return "Report test cases and section starting/ending events via oxen-logging"; + } + + void testCaseStarting(const TestCaseInfo& info) override { + test_trace(info.lineInfo, "Starting test case {} ({})", info.name, info.tagsAsString()); + } + void testCaseEnded(const TestCaseStats& stats) override { + auto& info = *stats.testInfo; + test_trace(info.lineInfo, "Finished test case {} ({})", info.name, info.tagsAsString()); + } + + void testCasePartialStarting(const Catch::TestCaseInfo& info, uint64_t partNumber) override { + if (partNumber > 0) + test_trace(info.lineInfo, "↪ Starting test case {} pass {}", info.name, partNumber); + } + + void testCasePartialEnded(const Catch::TestCaseStats& stats, uint64_t partNumber) override { + auto& info = *stats.testInfo; + if (partNumber > 0) + test_trace(info.lineInfo, "↩ Finished test case {} pass {}", info.name, partNumber); + } + + bool first_sect = true; + void sectionStarting(const SectionInfo& info) override { + if (first_sect) + first_sect = false; + test_trace(info.lineInfo, " ↪ Entering section {}", info.name); + } + void sectionEnded(const SectionStats& stats) override { + auto& info = stats.sectionInfo; + test_trace( + info.lineInfo, + " ↩ Finished section {} in {:.3f}ms", + info.name, + stats.durationInSeconds * 1000); + } +}; + +} // namespace session::test + +CATCH_REGISTER_LISTENER(session::test::CaseLogger) diff --git a/tests/main.cpp b/tests/main.cpp index 48d65e17..a814e50e 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -13,7 +13,9 @@ int main(int argc, char* argv[]) { "level")["--log-level"]("oxen-logging log level to apply to the test run") | Opt(log_file, "file")["--log-file"]( "oxen-logging log file to output logs to, or one of or one of " - "stdout/-/stderr/syslog."); + "stdout/-/stderr/syslog.") | + Opt(test_case_tracing)["-T"]["--test-tracing"]( + "enable oxen log tracing of test cases/sections"); session.cli(cli); From 31eed7eb7169d02dd6914e0e87fe0a4efdcd95cb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 14 Apr 2025 13:41:09 -0300 Subject: [PATCH 560/572] Speed up huge contacts test with fake session ids More than half the CPU time in the multi-part config tests is being used generating 12k proper session IDs (via Ed keygen + conversion to X) for the test. This replaces the session id generation with random values which is sufficient for this test and takes almost no time at all. --- tests/test_config_contacts.cpp | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 773f66eb..fadcc286 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -484,21 +484,17 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact std::string friend42; - std::array seedi = {0}; - for (uint16_t i = 0; i < 12000; i++) { + for (size_t i = 0; i < 12000; i++) { // Unlike the above case where we have nearly identical Session IDs, here our session IDs - // are randomly generated from fixed seeds and thus not usefully compressible, which results - // in a much larger (compressed) config. - seedi[0] = i % 256; - seedi[1] = i >> 8; - std::array i_ed_pk, i_curve_pk; - std::array i_ed_sk; - crypto_sign_ed25519_seed_keypair( - i_ed_pk.data(), - i_ed_sk.data(), - reinterpret_cast(seedi.data())); - rc = crypto_sign_ed25519_pk_to_curve25519(i_curve_pk.data(), i_ed_pk.data()); - std::string session_id = "05" + oxenc::to_hex(i_curve_pk.begin(), i_curve_pk.end()); + // are randomly generated and thus not usefully compressible, which results in a much larger + // (compressed) config. + std::mt19937_64 rng{i}; + std::array random_sessionid; + random_sessionid[0] = 0x05; + for (int i = 1; i < 33; i += 8) + oxenc::write_host_as_little(rng(), random_sessionid.data() + i); + + std::string session_id = oxenc::to_hex(random_sessionid); auto c = contacts.get_or_construct(session_id); c.nickname = "My friend {}"_format(i); From 8e1671c7872f12db5d87dde06338c1d60b802b06 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 14 Apr 2025 14:23:47 -0300 Subject: [PATCH 561/572] More useful test error on network timeout A timeout would fail all the CHECKs, but would keep going and then throw a weird json parsing error. This fixes that by checking the success condition to a REQUIRE instead of a CHECK so that the json parsing exception doesn't happen or get reported if the request failed. --- tests/test_session_network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index 596e1fc1..aa17de94 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -1244,9 +1244,9 @@ TEST_CASE("Network onion request", "[network][send_onion_request]") { // Wait for the result to be set auto result = result_promise.get_future().get(); - CHECK(result.success); CHECK_FALSE(result.timeout); CHECK(result.status_code == 200); + REQUIRE(result.success); REQUIRE(result.response.has_value()); INFO("*result.response is: " << *result.response); REQUIRE_NOTHROW([&] { [[maybe_unused]] auto _ = nlohmann::json::parse(*result.response); }); From e2722dc0fb2839e259c605fa612824abbd23b156 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 14 Apr 2025 14:13:55 -0300 Subject: [PATCH 562/572] formatting --- src/config/base.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/config/base.cpp b/src/config/base.cpp index 65705ddc..a8851934 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -543,9 +543,11 @@ std::unordered_set ConfigBase::_merge( // - confs that failed to parse (we can't understand them, so leave them behind as they may be // some future message). std::optional superconf = new_conf->unmerged_index(); // nullopt if we had to merge - std::unordered_set superconf_hashes = superconf && *superconf < all_hashes.size() - ? std::unordered_set{all_hashes[*superconf].begin(), all_hashes[*superconf].end()} - : std::unordered_set{}; + std::unordered_set superconf_hashes = + superconf && *superconf < all_hashes.size() + ? std::unordered_set< + std::string>{all_hashes[*superconf].begin(), all_hashes[*superconf].end()} + : std::unordered_set{}; const bool superconf_is_mine = superconf && *superconf == (mine_last ? all_hashes.size() - 1 : 0); @@ -561,10 +563,11 @@ std::unordered_set ConfigBase::_merge( : fmt::format("{}", fmt::join(all_hashes[*superconf], ", ")), superconf_is_mine ? "current" : "incoming") : "with merge required"); - + for (size_t i = 0; i < all_hashes.size(); i++) { if (i != superconf && !bad_confs.count(i) && !all_hashes[i].empty() && - superconf_hashes != std::unordered_set{all_hashes[i].begin(), all_hashes[i].end()}) { + superconf_hashes != + std::unordered_set{all_hashes[i].begin(), all_hashes[i].end()}) { bool all_already_existed = true; for (const auto& hash : all_hashes[i]) { @@ -628,7 +631,7 @@ std::unordered_set ConfigBase::_merge( } else { log::debug(cat, "All incoming configs rejected or already included, nothing to do"); } - + for (size_t i = 0; i < all_hashes.size(); i++) { if (!mine.empty() && i == (mine_last ? all_hashes.size() - 1 : 0)) continue; From cd827bac5b71a6a0f90c437f80aab21266300d26 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 14 Apr 2025 14:19:11 -0300 Subject: [PATCH 563/572] multiparts: use ordered map for internal storage We write all held multipart values when dumping with hash as key, but that won't necessarily be properly sorted with an unordered_map, so make it sorted instead. --- include/session/config/base.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 7707f5aa..eeda53e1 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -211,8 +211,9 @@ class ConfigBase : public ConfigSig { // Partial message sets that we have received but not yet been able to join into a full message. // The key is a hash of the final combined data (included in each part to identify related // parts) used as a unique identifier and checksum; the value is the PartialMessages struct - // containing set metadata and individual parts. - std::unordered_map _multiparts; + // containing set metadata and individual parts. (This is an ordered hash, because we relying + // on the keys being sorted when dumping our state to a config item.) + std::map _multiparts; // Parses a new multipart message, handling parsing, adding to _multiparts, etc. This is called // by _merge when it finds a `m`-type message for handling. From 6e802204104216f8f29ee605267e42a744e18843 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 07:52:07 +1000 Subject: [PATCH 564/572] Updated to the latest libQuic --- external/CMakeLists.txt | 2 +- external/oxen-libquic | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index c8eb4180..62a4f5b6 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -102,7 +102,7 @@ endif() set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") if(ENABLE_ONIONREQ) - libsession_system_or_submodule(OXENQUIC quic liboxenquic>=1.2.0 oxen-libquic) + libsession_system_or_submodule(OXENQUIC quic liboxenquic>=1.3.0 oxen-libquic) endif() if(NOT TARGET oxenc::oxenc) diff --git a/external/oxen-libquic b/external/oxen-libquic index 24dc67a5..88fdfea1 160000 --- a/external/oxen-libquic +++ b/external/oxen-libquic @@ -1 +1 @@ -Subproject commit 24dc67a5c2bde856abc5223216c0dd1683b8a838 +Subproject commit 88fdfea11aa1535de79f2e492e5f14897de4e689 From 15a885f907e9c99a4caf28fd09ac27abfc8c429c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 08:01:03 +1000 Subject: [PATCH 565/572] PR comments --- src/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c88bb61..e637f7de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -118,7 +118,7 @@ if(ENABLE_ONIONREQ) ) if (BUILD_STATIC_DEPS) - target_include_directories(onionreq PRIVATE ${CMAKE_BINARY_DIR}/static-deps/include) + target_include_directories(onionreq PUBLIC ${CMAKE_BINARY_DIR}/static-deps/include) endif() endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a8eea027..07338de6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,7 +44,6 @@ target_link_libraries(test_libs INTERFACE if (ENABLE_ONIONREQ) target_link_libraries(test_libs INTERFACE libsession::onionreq) - target_include_directories(test_libs INTERFACE ${CMAKE_BINARY_DIR}/static-deps/include) else() target_compile_definitions(test_libs INTERFACE DISABLE_ONIONREQ) endif() From f489fc73cfae09bf5a0f21455d40f84c0419856a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 08:04:46 +1000 Subject: [PATCH 566/572] Removed some unneeded `mutable` usage in lambdas --- src/session_network.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/session_network.cpp b/src/session_network.cpp index 10aa0b3b..ee59703e 100644 --- a/src/session_network.cpp +++ b/src/session_network.cpp @@ -680,7 +680,7 @@ void Network::disk_write_thread_loop() { } void Network::clear_cache() { - loop->call([this]() mutable { + loop->call([this] { { std::lock_guard lock{snode_cache_mutex}; need_clear_cache = true; @@ -696,7 +696,7 @@ size_t Network::snode_cache_size() { // MARK: Connection void Network::suspend() { - loop->call([this]() mutable { + loop->call([this] { suspended = true; close_connections(); log::info(cat, "Suspended."); @@ -704,14 +704,14 @@ void Network::suspend() { } void Network::resume() { - loop->call([this]() mutable { + loop->call([this] { suspended = false; log::info(cat, "Resumed."); }); } void Network::close_connections() { - loop->call([this]() mutable { _close_connections(); }); + loop->call([this] { _close_connections(); }); } void Network::_close_connections() { From 2510db6aab81138901cbc6f21b850a5421ae903b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 11:46:07 +1000 Subject: [PATCH 567/572] Refactored the 'EVENTUALLY' and 'ALWAYS' logic for networking tests --- tests/test_session_network.cpp | 145 +++++++++++++++++++++------------ tests/utils.hpp | 66 --------------- 2 files changed, 93 insertions(+), 118 deletions(-) diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index 426da379..a4d204bf 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -63,6 +63,9 @@ namespace session::network { class TestNetwork : public Network { public: std::unordered_map call_counts; + std::mutex call_counts_mutex; + std::condition_variable call_cv; + std::vector calls_to_ignore; std::chrono::milliseconds retry_delay_value = 0ms; std::optional> find_valid_path_response; @@ -76,7 +79,7 @@ class TestNetwork : public Network { bool pre_build_paths) : Network{cache_path, use_testnet, single_path_mode, pre_build_paths} { paths_changed = [this](std::vector>) { - call_counts["paths_changed"]++; + func_called("paths_changed"); }; } @@ -270,36 +273,28 @@ class TestNetwork : public Network { } void update_disk_cache_throttled(bool force_immediate_write) override { - const auto func_name = "update_disk_cache_throttled"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("update_disk_cache_throttled")) return; Network::update_disk_cache_throttled(force_immediate_write); } void establish_and_store_connection(std::string request_id) override { - const auto func_name = "establish_and_store_connection"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("establish_and_store_connection")) return; Network::establish_and_store_connection(request_id); } void refresh_snode_cache(std::optional existing_request_id) override { - const auto func_name = "refresh_snode_cache"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("refresh_snode_cache")) return; Network::refresh_snode_cache(existing_request_id); } void build_path(std::string path_id, PathType path_type) override { - const auto func_name = "build_path"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("build_path")) return; Network::build_path(path_id, path_type); @@ -307,9 +302,7 @@ class TestNetwork : public Network { std::optional find_valid_path( request_info info, std::vector paths) override { - const auto func_name = "find_valid_path"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("find_valid_path")) return std::nullopt; if (find_valid_path_response) @@ -319,9 +312,7 @@ class TestNetwork : public Network { } void check_request_queue_timeouts(std::optional request_timeout_id) override { - const auto func_name = "check_request_queue_timeouts"; - - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("check_request_queue_timeouts")) return; Network::check_request_queue_timeouts(request_timeout_id); @@ -329,10 +320,9 @@ class TestNetwork : public Network { void _send_onion_request( request_info info, network_response_callback_t handle_response) override { - const auto func_name = "_send_onion_request"; last_request_info = info; - if (check_should_ignore_and_log_call(func_name)) + if (check_should_ignore_and_log_call("_send_onion_request")) return; Network::_send_onion_request(std::move(info), std::move(handle_response)); @@ -365,7 +355,7 @@ class TestNetwork : public Network { std::vector> headers, std::optional response, std::optional handle_response) override { - call_counts["handle_errors"]++; + func_called("handle_errors"); Network::handle_errors( info, conn_info, @@ -381,7 +371,7 @@ class TestNetwork : public Network { std::vector>, std::optional> process_v3_onion_response(session::onionreq::Builder builder, std::string response) override { - call_counts["process_v3_onion_response"]++; + func_called("process_v3_onion_response"); if (handle_onion_requests_as_plaintext) return {200, {}, response}; @@ -394,7 +384,7 @@ class TestNetwork : public Network { std::vector>, std::optional> process_v4_onion_response(session::onionreq::Builder builder, std::string response) override { - call_counts["process_v4_onion_response"]++; + func_called("process_v4_onion_response"); if (handle_onion_requests_as_plaintext) return {200, {}, response}; @@ -409,17 +399,68 @@ class TestNetwork : public Network { (calls_to_ignore.emplace_back(std::forward(__args)), ...); } - bool check_should_ignore_and_log_call(std::string func_name) { - call_counts[func_name]++; + bool check_should_ignore_and_log_call(const std::string& name) { + func_called(name); - return std::find(calls_to_ignore.begin(), calls_to_ignore.end(), func_name) != + return std::find(calls_to_ignore.begin(), calls_to_ignore.end(), name) != calls_to_ignore.end(); } - void reset_calls() { return call_counts.clear(); } - bool called(std::string func_name, int times = 1) { return (call_counts[func_name] >= times); } + void func_called(const std::string& name) { + bool notify = false; + { + std::lock_guard lock(call_counts_mutex); + ++call_counts[name]; + notify = true; + } + + if (notify) + call_cv.notify_all(); + } + + void reset_calls() { + std::lock_guard lock_counts(call_counts_mutex); + call_counts.clear(); + } + + int get_call_count(const std::string& name) { + std::lock_guard lock(call_counts_mutex); + auto it = call_counts.find(name); + return (it != call_counts.end()) ? it->second : 0; + } + + bool called(const std::string& name, int times = 1) { return (get_call_count(name) >= times); } + + [[nodiscard]] bool called(const std::string& name, std::chrono::milliseconds timeout, int times = 1) { + if (times <= 0) times = 1; + + std::unique_lock lock(call_counts_mutex); + + auto predicate = [&]() { + auto it = call_counts.find(name); + return (it != call_counts.end() && it->second >= times); + }; + + return call_cv.wait_for(lock, timeout, predicate); + } - bool did_not_call(std::string func_name) { return !call_counts.contains(func_name); } + bool did_not_call(const std::string& name) { + std::lock_guard lock(call_counts_mutex); + return !call_counts.contains(name); + } + + [[nodiscard]] bool did_not_call(const std::string& name, std::chrono::milliseconds duration) { + std::unique_lock lock(call_counts_mutex); + auto predicate = [&]() { + return call_counts.contains(name); + }; + + if (predicate()) + return false; // Already called + + bool was_called_during_wait = call_cv.wait_for(lock, duration, predicate); + return !was_called_during_wait; + } }; } // namespace session::network @@ -666,7 +707,7 @@ TEST_CASE("Network", "[network][handle_errors]") { int16_t, std::vector>, std::optional) {}); - CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); + CHECK(network->called("_send_onion_request", 100ms)); REQUIRE(network->last_request_info.has_value()); CHECK(node_for_destination(network->last_request_info->destination) != node_for_destination(mock_request2.destination)); @@ -703,7 +744,7 @@ TEST_CASE("Network", "[network][handle_errors]") { int16_t, std::vector>, std::optional) {}); - CHECK(EVENTUALLY(100ms, network->called("refresh_snode_cache"))); + CHECK(network->called("refresh_snode_cache", 100ms)); // Check when the retry after refreshing the snode cache due to a 421 receives it's own 421 it // is handled like any other error @@ -930,7 +971,7 @@ TEST_CASE("Network", "[network][build_path]") { network.emplace(std::nullopt, true, false, false); network->set_suspended(true); network->build_path("Test1", PathType::standard); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); // If there are no unused connections it puts the path build in the queue and calls // establish_and_store_connection @@ -938,7 +979,7 @@ TEST_CASE("Network", "[network][build_path]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); + CHECK(network->called("establish_and_store_connection", 100ms)); // If the unused nodes are empty it refreshes them network.emplace(std::nullopt, true, false, false); @@ -970,7 +1011,7 @@ TEST_CASE("Network", "[network][build_path]") { network->build_path("Test1", PathType::standard); CHECK(network->get_path_build_failures() == 0); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); - CHECK(EVENTUALLY(100ms, network->called("refresh_snode_cache"))); + CHECK(network->called("refresh_snode_cache", 100ms)); // If it can't build a path after excluding nodes with the same IP it increments the // failure count and re-tries the path build after a small delay @@ -983,7 +1024,7 @@ TEST_CASE("Network", "[network][build_path]") { network->ignore_calls_to("build_path"); // Ignore the 2nd loop CHECK(network->get_path_build_failures() == 1); CHECK(network->get_path_build_queue().empty()); - CHECK(EVENTUALLY(100ms, network->called("build_path", 2))); + CHECK(network->called("build_path", 100ms, 2)); // It stores a successful non-standard path and kicks of queued requests but doesn't update the // status or call the 'paths_changed' hook @@ -1003,7 +1044,7 @@ TEST_CASE("Network", "[network][build_path]") { std::nullopt, PathType::download)); network->build_path("Test1", PathType::download); - CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); + CHECK(network->called("_send_onion_request", 100ms)); CHECK(network->get_paths(PathType::download).size() == 1); // It stores a successful 'standard' path, updates the status, calls the 'paths_changed' hook @@ -1024,7 +1065,7 @@ TEST_CASE("Network", "[network][build_path]") { std::nullopt, PathType::standard)); network->build_path("Test1", PathType::standard); - CHECK(EVENTUALLY(100ms, network->called("_send_onion_request"))); + CHECK(network->called("_send_onion_request", 100ms)); CHECK(network->get_paths(PathType::standard).size() == 1); CHECK(network->get_status() == ConnectionStatus::connected); CHECK(network->called("paths_changed")); @@ -1100,7 +1141,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue().empty()); // Adds a path build to the queue @@ -1108,7 +1149,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); + CHECK(network->called("establish_and_store_connection", 100ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add the correct number of 'standard' path builds to the queue @@ -1116,10 +1157,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::standard, false); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); + CHECK(network->called("establish_and_store_connection", 100ms, 2)); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::standard, PathType::standard}); @@ -1128,7 +1169,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); + CHECK(network->called("establish_and_store_connection", 100ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can add more path builds if there are enough active paths of the same type, no pending paths @@ -1137,7 +1178,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); network->build_path_if_needed(PathType::standard, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection"))); + CHECK(network->called("establish_and_store_connection", 100ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Cannot add more path builds if there are already enough active paths of the same type and a @@ -1146,7 +1187,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->set_paths(PathType::standard, {invalid_path, invalid_path}); network->build_path_if_needed(PathType::standard, true); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue().empty()); // Cannot add more path builds if there is already a build of the same type in the queue and the @@ -1156,7 +1197,7 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->set_paths(PathType::standard, {invalid_path}); network->set_path_build_queue({PathType::standard}); network->build_path_if_needed(PathType::standard, false); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::standard}); // Can only add the correct number of 'download' path builds to the queue @@ -1164,10 +1205,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::download, false); network->build_path_if_needed(PathType::download, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); + CHECK(network->called("establish_and_store_connection", 100ms, 2)); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::download, false); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::download, PathType::download}); @@ -1176,10 +1217,10 @@ TEST_CASE("Network", "[network][build_path_if_needed]") { network->ignore_calls_to("establish_and_store_connection"); network->build_path_if_needed(PathType::upload, false); network->build_path_if_needed(PathType::upload, false); - CHECK(EVENTUALLY(100ms, network->called("establish_and_store_connection", 2))); + CHECK(network->called("establish_and_store_connection", 100ms, 2)); network->reset_calls(); // This triggers 'call_soon' so we need to wait until they are enqueued network->build_path_if_needed(PathType::upload, false); - CHECK(ALWAYS(25ms, network->did_not_call("establish_and_store_connection"))); + CHECK(network->did_not_call("establish_and_store_connection", 25ms)); CHECK(network->get_path_build_queue() == std::deque{PathType::upload, PathType::upload}); } @@ -1224,7 +1265,7 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { std::optional) {}, oxen::quic::DEFAULT_TIMEOUT, std::nullopt); - CHECK(ALWAYS(300ms, network->did_not_call("check_request_queue_timeouts"))); + CHECK(network->did_not_call("check_request_queue_timeouts", 300ms)); // Test that it does start checking for timeouts when the request has a // paths build timeout @@ -1242,7 +1283,7 @@ TEST_CASE("Network", "[network][check_request_queue_timeouts]") { std::optional) {}, oxen::quic::DEFAULT_TIMEOUT, oxen::quic::DEFAULT_TIMEOUT); - CHECK(EVENTUALLY(300ms, network->called("check_request_queue_timeouts"))); + CHECK(network->called("check_request_queue_timeouts", 300ms)); // Test that it fails the request with a timeout if it has a build path timeout // and the path build takes too long diff --git a/tests/utils.hpp b/tests/utils.hpp index 4a4aceb0..91c6d56c 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -114,69 +114,3 @@ template std::set> make_set(T&&... args) { return {std::forward(args)...}; } - -template > Validator> -auto eventually_impl(std::chrono::milliseconds timeout, Call&& f, Validator&& isValid) - -> std::invoke_result_t { - using ResultType = std::invoke_result_t; - - // If we already have a value then don't bother with the loop - if (auto result = f(); isValid(result)) - return result; - - auto start = std::chrono::steady_clock::now(); - auto sleep_duration = std::chrono::milliseconds{10}; - while (std::chrono::steady_clock::now() - start < timeout) { - std::this_thread::sleep_for(sleep_duration); - - if (auto result = f(); isValid(result)) - return result; - } - - return ResultType{}; -} - -template > Validator> -bool always_impl(std::chrono::milliseconds duration, Call&& f, Validator&& isValid) { - auto start = std::chrono::steady_clock::now(); - auto sleep_duration = std::chrono::milliseconds{10}; - while (std::chrono::steady_clock::now() - start < duration) { - if (auto result = f(); !isValid(result)) - return false; - std::this_thread::sleep_for(sleep_duration); - } - return true; -} - -template - requires std::is_same_v, bool> -bool eventually_impl(std::chrono::milliseconds timeout, Call&& f) { - return eventually_impl(timeout, f, [](bool result) { return result; }); -} - -template - requires std::is_same_v< - std::invoke_result_t, - std::vector::value_type>> -auto eventually_impl(std::chrono::milliseconds timeout, Call&& f) -> std::invoke_result_t { - using ResultType = std::invoke_result_t; - return eventually_impl(timeout, f, [](const ResultType& result) { return !result.empty(); }); -} - -template - requires std::is_same_v, bool> -bool always_impl(std::chrono::milliseconds duration, Call&& f) { - return always_impl(duration, f, [](bool result) { return result; }); -} - -template - requires std::is_same_v< - std::invoke_result_t, - std::vector::value_type>> -bool always_impl(std::chrono::milliseconds duration, Call&& f) { - using ResultType = std::invoke_result_t; - return always_impl(duration, f, [](const ResultType& result) { return !result.empty(); }); -} - -#define EVENTUALLY(timeout, ...) eventually_impl(timeout, [&]() { return (__VA_ARGS__); }) -#define ALWAYS(duration, ...) always_impl(duration, [&]() { return (__VA_ARGS__); }) From 650dc3cd4a29067c07eef85e35c780876c807a8d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 11:50:20 +1000 Subject: [PATCH 568/572] Ran the formatter... --- tests/test_session_network.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index a4d204bf..09f17df5 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -415,14 +415,14 @@ class TestNetwork : public Network { } if (notify) - call_cv.notify_all(); + call_cv.notify_all(); } void reset_calls() { std::lock_guard lock_counts(call_counts_mutex); call_counts.clear(); } - + int get_call_count(const std::string& name) { std::lock_guard lock(call_counts_mutex); auto it = call_counts.find(name); @@ -431,8 +431,10 @@ class TestNetwork : public Network { bool called(const std::string& name, int times = 1) { return (get_call_count(name) >= times); } - [[nodiscard]] bool called(const std::string& name, std::chrono::milliseconds timeout, int times = 1) { - if (times <= 0) times = 1; + [[nodiscard]] bool called( + const std::string& name, std::chrono::milliseconds timeout, int times = 1) { + if (times <= 0) + times = 1; std::unique_lock lock(call_counts_mutex); @@ -451,12 +453,10 @@ class TestNetwork : public Network { [[nodiscard]] bool did_not_call(const std::string& name, std::chrono::milliseconds duration) { std::unique_lock lock(call_counts_mutex); - auto predicate = [&]() { - return call_counts.contains(name); - }; + auto predicate = [&]() { return call_counts.contains(name); }; if (predicate()) - return false; // Already called + return false; // Already called bool was_called_during_wait = call_cv.wait_for(lock, duration, predicate); return !was_called_during_wait; From ca53f1b60ce2602f1ab3039988f21e1b431a6e4b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 12:45:04 +1000 Subject: [PATCH 569/572] Bumped version to 1.4.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c810e5e..661bfe32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.3.1 + VERSION 1.4.0 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) From 038119227b12624ff18edb97273652a98fe062d0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Apr 2025 16:29:39 +1000 Subject: [PATCH 570/572] Renamed current_hashes on GroupKeys to active_hashes for consistency --- include/session/config/groups/keys.h | 4 ++-- include/session/config/groups/keys.hpp | 4 ++-- src/config/groups/keys.cpp | 6 +++--- tests/test_group_keys.cpp | 28 +++++++++++++------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index bfce6f50..f0ee5025 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -246,7 +246,7 @@ LIBSESSION_EXPORT bool groups_keys_load_message( config_object* info, config_object* members) LIBSESSION_WARN_UNUSED; -/// API: groups/groups_keys_current_hashes +/// API: groups/groups_keys_active_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 @@ -258,7 +258,7 @@ LIBSESSION_EXPORT bool groups_keys_load_message( /// 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); +LIBSESSION_EXPORT config_string_list* groups_keys_active_hashes(const config_group_keys* conf); /// API: groups/groups_keys_needs_rekey /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index d4358eb5..3e20c9b7 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -584,7 +584,7 @@ class Keys : public ConfigSig { Info& info, Members& members); - /// API: groups/Keys::current_hashes + /// API: groups/Keys::active_hashes /// /// Returns a set of message hashes of messages that contain currently active decryption keys. /// These are the messages that should be periodically renewed by clients with write access to @@ -594,7 +594,7 @@ class Keys : public ConfigSig { /// /// Outputs: /// - vector of message hashes - std::unordered_set current_hashes() const; + std::unordered_set active_hashes() const; /// API: groups/Keys::needs_rekey /// diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index c9917075..2fe6a156 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1124,7 +1124,7 @@ bool Keys::load_key_message( return false; } -std::unordered_set Keys::current_hashes() const { +std::unordered_set Keys::active_hashes() const { std::unordered_set hashes; for (const auto& [g, hash] : active_msgs_) hashes.insert(hash.begin(), hash.end()); @@ -1592,8 +1592,8 @@ LIBSESSION_C_API bool groups_keys_load_message( false); } -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 config_string_list* groups_keys_active_hashes(const config_group_keys* conf) { + return make_string_list(unbox(conf).active_hashes()); } LIBSESSION_C_API bool groups_keys_needs_rekey(const config_group_keys* conf) { diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index fa53140f..90588450 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -181,7 +181,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash1"s}}); CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash1"s}}); CHECK(a.members.size() == 1); - CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s}}); + CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s}}); } /* All attempts to merge non-admin members will throw, as none of the non admin members @@ -193,7 +193,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK_THROWS(m.info.merge(info_configs)); CHECK_THROWS(m.members.merge(mem_configs)); CHECK(m.members.size() == 0); - CHECK(m.keys.current_hashes().empty()); + CHECK(m.keys.active_hashes().empty()); } info_configs.clear(); @@ -228,7 +228,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash2"s}}); CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash2"s}}); CHECK(a.members.size() == 5); - CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s}}); + CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s}}); } for (auto& m : members) { @@ -237,7 +237,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.info.merge(info_configs) == std::unordered_set{{"fakehash2"s}}); CHECK(m.members.merge(mem_configs) == std::unordered_set{{"fakehash2"s}}); CHECK(m.members.size() == 5); - CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s}}); + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s}}); } info_configs.clear(); @@ -270,7 +270,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.get_name() == "tomatosauce"s); CHECK(a.info.get_description() == "this is where you go to play in the tomato sauce, I guess"s); - CHECK(a.keys.current_hashes() == + CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s}}); } @@ -282,7 +282,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.info.get_name() == "tomatosauce"s); CHECK(m.info.get_description() == "this is where you go to play in the tomato sauce, I guess"s); - CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}}); + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s}}); } info_configs.clear(); @@ -318,7 +318,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::unordered_set{{"fakehash4"s}}); CHECK(a.members.merge(mem_configs) == std::unordered_set{{"fakehash4"s}}); CHECK(a.members.size() == 3); - CHECK(a.keys.current_hashes() == + CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash1"s, "keyhash2"s, "keyhash3"s, "keyhash4"s}}); } @@ -327,7 +327,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { bool found_key = m.keys.load_key_message( "keyhash4", new_keys_config2, get_timestamp_ms(), m.info, m.members); - CHECK(m.keys.current_hashes() == + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s}}); if (i < 2) { // We should still be in the group CHECK(found_key); @@ -425,10 +425,10 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.members.size() == 5); if (i < 2) - CHECK(m.keys.current_hashes() == + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}}); else - CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash5"s}}); + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash5"s}}); } std::pair> decrypted1, decrypted2; @@ -457,7 +457,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { members[1].keys.dump()); CHECK(m1b.keys.size() == 4); CHECK(m1b.keys.group_keys().size() == 4); - CHECK(m1b.keys.current_hashes() == + CHECK(m1b.keys.active_hashes() == std::unordered_set{{"keyhash2"s, "keyhash3"s, "keyhash4"s, "keyhash5"s}}); CHECK(m1b.members.size() == 5); auto m1b_m2 = m1b.members.get(members[2].session_id); @@ -490,7 +490,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s}}); CHECK(a.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s}}); CHECK(a.members.size() == 5); - CHECK(a.keys.current_hashes() == std::unordered_set{ + CHECK(a.keys.active_hashes() == std::unordered_set{ {"keyhash1"s, "keyhash2"s, "keyhash3"s, @@ -526,7 +526,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s, "ifakehash7"s}}); CHECK(a.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s, "mfakehash7"s}}); CHECK(a.members.size() == 5); - CHECK(a.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); + CHECK(a.keys.active_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } for (size_t i = 0; i < members.size(); i++) { @@ -546,7 +546,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.info.merge(info_configs) == std::unordered_set{{"ifakehash6"s, "ifakehash7"s}}); CHECK(m.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s, "mfakehash7"s}}); CHECK(m.members.size() == 5); - CHECK(m.keys.current_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); + CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash6"s, "keyhash7"s}}); } // Make sure keys propagate on dump restore to info/members: From cb844389438f2ec476b0134b3e552ed81fad1bca Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 16 Apr 2025 07:42:46 +1000 Subject: [PATCH 571/572] Formatter --- tests/test_group_keys.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 90588450..33d5dea4 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -491,12 +491,12 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(a.members.merge(mem_configs) == std::unordered_set{{"mfakehash6"s}}); CHECK(a.members.size() == 5); CHECK(a.keys.active_hashes() == std::unordered_set{ - {"keyhash1"s, - "keyhash2"s, - "keyhash3"s, - "keyhash4"s, - "keyhash5"s, - "keyhash6"s}}); + {"keyhash1"s, + "keyhash2"s, + "keyhash3"s, + "keyhash4"s, + "keyhash5"s, + "keyhash6"s}}); } std::vector new_keys_config7 = From 4f799337e4708e1dfe425e977c4dbd8c5fa174fd Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 24 Apr 2025 13:27:25 +1000 Subject: [PATCH 572/572] chore: make cppcheck happy in ed25519 sign --- src/ed25519.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ed25519.cpp b/src/ed25519.cpp index df6e6e41..0cde00e3 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -63,12 +63,14 @@ std::vector sign( throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; } - std::array sig; + std::vector sig; + sig.resize(64); + if (0 != crypto_sign_ed25519_detached( sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; - return {sig.data(), sig.data() + sig.size()}; + return sig; } bool verify(