diff options
author | George Hazan <george.hazan@gmail.com> | 2023-08-23 19:38:07 +0300 |
---|---|---|
committer | George Hazan <george.hazan@gmail.com> | 2023-08-23 19:38:07 +0300 |
commit | abacfceb23fc41d0b632a7985c2291c02225b432 (patch) | |
tree | 186f0c918c8bf671e0bec368f9ff27fd2352d47c /protocols/Tox/libtox/src | |
parent | a8249e73fa598dde2fbde216c7e8f416ebdbf88d (diff) |
libtox synced with their master, it could help to fix #3649
Diffstat (limited to 'protocols/Tox/libtox/src')
52 files changed, 16995 insertions, 663 deletions
diff --git a/protocols/Tox/libtox/src/toxcore/DHT.c b/protocols/Tox/libtox/src/toxcore/DHT.c index 8aa161102b..91f0e0ae47 100644 --- a/protocols/Tox/libtox/src/toxcore/DHT.c +++ b/protocols/Tox/libtox/src/toxcore/DHT.c @@ -18,6 +18,7 @@ #include "mono_time.h" #include "network.h" #include "ping.h" +#include "shared_key_cache.h" #include "state.h" #include "util.h" @@ -43,6 +44,13 @@ /** Number of get node requests to send to quickly find close nodes. */ #define MAX_BOOTSTRAP_TIMES 5 +// TODO(sudden6): find out why we need multiple callbacks and if we really need 32 +#define DHT_FRIEND_MAX_LOCKS 32 + +/* Settings for the shared key cache */ +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 + typedef struct DHT_Friend_Callback { dht_ip_cb *ip_callback; void *data; @@ -61,7 +69,8 @@ struct DHT_Friend { /* Symmetric NAT hole punching stuff. */ NAT nat; - uint16_t lock_count; + /* Each set bit represents one installed callback */ + uint32_t lock_flags; DHT_Friend_Callback callbacks[DHT_FRIEND_MAX_LOCKS]; Node_format to_bootstrap[MAX_SENT_NODES]; @@ -71,6 +80,8 @@ struct DHT_Friend { static const DHT_Friend empty_dht_friend = {{0}}; const Node_format empty_node_format = {{0}}; +static_assert(sizeof (empty_dht_friend.lock_flags) * 8 == DHT_FRIEND_MAX_LOCKS, "Bitfield size and number of locks don't match"); + typedef struct Cryptopacket_Handler { cryptopacket_handler_cb *function; void *object; @@ -101,8 +112,8 @@ struct DHT { uint32_t loaded_num_nodes; unsigned int loaded_nodes_index; - Shared_Keys shared_keys_recv; - Shared_Keys shared_keys_sent; + Shared_Key_Cache *shared_keys_recv; + Shared_Key_Cache *shared_keys_sent; struct Ping *ping; Ping_Array *dht_ping_array; @@ -200,12 +211,6 @@ static IP_Port ip_port_normalize(const IP_Port *ip_port) return res; } -/** @brief Compares pk1 and pk2 with pk. - * - * @retval 0 if both are same distance. - * @retval 1 if pk1 is closer. - * @retval 2 if pk2 is closer. - */ int id_closest(const uint8_t *pk, const uint8_t *pk1, const uint8_t *pk2) { for (size_t i = 0; i < CRYPTO_PUBLIC_KEY_SIZE; ++i) { @@ -250,98 +255,25 @@ unsigned int bit_by_bit_cmp(const uint8_t *pk1, const uint8_t *pk2) } /** - * Shared key generations are costly, it is therefore smart to store commonly used - * ones so that they can be re-used later without being computed again. - * - * If a shared key is already in shared_keys, copy it to shared_key. - * Otherwise generate it into shared_key and copy it to shared_keys - */ -void get_shared_key(const Mono_Time *mono_time, Shared_Keys *shared_keys, uint8_t *shared_key, - const uint8_t *secret_key, const uint8_t *public_key) -{ - uint32_t num = -1; - uint32_t curr = 0; - - for (uint32_t i = 0; i < MAX_KEYS_PER_SLOT; ++i) { - const int index = public_key[30] * MAX_KEYS_PER_SLOT + i; - Shared_Key *const key = &shared_keys->keys[index]; - - if (key->stored) { - if (pk_equal(public_key, key->public_key)) { - memcpy(shared_key, key->shared_key, CRYPTO_SHARED_KEY_SIZE); - ++key->times_requested; - key->time_last_requested = mono_time_get(mono_time); - return; - } - - if (num != 0) { - if (mono_time_is_timeout(mono_time, key->time_last_requested, KEYS_TIMEOUT)) { - num = 0; - curr = index; - } else if (num > key->times_requested) { - num = key->times_requested; - curr = index; - } - } - } else if (num != 0) { - num = 0; - curr = index; - } - } - - encrypt_precompute(public_key, secret_key, shared_key); - - if (num != UINT32_MAX) { - Shared_Key *const key = &shared_keys->keys[curr]; - key->stored = true; - key->times_requested = 1; - memcpy(key->public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(key->shared_key, shared_key, CRYPTO_SHARED_KEY_SIZE); - key->time_last_requested = mono_time_get(mono_time); - } -} - -/** * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key * for packets that we receive. */ -void dht_get_shared_key_recv(DHT *dht, uint8_t *shared_key, const uint8_t *public_key) +const uint8_t *dht_get_shared_key_recv(DHT *dht, const uint8_t *public_key) { - get_shared_key(dht->mono_time, &dht->shared_keys_recv, shared_key, dht->self_secret_key, public_key); + return shared_key_cache_lookup(dht->shared_keys_recv, public_key); } /** * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key * for packets that we send. */ -void dht_get_shared_key_sent(DHT *dht, uint8_t *shared_key, const uint8_t *public_key) +const uint8_t *dht_get_shared_key_sent(DHT *dht, const uint8_t *public_key) { - get_shared_key(dht->mono_time, &dht->shared_keys_sent, shared_key, dht->self_secret_key, public_key); + return shared_key_cache_lookup(dht->shared_keys_sent, public_key); } #define CRYPTO_SIZE (1 + CRYPTO_PUBLIC_KEY_SIZE * 2 + CRYPTO_NONCE_SIZE) -/** - * @brief Create a request to peer. - * - * Packs the data and sender public key and encrypts the packet. - * - * @param[in] send_public_key public key of the sender. - * @param[in] send_secret_key secret key of the sender. - * @param[out] packet an array of @ref MAX_CRYPTO_REQUEST_SIZE big. - * @param[in] recv_public_key public key of the receiver. - * @param[in] data represents the data we send with the request. - * @param[in] data_length the length of the data. - * @param[in] request_id the id of the request (32 = friend request, 254 = ping request). - * - * @attention Constraints: - * @code - * sizeof(packet) >= MAX_CRYPTO_REQUEST_SIZE - * @endcode - * - * @retval -1 on failure. - * @return the length of the created packet on success. - */ int create_request(const Random *rng, const uint8_t *send_public_key, const uint8_t *send_secret_key, uint8_t *packet, const uint8_t *recv_public_key, const uint8_t *data, uint32_t data_length, uint8_t request_id) @@ -375,28 +307,6 @@ int create_request(const Random *rng, const uint8_t *send_public_key, const uint return len + CRYPTO_SIZE; } -/** - * @brief Decrypts and unpacks a DHT request packet. - * - * Puts the senders public key in the request in @p public_key, the data from - * the request in @p data. - * - * @param[in] self_public_key public key of the receiver (us). - * @param[in] self_secret_key secret key of the receiver (us). - * @param[out] public_key public key of the sender, copied from the input packet. - * @param[out] data decrypted request data, copied from the input packet, must - * have room for @ref MAX_CRYPTO_REQUEST_SIZE bytes. - * @param[in] packet is the request packet. - * @param[in] packet_length length of the packet. - * - * @attention Constraints: - * @code - * sizeof(data) >= MAX_CRYPTO_REQUEST_SIZE - * @endcode - * - * @retval -1 if not valid request. - * @return the length of the unpacked data. - */ int handle_request(const uint8_t *self_public_key, const uint8_t *self_secret_key, uint8_t *public_key, uint8_t *data, uint8_t *request_id, const uint8_t *packet, uint16_t packet_length) { @@ -435,9 +345,6 @@ int handle_request(const uint8_t *self_public_key, const uint8_t *self_secret_ke return len1; } -/** @return packet size of packed node with ip_family on success. - * @retval -1 on failure. - */ int packed_node_size(Family ip_family) { if (net_family_is_ipv4(ip_family) || net_family_is_tcp_ipv4(ip_family)) { @@ -452,13 +359,6 @@ int packed_node_size(Family ip_family) } -/** @brief Pack an IP_Port structure into data of max size length. - * - * Packed_length is the offset of data currently packed. - * - * @return size of packed IP_Port data on success. - * @retval -1 on failure. - */ int pack_ip_port(const Logger *logger, uint8_t *data, uint16_t length, const IP_Port *ip_port) { if (data == nullptr) { @@ -514,11 +414,6 @@ int pack_ip_port(const Logger *logger, uint8_t *data, uint16_t length, const IP_ } } -/** @brief Encrypt plain and write resulting DHT packet into packet with max size length. - * - * @return size of packet on success. - * @retval -1 on failure. - */ int dht_create_packet(const Random *rng, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE], const uint8_t *shared_key, const uint8_t type, const uint8_t *plain, size_t plain_length, @@ -554,13 +449,6 @@ int dht_create_packet(const Random *rng, const uint8_t public_key[CRYPTO_PUBLIC_ return 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + encrypted_length; } -/** @brief Unpack IP_Port structure from data of max size length into ip_port. - * - * len_processed is the offset of data currently unpacked. - * - * @return size of unpacked ip_port on success. - * @retval -1 on failure. - */ int unpack_ip_port(IP_Port *ip_port, const uint8_t *data, uint16_t length, bool tcp_enabled) { if (data == nullptr) { @@ -621,11 +509,6 @@ int unpack_ip_port(IP_Port *ip_port, const uint8_t *data, uint16_t length, bool } } -/** @brief Pack number of nodes into data of maxlength length. - * - * @return length of packed nodes on success. - * @retval -1 on failure. - */ int pack_nodes(const Logger *logger, uint8_t *data, uint16_t length, const Node_format *nodes, uint16_t number) { uint32_t packed_length = 0; @@ -655,13 +538,6 @@ int pack_nodes(const Logger *logger, uint8_t *data, uint16_t length, const Node_ return packed_length; } -/** @brief Unpack data of length into nodes of size max_num_nodes. - * Put the length of the data processed in processed_data_len. - * tcp_enabled sets if TCP nodes are expected (true) or not (false). - * - * @return number of unpacked nodes on success. - * @retval -1 on failure. - */ int unpack_nodes(Node_format *nodes, uint16_t max_num_nodes, uint16_t *processed_data_len, const uint8_t *data, uint16_t length, bool tcp_enabled) { @@ -1057,8 +933,7 @@ static bool send_announce_ping(DHT *dht, const uint8_t *public_key, const IP_Por public_key, CRYPTO_PUBLIC_KEY_SIZE); memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, &ping_id, sizeof(ping_id)); - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - dht_get_shared_key_sent(dht, shared_key, public_key); + const uint8_t *shared_key = dht_get_shared_key_sent(dht, public_key); uint8_t request[1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + sizeof(plain) + CRYPTO_MAC_SIZE]; @@ -1086,8 +961,7 @@ static int handle_data_search_response(void *object, const IP_Port *source, VLA(uint8_t, plain, plain_len); const uint8_t *public_key = packet + 1; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - dht_get_shared_key_recv(dht, shared_key, public_key); + const uint8_t *shared_key = dht_get_shared_key_recv(dht, public_key); if (decrypt_data_symmetric(shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1419,8 +1293,9 @@ uint32_t addto_lists(DHT *dht, const IP_Port *ip_port, const uint8_t *public_key return used; } - for (uint32_t i = 0; i < friend_foundip->lock_count; ++i) { - if (friend_foundip->callbacks[i].ip_callback != nullptr) { + for (uint32_t i = 0; i < DHT_FRIEND_MAX_LOCKS; ++i) { + const bool has_lock = (friend_foundip->lock_flags & (UINT32_C(1) << i)) > 0; + if (has_lock && friend_foundip->callbacks[i].ip_callback != nullptr) { friend_foundip->callbacks[i].ip_callback(friend_foundip->callbacks[i].data, friend_foundip->callbacks[i].number, &ipp_copy); } @@ -1515,15 +1390,12 @@ bool dht_getnodes(DHT *dht, const IP_Port *ip_port, const uint8_t *public_key, c memcpy(plain, client_id, CRYPTO_PUBLIC_KEY_SIZE); memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, &ping_id, sizeof(ping_id)); - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - dht_get_shared_key_sent(dht, shared_key, public_key); + const uint8_t *shared_key = dht_get_shared_key_sent(dht, public_key); const int len = dht_create_packet(dht->rng, dht->self_public_key, shared_key, NET_PACKET_GET_NODES, plain, sizeof(plain), data, sizeof(data)); - crypto_memzero(shared_key, sizeof(shared_key)); - if (len != sizeof(data)) { LOGGER_ERROR(dht->log, "getnodes packet encryption failed"); return false; @@ -1598,9 +1470,7 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p } uint8_t plain[CRYPTO_NODE_SIZE]; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - - dht_get_shared_key_recv(dht, shared_key, packet + 1); + const uint8_t *shared_key = dht_get_shared_key_recv(dht, packet + 1); const int len = decrypt_data_symmetric( shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1609,7 +1479,6 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p plain); if (len != CRYPTO_NODE_SIZE) { - crypto_memzero(shared_key, sizeof(shared_key)); return 1; } @@ -1617,8 +1486,6 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p ping_add(dht->ping, packet + 1, source); - crypto_memzero(shared_key, sizeof(shared_key)); - return 0; } @@ -1663,8 +1530,7 @@ static bool handle_sendnodes_core(void *object, const IP_Port *source, const uin } VLA(uint8_t, plain, 1 + data_size + sizeof(uint64_t)); - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - dht_get_shared_key_sent(dht, shared_key, packet + 1); + const uint8_t *shared_key = dht_get_shared_key_sent(dht, packet + 1); const int len = decrypt_data_symmetric( shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1672,8 +1538,6 @@ static bool handle_sendnodes_core(void *object, const IP_Port *source, const uin 1 + data_size + sizeof(uint64_t) + CRYPTO_MAC_SIZE, plain); - crypto_memzero(shared_key, sizeof(shared_key)); - if ((unsigned int)len != SIZEOF_VLA(plain)) { return false; } @@ -1745,35 +1609,75 @@ static int handle_sendnodes_ipv6(void *object, const IP_Port *source, const uint /*----------------------------------------------------------------------------------*/ /*------------------------END of packet handling functions--------------------------*/ -non_null(1) nullable(2, 3, 5) -static void dht_friend_lock(DHT_Friend *const dht_friend, dht_ip_cb *ip_callback, - void *data, int32_t number, uint16_t *lock_count) +non_null(1) nullable(2, 3) +static uint32_t dht_friend_lock(DHT_Friend *const dht_friend, dht_ip_cb *ip_callback, + void *data, int32_t number) { - const uint16_t lock_num = dht_friend->lock_count; - ++dht_friend->lock_count; + // find first free slot + uint8_t lock_num; + uint32_t lock_token = 0; + for (lock_num = 0; lock_num < DHT_FRIEND_MAX_LOCKS; ++lock_num) { + lock_token = UINT32_C(1) << lock_num; + if ((dht_friend->lock_flags & lock_token) == 0) { + break; + } + } + + // One of the conditions would be enough, but static analyzers don't get that + if (lock_token == 0 || lock_num == DHT_FRIEND_MAX_LOCKS) { + return 0; + } + + // Claim that slot + dht_friend->lock_flags |= lock_token; + dht_friend->callbacks[lock_num].ip_callback = ip_callback; dht_friend->callbacks[lock_num].data = data; dht_friend->callbacks[lock_num].number = number; - if (lock_count != nullptr) { - *lock_count = lock_num + 1; + return lock_token; +} + +non_null() +static void dht_friend_unlock(DHT_Friend *const dht_friend, uint32_t lock_token) +{ + // If this triggers, there was a double free + assert((lock_token & dht_friend->lock_flags) > 0); + + // find used slot + uint8_t lock_num; + for (lock_num = 0; lock_num < DHT_FRIEND_MAX_LOCKS; ++lock_num) { + if (((UINT32_C(1) << lock_num) & lock_token) > 0) { + break; + } } + + if (lock_num == DHT_FRIEND_MAX_LOCKS) { + // Gracefully handle double unlock + return; + } + + // Clear the slot + dht_friend->lock_flags &= ~lock_token; + + dht_friend->callbacks[lock_num].ip_callback = nullptr; + dht_friend->callbacks[lock_num].data = nullptr; + dht_friend->callbacks[lock_num].number = 0; } int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, - void *data, int32_t number, uint16_t *lock_count) + void *data, int32_t number, uint32_t *lock_token) { const uint32_t friend_num = index_of_friend_pk(dht->friends_list, dht->num_friends, public_key); if (friend_num != UINT32_MAX) { /* Is friend already in DHT? */ DHT_Friend *const dht_friend = &dht->friends_list[friend_num]; + const uint32_t tmp_lock_token = dht_friend_lock(dht_friend, ip_callback, data, number); - if (dht_friend->lock_count == DHT_FRIEND_MAX_LOCKS) { + if (tmp_lock_token == 0) { return -1; } - dht_friend_lock(dht_friend, ip_callback, data, number, lock_count); - return 0; } @@ -1791,7 +1695,8 @@ int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, dht_friend->nat.nat_ping_id = random_u64(dht->rng); ++dht->num_friends; - dht_friend_lock(dht_friend, ip_callback, data, number, lock_count); + *lock_token = dht_friend_lock(dht_friend, ip_callback, data, number); + assert(*lock_token != 0); // Friend was newly allocated dht_friend->num_to_bootstrap = get_close_nodes(dht, dht_friend->public_key, dht_friend->to_bootstrap, net_family_unspec(), true, false); @@ -1799,7 +1704,7 @@ int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, return 0; } -int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count) +int dht_delfriend(DHT *dht, const uint8_t *public_key, uint32_t lock_token) { const uint32_t friend_num = index_of_friend_pk(dht->friends_list, dht->num_friends, public_key); @@ -1808,13 +1713,9 @@ int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count) } DHT_Friend *const dht_friend = &dht->friends_list[friend_num]; - --dht_friend->lock_count; - - if (dht_friend->lock_count > 0 && lock_count > 0) { /* DHT friend is still in use.*/ - --lock_count; - dht_friend->callbacks[lock_count].ip_callback = nullptr; - dht_friend->callbacks[lock_count].data = nullptr; - dht_friend->callbacks[lock_count].number = 0; + dht_friend_unlock(dht_friend, lock_token); + if (dht_friend->lock_flags > 0) { + /* DHT friend is still in use.*/ return 0; } @@ -1855,7 +1756,7 @@ int dht_getfriendip(const DHT *dht, const uint8_t *public_key, IP_Port *ip_port) const DHT_Friend *const frnd = &dht->friends_list[friend_index]; const uint32_t client_index = index_of_client_pk(frnd->client_list, MAX_FRIEND_CLIENTS, public_key); - if (client_index == -1) { + if (client_index == UINT32_MAX) { return 0; } @@ -2059,11 +1960,6 @@ int dht_bootstrap_from_address(DHT *dht, const char *address, bool ipv6enabled, return 0; } -/** @brief Send the given packet to node with public_key. - * - * @return number of bytes sent. - * @retval -1 if failure. - */ int route_packet(const DHT *dht, const uint8_t *public_key, const uint8_t *packet, uint16_t length) { for (uint32_t i = 0; i < LCLIENT_LIST; ++i) { @@ -2752,6 +2648,15 @@ DHT *new_dht(const Logger *log, const Random *rng, const Network *ns, Mono_Time crypto_new_keypair(rng, dht->self_public_key, dht->self_secret_key); + dht->shared_keys_recv = shared_key_cache_new(mono_time, dht->self_secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + dht->shared_keys_sent = shared_key_cache_new(mono_time, dht->self_secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + + if (dht->shared_keys_recv == nullptr || dht->shared_keys_sent == nullptr) { + kill_dht(dht); + return nullptr; + } + + dht->dht_ping_array = ping_array_new(DHT_PING_ARRAY_SIZE, PING_TIMEOUT); if (dht->dht_ping_array == nullptr) { @@ -2765,7 +2670,8 @@ DHT *new_dht(const Logger *log, const Random *rng, const Network *ns, Mono_Time crypto_new_keypair(rng, random_public_key_bytes, random_secret_key_bytes); - if (dht_addfriend(dht, random_public_key_bytes, nullptr, nullptr, 0, nullptr) != 0) { + uint32_t token; // We don't intend to delete these ever, but need to pass the token + if (dht_addfriend(dht, random_public_key_bytes, nullptr, nullptr, 0, &token) != 0) { kill_dht(dht); return nullptr; } @@ -2813,12 +2719,12 @@ void kill_dht(DHT *dht) networking_registerhandler(dht->net, NET_PACKET_LAN_DISCOVERY, nullptr, nullptr); cryptopacket_registerhandler(dht, CRYPTO_PACKET_NAT_PING, nullptr, nullptr); + shared_key_cache_free(dht->shared_keys_recv); + shared_key_cache_free(dht->shared_keys_sent); ping_array_kill(dht->dht_ping_array); ping_kill(dht->ping); free(dht->friends_list); free(dht->loaded_nodes_list); - crypto_memzero(&dht->shared_keys_recv, sizeof(dht->shared_keys_recv)); - crypto_memzero(&dht->shared_keys_sent, sizeof(dht->shared_keys_sent)); crypto_memzero(dht->self_secret_key, sizeof(dht->self_secret_key)); free(dht); } @@ -2929,11 +2835,6 @@ void dht_save(const DHT *dht, uint8_t *data) /** Bootstrap from this number of nodes every time `dht_connect_after_load()` is called */ #define SAVE_BOOTSTAP_FREQUENCY 8 -/** @brief Start sending packets after DHT loaded_friends_list and loaded_clients_list are set. - * - * @retval 0 if successful - * @retval -1 otherwise - */ int dht_connect_after_load(DHT *dht) { if (dht == nullptr) { @@ -3003,11 +2904,6 @@ static State_Load_Status dht_load_state_callback(void *outer, const uint8_t *dat return STATE_LOAD_STATUS_CONTINUE; } -/** @brief Load the DHT from data of size size. - * - * @retval -1 if failure. - * @retval 0 if success. - */ int dht_load(DHT *dht, const uint8_t *data, uint32_t length) { const uint32_t cookie_len = sizeof(uint32_t); @@ -3066,16 +2962,6 @@ bool dht_non_lan_connected(const DHT *dht) return false; } -/** @brief Copies our own ip_port structure to `dest`. - * - * WAN addresses take priority over LAN addresses. - * - * This function will zero the `dest` buffer before use. - * - * @retval 0 if our ip port can't be found (this usually means we're not connected to the DHT). - * @retval 1 if IP is a WAN address. - * @retval 2 if IP is a LAN address. - */ unsigned int ipport_self_copy(const DHT *dht, IP_Port *dest) { ipport_reset(dest); diff --git a/protocols/Tox/libtox/src/toxcore/DHT.h b/protocols/Tox/libtox/src/toxcore/DHT.h index 34ecf9dd0b..86ac4f9d40 100644 --- a/protocols/Tox/libtox/src/toxcore/DHT.h +++ b/protocols/Tox/libtox/src/toxcore/DHT.h @@ -22,6 +22,20 @@ extern "C" { #endif +/* Encryption and signature keys definition */ +#define ENC_PUBLIC_KEY_SIZE CRYPTO_PUBLIC_KEY_SIZE +#define ENC_SECRET_KEY_SIZE CRYPTO_SECRET_KEY_SIZE +#define SIG_PUBLIC_KEY_SIZE CRYPTO_SIGN_PUBLIC_KEY_SIZE +#define SIG_SECRET_KEY_SIZE CRYPTO_SIGN_SECRET_KEY_SIZE + +/* Size of the group chat_id */ +#define CHAT_ID_SIZE SIG_PUBLIC_KEY_SIZE + +/* Extended keys for group chats */ +#define EXT_SECRET_KEY_SIZE (ENC_SECRET_KEY_SIZE + SIG_SECRET_KEY_SIZE) +#define EXT_PUBLIC_KEY_SIZE (ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE) + + /* Maximum size of a signature (may be smaller) */ #define SIGNATURE_SIZE CRYPTO_SIGNATURE_SIZE /** Maximum number of clients stored per friend. */ @@ -172,8 +186,6 @@ typedef struct NAT { uint64_t nat_ping_timestamp; } NAT; -#define DHT_FRIEND_MAX_LOCKS 32 - typedef struct Node_format { uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; IP_Port ip_port; @@ -242,24 +254,6 @@ non_null(1, 4) nullable(3) int unpack_nodes(Node_format *nodes, uint16_t max_num_nodes, uint16_t *processed_data_len, const uint8_t *data, uint16_t length, bool tcp_enabled); - -/*----------------------------------------------------------------------------------*/ -/* struct to store some shared keys so we don't have to regenerate them for each request. */ -#define MAX_KEYS_PER_SLOT 4 -#define KEYS_TIMEOUT 600 - -typedef struct Shared_Key { - uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - uint32_t times_requested; - bool stored; - uint64_t time_last_requested; -} Shared_Key; - -typedef struct Shared_Keys { - Shared_Key keys[256 * MAX_KEYS_PER_SLOT]; -} Shared_Keys; - /*----------------------------------------------------------------------------------*/ typedef int cryptopacket_handler_cb(void *object, const IP_Port *ip_port, const uint8_t *source_pubkey, @@ -284,30 +278,18 @@ non_null() const uint8_t *dht_get_friend_public_key(const DHT *dht, uint32_t fri /*----------------------------------------------------------------------------------*/ /** - * Shared key generations are costly, it is therefore smart to store commonly used - * ones so that they can be re-used later without being computed again. - * - * If a shared key is already in shared_keys, copy it to shared_key. - * Otherwise generate it into shared_key and copy it to shared_keys - */ -non_null() -void get_shared_key( - const Mono_Time *mono_time, Shared_Keys *shared_keys, uint8_t *shared_key, - const uint8_t *secret_key, const uint8_t *public_key); - -/** * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key * for packets that we receive. */ non_null() -void dht_get_shared_key_recv(DHT *dht, uint8_t *shared_key, const uint8_t *public_key); +const uint8_t *dht_get_shared_key_recv(DHT *dht, const uint8_t *public_key); /** * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key * for packets that we send. */ non_null() -void dht_get_shared_key_sent(DHT *dht, uint8_t *shared_key, const uint8_t *public_key); +const uint8_t *dht_get_shared_key_sent(DHT *dht, const uint8_t *public_key); /** * Sends a getnodes request to `ip_port` with the public key `public_key` for nodes @@ -327,29 +309,34 @@ non_null(1) nullable(2) void dht_callback_get_nodes_response(DHT *dht, dht_get_nodes_response_cb *function); /** @brief Add a new friend to the friends list. - * public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. + * @param public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. * - * ip_callback is the callback of a function that will be called when the ip address + * @param ip_callback is the callback of a function that will be called when the ip address * is found along with arguments data and number. + * @param data User data for the callback + * @param number Will be passed to ip_callback * - * lock_count will be set to a non zero number that must be passed to `dht_delfriend()` + * @param lock_token will be set to a non zero number that must be passed to `dht_delfriend()` * to properly remove the callback. * * @retval 0 if success. * @retval -1 if failure (friends list is full). */ -non_null(1, 2) nullable(3, 4, 6) +non_null(1, 2, 6) nullable(3, 4) int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, - void *data, int32_t number, uint16_t *lock_count); + void *data, int32_t number, uint32_t *lock_token); /** @brief Delete a friend from the friends list. * public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. + * @param dht The DHT object + * @param public_key The public key of the friend + * @param lock_token The token received by dht_addfriend(...) * * @retval 0 if success. * @retval -1 if failure (public_key not in friends list). */ non_null() -int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count); +int dht_delfriend(DHT *dht, const uint8_t *public_key, uint32_t lock_token); /** @brief Get ip of friend. * diff --git a/protocols/Tox/libtox/src/toxcore/LAN_discovery.c b/protocols/Tox/libtox/src/toxcore/LAN_discovery.c index 82c02b59e9..ef44d3b552 100644 --- a/protocols/Tox/libtox/src/toxcore/LAN_discovery.c +++ b/protocols/Tox/libtox/src/toxcore/LAN_discovery.c @@ -339,7 +339,8 @@ bool ip_is_lan(const IP *ip) } -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port) +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, + uint16_t port) { if (broadcast == nullptr) { return false; diff --git a/protocols/Tox/libtox/src/toxcore/LAN_discovery.h b/protocols/Tox/libtox/src/toxcore/LAN_discovery.h index 85ea9478f4..5d9c83335b 100644 --- a/protocols/Tox/libtox/src/toxcore/LAN_discovery.h +++ b/protocols/Tox/libtox/src/toxcore/LAN_discovery.h @@ -24,7 +24,8 @@ typedef struct Broadcast_Info Broadcast_Info; * @return true on success, false on failure. */ non_null() -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port); +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, + uint16_t port); /** * Discovers broadcast devices and IP addresses. diff --git a/protocols/Tox/libtox/src/toxcore/Messenger.c b/protocols/Tox/libtox/src/toxcore/Messenger.c index 6042fd725b..118c7d946c 100644 --- a/protocols/Tox/libtox/src/toxcore/Messenger.c +++ b/protocols/Tox/libtox/src/toxcore/Messenger.c @@ -14,7 +14,10 @@ #include <string.h> #include <time.h> +#include "DHT.h" #include "ccompat.h" +#include "group_chats.h" +#include "group_onion_announce.h" #include "logger.h" #include "mono_time.h" #include "network.h" @@ -26,6 +29,16 @@ static_assert(MAX_CONCURRENT_FILE_PIPES <= UINT8_MAX + 1, static const Friend empty_friend = {{0}}; +/** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +bool friend_is_valid(const Messenger *m, int32_t friendnumber) +{ + return (uint32_t)friendnumber < m->numfriends && m->friendlist[friendnumber].status != 0; +} + /** @brief Set the size of the friend list to numfriends. * * @retval -1 if realloc fails. @@ -107,15 +120,11 @@ void getaddress(const Messenger *m, uint8_t *address) } non_null() -static bool send_online_packet(Messenger *m, int32_t friendnumber) +static bool send_online_packet(Messenger *m, int friendcon_id) { - if (!m_friend_exists(m, friendnumber)) { - return false; - } - uint8_t packet = PACKET_ID_ONLINE; - return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, - m->friendlist[friendnumber].friendcon_id), &packet, sizeof(packet), false) != -1; + return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, friendcon_id), &packet, + sizeof(packet), false) != -1; } non_null() @@ -174,7 +183,7 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta } if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { - send_online_packet(m, i); + send_online_packet(m, friendcon_id); } return i; @@ -184,6 +193,20 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta return FAERR_NOMEM; } +non_null() +static int32_t m_add_friend_contact_norequest(Messenger *m, const uint8_t *real_pk) +{ + if (getfriend_id(m, real_pk) != -1) { + return FAERR_ALREADYSENT; + } + + if (pk_equal(real_pk, nc_get_self_public_key(m->net_crypto))) { + return FAERR_OWNKEY; + } + + return init_new_friend(m, real_pk, FRIEND_CONFIRMED); +} + /** * Add a friend. * @@ -268,10 +291,6 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk) { - if (getfriend_id(m, real_pk) != -1) { - return FAERR_ALREADYSENT; - } - if (!public_key_valid(real_pk)) { return FAERR_BADCHECKSUM; } @@ -280,7 +299,7 @@ int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk) return FAERR_OWNKEY; } - return init_new_friend(m, real_pk, FRIEND_CONFIRMED); + return m_add_friend_contact_norequest(m, real_pk); } non_null() @@ -344,6 +363,53 @@ static int friend_received_packet(const Messenger *m, int32_t friendnumber, uint m->friendlist[friendnumber].friendcon_id), number); } +bool m_create_group_connection(Messenger *m, GC_Chat *chat) +{ + random_bytes(m->rng, chat->m_group_public_key, CRYPTO_PUBLIC_KEY_SIZE); + const int friendcon_id = new_friend_connection(m->fr_c, chat->m_group_public_key); + + if (friendcon_id == -1) { + return false; + } + + const Friend_Conn *connection = get_conn(m->fr_c, friendcon_id); + + if (connection == nullptr) { + return false; + } + + chat->friend_connection_id = friendcon_id; + + if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { + send_online_packet(m, friendcon_id); + } + + const int onion_friend_number = friend_conn_get_onion_friendnum(connection); + Onion_Friend *onion_friend = onion_get_friend(m->onion_c, (uint16_t)onion_friend_number); + + onion_friend_set_gc_public_key(onion_friend, get_chat_id(chat->chat_public_key)); + onion_friend_set_gc_data(onion_friend, nullptr, 0); + + return true; +} + +/** + * Kills the friend connection for a groupchat. + */ +void m_kill_group_connection(Messenger *m, const GC_Chat *chat) +{ + remove_request_received(m->fr, chat->m_group_public_key); + + friend_connection_callbacks(m->fr_c, chat->friend_connection_id, MESSENGER_CALLBACK_INDEX, nullptr, + nullptr, nullptr, nullptr, 0); + + if (friend_con_connected(m->fr_c, chat->friend_connection_id) == FRIENDCONN_STATUS_CONNECTED) { + send_offline_packet(m, chat->friend_connection_id); + } + + kill_friend_connection(m->fr_c, chat->friend_connection_id); +} + non_null(1) nullable(3) static int do_receipts(Messenger *m, int32_t friendnumber, void *userdata) { @@ -991,6 +1057,11 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function m->conference_invite = function; } +/** @brief the callback for group invites. */ +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function) +{ + m->group_invite = function; +} /** @brief Send a conference invite packet. * @@ -1002,6 +1073,17 @@ bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, con return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_CONFERENCE, data, length, false); } + +/** @brief Send a group invite packet. + * + * @retval true if success + */ +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length) +{ + return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_GROUPCHAT, data, length, false); +} + + /*** FILE SENDING */ @@ -1922,12 +2004,13 @@ static void check_friend_request_timed_out(Messenger *m, uint32_t i, uint64_t t, } } +non_null(1) nullable(4) static int m_handle_status(void *object, int i, bool status, void *userdata) { Messenger *m = (Messenger *)object; if (status) { /* Went online. */ - send_online_packet(m, i); + send_online_packet(m, m->friendlist[i].friendcon_id); } else { /* Went offline. */ if (m->friendlist[i].status == FRIEND_ONLINE) { set_friend_status(m, i, FRIEND_CONFIRMED, userdata); @@ -1937,321 +2020,396 @@ static int m_handle_status(void *object, int i, bool status, void *userdata) return 0; } -static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t len, void *userdata) +non_null(1, 3) nullable(5) +static int m_handle_packet_offline(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) { - if (len == 0) { - return -1; + if (data_length == 0) { + set_friend_status(m, i, FRIEND_CONFIRMED, userdata); } - Messenger *m = (Messenger *)object; - const uint8_t packet_id = temp[0]; - const uint8_t *data = temp + 1; - const uint16_t data_length = len - 1; + return 0; +} - if (m->friendlist[i].status != FRIEND_ONLINE) { - if (packet_id == PACKET_ID_ONLINE && len == 1) { - set_friend_status(m, i, FRIEND_ONLINE, userdata); - send_online_packet(m, i); - } else { - return -1; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_nickname(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length > MAX_NAME_LENGTH) { + return 0; } - switch (packet_id) { - case PACKET_ID_OFFLINE: { - if (data_length > 0) { - break; - } - - set_friend_status(m, i, FRIEND_CONFIRMED, userdata); - break; - } + /* Make sure the NULL terminator is present. */ + VLA(uint8_t, data_terminated, data_length + 1); + memcpy(data_terminated, data, data_length); + data_terminated[data_length] = 0; - case PACKET_ID_NICKNAME: { - if (data_length > MAX_NAME_LENGTH) { - break; - } + /* inform of namechange before we overwrite the old name */ + if (m->friend_namechange != nullptr) { + m->friend_namechange(m, i, data_terminated, data_length, userdata); + } - /* Make sure the NULL terminator is present. */ - VLA(uint8_t, data_terminated, data_length + 1); - memcpy(data_terminated, data, data_length); - data_terminated[data_length] = 0; + memcpy(m->friendlist[i].name, data_terminated, data_length); + m->friendlist[i].name_length = data_length; - /* inform of namechange before we overwrite the old name */ - if (m->friend_namechange != nullptr) { - m->friend_namechange(m, i, data_terminated, data_length, userdata); - } + return 0; +} - memcpy(m->friendlist[i].name, data_terminated, data_length); - m->friendlist[i].name_length = data_length; +non_null(1, 3) nullable(5) +static int m_handle_packet_statusmessage(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length > MAX_STATUSMESSAGE_LENGTH) { + return 0; + } - break; - } + /* Make sure the NULL terminator is present. */ + VLA(uint8_t, data_terminated, data_length + 1); + memcpy(data_terminated, data, data_length); + data_terminated[data_length] = 0; - case PACKET_ID_STATUSMESSAGE: { - if (data_length > MAX_STATUSMESSAGE_LENGTH) { - break; - } + if (m->friend_statusmessagechange != nullptr) { + m->friend_statusmessagechange(m, i, data_terminated, data_length, userdata); + } - /* Make sure the NULL terminator is present. */ - VLA(uint8_t, data_terminated, data_length + 1); - memcpy(data_terminated, data, data_length); - data_terminated[data_length] = 0; + set_friend_statusmessage(m, i, data_terminated, data_length); - if (m->friend_statusmessagechange != nullptr) { - m->friend_statusmessagechange(m, i, data_terminated, data_length, userdata); - } + return 0; +} - set_friend_statusmessage(m, i, data_terminated, data_length); - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_userstatus(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length != 1) { + return 0; + } - case PACKET_ID_USERSTATUS: { - if (data_length != 1) { - break; - } + const Userstatus status = (Userstatus)data[0]; - const Userstatus status = (Userstatus)data[0]; + if (status >= USERSTATUS_INVALID) { + return 0; + } - if (status >= USERSTATUS_INVALID) { - break; - } + if (m->friend_userstatuschange != nullptr) { + m->friend_userstatuschange(m, i, status, userdata); + } - if (m->friend_userstatuschange != nullptr) { - m->friend_userstatuschange(m, i, status, userdata); - } + set_friend_userstatus(m, i, status); - set_friend_userstatus(m, i, status); - break; - } + return 0; +} - case PACKET_ID_TYPING: { - if (data_length != 1) { - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_typing(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length != 1) { + return 0; + } - const bool typing = data[0] != 0; + const bool typing = data[0] != 0; - set_friend_typing(m, i, typing); + set_friend_typing(m, i, typing); - if (m->friend_typingchange != nullptr) { - m->friend_typingchange(m, i, typing, userdata); - } + if (m->friend_typingchange != nullptr) { + m->friend_typingchange(m, i, typing, userdata); + } - break; - } + return 0; +} - case PACKET_ID_MESSAGE: // fall-through - case PACKET_ID_ACTION: { - if (data_length == 0) { - break; - } +non_null(1, 3) nullable(6) +static int m_handle_packet_message(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, const Message_Type message_type, void *userdata) +{ + if (data_length == 0) { + return 0; + } - const uint8_t *message = data; - const uint16_t message_length = data_length; + const uint8_t *message = data; + const uint16_t message_length = data_length; - /* Make sure the NULL terminator is present. */ - VLA(uint8_t, message_terminated, message_length + 1); - memcpy(message_terminated, message, message_length); - message_terminated[message_length] = 0; - const uint8_t type = packet_id - PACKET_ID_MESSAGE; + /* Make sure the NULL terminator is present. */ + VLA(uint8_t, message_terminated, message_length + 1); + memcpy(message_terminated, message, message_length); + message_terminated[message_length] = 0; - if (m->friend_message != nullptr) { - m->friend_message(m, i, type, message_terminated, message_length, userdata); - } + if (m->friend_message != nullptr) { + m->friend_message(m, i, message_type, message_terminated, message_length, userdata); + } - break; - } + return 0; +} - case PACKET_ID_INVITE_CONFERENCE: { - if (data_length == 0) { - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_invite_conference(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length == 0) { + return 0; + } - if (m->conference_invite != nullptr) { - m->conference_invite(m, i, data, data_length, userdata); - } + if (m->conference_invite != nullptr) { + m->conference_invite(m, i, data, data_length, userdata); + } - break; - } + return 0; +} - case PACKET_ID_FILE_SENDREQUEST: { - const unsigned int head_length = 1 + sizeof(uint32_t) + sizeof(uint64_t) + FILE_ID_LENGTH; +non_null(1, 3) nullable(5) +static int m_handle_packet_file_sendrequest(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + const unsigned int head_length = 1 + sizeof(uint32_t) + sizeof(uint64_t) + FILE_ID_LENGTH; - if (data_length < head_length) { - break; - } + if (data_length < head_length) { + return 0; + } - const uint8_t filenumber = data[0]; + const uint8_t filenumber = data[0]; #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES - if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { - break; - } + if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { + return 0; + } #endif - uint64_t filesize; - uint32_t file_type; - const uint16_t filename_length = data_length - head_length; + uint64_t filesize; + uint32_t file_type; + const uint16_t filename_length = data_length - head_length; - if (filename_length > MAX_FILENAME_LENGTH) { - break; - } + if (filename_length > MAX_FILENAME_LENGTH) { + return 0; + } - memcpy(&file_type, data + 1, sizeof(file_type)); - file_type = net_ntohl(file_type); + memcpy(&file_type, data + 1, sizeof(file_type)); + file_type = net_ntohl(file_type); - net_unpack_u64(data + 1 + sizeof(uint32_t), &filesize); - struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; + net_unpack_u64(data + 1 + sizeof(uint32_t), &filesize); + struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; - if (ft->status != FILESTATUS_NONE) { - break; - } + if (ft->status != FILESTATUS_NONE) { + return 0; + } - ft->status = FILESTATUS_NOT_ACCEPTED; - ft->size = filesize; - ft->transferred = 0; - ft->paused = FILE_PAUSE_NOT; - memcpy(ft->id, data + 1 + sizeof(uint32_t) + sizeof(uint64_t), FILE_ID_LENGTH); + ft->status = FILESTATUS_NOT_ACCEPTED; + ft->size = filesize; + ft->transferred = 0; + ft->paused = FILE_PAUSE_NOT; + memcpy(ft->id, data + 1 + sizeof(uint32_t) + sizeof(uint64_t), FILE_ID_LENGTH); - VLA(uint8_t, filename_terminated, filename_length + 1); - const uint8_t *filename = nullptr; + VLA(uint8_t, filename_terminated, filename_length + 1); + const uint8_t *filename = nullptr; - if (filename_length > 0) { - /* Force NULL terminate file name. */ - memcpy(filename_terminated, data + head_length, filename_length); - filename_terminated[filename_length] = 0; - filename = filename_terminated; - } + if (filename_length > 0) { + /* Force NULL terminate file name. */ + memcpy(filename_terminated, data + head_length, filename_length); + filename_terminated[filename_length] = 0; + filename = filename_terminated; + } - uint32_t real_filenumber = filenumber; - real_filenumber += 1; - real_filenumber <<= 16; + uint32_t real_filenumber = filenumber; + real_filenumber += 1; + real_filenumber <<= 16; - if (m->file_sendrequest != nullptr) { - m->file_sendrequest(m, i, real_filenumber, file_type, filesize, filename, filename_length, - userdata); - } + if (m->file_sendrequest != nullptr) { + m->file_sendrequest(m, i, real_filenumber, file_type, filesize, filename, filename_length, + userdata); + } - break; - } + return 0; +} - case PACKET_ID_FILE_CONTROL: { - if (data_length < 3) { - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_file_control(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length < 3) { + return 0; + } - // On the other side, "outbound" is "inbound", i.e. if they send 1, - // that means "inbound" on their side, but we call it "outbound" - // here. - const bool outbound = data[0] == 1; - uint8_t filenumber = data[1]; - const uint8_t control_type = data[2]; + // On the other side, "outbound" is "inbound", i.e. if they send 1, + // that means "inbound" on their side, but we call it "outbound" + // here. + const bool outbound = data[0] == 1; + const uint8_t filenumber = data[1]; + const uint8_t control_type = data[2]; #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES - if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { - break; - } + if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { + return 0; + } #endif - if (handle_filecontrol(m, i, outbound, filenumber, control_type, data + 3, data_length - 3, userdata) == -1) { - // TODO(iphydf): Do something different here? Right now, this - // check is pointless. - break; - } + if (handle_filecontrol(m, i, outbound, filenumber, control_type, data + 3, data_length - 3, userdata) == -1) { + // TODO(iphydf): Do something different here? Right now, this + // check is pointless. + return 0; + } - break; - } + return 0; +} - case PACKET_ID_FILE_DATA: { - if (data_length < 1) { - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_file_data(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length < 1) { + return 0; + } - uint8_t filenumber = data[0]; + const uint8_t filenumber = data[0]; #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES - if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { - break; - } + if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { + return 0; + } #endif - struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; + struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; - if (ft->status != FILESTATUS_TRANSFERRING) { - break; - } + if (ft->status != FILESTATUS_TRANSFERRING) { + return 0; + } - uint64_t position = ft->transferred; - uint32_t real_filenumber = filenumber; - real_filenumber += 1; - real_filenumber <<= 16; - uint16_t file_data_length = data_length - 1; - const uint8_t *file_data; + uint64_t position = ft->transferred; + uint32_t real_filenumber = filenumber; + real_filenumber += 1; + real_filenumber <<= 16; + uint16_t file_data_length = data_length - 1; + const uint8_t *file_data; - if (file_data_length == 0) { - file_data = nullptr; - } else { - file_data = data + 1; - } + if (file_data_length == 0) { + file_data = nullptr; + } else { + file_data = data + 1; + } - /* Prevent more data than the filesize from being passed to clients. */ - if ((ft->transferred + file_data_length) > ft->size) { - file_data_length = ft->size - ft->transferred; - } + /* Prevent more data than the filesize from being passed to clients. */ + if ((ft->transferred + file_data_length) > ft->size) { + file_data_length = ft->size - ft->transferred; + } - if (m->file_filedata != nullptr) { - m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); - } + if (m->file_filedata != nullptr) { + m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); + } - ft->transferred += file_data_length; + ft->transferred += file_data_length; - if (file_data_length > 0 && (ft->transferred >= ft->size || file_data_length != MAX_FILE_DATA_SIZE)) { - file_data_length = 0; - file_data = nullptr; - position = ft->transferred; + if (file_data_length > 0 && (ft->transferred >= ft->size || file_data_length != MAX_FILE_DATA_SIZE)) { + file_data_length = 0; + file_data = nullptr; + position = ft->transferred; - /* Full file received. */ - if (m->file_filedata != nullptr) { - m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); - } - } + /* Full file received. */ + if (m->file_filedata != nullptr) { + m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); + } + } - /* Data is zero, filetransfer is over. */ - if (file_data_length == 0) { - ft->status = FILESTATUS_NONE; - } + /* Data is zero, filetransfer is over. */ + if (file_data_length == 0) { + ft->status = FILESTATUS_NONE; + } - break; - } + return 0; +} - case PACKET_ID_MSI: { - if (data_length == 0) { - break; - } +non_null(1, 3) nullable(5) +static int m_handle_packet_msi(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ + if (data_length == 0) { + return 0; + } - if (m->msi_packet != nullptr) { - m->msi_packet(m, i, data, data_length, m->msi_packet_userdata); - } + if (m->msi_packet != nullptr) { + m->msi_packet(m, i, data, data_length, m->msi_packet_userdata); + } - break; - } + return 0; +} - default: { - handle_custom_lossless_packet(object, i, temp, len, userdata); - break; +non_null(1, 3) nullable(5) +static int m_handle_packet_invite_groupchat(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +#ifndef VANILLA_NACL + + // first two bytes are messenger packet type and group invite type + if (data_length < 2 + GC_JOIN_DATA_LENGTH) { + return 0; + } + + const uint8_t invite_type = data[1]; + const uint8_t *join_data = data + 2; + const uint32_t join_data_len = data_length - 2; + + if (m->group_invite != nullptr && data[1] == GROUP_INVITE && data_length != 2 + GC_JOIN_DATA_LENGTH) { + if (group_not_added(m->group_handler, join_data, join_data_len)) { + m->group_invite(m, i, join_data, GC_JOIN_DATA_LENGTH, + join_data + GC_JOIN_DATA_LENGTH, join_data_len - GC_JOIN_DATA_LENGTH, userdata); } + } else if (invite_type == GROUP_INVITE_ACCEPTED) { + handle_gc_invite_accepted_packet(m->group_handler, i, join_data, join_data_len); + } else if (invite_type == GROUP_INVITE_CONFIRMATION) { + handle_gc_invite_confirmed_packet(m->group_handler, i, join_data, join_data_len); } +#endif // VANILLA_NACL + return 0; } +non_null(1, 3) nullable(5) +static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t len, void *userdata) +{ + if (len == 0) { + return -1; + } + + Messenger *m = (Messenger *)object; + const uint8_t packet_id = temp[0]; + const uint8_t *data = temp + 1; + const uint16_t data_length = len - 1; + + if (m->friendlist[i].status != FRIEND_ONLINE) { + if (packet_id == PACKET_ID_ONLINE && len == 1) { + set_friend_status(m, i, FRIEND_ONLINE, userdata); + send_online_packet(m, m->friendlist[i].friendcon_id); + } else { + return -1; + } + } + + switch (packet_id) { + // TODO(Green-Sky): now all return 0 on error AND success, make errors errors? + case PACKET_ID_OFFLINE: + return m_handle_packet_offline(m, i, data, data_length, userdata); + case PACKET_ID_NICKNAME: + return m_handle_packet_nickname(m, i, data, data_length, userdata); + case PACKET_ID_STATUSMESSAGE: + return m_handle_packet_statusmessage(m, i, data, data_length, userdata); + case PACKET_ID_USERSTATUS: + return m_handle_packet_userstatus(m, i, data, data_length, userdata); + case PACKET_ID_TYPING: + return m_handle_packet_typing(m, i, data, data_length, userdata); + case PACKET_ID_MESSAGE: + return m_handle_packet_message(m, i, data, data_length, MESSAGE_NORMAL, userdata); + case PACKET_ID_ACTION: + return m_handle_packet_message(m, i, data, data_length, MESSAGE_ACTION, userdata); + case PACKET_ID_INVITE_CONFERENCE: + return m_handle_packet_invite_conference(m, i, data, data_length, userdata); + case PACKET_ID_FILE_SENDREQUEST: + return m_handle_packet_file_sendrequest(m, i, data, data_length, userdata); + case PACKET_ID_FILE_CONTROL: + return m_handle_packet_file_control(m, i, data, data_length, userdata); + case PACKET_ID_FILE_DATA: + return m_handle_packet_file_data(m, i, data, data_length, userdata); + case PACKET_ID_MSI: + return m_handle_packet_msi(m, i, data, data_length, userdata); + case PACKET_ID_INVITE_GROUPCHAT: + return m_handle_packet_invite_groupchat(m, i, data, data_length, userdata); + } + + return handle_custom_lossless_packet(object, i, temp, len, userdata); +} + non_null(1) nullable(2) static void do_friends(Messenger *m, void *userdata) { @@ -2370,6 +2528,89 @@ uint32_t messenger_run_interval(const Messenger *m) return crypto_interval; } +/** @brief Attempts to create a DHT announcement for a group chat with our connection info. An + * announcement can only be created if we either have a UDP or TCP connection to the network. + * + * @retval true if success. + */ +#ifndef VANILLA_NACL +non_null() +static bool self_announce_group(const Messenger *m, GC_Chat *chat, Onion_Friend *onion_friend) +{ + GC_Public_Announce announce = {{{{{0}}}}}; + + const bool ip_port_is_set = chat->self_udp_status != SELF_UDP_STATUS_NONE; + const int tcp_num = tcp_copy_connected_relays(chat->tcp_conn, announce.base_announce.tcp_relays, + GCA_MAX_ANNOUNCED_TCP_RELAYS); + + if (tcp_num == 0 && !ip_port_is_set) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + announce.base_announce.tcp_relays_count = (uint8_t)tcp_num; + announce.base_announce.ip_port_is_set = (uint8_t)(ip_port_is_set ? 1 : 0); + + if (ip_port_is_set) { + memcpy(&announce.base_announce.ip_port, &chat->self_ip_port, sizeof(IP_Port)); + } + + memcpy(announce.base_announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + memcpy(announce.chat_public_key, get_chat_id(chat->chat_public_key), ENC_PUBLIC_KEY_SIZE); + + uint8_t gc_data[GCA_MAX_DATA_LENGTH]; + const int length = gca_pack_public_announce(m->log, gc_data, GCA_MAX_DATA_LENGTH, &announce); + + if (length <= 0) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + if (gca_add_announce(m->mono_time, m->group_announce, &announce) == nullptr) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + onion_friend_set_gc_data(onion_friend, gc_data, (uint16_t)length); + chat->update_self_announces = false; + chat->last_time_self_announce = mono_time_get(chat->mono_time); + + if (tcp_num > 0) { + pk_copy(chat->announced_tcp_relay_pk, announce.base_announce.tcp_relays[0].public_key); + } else { + memset(chat->announced_tcp_relay_pk, 0, sizeof(chat->announced_tcp_relay_pk)); + } + + LOGGER_DEBUG(chat->log, "Published group announce. TCP relays: %d, UDP status: %d", tcp_num, + chat->self_udp_status); + return true; +} + +non_null() +static void do_gc_onion_friends(const Messenger *m) +{ + const uint16_t num_friends = onion_get_friend_count(m->onion_c); + + for (uint16_t i = 0; i < num_friends; ++i) { + Onion_Friend *onion_friend = onion_get_friend(m->onion_c, i); + + if (!onion_friend_is_groupchat(onion_friend)) { + continue; + } + + GC_Chat *chat = gc_get_group_by_public_key(m->group_handler, onion_friend_get_gc_public_key(onion_friend)); + + if (chat == nullptr) { + continue; + } + + if (chat->update_self_announces) { + self_announce_group(m, chat, onion_friend); + } + } +} +#endif // VANILLA_NACL + /** @brief The main loop that needs to be run at least 20 times per second. */ void do_messenger(Messenger *m, void *userdata) { @@ -2406,6 +2647,11 @@ void do_messenger(Messenger *m, void *userdata) do_onion_client(m->onion_c); do_friend_connections(m->fr_c, userdata); do_friends(m, userdata); +#ifndef VANILLA_NACL + do_gc(m->group_handler, userdata); + do_gca(m->mono_time, m->group_announce); + do_gc_onion_friends(m); +#endif m_connection_status_callback(m, userdata); if (mono_time_get(m->mono_time) > m->lastdump + DUMPING_CLIENTS_FRIENDS_EVERY_N_SECONDS) { @@ -2886,6 +3132,101 @@ static State_Load_Status friends_list_load(Messenger *m, const uint8_t *data, ui return STATE_LOAD_STATUS_CONTINUE; } +#ifndef VANILLA_NACL +non_null() +static void pack_groupchats(const GC_Session *c, Bin_Pack *bp) +{ + assert(bp != nullptr && c != nullptr); + bin_pack_array(bp, gc_count_groups(c)); + + for (uint32_t i = 0; i < c->chats_index; ++i) { // this loop must match the one in gc_count_groups() + const GC_Chat *chat = &c->chats[i]; + + if (!gc_group_is_valid(chat)) { + continue; + } + + gc_group_save(chat, bp); + } +} + +non_null() +static bool pack_groupchats_handler(Bin_Pack *bp, const void *obj) +{ + pack_groupchats((const GC_Session *)obj, bp); + return true; // TODO(iphydf): Return bool from pack functions. +} + +non_null() +static uint32_t saved_groups_size(const Messenger *m) +{ + GC_Session *c = m->group_handler; + return bin_pack_obj_size(pack_groupchats_handler, c); +} + +non_null() +static uint8_t *groups_save(const Messenger *m, uint8_t *data) +{ + const GC_Session *c = m->group_handler; + + const uint32_t num_groups = gc_count_groups(c); + + if (num_groups == 0) { + return data; + } + + const uint32_t len = m_plugin_size(m, STATE_TYPE_GROUPS); + + if (len == 0) { + return data; + } + + data = state_write_section_header(data, STATE_COOKIE_TYPE, len, STATE_TYPE_GROUPS); + + if (!bin_pack_obj(pack_groupchats_handler, c, data, len)) { + LOGGER_FATAL(m->log, "failed to pack group chats into buffer of length %u", len); + return data; + } + + data += len; + + LOGGER_DEBUG(m->log, "Saved %u groups (length %u)", num_groups, len); + + return data; +} + +non_null() +static State_Load_Status groups_load(Messenger *m, const uint8_t *data, uint32_t length) +{ + Bin_Unpack *bu = bin_unpack_new(data, length); + if (bu == nullptr) { + LOGGER_ERROR(m->log, "failed to allocate binary unpacker"); + return STATE_LOAD_STATUS_ERROR; + } + + uint32_t num_groups; + if (!bin_unpack_array(bu, &num_groups)) { + LOGGER_ERROR(m->log, "msgpack failed to unpack groupchats array: expected array"); + bin_unpack_free(bu); + return STATE_LOAD_STATUS_ERROR; + } + + LOGGER_DEBUG(m->log, "Loading %u groups (length %u)", num_groups, length); + + for (uint32_t i = 0; i < num_groups; ++i) { + const int group_number = gc_group_load(m->group_handler, bu); + + if (group_number < 0) { + LOGGER_WARNING(m->log, "Failed to load group %u", i); + } + } + + bin_unpack_free(bu); + + return STATE_LOAD_STATUS_CONTINUE; +} +#endif /* VANILLA_NACL */ + // name state plugin non_null() static uint32_t name_size(const Messenger *m) @@ -3072,6 +3413,9 @@ static void m_register_default_plugins(Messenger *m) m_register_state_plugin(m, STATE_TYPE_STATUSMESSAGE, status_message_size, load_status_message, save_status_message); m_register_state_plugin(m, STATE_TYPE_STATUS, status_size, load_status, save_status); +#ifndef VANILLA_NACL + m_register_state_plugin(m, STATE_TYPE_GROUPS, saved_groups_size, groups_load, groups_save); +#endif m_register_state_plugin(m, STATE_TYPE_TCP_RELAY, tcp_relay_size, load_tcp_relays, save_tcp_relays); m_register_state_plugin(m, STATE_TYPE_PATH_NODE, path_node_size, load_path_nodes, save_path_nodes); } @@ -3244,6 +3588,21 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network return nullptr; } +#ifndef VANILLA_NACL + m->group_announce = new_gca_list(); + + if (m->group_announce == nullptr) { + kill_net_crypto(m->net_crypto); + kill_dht(m->dht); + kill_networking(m->net); + friendreq_kill(m->fr); + logger_kill(m->log); + free(m); + return nullptr; + } + +#endif /* VANILLA_NACL */ + if (options->dht_announcements_enabled) { m->forwarding = new_forwarding(m->log, m->rng, m->mono_time, m->dht); m->announce = new_announcements(m->log, m->rng, m->mono_time, m->forwarding); @@ -3259,10 +3618,35 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network if ((options->dht_announcements_enabled && (m->forwarding == nullptr || m->announce == nullptr)) || m->onion == nullptr || m->onion_a == nullptr || m->onion_c == nullptr || m->fr_c == nullptr) { + kill_onion(m->onion); + kill_onion_announce(m->onion_a); + kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif /* VANILLA_NACL */ kill_friend_connections(m->fr_c); + kill_announcements(m->announce); + kill_forwarding(m->forwarding); + kill_net_crypto(m->net_crypto); + kill_dht(m->dht); + kill_networking(m->net); + friendreq_kill(m->fr); + logger_kill(m->log); + free(m); + return nullptr; + } + +#ifndef VANILLA_NACL + gca_onion_init(m->group_announce, m->onion_a); + + m->group_handler = new_dht_groupchats(m); + + if (m->group_handler == nullptr) { kill_onion(m->onion); kill_onion_announce(m->onion_a); kill_onion_client(m->onion_c); + kill_gca(m->group_announce); + kill_friend_connections(m->fr_c); kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); @@ -3274,15 +3658,23 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network return nullptr; } +#endif /* VANILLA_NACL */ + if (options->tcp_server_port != 0) { m->tcp_server = new_TCP_server(m->log, m->rng, m->ns, options->ipv6enabled, 1, &options->tcp_server_port, dht_get_self_secret_key(m->dht), m->onion, m->forwarding); if (m->tcp_server == nullptr) { - kill_friend_connections(m->fr_c); kill_onion(m->onion); kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL + kill_dht_groupchats(m->group_handler); +#endif + kill_friend_connections(m->fr_c); kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); @@ -3332,10 +3724,16 @@ void kill_messenger(Messenger *m) kill_TCP_server(m->tcp_server); } - kill_friend_connections(m->fr_c); kill_onion(m->onion); kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL + kill_dht_groupchats(m->group_handler); +#endif + kill_friend_connections(m->fr_c); kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); diff --git a/protocols/Tox/libtox/src/toxcore/Messenger.h b/protocols/Tox/libtox/src/toxcore/Messenger.h index 4abd7905d0..8279815277 100644 --- a/protocols/Tox/libtox/src/toxcore/Messenger.h +++ b/protocols/Tox/libtox/src/toxcore/Messenger.h @@ -15,6 +15,8 @@ #include "forwarding.h" #include "friend_connection.h" #include "friend_requests.h" +#include "group_announce.h" +#include "group_common.h" #include "logger.h" #include "net_crypto.h" #include "state.h" @@ -35,7 +37,11 @@ typedef enum Message_Type { MESSAGE_ACTION, } Message_Type; +// TODO(Jfreegman, Iphy): Remove this before merge +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED typedef struct Messenger Messenger; +#endif // MESSENGER_DEFINED // Returns the size of the data typedef uint32_t m_state_size_cb(const Messenger *m); @@ -191,6 +197,8 @@ typedef void m_friend_connectionstatuschange_internal_cb(Messenger *m, uint32_t uint8_t connection_status, void *user_data); typedef void m_conference_invite_cb(Messenger *m, uint32_t friend_number, const uint8_t *cookie, uint16_t length, void *user_data); +typedef void m_group_invite_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *userdata); typedef void m_msi_packet_cb(Messenger *m, uint32_t friend_number, const uint8_t *data, uint16_t length, void *user_data); typedef int m_lossy_rtp_packet_cb(Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t len, void *object); @@ -269,6 +277,9 @@ struct Messenger { uint64_t lastdump; uint8_t is_receiving_file; + GC_Session *group_handler; + GC_Announces_List *group_announce; + bool has_added_relays; // If the first connection has occurred in do_messenger uint16_t num_loaded_relays; @@ -288,6 +299,8 @@ struct Messenger { struct Group_Chats *conferences_object; /* Set by new_groupchats()*/ m_conference_invite_cb *conference_invite; + m_group_invite_cb *group_invite; + m_file_recv_cb *file_sendrequest; m_file_recv_control_cb *file_filecontrol; m_file_recv_chunk_cb *file_filedata; @@ -306,6 +319,14 @@ struct Messenger { }; /** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +non_null() +bool friend_is_valid(const Messenger *m, int32_t friendnumber); + +/** * Format: `[real_pk (32 bytes)][nospam number (4 bytes)][checksum (2 bytes)]` * * @param[out] address FRIEND_ADDRESS_SIZE byte address to give to others. @@ -348,6 +369,19 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u non_null() int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk); +/** @brief Initializes the friend connection and onion connection for a groupchat. + * + * @retval true on success. + */ +non_null() +bool m_create_group_connection(Messenger *m, GC_Chat *chat); + +/* + * Kills the friend connection for a groupchat. + */ +non_null() +void m_kill_group_connection(Messenger *m, const GC_Chat *chat); + /** @return the friend number associated to that public key. * @retval -1 if no such friend. */ @@ -589,6 +623,12 @@ non_null() void m_callback_core_connection(Messenger *m, m_self_connection_statu non_null(1) nullable(2) void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function); +/* Set the callback for group invites. + */ +non_null(1) nullable(2) +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function); + + /** @brief Send a conference invite packet. * * return true on success @@ -597,6 +637,17 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function non_null() bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, const uint8_t *data, uint16_t length); +/* Send a group invite packet. + * + * WARNING: Return-value semantics are different than for + * send_conference_invite_packet(). + * + * return true on success + */ +non_null() +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length); + + /*** FILE SENDING */ @@ -678,7 +729,8 @@ int file_seek(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uin * @retval -7 if wrong position. */ non_null(1) nullable(5) -int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, const uint8_t *data, uint16_t length); +int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, + const uint8_t *data, uint16_t length); /*** A/V related */ diff --git a/protocols/Tox/libtox/src/toxcore/TCP_common.c b/protocols/Tox/libtox/src/toxcore/TCP_common.c index 349e35fda7..a42faced1a 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_common.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_common.c @@ -278,7 +278,7 @@ int read_packet_TCP_secure_connection( return -1; } - VLA(uint8_t, data_encrypted, *next_packet_length); + VLA(uint8_t, data_encrypted, (int) *next_packet_length); const int len_packet = read_TCP_packet(logger, ns, sock, data_encrypted, *next_packet_length, ip_port); if (len_packet == -1) { @@ -286,7 +286,7 @@ int read_packet_TCP_secure_connection( } if (len_packet != *next_packet_length) { - LOGGER_ERROR(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length); + LOGGER_WARNING(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length); return 0; } diff --git a/protocols/Tox/libtox/src/toxcore/TCP_common.h b/protocols/Tox/libtox/src/toxcore/TCP_common.h index 88c0cb665a..78d1623d8a 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_common.h +++ b/protocols/Tox/libtox/src/toxcore/TCP_common.h @@ -23,6 +23,20 @@ void wipe_priority_list(TCP_Priority_List *p); #define NUM_RESERVED_PORTS 16 #define NUM_CLIENT_CONNECTIONS (256 - NUM_RESERVED_PORTS) +#ifdef USE_TEST_NETWORK +#define TCP_PACKET_FORWARD_REQUEST 11 +#define TCP_PACKET_FORWARDING 10 +#define TCP_PACKET_ROUTING_REQUEST 9 +#define TCP_PACKET_ROUTING_RESPONSE 8 +#define TCP_PACKET_CONNECTION_NOTIFICATION 7 +#define TCP_PACKET_DISCONNECT_NOTIFICATION 6 +#define TCP_PACKET_PING 5 +#define TCP_PACKET_PONG 4 +#define TCP_PACKET_OOB_SEND 3 +#define TCP_PACKET_OOB_RECV 2 +#define TCP_PACKET_ONION_REQUEST 1 +#define TCP_PACKET_ONION_RESPONSE 0 +#else #define TCP_PACKET_ROUTING_REQUEST 0 #define TCP_PACKET_ROUTING_RESPONSE 1 #define TCP_PACKET_CONNECTION_NOTIFICATION 2 @@ -35,6 +49,7 @@ void wipe_priority_list(TCP_Priority_List *p); #define TCP_PACKET_ONION_RESPONSE 9 #define TCP_PACKET_FORWARD_REQUEST 10 #define TCP_PACKET_FORWARDING 11 +#endif // test network #define TCP_HANDSHAKE_PLAIN_SIZE (CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE) #define TCP_SERVER_HANDSHAKE_SIZE (CRYPTO_NONCE_SIZE + TCP_HANDSHAKE_PLAIN_SIZE + CRYPTO_MAC_SIZE) diff --git a/protocols/Tox/libtox/src/toxcore/TCP_connection.c b/protocols/Tox/libtox/src/toxcore/TCP_connection.c index c67df1b2eb..4c71f366b3 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_connection.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_connection.c @@ -277,7 +277,6 @@ static TCP_con *get_tcp_connection(const TCP_Connections *tcp_c, int tcp_connect return &tcp_c->tcp_connections[tcp_connections_number]; } -/** Returns the number of connected TCP relays */ uint32_t tcp_connected_relays_count(const TCP_Connections *tcp_c) { uint32_t count = 0; @@ -635,6 +634,11 @@ static int find_tcp_connection_relay(const TCP_Connections *tcp_c, const uint8_t return -1; } +bool tcp_relay_is_valid(const TCP_Connections *tcp_c, const uint8_t *relay_pk) +{ + return find_tcp_connection_relay(tcp_c, relay_pk) != -1; +} + /** @brief Create a new TCP connection to public_key. * * public_key must be the counterpart to the secret key that the other peer used with `new_tcp_connections()`. diff --git a/protocols/Tox/libtox/src/toxcore/TCP_connection.h b/protocols/Tox/libtox/src/toxcore/TCP_connection.h index 65ead4d87f..0434a73eb0 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_connection.h +++ b/protocols/Tox/libtox/src/toxcore/TCP_connection.h @@ -78,10 +78,14 @@ const uint8_t *tcp_connections_public_key(const TCP_Connections *tcp_c); non_null() uint32_t tcp_connections_count(const TCP_Connections *tcp_c); -/** Returns the number of connected TCP relays */ +/** @brief Returns the number of connected TCP relays. */ non_null() uint32_t tcp_connected_relays_count(const TCP_Connections *tcp_c); +/** @brief Returns true if we know of a valid TCP relay with the passed public key. */ +non_null() +bool tcp_relay_is_valid(const TCP_Connections *tcp_c, const uint8_t *relay_pk); + /** @brief Send a packet to the TCP connection. * * return -1 on failure. diff --git a/protocols/Tox/libtox/src/toxcore/TCP_server.c b/protocols/Tox/libtox/src/toxcore/TCP_server.c index f571a312d2..ecd557d1b2 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_server.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_server.c @@ -166,7 +166,7 @@ static void free_accepted_connection_array(TCP_Server *tcp_server) tcp_server->size_accepted_connections = 0; } -/** +/** * @return index corresponding to connection with peer on success * @retval -1 on failure. */ @@ -388,7 +388,7 @@ non_null() static int send_routing_response(const Logger *logger, TCP_Secure_Connection *con, uint8_t rpid, const uint8_t *public_key) { - uint8_t data[1 + 1 + CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t data[2 + CRYPTO_PUBLIC_KEY_SIZE]; data[0] = TCP_PACKET_ROUTING_RESPONSE; data[1] = rpid; memcpy(data + 2, public_key, CRYPTO_PUBLIC_KEY_SIZE); @@ -1033,12 +1033,15 @@ TCP_Server *new_TCP_server(const Logger *logger, const Random *rng, const Networ non_null() static void do_TCP_accept_new(TCP_Server *tcp_server) { - for (uint32_t i = 0; i < tcp_server->num_listening_socks; ++i) { - Socket sock; + for (uint32_t sock_idx = 0; sock_idx < tcp_server->num_listening_socks; ++sock_idx) { + + for (uint32_t connection_idx = 0; connection_idx < MAX_INCOMING_CONNECTIONS; ++connection_idx) { + const Socket sock = net_accept(tcp_server->ns, tcp_server->socks_listening[sock_idx]); - do { - sock = net_accept(tcp_server->ns, tcp_server->socks_listening[i]); - } while (accept_connection(tcp_server, sock) != -1); + if (accept_connection(tcp_server, sock) == -1) { + break; + } + } } } #endif diff --git a/protocols/Tox/libtox/src/toxcore/announce.c b/protocols/Tox/libtox/src/toxcore/announce.c index 7914d19615..647bd21ea3 100644 --- a/protocols/Tox/libtox/src/toxcore/announce.c +++ b/protocols/Tox/libtox/src/toxcore/announce.c @@ -14,9 +14,14 @@ #include "LAN_discovery.h" #include "ccompat.h" +#include "shared_key_cache.h" #include "timed_auth.h" #include "util.h" +// Settings for the shared key cache +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 + uint8_t announce_response_of_request_type(uint8_t request_type) { switch (request_type) { @@ -53,7 +58,7 @@ struct Announcements { const uint8_t *public_key; const uint8_t *secret_key; - Shared_Keys shared_keys; + Shared_Key_Cache *shared_keys; uint8_t hmac_key[CRYPTO_HMAC_KEY_SIZE]; int32_t synch_offset; @@ -429,10 +434,13 @@ static int create_reply_plain_store_announce_request(Announcements *announce, } VLA(uint8_t, plain, plain_len); - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - get_shared_key(announce->mono_time, &announce->shared_keys, shared_key, - announce->secret_key, data_public_key); + const uint8_t* shared_key = shared_key_cache_lookup(announce->shared_keys, data_public_key); + + if (shared_key == nullptr) { + /* Error looking up/deriving the shared key */ + return -1; + } if (decrypt_data_symmetric(shared_key, data + CRYPTO_PUBLIC_KEY_SIZE, @@ -549,9 +557,7 @@ static int create_reply(Announcements *announce, const IP_Port *source, } VLA(uint8_t, plain, plain_len); - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - - dht_get_shared_key_recv(announce->dht, shared_key, data + 1); + const uint8_t *shared_key = dht_get_shared_key_recv(announce->dht, data + 1); if (decrypt_data_symmetric(shared_key, data + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -652,6 +658,11 @@ Announcements *new_announcements(const Logger *log, const Random *rng, const Mon announce->public_key = dht_get_self_public_key(announce->dht); announce->secret_key = dht_get_self_secret_key(announce->dht); new_hmac_key(announce->rng, announce->hmac_key); + announce->shared_keys = shared_key_cache_new(mono_time, announce->secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + if (announce->shared_keys == nullptr) { + free(announce); + return nullptr; + } announce->start_time = mono_time_get(announce->mono_time); @@ -677,7 +688,7 @@ void kill_announcements(Announcements *announce) networking_registerhandler(announce->net, NET_PACKET_STORE_ANNOUNCE_REQUEST, nullptr, nullptr); crypto_memzero(announce->hmac_key, CRYPTO_HMAC_KEY_SIZE); - crypto_memzero(&announce->shared_keys, sizeof(Shared_Keys)); + shared_key_cache_free(announce->shared_keys); for (uint32_t i = 0; i < ANNOUNCE_BUCKETS * ANNOUNCE_BUCKET_SIZE; ++i) { if (announce->entries[i].data != nullptr) { diff --git a/protocols/Tox/libtox/src/toxcore/bin_pack.c b/protocols/Tox/libtox/src/toxcore/bin_pack.c index ca8940ee44..3575803aed 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_pack.c +++ b/protocols/Tox/libtox/src/toxcore/bin_pack.c @@ -127,6 +127,11 @@ bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length) return cmp_write_bin(&bp->ctx, data, length); } +bool bin_pack_nil(Bin_Pack *bp) +{ + return cmp_write_nil(&bp->ctx); +} + bool bin_pack_bin_marker(Bin_Pack *bp, uint32_t size) { return cmp_write_bin_marker(&bp->ctx, size); @@ -159,3 +164,4 @@ bool bin_pack_bin_b(Bin_Pack *bp, const uint8_t *data, uint32_t length) { return bp->ctx.write(&bp->ctx, data, length) == length; } + diff --git a/protocols/Tox/libtox/src/toxcore/bin_pack.h b/protocols/Tox/libtox/src/toxcore/bin_pack.h index 542f533b88..51646c088a 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_pack.h +++ b/protocols/Tox/libtox/src/toxcore/bin_pack.h @@ -90,9 +90,10 @@ non_null() bool bin_pack_u16(Bin_Pack *bp, uint16_t val); non_null() bool bin_pack_u32(Bin_Pack *bp, uint32_t val); /** @brief Pack a `uint64_t` as MessagePack positive integer. */ non_null() bool bin_pack_u64(Bin_Pack *bp, uint64_t val); +/** @brief Pack an empty array member as a MessagePack nil value. */ +non_null() bool bin_pack_nil(Bin_Pack *bp); /** @brief Pack a byte array as MessagePack bin. */ non_null() bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length); - /** @brief Start packing a custom binary representation. * * A call to this function must be followed by exactly `size` bytes packed by functions below. diff --git a/protocols/Tox/libtox/src/toxcore/bin_unpack.c b/protocols/Tox/libtox/src/toxcore/bin_unpack.c index e4daec3a22..ff591ca87a 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_unpack.c +++ b/protocols/Tox/libtox/src/toxcore/bin_unpack.c @@ -104,6 +104,11 @@ bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val) return cmp_read_ulong(&bu->ctx, val); } +bool bin_unpack_nil(Bin_Unpack *bu) +{ + return cmp_read_nil(&bu->ctx); +} + bool bin_unpack_bin(Bin_Unpack *bu, uint8_t **data_ptr, uint32_t *data_length_ptr) { uint32_t bin_size; diff --git a/protocols/Tox/libtox/src/toxcore/bin_unpack.h b/protocols/Tox/libtox/src/toxcore/bin_unpack.h index c6ead96e1b..bd4d8785c1 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_unpack.h +++ b/protocols/Tox/libtox/src/toxcore/bin_unpack.h @@ -60,6 +60,9 @@ non_null() bool bin_unpack_u16(Bin_Unpack *bu, uint16_t *val); non_null() bool bin_unpack_u32(Bin_Unpack *bu, uint32_t *val); /** @brief Unpack a MessagePack positive int into a `uint64_t`. */ non_null() bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val); +/** @brief Unpack a Messagepack nil value. */ +non_null() bool bin_unpack_nil(Bin_Unpack *bu); + /** @brief Unpack a MessagePack bin into a newly allocated byte array. * * Allocates a new byte array and stores it into `data_ptr` with its length stored in diff --git a/protocols/Tox/libtox/src/toxcore/crypto_core.c b/protocols/Tox/libtox/src/toxcore/crypto_core.c index 1ec2cee5fd..14025252c4 100644 --- a/protocols/Tox/libtox/src/toxcore/crypto_core.c +++ b/protocols/Tox/libtox/src/toxcore/crypto_core.c @@ -176,7 +176,7 @@ bool crypto_memunlock(void *data, size_t length) #endif } -bool pk_equal(const uint8_t *pk1, const uint8_t *pk2) +bool pk_equal(const uint8_t pk1[CRYPTO_PUBLIC_KEY_SIZE], const uint8_t pk2[CRYPTO_PUBLIC_KEY_SIZE]) { #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION // Hope that this is better for the fuzzer @@ -186,7 +186,7 @@ bool pk_equal(const uint8_t *pk1, const uint8_t *pk2) #endif } -void pk_copy(uint8_t *dest, const uint8_t *src) +void pk_copy(uint8_t dest[CRYPTO_PUBLIC_KEY_SIZE], const uint8_t src[CRYPTO_PUBLIC_KEY_SIZE]) { memcpy(dest, src, CRYPTO_PUBLIC_KEY_SIZE); } @@ -472,7 +472,7 @@ int32_t crypto_new_keypair(const Random *rng, uint8_t *public_key, uint8_t *secr #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION random_bytes(rng, secret_key, CRYPTO_SECRET_KEY_SIZE); memset(public_key, 0, CRYPTO_PUBLIC_KEY_SIZE); // Make MSAN happy - crypto_scalarmult_curve25519_base(public_key, secret_key); + crypto_derive_public_key(public_key, secret_key); return 0; #else return crypto_box_keypair(public_key, secret_key); @@ -484,7 +484,7 @@ void crypto_derive_public_key(uint8_t *public_key, const uint8_t *secret_key) crypto_scalarmult_curve25519_base(public_key, secret_key); } -void new_hmac_key(const Random *rng, uint8_t *key) +void new_hmac_key(const Random *rng, uint8_t key[CRYPTO_HMAC_KEY_SIZE]) { random_bytes(rng, key, CRYPTO_HMAC_KEY_SIZE); } @@ -492,13 +492,22 @@ void new_hmac_key(const Random *rng, uint8_t *key) void crypto_hmac(uint8_t auth[CRYPTO_HMAC_SIZE], const uint8_t key[CRYPTO_HMAC_KEY_SIZE], const uint8_t *data, size_t length) { +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + memcpy(auth, key, 16); + memcpy(auth + 16, data, length < 16 ? length : 16); +#else crypto_auth(auth, data, length, key); +#endif } bool crypto_hmac_verify(const uint8_t auth[CRYPTO_HMAC_SIZE], const uint8_t key[CRYPTO_HMAC_KEY_SIZE], const uint8_t *data, size_t length) { +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + return memcmp(auth, key, 16) == 0 && memcmp(auth + 16, data, length < 16 ? length : 16) == 0; +#else return crypto_auth_verify(auth, data, length, key) == 0; +#endif } void crypto_sha256(uint8_t *hash, const uint8_t *data, size_t length) diff --git a/protocols/Tox/libtox/src/toxcore/crypto_core.h b/protocols/Tox/libtox/src/toxcore/crypto_core.h index 0aaadeacf1..3b9f27d677 100644 --- a/protocols/Tox/libtox/src/toxcore/crypto_core.h +++ b/protocols/Tox/libtox/src/toxcore/crypto_core.h @@ -358,7 +358,7 @@ int32_t decrypt_data(const uint8_t *public_key, const uint8_t *secret_key, const /** * @brief Fast encrypt/decrypt operations. * - * Use if this is not a one-time communication. @ref encrypt_precompute does the + * Use if this is not a one-time communication. `encrypt_precompute` does the * shared-key generation once so it does not have to be performed on every * encrypt/decrypt. */ diff --git a/protocols/Tox/libtox/src/toxcore/friend_connection.c b/protocols/Tox/libtox/src/toxcore/friend_connection.c index 9e5dee7cbd..7b8537c720 100644 --- a/protocols/Tox/libtox/src/toxcore/friend_connection.c +++ b/protocols/Tox/libtox/src/toxcore/friend_connection.c @@ -31,7 +31,7 @@ struct Friend_Conn { uint8_t real_public_key[CRYPTO_PUBLIC_KEY_SIZE]; uint8_t dht_temp_pk[CRYPTO_PUBLIC_KEY_SIZE]; - uint16_t dht_lock; + uint32_t dht_lock_token; IP_Port dht_ip_port; uint64_t dht_pk_lastrecv; uint64_t dht_ip_port_lastrecv; @@ -197,7 +197,7 @@ Friend_Conn *get_conn(const Friend_Connections *fr_c, int friendcon_id) return &fr_c->conns[friendcon_id]; } -/** +/** * @return friendcon_id corresponding to the real public key on success. * @retval -1 on failure. */ @@ -377,16 +377,15 @@ static void change_dht_pk(Friend_Connections *fr_c, int friendcon_id, const uint friend_con->dht_pk_lastrecv = mono_time_get(fr_c->mono_time); - if (friend_con->dht_lock > 0) { - if (dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock) != 0) { + if (friend_con->dht_lock_token > 0) { + if (dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token) != 0) { LOGGER_ERROR(fr_c->logger, "a. Could not delete dht peer. Please report this."); return; } - - friend_con->dht_lock = 0; + friend_con->dht_lock_token = 0; } - dht_addfriend(fr_c->dht, dht_public_key, dht_ip_callback, fr_c, friendcon_id, &friend_con->dht_lock); + dht_addfriend(fr_c->dht, dht_public_key, dht_ip_callback, fr_c, friendcon_id, &friend_con->dht_lock_token); memcpy(friend_con->dht_temp_pk, dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); } @@ -609,7 +608,7 @@ static int friend_new_connection(Friend_Connections *fr_c, int friendcon_id) } /* If dht_temp_pk does not contains a pk. */ - if (friend_con->dht_lock == 0) { + if (friend_con->dht_lock_token == 0) { return -1; } @@ -838,8 +837,9 @@ int kill_friend_connection(Friend_Connections *fr_c, int friendcon_id) onion_delfriend(fr_c->onion_c, friend_con->onion_friendnum); crypto_kill(fr_c->net_crypto, friend_con->crypt_connection_id); - if (friend_con->dht_lock > 0) { - dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock); + if (friend_con->dht_lock_token > 0) { + dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token); + friend_con->dht_lock_token = 0; } return wipe_friend_conn(fr_c, friendcon_id); @@ -882,7 +882,8 @@ int send_friend_request_packet(Friend_Connections *fr_c, int friendcon_id, uint3 if (friend_con->status == FRIENDCONN_STATUS_CONNECTED) { packet[0] = PACKET_ID_FRIEND_REQUESTS; - return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), false) != -1 ? 1 : 0; + return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), + false) != -1 ? 1 : 0; } packet[0] = CRYPTO_PACKET_FRIEND_REQ; @@ -967,9 +968,9 @@ void do_friend_connections(Friend_Connections *fr_c, void *userdata) if (friend_con != nullptr) { if (friend_con->status == FRIENDCONN_STATUS_CONNECTING) { if (friend_con->dht_pk_lastrecv + FRIEND_DHT_TIMEOUT < temp_time) { - if (friend_con->dht_lock > 0) { - dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock); - friend_con->dht_lock = 0; + if (friend_con->dht_lock_token > 0) { + dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token); + friend_con->dht_lock_token = 0; memset(friend_con->dht_temp_pk, 0, CRYPTO_PUBLIC_KEY_SIZE); } } @@ -978,7 +979,7 @@ void do_friend_connections(Friend_Connections *fr_c, void *userdata) friend_con->dht_ip_port.ip.family = net_family_unspec(); } - if (friend_con->dht_lock > 0) { + if (friend_con->dht_lock_token > 0) { if (friend_new_connection(fr_c, i) == 0) { set_direct_ip_port(fr_c->net_crypto, friend_con->crypt_connection_id, &friend_con->dht_ip_port, false); connect_to_saved_tcp_relays(fr_c, i, MAX_FRIEND_TCP_CONNECTIONS / 2); /* Only fill it half up. */ diff --git a/protocols/Tox/libtox/src/toxcore/group_announce.c b/protocols/Tox/libtox/src/toxcore/group_announce.c index 83dd3acf54..896b043dbd 100644 --- a/protocols/Tox/libtox/src/toxcore/group_announce.c +++ b/protocols/Tox/libtox/src/toxcore/group_announce.c @@ -186,7 +186,7 @@ static int gca_unpack_announce(const Logger *log, const uint8_t *data, uint16_t memcpy(announce->peer_public_key, data + offset, ENC_PUBLIC_KEY_SIZE); offset += ENC_PUBLIC_KEY_SIZE; - announce->ip_port_is_set = data[offset] == 1; + net_unpack_bool(&data[offset], &announce->ip_port_is_set); ++offset; announce->tcp_relays_count = data[offset]; diff --git a/protocols/Tox/libtox/src/toxcore/group_announce.h b/protocols/Tox/libtox/src/toxcore/group_announce.h index 11bba7d530..801363d6b2 100644 --- a/protocols/Tox/libtox/src/toxcore/group_announce.h +++ b/protocols/Tox/libtox/src/toxcore/group_announce.h @@ -20,7 +20,7 @@ extern "C" { /* The maximum number of announces to save for a particular group chat. */ #define GCA_MAX_SAVED_ANNOUNCES_PER_GC 16 -/* Maximum number of TCP relays that can be in an annoucne. */ +/* Maximum number of TCP relays that can be in an announce. */ #define GCA_MAX_ANNOUNCED_TCP_RELAYS 1 /* Maximum number of announces we can send in an announce response. */ diff --git a/protocols/Tox/libtox/src/toxcore/group_chats.c b/protocols/Tox/libtox/src/toxcore/group_chats.c new file mode 100644 index 0000000000..f9cb2d18fb --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_chats.c @@ -0,0 +1,8410 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_chats.h" + +#include <assert.h> + +#ifndef VANILLA_NACL +#include <sodium.h> +#endif + +#include <string.h> + +#include "DHT.h" +#include "LAN_discovery.h" +#include "Messenger.h" +#include "TCP_connection.h" +#include "ccompat.h" +#include "friend_connection.h" +#include "group_common.h" +#include "group_moderation.h" +#include "group_pack.h" +#include "mono_time.h" +#include "network.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/* The minimum size of a plaintext group handshake packet */ +#define GC_MIN_HS_PACKET_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) + +/* The minimum size of an encrypted group handshake packet. */ +#define GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE +\ + GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) + +/* Size of a group's shared state in packed format */ +#define GC_PACKED_SHARED_STATE_SIZE (EXT_PUBLIC_KEY_SIZE + sizeof(uint16_t) + MAX_GC_GROUP_NAME_SIZE +\ + sizeof(uint16_t) + 1 + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE +\ + MOD_MODERATION_HASH_SIZE + sizeof(uint32_t) + sizeof(uint32_t) + 1) + +/* Minimum size of a topic packet; includes topic length, public signature key, topic version and checksum */ +#define GC_MIN_PACKED_TOPIC_INFO_SIZE (sizeof(uint16_t) + SIG_PUBLIC_KEY_SIZE + sizeof(uint32_t) + sizeof(uint16_t)) + +#define GC_SHARED_STATE_ENC_PACKET_SIZE (SIGNATURE_SIZE + GC_PACKED_SHARED_STATE_SIZE) + +/* Header information attached to all broadcast messages: broadcast_type */ +#define GC_BROADCAST_ENC_HEADER_SIZE 1 + +/* Size of a group packet message ID */ +#define GC_MESSAGE_ID_BYTES sizeof(uint64_t) + +/* Size of a lossless ack packet */ +#define GC_LOSSLESS_ACK_PACKET_SIZE (GC_MESSAGE_ID_BYTES + 1) + +/* Smallest possible size of an encrypted lossless payload. + * + * Data includes the message_id, group packet type, and the nonce and MAC for decryption. + */ +#define GC_MIN_LOSSLESS_PAYLOAD_SIZE (GC_MESSAGE_ID_BYTES + CRYPTO_NONCE_SIZE + 1 + CRYPTO_MAC_SIZE) + +/* Smallest possible size of a lossy group packet */ +#define GC_MIN_LOSSY_PAYLOAD_SIZE (GC_MIN_LOSSLESS_PAYLOAD_SIZE - GC_MESSAGE_ID_BYTES) + +/* Maximum number of bytes to pad packets with. + * + * Packets are padded with a random number of zero bytes between zero and this value in order to hide + * the true length of the message, which reduces the amount of metadata leaked through packet analysis. + * + * Note: This behaviour was copied from the toxcore encryption implementation in net_crypto.c. + */ +#define GC_MAX_PACKET_PADDING 8 + +/* Minimum size of a ping packet, which contains the peer count, peer list checksum, shared state version, + * sanctions list version, sanctions list checksum, topic version, and topic checksum + */ +#define GC_PING_PACKET_MIN_DATA_SIZE ((sizeof(uint16_t) * 4) + (sizeof(uint32_t) * 3)) + +/* How often in seconds we can send a group sync request packet */ +#define GC_SYNC_REQUEST_LIMIT (GC_PING_TIMEOUT + 1) + +/* How often in seconds we can send the peer list to any peer in the group in a sync response */ +#define GC_SYNC_RESPONSE_PEER_LIST_LIMIT 3 + +/* How often in seconds we try to handshake with an unconfirmed peer */ +#define GC_SEND_HANDSHAKE_INTERVAL 3 + +/* How often in seconds we rotate session encryption keys with a peer */ +#define GC_KEY_ROTATION_TIMEOUT (5 * 60) + +/* How often in seconds we try to reconnect to peers that recently timed out */ +#define GC_TIMED_OUT_RECONN_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT * 3) + +/* How long in seconds before we stop trying to reconnect with a timed out peer */ +#define GC_TIMED_OUT_STALE_TIMEOUT (60 * 15) + +/* The value the topic lock is set to when the topic lock is enabled. */ +#define GC_TOPIC_LOCK_ENABLED 0 + +static_assert(GCC_BUFFER_SIZE <= UINT16_MAX, + "GCC_BUFFER_SIZE must be <= UINT16_MAX)"); + +static_assert(MAX_GC_PACKET_CHUNK_SIZE < MAX_GC_PACKET_SIZE, + "MAX_GC_PACKET_CHUNK_SIZE must be < MAX_GC_PACKET_SIZE"); + +// size of a lossless handshake packet - lossless packets can't/shouldn't be split up +static_assert(MAX_GC_PACKET_CHUNK_SIZE >= 171, + "MAX_GC_PACKET_CHUNK_SIZE must be >= 171"); + +// group_moderation constants assume this is the max packet size. +static_assert(MAX_GC_PACKET_SIZE >= 50000, + "MAX_GC_PACKET_SIZE doesn't match constants in group_moderation.h"); + +static_assert(MAX_GC_PACKET_SIZE <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE, + "MAX_GC_PACKET_SIZE must be <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE"); + +/** Types of broadcast messages. */ +typedef enum Group_Message_Type { + GC_MESSAGE_TYPE_NORMAL = 0x00, + GC_MESSAGE_TYPE_ACTION = 0x01, +} Group_Message_Type; + +/** Types of handshake request packets. */ +typedef enum Group_Handshake_Packet_Type { + GH_REQUEST = 0x00, // Requests a handshake + GH_RESPONSE = 0x01, // Responds to a handshake request +} Group_Handshake_Packet_Type; + +/** Types of handshake requests (within a handshake request packet). */ +typedef enum Group_Handshake_Request_Type { + HS_INVITE_REQUEST = 0x00, // Requests an invite to the group + HS_PEER_INFO_EXCHANGE = 0x01, // Requests a peer info exchange +} Group_Handshake_Request_Type; + +/** These bitmasks determine what group state info a peer is requesting in a sync request */ +typedef enum Group_Sync_Flags { + GF_PEERS = (1 << 0), // 1 + GF_TOPIC = (1 << 1), // 2 + GF_STATE = (1 << 2), // 4 +} Group_Sync_Flags; + +non_null() static bool self_gc_is_founder(const GC_Chat *chat); +non_null() static bool group_number_valid(const GC_Session *c, int group_number); +non_null() static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number); +non_null() static void group_delete(GC_Session *c, GC_Chat *chat); +non_null() static void group_cleanup(GC_Session *c, GC_Chat *chat); +non_null() static bool group_exists(const GC_Session *c, const uint8_t *chat_id); +non_null() static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat); +non_null(1, 2) nullable(4) +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata); +non_null() static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, + uint8_t *secret_key); +non_null() static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs); +non_null() static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer); + +static const GC_Chat empty_gc_chat = {nullptr}; + +non_null() +static void kill_group_friend_connection(const GC_Session *c, const GC_Chat *chat) +{ + if (chat->friend_connection_id != -1) { + m_kill_group_connection(c->messenger, chat); + } +} + +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type) +{ + assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + + const uint16_t min_header_size = packet_type == NET_PACKET_GC_LOSSY + ? GC_MIN_LOSSY_PAYLOAD_SIZE + : GC_MIN_LOSSLESS_PAYLOAD_SIZE; + const uint16_t header_size = ENC_PUBLIC_KEY_SIZE + GC_MAX_PACKET_PADDING + min_header_size; + + assert(length <= UINT16_MAX - header_size); + + return length + header_size; +} + +/** Return true if `peer_number` is our own. */ +static bool peer_number_is_self(int peer_number) +{ + return peer_number == 0; +} + +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number) +{ + return peer_number >= 0 && peer_number < (int)chat->numpeers; +} + +non_null() +static GC_Peer *get_gc_peer(const GC_Chat *chat, int peer_number) +{ + if (!gc_peer_number_is_valid(chat, peer_number)) { + return nullptr; + } + + return &chat->group[peer_number]; +} + +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return nullptr; + } + + return &peer->gconn; +} + +/** Returns the amount of empty padding a packet of designated length should have. */ +static uint16_t group_packet_padding_length(uint16_t length) +{ + return (MAX_GC_PACKET_CHUNK_SIZE - length) % GC_MAX_PACKET_PADDING; +} + +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick) +{ + if (nick != nullptr) { + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + assert(peer->nick_length > 0); + + memcpy(nick, peer->nick, peer->nick_length); + } +} + +uint16_t gc_get_self_nick_size(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->nick_length; +} + +/** @brief Sets self nick to `nick`. + * + * Returns false if `nick` is null or `length` is greater than MAX_GC_NICK_SIZE. + */ +non_null() +static bool self_gc_set_nick(const GC_Chat *chat, const uint8_t *nick, uint16_t length) +{ + if (nick == nullptr || length > MAX_GC_NICK_SIZE) { + return false; + } + + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + memcpy(peer->nick, nick, length); + peer->nick_length = length; + + return true; +} + +Group_Role gc_get_self_role(const GC_Chat *chat) +{ + + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->role; +} + +/** Sets self role. If role is invalid this function has no effect. */ +non_null() +static void self_gc_set_role(const GC_Chat *chat, Group_Role role) +{ + if (role <= GR_OBSERVER) { + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + peer->role = role; + } +} + +uint8_t gc_get_self_status(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->status; +} + +/** Sets self status. If status is invalid this function has no effect. */ +non_null() +static void self_gc_set_status(const GC_Chat *chat, Group_Peer_Status status) +{ + if (status == GS_NONE || status == GS_AWAY || status == GS_BUSY) { + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + peer->status = status; + return; + } + + LOGGER_WARNING(chat->log, "Attempting to set user status with invalid status: %u", (uint8_t)status); +} + +uint32_t gc_get_self_peer_id(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->peer_id; +} + +/** Sets self confirmed status. */ +non_null() +static void self_gc_set_confirmed(const GC_Chat *chat, bool confirmed) +{ + GC_Connection *gconn = get_gc_connection(chat, 0); + assert(gconn != nullptr); + + gconn->confirmed = confirmed; +} + +/** Returns true if self has the founder role */ +non_null() +static bool self_gc_is_founder(const GC_Chat *chat) +{ + return gc_get_self_role(chat) == GR_FOUNDER; +} + +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key) +{ + if (public_key != nullptr) { + memcpy(public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + } +} + +/** @brief Sets self extended public key to `ext_public_key`. + * + * If `ext_public_key` is null this function has no effect. + */ +non_null() +static void self_gc_set_ext_public_key(const GC_Chat *chat, const uint8_t *ext_public_key) +{ + if (ext_public_key != nullptr) { + GC_Connection *gconn = get_gc_connection(chat, 0); + assert(gconn != nullptr); + memcpy(gconn->addr.public_key, ext_public_key, EXT_PUBLIC_KEY_SIZE); + } +} + +/** + * Return true if `peer` has permission to speak according to the `voice_state`. + */ +non_null() +static bool peer_has_voice(const GC_Peer *peer, Group_Voice_State voice_state) +{ + const Group_Role role = peer->role; + + switch (voice_state) { + case GV_ALL: + return role <= GR_USER; + + case GV_MODS: + return role <= GR_MODERATOR; + + case GV_FOUNDER: + return role == GR_FOUNDER; + + default: + return false; + } +} + +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed) +{ + uint16_t packed_len = 0; + uint16_t count = 0; + + for (uint32_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (!saved_peer_is_valid(saved_peer)) { + continue; + } + + int packed_ipp_len = 0; + int packed_tcp_len = 0; + + if (ipport_isset(&saved_peer->ip_port)) { + if (packed_len > length) { + return -1; + } + + packed_ipp_len = pack_ip_port(chat->log, data + packed_len, length - packed_len, &saved_peer->ip_port); + + if (packed_ipp_len > 0) { + packed_len += packed_ipp_len; + } + } + + if (ipport_isset(&saved_peer->tcp_relay.ip_port)) { + if (packed_len > length) { + return -1; + } + + packed_tcp_len = pack_nodes(chat->log, data + packed_len, length - packed_len, &saved_peer->tcp_relay, 1); + + if (packed_tcp_len > 0) { + packed_len += packed_tcp_len; + } + } + + if (packed_len + ENC_PUBLIC_KEY_SIZE > length) { + return -1; + } + + if (packed_tcp_len > 0 || packed_ipp_len > 0) { + memcpy(data + packed_len, chat->saved_peers[i].public_key, ENC_PUBLIC_KEY_SIZE); + packed_len += ENC_PUBLIC_KEY_SIZE; + ++count; + } else { + LOGGER_WARNING(chat->log, "Failed to pack saved peer"); + } + } + + if (processed != nullptr) { + *processed = packed_len; + } + + return count; +} + +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length) +{ + uint16_t count = 0; + uint16_t unpacked_len = 0; + + for (size_t i = 0; unpacked_len < length; ++i) { + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + const int ipp_len = unpack_ip_port(&saved_peer->ip_port, data + unpacked_len, length - unpacked_len, false); + + if (ipp_len > 0) { + unpacked_len += ipp_len; + } + + if (unpacked_len > length) { + return -1; + } + + uint16_t tcp_len_processed = 0; + const int tcp_len = unpack_nodes(&saved_peer->tcp_relay, 1, &tcp_len_processed, data + unpacked_len, + length - unpacked_len, true); + + if (tcp_len == 1 && tcp_len_processed > 0) { + unpacked_len += tcp_len_processed; + } else if (ipp_len <= 0) { + LOGGER_WARNING(chat->log, "Failed to unpack saved peer: Invalid connection info."); + return -1; + } + + if (unpacked_len + ENC_PUBLIC_KEY_SIZE > length) { + return -1; + } + + if (tcp_len > 0 || ipp_len > 0) { + memcpy(saved_peer->public_key, data + unpacked_len, ENC_PUBLIC_KEY_SIZE); + unpacked_len += ENC_PUBLIC_KEY_SIZE; + ++count; + } else { + LOGGER_ERROR(chat->log, "Unpacked peer with bad connection info"); + return -1; + } + } + + return count; +} + +/** Returns true if chat privacy state is set to public. */ +non_null() +static bool is_public_chat(const GC_Chat *chat) +{ + return chat->shared_state.privacy_state == GI_PUBLIC; +} + +/** Returns true if group is password protected */ +non_null() +static bool chat_is_password_protected(const GC_Chat *chat) +{ + return chat->shared_state.password_length > 0; +} + +/** Returns true if `password` matches the current group password. */ +non_null() +static bool validate_password(const GC_Chat *chat, const uint8_t *password, uint16_t length) +{ + if (length > MAX_GC_PASSWORD_SIZE) { + return false; + } + + if (length != chat->shared_state.password_length) { + return false; + } + + return memcmp(chat->shared_state.password, password, length) == 0; +} + +/** @brief Returns the chat object that contains a peer with a public key equal to `id`. + * + * `id` must be at least ENC_PUBLIC_KEY_SIZE bytes in length. + */ +non_null() +static GC_Chat *get_chat_by_id(const GC_Session *c, const uint8_t *id) +{ + if (c == nullptr) { + return nullptr; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(id, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return chat; + } + + if (get_peer_number_of_enc_pk(chat, id, false) != -1) { + return chat; + } + } + + return nullptr; +} + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key) +{ + return jenkins_one_at_a_time_hash(public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** @brief Sets the sum of the public_key_hash of all confirmed peers. + * + * Must be called every time a peer is confirmed or deleted. + */ +non_null() +static void set_gc_peerlist_checksum(GC_Chat *chat) +{ + uint16_t sum = 0; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + sum += gconn->public_key_hash; + } + } + + chat->peers_checksum = sum; +} + +/** Returns a checksum of the topic currently set in `topic_info`. */ +non_null() +static uint16_t get_gc_topic_checksum(const GC_TopicInfo *topic_info) +{ + return data_checksum(topic_info->topic, topic_info->length); +} + +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->pending_delete) { + continue; + } + + if (confirmed && !gconn->confirmed) { + continue; + } + + if (memcmp(gconn->addr.public_key, public_enc_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +/** @brief Check if peer associated with `public_sig_key` is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + */ +non_null() +static int get_peer_number_of_sig_pk(const GC_Chat *chat, const uint8_t *public_sig_key) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (memcmp(get_sig_pk(gconn->addr.public_key), public_sig_key, SIG_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +non_null() +static bool gc_get_enc_pk_from_sig_pk(const GC_Chat *chat, uint8_t *public_key, const uint8_t *public_sig_key) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + const uint8_t *full_pk = gconn->addr.public_key; + + if (memcmp(public_sig_key, get_sig_pk(full_pk), SIG_PUBLIC_KEY_SIZE) == 0) { + memcpy(public_key, get_enc_key(full_pk), ENC_PUBLIC_KEY_SIZE); + return true; + } + } + + return false; +} + +non_null() +static GC_Connection *random_gc_connection(const GC_Chat *chat) +{ + if (chat->numpeers <= 1) { + return nullptr; + } + + const uint32_t base = random_range_u32(chat->rng, chat->numpeers - 1); + + for (uint32_t i = 0; i < chat->numpeers - 1; ++i) { + const uint32_t index = 1 + (base + i) % (chat->numpeers - 1); + GC_Connection *rand_gconn = get_gc_connection(chat, index); + + if (rand_gconn == nullptr) { + return nullptr; + } + + if (!rand_gconn->pending_delete && rand_gconn->confirmed) { + return rand_gconn; + } + } + + return nullptr; +} + +/** @brief Returns the peer number associated with peer_id. + * Returns -1 if peer_id is invalid. + */ +non_null() +static int get_peer_number_of_peer_id(const GC_Chat *chat, uint32_t peer_id) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + if (chat->group[i].peer_id == peer_id) { + return i; + } + } + + return -1; +} + +/** @brief Returns a unique peer ID. + * Returns UINT32_MAX if all possible peer ID's are taken. + * + * These ID's are permanently assigned to a peer when they join the group and should be + * considered arbitrary values. + */ +non_null() +static uint32_t get_new_peer_id(const GC_Chat *chat) +{ + for (uint32_t i = 0; i < UINT32_MAX - 1; ++i) { + if (get_peer_number_of_peer_id(chat, i) == -1) { + return i; + } + } + + return UINT32_MAX; +} + +/** @brief Sets the password for the group (locally only). + * + * Return true on success. + */ +non_null(1) nullable(2) +static bool set_gc_password_local(GC_Chat *chat, const uint8_t *passwd, uint16_t length) +{ + if (length > MAX_GC_PASSWORD_SIZE) { + return false; + } + + if (passwd == nullptr || length == 0) { + chat->shared_state.password_length = 0; + memset(chat->shared_state.password, 0, MAX_GC_PASSWORD_SIZE); + } else { + chat->shared_state.password_length = length; + crypto_memlock(chat->shared_state.password, sizeof(chat->shared_state.password)); + memcpy(chat->shared_state.password, passwd, length); + } + + return true; +} + +/** @brief Sets the local shared state to `version`. + * + * This should always be called instead of setting the variables manually. + */ +non_null() +static void set_gc_shared_state_version(GC_Chat *chat, uint32_t version) +{ + chat->shared_state.version = version; + chat->moderation.shared_state_version = version; +} + +/** @brief Expands the chat_id into the extended chat public key (encryption key + signature key). + * + * @param dest must have room for EXT_PUBLIC_KEY_SIZE bytes. + * + * Return true on success. + */ +non_null() +static bool expand_chat_id(uint8_t *dest, const uint8_t *chat_id) +{ + assert(dest != nullptr); + + const int ret = crypto_sign_ed25519_pk_to_curve25519(dest, chat_id); + memcpy(dest + ENC_PUBLIC_KEY_SIZE, chat_id, SIG_PUBLIC_KEY_SIZE); + + return ret != -1; +} + +/** Copies peer connect info from `gconn` to `addr`. */ +non_null() +static void copy_gc_saved_peer(const Random *rng, const GC_Connection *gconn, GC_SavedPeerInfo *addr) +{ + if (!gcc_copy_tcp_relay(rng, &addr->tcp_relay, gconn)) { + addr->tcp_relay = (Node_format) { + 0 + }; + } + + addr->ip_port = gconn->addr.ip_port; + memcpy(addr->public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** Return true if `saved_peer` has either a valid IP_Port or a valid TCP relay. */ +non_null() +static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer) +{ + return ipport_isset(&saved_peer->ip_port) || ipport_isset(&saved_peer->tcp_relay.ip_port); +} + +/** @brief Returns the index of the saved peers entry for `public_key`. + * Returns -1 if key is not found. + */ +non_null() +static int saved_peer_index(const GC_Chat *chat, const uint8_t *public_key) +{ + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (memcmp(saved_peer->public_key, public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +/** @brief Returns the index of the first vacant entry in saved peers list. + * + * If `public_key` is non-null and already exists in the list, its index will be returned. + * + * A vacant entry is an entry that does not have either an IP_port or tcp relay set (invalid), + * or an entry containing info on a peer that is not presently online (offline). + * + * Invalid entries are given priority over offline entries. + * + * Returns -1 if there are no vacant indices. + */ +non_null(1) nullable(2) +static int saved_peers_get_new_index(const GC_Chat *chat, const uint8_t *public_key) +{ + if (public_key != nullptr) { + const int idx = saved_peer_index(chat, public_key); + + if (idx != -1) { + return idx; + } + } + + // first check for invalid spots + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (!saved_peer_is_valid(saved_peer)) { + return i; + } + } + + // now look for entries with offline peers + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + const int peernumber = get_peer_number_of_enc_pk(chat, saved_peer->public_key, true); + + if (peernumber < 0) { + return i; + } + } + + return -1; +} + +/** @brief Attempts to add `gconn` to the saved peer list. + * + * If an entry already exists it will be updated. + * + * Older peers will only be overwritten if the peer is no longer + * present in the chat. This gives priority to more stable connections. + * + * This function should be called every time a new peer joins the group. + */ +non_null() +static void add_gc_saved_peers(GC_Chat *chat, const GC_Connection *gconn) +{ + const int idx = saved_peers_get_new_index(chat, gconn->addr.public_key); + + if (idx == -1) { + return; + } + + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; + copy_gc_saved_peer(chat->rng, gconn, saved_peer); +} + +/** @brief Finds the first vacant spot in the saved peers list and fills it with a present + * peer who isn't already in the list. + * + * This function should be called after a confirmed peer exits the group. + */ +non_null() +static void refresh_gc_saved_peers(GC_Chat *chat) +{ + const int idx = saved_peers_get_new_index(chat, nullptr); + + if (idx == -1) { + return; + } + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + if (gconn == nullptr) { + continue; + } + + if (!gconn->confirmed) { + continue; + } + + if (saved_peer_index(chat, gconn->addr.public_key) == -1) { + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; + copy_gc_saved_peer(chat->rng, gconn, saved_peer); + return; + } + } +} + +/** Returns the number of confirmed peers in peerlist. */ +non_null() +static uint16_t get_gc_confirmed_numpeers(const GC_Chat *chat) +{ + uint16_t count = 0; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + ++count; + } + } + + return count; +} + +non_null() static bool sign_gc_shared_state(GC_Chat *chat); +non_null() static bool broadcast_gc_mod_list(const GC_Chat *chat); +non_null() static bool broadcast_gc_shared_state(const GC_Chat *chat); +non_null() static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, + const uint8_t *sanction_data, uint16_t length, bool add_obs); + +/** Returns true if peer designated by `peer_number` is in the sanctions list as an observer. */ +non_null() +static bool peer_is_observer(const GC_Chat *chat, uint32_t peer_number) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + return sanctions_list_is_observer(&chat->moderation, get_enc_key(gconn->addr.public_key)); +} + +/** Returns true if peer designated by `peer_number` is the group founder. */ +non_null() +static bool peer_is_founder(const GC_Chat *chat, uint32_t peer_number) +{ + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + return memcmp(chat->shared_state.founder_public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE) == 0; +} + +/** Returns true if peer designated by `peer_number` is in the moderator list or is the founder. */ +non_null() +static bool peer_is_moderator(const GC_Chat *chat, uint32_t peer_number) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (peer_is_founder(chat, peer_number)) { + return false; + } + + return mod_list_verify_sig_pk(&chat->moderation, get_sig_pk(gconn->addr.public_key)); +} + +/** @brief Iterates through the peerlist and updates group roles according to the + * current group state. + * + * Also updates the roles checksum. If any role conflicts exist the shared state + * version is set to zero in order to force a sync update. + * + * This should be called every time the moderator list or sanctions list changes, + * and after a new peer is marked as confirmed. + */ +non_null() +static void update_gc_peer_roles(GC_Chat *chat) +{ + chat->roles_checksum = 0; + bool conflicts = false; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + if (gconn == nullptr) { + continue; + } + + if (!gconn->confirmed) { + continue; + } + + const uint8_t first_byte = gconn->addr.public_key[0]; + const bool is_founder = peer_is_founder(chat, i); + + if (is_founder) { + chat->group[i].role = GR_FOUNDER; + chat->roles_checksum += GR_FOUNDER + first_byte; + continue; + } + + const bool is_observer = peer_is_observer(chat, i); + const bool is_moderator = peer_is_moderator(chat, i); + const bool is_user = !(is_founder || is_moderator || is_observer); + + if (is_observer && is_moderator) { + conflicts = true; + } + + if (is_user) { + chat->group[i].role = GR_USER; + chat->roles_checksum += GR_USER + first_byte; + continue; + } + + if (is_moderator) { + chat->group[i].role = GR_MODERATOR; + chat->roles_checksum += GR_MODERATOR + first_byte; + continue; + } + + if (is_observer) { + chat->group[i].role = GR_OBSERVER; + chat->roles_checksum += GR_OBSERVER + first_byte; + continue; + } + } + + if (conflicts && !self_gc_is_founder(chat)) { + set_gc_shared_state_version(chat, 0); // need a new shared state + } +} + +/** @brief Removes the first found offline mod from the mod list. + * + * Broadcasts the shared state and moderator list on success, as well as the updated + * sanctions list if necessary. + * + * TODO(Jfreegman): Make this smarter in who to remove (e.g. the mod who hasn't been seen online in the longest time) + * + * Returns false on failure. + */ +non_null() +static bool prune_gc_mod_list(GC_Chat *chat) +{ + if (chat->moderation.num_mods == 0) { + return true; + } + + uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; + bool pruned_mod = false; + + for (uint16_t i = 0; i < chat->moderation.num_mods; ++i) { + if (get_peer_number_of_sig_pk(chat, chat->moderation.mod_list[i]) == -1) { + memcpy(public_sig_key, chat->moderation.mod_list[i], SIG_PUBLIC_KEY_SIZE); + + if (!mod_list_remove_index(&chat->moderation, i)) { + continue; + } + + pruned_mod = true; + break; + } + } + + return pruned_mod + && mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash) + && sign_gc_shared_state(chat) + && broadcast_gc_shared_state(chat) + && broadcast_gc_mod_list(chat) + && update_gc_sanctions_list(chat, public_sig_key) + && update_gc_topic(chat, public_sig_key); +} + +/** @brief Removes the first found offline sanctioned peer from the sanctions list and sends the + * event to the rest of the group. + * + * @retval false on failure or if no presently sanctioned peer is offline. + */ +non_null() +static bool prune_gc_sanctions_list(GC_Chat *chat) +{ + if (chat->moderation.num_sanctions == 0) { + return true; + } + + const Mod_Sanction *sanction = nullptr; + uint8_t target_ext_pk[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + + for (uint16_t i = 0; i < chat->moderation.num_sanctions; ++i) { + const int peer_number = get_peer_number_of_enc_pk(chat, chat->moderation.sanctions[i].target_public_enc_key, true); + + if (peer_number == -1) { + sanction = &chat->moderation.sanctions[i]; + memcpy(target_ext_pk, sanction->target_public_enc_key, ENC_PUBLIC_KEY_SIZE); + memcpy(target_ext_pk + ENC_PUBLIC_KEY_SIZE, sanction->setter_public_sig_key, SIG_PUBLIC_KEY_SIZE); + break; + } + } + + if (sanction == nullptr) { + return false; + } + + if (!sanctions_list_remove_observer(&chat->moderation, sanction->target_public_enc_key, nullptr)) { + LOGGER_WARNING(chat->log, "Failed to remove entry from observer list"); + return false; + } + + sanction = nullptr; + + uint8_t data[MOD_SANCTIONS_CREDS_SIZE]; + const uint16_t length = sanctions_creds_pack(&chat->moderation.sanctions_creds, data); + + if (length != MOD_SANCTIONS_CREDS_SIZE) { + LOGGER_ERROR(chat->log, "Failed to pack credentials (invalid length: %u)", length); + return false; + } + + if (!send_gc_set_observer(chat, target_ext_pk, data, length, false)) { + LOGGER_WARNING(chat->log, "Failed to broadcast set observer"); + return false; + } + + return true; +} + +/** @brief Size of peer data that we pack for transfer (nick length must be accounted for separately). + * packed data consists of: nick length, nick, and status. + */ +#define PACKED_GC_PEER_SIZE (sizeof(uint16_t) + MAX_GC_NICK_SIZE + sizeof(uint8_t)) + +/** @brief Packs peer info into data of maxlength length. + * + * Return length of packed peer on success. + * Return -1 on failure. + */ +non_null() +static int pack_gc_peer(uint8_t *data, uint16_t length, const GC_Peer *peer) +{ + if (PACKED_GC_PEER_SIZE > length) { + return -1; + } + + uint32_t packed_len = 0; + + net_pack_u16(data + packed_len, peer->nick_length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, peer->nick, MAX_GC_NICK_SIZE); + packed_len += MAX_GC_NICK_SIZE; + memcpy(data + packed_len, &peer->status, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + + return packed_len; +} + +/** @brief Unpacks peer info of size length into peer. + * + * Returns the length of processed data on success. + * Returns -1 on failure. + */ +non_null() +static int unpack_gc_peer(GC_Peer *peer, const uint8_t *data, uint16_t length) +{ + if (PACKED_GC_PEER_SIZE > length) { + return -1; + } + + uint16_t len_processed = 0; + + net_unpack_u16(data + len_processed, &peer->nick_length); + len_processed += sizeof(uint16_t); + peer->nick_length = min_u16(MAX_GC_NICK_SIZE, peer->nick_length); + memcpy(peer->nick, data + len_processed, MAX_GC_NICK_SIZE); + len_processed += MAX_GC_NICK_SIZE; + memcpy(&peer->status, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + return len_processed; +} + +/** @brief Packs shared_state into data. + * + * @param data must have room for at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_shared_state(uint8_t *data, uint16_t length, const GC_SharedState *shared_state) +{ + if (length < GC_PACKED_SHARED_STATE_SIZE) { + return 0; + } + + const uint8_t privacy_state = shared_state->privacy_state; + const uint8_t voice_state = shared_state->voice_state; + + uint16_t packed_len = 0; + + // version is always first + net_pack_u32(data + packed_len, shared_state->version); + packed_len += sizeof(uint32_t); + + memcpy(data + packed_len, shared_state->founder_public_key, EXT_PUBLIC_KEY_SIZE); + packed_len += EXT_PUBLIC_KEY_SIZE; + net_pack_u16(data + packed_len, shared_state->maxpeers); + packed_len += sizeof(uint16_t); + net_pack_u16(data + packed_len, shared_state->group_name_len); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, shared_state->group_name, MAX_GC_GROUP_NAME_SIZE); + packed_len += MAX_GC_GROUP_NAME_SIZE; + memcpy(data + packed_len, &privacy_state, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + net_pack_u16(data + packed_len, shared_state->password_length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, shared_state->password, MAX_GC_PASSWORD_SIZE); + packed_len += MAX_GC_PASSWORD_SIZE; + memcpy(data + packed_len, shared_state->mod_list_hash, MOD_MODERATION_HASH_SIZE); + packed_len += MOD_MODERATION_HASH_SIZE; + net_pack_u32(data + packed_len, shared_state->topic_lock); + packed_len += sizeof(uint32_t); + memcpy(data + packed_len, &voice_state, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + + return packed_len; +} + +/** @brief Unpacks shared state data into shared_state. + * + * @param data must contain at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns the length of processed data. + */ +non_null() +static uint16_t unpack_gc_shared_state(GC_SharedState *shared_state, const uint8_t *data, uint16_t length) +{ + if (length < GC_PACKED_SHARED_STATE_SIZE) { + return 0; + } + + uint16_t len_processed = 0; + + // version is always first + net_unpack_u32(data + len_processed, &shared_state->version); + len_processed += sizeof(uint32_t); + + memcpy(shared_state->founder_public_key, data + len_processed, EXT_PUBLIC_KEY_SIZE); + len_processed += EXT_PUBLIC_KEY_SIZE; + net_unpack_u16(data + len_processed, &shared_state->maxpeers); + len_processed += sizeof(uint16_t); + net_unpack_u16(data + len_processed, &shared_state->group_name_len); + shared_state->group_name_len = min_u16(shared_state->group_name_len, MAX_GC_GROUP_NAME_SIZE); + len_processed += sizeof(uint16_t); + memcpy(shared_state->group_name, data + len_processed, MAX_GC_GROUP_NAME_SIZE); + len_processed += MAX_GC_GROUP_NAME_SIZE; + + uint8_t privacy_state; + memcpy(&privacy_state, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + net_unpack_u16(data + len_processed, &shared_state->password_length); + len_processed += sizeof(uint16_t); + memcpy(shared_state->password, data + len_processed, MAX_GC_PASSWORD_SIZE); + len_processed += MAX_GC_PASSWORD_SIZE; + memcpy(shared_state->mod_list_hash, data + len_processed, MOD_MODERATION_HASH_SIZE); + len_processed += MOD_MODERATION_HASH_SIZE; + net_unpack_u32(data + len_processed, &shared_state->topic_lock); + len_processed += sizeof(uint32_t); + + uint8_t voice_state; + memcpy(&voice_state, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + shared_state->voice_state = (Group_Voice_State)voice_state; + shared_state->privacy_state = (Group_Privacy_State)privacy_state; + + return len_processed; +} + +/** @brief Packs topic info into data. + * + * @param data must have room for at least topic length + GC_MIN_PACKED_TOPIC_INFO_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_topic_info(uint8_t *data, uint16_t length, const GC_TopicInfo *topic_info) +{ + if (length < topic_info->length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return 0; + } + + uint16_t packed_len = 0; + + net_pack_u32(data + packed_len, topic_info->version); + packed_len += sizeof(uint32_t); + net_pack_u16(data + packed_len, topic_info->checksum); + packed_len += sizeof(uint16_t); + net_pack_u16(data + packed_len, topic_info->length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, topic_info->topic, topic_info->length); + packed_len += topic_info->length; + memcpy(data + packed_len, topic_info->public_sig_key, SIG_PUBLIC_KEY_SIZE); + packed_len += SIG_PUBLIC_KEY_SIZE; + + return packed_len; +} + +/** @brief Unpacks topic info into `topic_info`. + * + * Returns -1 on failure. + * Returns the length of the processed data on success. + */ +non_null() +static int unpack_gc_topic_info(GC_TopicInfo *topic_info, const uint8_t *data, uint16_t length) +{ + if (length < sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t)) { + return -1; + } + + uint16_t len_processed = 0; + + net_unpack_u32(data + len_processed, &topic_info->version); + len_processed += sizeof(uint32_t); + net_unpack_u16(data + len_processed, &topic_info->checksum); + len_processed += sizeof(uint16_t); + net_unpack_u16(data + len_processed, &topic_info->length); + len_processed += sizeof(uint16_t); + + if (topic_info->length > MAX_GC_TOPIC_SIZE) { + topic_info->length = MAX_GC_TOPIC_SIZE; + } + + if (length - len_processed < topic_info->length + SIG_PUBLIC_KEY_SIZE) { + return -1; + } + + if (topic_info->length > 0) { + memcpy(topic_info->topic, data + len_processed, topic_info->length); + len_processed += topic_info->length; + } + + memcpy(topic_info->public_sig_key, data + len_processed, SIG_PUBLIC_KEY_SIZE); + len_processed += SIG_PUBLIC_KEY_SIZE; + + return len_processed; +} + +/** @brief Creates a shared state packet and puts it in data. + * Packet includes self pk hash, shared state signature, and packed shared state info. + * data must have room for at least GC_SHARED_STATE_ENC_PACKET_SIZE bytes. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_shared_state_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ + if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { + return -1; + } + + memcpy(data, chat->shared_state_sig, SIGNATURE_SIZE); + const uint16_t header_len = SIGNATURE_SIZE; + + const uint16_t packed_len = pack_gc_shared_state(data + header_len, length - header_len, &chat->shared_state); + + if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { + return -1; + } + + return (int)(header_len + packed_len); +} + +/** @brief Creates a signature for the group's shared state in packed form. + * + * This function only works for the Founder. + * + * Returns true on success and increments the shared state version. + */ +non_null() +static bool sign_gc_shared_state(GC_Chat *chat) +{ + if (!self_gc_is_founder(chat)) { + LOGGER_ERROR(chat->log, "Failed to sign shared state (invalid permission)"); + return false; + } + + if (chat->shared_state.version != UINT32_MAX) { + /* improbable, but an overflow would break everything */ + set_gc_shared_state_version(chat, chat->shared_state.version + 1); + } else { + LOGGER_WARNING(chat->log, "Shared state version wraparound"); + } + + uint8_t shared_state[GC_PACKED_SHARED_STATE_SIZE]; + const uint16_t packed_len = pack_gc_shared_state(shared_state, sizeof(shared_state), &chat->shared_state); + + if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { + set_gc_shared_state_version(chat, chat->shared_state.version - 1); + LOGGER_ERROR(chat->log, "Failed to pack shared state"); + return false; + } + + const int ret = crypto_sign_detached(chat->shared_state_sig, nullptr, shared_state, packed_len, + get_sig_sk(chat->chat_secret_key)); + + if (ret != 0) { + set_gc_shared_state_version(chat, chat->shared_state.version - 1); + LOGGER_ERROR(chat->log, "Failed to sign shared state (%d)", ret); + return false; + } + + return true; +} + +/** @brief Decrypts data using the shared key associated with `gconn`. + * + * The packet payload should begin with a nonce. + * + * @param message_id should be set to NULL for lossy packets. + * + * Returns length of the plaintext data on success. + * Return -1 if encrypted payload length is invalid. + * Return -2 on decryption failure. + * Return -3 if plaintext payload length is invalid. + */ +non_null(1, 2, 3, 5, 6) nullable(4) +static int group_packet_unwrap(const Logger *log, const GC_Connection *gconn, uint8_t *data, uint64_t *message_id, + uint8_t *packet_type, const uint8_t *packet, uint16_t length) +{ + if (length <= CRYPTO_NONCE_SIZE) { + LOGGER_FATAL(log, "Invalid packet length: %u", length); + return -1; + } + + uint8_t *plain = (uint8_t *)malloc(length); + + if (plain == nullptr) { + LOGGER_ERROR(log, "Failed to allocate memory for plain data buffer"); + return -1; + } + + int plain_len = decrypt_data_symmetric(gconn->session_shared_key, packet, packet + CRYPTO_NONCE_SIZE, + length - CRYPTO_NONCE_SIZE, plain); + + if (plain_len <= 0) { + free(plain); + return plain_len == 0 ? -3 : -2; + } + + const int min_plain_len = message_id != nullptr ? 1 + GC_MESSAGE_ID_BYTES : 1; + + /* remove padding */ + const uint8_t *real_plain = plain; + + while (real_plain[0] == 0) { + ++real_plain; + --plain_len; + + if (plain_len < min_plain_len) { + free(plain); + return -3; + } + } + + uint32_t header_len = sizeof(uint8_t); + *packet_type = real_plain[0]; + plain_len -= sizeof(uint8_t); + + if (message_id != nullptr) { + net_unpack_u64(real_plain + sizeof(uint8_t), message_id); + plain_len -= GC_MESSAGE_ID_BYTES; + header_len += GC_MESSAGE_ID_BYTES; + } + + memcpy(data, real_plain + header_len, plain_len); + + free(plain); + + return plain_len; +} + +int group_packet_wrap( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, + uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, + uint8_t gp_packet_type, uint8_t net_packet_type) +{ + const uint16_t padding_len = group_packet_padding_length(length); + const uint16_t min_packet_size = net_packet_type == NET_PACKET_GC_LOSSLESS + ? length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + GC_MESSAGE_ID_BYTES + 1 + : length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + 1; + + if (min_packet_size > packet_size) { + LOGGER_ERROR(log, "Invalid packet buffer size: %u", packet_size); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE) { + LOGGER_ERROR(log, "Packet payload size (%u) exceeds maximum (%u)", length, MAX_GC_PACKET_CHUNK_SIZE); + return -1; + } + + uint8_t *plain = (uint8_t *)malloc(packet_size); + + if (plain == nullptr) { + return -1; + } + + assert(padding_len < packet_size); + + memset(plain, 0, padding_len); + + uint16_t enc_header_len = sizeof(uint8_t); + plain[padding_len] = gp_packet_type; + + if (net_packet_type == NET_PACKET_GC_LOSSLESS) { + net_pack_u64(plain + padding_len + sizeof(uint8_t), message_id); + enc_header_len += GC_MESSAGE_ID_BYTES; + } + + if (length > 0 && data != nullptr) { + memcpy(plain + padding_len + enc_header_len, data, length); + } + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(rng, nonce); + + const uint16_t plain_len = padding_len + enc_header_len + length; + const uint16_t encrypt_buf_size = plain_len + CRYPTO_MAC_SIZE; + + uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + + if (encrypt == nullptr) { + free(plain); + return -2; + } + + const int enc_len = encrypt_data_symmetric(shared_key, nonce, plain, plain_len, encrypt); + + free(plain); + + if (enc_len != encrypt_buf_size) { + LOGGER_ERROR(log, "encryption failed. packet type: 0x%02x, enc_len: %d", gp_packet_type, enc_len); + free(encrypt); + return -3; + } + + packet[0] = net_packet_type; + memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + + free(encrypt); + + return 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Sends a lossy packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null() +static bool send_lossy_group_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + + if (!gconn->handshaked || gconn->pending_delete) { + return false; + } + + if (data == nullptr || length == 0) { + return false; + } + + const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSY); + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int len = group_packet_wrap( + chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, + packet_size, data, length, 0, packet_type, NET_PACKET_GC_LOSSY); + + if (len < 0) { + LOGGER_ERROR(chat->log, "Failed to encrypt packet (type: 0x%02x, error: %d)", packet_type, len); + free(packet); + return false; + } + + const bool ret = gcc_send_packet(chat, gconn, packet, (uint16_t)len); + + free(packet); + + return ret; +} + +/** @brief Sends a lossless packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null(1, 2) nullable(3) +static bool send_lossless_group_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type) +{ + assert(length <= MAX_GC_PACKET_SIZE); + + if (!gconn->handshaked || gconn->pending_delete) { + return false; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE) { + return gcc_send_lossless_packet_fragments(chat, gconn, data, length, packet_type); + } + + return gcc_send_lossless_packet(chat, gconn, data, length, packet_type) == 0; +} + +/** @brief Sends a group sync request to peer. + * + * Returns true on success or if sync request timeout has not expired. + */ +non_null() +static bool send_gc_sync_request(GC_Chat *chat, GC_Connection *gconn, uint16_t sync_flags) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_sync_request, GC_SYNC_REQUEST_LIMIT)) { + return true; + } + + chat->last_sync_request = mono_time_get(chat->mono_time); + + uint8_t data[(sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE]; + uint16_t length = sizeof(uint16_t); + + net_pack_u16(data, sync_flags); + + if (chat_is_password_protected(chat)) { + net_pack_u16(data + length, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_REQUEST); +} + +/** @brief Sends a sync response packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool send_gc_sync_response(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_RESPONSE); +} + +non_null() static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type); +non_null() static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Unpacks a sync announce. + * + * If the announced peer is not already in our peer list, we attempt to + * initiate a peer info exchange with them. + * + * Return true on success (whether or not the peer was added). + */ +non_null() +static bool unpack_gc_sync_announce(GC_Chat *chat, const uint8_t *data, const uint16_t length) +{ + GC_Announce announce = {0}; + + const int unpacked_announces = gca_unpack_announces_list(chat->log, data, length, &announce, 1); + + if (unpacked_announces <= 0) { + LOGGER_WARNING(chat->log, "Failed to unpack announces: %d", unpacked_announces); + return false; + } + + if (memcmp(announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + LOGGER_WARNING(chat->log, "Attempted to unpack our own announce"); + return true; + } + + if (!gca_is_valid_announce(&announce)) { + LOGGER_WARNING(chat->log, "got invalid announce"); + return false; + } + + const IP_Port *ip_port = announce.ip_port_is_set ? &announce.ip_port : nullptr; + const int new_peer_number = peer_add(chat, ip_port, announce.peer_public_key); + + if (new_peer_number == -1) { + LOGGER_ERROR(chat->log, "peer_add() failed"); + return false; + } + + if (new_peer_number == -2) { // peer already added + return true; + } + + if (new_peer_number > 0) { + GC_Connection *new_gconn = get_gc_connection(chat, new_peer_number); + + if (new_gconn == nullptr) { + return false; + } + + uint32_t added_tcp_relays = 0; + + for (uint8_t i = 0; i < announce.tcp_relays_count; ++i) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, new_gconn->tcp_connection_num, + &announce.tcp_relays[i].ip_port, + announce.tcp_relays[i].public_key); + + if (add_tcp_result == -1) { + continue; + } + + if (gcc_save_tcp_relay(chat->rng, new_gconn, &announce.tcp_relays[i]) == 0) { + ++added_tcp_relays; + } + } + + if (!announce.ip_port_is_set && added_tcp_relays == 0) { + gcc_mark_for_deletion(new_gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + LOGGER_ERROR(chat->log, "Sync error: Invalid peer connection info"); + return false; + } + + new_gconn->pending_handshake_type = HS_PEER_INFO_EXCHANGE; + + return true; + } + + LOGGER_FATAL(chat->log, "got impossible return value %d", new_peer_number); + + return false; +} + +/** @brief Handles a sync response packet. + * + * Note: This function may change peer numbers. + * + * Return 0 on success. + * Return -1 if the group is full or the peer failed to unpack. + * Return -2 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_sync_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (chat->connection_state == CS_CONNECTED && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -1; + } + + if (length > 0) { + if (!unpack_gc_sync_announce(chat, data, length)) { + return -1; + } + } + + chat->connection_state = CS_CONNECTED; + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -2; + } + + if (!send_gc_peer_exchange(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send peer exchange on sync response"); + } + + if (c->self_join != nullptr && chat->time_connected == 0) { + c->self_join(c->messenger, chat->group_number, userdata); + chat->time_connected = mono_time_get(chat->mono_time); + } + + return 0; +} + +non_null() static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key); +non_null() static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Creates a sync announce for peer designated by `gconn` and puts it in `announce`, which + * must be zeroed by the caller. + * + * Returns true if announce was successfully created. + */ +non_null() +static bool create_sync_announce(const GC_Chat *chat, const GC_Connection *gconn, uint32_t peer_number, + GC_Announce *announce) +{ + if (chat == nullptr || gconn == nullptr) { + return false; + } + + if (gconn->tcp_relays_count > 0) { + if (gcc_copy_tcp_relay(chat->rng, &announce->tcp_relays[0], gconn)) { + announce->tcp_relays_count = 1; + } + } + + get_gc_peer_public_key(chat, peer_number, announce->peer_public_key); + + if (gcc_ip_port_is_set(gconn)) { + announce->ip_port = gconn->addr.ip_port; + announce->ip_port_is_set = true; + } + + return true; +} + +non_null() +static bool sync_response_send_peers(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, bool first_sync) +{ + // Always respond to a peer's first sync request + if (!first_sync && !mono_time_is_timeout(chat->mono_time, + chat->last_sync_response_peer_list, + GC_SYNC_RESPONSE_PEER_LIST_LIMIT)) { + return true; + } + + uint8_t *response = (uint8_t *)malloc(MAX_GC_PACKET_CHUNK_SIZE); + + if (response == nullptr) { + return false; + } + + size_t num_announces = 0; + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *peer_gconn = get_gc_connection(chat, i); + + if (peer_gconn == nullptr || !peer_gconn->confirmed) { + continue; + } + + if (peer_gconn->public_key_hash == gconn->public_key_hash || i == peer_number) { + continue; + } + + GC_Announce announce = {0}; + + if (!create_sync_announce(chat, peer_gconn, i, &announce)) { + continue; + } + + const int packed_length = gca_pack_announce(chat->log, response, MAX_GC_PACKET_CHUNK_SIZE, &announce); + + if (packed_length <= 0) { + LOGGER_WARNING(chat->log, "Failed to pack announce: %d", packed_length); + continue; + } + + if (!send_gc_sync_response(chat, gconn, response, packed_length)) { + LOGGER_WARNING(chat->log, "Failed to send peer announce info"); + continue; + } + + ++num_announces; + } + + free(response); + + if (num_announces == 0) { + // we send an empty sync response even if we didn't send any peers as an acknowledgement + if (!send_gc_sync_response(chat, gconn, nullptr, 0)) { + LOGGER_WARNING(chat->log, "Failed to send peer announce info"); + return false; + } + } else { + chat->last_sync_response_peer_list = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Sends group state specified by `sync_flags` peer designated by `peer_number`. + * + * Return true on success. + */ +non_null() +static bool sync_response_send_state(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + uint16_t sync_flags) +{ + const bool first_sync = gconn->last_sync_response == 0; + + // Do not change the order of these four send calls. See: https://toktok.ltd/spec.html#sync_request-0xf8 + if ((sync_flags & GF_STATE) > 0 && chat->shared_state.version > 0) { + if (!send_peer_shared_state(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send shared state"); + return false; + } + + if (!send_peer_mod_list(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send mod list"); + return false; + } + + if (!send_peer_sanctions_list(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send sanctions list"); + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + if ((sync_flags & GF_TOPIC) > 0 && chat->time_connected > 0 && chat->topic_info.version > 0) { + if (!send_peer_topic(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send topic"); + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + if ((sync_flags & GF_PEERS) > 0) { + if (!sync_response_send_peers(chat, gconn, peer_number, first_sync)) { + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Handles a sync request packet and sends a response containing the peer list. + * + * May send additional group info in separate packets, including the topic, shared state, mod list, + * and sanctions list, if respective sync flags are set. + * + * If the group is password protected the password in the request data must first be verified. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if password is invalid. + * Return -3 if we fail to send a response packet. + * Return -4 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_sync_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -4; + } + + if (length < sizeof(uint16_t)) { + return -1; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (chat->shared_state.version == 0) { + LOGGER_DEBUG(chat->log, "Got sync request with uninitialized state"); + return 0; + } + + if (!mono_time_is_timeout(chat->mono_time, gconn->last_sync_response, GC_PING_TIMEOUT)) { + LOGGER_DEBUG(chat->log, "sync request rate limit for peer %d", peer_number); + return 0; + } + + uint16_t sync_flags; + net_unpack_u16(data, &sync_flags); + + if (chat_is_password_protected(chat)) { + if (length < (sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE) { + return -2; + } + + uint16_t password_length; + net_unpack_u16(data + sizeof(uint16_t), &password_length); + + const uint8_t *password = data + (sizeof(uint16_t) * 2); + + if (!validate_password(chat, password, password_length)) { + LOGGER_DEBUG(chat->log, "Invalid password"); + return -2; + } + } + + if (!sync_response_send_state(chat, gconn, peer_number, sync_flags)) { + return -3; + } + + return 0; +} + +non_null() static void copy_self(const GC_Chat *chat, GC_Peer *peer); +non_null() static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Shares our TCP relays with peer and adds shared relays to our connection with them. + * + * Returns true on success or if we're not connected to any TCP relays. + */ +non_null() +static bool send_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn) +{ + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + uint8_t data[GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6]; + + const uint32_t n = tcp_copy_connected_relays_index(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, + gconn->tcp_relay_share_index); + + if (n == 0) { + return true; + } + + gconn->tcp_relay_share_index += GCC_MAX_TCP_SHARED_RELAYS; + + for (uint32_t i = 0; i < n; ++i) { + add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_relays[i].ip_port, + tcp_relays[i].public_key); + } + + const int nodes_len = pack_nodes(chat->log, data, sizeof(data), tcp_relays, n); + + if (nodes_len <= 0 || (uint32_t)nodes_len > sizeof(data)) { + LOGGER_ERROR(chat->log, "Failed to pack tcp relays (nodes_len: %d)", nodes_len); + return false; + } + + if (!send_lossless_group_packet(chat, gconn, data, (uint16_t)nodes_len, GP_TCP_RELAYS)) { + LOGGER_ERROR(chat->log, "Failed to send tcp relays"); + return false; + } + + return true; +} + +/** @brief Adds a peer's shared TCP relays to our connection with them. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contains invalid data. + */ +non_null() +static int handle_gc_tcp_relays(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length == 0) { + return -1; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, nullptr, data, length, true); + + if (num_nodes <= 0) { + return -2; + } + + for (int i = 0; i < num_nodes; ++i) { + const Node_format *tcp_node = &tcp_relays[i]; + + if (add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_node->ip_port, + tcp_node->public_key) == 0) { + gcc_save_tcp_relay(chat->rng, gconn, tcp_node); + + if (gconn->tcp_relays_count == 1) { + add_gc_saved_peers(chat, gconn); // make sure we save at least one tcp relay + } + } + } + + return 0; +} + +/** @brief Send invite request to peer_number. + * + * If the group requires a password, the packet will + * contain the password supplied by the invite requestor. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_request(const GC_Chat *chat, GC_Connection *gconn) +{ + uint16_t length = 0; + uint8_t data[sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE]; + + if (chat_is_password_protected(chat)) { + net_pack_u16(data, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + return send_lossless_group_packet(chat, gconn, data, length, GP_INVITE_REQUEST); +} + +non_null() +static bool send_gc_invite_response(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_INVITE_RESPONSE); +} + +/** @brief Handles an invite response packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if we fail to send a sync request. + */ +non_null() +static int handle_gc_invite_response(GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t flags = GF_PEERS | GF_TOPIC | GF_STATE; + + if (!send_gc_sync_request(chat, gconn, flags)) { + return -1; + } + + return 0; +} + +/** + * @brief Handles an invite response reject packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_invite_response_reject(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, + void *userdata) +{ + if (length < sizeof(uint8_t)) { + return -1; + } + + if (chat->connection_state == CS_CONNECTED) { + return 0; + } + + if (gc_get_self_role(chat) == GR_FOUNDER) { + return 0; + } + + uint8_t type = data[0]; + + if (type >= GJ_INVALID) { + type = GJ_INVITE_FAILED; + } + + chat->connection_state = CS_DISCONNECTED; + + if (c->rejected != nullptr) { + c->rejected(c->messenger, chat->group_number, type, userdata); + } + + return 0; +} + +/** @brief Sends an invite response rejection packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_response_reject(const GC_Chat *chat, const GC_Connection *gconn, uint8_t type) +{ + if (type >= GJ_INVALID) { + type = GJ_INVITE_FAILED; + } + + uint8_t data[1]; + data[0] = type; + const uint16_t length = 1; + + return send_lossy_group_packet(chat, gconn, data, length, GP_INVITE_RESPONSE_REJECT); +} + +/** @brief Handles an invite request and verifies that the correct password has been supplied + * if the group is password protected. + * + * Return 0 if invite request is successfully handled. + * Return -1 if the group is full. + * Return -2 if the supplied password is invalid. + * Return -3 if we fail to send an invite response. + * Return -4 if peer_number does not designate a valid peer. + */ +non_null() +static int handle_gc_invite_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ + if (chat->shared_state.version == 0) { // we aren't synced yet; ignore request + return 0; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -4; + } + + int ret = -1; + + uint8_t invite_error; + + if (get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers && !peer_is_founder(chat, peer_number)) { + invite_error = GJ_GROUP_FULL; + goto FAILED_INVITE; + } + + if (chat_is_password_protected(chat)) { + invite_error = GJ_INVALID_PASSWORD; + ret = -2; + + if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { + goto FAILED_INVITE; + } + + uint16_t password_length; + net_unpack_u16(data, &password_length); + + const uint8_t *password = data + sizeof(uint16_t); + + if (!validate_password(chat, password, password_length)) { + goto FAILED_INVITE; + } + } + + if (!send_gc_invite_response(chat, gconn)) { + return -3; + } + + return 0; + +FAILED_INVITE: + send_gc_invite_response_reject(chat, gconn, invite_error); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + + return ret; +} + +/** @brief Sends a lossless packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossless_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + send_lossless_group_packet(chat, gconn, data, length, type); + } + } +} + +/** @brief Sends a lossy packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossy_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + send_lossy_group_packet(chat, gconn, data, length, type); + } + } +} + +/** @brief Creates packet with broadcast header info followed by data of length. + * + * Returns length of packet including header. + */ +non_null(3) nullable(1) +static uint16_t make_gc_broadcast_header(const uint8_t *data, uint16_t length, uint8_t *packet, uint8_t bc_type) +{ + packet[0] = bc_type; + const uint16_t header_len = sizeof(uint8_t); + + if (data != nullptr && length > 0) { + memcpy(packet + header_len, data, length); + } + + return length + header_len; +} + +/** @brief sends a group broadcast packet to all confirmed peers. + * + * Returns true on success. + */ +non_null(1) nullable(2) +static bool send_gc_broadcast_message(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t bc_type) +{ + if (length + GC_BROADCAST_ENC_HEADER_SIZE > MAX_GC_PACKET_SIZE) { + LOGGER_ERROR(chat->log, "Failed to broadcast message: invalid length %u", length); + return false; + } + + uint8_t *packet = (uint8_t *)malloc(length + GC_BROADCAST_ENC_HEADER_SIZE); + + if (packet == nullptr) { + return false; + } + + const uint16_t packet_len = make_gc_broadcast_header(data, length, packet, bc_type); + + send_gc_lossless_packet_all_peers(chat, packet, packet_len, GP_BROADCAST); + + free(packet); + + return true; +} + +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat); + +/** @brief Compares the supplied values with our own state and returns the appropriate + * sync flags for a sync request. + */ +non_null() +static uint16_t get_sync_flags(const GC_Chat *chat, uint16_t peers_checksum, uint16_t peer_count, + uint32_t sstate_version, uint32_t screds_version, uint16_t roles_checksum, + uint32_t topic_version, uint16_t topic_checksum) +{ + uint16_t sync_flags = 0; + + if (peers_checksum != chat->peers_checksum && peer_count >= get_gc_confirmed_numpeers(chat)) { + sync_flags |= GF_PEERS; + } + + if (sstate_version > 0) { + const uint16_t self_roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; + + if ((sstate_version > chat->shared_state.version || screds_version > chat->moderation.sanctions_creds.version) + || (screds_version == chat->moderation.sanctions_creds.version + && roles_checksum != self_roles_checksum)) { + sync_flags |= GF_STATE; + } + } + + if (group_topic_lock_enabled(chat)) { + if (topic_version > chat->topic_info.version || + (topic_version == chat->topic_info.version && topic_checksum > chat->topic_info.checksum)) { + sync_flags |= GF_TOPIC; + } + } else if (topic_checksum > chat->topic_info.checksum) { + sync_flags |= GF_TOPIC; + } + + return sync_flags; +} + +/** @brief Compares a peer's group sync info that we received in a ping packet to our own. + * + * If their info appears to be more recent than ours we send them a sync request. + * + * This function should only be called from `handle_gc_ping()`. + * + * Returns true if a sync request packet is successfully sent. + */ +non_null() +static bool do_gc_peer_state_sync(GC_Chat *chat, GC_Connection *gconn, const uint8_t *sync_data, + const uint16_t length) +{ + if (length < GC_PING_PACKET_MIN_DATA_SIZE) { + return false; + } + + uint16_t peers_checksum; + uint16_t peer_count; + uint32_t sstate_version; + uint32_t screds_version; + uint16_t roles_checksum; + uint32_t topic_version; + uint16_t topic_checksum; + + size_t unpacked_len = 0; + + net_unpack_u16(sync_data, &peers_checksum); + unpacked_len += sizeof(uint16_t); + + net_unpack_u16(sync_data + unpacked_len, &peer_count); + unpacked_len += sizeof(uint16_t); + + net_unpack_u32(sync_data + unpacked_len, &sstate_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u32(sync_data + unpacked_len, &screds_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u16(sync_data + unpacked_len, &roles_checksum); + unpacked_len += sizeof(uint16_t); + + net_unpack_u32(sync_data + unpacked_len, &topic_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u16(sync_data + unpacked_len, &topic_checksum); + unpacked_len += sizeof(uint16_t); + + if (unpacked_len != GC_PING_PACKET_MIN_DATA_SIZE) { + LOGGER_FATAL(chat->log, "Unpacked length is impossible (%zu)", unpacked_len); + return false; + } + + const uint16_t sync_flags = get_sync_flags(chat, peers_checksum, peer_count, sstate_version, screds_version, + roles_checksum, topic_version, topic_checksum); + + if (sync_flags > 0) { + return send_gc_sync_request(chat, gconn, sync_flags); + } + + return false; +} + +/** @brief Handles a ping packet. + * + * The packet contains sync information including peer's peer list checksum, + * shared state version, topic version, and sanction credentials version. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size or peer is not confirmed. + */ +non_null() +static int handle_gc_ping(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < GC_PING_PACKET_MIN_DATA_SIZE) { + return -1; + } + + if (!gconn->confirmed) { + return -1; + } + + do_gc_peer_state_sync(chat, gconn, data, length); + + if (length > GC_PING_PACKET_MIN_DATA_SIZE) { + IP_Port ip_port; + memset(&ip_port, 0, sizeof(IP_Port)); + + if (unpack_ip_port(&ip_port, data + GC_PING_PACKET_MIN_DATA_SIZE, + length - GC_PING_PACKET_MIN_DATA_SIZE, false) > 0) { + gcc_set_ip_port(gconn, &ip_port); + add_gc_saved_peers(chat, gconn); + } + } + + return 0; +} + +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status) +{ + const GC_Session *c = m->group_handler; + const GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + self_gc_set_status(chat, status); + + uint8_t data[1]; + data[0] = gc_get_self_status(chat); + + if (!send_gc_broadcast_message(chat, data, 1, GM_STATUS)) { + return -2; + } + + return 0; +} + +/** @brief Handles a status broadcast from `peer`. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid length. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_status(const GC_Session *c, const GC_Chat *chat, GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < sizeof(uint8_t)) { + return -1; + } + + const Group_Peer_Status status = (Group_Peer_Status)data[0]; + + if (status > GS_BUSY) { + LOGGER_WARNING(chat->log, "Received invalid status %u", status); + return 0; + } + + peer->status = status; + + if (c->status_change != nullptr) { + c->status_change(c->messenger, chat->group_number, peer->peer_id, status, userdata); + } + + return 0; +} + +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return UINT8_MAX; + } + + return peer->status; +} + +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return UINT8_MAX; + } + + return peer->role; +} + +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest) +{ + if (dest != nullptr) { + memcpy(dest, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + } +} + +/** @brief Sends self peer info to `gconn`. + * + * If the group is password protected the request will contain the group + * password, which the recipient will validate in the respective + * group message handler. + * + * Returns true on success. + */ +non_null() +static bool send_self_to_peer(const GC_Chat *chat, GC_Connection *gconn) +{ + GC_Peer *self = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + + if (self == nullptr) { + return false; + } + + copy_self(chat, self); + + const uint16_t data_size = PACKED_GC_PEER_SIZE + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE; + uint8_t *data = (uint8_t *)malloc(data_size); + + if (data == nullptr) { + free(self); + return false; + } + + uint16_t length = 0; + + if (chat_is_password_protected(chat)) { + net_pack_u16(data, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + sizeof(uint16_t), chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + const int packed_len = pack_gc_peer(data + length, data_size - length, self); + length += packed_len; + + free(self); + + if (packed_len <= 0) { + LOGGER_DEBUG(chat->log, "pack_gc_peer failed in handle_gc_peer_info_request_request %d", packed_len); + free(data); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, data, length, GP_PEER_INFO_RESPONSE); + + free(data); + + return ret; +} + +/** @brief Handles a peer info request packet. + * + * Return 0 on success. + * Return -1 if unconfirmed peer is trying to join a full group. + * Return -2 if response fails. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_peer_info_request(const GC_Chat *chat, uint32_t peer_number) +{ + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -1; + } + + if (!send_self_to_peer(chat, gconn)) { + return -2; + } + + return 0; +} + +/** @brief Sends a peer info request to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_PEER_INFO_REQUEST); +} + +/** @brief Do peer info exchange with peer designated by `gconn`. + * + * This function sends two packets to a peer. The first packet is a peer info response containing our own info, + * and the second packet is a peer info request. + * + * Return false if either packet fails to send. + */ +static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_self_to_peer(chat, gconn) && send_gc_peer_info_request(chat, gconn); +} + +/** @brief Updates peer's info, validates their group role, and sets them as a confirmed peer. + * If the group is password protected the password must first be validated. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if group number is invalid. + * Return -3 if peer number is invalid. + * Return -4 if unconfirmed peer is trying to join a full group. + * Return -5 if supplied group password is invalid. + * Return -6 if we fail to add the peer to the peer list. + * Return -7 if peer's role cannot be validated. + * Return -8 if malloc fails. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_peer_info_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, + const uint8_t *data, uint16_t length, void *userdata) +{ + if (length < PACKED_GC_PEER_SIZE) { + return -1; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -3; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -4; + } + + uint16_t unpacked_len = 0; + + if (chat_is_password_protected(chat)) { + if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { + return -5; + } + + uint16_t password_length; + net_unpack_u16(data, &password_length); + unpacked_len += sizeof(uint16_t); + + if (!validate_password(chat, data + unpacked_len, password_length)) { + return -5; + } + + unpacked_len += MAX_GC_PASSWORD_SIZE; + } + + if (length <= unpacked_len) { + return -1; + } + + GC_Peer *peer_info = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + + if (peer_info == nullptr) { + return -8; + } + + if (unpack_gc_peer(peer_info, data + unpacked_len, length - unpacked_len) == -1) { + LOGGER_ERROR(chat->log, "unpack_gc_peer() failed"); + free(peer_info); + return -6; + } + + if (peer_update(chat, peer_info, peer_number) == -1) { + LOGGER_WARNING(chat->log, "peer_update() failed"); + free(peer_info); + return -6; + } + + free(peer_info); + + const bool was_confirmed = gconn->confirmed; + gconn->confirmed = true; + + update_gc_peer_roles(chat); + + add_gc_saved_peers(chat, gconn); + + set_gc_peerlist_checksum(chat); + + if (c->peer_join != nullptr && !was_confirmed) { + c->peer_join(c->messenger, chat->group_number, peer->peer_id, userdata); + } + + return 0; +} + +/** @brief Sends the group shared state and its signature to peer_number. + * + * Returns true on success. + */ +non_null() +static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat->shared_state.version == 0) { + return false; + } + + uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; + const int length = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + + if (length != GC_SHARED_STATE_ENC_PACKET_SIZE) { + return false; + } + + return send_lossless_group_packet(chat, gconn, packet, (uint16_t)length, GP_SHARED_STATE); +} + +/** @brief Sends the group shared state and signature to all confirmed peers. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_shared_state(const GC_Chat *chat) +{ + uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; + const int packet_len = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + + if (packet_len != GC_SHARED_STATE_ENC_PACKET_SIZE) { + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SHARED_STATE); + + return true; +} + +/** @brief Helper function for `do_gc_shared_state_changes()`. + * + * If the privacy state has been set to private, we kill our group's connection to the DHT. + * Otherwise, we create a new connection with the DHT and flag an announcement. + */ +non_null(1, 2) nullable(3) +static void do_privacy_state_change(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + if (is_public_chat(chat)) { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } else { + chat->update_self_announces = true; + chat->join_type = HJ_PUBLIC; + } + } else { + kill_group_friend_connection(c, chat); + cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); + chat->join_type = HJ_PRIVATE; + } + + if (c->privacy_state != nullptr) { + c->privacy_state(c->messenger, chat->group_number, chat->shared_state.privacy_state, userdata); + } +} + +/** + * Compares old_shared_state with the chat instance's current shared state and triggers the + * appropriate callbacks depending on what pieces of state information changed. Also + * handles DHT announcement/removal if the privacy state changed. + * + * The initial retrieval of the shared state on group join will be ignored by this function. + */ +non_null(1, 2, 3) nullable(4) +static void do_gc_shared_state_changes(const GC_Session *c, GC_Chat *chat, const GC_SharedState *old_shared_state, + void *userdata) +{ + /* Max peers changed */ + if (chat->shared_state.maxpeers != old_shared_state->maxpeers && c->peer_limit != nullptr) { + c->peer_limit(c->messenger, chat->group_number, chat->shared_state.maxpeers, userdata); + } + + /* privacy state changed */ + if (chat->shared_state.privacy_state != old_shared_state->privacy_state) { + do_privacy_state_change(c, chat, userdata); + } + + /* password changed */ + if (chat->shared_state.password_length != old_shared_state->password_length + || memcmp(chat->shared_state.password, old_shared_state->password, old_shared_state->password_length) != 0) { + + if (c->password != nullptr) { + c->password(c->messenger, chat->group_number, chat->shared_state.password, + chat->shared_state.password_length, userdata); + } + } + + /* topic lock state changed */ + if (chat->shared_state.topic_lock != old_shared_state->topic_lock && c->topic_lock != nullptr) { + const Group_Topic_Lock lock_state = group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; + c->topic_lock(c->messenger, chat->group_number, lock_state, userdata); + } + + /* voice state changed */ + if (chat->shared_state.voice_state != old_shared_state->voice_state && c->voice_state != nullptr) { + c->voice_state(c->messenger, chat->group_number, chat->shared_state.voice_state, userdata); + } +} + +/** @brief Sends a sync request to a random peer in the group with the specificed sync flags. + * + * Return true on success. + */ +non_null() +static bool send_gc_random_sync_request(GC_Chat *chat, uint16_t sync_flags) +{ + GC_Connection *rand_gconn = random_gc_connection(chat); + + if (rand_gconn == nullptr) { + return false; + } + + return send_gc_sync_request(chat, rand_gconn, sync_flags); +} + +/** @brief Returns true if all shared state values are legal. */ +non_null() +static bool validate_gc_shared_state(const GC_SharedState *state) +{ + return state->maxpeers > 0 + && state->password_length <= MAX_GC_PASSWORD_SIZE + && state->group_name_len > 0 + && state->group_name_len <= MAX_GC_GROUP_NAME_SIZE + && state->privacy_state <= GI_PRIVATE + && state->voice_state <= GV_FOUNDER; +} + +/** @brief Handles a shared state error and attempts to send a sync request to a random peer. + * + * Return 0 if error is currectly handled. + * Return -1 on failure. + */ +non_null() +static int handle_gc_shared_state_error(GC_Chat *chat, GC_Connection *gconn) +{ + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return 0; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (!send_gc_random_sync_request(chat, GF_STATE)) { + return -1; + } + + return 0; +} + +/** @brief Handles a shared state packet and validates the new shared state. + * + * Return 0 if packet is successfully handled. + * Return -1 if packet is invalid and this is not successfully handled. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_shared_state(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { + return handle_gc_shared_state_error(chat, gconn); + } + + const uint8_t *signature = data; + const uint8_t *ss_data = data + SIGNATURE_SIZE; + const uint16_t ss_length = length - SIGNATURE_SIZE; + + if (crypto_sign_verify_detached(signature, ss_data, GC_PACKED_SHARED_STATE_SIZE, + get_sig_pk(chat->chat_public_key)) == -1) { + LOGGER_DEBUG(chat->log, "Failed to validate shared state signature"); + return handle_gc_shared_state_error(chat, gconn); + } + + uint32_t version; + net_unpack_u32(ss_data, &version); // version is the first 4 bytes of shared state data payload + + if (version == 0 || version < chat->shared_state.version) { + LOGGER_DEBUG(chat->log, "Invalid shared state version (got %u, expected >= %u)", + version, chat->shared_state.version); + return 0; + } + + GC_SharedState old_shared_state = chat->shared_state; + GC_SharedState new_shared_state; + + if (unpack_gc_shared_state(&new_shared_state, ss_data, ss_length) == 0) { + LOGGER_WARNING(chat->log, "Failed to unpack shared state"); + return 0; + } + + if (!validate_gc_shared_state(&new_shared_state)) { + LOGGER_WARNING(chat->log, "Failed to validate shared state"); + return 0; + } + + if (chat->shared_state.version == 0) { // init founder public sig key in moderation object + memcpy(chat->moderation.founder_public_sig_key, + get_sig_pk(new_shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); + } + + chat->shared_state = new_shared_state; + + memcpy(chat->shared_state_sig, signature, sizeof(chat->shared_state_sig)); + + set_gc_shared_state_version(chat, chat->shared_state.version); + + do_gc_shared_state_changes(c, chat, &old_shared_state, userdata); + + return 0; +} + +/** @brief Validates `data` containing a moderation list and unpacks it into the + * shared state of `chat`. + * + * Return 1 if data is valid but mod list doesn't match shared state. + * Return 0 if data is valid. + * Return -1 if data is invalid. + */ +non_null() +static int validate_unpack_mod_list(GC_Chat *chat, const uint8_t *data, uint16_t length, uint16_t num_mods) +{ + if (num_mods > MOD_MAX_NUM_MODERATORS) { + return -1; + } + + uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE] = {0}; + + if (length > 0) { + mod_list_get_data_hash(mod_list_hash, data, length); + } + + // we make sure that this mod list's hash matches the one we got in our last shared state update + if (chat->shared_state.version > 0 + && memcmp(mod_list_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE) != 0) { + LOGGER_WARNING(chat->log, "failed to validate mod list hash"); + return 1; + } + + if (mod_list_unpack(&chat->moderation, data, length, num_mods) == -1) { + LOGGER_WARNING(chat->log, "failed to unpack mod list"); + return -1; + } + + return 0; +} + +/** @brief Handles new mod_list and compares its hash against the mod_list_hash in the shared state. + * + * If the new list fails validation, we attempt to send a sync request to a random peer. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contained invalid data or validation failed. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_mod_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, void *userdata) +{ + if (length < sizeof(uint16_t)) { + return -1; + } + + // only the founder can modify the list; the founder can never be out of sync + if (self_gc_is_founder(chat)) { + return 0; + } + + uint16_t num_mods; + net_unpack_u16(data, &num_mods); + + const int unpack_ret = validate_unpack_mod_list(chat, data + sizeof(uint16_t), length - sizeof(uint16_t), num_mods); + + if (unpack_ret == 0) { + update_gc_peer_roles(chat); + + if (chat->connection_state == CS_CONNECTED && c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_MOD, userdata); + } + + return 0; + } + + if (unpack_ret == 1) { + return 0; + } + + // unpack/validation failed: handle error + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return -2; + } + + if (chat->numpeers <= 1) { + return 0; + } + + send_gc_random_sync_request(chat, GF_STATE); + + return 0; +} + +/** @brief Handles a sanctions list validation error and attempts to send a sync request to a random peer. + * + * Return 0 on success. + * Return -1 on failure. + */ +non_null() +static int handle_gc_sanctions_list_error(GC_Chat *chat) +{ + if (chat->moderation.sanctions_creds.version > 0) { + return 0; + } + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return 0; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (!send_gc_random_sync_request(chat, GF_STATE)) { + return -1; + } + + return 0; +} + +/** @brief Handles a sanctions list packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to gracefully handle a sanctions list error. + * Return -2 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_sanctions_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, + void *userdata) +{ + if (length < sizeof(uint16_t)) { + return -2; + } + + uint16_t num_sanctions; + net_unpack_u16(data, &num_sanctions); + + if (num_sanctions > MOD_MAX_NUM_SANCTIONS) { + LOGGER_DEBUG(chat->log, "num_sanctions: %u exceeds maximum", num_sanctions); + return handle_gc_sanctions_list_error(chat); + } + + Mod_Sanction_Creds creds; + + Mod_Sanction *sanctions = (Mod_Sanction *)calloc(num_sanctions, sizeof(Mod_Sanction)); + + if (sanctions == nullptr) { + return -1; + } + + const int unpacked_num = sanctions_list_unpack(sanctions, &creds, num_sanctions, data + sizeof(uint16_t), + length - sizeof(uint16_t), nullptr); + + if (unpacked_num != num_sanctions) { + LOGGER_WARNING(chat->log, "Failed to unpack sanctions list: %d", unpacked_num); + free(sanctions); + return handle_gc_sanctions_list_error(chat); + } + + if (!sanctions_list_check_integrity(&chat->moderation, &creds, sanctions, num_sanctions)) { + LOGGER_WARNING(chat->log, "Sanctions list failed integrity check"); + free(sanctions); + return handle_gc_sanctions_list_error(chat); + } + + if (creds.version < chat->moderation.sanctions_creds.version) { + free(sanctions); + return 0; + } + + // this may occur if two mods change the sanctions list at the exact same time + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + free(sanctions); + return 0; + } + + sanctions_list_cleanup(&chat->moderation); + + chat->moderation.sanctions_creds = creds; + chat->moderation.sanctions = sanctions; + chat->moderation.num_sanctions = num_sanctions; + + update_gc_peer_roles(chat); + + if (chat->connection_state == CS_CONNECTED) { + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_OBSERVER, userdata); + } + } + + return 0; +} + +/** @brief Makes a mod_list packet. + * + * Returns length of packet data on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_mod_list_packet(const GC_Chat *chat, uint8_t *data, uint32_t maxlen, uint16_t mod_list_size) +{ + if (maxlen < sizeof(uint16_t) + mod_list_size) { + return -1; + } + + net_pack_u16(data, chat->moderation.num_mods); + const uint16_t length = sizeof(uint16_t) + mod_list_size; + + if (mod_list_size > 0) { + uint8_t *packed_mod_list = (uint8_t *)malloc(mod_list_size); + + if (packed_mod_list == nullptr) { + return -1; + } + + mod_list_pack(&chat->moderation, packed_mod_list); + memcpy(data + sizeof(uint16_t), packed_mod_list, mod_list_size); + + free(packed_mod_list); + } + + return length; +} + +/** @brief Sends the moderator list to peer. + * + * Return true on success. + */ +non_null() +static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + const uint16_t length = sizeof(uint16_t) + mod_list_size; + uint8_t *packet = (uint8_t *)malloc(length); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + + if (packet_len != length) { + free(packet); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, packet, length, GP_MOD_LIST); + + free(packet); + + return ret; +} + +/** @brief Makes a sanctions list packet. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_sanctions_list_packet(const GC_Chat *chat, uint8_t *data, uint16_t maxlen) +{ + if (maxlen < sizeof(uint16_t)) { + return -1; + } + + net_pack_u16(data, chat->moderation.num_sanctions); + const uint16_t length = sizeof(uint16_t); + + const int packed_len = sanctions_list_pack(data + length, maxlen - length, chat->moderation.sanctions, + chat->moderation.num_sanctions, &chat->moderation.sanctions_creds); + + if (packed_len < 0) { + return -1; + } + + return (int)(length + packed_len); +} + +/** @brief Sends the sanctions list to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat->moderation.sanctions_creds.version == 0) { + return true; + } + + const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + + sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + + if (packet_len == -1) { + free(packet); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + + free(packet); + + return ret; +} + +/** @brief Sends the sanctions list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_sanctions_list(const GC_Chat *chat) +{ + const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + + sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + + if (packet_len == -1) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + + free(packet); + + return true; +} + +/** @brief Re-signs all sanctions list entries signed by public_sig_key and broadcasts + * the updated sanctions list to all group peers. + * + * Returns true on success. + */ +non_null() +static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key) +{ + const uint16_t num_replaced = sanctions_list_replace_sig(&chat->moderation, public_sig_key); + + if (num_replaced == 0) { + return true; + } + + return broadcast_gc_sanctions_list(chat); +} + +/** @brief Sends mod_list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_mod_list(const GC_Chat *chat) +{ + const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + const uint16_t length = sizeof(uint16_t) + mod_list_size; + uint8_t *packet = (uint8_t *)malloc(length); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + + if (packet_len != length) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, length, GP_MOD_LIST); + + free(packet); + + return true; +} + +/** @brief Sends a parting signal to the group. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the packet failed to send. + */ +non_null(1) nullable(2) +static int send_gc_self_exit(const GC_Chat *chat, const uint8_t *partmessage, uint16_t length) +{ + if (length > MAX_GC_PART_MESSAGE_SIZE) { + return -1; + } + + if (!send_gc_broadcast_message(chat, partmessage, length, GM_PEER_EXIT)) { + return -2; + } + + return 0; +} + +/** @brief Handles a peer exit broadcast. */ +non_null(1, 2) nullable(3) +static void handle_gc_peer_exit(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length > MAX_GC_PART_MESSAGE_SIZE) { + length = MAX_GC_PART_MESSAGE_SIZE; + } + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_QUIT, data, length); +} + +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length) +{ + const GC_Session *c = m->group_handler; + const GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (length > MAX_GC_NICK_SIZE) { + return -2; + } + + if (length == 0 || nick == nullptr) { + return -3; + } + + if (!self_gc_set_nick(chat, nick, length)) { + return -2; + } + + if (!send_gc_broadcast_message(chat, nick, length, GM_NICK)) { + return -4; + } + + return 0; +} + +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + if (name != nullptr) { + memcpy(name, peer->nick, peer->nick_length); + } + + return true; +} + +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + return peer->nick_length; +} + +/** @brief Handles a nick change broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 on failure. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_nick(const GC_Session *c, GC_Chat *chat, GC_Peer *peer, const uint8_t *nick, + uint16_t length, void *userdata) +{ + /* If this happens malicious behaviour is highly suspect */ + if (length == 0 || length > MAX_GC_NICK_SIZE) { + GC_Connection *gconn = &peer->gconn; + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); + LOGGER_WARNING(chat->log, "Invalid nick length for nick: %s (%u)", nick, length); + return -1; + } + + memcpy(peer->nick, nick, length); + peer->nick_length = length; + + if (c->nick_change != nullptr) { + c->nick_change(c->messenger, chat->group_number, peer->peer_id, nick, length, userdata); + } + + return 0; +} + +/** @brief Copies peer_number's public key to `public_key`. + * + * Returns 0 on success. + * Returns -1 if peer_number is invalid. + * Returns -2 if `public_key` is null. + */ +non_null() +static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (public_key == nullptr) { + return -2; + } + + memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return 0; +} + +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (public_key == nullptr) { + return -2; + } + + memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return 0; +} + +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + if (peer_number_is_self(peer_number)) { // we cannot have a connection with ourselves + return 0; + } + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return 0; + } + + if (gcc_conn_is_direct(chat->mono_time, gconn)) { + return 2; + } + + return 1; +} + +/** @brief Creates a topic packet and puts it in data. + * + * Packet includes the topic, topic length, public signature key of the + * setter, topic version, and the signature. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_topic_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ + if (length < SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + memcpy(data, chat->topic_sig, SIGNATURE_SIZE); + uint16_t data_length = SIGNATURE_SIZE; + + const uint16_t packed_len = pack_gc_topic_info(data + data_length, length - data_length, &chat->topic_info); + data_length += packed_len; + + if (packed_len != chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + return data_length; +} + +/** @brief Sends the group topic to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + + if (packet_len != packet_buf_size) { + free(packet); + return false; + } + + if (!send_lossless_group_packet(chat, gconn, packet, packet_buf_size, GP_TOPIC)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** + * @brief Initiates a session key rotation with peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_peer_key_rotation_request(const GC_Chat *chat, GC_Connection *gconn) +{ + // Only the peer closest to the chat_id sends requests. This is to prevent both peers from sending + // requests at the same time and ending up with a different resulting shared key + if (!gconn->self_is_closer) { + // if this peer hasn't sent us a rotation request in a reasonable timeframe we drop their connection + if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT + GC_PING_TIMEOUT)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + } + + return true; + } + + uint8_t packet[1 + ENC_PUBLIC_KEY_SIZE]; + + net_pack_bool(&packet[0], false); // request type + + create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + + // copy new session public key to packet + memcpy(packet + 1, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); + + if (!send_lossless_group_packet(chat, gconn, packet, sizeof(packet), GP_KEY_ROTATION)) { + return false; + } + + gconn->pending_key_rotation_request = true; + + return true; +} + +/** @brief Sends the group topic to all group members. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_topic(const GC_Chat *chat) +{ + const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + + if (packet_len != packet_buf_size) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, packet_buf_size, GP_TOPIC); + + free(packet); + + return true; +} + +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length) +{ + if (length > MAX_GC_TOPIC_SIZE) { + return -1; + } + + const bool topic_lock_enabled = group_topic_lock_enabled(chat); + + if (topic_lock_enabled && gc_get_self_role(chat) > GR_MODERATOR) { + return -2; + } + + if (gc_get_self_role(chat) > GR_USER) { + return -2; + } + + const GC_TopicInfo old_topic_info = chat->topic_info; + + uint8_t old_topic_sig[SIGNATURE_SIZE]; + memcpy(old_topic_sig, chat->topic_sig, SIGNATURE_SIZE); + + // TODO(jfreegman): improbable, but an overflow would break topic setting + if (chat->topic_info.version == UINT32_MAX) { + return -3; + } + + // only increment topic version when lock is enabled + if (topic_lock_enabled) { + ++chat->topic_info.version; + } + + chat->topic_info.length = length; + + if (length > 0) { + assert(topic != nullptr); + memcpy(chat->topic_info.topic, topic, length); + } else { + memset(chat->topic_info.topic, 0, sizeof(chat->topic_info.topic)); + } + + memcpy(chat->topic_info.public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + + chat->topic_info.checksum = get_gc_topic_checksum(&chat->topic_info); + + const uint16_t packet_buf_size = length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packed_topic = (uint8_t *)malloc(packet_buf_size); + + if (packed_topic == nullptr) { + return -3; + } + + int err = -3; + + const uint16_t packed_len = pack_gc_topic_info(packed_topic, packet_buf_size, &chat->topic_info); + + if (packed_len != packet_buf_size) { + goto ON_ERROR; + } + + if (crypto_sign_detached(chat->topic_sig, nullptr, packed_topic, packet_buf_size, + get_sig_sk(chat->self_secret_key)) == -1) { + goto ON_ERROR; + } + + if (!broadcast_gc_topic(chat)) { + err = -4; + goto ON_ERROR; + } + + chat->topic_prev_checksum = old_topic_info.checksum; + chat->topic_time_set = mono_time_get(chat->mono_time); + + free(packed_topic); + return 0; + +ON_ERROR: + chat->topic_info = old_topic_info; + memcpy(chat->topic_sig, old_topic_sig, SIGNATURE_SIZE); + free(packed_topic); + return err; +} + +void gc_get_topic(const GC_Chat *chat, uint8_t *topic) +{ + if (topic != nullptr) { + memcpy(topic, chat->topic_info.topic, chat->topic_info.length); + } +} + +uint16_t gc_get_topic_size(const GC_Chat *chat) +{ + return chat->topic_info.length; +} + +/** + * If public_sig_key is equal to the key of the topic setter, replaces topic credentials + * and re-broadcasts the updated topic info to the group. + * + * Returns true on success + */ +non_null() +static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key) +{ + if (memcmp(public_sig_key, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) != 0) { + return true; + } + + return gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) == 0; +} + +/** @brief Validates `topic_info`. + * + * Return true if topic info is valid. + */ +non_null() +static bool handle_gc_topic_validate(const GC_Chat *chat, const GC_Peer *peer, const GC_TopicInfo *topic_info, + bool topic_lock_enabled) +{ + if (topic_info->checksum != get_gc_topic_checksum(topic_info)) { + LOGGER_WARNING(chat->log, "received invalid topic checksum"); + return false; + } + + if (topic_lock_enabled) { + if (!mod_list_verify_sig_pk(&chat->moderation, topic_info->public_sig_key)) { + LOGGER_DEBUG(chat->log, "Invalid topic signature (bad credentials)"); + return false; + } + + if (topic_info->version < chat->topic_info.version) { + return false; + } + } else { + uint8_t public_enc_key[ENC_PUBLIC_KEY_SIZE]; + + if (gc_get_enc_pk_from_sig_pk(chat, public_enc_key, topic_info->public_sig_key)) { + if (sanctions_list_is_observer(&chat->moderation, public_enc_key)) { + LOGGER_DEBUG(chat->log, "Invalid topic signature (sanctioned peer attempted to change topic)"); + return false; + } + } + + if (topic_info->version == chat->shared_state.topic_lock) { + // always accept topic on initial connection + if (!mono_time_is_timeout(chat->mono_time, chat->time_connected, GC_PING_TIMEOUT)) { + return true; + } + + if (chat->topic_prev_checksum == topic_info->checksum && + !mono_time_is_timeout(chat->mono_time, chat->topic_time_set, GC_CONFIRMED_PEER_TIMEOUT)) { + LOGGER_DEBUG(chat->log, "Topic reversion (probable sync error)"); + return false; + } + + return true; + } + + // the topic version should never change when the topic lock is disabled except when + // the founder changes the topic prior to enabling the lock + if (!(peer->role == GR_FOUNDER && topic_info->version == chat->shared_state.topic_lock + 1)) { + LOGGER_ERROR(chat->log, "topic version %u differs from topic lock %u", topic_info->version, + chat->shared_state.topic_lock); + return false; + } + } + + return true; +} + +/** @brief Handles a topic packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_topic(const GC_Session *c, GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < SIGNATURE_SIZE + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + const uint16_t old_checksum = chat->topic_info.checksum; + + GC_TopicInfo topic_info; + + if (unpack_gc_topic_info(&topic_info, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE) == -1) { + LOGGER_WARNING(chat->log, "failed to unpack topic"); + return 0; + } + + const uint8_t *signature = data; + + if (crypto_sign_verify_detached(signature, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE, + topic_info.public_sig_key) == -1) { + LOGGER_WARNING(chat->log, "failed to verify topic signature"); + return 0; + } + + const bool topic_lock_enabled = group_topic_lock_enabled(chat); + + if (!handle_gc_topic_validate(chat, peer, &topic_info, topic_lock_enabled)) { + return 0; + } + + // prevents sync issues from triggering the callback needlessly + const bool skip_callback = chat->topic_info.length == topic_info.length + && memcmp(chat->topic_info.topic, topic_info.topic, topic_info.length) == 0; + + chat->topic_prev_checksum = old_checksum; + chat->topic_time_set = mono_time_get(chat->mono_time); + chat->topic_info = topic_info; + memcpy(chat->topic_sig, signature, SIGNATURE_SIZE); + + if (!skip_callback && chat->connection_state == CS_CONNECTED && c->topic_change != nullptr) { + const int setter_peer_number = get_peer_number_of_sig_pk(chat, topic_info.public_sig_key); + const uint32_t peer_id = setter_peer_number >= 0 ? chat->group[setter_peer_number].peer_id : 0; + + c->topic_change(c->messenger, chat->group_number, peer_id, topic_info.topic, topic_info.length, userdata); + } + + return 0; +} + +/** @brief Handles a key exchange packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if length is invalid. + * Return -2 if we fail to create a new session keypair. + * Return -3 if response packet fails to send. + */ +non_null() +static int handle_gc_key_exchange(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < 1 + ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + bool is_response; + net_unpack_bool(&data[0], &is_response); + + const uint8_t *sender_public_session_key = data + 1; + + if (is_response) { + if (!gconn->pending_key_rotation_request) { + LOGGER_WARNING(chat->log, "got unsolicited key rotation response from peer %u", gconn->public_key_hash); + return 0; + } + + // now that we have response we can compute our new shared key and begin using it + gcc_make_session_shared_key(gconn, sender_public_session_key); + + gconn->pending_key_rotation_request = false; + + return 0; + } + + // key generation is pretty cpu intensive so we make sure a peer can't DOS us by spamming requests + if (!mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT / 2)) { + return 0; + } + + uint8_t response[1 + ENC_PUBLIC_KEY_SIZE]; + uint8_t new_session_pk[ENC_PUBLIC_KEY_SIZE]; + uint8_t new_session_sk[ENC_SECRET_KEY_SIZE]; + + net_pack_bool(&response[0], true); + + crypto_memlock(new_session_sk, sizeof(new_session_sk)); + + create_gc_session_keypair(chat->log, chat->rng, new_session_pk, new_session_sk); + + memcpy(response + 1, new_session_pk, ENC_PUBLIC_KEY_SIZE); + + if (!send_lossless_group_packet(chat, gconn, response, sizeof(response), GP_KEY_ROTATION)) { + return -3; + } + + // save new keys and compute new shared key AFTER sending response packet with old key + memcpy(gconn->session_public_key, new_session_pk, sizeof(gconn->session_public_key)); + memcpy(gconn->session_secret_key, new_session_sk, sizeof(gconn->session_secret_key)); + + gcc_make_session_shared_key(gconn, sender_public_session_key); + + crypto_memunlock(new_session_sk, sizeof(new_session_sk)); + + gconn->last_key_rotation = mono_time_get(chat->mono_time); + + return 0; +} + +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name) +{ + if (group_name != nullptr) { + memcpy(group_name, chat->shared_state.group_name, chat->shared_state.group_name_len); + } +} + +uint16_t gc_get_group_name_size(const GC_Chat *chat) +{ + return chat->shared_state.group_name_len; +} + +void gc_get_password(const GC_Chat *chat, uint8_t *password) +{ + if (password != nullptr) { + memcpy(password, chat->shared_state.password, chat->shared_state.password_length); + } +} + +uint16_t gc_get_password_size(const GC_Chat *chat) +{ + return chat->shared_state.password_length; +} + +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length) +{ + if (!self_gc_is_founder(chat)) { + return -1; + } + + uint8_t *oldpasswd = nullptr; + const uint16_t oldlen = chat->shared_state.password_length; + + if (oldlen > 0) { + oldpasswd = (uint8_t *)malloc(oldlen); + + if (oldpasswd == nullptr) { + return -4; + } + + memcpy(oldpasswd, chat->shared_state.password, oldlen); + } + + if (!set_gc_password_local(chat, password, password_length)) { + free(oldpasswd); + return -2; + } + + if (!sign_gc_shared_state(chat)) { + set_gc_password_local(chat, oldpasswd, oldlen); + free(oldpasswd); + return -2; + } + + free(oldpasswd); + + if (!broadcast_gc_shared_state(chat)) { + return -3; + } + + return 0; +} + +/** @brief Validates change to moderator list and either adds or removes peer from our moderator list. + * + * Return target's peer number on success. + * Return -1 on packet handle failure. + * Return -2 if target peer is not online. + * Return -3 if target peer is not a valid role (probably indicates sync issues). + * Return -4 on validation failure. + */ +non_null() +static int validate_unpack_gc_set_mod(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length, + bool add_mod) +{ + int target_peer_number; + uint8_t mod_data[MOD_LIST_ENTRY_SIZE]; + + if (add_mod) { + if (length < 1 + MOD_LIST_ENTRY_SIZE) { + return -1; + } + + memcpy(mod_data, data + 1, MOD_MODERATION_HASH_SIZE); + target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + + if (!gc_peer_number_is_valid(chat, target_peer_number)) { + return -2; + } + + const Group_Role target_role = chat->group[target_peer_number].role; + + if (target_role != GR_USER) { + return -3; + } + + if (!mod_list_add_entry(&chat->moderation, mod_data)) { + return -4; + } + } else { + memcpy(mod_data, data + 1, SIG_PUBLIC_KEY_SIZE); + target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + + if (!gc_peer_number_is_valid(chat, target_peer_number)) { + return -2; + } + + const Group_Role target_role = chat->group[target_peer_number].role; + + if (target_role != GR_MODERATOR) { + return -3; + } + + if (!mod_list_remove_entry(&chat->moderation, mod_data)) { + return -4; + } + } + + update_gc_peer_roles(chat); + + return target_peer_number; +} + +/** @brief Handles a moderator set broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_mod(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < 1 + SIG_PUBLIC_KEY_SIZE) { + return -1; + } + + const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + + if (setter_peer == nullptr) { + return -3; + } + + if (setter_peer->role != GR_FOUNDER) { + return 0; + } + + bool add_mod; + net_unpack_bool(&data[0], &add_mod); + + const int target_peer_number = validate_unpack_gc_set_mod(chat, peer_number, data, length, add_mod); + + if (target_peer_number == -1) { + return -2; + } + + const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer == nullptr) { + return 0; + } + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + add_mod ? MV_MOD : MV_USER, userdata); + } + + return 0; +} + +/** @brief Sends a set mod broadcast to the group. + * + * Return true on success. + */ +non_null() +static bool send_gc_set_mod(const GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ + const uint16_t length = 1 + SIG_PUBLIC_KEY_SIZE; + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + return false; + } + + net_pack_bool(&data[0], add_mod); + + memcpy(data + 1, get_sig_pk(gconn->addr.public_key), SIG_PUBLIC_KEY_SIZE); + + if (!send_gc_broadcast_message(chat, data, length, GM_SET_MOD)) { + free(data); + return false; + } + + free(data); + + return true; +} + +/** + * Adds or removes the peer designated by gconn from moderator list if `add_mod` is true or false respectively. + * Re-signs and re-distributes an updated mod_list hash. + * + * Returns true on success. + */ +non_null() +static bool founder_gc_set_moderator(GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ + if (!self_gc_is_founder(chat)) { + return false; + } + + if (add_mod) { + if (chat->moderation.num_mods >= MOD_MAX_NUM_MODERATORS) { + if (!prune_gc_mod_list(chat)) { + return false; + } + } + + if (!mod_list_add_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { + return false; + } + } else { + if (!mod_list_remove_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { + return false; + } + + if (!update_gc_sanctions_list(chat, get_sig_pk(gconn->addr.public_key)) + || !update_gc_topic(chat, get_sig_pk(gconn->addr.public_key))) { + return false; + } + } + + uint8_t old_hash[MOD_MODERATION_HASH_SIZE]; + memcpy(old_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); + + if (!mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash)) { + return false; + } + + if (!sign_gc_shared_state(chat) || !broadcast_gc_shared_state(chat)) { + memcpy(chat->shared_state.mod_list_hash, old_hash, MOD_MODERATION_HASH_SIZE); + return false; + } + + return send_gc_set_mod(chat, gconn, add_mod); +} + +/** @brief Validates `data` containing a change for the sanction list and unpacks it + * into the sanctions list for `chat`. + * + * if `add_obs` is true we're adding an observer to the list. + * + * Return 1 if sanctions list is not modified. + * Return 0 if data is valid and sanctions list is successfully modified. + * Return -1 if data is invalid format. + */ +non_null() +static int validate_unpack_observer_entry(GC_Chat *chat, const uint8_t *data, uint16_t length, + const uint8_t *public_key, bool add_obs) +{ + Mod_Sanction_Creds creds; + + if (add_obs) { + Mod_Sanction sanction; + + if (sanctions_list_unpack(&sanction, &creds, 1, data, length, nullptr) != 1) { + return -1; + } + + // this may occur if two mods change the sanctions list at the exact same time + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + return 1; + } + + if (sanctions_list_entry_exists(&chat->moderation, &sanction) + || !sanctions_list_add_entry(&chat->moderation, &sanction, &creds)) { + return -1; + } + } else { + if (length < MOD_SANCTIONS_CREDS_SIZE) { + return -1; + } + + if (sanctions_creds_unpack(&creds, data) != MOD_SANCTIONS_CREDS_SIZE) { + return -1; + } + + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + return 1; + } + + if (!sanctions_list_is_observer(&chat->moderation, public_key) + || !sanctions_list_remove_observer(&chat->moderation, public_key, &creds)) { + return 1; + } + } + + return 0; +} + +/** @brief Handles a set observer broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_observer(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length <= 1 + EXT_PUBLIC_KEY_SIZE) { + return -1; + } + + const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + + if (setter_peer == nullptr) { + return -3; + } + + if (setter_peer->role > GR_MODERATOR) { + LOGGER_DEBUG(chat->log, "peer with insufficient permissions tried to modify sanctions list"); + return 0; + } + + bool add_obs; + net_unpack_bool(&data[0], &add_obs); + + const uint8_t *public_key = data + 1; + + const int target_peer_number = get_peer_number_of_enc_pk(chat, public_key, false); + + if (target_peer_number >= 0 && (uint32_t)target_peer_number == peer_number) { + return -2; + } + + const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer != nullptr) { + if ((add_obs && target_peer->role != GR_USER) || (!add_obs && target_peer->role != GR_OBSERVER)) { + return 0; + } + } + + const int ret = validate_unpack_observer_entry(chat, + data + 1 + EXT_PUBLIC_KEY_SIZE, + length - 1 - EXT_PUBLIC_KEY_SIZE, + public_key, add_obs); + + if (ret == -1) { + return -2; + } + + + if (ret == 1) { + return 0; + } + + update_gc_peer_roles(chat); + + if (target_peer != nullptr) { + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + add_obs ? MV_OBSERVER : MV_USER, userdata); + } + } + + return 0; +} + +/** @brief Broadcasts observer role data to the group. + * + * Returns true on success. + */ +non_null() +static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, const uint8_t *sanction_data, + uint16_t length, bool add_obs) +{ + const uint16_t packet_len = 1 + EXT_PUBLIC_KEY_SIZE + length; + uint8_t *packet = (uint8_t *)malloc(packet_len); + + if (packet == nullptr) { + return false; + } + + net_pack_bool(&packet[0], add_obs); + + memcpy(packet + 1, target_ext_pk, EXT_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + EXT_PUBLIC_KEY_SIZE, sanction_data, length); + + if (!send_gc_broadcast_message(chat, packet, packet_len, GM_SET_OBSERVER)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** @brief Adds or removes peer_number from the observer list if add_obs is true or false respectively. + * Broadcasts this change to the entire group. + * + * Returns true on success. + */ +non_null() +static bool mod_gc_set_observer(GC_Chat *chat, uint32_t peer_number, bool add_obs) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (gc_get_self_role(chat) >= GR_USER) { + return false; + } + + uint8_t sanction_data[MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_CREDS_SIZE]; + uint16_t length = 0; + + if (add_obs) { + if (chat->moderation.num_sanctions >= MOD_MAX_NUM_SANCTIONS) { + if (!prune_gc_sanctions_list(chat)) { + return false; + } + } + + // if sanctioned peer set the topic we need to overwrite his signature and redistribute + // topic info + const int setter_peer_number = get_peer_number_of_sig_pk(chat, chat->topic_info.public_sig_key); + + if (setter_peer_number >= 0 && (uint32_t)setter_peer_number == peer_number) { + if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { + return false; + } + } + + Mod_Sanction sanction; + + if (!sanctions_list_make_entry(&chat->moderation, gconn->addr.public_key, &sanction, SA_OBSERVER)) { + LOGGER_WARNING(chat->log, "sanctions_list_make_entry failed in mod_gc_set_observer"); + return false; + } + + const int packed_len = sanctions_list_pack(sanction_data, sizeof(sanction_data), &sanction, 1, + &chat->moderation.sanctions_creds); + + if (packed_len == -1) { + return false; + } + + length += packed_len; + } else { + if (!sanctions_list_remove_observer(&chat->moderation, gconn->addr.public_key, nullptr)) { + LOGGER_WARNING(chat->log, "failed to remove sanction"); + return false; + } + + const uint16_t packed_len = sanctions_creds_pack(&chat->moderation.sanctions_creds, sanction_data); + + if (packed_len != MOD_SANCTIONS_CREDS_SIZE) { + return false; + } + + length += packed_len; + } + + if (length > sizeof(sanction_data)) { + LOGGER_FATAL(chat->log, "Invalid sanction data length: %u", length); + return false; + } + + update_gc_peer_roles(chat); + + return send_gc_set_observer(chat, gconn->addr.public_key, sanction_data, length, add_obs); +} + +/** @brief Sets the role of `peer_number` to `new_role`. If necessary this function will first + * remove the peer's current role before applying the new one. + * + * Return true on success. + */ +non_null() +static bool apply_new_gc_role(GC_Chat *chat, uint32_t peer_number, Group_Role current_role, Group_Role new_role) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + switch (current_role) { + case GR_MODERATOR: { + if (!founder_gc_set_moderator(chat, gconn, false)) { + return false; + } + + update_gc_peer_roles(chat); + + if (new_role == GR_OBSERVER) { + return mod_gc_set_observer(chat, peer_number, true); + } + + break; + } + + case GR_OBSERVER: { + if (!mod_gc_set_observer(chat, peer_number, false)) { + return false; + } + + update_gc_peer_roles(chat); + + if (new_role == GR_MODERATOR) { + return founder_gc_set_moderator(chat, gconn, true); + } + + break; + } + + case GR_USER: { + if (new_role == GR_MODERATOR) { + return founder_gc_set_moderator(chat, gconn, true); + } else if (new_role == GR_OBSERVER) { + return mod_gc_set_observer(chat, peer_number, true); + } + + break; + } + + case GR_FOUNDER: + + // Intentional fallthrough + default: { + return false; + } + } + + return true; +} + +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -2; + } + + const GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed) { + return -2; + } + + const Group_Role current_role = peer->role; + + if (new_role == GR_FOUNDER || peer->role == new_role) { + return -4; + } + + if (peer_number_is_self(peer_number)) { + return -6; + } + + if (current_role == GR_FOUNDER || gc_get_self_role(chat) >= GR_USER) { + return -3; + } + + // moderators can't demote moderators or promote peers to moderator + if (!self_gc_is_founder(chat) && (new_role == GR_MODERATOR || current_role == GR_MODERATOR)) { + return -3; + } + + if (!apply_new_gc_role(chat, peer_number, current_role, new_role)) { + return -5; + } + + update_gc_peer_roles(chat); + + return 0; +} + +/** @brief Return true if topic lock is enabled */ +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat) +{ + return chat->shared_state.topic_lock == GC_TOPIC_LOCK_ENABLED; +} + +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat) +{ + return chat->shared_state.privacy_state; +} + +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat) +{ + return group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; +} + +Group_Voice_State gc_get_voice_state(const GC_Chat *chat) +{ + return chat->shared_state.voice_state; +} + +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (new_lock_state > TL_DISABLED) { + return -2; + } + + if (!self_gc_is_founder(chat)) { + return -3; + } + + if (chat->connection_state <= CS_DISCONNECTED) { + return -4; + } + + const Group_Topic_Lock old_lock_state = gc_get_topic_lock_state(chat); + + if (new_lock_state == old_lock_state) { + return 0; + } + + const uint32_t old_topic_lock = chat->shared_state.topic_lock; + + // If we're enabling the lock the founder needs to sign the current topic and re-broadcast + // it with a new version. This needs to happen before we re-broadcast the shared state because + // if it fails we don't want to enable the topic lock with an invalid topic signature or version. + if (new_lock_state == TL_ENABLED) { + chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; + + if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { + chat->shared_state.topic_lock = old_topic_lock; + return -6; + } + } else { + chat->shared_state.topic_lock = chat->topic_info.version; + } + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.topic_lock = old_topic_lock; + return -5; + } + + if (!broadcast_gc_shared_state(chat)) { + return -6; + } + + return 0; +} + +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!self_gc_is_founder(chat)) { + return -2; + } + + if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { + return -3; + } + + const Group_Voice_State old_voice_state = chat->shared_state.voice_state; + + if (new_voice_state == old_voice_state) { + return 0; + } + + chat->shared_state.voice_state = new_voice_state; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.voice_state = old_voice_state; + return -4; + } + + if (!broadcast_gc_shared_state(chat)) { + return -5; + } + + return 0; +} + +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!self_gc_is_founder(chat)) { + return -2; + } + + if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { + return -3; + } + + const Group_Privacy_State old_privacy_state = chat->shared_state.privacy_state; + + if (new_privacy_state == old_privacy_state) { + return 0; + } + + chat->shared_state.privacy_state = new_privacy_state; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.privacy_state = old_privacy_state; + return -4; + } + + if (new_privacy_state == GI_PRIVATE) { + cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); + kill_group_friend_connection(c, chat); + chat->join_type = HJ_PRIVATE; + } else { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } else { + chat->update_self_announces = true; + chat->join_type = HJ_PUBLIC; + } + } + + if (!broadcast_gc_shared_state(chat)) { + return -5; + } + + return 0; +} + +uint16_t gc_get_max_peers(const GC_Chat *chat) +{ + return chat->shared_state.maxpeers; +} + +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers) +{ + if (!self_gc_is_founder(chat)) { + return -1; + } + + const uint16_t old_maxpeers = chat->shared_state.maxpeers; + + if (max_peers == chat->shared_state.maxpeers) { + return 0; + } + + chat->shared_state.maxpeers = max_peers; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.maxpeers = old_maxpeers; + return -2; + } + + if (!broadcast_gc_shared_state(chat)) { + return -3; + } + + return 0; +} + +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, uint32_t *message_id) +{ + if (length > MAX_GC_MESSAGE_SIZE) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + if (type != GC_MESSAGE_TYPE_NORMAL && type != GC_MESSAGE_TYPE_ACTION) { + return -3; + } + + const GC_Peer *self = get_gc_peer(chat, 0); + assert(self != nullptr); + + if (gc_get_self_role(chat) >= GR_OBSERVER || !peer_has_voice(self, chat->shared_state.voice_state)) { + return -4; + } + + const uint8_t packet_type = type == GC_MESSAGE_TYPE_NORMAL ? GM_PLAIN_MESSAGE : GM_ACTION_MESSAGE; + + const uint16_t length_raw = length + GC_MESSAGE_PSEUDO_ID_SIZE; + uint8_t *message_raw = (uint8_t *)malloc(length_raw); + + if (message_raw == nullptr) { + return -5; + } + + const uint32_t pseudo_msg_id = random_u32(chat->rng); + + net_pack_u32(message_raw, pseudo_msg_id); + memcpy(message_raw + GC_MESSAGE_PSEUDO_ID_SIZE, message, length); + + if (!send_gc_broadcast_message(chat, message_raw, length_raw, packet_type)) { + free(message_raw); + return -5; + } + + if (message_id != nullptr) { + *message_id = pseudo_msg_id; + } + + free(message_raw); + return 0; +} + +/** @brief Handles a message broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, uint8_t type, void *userdata) +{ + if (data == nullptr || length > MAX_GC_MESSAGE_RAW_SIZE || length <= GC_MESSAGE_PSEUDO_ID_SIZE) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER || !peer_has_voice(peer, chat->shared_state.voice_state)) { + return 0; + } + + if (type != GM_PLAIN_MESSAGE && type != GM_ACTION_MESSAGE) { + LOGGER_WARNING(chat->log, "received invalid message type: %u", type); + return 0; + } + + const uint8_t cb_type = (type == GM_PLAIN_MESSAGE) ? MESSAGE_NORMAL : MESSAGE_ACTION; + + uint32_t pseudo_msg_id; + net_unpack_u32(data, &pseudo_msg_id); + + if (c->message != nullptr) { + c->message(c->messenger, chat->group_number, peer->peer_id, cb_type, data + GC_MESSAGE_PSEUDO_ID_SIZE, + length - GC_MESSAGE_PSEUDO_ID_SIZE, pseudo_msg_id, userdata); + } + + return 0; +} + +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, + uint16_t length) +{ + if (length > MAX_GC_MESSAGE_SIZE) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (type > MESSAGE_ACTION) { + return -4; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -5; + } + + uint8_t *message_with_type = (uint8_t *)malloc(length + 1); + + if (message_with_type == nullptr) { + return -6; + } + + message_with_type[0] = type; + memcpy(message_with_type + 1, message, length); + + uint8_t *packet = (uint8_t *)malloc(length + 1 + GC_BROADCAST_ENC_HEADER_SIZE); + + if (packet == nullptr) { + free(message_with_type); + return -6; + } + + const uint16_t packet_len = make_gc_broadcast_header(message_with_type, length + 1, packet, GM_PRIVATE_MESSAGE); + + free(message_with_type); + + if (!send_lossless_group_packet(chat, gconn, packet, packet_len, GP_BROADCAST)) { + free(packet); + return -6; + } + + free(packet); + + return 0; +} + +/** @brief Handles a private message. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_private_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (data == nullptr || length > MAX_GC_MESSAGE_SIZE || length <= 1) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + const uint8_t message_type = data[0]; + + if (message_type > MESSAGE_ACTION) { + LOGGER_WARNING(chat->log, "Received invalid private message type: %u", message_type); + return 0; + } + + if (c->private_message != nullptr) { + c->private_message(c->messenger, chat->group_number, peer->peer_id, message_type, data + 1, length - 1, userdata); + } + + return 0; +} + +/** @brief Returns false if a custom packet is too large. */ +static bool custom_gc_packet_length_is_valid(uint16_t length, bool lossless) +{ + if (lossless) { + if (length > MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE) { + return false; + } + } else { + if (length > MAX_GC_CUSTOM_LOSSY_PACKET_SIZE) { + return false; + } + } + + return true; +} + +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, + uint16_t length) +{ + if (!custom_gc_packet_length_is_valid(length, lossless)) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -4; + } + + bool ret; + + if (lossless) { + ret = send_lossless_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); + } else { + ret = send_lossy_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); + } + + return ret ? 0 : -5; +} + + + +/** @brief Handles a custom private packet. + * + * @retval 0 if packet is handled correctly. + * @retval -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_custom_private_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, + const uint8_t *data, uint16_t length, bool lossless, void *userdata) +{ + if (!custom_gc_packet_length_is_valid(length, lossless)) { + return -1; + } + + if (data == nullptr || length == 0) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + if (c->custom_private_packet != nullptr) { + c->custom_private_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); + } + + return 0; +} + +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length) +{ + if (!custom_gc_packet_length_is_valid(length, lossless)) { + return -1; + } + + if (data == nullptr || length == 0) { + return -2; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -3; + } + + if (lossless) { + send_gc_lossless_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); + } else { + send_gc_lossy_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); + } + + return 0; +} + +/** @brief Handles a custom packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_custom_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, bool lossless, void *userdata) +{ + if (!custom_gc_packet_length_is_valid(length, lossless)) { + return -1; + } + + if (data == nullptr || length == 0) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + if (c->custom_packet != nullptr) { + c->custom_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); + } + + return 0; +} + +/** @brief Handles a peer kick broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_kick_peer(const GC_Session *c, GC_Chat *chat, const GC_Peer *setter_peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + if (setter_peer->role >= GR_USER) { + return 0; + } + + const uint8_t *target_pk = data; + + const int target_peer_number = get_peer_number_of_enc_pk(chat, target_pk, false); + GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer != nullptr) { + if (target_peer->role != GR_USER) { + return 0; + } + } + + if (peer_number_is_self(target_peer_number)) { + assert(target_peer != nullptr); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + chat->connection_state = CS_DISCONNECTED; + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + MV_KICK, userdata); + } + + return 0; + } + + if (target_peer == nullptr) { /** we don't need to/can't kick a peer that isn't in our peerlist */ + return 0; + } + + gcc_mark_for_deletion(&target_peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_KICKED, nullptr, 0); + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, MV_KICK, userdata); + } + + return 0; +} + +/** @brief Sends a packet to instruct all peers to remove gconn from their peerlist. + * + * Returns true on success. + */ +non_null() +static bool send_gc_kick_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ + uint8_t packet[ENC_PUBLIC_KEY_SIZE]; + memcpy(packet, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return send_gc_broadcast_message(chat, packet, ENC_PUBLIC_KEY_SIZE, GM_KICK_PEER); +} + +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + if (peer_number_is_self(peer_number)) { + return -6; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -2; + } + + if (gc_get_self_role(chat) >= GR_USER || peer->role == GR_FOUNDER) { + return -3; + } + + if (!self_gc_is_founder(chat) && peer->role == GR_MODERATOR) { + return -3; + } + + if (peer->role == GR_MODERATOR || peer->role == GR_OBSERVER) { + // this first removes peer from any lists they're on and broadcasts new lists to group + if (gc_set_peer_role(c->messenger, chat->group_number, peer_id, GR_USER) < 0) { + return -4; + } + } + + if (!send_gc_kick_peer(chat, &peer->gconn)) { + return -5; + } + + gcc_mark_for_deletion(&peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_NO_CALLBACK, nullptr, 0); + + return 0; +} + +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type) +{ + if (gconn->pending_delete) { + return true; + } + + if (type == GR_ACK_REQ) { + const uint64_t tm = mono_time_get(chat->mono_time); + + if (gconn->last_requested_packet_time == tm) { + return true; + } + + gconn->last_requested_packet_time = tm; + } else if (type != GR_ACK_RECV) { + return false; + } + + uint8_t data[GC_LOSSLESS_ACK_PACKET_SIZE]; + data[0] = (uint8_t) type; + net_pack_u64(data + 1, message_id); + + return send_lossy_group_packet(chat, gconn, data, GC_LOSSLESS_ACK_PACKET_SIZE, GP_MESSAGE_ACK); +} + +/** @brief Handles a lossless message acknowledgement. + * + * If the type is GR_ACK_RECV we remove the packet from our + * send array. If the type is GR_ACK_REQ we re-send the packet + * associated with the requested message_id. + * + * Returns 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if we failed to handle the ack (may be caused by connection issues). + * Return -3 if we failed to re-send a requested packet. + */ +non_null() +static int handle_gc_message_ack(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < GC_LOSSLESS_ACK_PACKET_SIZE) { + return -1; + } + + uint64_t message_id; + net_unpack_u64(data + 1, &message_id); + + const Group_Message_Ack_Type type = (Group_Message_Ack_Type) data[0]; + + if (type == GR_ACK_RECV) { + if (!gcc_handle_ack(chat->log, gconn, message_id)) { + return -2; + } + + return 0; + } + + if (type != GR_ACK_REQ) { + return 0; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + const uint16_t idx = gcc_get_array_index(message_id); + + /* re-send requested packet */ + if (gconn->send_array[idx].message_id == message_id) { + if (gcc_encrypt_and_send_lossless_packet(chat, gconn, gconn->send_array[idx].data, + gconn->send_array[idx].data_length, + gconn->send_array[idx].message_id, + gconn->send_array[idx].packet_type)) { + gconn->send_array[idx].last_send_try = tm; + LOGGER_DEBUG(chat->log, "Re-sent requested packet %llu", (unsigned long long)message_id); + } else { + return -3; + } + } + + return 0; +} + +/** @brief Sends a handshake response ack to peer. + * + * Return true on success. + */ +non_null() +static bool send_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_HS_RESPONSE_ACK); +} + +/** @brief Handles a handshake response ack. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to respond with an invite request. + */ +non_null() +static int handle_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ + gconn->handshaked = true; // has to be true before we can send a lossless packet + + if (!send_gc_invite_request(chat, gconn)) { + gconn->handshaked = false; + return -1; + } + + return 0; +} + +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + if (peer_number_is_self(peer_number)) { + return -2; + } + + peer->ignore = ignore; + + return 0; +} + +/** @brief Handles a broadcast packet. + * + * Returns 0 if packet is handled correctly. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_broadcast(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < GC_BROADCAST_ENC_HEADER_SIZE) { + return -1; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed) { + return -1; + } + + const uint8_t broadcast_type = data[0]; + + const uint16_t m_len = length - 1; + const uint8_t *message = data + 1; + + int ret = 0; + + switch (broadcast_type) { + case GM_STATUS: { + ret = handle_gc_status(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_NICK: { + ret = handle_gc_nick(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_ACTION_MESSAGE: + + // intentional fallthrough + case GM_PLAIN_MESSAGE: { + ret = handle_gc_message(c, chat, peer, message, m_len, broadcast_type, userdata); + break; + } + + case GM_PRIVATE_MESSAGE: { + ret = handle_gc_private_message(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_PEER_EXIT: { + handle_gc_peer_exit(chat, gconn, message, m_len); + ret = 0; + break; + } + + case GM_KICK_PEER: { + ret = handle_gc_kick_peer(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_SET_MOD: { + ret = handle_gc_set_mod(c, chat, peer_number, message, m_len, userdata); + break; + } + + case GM_SET_OBSERVER: { + ret = handle_gc_set_observer(c, chat, peer_number, message, m_len, userdata); + break; + } + + default: { + LOGGER_DEBUG(chat->log, "Received an invalid broadcast type 0x%02x", broadcast_type); + break; + } + } + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Broadcast handle error %d: type: 0x%02x, peernumber: %u", + ret, broadcast_type, peer_number); + return -1; + } + + return 0; +} + +/** @brief Decrypts data of size `length` using self secret key and sender's public key. + * + * The packet payload should begin with a nonce. + * + * Returns length of plaintext data on success. + * Return -1 if length is invalid. + * Return -2 if decryption fails. + */ +non_null() +static int unwrap_group_handshake_packet(const Logger *log, const uint8_t *self_sk, const uint8_t *sender_pk, + uint8_t *plain, size_t plain_size, const uint8_t *packet, uint16_t length) +{ + if (length <= CRYPTO_NONCE_SIZE) { + LOGGER_FATAL(log, "Invalid handshake packet length %u", length); + return -1; + } + + const int plain_len = decrypt_data(sender_pk, self_sk, packet, packet + CRYPTO_NONCE_SIZE, + length - CRYPTO_NONCE_SIZE, plain); + + if (plain_len < 0 || (uint32_t)plain_len != plain_size) { + LOGGER_DEBUG(log, "decrypt handshake request failed: len: %d, size: %zu", plain_len, plain_size); + return -2; + } + + return plain_len; +} + +/** @brief Encrypts data of length using the peer's shared key a new nonce. + * + * Adds plaintext header consisting of: packet identifier, target public encryption key, + * self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if packet size is invalid. + * Return -2 on malloc failure. + * Return -3 if encryption fails. + */ +non_null() +static int wrap_group_handshake_packet( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *self_sk, + const uint8_t *target_pk, uint8_t *packet, uint32_t packet_size, + const uint8_t *data, uint16_t length) +{ + if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { + LOGGER_FATAL(log, "Invalid packet size: %u", packet_size); + return -1; + } + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(rng, nonce); + + const size_t encrypt_buf_size = length + CRYPTO_MAC_SIZE; + uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + + if (encrypt == nullptr) { + return -2; + } + + const int enc_len = encrypt_data(target_pk, self_sk, nonce, data, length, encrypt); + + if (enc_len < 0 || (size_t)enc_len != encrypt_buf_size) { + LOGGER_ERROR(log, "Failed to encrypt group handshake packet (len: %d)", enc_len); + free(encrypt); + return -3; + } + + packet[0] = NET_PACKET_GC_HANDSHAKE; + memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, target_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + + free(encrypt); + + return 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Makes, wraps and encrypts a group handshake packet (both request and response are the same format). + * + * Packet contains the packet header, handshake type, self public encryption key, self public signature key, + * request type, and a single TCP relay node. + * + * Returns length of encrypted packet on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_handshake_packet(const GC_Chat *chat, const GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type, uint8_t *packet, size_t packet_size, + const Node_format *node) +{ + if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { + LOGGER_FATAL(chat->log, "invalid packet size: %zu", packet_size); + return -1; + } + + if (chat == nullptr || gconn == nullptr || node == nullptr) { + return -1; + } + + uint8_t data[GC_MIN_HS_PACKET_PAYLOAD_SIZE + sizeof(Node_format)]; + + uint16_t length = sizeof(uint8_t); + + data[0] = handshake_type; + memcpy(data + length, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + memcpy(data + length, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + length += SIG_PUBLIC_KEY_SIZE; + memcpy(data + length, &request_type, sizeof(uint8_t)); + length += sizeof(uint8_t); + memcpy(data + length, &join_type, sizeof(uint8_t)); + length += sizeof(uint8_t); + + int nodes_size = pack_nodes(chat->log, data + length, sizeof(Node_format), node, MAX_SENT_GC_NODES); + + if (nodes_size > 0) { + length += nodes_size; + } else { + nodes_size = 0; + } + + const int enc_len = wrap_group_handshake_packet( + chat->log, chat->rng, chat->self_public_key, chat->self_secret_key, + gconn->addr.public_key, packet, (uint16_t)packet_size, data, length); + + if (enc_len != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + nodes_size) { + LOGGER_WARNING(chat->log, "Failed to wrap handshake packet: %d", enc_len); + return -1; + } + + return enc_len; +} + +/** @brief Sends a handshake packet to `gconn`. + * + * Handshake_type should be GH_REQUEST or GH_RESPONSE. + * + * Returns true on success. + */ +non_null() +static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type) +{ + if (gconn == nullptr) { + return false; + } + + Node_format node; + memset(&node, 0, sizeof(node)); + + if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) { + LOGGER_TRACE(chat->log, "Failed to copy TCP relay during handshake (%u TCP relays)", gconn->tcp_relays_count); + } + + uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; + const int length = make_gc_handshake_packet(chat, gconn, handshake_type, request_type, join_type, packet, + sizeof(packet), &node); + + if (length < 0) { + return false; + } + + const bool try_tcp_fallback = gconn->handshake_attempts % 2 == 1 && gconn->tcp_relays_count > 0; + ++gconn->handshake_attempts; + + int ret = -1; + + if (!try_tcp_fallback && gcc_direct_conn_is_possible(chat, gconn)) { + ret = sendpacket(chat->net, &gconn->addr.ip_port, packet, (uint16_t)length); + } + + if (ret != length && gconn->tcp_relays_count == 0) { + LOGGER_WARNING(chat->log, "UDP handshake failed and no TCP relays to fall back on"); + return false; + } + + // Send a TCP handshake if UDP fails, or if UDP succeeded last time but we never got a response + if (gconn->tcp_relays_count > 0 && (ret != length || try_tcp_fallback)) { + if (send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, (uint16_t)length) == -1) { + LOGGER_DEBUG(chat->log, "Send handshake packet failed. Type 0x%02x", request_type); + return false; + } + } + + if (gconn->is_pending_handshake_response) { + gcc_set_send_message_id(gconn, 3); // handshake response is always second packet + } else { + gcc_set_send_message_id(gconn, 2); // handshake request is always first packet + } + + return true; +} + +/** @brief Sends an out-of-band TCP handshake request packet to `gconn`. + * + * Return true on success. + */ +static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn) +{ + if (gconn == nullptr) { + return false; + } + + Node_format node; + memset(&node, 0, sizeof(node)); + + if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) { + LOGGER_WARNING(chat->log, "Failed to copy TCP relay"); + return false; + } + + uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; + const int length = make_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type, + packet, sizeof(packet), &node); + + if (length < 0) { + LOGGER_WARNING(chat->log, "Failed to make handshake packet"); + return false; + } + + return tcp_send_oob_packet_using_relay(chat->tcp_conn, gconn->oob_relay_pk, gconn->addr.public_key, + packet, (uint16_t)length) == 0; +} + +/** @brief Handles a handshake response packet and takes appropriate action depending on the value of request_type. + * + * This function assumes the length has already been validated. + * + * Returns peer_number of new connected peer on success. + * Returns -1 on failure. + */ +non_null() +static int handle_gc_handshake_response(const GC_Chat *chat, const uint8_t *sender_pk, const uint8_t *data, + uint16_t length) +{ + // this should be checked at lower level; this is a redundant defense check. Ideally we should + // guarantee that this can never happen in the future. + if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1) { + LOGGER_FATAL(chat->log, "Invalid handshake response size (%u)", length); + return -1; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + if (peer_number == -1) { + return -1; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + const uint8_t *sender_session_pk = data; + + gcc_make_session_shared_key(gconn, sender_session_pk); + + set_sig_pk(gconn->addr.public_key, data + ENC_PUBLIC_KEY_SIZE); + + gcc_set_recv_message_id(gconn, 2); // handshake response is always second packet + + gconn->handshaked = true; + + send_gc_hs_response_ack(chat, gconn); + + const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + + switch (request_type) { + case HS_INVITE_REQUEST: { + if (!send_gc_invite_request(chat, gconn)) { + return -1; + } + + break; + } + + case HS_PEER_INFO_EXCHANGE: { + if (!send_gc_peer_exchange(chat, gconn)) { + return -1; + } + + break; + } + + default: { + return -1; + } + } + + return peer_number; +} + +/** @brief Sends a handshake response packet of type `request_type` to `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_handshake_response(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_gc_handshake_packet(chat, gconn, GH_RESPONSE, gconn->pending_handshake_type, 0); +} + +/** @brief Handles handshake request packets. + * + * Peer is added to peerlist and a lossless connection is established. + * + * This function assumes the length has already been validated. + * + * Return new peer's peer_number on success. + * Return -1 on failure. + */ +#define GC_NEW_PEER_CONNECTION_LIMIT 10 +non_null(1, 3, 4) nullable(2) +static int handle_gc_handshake_request(GC_Chat *chat, const IP_Port *ipp, const uint8_t *sender_pk, + const uint8_t *data, uint16_t length) +{ + // this should be checked at lower level; this is a redundant defense check. Ideally we should + // guarantee that this can never happen in the future. + if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) { + LOGGER_FATAL(chat->log, "Invalid length (%u)", length); + return -1; + } + + if (chat->connection_state <= CS_DISCONNECTED) { + LOGGER_DEBUG(chat->log, "Handshake request ignored; state is disconnected"); + return -1; + } + + if (chat->connection_O_metre >= GC_NEW_PEER_CONNECTION_LIMIT) { + chat->block_handshakes = true; + LOGGER_DEBUG(chat->log, "Handshake overflow. Blocking handshakes."); + return -1; + } + + ++chat->connection_O_metre; + + const uint8_t *public_sig_key = data + ENC_PUBLIC_KEY_SIZE; + + const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + const uint8_t join_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1]; + + int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + const bool is_new_peer = peer_number < 0; + + if (is_new_peer) { + peer_number = peer_add(chat, ipp, sender_pk); + + if (peer_number < 0) { + LOGGER_WARNING(chat->log, "Failed to add peer during handshake request"); + return -1; + } + } else { + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + LOGGER_WARNING(chat->log, "Invalid peer number"); + return -1; + } + + if (gconn->handshaked) { + gconn->handshaked = false; + LOGGER_DEBUG(chat->log, "Handshaked peer sent a handshake request"); + return -1; + } + + // peers sent handshake request at same time so the closer peer becomes the requestor + // and ignores the request packet while further peer continues on with the response + if (gconn->self_is_closer) { + LOGGER_DEBUG(chat->log, "Simultaneous handshake requests; other peer is closer"); + return 0; + } + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + LOGGER_DEBUG(chat->log, "Peer connection invalid"); + return -1; + } + + gcc_set_ip_port(gconn, ipp); + + Node_format node[GCA_MAX_ANNOUNCED_TCP_RELAYS]; + const int processed = ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1; + + const int nodes_count = unpack_nodes(node, GCA_MAX_ANNOUNCED_TCP_RELAYS, nullptr, + data + processed, length - processed, true); + + if (nodes_count <= 0 && ipp == nullptr) { + if (is_new_peer) { + LOGGER_WARNING(chat->log, "Broken tcp relay for new peer"); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + } + + return -1; + } + + if (nodes_count > 0) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &node->ip_port, node->public_key); + + if (add_tcp_result < 0 && is_new_peer && ipp == nullptr) { + LOGGER_WARNING(chat->log, "Broken tcp relay for new peer"); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + return -1; + } + + if (add_tcp_result == 0) { + gcc_save_tcp_relay(chat->rng, gconn, node); + } + } + + const uint8_t *sender_session_pk = data; + + gcc_make_session_shared_key(gconn, sender_session_pk); + + set_sig_pk(gconn->addr.public_key, public_sig_key); + + if (join_type == HJ_PUBLIC && !is_public_chat(chat)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + LOGGER_DEBUG(chat->log, "Ignoring invalid invite request"); + return -1; + } + + gcc_set_recv_message_id(gconn, 1); // handshake request is always first packet + + gconn->is_pending_handshake_response = true; + gconn->pending_handshake_type = request_type; + + return peer_number; +} + +/** @brief Handles handshake request and handshake response packets. + * + * Returns the peer_number of the connecting peer on success. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(3, 7) +static int handle_gc_handshake_packet(GC_Chat *chat, const uint8_t *sender_pk, const IP_Port *ipp, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + return -1; + } + + const size_t data_buf_size = length - CRYPTO_NONCE_SIZE - CRYPTO_MAC_SIZE; + uint8_t *data = (uint8_t *)malloc(data_buf_size); + + if (data == nullptr) { + return -1; + } + + const int plain_len = unwrap_group_handshake_packet(chat->log, chat->self_secret_key, sender_pk, data, + data_buf_size, packet, length); + + if (plain_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE) { + LOGGER_DEBUG(chat->log, "Failed to unwrap handshake packet (probably a stale request using an old key)"); + free(data); + return -1; + } + + const uint8_t handshake_type = data[0]; + + const uint8_t *real_data = data + 1; + const uint16_t real_len = (uint16_t)plain_len - 1; + + int peer_number; + + if (handshake_type == GH_REQUEST) { + peer_number = handle_gc_handshake_request(chat, ipp, sender_pk, real_data, real_len); + } else if (handshake_type == GH_RESPONSE) { + peer_number = handle_gc_handshake_response(chat, sender_pk, real_data, real_len); + } else { + free(data); + return -1; + } + + free(data); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (direct_conn) { + gconn->last_received_direct_time = mono_time_get(chat->mono_time); + } + + return peer_number; +} + +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, uint8_t packet_type, void *userdata) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + GC_Connection *gconn = &peer->gconn; + + int ret; + + switch (packet_type) { + case GP_BROADCAST: { + ret = handle_gc_broadcast(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_PEER_INFO_REQUEST: { + ret = handle_gc_peer_info_request(chat, peer_number); + break; + } + + case GP_PEER_INFO_RESPONSE: { + ret = handle_gc_peer_info_response(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_SYNC_REQUEST: { + ret = handle_gc_sync_request(chat, peer_number, data, length); + break; + } + + case GP_SYNC_RESPONSE: { + ret = handle_gc_sync_response(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_INVITE_REQUEST: { + ret = handle_gc_invite_request(chat, peer_number, data, length); + break; + } + + case GP_INVITE_RESPONSE: { + ret = handle_gc_invite_response(chat, gconn); + break; + } + + case GP_TOPIC: { + ret = handle_gc_topic(c, chat, peer, data, length, userdata); + break; + } + + case GP_SHARED_STATE: { + ret = handle_gc_shared_state(c, chat, gconn, data, length, userdata); + break; + } + + case GP_MOD_LIST: { + ret = handle_gc_mod_list(c, chat, data, length, userdata); + break; + } + + case GP_SANCTIONS_LIST: { + ret = handle_gc_sanctions_list(c, chat, data, length, userdata); + break; + } + + case GP_HS_RESPONSE_ACK: { + ret = handle_gc_hs_response_ack(chat, gconn); + break; + } + + case GP_TCP_RELAYS: { + ret = handle_gc_tcp_relays(chat, gconn, data, length); + break; + } + + case GP_KEY_ROTATION: { + ret = handle_gc_key_exchange(chat, gconn, data, length); + break; + } + + case GP_CUSTOM_PACKET: { + ret = handle_gc_custom_packet(c, chat, peer, data, length, true, userdata); + break; + } + + case GP_CUSTOM_PRIVATE_PACKET: { + ret = handle_gc_custom_private_packet(c, chat, peer, data, length, true, userdata); + break; + } + + default: { + LOGGER_DEBUG(chat->log, "Handling invalid lossless group packet type 0x%02x", packet_type); + return false; + } + } + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Lossless packet handle error %d: type: 0x%02x, peernumber: %d", + ret, packet_type, peer_number); + return false; + } + + peer = get_gc_peer(chat, peer_number); + + if (peer != nullptr) { + peer->gconn.last_requested_packet_time = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Handles a packet fragment. + * + * If the fragment is the last one in a sequence we send an ack. Otherwise we + * store the fragment in the receive array and wait for the next segment. + * + * Segments must be processed in correct sequence, and we cannot handle + * non-fragment packets while a sequence is incomplete. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 4) nullable(5, 9) +static bool handle_gc_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + void *userdata) +{ + if (gconn->last_chunk_id != 0 && message_id != gconn->last_chunk_id + 1) { + return gc_send_message_ack(chat, gconn, gconn->last_chunk_id + 1, GR_ACK_REQ); + } + + if (gconn->last_chunk_id == 0 && message_id != gconn->received_message_id + 1) { + return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); + } + + const int frag_ret = gcc_handle_packet_fragment(c, chat, peer_number, gconn, data, length, packet_type, + message_id, userdata); + + if (frag_ret == -1) { + return false; + } + + if (frag_ret == 0) { + gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + gconn->last_received_packet_time = mono_time_get(chat->mono_time); + + return true; +} + +/** @brief Handles lossless groupchat packets. + * + * This function assumes the length has already been validated. + * + * Returns true if packet is successfully handled. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossless_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_LOSSLESS_PAYLOAD_SIZE) { + return false; + } + + int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (gconn->pending_delete) { + return true; + } + + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + LOGGER_DEBUG(chat->log, "Failed to allocate memory for packet data buffer"); + return false; + } + + uint8_t packet_type; + uint64_t message_id; + + const int len = group_packet_unwrap(chat->log, gconn, data, &message_id, &packet_type, packet, length); + + if (len < 0) { + Ip_Ntoa ip_str; + LOGGER_DEBUG(chat->log, "Failed to unwrap lossless packet from %s:%d: %d", + net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len); + free(data); + return false; + } + + if (!gconn->handshaked && (packet_type != GP_HS_RESPONSE_ACK && packet_type != GP_INVITE_REQUEST)) { + LOGGER_DEBUG(chat->log, "Got lossless packet type 0x%02x from unconfirmed peer", packet_type); + free(data); + return false; + } + + const bool is_invite_packet = packet_type == GP_INVITE_REQUEST || packet_type == GP_INVITE_RESPONSE + || packet_type == GP_INVITE_RESPONSE_REJECT; + + if (message_id == 3 && is_invite_packet && gconn->received_message_id <= 1) { + // we missed initial handshake request. Drop this packet and wait for another handshake request. + LOGGER_DEBUG(chat->log, "Missed handshake packet, type: 0x%02x", packet_type); + free(data); + return false; + } + + const int lossless_ret = gcc_handle_received_message(chat->log, chat->mono_time, gconn, data, (uint16_t) len, + packet_type, message_id, direct_conn); + + if (packet_type == GP_INVITE_REQUEST && !gconn->handshaked) { // Both peers sent request at same time + free(data); + return true; + } + + if (lossless_ret < 0) { + LOGGER_DEBUG(chat->log, "failed to handle packet %llu (type: 0x%02x, id: %llu)", + (unsigned long long)message_id, packet_type, (unsigned long long)message_id); + free(data); + return false; + } + + /* Duplicate packet */ + if (lossless_ret == 0) { + free(data); + return gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + /* request missing packet */ + if (lossless_ret == 1) { + LOGGER_TRACE(chat->log, "received out of order packet from peer %u. expected %llu, got %llu", peer_number, + (unsigned long long)gconn->received_message_id + 1, (unsigned long long)message_id); + free(data); + return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); + } + + /* handle packet fragment */ + if (lossless_ret == 3) { + const bool frag_ret = handle_gc_packet_fragment(c, chat, peer_number, gconn, data, (uint16_t)len, packet_type, + message_id, userdata); + free(data); + return frag_ret; + } + + const bool ret = handle_gc_lossless_helper(c, chat, peer_number, data, (uint16_t)len, packet_type, userdata); + + free(data); + + if (!ret) { + return false; + } + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + if (gconn != nullptr && lossless_ret == 2) { + gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + return true; +} + +/** @brief Handles lossy groupchat message packets. + * + * This function assumes the length has already been validated. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossy_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_LOSSY_PAYLOAD_SIZE) { + return false; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->handshaked || gconn->pending_delete) { + LOGGER_DEBUG(chat->log, "Got lossy packet from invalid peer"); + return false; + } + + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); + return false; + } + + uint8_t packet_type; + + const int len = group_packet_unwrap(chat->log, gconn, data, nullptr, &packet_type, packet, length); + + if (len <= 0) { + Ip_Ntoa ip_str; + LOGGER_DEBUG(chat->log, "Failed to unwrap lossy packet from %s:%d: %d", + net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len); + free(data); + return false; + } + + int ret = -1; + const uint16_t payload_len = (uint16_t)len; + + switch (packet_type) { + case GP_MESSAGE_ACK: { + ret = handle_gc_message_ack(chat, gconn, data, payload_len); + break; + } + + case GP_PING: { + ret = handle_gc_ping(chat, gconn, data, payload_len); + break; + } + + case GP_INVITE_RESPONSE_REJECT: { + ret = handle_gc_invite_response_reject(c, chat, data, payload_len, userdata); + break; + } + + case GP_CUSTOM_PACKET: { + ret = handle_gc_custom_packet(c, chat, peer, data, payload_len, false, userdata); + break; + } + + case GP_CUSTOM_PRIVATE_PACKET: { + ret = handle_gc_custom_private_packet(c, chat, peer, data, payload_len, false, userdata); + break; + } + + default: { + LOGGER_WARNING(chat->log, "Warning: handling invalid lossy group packet type 0x%02x", packet_type); + free(data); + return false; + } + } + + free(data); + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Lossy packet handle error %d: type: 0x%02x, peernumber %d", ret, packet_type, + peer_number); + return false; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + if (direct_conn) { + gconn->last_received_direct_time = tm; + } + + gconn->last_received_packet_time = tm; + + return true; +} + +/** @brief Return true if group is either connected or attempting to connect. */ +non_null() +static bool group_can_handle_packets(const GC_Chat *chat) +{ + const GC_Conn_State state = chat->connection_state; + return state == CS_CONNECTING || state == CS_CONNECTED; +} + +/** @brief Sends a group packet to appropriate handler function. + * + * Returns non-negative value on success. + * Returns -1 on failure. + */ +#define MIN_TCP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 3) nullable(5) +static int handle_gc_tcp_packet(void *object, int id, const uint8_t *packet, uint16_t length, void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= MIN_TCP_PACKET_SIZE) { + LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, + MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { + LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, + MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + const uint8_t packet_type = packet[0]; + + const uint8_t *sender_pk = packet + 1; + + const GC_Session *c = m->group_handler; + GC_Chat *chat = nullptr; + + if (packet_type == NET_PACKET_GC_HANDSHAKE) { + chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + } else { + chat = get_chat_by_id(c, sender_pk); + } + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; + uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; + + switch (packet_type) { + case NET_PACKET_GC_LOSSLESS: { + if (!handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { + return -1; + } + + return 0; + } + + case NET_PACKET_GC_LOSSY: { + if (!handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { + return -1; + } + + return 0; + } + + case NET_PACKET_GC_HANDSHAKE: { + // handshake packets have an extra public key in plaintext header + if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { + return -1; + } + + payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; + payload = payload + ENC_PUBLIC_KEY_SIZE; + + return handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata); + } + + default: { + return -1; + } + } +} + +non_null(1, 2, 4) nullable(6) +static int handle_gc_tcp_oob_packet(void *object, const uint8_t *public_key, unsigned int tcp_connections_number, + const uint8_t *packet, uint16_t length, void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= GC_MIN_HS_PACKET_PAYLOAD_SIZE) { + LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, + GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, + GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); + return -1; + } + + const GC_Session *c = m->group_handler; + GC_Chat *chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t packet_type = packet[0]; + + if (packet_type != NET_PACKET_GC_HANDSHAKE) { + return -1; + } + + const uint8_t *sender_pk = packet + 1; + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE; + const uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE - ENC_PUBLIC_KEY_SIZE; + + if (payload_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + return -1; + } + + if (handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata) == -1) { + return -1; + } + + return 0; +} + +#define MIN_UDP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 2, 3) nullable(5) +static int handle_gc_udp_packet(void *object, const IP_Port *ipp, const uint8_t *packet, uint16_t length, + void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= MIN_UDP_PACKET_SIZE) { + LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, + MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { + LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, + MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + const uint8_t packet_type = packet[0]; + const uint8_t *sender_pk = packet + 1; + + const GC_Session *c = m->group_handler; + GC_Chat *chat = nullptr; + + if (packet_type == NET_PACKET_GC_HANDSHAKE) { + chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + } else { + chat = get_chat_by_id(c, sender_pk); + } + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; + uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; + bool ret = false; + + switch (packet_type) { + case NET_PACKET_GC_LOSSLESS: { + ret = handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, true, userdata); + break; + } + + case NET_PACKET_GC_LOSSY: { + ret = handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, true, userdata); + break; + } + + case NET_PACKET_GC_HANDSHAKE: { + // handshake packets have an extra public key in plaintext header + if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { + return -1; + } + + payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; + payload = payload + ENC_PUBLIC_KEY_SIZE; + + ret = handle_gc_handshake_packet(chat, sender_pk, ipp, payload, payload_len, true, userdata) != -1; + break; + } + + default: { + return -1; + } + } + + return ret ? 0 : -1; +} + +void gc_callback_message(const Messenger *m, gc_message_cb *function) +{ + GC_Session *c = m->group_handler; + c->message = function; +} + +void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function) +{ + GC_Session *c = m->group_handler; + c->private_message = function; +} + +void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function) +{ + GC_Session *c = m->group_handler; + c->custom_packet = function; +} + +void gc_callback_custom_private_packet(const Messenger *m, gc_custom_private_packet_cb *function) +{ + GC_Session *c = m->group_handler; + c->custom_private_packet = function; +} + +void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function) +{ + GC_Session *c = m->group_handler; + c->moderation = function; +} + +void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->nick_change = function; +} + +void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->status_change = function; +} + +void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->topic_change = function; +} + +void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function) +{ + GC_Session *c = m->group_handler; + c->topic_lock = function; +} + +void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function) +{ + GC_Session *c = m->group_handler; + c->voice_state = function; +} + +void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_limit = function; +} + +void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function) +{ + GC_Session *c = m->group_handler; + c->privacy_state = function; +} + +void gc_callback_password(const Messenger *m, gc_password_cb *function) +{ + GC_Session *c = m->group_handler; + c->password = function; +} + +void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_join = function; +} + +void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_exit = function; +} + +void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function) +{ + GC_Session *c = m->group_handler; + c->self_join = function; +} + +void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function) +{ + GC_Session *c = m->group_handler; + c->rejected = function; +} + +/** @brief Deletes peer_number from group. + * + * `no_callback` should be set to true if the `peer_exit` callback + * should not be triggered. + * + * Return true on success. + */ +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + // We need to save some peer info for the callback before deleting it + const bool peer_confirmed = peer->gconn.confirmed; + const uint32_t peer_id = peer->peer_id; + uint8_t nick[MAX_GC_NICK_SIZE]; + const uint16_t nick_length = peer->nick_length; + const GC_Exit_Info exit_info = peer->gconn.exit_info; + + assert(nick_length <= MAX_GC_NICK_SIZE); + memcpy(nick, peer->nick, nick_length); + + gcc_peer_cleanup(&peer->gconn); + + --chat->numpeers; + + if (chat->numpeers != peer_number) { + chat->group[peer_number] = chat->group[chat->numpeers]; + } + + chat->group[chat->numpeers] = (GC_Peer) { + 0 + }; + + GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, chat->numpeers * sizeof(GC_Peer)); + + if (tmp_group == nullptr) { + return false; + } + + chat->group = tmp_group; + + set_gc_peerlist_checksum(chat); + + if (peer_confirmed) { + refresh_gc_saved_peers(chat); + } + + if (exit_info.exit_type != GC_EXIT_TYPE_NO_CALLBACK && c->peer_exit != nullptr && peer_confirmed) { + c->peer_exit(c->messenger, chat->group_number, peer_id, exit_info.exit_type, nick, + nick_length, exit_info.part_message, exit_info.length, userdata); + } + + return true; +} + +/** @brief Updates peer_number with info from `peer` and validates peer data. + * + * Returns peer_number on success. + * Returns -1 on failure. + */ +static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number) +{ + if (peer->nick_length == 0) { + return -1; + } + + if (peer->status > GS_BUSY) { + return -1; + } + + if (peer->role > GR_OBSERVER) { + return -1; + } + + GC_Peer *curr_peer = get_gc_peer(chat, peer_number); + assert(curr_peer != nullptr); + + curr_peer->status = peer->status; + curr_peer->nick_length = peer->nick_length; + + memcpy(curr_peer->nick, peer->nick, peer->nick_length); + + return peer_number; +} + +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key) +{ + if (get_peer_number_of_enc_pk(chat, public_key, false) != -1) { + return -2; + } + + const uint32_t peer_id = get_new_peer_id(chat); + + if (peer_id == UINT32_MAX) { + LOGGER_WARNING(chat->log, "Failed to add peer: all peer ID's are taken?"); + return -1; + } + + const int peer_number = chat->numpeers; + int tcp_connection_num = -1; + + if (peer_number > 0) { // we don't need a connection to ourself + tcp_connection_num = new_tcp_connection_to(chat->tcp_conn, public_key, 0); + + if (tcp_connection_num == -1) { + LOGGER_WARNING(chat->log, "Failed to init tcp connection for peer %d", peer_number); + } + } + + GC_Message_Array_Entry *send = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); + GC_Message_Array_Entry *recv = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); + + if (send == nullptr || recv == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for gconn buffers"); + + if (tcp_connection_num != -1) { + kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); + } + + free(send); + free(recv); + return -1; + } + + GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, (chat->numpeers + 1) * sizeof(GC_Peer)); + + if (tmp_group == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for group realloc"); + + if (tcp_connection_num != -1) { + kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); + } + + free(send); + free(recv); + return -1; + } + + ++chat->numpeers; + chat->group = tmp_group; + + chat->group[peer_number] = (GC_Peer) { + 0 + }; + + GC_Connection *gconn = &chat->group[peer_number].gconn; + + gconn->send_array = send; + gconn->recv_array = recv; + + gcc_set_ip_port(gconn, ipp); + chat->group[peer_number].role = GR_USER; + chat->group[peer_number].peer_id = peer_id; + chat->group[peer_number].ignore = false; + + crypto_memlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); + + create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + + if (peer_number > 0) { + memcpy(gconn->addr.public_key, public_key, ENC_PUBLIC_KEY_SIZE); // we get the sig key in the handshake + } else { + memcpy(gconn->addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + gcc_set_send_message_id(gconn, 1); + gconn->public_key_hash = gc_get_pk_jenkins_hash(public_key); + gconn->last_received_packet_time = tm; + gconn->last_key_rotation = tm; + gconn->tcp_connection_num = tcp_connection_num; + gconn->last_sent_ip_time = tm; + gconn->last_sent_ping_time = tm - (GC_PING_TIMEOUT / 2) + (peer_number % (GC_PING_TIMEOUT / 2)); + gconn->self_is_closer = id_closest(get_chat_id(chat->chat_public_key), + get_enc_key(chat->self_public_key), + get_enc_key(gconn->addr.public_key)) == 1; + return peer_number; +} + +/** @brief Copies own peer data to `peer`. */ +non_null() +static void copy_self(const GC_Chat *chat, GC_Peer *peer) +{ + *peer = (GC_Peer) { + 0 + }; + + peer->status = gc_get_self_status(chat); + gc_get_self_nick(chat, peer->nick); + peer->nick_length = gc_get_self_nick_size(chat); + peer->role = gc_get_self_role(chat); +} + +/** @brief Returns true if we haven't received a ping from this peer after n seconds. + * n depends on whether or not the peer has been confirmed. + */ +non_null() +static bool peer_timed_out(const Mono_Time *mono_time, const GC_Connection *gconn) +{ + return mono_time_is_timeout(mono_time, gconn->last_received_packet_time, gconn->confirmed + ? GC_CONFIRMED_PEER_TIMEOUT + : GC_UNCONFIRMED_PEER_TIMEOUT); +} + +/** @brief Attempts to send pending handshake packets to peer designated by `gconn`. + * + * One request of each type can be sent per `GC_SEND_HANDSHAKE_INTERVAL` seconds. + * + * Return true on success. + */ +non_null() +static bool send_pending_handshake(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat == nullptr || gconn == nullptr) { + return false; + } + + if (gconn->is_pending_handshake_response) { + if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_response, GC_SEND_HANDSHAKE_INTERVAL)) { + return true; + } + + gconn->last_handshake_response = mono_time_get(chat->mono_time); + + return send_gc_handshake_response(chat, gconn); + } + + if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_request, GC_SEND_HANDSHAKE_INTERVAL)) { + return true; + } + + gconn->last_handshake_request = mono_time_get(chat->mono_time); + + if (gconn->is_oob_handshake) { + return send_gc_oob_handshake_request(chat, gconn); + } + + return send_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type); +} + +#define GC_TCP_RELAY_SEND_INTERVAL (60 * 3) +non_null(1, 2) nullable(3) +static void do_peer_connections(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->pending_delete) { + continue; + } + + if (peer_timed_out(chat->mono_time, gconn)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + continue; + } + + gcc_resend_packets(chat, gconn); + + if (gconn->tcp_relays_count > 0 && + mono_time_is_timeout(chat->mono_time, gconn->last_sent_tcp_relays_time, GC_TCP_RELAY_SEND_INTERVAL)) { + if (gconn->confirmed) { + send_gc_tcp_relays(chat, gconn); + gconn->last_sent_tcp_relays_time = mono_time_get(chat->mono_time); + } + } + + gcc_check_recv_array(c, chat, gconn, i, userdata); // may change peer numbers + } +} + +/** @brief Executes pending handshakes for peers. + * + * If our peerlist is empty we periodically try to + * load peers from our saved peers list and initiate handshake requests with them. + */ +#define LOAD_PEERS_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT + 10) +non_null() +static void do_handshakes(GC_Chat *chat) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->handshaked || gconn->pending_delete) { + continue; + } + + send_pending_handshake(chat, gconn); + } + + if (chat->numpeers <= 1) { + const uint64_t tm = mono_time_get(chat->mono_time); + + if (mono_time_is_timeout(chat->mono_time, chat->last_time_peers_loaded, LOAD_PEERS_TIMEOUT)) { + load_gc_peers(chat, chat->saved_peers, GC_MAX_SAVED_PEERS); + chat->last_time_peers_loaded = tm; + } + } +} + +/** @brief Adds `gconn` to the group timeout list. */ +non_null() +static void add_gc_peer_timeout_list(GC_Chat *chat, const GC_Connection *gconn) +{ + const size_t idx = chat->timeout_list_index; + const uint64_t tm = mono_time_get(chat->mono_time); + + copy_gc_saved_peer(chat->rng, gconn, &chat->timeout_list[idx].addr); + + chat->timeout_list[idx].last_seen = tm; + chat->timeout_list[idx].last_reconn_try = 0; + chat->timeout_list_index = (idx + 1) % MAX_GC_SAVED_TIMEOUTS; +} + +non_null(1, 2) nullable(3) +static void do_peer_delete(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->pending_delete) { + const GC_Exit_Info *exit_info = &gconn->exit_info; + + if (exit_info->exit_type == GC_EXIT_TYPE_TIMEOUT && gconn->confirmed) { + add_gc_peer_timeout_list(chat, gconn); + } + + if (!peer_delete(c, chat, i, userdata)) { + LOGGER_ERROR(chat->log, "Failed to delete peer %u", i); + } + + if (i >= chat->numpeers) { + break; + } + } + } +} + +/** @brief Constructs and sends a ping packet to `gconn` containing info needed for group syncing + * and connection maintenance. + * + * Return true on success. + */ +non_null() +static bool ping_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ + const uint16_t buf_size = GC_PING_PACKET_MIN_DATA_SIZE + sizeof(IP_Port); + uint8_t *data = (uint8_t *)malloc(buf_size); + + if (data == nullptr) { + return false; + } + + const uint16_t roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; + uint16_t packed_len = 0; + + net_pack_u16(data, chat->peers_checksum); + packed_len += sizeof(uint16_t); + + net_pack_u16(data + packed_len, get_gc_confirmed_numpeers(chat)); + packed_len += sizeof(uint16_t); + + net_pack_u32(data + packed_len, chat->shared_state.version); + packed_len += sizeof(uint32_t); + + net_pack_u32(data + packed_len, chat->moderation.sanctions_creds.version); + packed_len += sizeof(uint32_t); + + net_pack_u16(data + packed_len, roles_checksum); + packed_len += sizeof(uint16_t); + + net_pack_u32(data + packed_len, chat->topic_info.version); + packed_len += sizeof(uint32_t); + + net_pack_u16(data + packed_len, chat->topic_info.checksum); + packed_len += sizeof(uint16_t); + + if (packed_len != GC_PING_PACKET_MIN_DATA_SIZE) { + LOGGER_FATAL(chat->log, "Packed length is impossible"); + } + + if (chat->self_udp_status == SELF_UDP_STATUS_WAN && !gcc_conn_is_direct(chat->mono_time, gconn) + && mono_time_is_timeout(chat->mono_time, gconn->last_sent_ip_time, GC_SEND_IP_PORT_INTERVAL)) { + + const int packed_ipp_len = pack_ip_port(chat->log, data + buf_size - sizeof(IP_Port), sizeof(IP_Port), + &chat->self_ip_port); + + if (packed_ipp_len > 0) { + packed_len += packed_ipp_len; + } + } + + if (!send_lossy_group_packet(chat, gconn, data, packed_len, GP_PING)) { + free(data); + return true; + } + + free(data); + + return false; +} + +/** + * Sends a ping packet to peers that haven't been pinged in at least GC_PING_TIMEOUT seconds, and + * a key rotation request to peers with whom we haven't refreshed keys in at least GC_KEY_ROTATION_TIMEOUT + * seconds. + * + * Ping packet always includes your confirmed peer count, a peer list checksum, your shared state and sanctions + * list version for syncing purposes. We also occasionally try to send our own IP info to peers that we + * do not have a direct connection with. + */ +#define GC_DO_PINGS_INTERVAL 2 +non_null() +static void do_gc_ping_and_key_rotation(GC_Chat *chat) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_ping_interval, GC_DO_PINGS_INTERVAL)) { + return; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (!gconn->confirmed) { + continue; + } + + if (mono_time_is_timeout(chat->mono_time, gconn->last_sent_ping_time, GC_PING_TIMEOUT)) { + if (ping_peer(chat, gconn)) { + gconn->last_sent_ping_time = tm; + } + } + + if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT)) { + if (send_peer_key_rotation_request(chat, gconn)) { + gconn->last_key_rotation = tm; + } + } + } + + chat->last_ping_interval = tm; +} + +non_null() +static void do_new_connection_cooldown(GC_Chat *chat) +{ + if (chat->connection_O_metre == 0) { + return; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + if (chat->connection_cooldown_timer < tm) { + chat->connection_cooldown_timer = tm; + --chat->connection_O_metre; + + if (chat->connection_O_metre == 0 && chat->block_handshakes) { + chat->block_handshakes = false; + LOGGER_DEBUG(chat->log, "Unblocking handshakes"); + } + } +} + +#define TCP_RELAYS_CHECK_INTERVAL 10 +non_null(1, 2) nullable(3) +static void do_gc_tcp(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + if (chat->tcp_conn == nullptr || !group_can_handle_packets(chat)) { + return; + } + + do_tcp_connections(chat->log, chat->tcp_conn, userdata); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + const bool tcp_set = !gcc_conn_is_direct(chat->mono_time, gconn); + set_tcp_connection_to_status(chat->tcp_conn, gconn->tcp_connection_num, tcp_set); + } + + if (mono_time_is_timeout(chat->mono_time, chat->last_checked_tcp_relays, TCP_RELAYS_CHECK_INTERVAL) + && tcp_connected_relays_count(chat->tcp_conn) != chat->connected_tcp_relays) { + add_tcp_relays_to_chat(c, chat); + chat->connected_tcp_relays = tcp_connected_relays_count(chat->tcp_conn); + chat->last_checked_tcp_relays = mono_time_get(chat->mono_time); + } +} + +/** + * Updates our TCP and UDP connection status and flags a new announcement if our connection has + * changed and we have either a UDP or TCP connection. + */ +#define GC_SELF_CONNECTION_CHECK_INTERVAL 5 // how often in seconds we should run this function +#define GC_SELF_REFRESH_ANNOUNCE_INTERVAL (60 * 20) // how often in seconds we force refresh our group announcement +non_null() +static void do_self_connection(const GC_Session *c, GC_Chat *chat) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_self_announce_check, GC_SELF_CONNECTION_CHECK_INTERVAL)) { + return; + } + + const unsigned int self_udp_status = ipport_self_copy(c->messenger->dht, &chat->self_ip_port); + const bool udp_change = (chat->self_udp_status != self_udp_status) && (self_udp_status != SELF_UDP_STATUS_NONE); + + // We flag a group announce if our UDP status has changed since last run, or if our last announced TCP + // relay is no longer valid. Additionally, we will always flag an announce in the specified interval + // regardless of the prior conditions. Private groups are never announced. + if (is_public_chat(chat) && + ((udp_change || !tcp_relay_is_valid(chat->tcp_conn, chat->announced_tcp_relay_pk)) + || mono_time_is_timeout(chat->mono_time, chat->last_time_self_announce, GC_SELF_REFRESH_ANNOUNCE_INTERVAL))) { + chat->update_self_announces = true; + } + + chat->self_udp_status = (Self_UDP_Status) self_udp_status; + chat->last_self_announce_check = mono_time_get(chat->mono_time); +} + +/** @brief Attempts to initiate a new connection with peers in the timeout list. + * + * This function is not used for public groups as the DHT and group sync mechanism + * should automatically do this for us. + */ +#define TIMED_OUT_RECONN_INTERVAL 2 +non_null() +static void do_timed_out_reconn(GC_Chat *chat) +{ + if (is_public_chat(chat)) { + return; + } + + if (!mono_time_is_timeout(chat->mono_time, chat->last_timed_out_reconn_try, TIMED_OUT_RECONN_INTERVAL)) { + return; + } + + const uint64_t curr_time = mono_time_get(chat->mono_time); + + for (size_t i = 0; i < MAX_GC_SAVED_TIMEOUTS; ++i) { + GC_TimedOutPeer *timeout = &chat->timeout_list[i]; + + if (timeout->last_seen == 0 || timeout->last_seen == curr_time) { + continue; + } + + if (mono_time_is_timeout(chat->mono_time, timeout->last_seen, GC_TIMED_OUT_STALE_TIMEOUT) + || get_peer_number_of_enc_pk(chat, timeout->addr.public_key, true) != -1) { + *timeout = (GC_TimedOutPeer) { + {{ + 0 + } + } + }; + continue; + } + + if (mono_time_is_timeout(chat->mono_time, timeout->last_reconn_try, GC_TIMED_OUT_RECONN_TIMEOUT)) { + if (load_gc_peers(chat, &timeout->addr, 1) != 1) { + LOGGER_WARNING(chat->log, "Failed to load timed out peer"); + } + + timeout->last_reconn_try = curr_time; + } + } + + chat->last_timed_out_reconn_try = curr_time; +} + +void do_gc(GC_Session *c, void *userdata) +{ + if (c == nullptr) { + return; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + const GC_Conn_State state = chat->connection_state; + + if (state == CS_NONE) { + continue; + } + + if (state != CS_DISCONNECTED) { + do_peer_connections(c, chat, userdata); + do_gc_tcp(c, chat, userdata); + do_handshakes(chat); + do_self_connection(c, chat); + } + + if (chat->connection_state == CS_CONNECTED) { + do_gc_ping_and_key_rotation(chat); + do_timed_out_reconn(chat); + } + + do_new_connection_cooldown(chat); + do_peer_delete(c, chat, userdata); + } +} + +/** @brief Set the size of the groupchat list to n. + * + * Return true on success. + */ +non_null() +static bool realloc_groupchats(GC_Session *c, uint32_t n) +{ + if (n == 0) { + free(c->chats); + c->chats = nullptr; + return true; + } + + GC_Chat *temp = (GC_Chat *)realloc(c->chats, n * sizeof(GC_Chat)); + + if (temp == nullptr) { + return false; + } + + c->chats = temp; + return true; +} + +non_null() +static int get_new_group_index(GC_Session *c) +{ + if (c == nullptr) { + return -1; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + if (c->chats[i].connection_state == CS_NONE) { + return i; + } + } + + if (!realloc_groupchats(c, c->chats_index + 1)) { + return -1; + } + + const int new_index = c->chats_index; + + c->chats[new_index] = empty_gc_chat; + + memset(&c->chats[new_index].saved_invites, -1, sizeof(c->chats[new_index].saved_invites)); + + ++c->chats_index; + + return new_index; +} + +/** Attempts to associate new TCP relays with our group connection. */ +static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat) +{ + const Messenger *m = c->messenger; + + const uint32_t num_relays = tcp_connections_count(nc_get_tcp_c(m->net_crypto)); + + if (num_relays == 0) { + return; + } + + Node_format *tcp_relays = (Node_format *)calloc(num_relays, sizeof(Node_format)); + + if (tcp_relays == nullptr) { + return; + } + + const uint32_t num_copied = tcp_copy_connected_relays(nc_get_tcp_c(m->net_crypto), tcp_relays, (uint16_t)num_relays); + + for (uint32_t i = 0; i < num_copied; ++i) { + add_tcp_relay_global(chat->tcp_conn, &tcp_relays[i].ip_port, tcp_relays[i].public_key); + } + + free(tcp_relays); +} + +non_null() +static bool init_gc_tcp_connection(const GC_Session *c, GC_Chat *chat) +{ + const Messenger *m = c->messenger; + + chat->tcp_conn = new_tcp_connections(chat->log, chat->rng, m->ns, chat->mono_time, chat->self_secret_key, + &m->options.proxy_info); + + if (chat->tcp_conn == nullptr) { + return false; + } + + add_tcp_relays_to_chat(c, chat); + + set_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_packet, c->messenger); + set_oob_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_oob_packet, c->messenger); + + return true; +} + +/** Initializes default shared state values. */ +non_null() +static void init_gc_shared_state(GC_Chat *chat, const Group_Privacy_State privacy_state) +{ + chat->shared_state.maxpeers = MAX_GC_PEERS_DEFAULT; + chat->shared_state.privacy_state = privacy_state; + chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; + chat->shared_state.voice_state = GV_ALL; +} + +/** @brief Initializes the group shared state for the founder. + * + * Return true on success. + */ +non_null() +static bool init_gc_shared_state_founder(GC_Chat *chat, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t name_length) +{ + memcpy(chat->shared_state.founder_public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + memcpy(chat->shared_state.group_name, group_name, name_length); + chat->shared_state.group_name_len = name_length; + chat->shared_state.privacy_state = privacy_state; + + return sign_gc_shared_state(chat); +} + +/** @brief Initializes shared state for moderation object. + * + * This must be called before any moderation + * or sanctions related operations. + */ +non_null() +static void init_gc_moderation(GC_Chat *chat) +{ + memcpy(chat->moderation.founder_public_sig_key, + get_sig_pk(chat->shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); + memcpy(chat->moderation.self_public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + memcpy(chat->moderation.self_secret_sig_key, get_sig_pk(chat->self_secret_key), SIG_SECRET_KEY_SIZE); + chat->moderation.shared_state_version = chat->shared_state.version; + chat->moderation.log = chat->log; +} + +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat); + +non_null() +static int create_new_group(GC_Session *c, const uint8_t *nick, size_t nick_length, bool founder, + const Group_Privacy_State privacy_state) +{ + if (nick == nullptr || nick_length == 0) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -1; + } + + const int group_number = get_new_group_index(c); + + if (group_number == -1) { + return -1; + } + + Messenger *m = c->messenger; + GC_Chat *chat = &c->chats[group_number]; + + chat->log = m->log; + chat->rng = m->rng; + + const uint64_t tm = mono_time_get(m->mono_time); + + chat->group_number = group_number; + chat->numpeers = 0; + chat->connection_state = CS_CONNECTING; + chat->net = m->net; + chat->mono_time = m->mono_time; + chat->last_ping_interval = tm; + chat->friend_connection_id = -1; + + if (!create_new_chat_ext_keypair(chat)) { + LOGGER_ERROR(chat->log, "Failed to create extended keypair"); + group_delete(c, chat); + return -1; + } + + if (!init_gc_tcp_connection(c, chat)) { + group_delete(c, chat); + return -1; + } + + if (peer_add(chat, nullptr, chat->self_public_key) != 0) { /* you are always peer_number/index 0 */ + group_delete(c, chat); + return -1; + } + + if (!self_gc_set_nick(chat, nick, (uint16_t)nick_length)) { + group_delete(c, chat); + return -1; + } + + self_gc_set_status(chat, GS_NONE); + self_gc_set_role(chat, founder ? GR_FOUNDER : GR_USER); + self_gc_set_confirmed(chat, true); + self_gc_set_ext_public_key(chat, chat->self_public_key); + + init_gc_shared_state(chat, privacy_state); + init_gc_moderation(chat); + + return group_number; +} + +/** @brief Inits the sanctions list credentials. + * + * This should be called by the group founder on creation. + * + * This function must be called after `init_gc_moderation()`. + * + * Return true on success. + */ +non_null() +static bool init_gc_sanctions_creds(GC_Chat *chat) +{ + return sanctions_list_make_creds(&chat->moderation); +} + +/** @brief Attempts to add `num_addrs` peers from `addrs` to our peerlist and initiate invite requests + * for all of them. + * + * Returns the number of peers successfully loaded. + */ +static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs) +{ + size_t count = 0; + + for (size_t i = 0; i < num_addrs; ++i) { + if (!saved_peer_is_valid(&addrs[i])) { + continue; + } + + const bool ip_port_is_set = ipport_isset(&addrs[i].ip_port); + const IP_Port *ip_port = ip_port_is_set ? &addrs[i].ip_port : nullptr; + + const int peer_number = peer_add(chat, ip_port, addrs[i].public_key); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + continue; + } + + add_tcp_relay_global(chat->tcp_conn, &addrs[i].tcp_relay.ip_port, addrs[i].tcp_relay.public_key); + + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &addrs[i].tcp_relay.ip_port, + addrs[i].tcp_relay.public_key); + + if (add_tcp_result == -1 && !ip_port_is_set) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + continue; + } + + if (add_tcp_result == 0) { + const int save_tcp_result = gcc_save_tcp_relay(chat->rng, gconn, &addrs[i].tcp_relay); + + if (save_tcp_result == -1) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + continue; + } + + memcpy(gconn->oob_relay_pk, addrs[i].tcp_relay.public_key, CRYPTO_PUBLIC_KEY_SIZE); + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + gconn->is_oob_handshake = !gcc_direct_conn_is_possible(chat, gconn); + gconn->is_pending_handshake_response = false; + gconn->pending_handshake_type = HS_INVITE_REQUEST; + gconn->last_received_packet_time = tm; + gconn->last_key_rotation = tm; + + ++count; + } + + update_gc_peer_roles(chat); + + return count; +} + +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp) +{ + gc_save_pack_group(chat, bp); +} + +int gc_group_load(GC_Session *c, Bin_Unpack *bu) +{ + const int group_number = get_new_group_index(c); + + if (group_number < 0) { + return -1; + } + + const uint64_t tm = mono_time_get(c->messenger->mono_time); + + Messenger *m = c->messenger; + GC_Chat *chat = &c->chats[group_number]; + + chat->group_number = group_number; + chat->numpeers = 0; + chat->net = m->net; + chat->mono_time = m->mono_time; + chat->log = m->log; + chat->rng = m->rng; + chat->last_ping_interval = tm; + chat->friend_connection_id = -1; + + if (!gc_load_unpack_group(chat, bu)) { + LOGGER_ERROR(chat->log, "Failed to unpack group"); + return -1; + } + + init_gc_moderation(chat); + + if (!init_gc_tcp_connection(c, chat)) { + LOGGER_ERROR(chat->log, "Failed to init tcp connection"); + return -1; + } + + if (chat->connection_state == CS_DISCONNECTED) { + return group_number; + } + + if (is_public_chat(chat)) { + if (!m_create_group_connection(m, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } + } + + return group_number; +} + +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t group_name_length, + const uint8_t *nick, size_t nick_length) +{ + if (group_name_length > MAX_GC_GROUP_NAME_SIZE) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -1; + } + + if (group_name_length == 0 || group_name == nullptr) { + return -2; + } + + if (nick_length == 0 || nick == nullptr) { + return -2; + } + + const int group_number = create_new_group(c, nick, nick_length, true, privacy_state); + + if (group_number == -1) { + return -3; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -3; + } + + crypto_memlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); + + create_extended_keypair(chat->chat_public_key, chat->chat_secret_key); + + if (!init_gc_shared_state_founder(chat, privacy_state, group_name, group_name_length)) { + group_delete(c, chat); + return -4; + } + + init_gc_moderation(chat); + + if (!init_gc_sanctions_creds(chat)) { + group_delete(c, chat); + return -4; + } + + if (gc_set_topic(chat, nullptr, 0) != 0) { + group_delete(c, chat); + return -4; + } + + chat->join_type = HJ_PRIVATE; + chat->connection_state = CS_CONNECTED; + chat->time_connected = mono_time_get(c->messenger->mono_time); + + if (is_public_chat(chat)) { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + group_delete(c, chat); + return -5; + } + + chat->join_type = HJ_PUBLIC; + } + + update_gc_peer_roles(chat); + + return group_number; +} + +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, + uint16_t passwd_len) +{ + if (chat_id == nullptr || group_exists(c, chat_id) || getfriend_id(c->messenger, chat_id) != -1) { + return -2; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -3; + } + + if (nick == nullptr || nick_length == 0) { + return -4; + } + + const int group_number = create_new_group(c, nick, nick_length, false, GI_PUBLIC); + + if (group_number == -1) { + return -1; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!expand_chat_id(chat->chat_public_key, chat_id)) { + group_delete(c, chat); + return -1; + } + + chat->connection_state = CS_CONNECTING; + + if (passwd != nullptr && passwd_len > 0) { + if (!set_gc_password_local(chat, passwd, passwd_len)) { + group_delete(c, chat); + return -5; + } + } + + if (!m_create_group_connection(c->messenger, chat)) { + group_delete(c, chat); + return -6; + } + + update_gc_peer_roles(chat); + + return group_number; +} + +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + return false; + } + + chat->connection_state = CS_DISCONNECTED; + + send_gc_broadcast_message(chat, nullptr, 0, GM_PEER_EXIT); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + return true; +} + +int gc_rejoin_group(GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + return -1; + } + + chat->time_connected = 0; + + if (group_can_handle_packets(chat)) { + send_gc_self_exit(chat, nullptr, 0); + } + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + if (is_public_chat(chat)) { + kill_group_friend_connection(c, chat); + + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_WARNING(chat->log, "Failed to create new messenger connection for group"); + return -2; + } + + chat->update_self_announces = true; + } + + chat->connection_state = CS_CONNECTING; + + return 0; +} + +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length) +{ + if (length < CHAT_ID_SIZE) { + return false; + } + + return !group_exists(c, chat_id); +} + +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, + gc_send_group_invite_packet_cb *callback) +{ + if (!friend_is_valid(c->messenger, friend_number)) { + return -1; + } + + const uint16_t group_name_length = chat->shared_state.group_name_len; + + assert(group_name_length <= MAX_GC_GROUP_NAME_SIZE); + + uint8_t *packet = (uint8_t *)malloc(2 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE + group_name_length); + + if (packet == nullptr) { + return -1; + } + + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE; + + memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + uint16_t length = 2 + CHAT_ID_SIZE; + + memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + + + memcpy(packet + length, chat->shared_state.group_name, group_name_length); + length += group_name_length; + + assert(length <= MAX_GC_PACKET_SIZE); + + if (!callback(c->messenger, friend_number, packet, length)) { + free(packet); + return -2; + } + + free(packet); + + chat->saved_invites[chat->saved_invites_index] = friend_number; + chat->saved_invites_index = (chat->saved_invites_index + 1) % MAX_GC_SAVED_INVITES; + + return 0; +} + +/** @brief Sends an invite accepted packet to `friend_number`. + * + * Return 0 on success. + * Return -1 if `friend_number` does not designate a valid friend. + * Return -2 if `chat `is null. + * Return -3 if packet failed to send. + */ +non_null() +static int send_gc_invite_accepted_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number) +{ + if (!friend_is_valid(m, friend_number)) { + return -1; + } + + if (chat == nullptr) { + return -2; + } + + uint8_t packet[1 + 1 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE]; + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE_ACCEPTED; + + memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + uint16_t length = 2 + CHAT_ID_SIZE; + + memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + + if (!send_group_invite_packet(m, friend_number, packet, length)) { + LOGGER_ERROR(chat->log, "Failed to send group invite packet."); + return -3; + } + + return 0; +} + +/** @brief Sends an invite confirmed packet to friend designated by `friend_number`. + * + * `data` must contain the group's Chat ID, the sender's public encryption key, + * and either the sender's packed IP_Port, or at least one packed TCP node that + * the sender can be connected to through (or both). + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_confirmed_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number, + const uint8_t *data, uint16_t length) +{ + if (!friend_is_valid(m, friend_number)) { + return false; + } + + if (chat == nullptr) { + return false; + } + + if (length > MAX_GC_PACKET_SIZE) { + return false; + } + + const uint16_t packet_length = 2 + length; + uint8_t *packet = (uint8_t *)malloc(packet_length); + + if (packet == nullptr) { + return false; + } + + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE_CONFIRMATION; + + memcpy(packet + 2, data, length); + + if (!send_group_invite_packet(m, friend_number, packet, packet_length)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** @brief Adds `num_nodes` tcp relays from `tcp_relays` to tcp relays list associated with `gconn` + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn, const Node_format *tcp_relays, + size_t num_nodes) +{ + uint32_t relays_added = 0; + + for (size_t i = 0; i < num_nodes; ++i) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, + gconn->tcp_connection_num, &tcp_relays[i].ip_port, + tcp_relays[i].public_key); + + if (add_tcp_result == 0) { + if (gcc_save_tcp_relay(chat->rng, gconn, &tcp_relays[i]) == 0) { + ++relays_added; + } + } + } + + return relays_added; +} + +non_null() +static bool copy_friend_ip_port_to_gconn(const Messenger *m, int friend_number, GC_Connection *gconn) +{ + if (!friend_is_valid(m, friend_number)) { + return false; + } + + const Friend *f = &m->friendlist[friend_number]; + const int friend_connection_id = f->friendcon_id; + const Friend_Conn *connection = get_conn(m->fr_c, friend_connection_id); + + if (connection == nullptr) { + return false; + } + + const IP_Port *friend_ip_port = friend_conn_get_dht_ip_port(connection); + + if (!ipport_isset(friend_ip_port)) { + return false; + } + + gconn->addr.ip_port = *friend_ip_port; + + return true; +} + +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ + if (length < GC_JOIN_DATA_LENGTH) { + return -1; + } + + if (!friend_is_valid(c->messenger, friend_number)) { + return -4; + } + + uint8_t chat_id[CHAT_ID_SIZE]; + uint8_t invite_chat_pk[ENC_PUBLIC_KEY_SIZE]; + + memcpy(chat_id, data, CHAT_ID_SIZE); + memcpy(invite_chat_pk, data + CHAT_ID_SIZE, ENC_PUBLIC_KEY_SIZE); + + const GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + + if (chat == nullptr) { + return -2; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, invite_chat_pk, false); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, + nullptr, data + ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE, + length - GC_JOIN_DATA_LENGTH, true); + + const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(c->messenger, friend_number, gconn); + + uint32_t tcp_relays_added = 0; + + if (num_nodes > 0) { + tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_nodes); + } else { + LOGGER_WARNING(chat->log, "Invite confirm packet did not contain any TCP relays"); + } + + if (tcp_relays_added == 0 && !copy_ip_port_result) { + LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); + return -5; + } + + gconn->pending_handshake_type = HS_INVITE_REQUEST; + + return 0; +} + +/** Return true if we have a pending sent invite for our friend designated by `friend_number`. */ +non_null() +static bool friend_was_invited(const Messenger *m, GC_Chat *chat, int friend_number) +{ + for (size_t i = 0; i < MAX_GC_SAVED_INVITES; ++i) { + if (chat->saved_invites[i] == friend_number) { + chat->saved_invites[i] = -1; + return friend_is_valid(m, friend_number); + } + } + + return false; +} + +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ + if (length < GC_JOIN_DATA_LENGTH) { + return false; + } + + const Messenger *m = c->messenger; + + const uint8_t *chat_id = data; + + GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + + if (chat == nullptr || !group_can_handle_packets(chat)) { + return false; + } + + const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + + const int peer_number = peer_add(chat, nullptr, invite_chat_pk); + + if (!friend_was_invited(m, chat, friend_number)) { + return false; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const uint32_t num_tcp_relays = tcp_copy_connected_relays(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS); + + const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(m, friend_number, gconn); + + if (num_tcp_relays == 0 && !copy_ip_port_result) { + return false; + } + + uint16_t len = GC_JOIN_DATA_LENGTH; + uint8_t out_data[GC_JOIN_DATA_LENGTH + (GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6)]; + + memcpy(out_data, chat_id, CHAT_ID_SIZE); + memcpy(out_data + CHAT_ID_SIZE, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + + if (num_tcp_relays > 0) { + const uint32_t tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_tcp_relays); + + if (tcp_relays_added == 0 && !copy_ip_port_result) { + LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); + return false; + } + + const int nodes_len = pack_nodes(chat->log, out_data + len, sizeof(out_data) - len, tcp_relays, + (uint16_t)num_tcp_relays); + + if (nodes_len <= 0 && !copy_ip_port_result) { + return false; + } + + len += nodes_len; + } + + return send_gc_invite_confirmed_packet(m, chat, friend_number, out_data, len); +} + +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, + size_t nick_length, const uint8_t *passwd, uint16_t passwd_len) +{ + if (length < CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -3; + } + + if (nick == nullptr || nick_length == 0) { + return -4; + } + + if (!friend_is_valid(c->messenger, friend_number)) { + return -6; + } + + const uint8_t *chat_id = data; + const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + + const int group_number = create_new_group(c, nick, nick_length, false, GI_PUBLIC); + + if (group_number == -1) { + return -2; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -2; + } + + if (!expand_chat_id(chat->chat_public_key, chat_id)) { + group_delete(c, chat); + return -2; + } + + if (passwd != nullptr && passwd_len > 0) { + if (!set_gc_password_local(chat, passwd, passwd_len)) { + group_delete(c, chat); + return -5; + } + } + + const int peer_id = peer_add(chat, nullptr, invite_chat_pk); + + if (peer_id < 0) { + return -2; + } + + chat->join_type = HJ_PRIVATE; + + if (send_gc_invite_accepted_packet(c->messenger, chat, friend_number) != 0) { + return -7; + } + + return group_number; +} + +non_null(1, 3) nullable(5) +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data); + +GC_Session *new_dht_groupchats(Messenger *m) +{ + if (m == nullptr) { + return nullptr; + } + + GC_Session *c = (GC_Session *)calloc(1, sizeof(GC_Session)); + + if (c == nullptr) { + return nullptr; + } + + c->messenger = m; + c->announces_list = m->group_announce; + + networking_registerhandler(m->net, NET_PACKET_GC_LOSSLESS, &handle_gc_udp_packet, m); + networking_registerhandler(m->net, NET_PACKET_GC_LOSSY, &handle_gc_udp_packet, m); + networking_registerhandler(m->net, NET_PACKET_GC_HANDSHAKE, &handle_gc_udp_packet, m); + onion_group_announce_register(m->onion_c, gc_handle_announce_response_callback, c); + + return c; +} + +static void group_cleanup(GC_Session *c, GC_Chat *chat) +{ + kill_group_friend_connection(c, chat); + + mod_list_cleanup(&chat->moderation); + sanctions_list_cleanup(&chat->moderation); + + if (chat->tcp_conn != nullptr) { + kill_tcp_connections(chat->tcp_conn); + } + + gcc_cleanup(chat); + + if (chat->group != nullptr) { + free(chat->group); + chat->group = nullptr; + } + + crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + crypto_memunlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); + crypto_memunlock(chat->shared_state.password, sizeof(chat->shared_state.password)); +} + +/** Deletes chat from group chat array and cleans up. */ +static void group_delete(GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + if (chat != nullptr) { + LOGGER_ERROR(chat->log, "Null pointer"); + } + + return; + } + + group_cleanup(c, chat); + + c->chats[chat->group_number] = empty_gc_chat; + + uint32_t i; + + for (i = c->chats_index; i > 0; --i) { + if (c->chats[i - 1].connection_state != CS_NONE) { + break; + } + } + + if (c->chats_index != i) { + c->chats_index = i; + + if (!realloc_groupchats(c, c->chats_index)) { + LOGGER_ERROR(chat->log, "Failed to reallocate groupchats array"); + } + } +} + +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length) +{ + const int ret = group_can_handle_packets(chat) ? send_gc_self_exit(chat, message, length) : 0; + group_delete(c, chat); + return ret; +} + +void kill_dht_groupchats(GC_Session *c) +{ + if (c == nullptr) { + return; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (group_can_handle_packets(chat)) { + send_gc_self_exit(chat, nullptr, 0); + } + + group_cleanup(c, chat); + } + + networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSY, nullptr, nullptr); + networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSLESS, nullptr, nullptr); + networking_registerhandler(c->messenger->net, NET_PACKET_GC_HANDSHAKE, nullptr, nullptr); + onion_group_announce_register(c->messenger->onion_c, nullptr, nullptr); + + free(c->chats); + free(c); +} + +bool gc_group_is_valid(const GC_Chat *chat) +{ + return chat->connection_state != CS_NONE && chat->shared_state.version > 0; +} + +/** Return true if `group_number` designates an active group in session `c`. */ +static bool group_number_valid(const GC_Session *c, int group_number) +{ + if (group_number < 0 || group_number >= c->chats_index) { + return false; + } + + if (c->chats == nullptr) { + return false; + } + + return c->chats[group_number].connection_state != CS_NONE; +} + +uint32_t gc_count_groups(const GC_Session *c) +{ + uint32_t count = 0; + + for (uint32_t i = 0; i < c->chats_index; ++i) { + const GC_Chat *chat = &c->chats[i]; + + if (gc_group_is_valid(chat)) { + ++count; + } + } + + return count; +} + +GC_Chat *gc_get_group(const GC_Session *c, int group_number) +{ + if (!group_number_valid(c, group_number)) { + return nullptr; + } + + return &c->chats[group_number]; +} + +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key) +{ + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(public_key, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE) == 0) { + return chat; + } + } + + return nullptr; +} + +/** Return True if chat_id exists in the session chat array */ +static bool group_exists(const GC_Session *c, const uint8_t *chat_id) +{ + for (uint32_t i = 0; i < c->chats_index; ++i) { + const GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(get_chat_id(chat->chat_public_key), chat_id, CHAT_ID_SIZE) == 0) { + return true; + } + } + + return false; +} + +/** Creates a new 32-byte session encryption keypair and puts the results in `public_key` and `secret_key`. */ +static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, uint8_t *secret_key) +{ + if (crypto_new_keypair(rng, public_key, secret_key) != 0) { + LOGGER_FATAL(log, "Failed to create group session keypair"); + } +} + +/** + * Creates a new 64-byte extended keypair for `chat` and puts results in `self_public_key` + * and `self_secret_key` buffers. The first 32-bytes of the generated keys are used for + * encryption, while the remaining 32-bytes are used for signing. + * + * Return false if key generation fails. + */ +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat) +{ + crypto_memlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + + if (!create_extended_keypair(chat->self_public_key, chat->self_secret_key)) { + crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + return false; + } + + return true; +} + +/** @brief Handles a group announce onion response. + * + * Return true on success. + */ +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data) +{ + const GC_Session *c = (GC_Session *)user_data; + + if (c == nullptr) { + return false; + } + + if (sendback_num == 0) { + return false; + } + + GC_Announce announces[GCA_MAX_SENT_ANNOUNCES]; + const uint8_t *gc_public_key = onion_friend_get_gc_public_key_num(onion_c, sendback_num - 1); + GC_Chat *chat = gc_get_group_by_public_key(c, gc_public_key); + + if (chat == nullptr) { + return false; + } + + const int gc_announces_count = gca_unpack_announces_list(chat->log, data, data_length, + announces, GCA_MAX_SENT_ANNOUNCES); + + if (gc_announces_count == -1) { + return false; + } + + const int added_peers = gc_add_peers_from_announces(chat, announces, gc_announces_count); + + return added_peers >= 0; +} + +/** @brief Adds TCP relays from `announce` to the TCP relays list for `gconn`. + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays_from_announce(const GC_Chat *chat, GC_Connection *gconn, const GC_Announce *announce) +{ + uint32_t added_relays = 0; + + for (uint8_t j = 0; j < announce->tcp_relays_count; ++j) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &announce->tcp_relays[j].ip_port, + announce->tcp_relays[j].public_key); + + if (add_tcp_result == -1) { + continue; + } + + if (gcc_save_tcp_relay(chat->rng, gconn, &announce->tcp_relays[j]) == -1) { + continue; + } + + if (added_relays == 0) { + memcpy(gconn->oob_relay_pk, announce->tcp_relays[j].public_key, CRYPTO_PUBLIC_KEY_SIZE); + } + + ++added_relays; + } + + return added_relays; +} + +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count) +{ + if (chat == nullptr || announces == nullptr) { + return -1; + } + + if (!is_public_chat(chat)) { + return 0; + } + + int added_peers = 0; + + for (uint8_t i = 0; i < gc_announces_count; ++i) { + const GC_Announce *announce = &announces[i]; + + if (!gca_is_valid_announce(announce)) { + continue; + } + + const bool ip_port_set = announce->ip_port_is_set; + const IP_Port *ip_port = ip_port_set ? &announce->ip_port : nullptr; + const int peer_number = peer_add(chat, ip_port, announce->peer_public_key); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + continue; + } + + const uint32_t added_tcp_relays = add_gc_tcp_relays_from_announce(chat, gconn, announce); + + if (!ip_port_set && added_tcp_relays == 0) { + LOGGER_ERROR(chat->log, "Got invalid announcement: %u relays, IPP set: %d", + added_tcp_relays, ip_port_set); + continue; + } + + gconn->pending_handshake_type = HS_INVITE_REQUEST; + + if (!ip_port_set) { + gconn->is_oob_handshake = true; + } + + ++added_peers; + } + + return added_peers; +} +#endif // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_chats.h b/protocols/Tox/libtox/src/toxcore/group_chats.h new file mode 100644 index 0000000000..821e629a64 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_chats.h @@ -0,0 +1,782 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CHATS_H +#define GROUP_CHATS_H + +#include <stdbool.h> +#include <stdint.h> + +#include "TCP_connection.h" +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_announce.h" +#include "group_common.h" +#include "group_connection.h" +#include "logger.h" + +#define GC_PING_TIMEOUT 12 +#define GC_SEND_IP_PORT_INTERVAL (GC_PING_TIMEOUT * 5) +#define GC_CONFIRMED_PEER_TIMEOUT (GC_PING_TIMEOUT * 4 + 10) +#define GC_UNCONFIRMED_PEER_TIMEOUT GC_PING_TIMEOUT + +#define GC_JOIN_DATA_LENGTH (ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE) + +/** Group topic lock states. */ +typedef enum Group_Topic_Lock { + TL_ENABLED = 0x00, // Only the Founder and moderators may set the topic + TL_DISABLED = 0x01, // Anyone except Observers may set the topic +} Group_Topic_Lock; + +/** Group moderation events. */ +typedef enum Group_Moderation_Event { + MV_KICK = 0x00, // A peer has been kicked + MV_OBSERVER = 0x01, // A peer has been demoted to Observer + MV_USER = 0x02, // A peer has been demoted or promoted to User + MV_MOD = 0x03, // A peer has been promoted to or demoted from Moderator +} Group_Moderation_Event; + +/** Messenger level group invite types */ +typedef enum Group_Invite_Message_Type { + GROUP_INVITE = 0x00, // Peer has initiated an invite + GROUP_INVITE_ACCEPTED = 0x01, // Peer has accepted the invite + GROUP_INVITE_CONFIRMATION = 0x02, // Peer has confirmed the accepted invite +} Group_Invite_Message_Type; + +/** Group join rejection types. */ +typedef enum Group_Join_Rejected { + GJ_GROUP_FULL = 0x00, + GJ_INVALID_PASSWORD = 0x01, + GJ_INVITE_FAILED = 0x02, + GJ_INVALID = 0x03, +} Group_Join_Rejected; + +/** Group broadcast packet types */ +typedef enum Group_Broadcast_Type { + GM_STATUS = 0x00, // Peer changed their status + GM_NICK = 0x01, // Peer changed their nickname + GM_PLAIN_MESSAGE = 0x02, // Peer sent a normal message + GM_ACTION_MESSAGE = 0x03, // Peer sent an action message + GM_PRIVATE_MESSAGE = 0x04, // Peer sent a private message + GM_PEER_EXIT = 0x05, // Peer left the group + GM_KICK_PEER = 0x06, // Peer was kicked from the group + GM_SET_MOD = 0x07, // Peer was promoted to or demoted from Moderator role + GM_SET_OBSERVER = 0x08, // Peer was demoted to or promoted from Observer role +} Group_Broadcast_Type; + +/*** + * Group packet types. + * + * For a detailed spec, see docs/DHT_Group_Chats_Packet_Spec.md + */ +typedef enum Group_Packet_Type { + /* lossy packets (ID 0 is reserved) */ + GP_PING = 0x01, + GP_MESSAGE_ACK = 0x02, + GP_INVITE_RESPONSE_REJECT = 0x03, + + /* lossless packets */ + GP_CUSTOM_PRIVATE_PACKET = 0xee, + GP_FRAGMENT = 0xef, + GP_KEY_ROTATION = 0xf0, + GP_TCP_RELAYS = 0xf1, + GP_CUSTOM_PACKET = 0xf2, + GP_BROADCAST = 0xf3, + GP_PEER_INFO_REQUEST = 0xf4, + GP_PEER_INFO_RESPONSE = 0xf5, + GP_INVITE_REQUEST = 0xf6, + GP_INVITE_RESPONSE = 0xf7, + GP_SYNC_REQUEST = 0xf8, + GP_SYNC_RESPONSE = 0xf9, + GP_TOPIC = 0xfa, + GP_SHARED_STATE = 0xfb, + GP_MOD_LIST = 0xfc, + GP_SANCTIONS_LIST = 0xfd, + GP_FRIEND_INVITE = 0xfe, + GP_HS_RESPONSE_ACK = 0xff, +} Group_Packet_Type; + +/** Lossless message acknowledgement types. */ +typedef enum Group_Message_Ack_Type { + GR_ACK_RECV = 0x00, // indicates a message has been received + GR_ACK_REQ = 0x01, // indicates a message needs to be re-sent +} Group_Message_Ack_Type; + +/** @brief Returns the GC_Connection object associated with `peer_number`. + * Returns null if peer_number does not designate a valid peer. + */ +non_null() +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number); + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +non_null() +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key); + +/** @brief Check if peer with the public encryption key is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + * + * If `confirmed` is true the peer number will only be returned if the peer is confirmed. + */ +non_null() +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed); + +/** @brief Encrypts `data` of size `length` using the peer's shared key and a new nonce. + * + * Adds encrypted header consisting of: packet type, message_id (only for lossless packets). + * Adds plaintext header consisting of: packet identifier, self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if plaintext length is invalid. + * Return -2 if malloc fails. + * Return -3 if encryption fails. + */ +non_null(1, 2, 3, 4, 5) nullable(7) +int group_packet_wrap( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, + uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, + uint8_t gp_packet_type, uint8_t net_packet_type); + +/** @brief Returns the size of a wrapped/encrypted packet with a plain size of `length`. + * + * `packet_type` should be either NET_PACKET_GC_LOSSY or NET_PACKET_GC_LOSSLESS. + */ +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type); + +/** @brief Sends a plain message or an action, depending on type. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * `message_id` should either point to a uint32_t or be NULL. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the message type is invalid. + * Returns -4 if the sender does not have permission to speak. + * Returns -5 if the packet fails to send. + */ +non_null(1, 2, 3, 4) nullable(5) +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, + uint32_t *message_id); + +/** @brief Sends a private message to peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the peer_id is invalid. + * Returns -4 if the message type is invalid. + * Returns -5 if the sender has the observer role. + * Returns -6 if the packet fails to send. + */ +non_null() +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, + uint16_t length); + +/** @brief Sends a custom packet to the group. If lossless is true, the packet will be lossless. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the sender has the observer role. + */ +non_null() +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length); + +/** @brief Sends a custom private packet to the peer designated by peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * @retval 0 on success. + * @retval -1 if the message is too long. + * @retval -2 if the message pointer is NULL or length is zero. + * @retval -3 if the supplied peer_id does not designate a valid peer. + * @retval -4 if the sender has the observer role. + * @retval -5 if the packet fails to send. + */ +non_null() +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, + uint16_t length); + +/** @brief Sets ignore for peer_id. + * + * Returns 0 on success. + * Returns -1 if the peer_id is invalid. + * Returns -2 if the caller attempted to ignore himself. + */ +non_null() +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore); + +/** @brief Sets the group topic and broadcasts it to the group. + * + * If `length` is equal to zero the topic will be unset. + * + * Returns 0 on success. + * Returns -1 if the topic is too long (must be `<= MAX_GC_TOPIC_SIZE`). + * Returns -2 if the caller does not have the required permissions to set the topic. + * Returns -3 if the packet cannot be created or signing fails. + * Returns -4 if the packet fails + */ +non_null(1) nullable(2) +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length); + +/** @brief Copies the group topic to `topic`. If topic is null this function has no effect. + * + * Call `gc_get_topic_size` to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last topic callback. + */ +non_null(1) nullable(2) +void gc_get_topic(const GC_Chat *chat, uint8_t *topic); + +/** @brief Returns the topic length. + * + * The return value is equal to the `length` agument received by the last topic callback. + */ +non_null() +uint16_t gc_get_topic_size(const GC_Chat *chat); + +/** @brief Copies group name to `group_name`. If `group_name` is null this function has no effect. + * + * Call `gc_get_group_name_size` to determine the allocation size for the `group_name` + * parameter. + */ +non_null() +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name); + +/** @brief Returns the group name length. */ +non_null() +uint16_t gc_get_group_name_size(const GC_Chat *chat); + +/** @brief Copies the group password to password. + * + * If password is null this function has no effect. + * + * Call the `gc_get_password_size` function to determine the allocation size for + * the `password` buffer. + * + * The data received is equal to the data received by the last password callback. + */ +non_null() +void gc_get_password(const GC_Chat *chat, uint8_t *password); + +/** @brief Returns the group password length. */ +non_null() +uint16_t gc_get_password_size(const GC_Chat *chat); + +/** @brief Returns the group privacy state. + * + * The value returned is equal to the data receieved by the last privacy_state callback. + */ +non_null() +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat); + +/** @brief Returns the group topic lock state. + * + * The value returned is equal to the data received by the last last topic_lock callback. + */ +non_null() +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat); + +/** @brief Returns the group voice state. + * + * The value returned is equal to the data received by the last voice_state callback. + */ +non_null() +Group_Voice_State gc_get_voice_state(const GC_Chat *chat); + +/** @brief Returns the group peer limit. + * + * The value returned is equal to the data receieved by the last peer_limit callback. + */ +non_null() +uint16_t gc_get_max_peers(const GC_Chat *chat); + +/** @brief Sets your own nick to `nick`. + * + * `length` cannot exceed MAX_GC_NICK_SIZE. if `length` is zero or `name` is a + * null pointer the function call will fail. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the length is too long. + * Returns -3 if the length is zero or nick is a NULL pointer. + * Returns -4 if the packet fails to send. + */ +non_null() +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length); + +/** @brief Copies your own name to `nick`. + * + * If `nick` is null this function has no effect. + */ +non_null() +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick); + +/** @brief Return your own nick length. + * + * If no nick was set before calling this function it will return 0. + */ +non_null() +uint16_t gc_get_self_nick_size(const GC_Chat *chat); + +/** @brief Returns your own group role. */ +non_null() +Group_Role gc_get_self_role(const GC_Chat *chat); + +/** @brief Return your own status. */ +non_null() +uint8_t gc_get_self_status(const GC_Chat *chat); + +/** @brief Returns your own peer id. */ +non_null() +uint32_t gc_get_self_peer_id(const GC_Chat *chat); + +/** @brief Copies self public key to `public_key`. + * + * If `public_key` is null this function has no effect. + * + * This key is permanently tied to our identity for `chat` until we explicitly + * exit the group. This key is the only way for other peers to reliably identify + * us across client restarts. + */ +non_null(1) nullable(2) +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key); + +/** @brief Copies nick designated by `peer_id` to `name`. + * + * Call `gc_get_peer_nick_size` to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last nick_change callback. + * + * Returns true on success. + * Returns false if peer_id is invalid. + */ +non_null(1) nullable(3) +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name); + +/** @brief Returns the length of the nick for the peer designated by `peer_id`. + * Returns -1 if peer_id is invalid. + * + * The value returned is equal to the `length` argument received by the last + * nick_change callback. + */ +non_null() +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Copies peer_id's public key to `public_key`. + * + * This key is permanently tied to the peer's identity for `chat` until they explicitly + * exit the group. This key is the only way for to reliably identify the given peer + * across client restarts. + * + * `public_key` shold have room for at least ENC_PUBLIC_KEY_SIZE bytes. + * + * Returns 0 on success. + * Returns -1 if peer_id is invalid or doesn't correspond to a valid peer connection. + * Returns -2 if `public_key` is null. + */ +non_null(1) nullable(3) +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key); + +/** @brief Gets the connection status for peer associated with `peer_id`. + * + * Returns 2 if we have a direct (UDP) connection with a peer. + * Returns 1 if we have an indirect (TCP) connection with a peer. + * Returns 0 if peer_id is invalid or corresponds to ourselves. + * + * Note: Return values must correspond to Tox_Connection enum in API. + */ +non_null() +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the caller's status to `status`. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the packet failed to send. + */ +non_null() +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status); + +/** @brief Returns the status of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The status returned is equal to the last status received through the status_change + * callback. + */ +non_null() +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Returns the group role of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The role returned is equal to the last role received through the moderation callback. + */ +non_null() +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the role of peer_id. role must be one of: GR_MODERATOR, GR_USER, GR_OBSERVER + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if caller does not have sufficient permissions for the action. + * Returns -4 if the role assignment is invalid. + * Returns -5 if the role failed to be set. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role); + +/** @brief Sets the group password and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If `password` is null or `password_length` is 0 the password will be unset for the group. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for the action. + * Returns -2 if the password is too long. + * Returns -3 if the packet failed to send. + * Returns -4 if malloc failed. + */ +non_null(1) nullable(2) +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length); + +/** @brief Sets the topic lock and distributes the new shared state to the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if `topic_lock` is an invalid type. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the group is disconnected. + * Returns -5 if the topic lock could not be set. + * Returns -6 if the packet failed to send. + */ +non_null() +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state); + +/** @brief Sets the group privacy state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the privacy state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state); + +/** @brief Sets the group voice state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the voice state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state); + +/** @brief Sets the peer limit to maxpeers and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for this action. + * Returns -2 if the peer limit could not be set. + * Returns -3 if the packet failed to send. + */ +non_null() +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers); + +/** @brief Removes peer designated by `peer_id` from peer list and sends a broadcast instructing + * all other peers to remove the peer from their peerlist as well. + * + * This function will not trigger the peer_exit callback for the caller. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the action failed. + * Returns -5 if the packet failed to send. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id); + +/** @brief Copies the chat_id to dest. If dest is null this function has no effect. + * + * `dest` should have room for at least CHAT_ID_SIZE bytes. + */ +non_null(1) nullable(2) +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest); + + +/** Group callbacks */ +non_null(1) nullable(2) void gc_callback_message(const Messenger *m, gc_message_cb *function); +non_null(1) nullable(2) void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function); +non_null(1) nullable(2) void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function); +non_null(1) nullable(2) void gc_callback_custom_private_packet(const Messenger *m, + gc_custom_private_packet_cb *function); +non_null(1) nullable(2) void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function); +non_null(1) nullable(2) void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function); +non_null(1) nullable(2) void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function); +non_null(1) nullable(2) void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function); +non_null(1) nullable(2) void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function); +non_null(1) nullable(2) void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function); +non_null(1) nullable(2) void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function); +non_null(1) nullable(2) void gc_callback_password(const Messenger *m, gc_password_cb *function); +non_null(1) nullable(2) void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function); +non_null(1) nullable(2) void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function); +non_null(1) nullable(2) void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function); +non_null(1) nullable(2) void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function); +non_null(1) nullable(2) void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function); + +/** @brief The main loop. Should be called with every Messenger iteration. */ +non_null(1) nullable(2) +void do_gc(GC_Session *c, void *userdata); + +/** + * Make sure that DHT is initialized before calling this. + * Returns a NULL pointer on failure. + */ +nullable(1) +GC_Session *new_dht_groupchats(Messenger *m); + +/** @brief Cleans up groupchat structures and calls `gc_group_exit()` for every group chat */ +nullable(1) +void kill_dht_groupchats(GC_Session *c); + +/** @brief Loads a previously saved group and attempts to join it. + * + * `bu` is the packed group info. + * + * Returns group_number on success. + * Returns -1 on failure. + */ +non_null() +int gc_group_load(GC_Session *c, Bin_Unpack *bu); + +/** + * @brief Saves info from `chat` to `bp` in binary format. + */ +non_null() +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp); + +/** @brief Creates a new group and adds it to the group sessions group array. + * + * The caller of this function has founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * Return -1 if the nick or group name is too long. + * Return -2 if the nick or group name is empty. + * Return -3 if the the group object fails to initialize. + * Return -4 if the group state fails to initialize. + * Return -5 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t group_name_length, + const uint8_t *nick, size_t nick_length); + +/** @brief Joins a group designated by `chat_id`. + * + * This function creates a new GC_Chat object, adds it to the chats array, and sends a DHT + * announcement to find peers in the group associated with `chat_id`. Once a peer has been + * found a join attempt will be initiated. + * + * If the group is not password protected password should be set to NULL and password_length should be 0. + * + * Return group_number on success. + * Return -1 if the group object fails to initialize. + * Return -2 if chat_id is NULL or a group with chat_id already exists in the chats array. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the group password. + * Return -6 if the Messenger friend connection fails to initialize. + */ +non_null(1, 2, 3) nullable(5) +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, + uint16_t passwd_len); + +/** @brief Disconnects from all peers in a group but saves the group state for later use. + * + * Return true on sucess. + * Return false if the group handler object or chat object is null. + */ +non_null() +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat); + +/** @brief Disconnects from all peers in a group and attempts to reconnect. + * + * All self state and credentials are retained. + * + * Returns 0 on success. + * Returns -1 if the group handler object or chat object is null. + * Returns -2 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_rejoin_group(GC_Session *c, GC_Chat *chat); + +/** @brief Joins a group using the invite data received in a friend's group invite. + * + * The invite is only valid while the inviter is present in the group. + * + * Return group_number on success. + * Return -1 if the invite data is malformed. + * Return -2 if the group object fails to initialize. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the password. + * Return -6 if friend doesn't exist. + * Return -7 if sending packet failed. + */ +non_null(1, 3, 5) nullable(7) +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, + size_t nick_length, const uint8_t *passwd, uint16_t passwd_len); + +typedef bool gc_send_group_invite_packet_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *packet, + uint16_t length); + +/** @brief Invites friend designated by `friendnumber` to chat. + * Packet includes: Type, chat_id, TCP node or packed IP_Port. + * + * Return 0 on success. + * Return -1 if friendnumber does not exist. + * Return -2 on failure to create the invite data. + * Return -3 if the packet fails to send. + */ +non_null() +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, + gc_send_group_invite_packet_cb *callback); + +/** @brief Leaves a group and sends an exit broadcast packet with an optional parting message. + * + * All group state is permanently lost, including keys and roles. + * + * Return 0 on success. + * Return -1 if the parting message is too long. + * Return -2 if the parting message failed to send. + */ +non_null(1, 2) nullable(3) +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length); + +/** @brief Returns true if `chat` is a valid group chat. + * + * A valid group chat constitutes an initialized chat instance with a non-zero shared state version. + * The shared state version will be non-zero either if a peer has created the group, or if + * they have ever successfully connected to the group. + */ +non_null() +bool gc_group_is_valid(const GC_Chat *chat); + +/** @brief Returns the number of active groups in `c`. */ +non_null() +uint32_t gc_count_groups(const GC_Session *c); + +/** @brief Returns true if peer_number exists */ +non_null() +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number); + +/** @brief Return group_number's GC_Chat pointer on success + * Return NULL on failure + */ +non_null() +GC_Chat *gc_get_group(const GC_Session *c, int group_number); + +/** @brief Sends a lossy message acknowledgement to peer associated with `gconn`. + * + * If `type` is GR_ACK_RECV we send a read-receipt for read_id's packet. If `type` is GR_ACK_REQ + * we send a request for the respective id's packet. + * + * Requests are limited to one per second per peer. + * + * @retval true on success. + */ +non_null() +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type); + +/** @brief Helper function for `handle_gc_lossless_packet()`. + * + * Note: This function may modify the peer list and change peer numbers. + * + * @retval true if packet is successfully handled. + */ +non_null(1, 2) nullable(4, 7) +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, uint8_t packet_type, void *userdata); + +/** @brief Handles an invite accept packet. + * + * @retval true on success. + */ +non_null() +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Return true if `chat_id` is not present in our group sessions array. + * + * `length` must be at least CHAT_ID_SIZE bytes in length. + */ +non_null() +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length); + +/** @brief Handles an invite confirmed packet. + * + * Return 0 on success. + * Return -1 if length is invalid. + * Return -2 if data contains invalid chat_id. + * Return -3 if data contains invalid peer info. + * Return -4 if `friend_number` does not designate a valid friend. + * Return -5 if data contains invalid connection info. + */ +non_null() +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Returns the group designated by `public_key`. + * Returns null if group does not exist. + */ +non_null() +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key); + +/** @brief Attempts to add peers from `announces` to our peer list and initiate an invite request. + * + * Returns the number of peers added on success. + * Returns -1 on failure. + */ +non_null() +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count); + +#endif // GROUP_CHATS_H diff --git a/protocols/Tox/libtox/src/toxcore/group_common.h b/protocols/Tox/libtox/src/toxcore/group_common.h new file mode 100644 index 0000000000..ef901e8a3a --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_common.h @@ -0,0 +1,409 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2022 The TokTok team. + */ + +/** + * Common groupchat data structures. + */ + +#ifndef GROUP_COMMON_H +#define GROUP_COMMON_H + +#include <stdbool.h> +#include <stdint.h> + +#include "DHT.h" +#include "TCP_connection.h" +#include "group_moderation.h" + +#define MAX_GC_PART_MESSAGE_SIZE 128 +#define MAX_GC_NICK_SIZE 128 +#define MAX_GC_TOPIC_SIZE 512 +#define MAX_GC_GROUP_NAME_SIZE 48 +#define GC_MESSAGE_PSEUDO_ID_SIZE 4 +#define GROUP_MAX_MESSAGE_LENGTH 1372 + +/* Max size of a packet chunk. Packets larger than this must be split up. + * + * For an explanation on why this value was chosen, see the following link: https://archive.ph/vsCOG + */ +#define MAX_GC_PACKET_CHUNK_SIZE 500 + +#define MAX_GC_MESSAGE_SIZE GROUP_MAX_MESSAGE_LENGTH +#define MAX_GC_MESSAGE_RAW_SIZE (MAX_GC_MESSAGE_SIZE + GC_MESSAGE_PSEUDO_ID_SIZE) +#define MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE 1373 +#define MAX_GC_CUSTOM_LOSSY_PACKET_SIZE MAX_GC_PACKET_CHUNK_SIZE +#define MAX_GC_PASSWORD_SIZE 32 +#define MAX_GC_SAVED_INVITES 10 +#define MAX_GC_PEERS_DEFAULT 100 +#define MAX_GC_SAVED_TIMEOUTS 12 +#define GC_MAX_SAVED_PEERS 100 +#define GC_SAVED_PEER_SIZE (ENC_PUBLIC_KEY_SIZE + sizeof(Node_format) + sizeof(IP_Port)) + +/* Max size of a complete encrypted packet including headers. */ +#define MAX_GC_PACKET_SIZE (MAX_GC_PACKET_CHUNK_SIZE * 100) + +/* Max number of messages to store in the send/recv arrays */ +#define GCC_BUFFER_SIZE 8192 + +/** Self UDP status. Must correspond to return values from `ipport_self_copy()`. */ +typedef enum Self_UDP_Status { + SELF_UDP_STATUS_NONE = 0x00, + SELF_UDP_STATUS_WAN = 0x01, + SELF_UDP_STATUS_LAN = 0x02, +} Self_UDP_Status; + +/** Group exit types. */ +typedef enum Group_Exit_Type { + GC_EXIT_TYPE_QUIT = 0x00, // Peer left the group + GC_EXIT_TYPE_TIMEOUT = 0x01, // Peer connection timed out + GC_EXIT_TYPE_DISCONNECTED = 0x02, // Peer diconnected from group + GC_EXIT_TYPE_SELF_DISCONNECTED = 0x03, // Self disconnected from group + GC_EXIT_TYPE_KICKED = 0x04, // Peer was kicked from the group + GC_EXIT_TYPE_SYNC_ERR = 0x05, // Peer failed to sync with the group + GC_EXIT_TYPE_NO_CALLBACK = 0x06, // The peer exit callback should not be triggered +} Group_Exit_Type; + +typedef struct GC_Exit_Info { + uint8_t part_message[MAX_GC_PART_MESSAGE_SIZE]; + uint16_t length; + Group_Exit_Type exit_type; +} GC_Exit_Info; + +typedef struct GC_PeerAddress { + uint8_t public_key[EXT_PUBLIC_KEY_SIZE]; + IP_Port ip_port; +} GC_PeerAddress; + +typedef struct GC_Message_Array_Entry { + uint8_t *data; + uint16_t data_length; + uint8_t packet_type; + uint64_t message_id; + uint64_t time_added; + uint64_t last_send_try; +} GC_Message_Array_Entry; + +typedef struct GC_Connection { + uint64_t send_message_id; /* message_id of the next message we send to peer */ + + uint16_t send_array_start; /* send_array index of oldest item */ + GC_Message_Array_Entry *send_array; + + uint64_t received_message_id; /* message_id of peer's last message to us */ + GC_Message_Array_Entry *recv_array; + + uint64_t last_chunk_id; /* The message ID of the last packet fragment we received */ + + GC_PeerAddress addr; /* holds peer's extended real public key and ip_port */ + uint32_t public_key_hash; /* Jenkins one at a time hash of peer's real encryption public key */ + + uint8_t session_public_key[ENC_PUBLIC_KEY_SIZE]; /* self session public key for this peer */ + uint8_t session_secret_key[ENC_SECRET_KEY_SIZE]; /* self session secret key for this peer */ + uint8_t session_shared_key[CRYPTO_SHARED_KEY_SIZE]; /* made with our session sk and peer's session pk */ + + int tcp_connection_num; + uint64_t last_sent_tcp_relays_time; /* the last time we attempted to send this peer our tcp relays */ + uint16_t tcp_relay_share_index; + uint64_t last_received_direct_time; /* the last time we received a direct UDP packet from this connection */ + uint64_t last_sent_ip_time; /* the last time we sent our ip info to this peer in a ping packet */ + + Node_format connected_tcp_relays[MAX_FRIEND_TCP_CONNECTIONS]; + uint16_t tcp_relays_count; + + uint64_t last_received_packet_time; /* The last time we successfully processed any packet from this peer */ + uint64_t last_requested_packet_time; /* The last time we requested a missing packet from this peer */ + uint64_t last_sent_ping_time; + uint64_t last_sync_response; /* the last time we sent this peer a sync response */ + uint8_t oob_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; + bool self_is_closer; /* true if we're "closer" to the chat_id than this peer (uses real pk's) */ + + bool confirmed; /* true if this peer has given us their info */ + bool handshaked; /* true if we've successfully handshaked with this peer */ + uint16_t handshake_attempts; + uint64_t last_handshake_request; + uint64_t last_handshake_response; + uint8_t pending_handshake_type; + bool is_pending_handshake_response; + bool is_oob_handshake; + + uint64_t last_key_rotation; /* the last time we rotated session keys for this peer */ + bool pending_key_rotation_request; + + bool pending_delete; /* true if this peer has been marked for deletion */ + GC_Exit_Info exit_info; +} GC_Connection; + +/*** + * Group roles. Roles are hierarchical in that each role has a set of privileges plus + * all the privileges of the roles below it. + */ +typedef enum Group_Role { + /** Group creator. All-powerful. Cannot be demoted or kicked. */ + GR_FOUNDER = 0x00, + + /** + * May promote or demote peers below them to any role below them. + * May also kick peers below them and set the topic. + */ + GR_MODERATOR = 0x01, + + /** may interact normally with the group. */ + GR_USER = 0x02, + + /** May not interact with the group but may observe. */ + GR_OBSERVER = 0x03, +} Group_Role; + +typedef enum Group_Peer_Status { + GS_NONE = 0x00, + GS_AWAY = 0x01, + GS_BUSY = 0x02, +} Group_Peer_Status; + +/** + * Group voice states. The state determines which Group Roles have permission to speak. + */ +typedef enum Group_Voice_State { + /** Every group role except Observers may speak. */ + GV_ALL = 0x00, + + /** Only Moderators and the Founder may speak. */ + GV_MODS = 0x01, + + /** Only the Founder may speak. */ + GV_FOUNDER = 0x02, +} Group_Voice_State; + +/** Group connection states. */ +typedef enum GC_Conn_State { + CS_NONE = 0x00, // Indicates a group is not initialized + CS_DISCONNECTED = 0x01, // Not receiving or sending any packets + CS_CONNECTING = 0x02, // Attempting to establish a connection with peers in the group + CS_CONNECTED = 0x03, // Has successfully received a sync response from a peer in the group +} GC_Conn_State; + +/** Group privacy states. */ +typedef enum Group_Privacy_State { + GI_PUBLIC = 0x00, // Anyone with the chat ID may join the group + GI_PRIVATE = 0x01, // Peers may only join the group via a friend invite +} Group_Privacy_State; + +/** Handshake join types. */ +typedef enum Group_Handshake_Join_Type { + HJ_PUBLIC = 0x00, // Indicates the group was joined via the DHT + HJ_PRIVATE = 0x01, // Indicates the group was joined via private friend invite +} Group_Handshake_Join_Type; + +typedef struct GC_SavedPeerInfo { + uint8_t public_key[ENC_PUBLIC_KEY_SIZE]; + Node_format tcp_relay; + IP_Port ip_port; +} GC_SavedPeerInfo; + +/** Holds info about peers who recently timed out */ +typedef struct GC_TimedOutPeer { + GC_SavedPeerInfo addr; + uint64_t last_seen; // the time the peer disconnected + uint64_t last_reconn_try; // the last time we tried to establish a new connection +} GC_TimedOutPeer; + +typedef struct GC_Peer { + /* Below state is sent to other peers in peer info exchange */ + uint8_t nick[MAX_GC_NICK_SIZE]; + uint16_t nick_length; + uint8_t status; + + /* Below state is local only */ + Group_Role role; + uint32_t peer_id; // permanent ID (used for the public API) + bool ignore; + + GC_Connection gconn; +} GC_Peer; + +typedef struct GC_SharedState { + uint32_t version; + uint8_t founder_public_key[EXT_PUBLIC_KEY_SIZE]; + uint16_t maxpeers; + uint16_t group_name_len; + uint8_t group_name[MAX_GC_GROUP_NAME_SIZE]; + Group_Privacy_State privacy_state; // GI_PUBLIC (uses DHT) or GI_PRIVATE (invite only) + uint16_t password_length; + uint8_t password[MAX_GC_PASSWORD_SIZE]; + uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE]; + uint32_t topic_lock; // equal to GC_TOPIC_LOCK_ENABLED when lock is enabled + Group_Voice_State voice_state; +} GC_SharedState; + +typedef struct GC_TopicInfo { + uint32_t version; + uint16_t length; + uint16_t checksum; // used for syncing problems. the checksum with the highest value gets priority. + uint8_t topic[MAX_GC_TOPIC_SIZE]; + uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; // Public signature key of the topic setter +} GC_TopicInfo; + +typedef struct GC_Chat { + Mono_Time *mono_time; + const Logger *log; + const Random *rng; + + uint32_t connected_tcp_relays; + Self_UDP_Status self_udp_status; + IP_Port self_ip_port; + + Networking_Core *net; + TCP_Connections *tcp_conn; + + uint64_t last_checked_tcp_relays; + Group_Handshake_Join_Type join_type; + + GC_Peer *group; + Moderation moderation; + + GC_Conn_State connection_state; + + GC_SharedState shared_state; + uint8_t shared_state_sig[SIGNATURE_SIZE]; // signed by founder using the chat secret key + + GC_TopicInfo topic_info; + uint8_t topic_sig[SIGNATURE_SIZE]; // signed by the peer who set the current topic + uint16_t topic_prev_checksum; // checksum of the previous topic + uint64_t topic_time_set; + + uint16_t peers_checksum; // sum of the public key hash of every confirmed peer in the group + uint16_t roles_checksum; // sum of every confirmed peer's role plus the first byte of their public key + + uint32_t numpeers; + int group_number; + + uint8_t chat_public_key[EXT_PUBLIC_KEY_SIZE]; // the chat_id is the sig portion + uint8_t chat_secret_key[EXT_SECRET_KEY_SIZE]; // only used by the founder + + uint8_t self_public_key[EXT_PUBLIC_KEY_SIZE]; + uint8_t self_secret_key[EXT_SECRET_KEY_SIZE]; + + uint64_t time_connected; + uint64_t last_ping_interval; + uint64_t last_sync_request; // The last time we sent a sync request to any peer + uint64_t last_sync_response_peer_list; // The last time we sent the peer list to any peer + uint64_t last_time_peers_loaded; + + /* keeps track of frequency of new inbound connections */ + uint8_t connection_O_metre; + uint64_t connection_cooldown_timer; + bool block_handshakes; + + int32_t saved_invites[MAX_GC_SAVED_INVITES]; + uint8_t saved_invites_index; + + /** A list of recently seen peers in case we disconnect from a private group. + * Peers are added once they're confirmed, and only if there are vacant + * spots (older connections get priority). An entry is removed only when the list + * is full, its respective peer goes offline, and an online peer who isn't yet + * present in the list can be added. + */ + GC_SavedPeerInfo saved_peers[GC_MAX_SAVED_PEERS]; + + GC_TimedOutPeer timeout_list[MAX_GC_SAVED_TIMEOUTS]; + size_t timeout_list_index; + uint64_t last_timed_out_reconn_try; // the last time we tried to reconnect to timed out peers + + bool update_self_announces; // true if we should try to update our announcements + uint64_t last_self_announce_check; // the last time we checked if we should update our announcements + uint64_t last_time_self_announce; // the last time we announced the group + uint8_t announced_tcp_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; // The pk of the last TCP relay we announced + + uint8_t m_group_public_key[CRYPTO_PUBLIC_KEY_SIZE]; // public key for group's messenger friend connection + int friend_connection_id; // identifier for group's messenger friend connection +} GC_Chat; + +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED +typedef struct Messenger Messenger; +#endif /* MESSENGER_DEFINED */ + +typedef void gc_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *data, size_t length, uint32_t message_id, void *user_data); +typedef void gc_private_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *data, size_t length, void *user_data); +typedef void gc_custom_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_custom_private_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, + size_t length, void *user_data); +typedef void gc_moderation_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, uint32_t target_peer, + unsigned int mod_event, void *user_data); +typedef void gc_nick_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_status_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int status, + void *user_data); +typedef void gc_topic_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_topic_lock_cb(const Messenger *m, uint32_t group_number, unsigned int topic_lock, void *user_data); +typedef void gc_voice_state_cb(const Messenger *m, uint32_t group_number, unsigned int voice_state, void *user_data); +typedef void gc_peer_limit_cb(const Messenger *m, uint32_t group_number, uint32_t max_peers, void *user_data); +typedef void gc_privacy_state_cb(const Messenger *m, uint32_t group_number, unsigned int state, void *user_data); +typedef void gc_password_cb(const Messenger *m, uint32_t group_number, const uint8_t *data, size_t length, + void *user_data); +typedef void gc_peer_join_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data); +typedef void gc_peer_exit_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int exit_type, + const uint8_t *nick, size_t nick_len, const uint8_t *data, size_t length, void *user_data); +typedef void gc_self_join_cb(const Messenger *m, uint32_t group_number, void *user_data); +typedef void gc_rejected_cb(const Messenger *m, uint32_t group_number, unsigned int type, void *user_data); + +typedef struct GC_Session { + Messenger *messenger; + GC_Chat *chats; + struct GC_Announces_List *announces_list; + + uint32_t chats_index; + + gc_message_cb *message; + gc_private_message_cb *private_message; + gc_custom_packet_cb *custom_packet; + gc_custom_private_packet_cb *custom_private_packet; + gc_moderation_cb *moderation; + gc_nick_change_cb *nick_change; + gc_status_change_cb *status_change; + gc_topic_change_cb *topic_change; + gc_topic_lock_cb *topic_lock; + gc_voice_state_cb *voice_state; + gc_peer_limit_cb *peer_limit; + gc_privacy_state_cb *privacy_state; + gc_password_cb *password; + gc_peer_join_cb *peer_join; + gc_peer_exit_cb *peer_exit; + gc_self_join_cb *self_join; + gc_rejected_cb *rejected; +} GC_Session; + +/** @brief Adds a new peer to group_number's peer list. + * + * Return peer_number on success. + * Return -1 on failure. + * Return -2 if a peer with public_key is already in our peerlist. + */ +non_null(1, 3) nullable(2) +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key); + +/** @brief Unpacks saved peers from `data` of size `length` into `chat`. + * + * Returns the number of unpacked peers on success. + * Returns -1 on failure. + */ +non_null() +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length); + +/** @brief Packs all valid entries from saved peerlist into `data`. + * + * If `processed` is non-null it will be set to the length of the packed data. + * + * Return the number of packed saved peers on success. + * Return -1 if buffer is too small. + */ +non_null(1, 2) nullable(4) +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed); + +#endif // GROUP_COMMON_H diff --git a/protocols/Tox/libtox/src/toxcore/group_connection.c b/protocols/Tox/libtox/src/toxcore/group_connection.c new file mode 100644 index 0000000000..86c353c00c --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_connection.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_connection.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "DHT.h" +#include "ccompat.h" +#include "crypto_core.h" +#include "group_chats.h" +#include "group_common.h" +#include "mono_time.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/** Seconds since last direct UDP packet was received before the connection is considered dead */ +#define GCC_UDP_DIRECT_TIMEOUT (GC_PING_TIMEOUT + 4) + +/** Returns true if array entry does not contain an active packet. */ +non_null() +static bool array_entry_is_empty(const GC_Message_Array_Entry *array_entry) +{ + assert(array_entry != nullptr); + return array_entry->time_added == 0; +} + +/** @brief Clears an array entry. */ +non_null() +static void clear_array_entry(GC_Message_Array_Entry *const array_entry) +{ + if (array_entry->data != nullptr) { + free(array_entry->data); + } + + *array_entry = (GC_Message_Array_Entry) { + nullptr + }; +} + +/** + * Clears every send array message from queue starting at the index designated by + * `start_id` and ending at `end_id`, and sets the send_message_id for `gconn` + * to `start_id`. + */ +non_null() +static void clear_send_queue_id_range(GC_Connection *gconn, uint64_t start_id, uint64_t end_id) +{ + const uint16_t start_idx = gcc_get_array_index(start_id); + const uint16_t end_idx = gcc_get_array_index(end_id); + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + GC_Message_Array_Entry *entry = &gconn->send_array[i]; + clear_array_entry(entry); + } + + gconn->send_message_id = start_id; +} + +uint16_t gcc_get_array_index(uint64_t message_id) +{ + return message_id % GCC_BUFFER_SIZE; +} + +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id) +{ + gconn->send_message_id = id; + gconn->send_array_start = id % GCC_BUFFER_SIZE; +} + +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id) +{ + gconn->received_message_id = id; +} + +/** @brief Puts packet data in array_entry. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool create_array_entry(const Mono_Time *mono_time, GC_Message_Array_Entry *array_entry, const uint8_t *data, + uint16_t length, uint8_t packet_type, uint64_t message_id) +{ + if (length > 0) { + if (data == nullptr) { + return false; + } + + array_entry->data = (uint8_t *)malloc(sizeof(uint8_t) * length); + + if (array_entry->data == nullptr) { + return false; + } + + memcpy(array_entry->data, data, length); + } + + const uint64_t tm = mono_time_get(mono_time); + + array_entry->data_length = length; + array_entry->packet_type = packet_type; + array_entry->message_id = message_id; + array_entry->time_added = tm; + array_entry->last_send_try = tm; + + return true; +} + +/** @brief Adds data of length to gconn's send_array. + * + * Returns true on success and increments gconn's send_message_id. + */ +non_null(1, 2, 3) nullable(4) +static bool add_to_send_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + /* check if send_array is full */ + if ((gconn->send_message_id % GCC_BUFFER_SIZE) == (uint16_t)(gconn->send_array_start - 1)) { + LOGGER_DEBUG(log, "Send array overflow"); + return false; + } + + const uint16_t idx = gcc_get_array_index(gconn->send_message_id); + GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + + if (!array_entry_is_empty(array_entry)) { + LOGGER_DEBUG(log, "Send array entry isn't empty"); + return false; + } + + if (!create_array_entry(mono_time, array_entry, data, length, packet_type, gconn->send_message_id)) { + LOGGER_WARNING(log, "Failed to create array entry"); + return false; + } + + ++gconn->send_message_id; + + return true; +} + +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type) +{ + const uint64_t message_id = gconn->send_message_id; + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, data, length, packet_type)) { + LOGGER_WARNING(chat->log, "Failed to add payload to send array: (type: 0x%02x, length: %d)", packet_type, length); + return -1; + } + + if (!gcc_encrypt_and_send_lossless_packet(chat, gconn, data, length, message_id, packet_type)) { + LOGGER_DEBUG(chat->log, "Failed to send payload: (type: 0x%02x, length: %d)", packet_type, length); + return -2; + } + + return 0; +} + + +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + if (length <= MAX_GC_PACKET_CHUNK_SIZE || data == nullptr) { + LOGGER_FATAL(chat->log, "invalid length or null data pointer"); + return false; + } + + const uint16_t start_id = gconn->send_message_id; + + // First packet segment is comprised of packet type + first chunk of payload + uint8_t chunk[MAX_GC_PACKET_CHUNK_SIZE]; + chunk[0] = packet_type; + memcpy(chunk + 1, data, MAX_GC_PACKET_CHUNK_SIZE - 1); + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, MAX_GC_PACKET_CHUNK_SIZE, GP_FRAGMENT)) { + return false; + } + + uint16_t processed = MAX_GC_PACKET_CHUNK_SIZE - 1; + + // The rest of the segments are added in chunks + while (length > processed) { + const uint16_t chunk_len = min_u16(MAX_GC_PACKET_CHUNK_SIZE, length - processed); + + memcpy(chunk, data + processed, chunk_len); + processed += chunk_len; + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, chunk_len, GP_FRAGMENT)) { + clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); + return false; + } + } + + // empty packet signals the end of the sequence + if (!add_to_send_array(chat->log, chat->mono_time, gconn, nullptr, 0, GP_FRAGMENT)) { + clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); + return false; + } + + const uint16_t start_idx = gcc_get_array_index(start_id); + const uint16_t end_idx = gcc_get_array_index(gconn->send_message_id); + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + GC_Message_Array_Entry *entry = &gconn->send_array[i]; + + if (array_entry_is_empty(entry)) { + LOGGER_FATAL(chat->log, "array entry for packet chunk is empty"); + return false; + } + + assert(entry->packet_type == GP_FRAGMENT); + + gcc_encrypt_and_send_lossless_packet(chat, gconn, entry->data, entry->data_length, + entry->message_id, entry->packet_type); + } + + return true; +} + +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id) +{ + uint16_t idx = gcc_get_array_index(message_id); + GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + + if (array_entry_is_empty(array_entry)) { + return true; + } + + if (array_entry->message_id != message_id) { // wrap-around indicates a connection problem + LOGGER_DEBUG(log, "Wrap-around on message %llu", (unsigned long long)message_id); + return false; + } + + clear_array_entry(array_entry); + + /* Put send_array_start in proper position */ + if (idx == gconn->send_array_start) { + const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + + while (array_entry_is_empty(&gconn->send_array[idx]) && gconn->send_array_start != end) { + gconn->send_array_start = (gconn->send_array_start + 1) % GCC_BUFFER_SIZE; + idx = (idx + 1) % GCC_BUFFER_SIZE; + } + } + + return true; +} + +bool gcc_ip_port_is_set(const GC_Connection *gconn) +{ + return ipport_isset(&gconn->addr.ip_port); +} + +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp) +{ + if (ipp != nullptr && ipport_isset(ipp)) { + gconn->addr.ip_port = *ipp; + } +} + +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn) +{ + if (gconn == nullptr || tcp_node == nullptr) { + return false; + } + + if (gconn->tcp_relays_count == 0) { + return false; + } + + const uint32_t rand_idx = random_range_u32(rng, gconn->tcp_relays_count); + + if (!ipport_isset(&gconn->connected_tcp_relays[rand_idx].ip_port)) { + return false; + } + + *tcp_node = gconn->connected_tcp_relays[rand_idx]; + + return true; +} + +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node) +{ + if (gconn == nullptr || tcp_node == nullptr) { + return -1; + } + + if (!ipport_isset(&tcp_node->ip_port)) { + return -1; + } + + for (uint16_t i = 0; i < gconn->tcp_relays_count; ++i) { + if (pk_equal(gconn->connected_tcp_relays[i].public_key, tcp_node->public_key)) { + return -2; + } + } + + uint32_t idx = gconn->tcp_relays_count; + + if (gconn->tcp_relays_count >= MAX_FRIEND_TCP_CONNECTIONS) { + idx = random_range_u32(rng, gconn->tcp_relays_count); + } else { + ++gconn->tcp_relays_count; + } + + gconn->connected_tcp_relays[idx] = *tcp_node; + + return 0; +} + +/** @brief Stores `data` of length `length` in the receive array for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3) nullable(4) +static bool store_in_recv_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, + uint16_t length, uint8_t packet_type, uint64_t message_id) +{ + const uint16_t idx = gcc_get_array_index(message_id); + GC_Message_Array_Entry *ary_entry = &gconn->recv_array[idx]; + + if (!array_entry_is_empty(ary_entry)) { + LOGGER_DEBUG(log, "Recv array is not empty"); + return false; + } + + if (!create_array_entry(mono_time, ary_entry, data, length, packet_type, message_id)) { + LOGGER_WARNING(log, "Failed to create array entry"); + return false; + } + + return true; +} + +/** + * Reassembles a fragmented packet sequence ending with the data in the receive + * array at slot `message_id - 1` and starting with the last found slot containing + * a GP_FRAGMENT packet when searching backwards in the array. + * + * The fully reassembled packet is stored in `payload`, which must be passed as a + * null pointer, and must be free'd by the caller. + * + * Return the length of the fully reassembled packet on success. + * Return 0 on failure. + */ +non_null(1, 3) nullable(2) +static uint16_t reassemble_packet(const Logger *log, GC_Connection *gconn, uint8_t **payload, uint64_t message_id) +{ + uint16_t end_idx = gcc_get_array_index(message_id - 1); + uint16_t start_idx = end_idx; + uint16_t packet_length = 0; + + GC_Message_Array_Entry *entry = &gconn->recv_array[end_idx]; + + // search backwards in recv array until we find an empty slot or a non-fragment packet type + while (!array_entry_is_empty(entry) && entry->packet_type == GP_FRAGMENT) { + assert(entry->data != nullptr); + assert(entry->data_length <= MAX_GC_PACKET_CHUNK_SIZE); + + const uint16_t diff = packet_length + entry->data_length; + + assert(diff > packet_length); // overflow check + packet_length = diff; + + if (packet_length > MAX_GC_PACKET_SIZE) { + LOGGER_ERROR(log, "Payload of size %u exceeded max packet size", packet_length); // should never happen + return 0; + } + + start_idx = start_idx > 0 ? start_idx - 1 : GCC_BUFFER_SIZE - 1; + entry = &gconn->recv_array[start_idx]; + + if (start_idx == end_idx) { + LOGGER_ERROR(log, "Packet reassemble wrap-around"); + return 0; + } + } + + if (packet_length == 0) { + return 0; + } + + assert(*payload == nullptr); + *payload = (uint8_t *)malloc(packet_length); + + if (*payload == nullptr) { + LOGGER_ERROR(log, "Failed to allocate %u bytes for payload buffer", packet_length); + return 0; + } + + start_idx = (start_idx + 1) % GCC_BUFFER_SIZE; + end_idx = (end_idx + 1) % GCC_BUFFER_SIZE; + + uint16_t processed = 0; + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + entry = &gconn->recv_array[i]; + + assert(processed + entry->data_length <= packet_length); + memcpy(*payload + processed, entry->data, entry->data_length); + processed += entry->data_length; + + clear_array_entry(entry); + } + + return processed; +} + +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, + GC_Connection *gconn, const uint8_t *chunk, uint16_t length, uint8_t packet_type, + uint64_t message_id, void *userdata) +{ + if (length > 0) { + if (!store_in_recv_array(chat->log, chat->mono_time, gconn, chunk, length, packet_type, message_id)) { + return -1; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + gconn->last_chunk_id = message_id; + + return 1; + } + + uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; + memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + + uint8_t *payload = nullptr; + const uint16_t processed_len = reassemble_packet(chat->log, gconn, &payload, message_id); + + if (processed_len == 0) { + free(payload); + return -1; + } + + if (!handle_gc_lossless_helper(c, chat, peer_number, payload + 1, processed_len - 1, payload[0], userdata)) { + free(payload); + return -1; + } + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return 0; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + gconn->last_chunk_id = 0; + + free(payload); + + return 0; +} + +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + bool direct_conn) +{ + if (direct_conn) { + gconn->last_received_direct_time = mono_time_get(mono_time); + } + + /* Appears to be a duplicate packet so we discard it */ + if (message_id < gconn->received_message_id + 1) { + return 0; + } + + if (packet_type == GP_FRAGMENT) { // we handle packet fragments as a special case + return 3; + } + + /* we're missing an older message from this peer so we store it in recv_array */ + if (message_id > gconn->received_message_id + 1) { + if (!store_in_recv_array(log, mono_time, gconn, data, length, packet_type, message_id)) { + return -1; + } + + return 1; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + + return 2; +} + +/** @brief Handles peer_number's array entry with appropriate handler and clears it from array. + * + * This function increments the received message ID for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3, 5) nullable(6) +static bool process_recv_array_entry(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + GC_Message_Array_Entry *const array_entry, void *userdata) +{ + uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; + memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + + const bool ret = handle_gc_lossless_helper(c, chat, peer_number, array_entry->data, array_entry->data_length, + array_entry->packet_type, userdata); + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + clear_array_entry(array_entry); + + if (gconn == nullptr) { + return true; + } + + if (!ret) { + gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_REQ); + return false; + } + + gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_RECV); + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + + return true; +} + +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + void *userdata) +{ + if (gconn->last_chunk_id != 0) { // dont check array if we have an unfinished fragment sequence + return; + } + + const uint16_t idx = (gconn->received_message_id + 1) % GCC_BUFFER_SIZE; + GC_Message_Array_Entry *const array_entry = &gconn->recv_array[idx]; + + if (!array_entry_is_empty(array_entry)) { + process_recv_array_entry(c, chat, gconn, peer_number, array_entry, userdata); + } +} + +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint64_t tm = mono_time_get(chat->mono_time); + const uint16_t start = gconn->send_array_start; + const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + + GC_Message_Array_Entry *array_entry = &gconn->send_array[start]; + + if (array_entry_is_empty(array_entry)) { + return; + } + + if (mono_time_is_timeout(chat->mono_time, array_entry->time_added, GC_CONFIRMED_PEER_TIMEOUT)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + LOGGER_DEBUG(chat->log, "Send array stuck; timing out peer"); + return; + } + + for (uint16_t i = start; i != end; i = (i + 1) % GCC_BUFFER_SIZE) { + array_entry = &gconn->send_array[i]; + + if (array_entry_is_empty(array_entry)) { + continue; + } + + if (tm == array_entry->last_send_try) { + continue; + } + + const uint64_t delta = array_entry->last_send_try - array_entry->time_added; + array_entry->last_send_try = tm; + + /* if this occurrs less than once per second this won't be reliable */ + if (delta > 1 && is_power_of_2(delta)) { + gcc_encrypt_and_send_lossless_packet(chat, gconn, array_entry->data, array_entry->data_length, + array_entry->message_id, array_entry->packet_type); + } + } +} + +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length) +{ + if (packet == nullptr || length == 0) { + return false; + } + + bool direct_send_attempt = false; + + if (gcc_direct_conn_is_possible(chat, gconn)) { + if (gcc_conn_is_direct(chat->mono_time, gconn)) { + return (uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length; + } + + if ((uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length) { + direct_send_attempt = true; + } + } + + const int ret = send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, length); + return ret == 0 || direct_send_attempt; +} + +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint64_t message_id, uint8_t packet_type) +{ + const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSLESS); + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); + return false; + } + + const int enc_len = group_packet_wrap( + chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, + packet_size, data, length, message_id, packet_type, NET_PACKET_GC_LOSSLESS); + + if (enc_len < 0) { + LOGGER_ERROR(chat->log, "Failed to wrap packet (type: 0x%02x, error: %d)", packet_type, enc_len); + free(packet); + return false; + } + + if (!gcc_send_packet(chat, gconn, packet, (uint16_t)enc_len)) { + LOGGER_DEBUG(chat->log, "Failed to send packet (type: 0x%02x, enc_len: %d)", packet_type, enc_len); + free(packet); + return false; + } + + free(packet); + + return true; +} + +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk) +{ + encrypt_precompute(sender_pk, gconn->session_secret_key, gconn->session_shared_key); +} + +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn) +{ + return GCC_UDP_DIRECT_TIMEOUT + gconn->last_received_direct_time > mono_time_get(mono_time); +} + +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn) +{ + return !net_family_is_unspec(gconn->addr.ip_port.ip.family) && !net_family_is_unspec(net_family(chat->net)); +} + +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, + const uint8_t *part_message, uint16_t length) +{ + if (gconn == nullptr) { + return; + } + + if (gconn->pending_delete) { + return; + } + + gconn->pending_delete = true; + gconn->exit_info.exit_type = type; + + kill_tcp_connection_to(tcp_conn, gconn->tcp_connection_num); + + if (length > 0 && length <= MAX_GC_PART_MESSAGE_SIZE && part_message != nullptr) { + memcpy(gconn->exit_info.part_message, part_message, length); + gconn->exit_info.length = length; + } +} + +void gcc_peer_cleanup(GC_Connection *gconn) +{ + for (size_t i = 0; i < GCC_BUFFER_SIZE; ++i) { + free(gconn->send_array[i].data); + free(gconn->recv_array[i].data); + } + + free(gconn->recv_array); + free(gconn->send_array); + + crypto_memunlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); + crypto_memunlock(gconn->session_shared_key, sizeof(gconn->session_shared_key)); + crypto_memzero(gconn, sizeof(GC_Connection)); +} + +void gcc_cleanup(const GC_Chat *chat) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_peer_cleanup(gconn); + } +} + +#endif // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_connection.h b/protocols/Tox/libtox/src/toxcore/group_connection.h new file mode 100644 index 0000000000..2202c7ab36 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_connection.h @@ -0,0 +1,189 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CONNECTION_H +#define GROUP_CONNECTION_H + +#include "group_common.h" + +/* Max number of TCP relays we share with a peer on handshake */ +#define GCC_MAX_TCP_SHARED_RELAYS 3 + +/** Marks a peer for deletion. If gconn is null or already marked for deletion this function has no effect. */ +non_null(1, 2) nullable(4) +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, + const uint8_t *part_message, uint16_t length); + +/** @brief Decides if message need to be put in recv_array or immediately handled. + * + * Return 3 if message is in correct sequence and is a fragment packet. + * Return 2 if message is in correct sequence and may be handled immediately. + * Return 1 if packet is out of sequence and added to recv_array. + * Return 0 if message is a duplicate. + * Return -1 on failure + */ +non_null(1, 2, 3) nullable(4) +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + bool direct_conn); + +/** @brief Handles a packet fragment. + * + * If the fragment is incomplete, it gets stored in the recv + * array. Otherwise the segment is re-assembled into a complete + * payload and processed. + * + * Return 1 if fragment is successfully handled and is not the end of the sequence. + * Return 0 if fragment is the end of a sequence and successfully handled. + * Return -1 on failure. + */ +non_null(1, 2, 4) nullable(5, 9) +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, + const uint8_t *chunk, uint16_t length, uint8_t packet_type, uint64_t message_id, + void *userdata); + +/** @brief Return array index for message_id */ +uint16_t gcc_get_array_index(uint64_t message_id); + +/** @brief Removes send_array item with message_id. + * + * Return true on success. + */ +non_null() +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id); + +/** @brief Sets the send_message_id and send_array_start for `gconn` to `id`. + * + * This should only be used to initialize a new lossless connection. + */ +non_null() +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id); + +/** @brief Sets the received_message_id for `gconn` to `id`. */ +non_null() +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id); + +/** + * @brief Returns true if the ip_port is set for gconn. + */ +non_null() +bool gcc_ip_port_is_set(const GC_Connection *gconn); + +/** + * @brief Sets the ip_port for gconn to ipp. + * + * If ipp is not set this function has no effect. + */ +non_null(1) nullable(2) +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp); + +/** @brief Copies a random TCP relay node from gconn to tcp_node. + * + * Return true on success. + */ +non_null() +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn); + +/** @brief Saves tcp_node to gconn's list of connected tcp relays. + * + * If relays list is full a random node is overwritten with the new node. + * + * Return 0 on success. + * Return -1 on failure. + * Return -2 if node is already in list. + */ +non_null() +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node); + +/** @brief Checks for and handles messages that are in proper sequence in gconn's recv_array. + * This should always be called after a new packet is successfully handled. + */ +non_null(1, 2, 3) nullable(5) +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + void *userdata); + +/** @brief Attempts to re-send lossless packets that have not yet received an ack. */ +non_null() +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn); + +/** + * Uses public encryption key `sender_pk` and the shared secret key associated with `gconn` + * to generate a shared 32-byte encryption key that can be used by the owners of both keys for symmetric + * encryption and decryption. + * + * Puts the result in the shared session key buffer for `gconn`, which must have room for + * CRYPTO_SHARED_KEY_SIZE bytes. This resulting shared key should be treated as a secret key. + */ +non_null() +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk); + +/** @brief Return true if we have a direct connection with `gconn`. */ +non_null() +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn); + +/** @brief Return true if a direct UDP connection is possible with `gconn`. */ +non_null() +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Sends a packet to the peer associated with gconn. + * + * This is a lower level function that does not encrypt or wrap the packet. + * + * Return true on success. + */ +non_null() +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length); + +/** @brief Sends a lossless packet to `gconn` comprised of `data` of size `length`. + * + * This function will add the packet to the lossless send array, encrypt/wrap it using the + * shared key associated with `gconn`, and send it over the wire. + * + * Return 0 on success. + * Return -1 if the packet couldn't be added to the send array. + * Return -2 if the packet failed to be encrypted or failed to send. + */ +non_null(1, 2) nullable(3) +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type); + +/** @brief Splits a lossless packet up into fragments, wraps each fragment in a GP_FRAGMENT + * header, encrypts them, and send them in succession. + * + * This function will first try to add each packet fragment to the send array as an atomic + * unit. If any chunk fails to be added the process will be reversed and an error will be + * returned. Otherwise it will then try to send all the fragments in succession. + * + * Return true if all fragments are successfully added to the send array. + */ +non_null() +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type); + + +/** @brief Encrypts `data` of `length` bytes, designated by `message_id`, using the shared key + * associated with `gconn` and sends lossless packet over the wire. + * + * This function does not add the packet to the send array. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint64_t message_id, uint8_t packet_type); + +/** @brief Called when a peer leaves the group. */ +non_null() +void gcc_peer_cleanup(GC_Connection *gconn); + +/** @brief Called on group exit. */ +non_null() +void gcc_cleanup(const GC_Chat *chat); + +#endif // GROUP_CONNECTION_H diff --git a/protocols/Tox/libtox/src/toxcore/group_moderation.c b/protocols/Tox/libtox/src/toxcore/group_moderation.c index dea38a648b..b16c397cbe 100644 --- a/protocols/Tox/libtox/src/toxcore/group_moderation.c +++ b/protocols/Tox/libtox/src/toxcore/group_moderation.c @@ -27,6 +27,10 @@ static_assert(MOD_MAX_NUM_SANCTIONS * MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_C "MOD_MAX_NUM_SANCTIONS must be able to fit inside the maximum allowed payload size"); static_assert(MOD_MAX_NUM_MODERATORS * MOD_LIST_ENTRY_SIZE <= MAX_PACKET_SIZE_NO_HEADERS, "MOD_MAX_NUM_MODERATORS must be able to fit insize the maximum allowed payload size"); +static_assert(MOD_MAX_NUM_MODERATORS <= MOD_MAX_NUM_MODERATORS_LIMIT, + "MOD_MAX_NUM_MODERATORS must be <= MOD_MAX_NUM_MODERATORS_LIMIT"); +static_assert(MOD_MAX_NUM_SANCTIONS <= MOD_MAX_NUM_SANCTIONS_LIMIT, + "MOD_MAX_NUM_SANCTIONS must be <= MOD_MAX_NUM_SANCTIONS_LIMIT"); uint16_t mod_list_packed_size(const Moderation *moderation) { @@ -398,7 +402,7 @@ int sanctions_list_unpack(Mod_Sanction *sanctions, Mod_Sanction_Creds *creds, ui */ non_null(4) nullable(1) static bool sanctions_list_make_hash(const Mod_Sanction *sanctions, uint32_t new_version, uint16_t num_sanctions, - uint8_t *hash) + uint8_t *hash) { if (num_sanctions == 0 || sanctions == nullptr) { memset(hash, 0, MOD_SANCTION_HASH_SIZE); @@ -568,7 +572,7 @@ bool sanctions_list_check_integrity(const Moderation *moderation, const Mod_Sanc } /** @brief Validates a sanctions list if credentials are supplied. If successful, - * or if no credentials are supplid, assigns new sanctions list and credentials + * or if no credentials are supplied, assigns new sanctions list and credentials * to moderation object. * * @param moderation The moderation object being operated on. diff --git a/protocols/Tox/libtox/src/toxcore/group_moderation.h b/protocols/Tox/libtox/src/toxcore/group_moderation.h index 36b44a42f0..8b2a73869a 100644 --- a/protocols/Tox/libtox/src/toxcore/group_moderation.h +++ b/protocols/Tox/libtox/src/toxcore/group_moderation.h @@ -36,9 +36,17 @@ extern "C" { /* The max size of a groupchat packet with 100 bytes reserved for header data */ #define MAX_PACKET_SIZE_NO_HEADERS 49900 -/* These values must take into account the maximum allowed packet size and headers. */ -#define MOD_MAX_NUM_MODERATORS (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) -#define MOD_MAX_NUM_SANCTIONS (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) +/* The maximum possible number of moderators that can be sent in a group packet sequence. */ +#define MOD_MAX_NUM_MODERATORS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) + +/* The maximum number of moderators that we allow in a group: 100 */ +#define MOD_MAX_NUM_MODERATORS ((MOD_MAX_NUM_MODERATORS_LIMIT / 16) + 3) + +/* The maximum number of sanctions that be sent in a group packet sequence. */ +#define MOD_MAX_NUM_SANCTIONS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) + +/* The maximum number of sanctions that we allow in a group: 30 */ +#define MOD_MAX_NUM_SANCTIONS (MOD_MAX_NUM_SANCTIONS_LIMIT / 12) typedef enum Mod_Sanction_Type { SA_OBSERVER = 0x00, diff --git a/protocols/Tox/libtox/src/toxcore/group_onion_announce.c b/protocols/Tox/libtox/src/toxcore/group_onion_announce.c new file mode 100644 index 0000000000..b797770e52 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_onion_announce.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#include "group_onion_announce.h" + +#include <assert.h> +#include <string.h> + +#include "ccompat.h" + +static_assert(GCA_ANNOUNCE_MAX_SIZE <= ONION_MAX_EXTRA_DATA_SIZE, + "GC_Announce does not fit into the onion packet extra data"); + +static pack_extra_data_cb pack_group_announces; +non_null() +static int pack_group_announces(void *object, const Logger *logger, const Mono_Time *mono_time, + uint8_t num_nodes, uint8_t *plain, uint16_t plain_size, + uint8_t *response, uint16_t response_size, uint16_t offset) +{ + GC_Announces_List *gc_announces_list = (GC_Announces_List *)object; + GC_Public_Announce public_announce; + + if (gca_unpack_public_announce(logger, plain, plain_size, + &public_announce) == -1) { + LOGGER_WARNING(logger, "Failed to unpack public group announce"); + return -1; + } + + const GC_Peer_Announce *new_announce = gca_add_announce(mono_time, gc_announces_list, &public_announce); + + if (new_announce == nullptr) { + LOGGER_ERROR(logger, "Failed to add group announce"); + return -1; + } + + GC_Announce gc_announces[GCA_MAX_SENT_ANNOUNCES]; + const int num_ann = gca_get_announces(gc_announces_list, + gc_announces, + GCA_MAX_SENT_ANNOUNCES, + public_announce.chat_public_key, + new_announce->base_announce.peer_public_key); + + if (num_ann < 0) { + LOGGER_ERROR(logger, "failed to get group announce"); + return -1; + } + + assert(num_ann <= UINT8_MAX); + + size_t announces_length = 0; + + if (gca_pack_announces_list(logger, response + offset, response_size - offset, gc_announces, (uint8_t)num_ann, + &announces_length) != num_ann) { + LOGGER_WARNING(logger, "Failed to pack group announces list"); + return -1; + } + + return announces_length; +} + +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a) +{ + onion_announce_extra_data_callback(onion_a, GCA_MAX_SENT_ANNOUNCES * sizeof(GC_Announce), pack_group_announces, + group_announce); +} + +#ifndef VANILLA_NACL + +int create_gca_announce_request( + const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, + const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, + const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, + const uint8_t *gc_data, uint16_t gc_data_length) +{ + if (max_packet_length < ONION_ANNOUNCE_REQUEST_MAX_SIZE || gc_data_length == 0) { + return -1; + } + + uint8_t plain[ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + GCA_ANNOUNCE_MAX_SIZE]; + uint8_t *position_in_plain = plain; + const size_t encrypted_size = sizeof(plain) - GCA_ANNOUNCE_MAX_SIZE + gc_data_length; + + memcpy(plain, ping_id, ONION_PING_ID_SIZE); + position_in_plain += ONION_PING_ID_SIZE; + + memcpy(position_in_plain, client_id, CRYPTO_PUBLIC_KEY_SIZE); + position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + + memcpy(position_in_plain, data_public_key, CRYPTO_PUBLIC_KEY_SIZE); + position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + + memcpy(position_in_plain, &sendback_data, sizeof(sendback_data)); + position_in_plain += sizeof(sendback_data); + + memcpy(position_in_plain, gc_data, gc_data_length); + + packet[0] = NET_PACKET_ANNOUNCE_REQUEST; + random_nonce(rng, packet + 1); + memcpy(packet + 1 + CRYPTO_NONCE_SIZE, public_key, CRYPTO_PUBLIC_KEY_SIZE); + + const int len = encrypt_data(dest_client_id, secret_key, packet + 1, plain, + encrypted_size, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE); + + const uint32_t full_length = (uint32_t)len + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE; + + if (full_length != ONION_ANNOUNCE_REQUEST_MIN_SIZE + gc_data_length) { + return -1; + } + + return full_length; +} +#endif // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_onion_announce.h b/protocols/Tox/libtox/src/toxcore/group_onion_announce.h new file mode 100644 index 0000000000..5c6d64ec59 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_onion_announce.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#ifndef C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H +#define C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H + +#include "group_announce.h" +#include "onion_announce.h" + +non_null() +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a); + +non_null() +int create_gca_announce_request( + const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, + const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, + const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, + const uint8_t *gc_data, uint16_t gc_data_length); + +#endif // C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H diff --git a/protocols/Tox/libtox/src/toxcore/group_pack.c b/protocols/Tox/libtox/src/toxcore/group_pack.c new file mode 100644 index 0000000000..ecdd965610 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_pack.c @@ -0,0 +1,423 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#include "group_pack.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "ccompat.h" +#include "util.h" + +non_null() +static bool load_unpack_state_values(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 8)) { + LOGGER_ERROR(chat->log, "Group state values array malformed"); + return false; + } + + bool manually_disconnected = false; + uint8_t privacy_state = 0; + uint8_t voice_state = 0; + + if (!(bin_unpack_bool(bu, &manually_disconnected) + && bin_unpack_u16(bu, &chat->shared_state.group_name_len) + && bin_unpack_u08(bu, &privacy_state) + && bin_unpack_u16(bu, &chat->shared_state.maxpeers) + && bin_unpack_u16(bu, &chat->shared_state.password_length) + && bin_unpack_u32(bu, &chat->shared_state.version) + && bin_unpack_u32(bu, &chat->shared_state.topic_lock) + && bin_unpack_u08(bu, &voice_state))) { + LOGGER_ERROR(chat->log, "Failed to unpack state value"); + return false; + } + + chat->connection_state = manually_disconnected ? CS_DISCONNECTED : CS_CONNECTING; + chat->shared_state.privacy_state = (Group_Privacy_State)privacy_state; + chat->shared_state.voice_state = (Group_Voice_State)voice_state; + + // we always load saved groups as private in case the group became private while we were offline. + // this will have no detrimental effect if the group is public, as the correct privacy + // state will be set via sync. + chat->join_type = HJ_PRIVATE; + + return true; +} + +non_null() +static bool load_unpack_state_bin(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 5)) { + LOGGER_ERROR(chat->log, "Group state binary array malformed"); + return false; + } + + if (!(bin_unpack_bin_fixed(bu, chat->shared_state_sig, SIGNATURE_SIZE) + && bin_unpack_bin_fixed(bu, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->shared_state.group_name, chat->shared_state.group_name_len) + && bin_unpack_bin_fixed(bu, chat->shared_state.password, chat->shared_state.password_length) + && bin_unpack_bin_fixed(bu, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack state binary data"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_topic_info(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 6)) { + LOGGER_ERROR(chat->log, "Group topic array malformed"); + return false; + } + + if (!(bin_unpack_u32(bu, &chat->topic_info.version) + && bin_unpack_u16(bu, &chat->topic_info.length) + && bin_unpack_u16(bu, &chat->topic_info.checksum) + && bin_unpack_bin_fixed(bu, chat->topic_info.topic, chat->topic_info.length) + && bin_unpack_bin_fixed(bu, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->topic_sig, SIGNATURE_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack topic info"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_mod_list(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 2)) { + LOGGER_ERROR(chat->log, "Group mod list array malformed"); + return false; + } + + if (!bin_unpack_u16(bu, &chat->moderation.num_mods)) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list value"); + return false; + } + + if (chat->moderation.num_mods == 0) { + bin_unpack_nil(bu); + return true; + } + + if (chat->moderation.num_mods > MOD_MAX_NUM_MODERATORS) { + LOGGER_ERROR(chat->log, "moderation count %u exceeds maximum %u", chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); + return false; + } + + uint8_t *packed_mod_list = (uint8_t *)malloc(chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE); + + if (packed_mod_list == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packed mod list"); + return false; + } + + const size_t packed_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + + if (!bin_unpack_bin_fixed(bu, packed_mod_list, packed_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list binary data"); + free(packed_mod_list); + return false; + } + + if (mod_list_unpack(&chat->moderation, packed_mod_list, packed_size, chat->moderation.num_mods) == -1) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list info"); + free(packed_mod_list); + return false; + } + + free(packed_mod_list); + + return true; +} + +non_null() +static bool load_unpack_keys(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 4)) { + LOGGER_ERROR(chat->log, "Group keys array malformed"); + return false; + } + + if (!(bin_unpack_bin_fixed(bu, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->chat_secret_key, EXT_SECRET_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->self_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->self_secret_key, EXT_SECRET_KEY_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack keys"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_self_info(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 4)) { + LOGGER_ERROR(chat->log, "Group self info array malformed"); + return false; + } + + uint8_t self_nick[MAX_GC_NICK_SIZE]; + uint16_t self_nick_len = 0; + uint8_t self_role = GR_USER; + uint8_t self_status = GS_NONE; + + if (!(bin_unpack_u16(bu, &self_nick_len) + && bin_unpack_u08(bu, &self_role) + && bin_unpack_u08(bu, &self_status))) { + LOGGER_ERROR(chat->log, "Failed to unpack self values"); + return false; + } + + assert(self_nick_len <= MAX_GC_NICK_SIZE); + + if (!bin_unpack_bin_fixed(bu, self_nick, self_nick_len)) { + LOGGER_ERROR(chat->log, "Failed to unpack self nick bytes"); + return false; + } + + // we have to add ourself before setting self info + if (peer_add(chat, nullptr, chat->self_public_key) != 0) { + LOGGER_ERROR(chat->log, "Failed to add self to peer list"); + return false; + } + + assert(chat->numpeers > 0); + + GC_Peer *self = &chat->group[0]; + + memcpy(self->gconn.addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + memcpy(self->nick, self_nick, self_nick_len); + self->nick_length = self_nick_len; + self->role = (Group_Role)self_role; + self->status = (Group_Peer_Status)self_status; + self->gconn.confirmed = true; + + return true; +} + +non_null() +static bool load_unpack_saved_peers(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 2)) { + LOGGER_ERROR(chat->log, "Group saved peers array malformed"); + return false; + } + + // Saved peers + uint16_t saved_peers_size = 0; + + if (!bin_unpack_u16(bu, &saved_peers_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers value"); + return false; + } + + if (saved_peers_size == 0) { + bin_unpack_nil(bu); + return true; + } + + uint8_t *saved_peers = (uint8_t *)malloc(saved_peers_size * GC_SAVED_PEER_SIZE); + + if (saved_peers == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peer list"); + return false; + } + + if (!bin_unpack_bin_fixed(bu, saved_peers, saved_peers_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers binary data"); + free(saved_peers); + return false; + } + + if (unpack_gc_saved_peers(chat, saved_peers, saved_peers_size) == -1) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers"); // recoverable error + } + + free(saved_peers); + + return true; +} + +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 7)) { + LOGGER_ERROR(chat->log, "Group info array malformed"); + return false; + } + + return load_unpack_state_values(chat, bu) + && load_unpack_state_bin(chat, bu) + && load_unpack_topic_info(chat, bu) + && load_unpack_mod_list(chat, bu) + && load_unpack_keys(chat, bu) + && load_unpack_self_info(chat, bu) + && load_unpack_saved_peers(chat, bu); +} + +non_null() +static void save_pack_state_values(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 8); + bin_pack_bool(bp, chat->connection_state == CS_DISCONNECTED); // 1 + bin_pack_u16(bp, chat->shared_state.group_name_len); // 2 + bin_pack_u08(bp, chat->shared_state.privacy_state); // 3 + bin_pack_u16(bp, chat->shared_state.maxpeers); // 4 + bin_pack_u16(bp, chat->shared_state.password_length); // 5 + bin_pack_u32(bp, chat->shared_state.version); // 6 + bin_pack_u32(bp, chat->shared_state.topic_lock); // 7 + bin_pack_u08(bp, chat->shared_state.voice_state); // 8 +} + +non_null() +static void save_pack_state_bin(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 5); + + bin_pack_bin(bp, chat->shared_state_sig, SIGNATURE_SIZE); // 1 + bin_pack_bin(bp, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE); // 2 + bin_pack_bin(bp, chat->shared_state.group_name, chat->shared_state.group_name_len); // 3 + bin_pack_bin(bp, chat->shared_state.password, chat->shared_state.password_length); // 4 + bin_pack_bin(bp, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); // 5 +} + +non_null() +static void save_pack_topic_info(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 6); + + bin_pack_u32(bp, chat->topic_info.version); // 1 + bin_pack_u16(bp, chat->topic_info.length); // 2 + bin_pack_u16(bp, chat->topic_info.checksum); // 3 + bin_pack_bin(bp, chat->topic_info.topic, chat->topic_info.length); // 4 + bin_pack_bin(bp, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE); // 5 + bin_pack_bin(bp, chat->topic_sig, SIGNATURE_SIZE); // 6 +} + +non_null() +static void save_pack_mod_list(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 2); + + const uint16_t num_mods = min_u16(chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); + + if (num_mods == 0) { + bin_pack_u16(bp, num_mods); // 1 + bin_pack_nil(bp); // 2 + return; + } + + uint8_t *packed_mod_list = (uint8_t *)malloc(num_mods * MOD_LIST_ENTRY_SIZE); + + // we can still recover without the mod list + if (packed_mod_list == nullptr) { + bin_pack_u16(bp, 0); // 1 + bin_pack_nil(bp); // 2 + LOGGER_ERROR(chat->log, "Failed to allocate memory for moderation list"); + return; + } + + bin_pack_u16(bp, num_mods); // 1 + + mod_list_pack(&chat->moderation, packed_mod_list); + + const size_t packed_size = num_mods * MOD_LIST_ENTRY_SIZE; + + bin_pack_bin(bp, packed_mod_list, packed_size); // 2 + + free(packed_mod_list); +} + +non_null() +static void save_pack_keys(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 4); + + bin_pack_bin(bp, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE); // 1 + bin_pack_bin(bp, chat->chat_secret_key, EXT_SECRET_KEY_SIZE); // 2 + bin_pack_bin(bp, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); // 3 + bin_pack_bin(bp, chat->self_secret_key, EXT_SECRET_KEY_SIZE); // 4 +} + +non_null() +static void save_pack_self_info(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 4); + + const GC_Peer *self = &chat->group[0]; + + assert(self->nick_length <= MAX_GC_NICK_SIZE); + + bin_pack_u16(bp, self->nick_length); // 1 + bin_pack_u08(bp, (uint8_t)self->role); // 2 + bin_pack_u08(bp, (uint8_t)self->status); // 3 + bin_pack_bin(bp, self->nick, self->nick_length); // 4 +} + +non_null() +static void save_pack_saved_peers(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 2); + + uint8_t *saved_peers = (uint8_t *)malloc(GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE); + + // we can still recover without the saved peers list + if (saved_peers == nullptr) { + bin_pack_u16(bp, 0); // 1 + bin_pack_nil(bp); // 2 + LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peers list"); + return; + } + + uint16_t packed_size = 0; + const int count = pack_gc_saved_peers(chat, saved_peers, GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE, &packed_size); + + if (count < 0) { + LOGGER_ERROR(chat->log, "Failed to pack saved peers"); + } + + bin_pack_u16(bp, packed_size); // 1 + + if (packed_size == 0) { + bin_pack_nil(bp); // 2 + free(saved_peers); + return; + } + + bin_pack_bin(bp, saved_peers, packed_size); // 2 + + free(saved_peers); +} + +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp) +{ + if (chat->numpeers == 0) { + LOGGER_ERROR(chat->log, "Failed to pack group: numpeers is 0"); + return; + } + + bin_pack_array(bp, 7); + + save_pack_state_values(chat, bp); // 1 + save_pack_state_bin(chat, bp); // 2 + save_pack_topic_info(chat, bp); // 3 + save_pack_mod_list(chat, bp); // 4 + save_pack_keys(chat, bp); // 5 + save_pack_self_info(chat, bp); // 6 + save_pack_saved_peers(chat, bp); // 7 +} diff --git a/protocols/Tox/libtox/src/toxcore/group_pack.h b/protocols/Tox/libtox/src/toxcore/group_pack.h new file mode 100644 index 0000000000..ae831ac708 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_pack.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#ifndef GROUP_PACK_H +#define GROUP_PACK_H + +#include <stdbool.h> + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_common.h" + +/** + * Packs group data from `chat` into `mp` in binary format. Parallel to the + * `gc_load_unpack_group` function. + */ +non_null() +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp); + +/** + * Unpacks binary group data from `obj` into `chat`. Parallel to the `gc_save_pack_group` + * function. + * + * Return true if unpacking is successful. + */ +non_null() +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu); + +#endif // GROUP_PACK_H diff --git a/protocols/Tox/libtox/src/toxcore/net_crypto.c b/protocols/Tox/libtox/src/toxcore/net_crypto.c index c34a45734c..71f6e39e64 100644 --- a/protocols/Tox/libtox/src/toxcore/net_crypto.c +++ b/protocols/Tox/libtox/src/toxcore/net_crypto.c @@ -223,15 +223,15 @@ static int create_cookie_request(const Net_Crypto *c, uint8_t *packet, const uin memcpy(plain, c->self_public_key, CRYPTO_PUBLIC_KEY_SIZE); memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, padding, CRYPTO_PUBLIC_KEY_SIZE); memcpy(plain + (CRYPTO_PUBLIC_KEY_SIZE * 2), &number, sizeof(uint64_t)); - - dht_get_shared_key_sent(c->dht, shared_key, dht_public_key); + const uint8_t *tmp_shared_key = dht_get_shared_key_sent(c->dht, dht_public_key); + memcpy(shared_key, tmp_shared_key, CRYPTO_SHARED_KEY_SIZE); uint8_t nonce[CRYPTO_NONCE_SIZE]; random_nonce(c->rng, nonce); packet[0] = NET_PACKET_COOKIE_REQUEST; memcpy(packet + 1, dht_get_self_public_key(c->dht), CRYPTO_PUBLIC_KEY_SIZE); memcpy(packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); const int len = encrypt_data_symmetric(shared_key, nonce, plain, sizeof(plain), - packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); + packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); if (len != COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE) { return -1; @@ -341,10 +341,11 @@ static int handle_cookie_request(const Net_Crypto *c, uint8_t *request_plain, ui } memcpy(dht_public_key, packet + 1, CRYPTO_PUBLIC_KEY_SIZE); - dht_get_shared_key_sent(c->dht, shared_key, dht_public_key); + const uint8_t *tmp_shared_key = dht_get_shared_key_sent(c->dht, dht_public_key); + memcpy(shared_key, tmp_shared_key, CRYPTO_SHARED_KEY_SIZE); const int len = decrypt_data_symmetric(shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, - packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, - request_plain); + packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, + request_plain); if (len != COOKIE_REQUEST_PLAIN_LENGTH) { return -1; @@ -488,7 +489,7 @@ static int create_crypto_handshake(const Net_Crypto *c, uint8_t *packet, const u random_nonce(c->rng, packet + 1 + COOKIE_LENGTH); const int len = encrypt_data(peer_real_pk, c->self_secret_key, packet + 1 + COOKIE_LENGTH, plain, sizeof(plain), - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); if (len != HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE)) { return -1; @@ -541,8 +542,8 @@ static bool handle_crypto_handshake(const Net_Crypto *c, uint8_t *nonce, uint8_t uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; const int len = decrypt_data(cookie_plain, c->self_secret_key, packet + 1 + COOKIE_LENGTH, - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, - HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, + HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); if (len != sizeof(plain)) { return false; @@ -1273,7 +1274,7 @@ static int handle_data_packet(const Net_Crypto *c, int crypt_connection_id, uint const uint16_t diff = num - num_cur_nonce; increment_nonce_number(nonce, diff); const int len = decrypt_data_symmetric(conn->shared_key, nonce, packet + 1 + sizeof(uint16_t), - length - (1 + sizeof(uint16_t)), data); + length - (1 + sizeof(uint16_t)), data); if ((unsigned int)len != length - crypto_packet_overhead) { return -1; @@ -1515,7 +1516,7 @@ static void connection_kill(Net_Crypto *c, int crypt_connection_id, void *userda if (conn->connection_status_callback != nullptr) { conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, - false, userdata); + false, userdata); } while (true) { /* TODO(irungentoo): is this really the best way to do this? */ @@ -1602,7 +1603,7 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const if (conn->connection_status_callback != nullptr) { conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, - true, userdata); + true, userdata); } } @@ -1616,8 +1617,8 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const } const int requested = handle_request_packet(c->mono_time, &conn->send_array, - real_data, real_length, - &rtt_calc_time, rtt_time); + real_data, real_length, + &rtt_calc_time, rtt_time); if (requested == -1) { return -1; @@ -2657,7 +2658,7 @@ static void send_crypto_packets(Net_Crypto *c) &conn->recv_array) + 1.0) / (conn->packet_recv_rate + 1.0)); const double request_packet_interval2 = ((CRYPTO_PACKET_MIN_RATE / conn->packet_recv_rate) * - (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL; + (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL; if (request_packet_interval2 < request_packet_interval) { request_packet_interval = request_packet_interval2; @@ -2750,7 +2751,7 @@ static void send_crypto_packets(Net_Crypto *c) PACKET_COUNTER_AVERAGE_INTERVAL)); const double min_speed_request = 1000.0 * (((double)(total_sent + total_resent)) / ( - (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL)); + (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL)); if (min_speed < CRYPTO_PACKET_MIN_RATE) { min_speed = CRYPTO_PACKET_MIN_RATE; diff --git a/protocols/Tox/libtox/src/toxcore/net_crypto.h b/protocols/Tox/libtox/src/toxcore/net_crypto.h index f6e062f3be..0c3dfd0d45 100644 --- a/protocols/Tox/libtox/src/toxcore/net_crypto.h +++ b/protocols/Tox/libtox/src/toxcore/net_crypto.h @@ -59,6 +59,7 @@ #define PACKET_ID_FILE_SENDREQUEST 80 #define PACKET_ID_FILE_CONTROL 81 #define PACKET_ID_FILE_DATA 82 +#define PACKET_ID_INVITE_GROUPCHAT 95 #define PACKET_ID_INVITE_CONFERENCE 96 #define PACKET_ID_ONLINE_PACKET 97 #define PACKET_ID_DIRECT_CONFERENCE 98 diff --git a/protocols/Tox/libtox/src/toxcore/network.c b/protocols/Tox/libtox/src/toxcore/network.c index 35893b73dc..8544496c1c 100644 --- a/protocols/Tox/libtox/src/toxcore/network.c +++ b/protocols/Tox/libtox/src/toxcore/network.c @@ -655,6 +655,121 @@ static uint32_t data_1(uint16_t buflen, const uint8_t *buffer) return data; } +static const char *net_packet_type_name(Net_Packet_Type type) +{ + switch (type) { + case NET_PACKET_PING_REQUEST: + return "PING_REQUEST"; + + case NET_PACKET_PING_RESPONSE: + return "PING_RESPONSE"; + + case NET_PACKET_GET_NODES: + return "GET_NODES"; + + case NET_PACKET_SEND_NODES_IPV6: + return "SEND_NODES_IPV6"; + + case NET_PACKET_COOKIE_REQUEST: + return "COOKIE_REQUEST"; + + case NET_PACKET_COOKIE_RESPONSE: + return "COOKIE_RESPONSE"; + + case NET_PACKET_CRYPTO_HS: + return "CRYPTO_HS"; + + case NET_PACKET_CRYPTO_DATA: + return "CRYPTO_DATA"; + + case NET_PACKET_CRYPTO: + return "CRYPTO"; + + case NET_PACKET_GC_HANDSHAKE: + return "GC_HANDSHAKE"; + + case NET_PACKET_GC_LOSSLESS: + return "GC_LOSSLESS"; + + case NET_PACKET_GC_LOSSY: + return "GC_LOSSY"; + + case NET_PACKET_LAN_DISCOVERY: + return "LAN_DISCOVERY"; + + case NET_PACKET_ONION_SEND_INITIAL: + return "ONION_SEND_INITIAL"; + + case NET_PACKET_ONION_SEND_1: + return "ONION_SEND_1"; + + case NET_PACKET_ONION_SEND_2: + return "ONION_SEND_2"; + + case NET_PACKET_ANNOUNCE_REQUEST_OLD: + return "ANNOUNCE_REQUEST_OLD"; + + case NET_PACKET_ANNOUNCE_RESPONSE_OLD: + return "ANNOUNCE_RESPONSE_OLD"; + + case NET_PACKET_ONION_DATA_REQUEST: + return "ONION_DATA_REQUEST"; + + case NET_PACKET_ONION_DATA_RESPONSE: + return "ONION_DATA_RESPONSE"; + + case NET_PACKET_ANNOUNCE_REQUEST: + return "ANNOUNCE_REQUEST"; + + case NET_PACKET_ANNOUNCE_RESPONSE: + return "ANNOUNCE_RESPONSE"; + + case NET_PACKET_ONION_RECV_3: + return "ONION_RECV_3"; + + case NET_PACKET_ONION_RECV_2: + return "ONION_RECV_2"; + + case NET_PACKET_ONION_RECV_1: + return "ONION_RECV_1"; + + case NET_PACKET_FORWARD_REQUEST: + return "FORWARD_REQUEST"; + + case NET_PACKET_FORWARDING: + return "FORWARDING"; + + case NET_PACKET_FORWARD_REPLY: + return "FORWARD_REPLY"; + + case NET_PACKET_DATA_SEARCH_REQUEST: + return "DATA_SEARCH_REQUEST"; + + case NET_PACKET_DATA_SEARCH_RESPONSE: + return "DATA_SEARCH_RESPONSE"; + + case NET_PACKET_DATA_RETRIEVE_REQUEST: + return "DATA_RETRIEVE_REQUEST"; + + case NET_PACKET_DATA_RETRIEVE_RESPONSE: + return "DATA_RETRIEVE_RESPONSE"; + + case NET_PACKET_STORE_ANNOUNCE_REQUEST: + return "STORE_ANNOUNCE_REQUEST"; + + case NET_PACKET_STORE_ANNOUNCE_RESPONSE: + return "STORE_ANNOUNCE_RESPONSE"; + + case BOOTSTRAP_INFO_PACKET_ID: + return "BOOTSTRAP_INFO"; + + case NET_PACKET_MAX: + return "MAX"; + } + + return "<unknown>"; +} + non_null() static void loglogdata(const Logger *log, const char *message, const uint8_t *buffer, uint16_t buflen, const IP_Port *ip_port, long res) @@ -663,21 +778,24 @@ static void loglogdata(const Logger *log, const char *message, const uint8_t *bu Ip_Ntoa ip_str; const int error = net_error(); char *strerror = net_new_strerror(error); - LOGGER_TRACE(log, "[%2u] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", - buffer[0], message, min_u16(buflen, 999), 'E', + LOGGER_TRACE(log, "[%02x = %-20s] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", + buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, + min_u16(buflen, 999), 'E', net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), error, strerror, data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]); net_kill_strerror(strerror); } else if ((res > 0) && ((size_t)res <= buflen)) { Ip_Ntoa ip_str; - LOGGER_TRACE(log, "[%2u] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", - buffer[0], message, min_u16(res, 999), (size_t)res < buflen ? '<' : '=', + LOGGER_TRACE(log, "[%02x = %-20s] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", + buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, + min_u16(res, 999), (size_t)res < buflen ? '<' : '=', net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), 0, "OK", data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]); } else { /* empty or overwrite */ Ip_Ntoa ip_str; - LOGGER_TRACE(log, "[%2u] %s %lu%c%u %s:%u (%u: %s) | %08x%08x...%02x", - buffer[0], message, res, res == 0 ? '!' : '>', buflen, + LOGGER_TRACE(log, "[%02x = %-20s] %s %lu%c%u %s:%u (%u: %s) | %08x%08x...%02x", + buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, + res, res == 0 ? '!' : '>', buflen, net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), 0, "OK", data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]); } @@ -1616,7 +1734,7 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port) } else { Ip_Ntoa ip_str; LOGGER_ERROR(log, "cannot connect to %s:%d which is neither IPv4 nor IPv6", - net_ip_ntoa(&ip_port->ip, &ip_str), ip_port->port); + net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port)); return false; } @@ -1638,7 +1756,7 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port) if (!should_ignore_connect_error(error)) { char *net_strerror = net_new_strerror(error); LOGGER_ERROR(log, "failed to connect to %s:%d: %d (%s)", - net_ip_ntoa(&ip_port->ip, &ip_str), ip_port->port, error, net_strerror); + net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), error, net_strerror); net_kill_strerror(net_strerror); return false; } @@ -1649,18 +1767,6 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port) int32_t net_getipport(const char *node, IP_Port **res, int tox_type) { -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION - if ((true)) { - *res = (IP_Port *)calloc(1, sizeof(IP_Port)); - assert(*res != nullptr); - IP_Port *ip_port = *res; - ip_port->ip.ip.v4.uint32 = 0x7F000003; // 127.0.0.3 - ip_port->ip.family = *make_tox_family(AF_INET); - - return 1; - } -#endif - // Try parsing as IP address first. IP_Port parsed = {{{0}}}; @@ -1676,6 +1782,18 @@ int32_t net_getipport(const char *node, IP_Port **res, int tox_type) return 1; } +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if ((true)) { + *res = (IP_Port *)calloc(1, sizeof(IP_Port)); + assert(*res != nullptr); + IP_Port *ip_port = *res; + ip_port->ip.ip.v4.uint32 = net_htonl(0x7F000003); // 127.0.0.3 + ip_port->ip.family = *make_tox_family(AF_INET); + + return 1; + } +#endif + // It's not an IP address, so now we try doing a DNS lookup. struct addrinfo *infos; const int ret = getaddrinfo(node, nullptr, nullptr, &infos); @@ -1814,6 +1932,12 @@ uint16_t net_ntohs(uint16_t hostshort) return ntohs(hostshort); } +size_t net_pack_bool(uint8_t *bytes, bool v) +{ + bytes[0] = v ? 1 : 0; + return 1; +} + size_t net_pack_u16(uint8_t *bytes, uint16_t v) { bytes[0] = (v >> 8) & 0xff; @@ -1837,6 +1961,12 @@ size_t net_pack_u64(uint8_t *bytes, uint64_t v) return p - bytes; } +size_t net_unpack_bool(const uint8_t *bytes, bool *v) +{ + *v = bytes[0] != 0; + return 1; +} + size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v) { const uint8_t hi = bytes[0]; diff --git a/protocols/Tox/libtox/src/toxcore/network.h b/protocols/Tox/libtox/src/toxcore/network.h index a1fc6a74fc..ffc04ed6e4 100644 --- a/protocols/Tox/libtox/src/toxcore/network.h +++ b/protocols/Tox/libtox/src/toxcore/network.h @@ -109,10 +109,9 @@ typedef enum Net_Packet_Type { NET_PACKET_CRYPTO = 0x24, /* Encrypted data packet ID. */ NET_PACKET_LAN_DISCOVERY = 0x25, /* LAN discovery packet ID. */ - // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats - // NET_PACKET_GC_HANDSHAKE = 0x62, /* Group chat handshake packet ID */ - // NET_PACKET_GC_LOSSLESS = 0x63, /* Group chat lossless packet ID */ - // NET_PACKET_GC_LOSSY = 0x64, /* Group chat lossy packet ID */ + NET_PACKET_GC_HANDSHAKE = 0x62, /* Group chat handshake packet ID */ + NET_PACKET_GC_LOSSLESS = 0x63, /* Group chat lossless packet ID */ + NET_PACKET_GC_LOSSY = 0x64, /* Group chat lossy packet ID */ /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */ NET_PACKET_ONION_SEND_INITIAL = 0x8f, @@ -131,6 +130,17 @@ typedef enum Net_Packet_Type { NET_PACKET_ONION_RECV_2 = 0x9c, NET_PACKET_ONION_RECV_1 = 0x9d, + NET_PACKET_FORWARD_REQUEST = 0x9e, + NET_PACKET_FORWARDING = 0x9f, + NET_PACKET_FORWARD_REPLY = 0xa0, + + NET_PACKET_DATA_SEARCH_REQUEST = 0xa1, + NET_PACKET_DATA_SEARCH_RESPONSE = 0xa2, + NET_PACKET_DATA_RETRIEVE_REQUEST = 0xa3, + NET_PACKET_DATA_RETRIEVE_RESPONSE = 0xa4, + NET_PACKET_STORE_ANNOUNCE_REQUEST = 0xa5, + NET_PACKET_STORE_ANNOUNCE_RESPONSE = 0xa6, + BOOTSTRAP_INFO_PACKET_ID = 0xf1, /* Only used for bootstrap nodes */ NET_PACKET_MAX = 0xff, /* This type must remain within a single uint8. */ @@ -148,10 +158,9 @@ typedef enum Net_Packet_Type { NET_PACKET_CRYPTO = 0x20, /* Encrypted data packet ID. */ NET_PACKET_LAN_DISCOVERY = 0x21, /* LAN discovery packet ID. */ - // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats - // NET_PACKET_GC_HANDSHAKE = 0x5a, /* Group chat handshake packet ID */ - // NET_PACKET_GC_LOSSLESS = 0x5b, /* Group chat lossless packet ID */ - // NET_PACKET_GC_LOSSY = 0x5c, /* Group chat lossy packet ID */ + NET_PACKET_GC_HANDSHAKE = 0x5a, /* Group chat handshake packet ID */ + NET_PACKET_GC_LOSSLESS = 0x5b, /* Group chat lossless packet ID */ + NET_PACKET_GC_LOSSY = 0x5c, /* Group chat lossy packet ID */ /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */ NET_PACKET_ONION_SEND_INITIAL = 0x80, @@ -304,6 +313,8 @@ uint32_t net_ntohl(uint32_t hostlong); uint16_t net_ntohs(uint16_t hostshort); non_null() +size_t net_pack_bool(uint8_t *bytes, bool v); +non_null() size_t net_pack_u16(uint8_t *bytes, uint16_t v); non_null() size_t net_pack_u32(uint8_t *bytes, uint32_t v); @@ -311,6 +322,8 @@ non_null() size_t net_pack_u64(uint8_t *bytes, uint64_t v); non_null() +size_t net_unpack_bool(const uint8_t *bytes, bool *v); +non_null() size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v); non_null() size_t net_unpack_u32(const uint8_t *bytes, uint32_t *v); @@ -531,8 +544,9 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port); * Skip all addresses with socktype != type (use type = -1 to get all addresses) * To correctly deallocate array memory use `net_freeipport()` * - * return number of elements in res array - * and -1 on error. + * @return number of elements in res array. + * @retval 0 if res array empty. + * @retval -1 on error. */ non_null() int32_t net_getipport(const char *node, IP_Port **res, int tox_type); diff --git a/protocols/Tox/libtox/src/toxcore/onion.c b/protocols/Tox/libtox/src/toxcore/onion.c index d7e3f02665..63ff6d8520 100644 --- a/protocols/Tox/libtox/src/toxcore/onion.c +++ b/protocols/Tox/libtox/src/toxcore/onion.c @@ -27,6 +27,11 @@ #define KEY_REFRESH_INTERVAL (2 * 60 * 60) + +// Settings for the shared key cache +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 + /** Change symmetric keys every 2 hours to make paths expire eventually. */ non_null() static void change_symmetric_key(Onion *onion) @@ -314,9 +319,14 @@ static int handle_send_initial(void *object, const IP_Port *source, const uint8_ change_symmetric_key(onion); uint8_t plain[ONION_MAX_PACKET_SIZE]; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - get_shared_key(onion->mono_time, &onion->shared_keys_1, shared_key, dht_get_self_secret_key(onion->dht), - packet + 1 + CRYPTO_NONCE_SIZE); + const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; + const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_1, public_key); + + if (shared_key == nullptr) { + /* Error looking up/deriving the shared key */ + return 1; + } + const int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE, length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE), plain); @@ -385,9 +395,14 @@ static int handle_send_1(void *object, const IP_Port *source, const uint8_t *pac change_symmetric_key(onion); uint8_t plain[ONION_MAX_PACKET_SIZE]; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - get_shared_key(onion->mono_time, &onion->shared_keys_2, shared_key, dht_get_self_secret_key(onion->dht), - packet + 1 + CRYPTO_NONCE_SIZE); + const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; + const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_2, public_key); + + if (shared_key == nullptr) { + /* Error looking up/deriving the shared key */ + return 1; + } + int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE, length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + RETURN_1), plain); @@ -443,9 +458,14 @@ static int handle_send_2(void *object, const IP_Port *source, const uint8_t *pac change_symmetric_key(onion); uint8_t plain[ONION_MAX_PACKET_SIZE]; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - get_shared_key(onion->mono_time, &onion->shared_keys_3, shared_key, dht_get_self_secret_key(onion->dht), - packet + 1 + CRYPTO_NONCE_SIZE); + const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; + const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_3, public_key); + + if (shared_key == nullptr) { + /* Error looking up/deriving the shared key */ + return 1; + } + int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE, length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + RETURN_2), plain); @@ -457,7 +477,8 @@ static int handle_send_2(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = plain[SIZE_IPPORT]; - if (packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && packet_id != NET_PACKET_ONION_DATA_REQUEST) { + if (packet_id != NET_PACKET_ANNOUNCE_REQUEST && packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && + packet_id != NET_PACKET_ONION_DATA_REQUEST) { return 1; } @@ -507,7 +528,8 @@ static int handle_recv_3(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_3]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } @@ -555,7 +577,8 @@ static int handle_recv_2(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_2]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } @@ -603,7 +626,8 @@ static int handle_recv_1(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_1]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } @@ -664,6 +688,19 @@ Onion *new_onion(const Logger *log, const Mono_Time *mono_time, const Random *rn new_symmetric_key(rng, onion->secret_symmetric_key); onion->timestamp = mono_time_get(onion->mono_time); + const uint8_t *secret_key = dht_get_self_secret_key(dht); + onion->shared_keys_1 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + onion->shared_keys_2 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + onion->shared_keys_3 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + + if (onion->shared_keys_1 == nullptr || + onion->shared_keys_2 == nullptr || + onion->shared_keys_3 == nullptr) { + kill_onion(onion); + return nullptr; + } + + networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_INITIAL, &handle_send_initial, onion); networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_1, &handle_send_1, onion); networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_2, &handle_send_2, onion); @@ -691,5 +728,9 @@ void kill_onion(Onion *onion) crypto_memzero(onion->secret_symmetric_key, sizeof(onion->secret_symmetric_key)); + shared_key_cache_free(onion->shared_keys_1); + shared_key_cache_free(onion->shared_keys_2); + shared_key_cache_free(onion->shared_keys_3); + free(onion); } diff --git a/protocols/Tox/libtox/src/toxcore/onion.h b/protocols/Tox/libtox/src/toxcore/onion.h index 3da2137c44..5c8f920b44 100644 --- a/protocols/Tox/libtox/src/toxcore/onion.h +++ b/protocols/Tox/libtox/src/toxcore/onion.h @@ -12,6 +12,7 @@ #include "DHT.h" #include "logger.h" #include "mono_time.h" +#include "shared_key_cache.h" typedef int onion_recv_1_cb(void *object, const IP_Port *dest, const uint8_t *data, uint16_t length); @@ -24,9 +25,9 @@ typedef struct Onion { uint8_t secret_symmetric_key[CRYPTO_SYMMETRIC_KEY_SIZE]; uint64_t timestamp; - Shared_Keys shared_keys_1; - Shared_Keys shared_keys_2; - Shared_Keys shared_keys_3; + Shared_Key_Cache *shared_keys_1; + Shared_Key_Cache *shared_keys_2; + Shared_Key_Cache *shared_keys_3; onion_recv_1_cb *recv_1_function; void *callback_object; diff --git a/protocols/Tox/libtox/src/toxcore/onion_announce.c b/protocols/Tox/libtox/src/toxcore/onion_announce.c index f12e560837..fb04d21f56 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_announce.c +++ b/protocols/Tox/libtox/src/toxcore/onion_announce.c @@ -16,6 +16,7 @@ #include "LAN_discovery.h" #include "ccompat.h" #include "mono_time.h" +#include "shared_key_cache.h" #include "util.h" #define PING_ID_TIMEOUT ONION_ANNOUNCE_TIMEOUT @@ -31,6 +32,10 @@ #define ONION_MINIMAL_SIZE (ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE * 2 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH) +/* Settings for the shared key cache */ +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 + static_assert(ONION_PING_ID_SIZE == CRYPTO_PUBLIC_KEY_SIZE, "announce response packets assume that ONION_PING_ID_SIZE is equal to CRYPTO_PUBLIC_KEY_SIZE"); @@ -51,7 +56,7 @@ struct Onion_Announce { Onion_Announce_Entry entries[ONION_ANNOUNCE_MAX_ENTRIES]; uint8_t hmac_key[CRYPTO_HMAC_KEY_SIZE]; - Shared_Keys shared_keys_recv; + Shared_Key_Cache *shared_keys_recv; uint16_t extra_data_max_size; pack_extra_data_cb *extra_data_callback; @@ -346,7 +351,6 @@ non_null() static int add_to_entries(Onion_Announce *onion_a, const IP_Port *ret_ip_port, const uint8_t *public_key, const uint8_t *data_public_key, const uint8_t *ret) { - int pos = in_entries(onion_a, public_key); if (pos == -1) { @@ -427,9 +431,12 @@ static int handle_announce_request_common( pack_extra_data_cb *pack_extra_data_callback) { const uint8_t *packet_public_key = packet + 1 + CRYPTO_NONCE_SIZE; - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - get_shared_key(onion_a->mono_time, &onion_a->shared_keys_recv, shared_key, dht_get_self_secret_key(onion_a->dht), - packet_public_key); + const uint8_t *shared_key = shared_key_cache_lookup(onion_a->shared_keys_recv, packet_public_key); + + if (shared_key == nullptr) { + /* Error looking up/deriving the shared key */ + return 1; + } uint8_t *plain = (uint8_t *)malloc(plain_size); @@ -654,13 +661,16 @@ Onion_Announce *new_onion_announce(const Logger *log, const Random *rng, const M onion_a->extra_data_object = nullptr; new_hmac_key(rng, onion_a->hmac_key); + onion_a->shared_keys_recv = shared_key_cache_new(mono_time, dht_get_self_secret_key(dht), KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + if (onion_a->shared_keys_recv == nullptr) { + kill_onion_announce(onion_a); + return nullptr; + } + networking_registerhandler(onion_a->net, NET_PACKET_ANNOUNCE_REQUEST, &handle_announce_request, onion_a); networking_registerhandler(onion_a->net, NET_PACKET_ANNOUNCE_REQUEST_OLD, &handle_announce_request_old, onion_a); networking_registerhandler(onion_a->net, NET_PACKET_ONION_DATA_REQUEST, &handle_data_request, onion_a); - // TODO(Jfreegman): Remove this when we merge the rest of new groupchats - onion_announce_extra_data_callback(onion_a, 0, nullptr, nullptr); - return onion_a; } @@ -675,6 +685,7 @@ void kill_onion_announce(Onion_Announce *onion_a) networking_registerhandler(onion_a->net, NET_PACKET_ONION_DATA_REQUEST, nullptr, nullptr); crypto_memzero(onion_a->hmac_key, CRYPTO_HMAC_KEY_SIZE); + shared_key_cache_free(onion_a->shared_keys_recv); free(onion_a); } diff --git a/protocols/Tox/libtox/src/toxcore/onion_announce.h b/protocols/Tox/libtox/src/toxcore/onion_announce.h index 8cf1f49539..24303abccb 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_announce.h +++ b/protocols/Tox/libtox/src/toxcore/onion_announce.h @@ -20,6 +20,7 @@ #define ONION_ANNOUNCE_SENDBACK_DATA_LENGTH (sizeof(uint64_t)) +#define MAX_SENT_GC_NODES 1 #define ONION_ANNOUNCE_REQUEST_MIN_SIZE (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_MAC_SIZE) #define ONION_ANNOUNCE_REQUEST_MAX_SIZE (ONION_ANNOUNCE_REQUEST_MIN_SIZE + ONION_MAX_EXTRA_DATA_SIZE) @@ -125,7 +126,7 @@ typedef int pack_extra_data_cb(void *object, const Logger *logger, const Mono_Ti uint8_t num_nodes, uint8_t *plain, uint16_t plain_size, uint8_t *response, uint16_t response_size, uint16_t offset); -non_null(1) nullable(3, 4) +non_null() void onion_announce_extra_data_callback(Onion_Announce *onion_a, uint16_t extra_data_max_size, pack_extra_data_cb *extra_data_callback, void *extra_data_object); diff --git a/protocols/Tox/libtox/src/toxcore/onion_client.c b/protocols/Tox/libtox/src/toxcore/onion_client.c index 91168c661c..ff63473e10 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_client.c +++ b/protocols/Tox/libtox/src/toxcore/onion_client.c @@ -15,6 +15,7 @@ #include "LAN_discovery.h" #include "ccompat.h" +#include "group_onion_announce.h" #include "mono_time.h" #include "util.h" @@ -54,7 +55,7 @@ typedef struct Last_Pinged { uint64_t timestamp; } Last_Pinged; -typedef struct Onion_Friend { +struct Onion_Friend { bool is_valid; bool is_online; @@ -87,7 +88,12 @@ typedef struct Onion_Friend { onion_dht_pk_cb *dht_pk_callback; void *dht_pk_callback_object; uint32_t dht_pk_callback_number; -} Onion_Friend; + + uint8_t gc_data[GCA_MAX_DATA_LENGTH]; + uint8_t gc_public_key[ENC_PUBLIC_KEY_SIZE]; + uint16_t gc_data_length; + bool is_groupchat; +}; static const Onion_Friend empty_onion_friend = {false}; @@ -138,8 +144,51 @@ struct Onion_Client { unsigned int onion_connected; bool udp_connected; + + onion_group_announce_cb *group_announce_response; + void *group_announce_response_user_data; }; +uint16_t onion_get_friend_count(const Onion_Client *const onion_c) +{ + return onion_c->num_friends; +} + +Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num) +{ + return &onion_c->friends_list[friend_num]; +} + +const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend) +{ + return onion_friend->gc_public_key; +} + +const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num) +{ + return onion_c->friends_list[num].gc_public_key; +} + +void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key) +{ + memcpy(onion_friend->gc_public_key, public_key, ENC_PUBLIC_KEY_SIZE); +} + +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length) +{ + if (gc_data_length > 0 && gc_data != nullptr) { + memcpy(onion_friend->gc_data, gc_data, gc_data_length); + } + + onion_friend->gc_data_length = gc_data_length; + onion_friend->is_groupchat = true; +} + +bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend) +{ + return onion_friend->is_groupchat; +} + DHT *onion_get_dht(const Onion_Client *onion_c) { return onion_c->dht; @@ -351,8 +400,8 @@ static bool path_timed_out(const Mono_Time *mono_time, const Onion_Client_Paths const uint64_t timeout = is_new ? ONION_PATH_FIRST_TIMEOUT : ONION_PATH_TIMEOUT; return (onion_paths->last_path_used_times[pathnum] >= ONION_PATH_MAX_NO_RESPONSE_USES - && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) - || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME); + && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) + || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME); } /** should node be considered to have timed out */ @@ -360,8 +409,8 @@ non_null() static bool onion_node_timed_out(const Onion_Node *node, const Mono_Time *mono_time) { return node->timestamp == 0 - || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS - && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT)); + || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS + && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT)); } /** @brief Create a new path or use an old suitable one (if pathnum is valid) @@ -600,19 +649,35 @@ static int client_send_announce_request(Onion_Client *onion_c, uint32_t num, con ping_id = zero_ping_id; } - uint8_t request[ONION_ANNOUNCE_REQUEST_SIZE]; + uint8_t request[ONION_ANNOUNCE_REQUEST_MAX_SIZE]; int len; if (num == 0) { len = create_announce_request( - onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), - nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), - onion_c->temp_public_key, sendback); + onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), + nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), + onion_c->temp_public_key, sendback); } else { - len = create_announce_request( - onion_c->rng, request, sizeof(request), dest_pubkey, onion_c->friends_list[num - 1].temp_public_key, - onion_c->friends_list[num - 1].temp_secret_key, ping_id, - onion_c->friends_list[num - 1].real_public_key, zero_ping_id, sendback); + Onion_Friend *onion_friend = &onion_c->friends_list[num - 1]; + + if (onion_friend->gc_data_length == 0) { // contact is a friend + len = create_announce_request( + onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, + onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, + zero_ping_id, sendback); + } else { // contact is a gc +#ifndef VANILLA_NACL + onion_friend->is_groupchat = true; + + len = create_gca_announce_request( + onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, + onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, + zero_ping_id, sendback, onion_friend->gc_data, + onion_friend->gc_data_length); +#else + return -1; +#endif // VANILLA_NACL + } } if (len == -1) { @@ -852,6 +917,16 @@ static int client_ping_nodes(Onion_Client *onion_c, uint32_t num, const Node_for } non_null() +static bool handle_group_announce_response(Onion_Client *onion_c, uint32_t num, const uint8_t *plain, size_t plain_size) +{ + if (onion_c->group_announce_response == nullptr) { + return true; + } + + return onion_c->group_announce_response(onion_c, num, plain, plain_size, onion_c->group_announce_response_user_data); +} + +non_null(1, 2, 3) nullable(5) static int handle_announce_response(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, void *userdata) { @@ -861,6 +936,95 @@ static int handle_announce_response(void *object, const IP_Port *source, const u return 1; } + uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; + IP_Port ip_port; + uint32_t path_num; + const uint32_t num = check_sendback(onion_c, packet + 1, public_key, &ip_port, &path_num); + + if (num > onion_c->num_friends) { + return 1; + } + + uint8_t plain[1 + ONION_PING_ID_SIZE + ONION_ANNOUNCE_RESPONSE_MAX_SIZE - ONION_ANNOUNCE_RESPONSE_MIN_SIZE]; + const int plain_size = 1 + ONION_PING_ID_SIZE + length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE; + int len; + + if (num == 0) { + len = decrypt_data(public_key, nc_get_self_secret_key(onion_c->c), + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, + length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); + } else { + if (!onion_c->friends_list[num - 1].is_valid) { + return 1; + } + + len = decrypt_data(public_key, onion_c->friends_list[num - 1].temp_secret_key, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, + length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); + } + + if ((uint32_t)len != plain_size) { + return 1; + } + + const uint32_t path_used = set_path_timeouts(onion_c, num, path_num); + + if (client_add_to_list(onion_c, num, public_key, &ip_port, plain[0], plain + 1, path_used) == -1) { + return 1; + } + + uint16_t len_nodes = 0; + const uint8_t nodes_count = plain[1 + ONION_PING_ID_SIZE]; + + if (nodes_count > 0) { + if (nodes_count > MAX_SENT_NODES) { + return 1; + } + + Node_format nodes[MAX_SENT_NODES]; + const int num_nodes = unpack_nodes(nodes, nodes_count, &len_nodes, plain + 2 + ONION_PING_ID_SIZE, + plain_size - 2 - ONION_PING_ID_SIZE, false); + + if (num_nodes < 0) { + return 1; + } + + if (client_ping_nodes(onion_c, num, nodes, num_nodes, source) == -1) { + return 1; + } + } + + if (len_nodes + 1 < length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE) { + const uint16_t offset = 2 + ONION_PING_ID_SIZE + len_nodes; + + if (plain_size < offset) { + return 1; + } + + if (!handle_group_announce_response(onion_c, num, plain + offset, plain_size - offset)) { + return 1; + } + } + + // TODO(irungentoo): LAN vs non LAN ips?, if we are connected only to LAN, are we offline? + onion_c->last_packet_recv = mono_time_get(onion_c->mono_time); + + return 0; +} + +/* TODO(jfreegman): DEPRECATE */ +non_null(1, 2, 3) nullable(5) +static int handle_announce_response_old(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, + void *userdata) +{ + Onion_Client *onion_c = (Onion_Client *)object; + + if (length < ONION_ANNOUNCE_RESPONSE_MIN_SIZE || length > ONION_ANNOUNCE_RESPONSE_MAX_SIZE) { + return 1; + } + const uint16_t len_nodes = length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE; uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; @@ -1038,10 +1202,14 @@ static int handle_tcp_onion(void *object, const uint8_t *data, uint16_t length, IP_Port ip_port = {{{0}}}; ip_port.ip.family = net_family_tcp_server(); - if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { + if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE) { return handle_announce_response(object, &ip_port, data, length, userdata); } + if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { + return handle_announce_response_old(object, &ip_port, data, length, userdata); + } + if (data[0] == NET_PACKET_ONION_DATA_RESPONSE) { return handle_data_response(object, &ip_port, data, length, userdata); } @@ -1118,8 +1286,8 @@ int send_onion_data(Onion_Client *onion_c, int friend_num, const uint8_t *data, uint8_t o_packet[ONION_MAX_PACKET_SIZE]; len = create_data_request( - onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, - node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet)); + onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, + node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet)); if (len == -1) { continue; @@ -1167,8 +1335,8 @@ static int send_dht_dhtpk(const Onion_Client *onion_c, int friend_num, const uin uint8_t packet_data[MAX_CRYPTO_REQUEST_SIZE]; len = create_request( - onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, - onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK); + onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, + onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK); assert(len <= UINT16_MAX); const Packet packet = {packet_data, (uint16_t)len}; @@ -1237,7 +1405,8 @@ static int send_dhtpk_announce(Onion_Client *onion_c, uint16_t friend_num, uint8 int nodes_len = 0; if (num_nodes != 0) { - nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, nodes, num_nodes); + nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, + nodes, num_nodes); if (nodes_len <= 0) { return -1; @@ -1344,7 +1513,8 @@ int onion_addfriend(Onion_Client *onion_c, const uint8_t *public_key) onion_c->friends_list[index].is_valid = true; memcpy(onion_c->friends_list[index].real_public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); - crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, onion_c->friends_list[index].temp_secret_key); + crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, + onion_c->friends_list[index].temp_secret_key); return index; } @@ -1674,6 +1844,12 @@ void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_ha onion_c->onion_data_handlers[byte].object = object; } +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data) +{ + onion_c->group_announce_response = func; + onion_c->group_announce_response_user_data = user_data; +} + #define ANNOUNCE_INTERVAL_NOT_ANNOUNCED 3 #define ANNOUNCE_INTERVAL_ANNOUNCED ONION_NODE_PING_INTERVAL @@ -1925,7 +2101,8 @@ Onion_Client *new_onion_client(const Logger *logger, const Random *rng, const Mo onion_c->c = c; new_symmetric_key(rng, onion_c->secret_symmetric_key); crypto_new_keypair(rng, onion_c->temp_public_key, onion_c->temp_secret_key); - networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response, onion_c); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, &handle_announce_response, onion_c); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response_old, onion_c); networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, &handle_data_response, onion_c); oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, &handle_dhtpk_announce, onion_c); cryptopacket_registerhandler(onion_c->dht, CRYPTO_PACKET_DHTPK, &handle_dht_dhtpk, onion_c); @@ -1942,6 +2119,7 @@ void kill_onion_client(Onion_Client *onion_c) ping_array_kill(onion_c->announce_ping_array); realloc_onion_friends(onion_c, 0); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, nullptr, nullptr); networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, nullptr, nullptr); networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, nullptr, nullptr); oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, nullptr, nullptr); diff --git a/protocols/Tox/libtox/src/toxcore/onion_client.h b/protocols/Tox/libtox/src/toxcore/onion_client.h index 2a30005c69..23a48ef5e6 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_client.h +++ b/protocols/Tox/libtox/src/toxcore/onion_client.h @@ -43,6 +43,8 @@ #define MAX_PATH_NODES 32 +#define GCA_MAX_DATA_LENGTH GCA_PUBLIC_ANNOUNCE_MAX_SIZE + /** * If no announce response packets are received within this interval tox will * be considered offline. We give time for a node to be pinged often enough @@ -195,6 +197,13 @@ typedef int oniondata_handler_cb(void *object, const uint8_t *source_pubkey, con non_null(1) nullable(3, 4) void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_handler_cb *cb, void *object); +typedef bool onion_group_announce_cb(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data); + +/** Function to call when the onion gets a group announce response. */ +non_null(1) nullable(2, 3) +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data); + non_null() void do_onion_client(Onion_Client *onion_c); @@ -217,4 +226,15 @@ typedef enum Onion_Connection_Status { non_null() Onion_Connection_Status onion_connection_status(const Onion_Client *onion_c); +typedef struct Onion_Friend Onion_Friend; + +non_null() uint16_t onion_get_friend_count(const Onion_Client *const onion_c); +non_null() Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num); +non_null() const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend); +non_null() const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num); +non_null() void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key); +non_null(1) nullable(2) +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length); +non_null() bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend); + #endif diff --git a/protocols/Tox/libtox/src/toxcore/ping.c b/protocols/Tox/libtox/src/toxcore/ping.c index 91a780b123..f44777b8ee 100644 --- a/protocols/Tox/libtox/src/toxcore/ping.c +++ b/protocols/Tox/libtox/src/toxcore/ping.c @@ -53,10 +53,9 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key return; } - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; // generate key to encrypt ping_id with recipient privkey - dht_get_shared_key_sent(ping->dht, shared_key, public_key); + const uint8_t *shared_key = dht_get_shared_key_sent(ping->dht, public_key); // Generate random ping_id. uint8_t data[PING_DATA_SIZE]; pk_copy(data, public_key); @@ -64,7 +63,6 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key ping_id = ping_array_add(ping->ping_array, ping->mono_time, ping->rng, data, sizeof(data)); if (ping_id == 0) { - crypto_memzero(shared_key, sizeof(shared_key)); return; } @@ -82,8 +80,6 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key ping_plain, sizeof(ping_plain), pk + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); - crypto_memzero(shared_key, sizeof(shared_key)); - if (rc != PING_PLAIN_SIZE + CRYPTO_MAC_SIZE) { return; } @@ -139,11 +135,11 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_ return 1; } - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + const uint8_t *shared_key = dht_get_shared_key_recv(dht, packet + 1); + uint8_t ping_plain[PING_PLAIN_SIZE]; // Decrypt ping_id - dht_get_shared_key_recv(dht, shared_key, packet + 1); const int rc = decrypt_data_symmetric(shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, @@ -151,12 +147,10 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_ ping_plain); if (rc != sizeof(ping_plain)) { - crypto_memzero(shared_key, sizeof(shared_key)); return 1; } if (ping_plain[0] != NET_PACKET_PING_REQUEST) { - crypto_memzero(shared_key, sizeof(shared_key)); return 1; } @@ -166,8 +160,6 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_ ping_send_response(ping, source, packet + 1, ping_id, shared_key); ping_add(ping, packet + 1, source); - crypto_memzero(shared_key, sizeof(shared_key)); - return 0; } @@ -188,10 +180,8 @@ static int handle_ping_response(void *object, const IP_Port *source, const uint8 return 1; } - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - // generate key to encrypt ping_id with recipient privkey - dht_get_shared_key_sent(ping->dht, shared_key, packet + 1); + const uint8_t *shared_key = dht_get_shared_key_sent(ping->dht, packet + 1); uint8_t ping_plain[PING_PLAIN_SIZE]; // Decrypt ping_id @@ -201,8 +191,6 @@ static int handle_ping_response(void *object, const IP_Port *source, const uint8 PING_PLAIN_SIZE + CRYPTO_MAC_SIZE, ping_plain); - crypto_memzero(shared_key, sizeof(shared_key)); - if (rc != sizeof(ping_plain)) { return 1; } diff --git a/protocols/Tox/libtox/src/toxcore/shared_key_cache.c b/protocols/Tox/libtox/src/toxcore/shared_key_cache.c new file mode 100644 index 0000000000..7846ae4f69 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/shared_key_cache.c @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2022 The TokTok team. + */ + +#include "shared_key_cache.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> // calloc(...) +#include <string.h> // memcpy(...) + +#include "ccompat.h" +#include "crypto_core.h" +#include "mono_time.h" + +typedef struct Shared_Key { + uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + uint64_t time_last_requested; +} Shared_Key; + +struct Shared_Key_Cache { + Shared_Key *keys; + const uint8_t* self_secret_key; + uint64_t timeout; /** After this time (in seconds), a key is erased on the next housekeeping cycle */ + const Mono_Time *time; + uint8_t keys_per_slot; +}; + +non_null() +static bool shared_key_is_empty(const Shared_Key *k) { + assert(k != nullptr); + /* + * Since time can never be 0, we use that to determine if a key slot is empty. + * Additionally this allows us to use crypto_memzero and leave the slot in a valid state. + */ + return k->time_last_requested == 0; +} + +non_null() +static void shared_key_set_empty(Shared_Key *k) { + crypto_memzero(k, sizeof (Shared_Key)); + assert(shared_key_is_empty(k)); +} + +Shared_Key_Cache *shared_key_cache_new(const Mono_Time *time, const uint8_t *self_secret_key, uint64_t timeout, uint8_t keys_per_slot) +{ + if (time == nullptr || self_secret_key == nullptr || timeout == 0 || keys_per_slot == 0) { + return nullptr; + } + + // Time must not be zero, since we use that as special value for empty slots + if (mono_time_get(time) == 0) { + // Fail loudly in debug environments + assert(false); + return nullptr; + } + + Shared_Key_Cache *res = (Shared_Key_Cache *)calloc(1, sizeof (Shared_Key_Cache)); + if (res == nullptr) { + return nullptr; + } + + res->self_secret_key = self_secret_key; + res->time = time; + res->keys_per_slot = keys_per_slot; + // We take one byte from the public key for each bucket and store keys_per_slot elements there + const size_t cache_size = 256 * keys_per_slot; + res->keys = (Shared_Key *)calloc(cache_size, sizeof (Shared_Key)); + + if (res->keys == nullptr) { + free(res); + return nullptr; + } + + crypto_memlock(res->keys, cache_size * sizeof (Shared_Key)); + + return res; +} + +void shared_key_cache_free(Shared_Key_Cache *cache) +{ + if (cache == nullptr) { + return; + } + + const size_t cache_size = 256 * cache->keys_per_slot; + // Don't leave key material in memory + crypto_memzero(cache->keys, cache_size * sizeof (Shared_Key)); + crypto_memunlock(cache->keys, cache_size * sizeof (Shared_Key)); + free(cache->keys); + free(cache); +} + +/* NOTE: On each lookup housekeeping is performed to evict keys that did timeout. */ +const uint8_t *shared_key_cache_lookup(Shared_Key_Cache *cache, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]) +{ + // caching the time is not necessary, but calls to mono_time_get(...) are not free + const uint64_t cur_time = mono_time_get(cache->time); + // We can't use the first and last bytes because they are masked in curve25519. Selected 8 for good alignment. + const uint8_t bucket_idx = public_key[8]; + Shared_Key* bucket_start = &cache->keys[bucket_idx*cache->keys_per_slot]; + + const uint8_t* found = nullptr; + + // Perform lookup + for(size_t i = 0; i < cache->keys_per_slot; ++i) { + if (shared_key_is_empty(&bucket_start[i])) { + continue; + } + + if (pk_equal(public_key, bucket_start[i].public_key)) { + found = bucket_start[i].shared_key; + bucket_start[i].time_last_requested = cur_time; + break; + } + } + + // Perform housekeeping for this bucket + for (size_t i = 0; i < cache->keys_per_slot; ++i) { + if (shared_key_is_empty(&bucket_start[i])) { + continue; + } + + const bool timed_out = (bucket_start[i].time_last_requested + cache->timeout) < cur_time; + if (timed_out) { + shared_key_set_empty(&bucket_start[i]); + } + } + + if (found == nullptr) { + // Insert into cache + + uint64_t oldest_timestamp = UINT64_MAX; + size_t oldest_index = 0; + + /* + * Find least recently used entry, unused entries are prioritised, + * because their time_last_requested field is zeroed. + */ + for (size_t i = 0; i < cache->keys_per_slot; ++i) { + if (bucket_start[i].time_last_requested < oldest_timestamp) { + oldest_timestamp = bucket_start[i].time_last_requested; + oldest_index = i; + } + } + + // Compute the shared key for the cache + if (encrypt_precompute(public_key, cache->self_secret_key, bucket_start[oldest_index].shared_key) != 0) { + // Don't put anything in the cache on error + return nullptr; + } + + // update cache entry + memcpy(bucket_start[oldest_index].public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); + bucket_start[oldest_index].time_last_requested = cur_time; + found = bucket_start[oldest_index].shared_key; + } + + return found; +} diff --git a/protocols/Tox/libtox/src/toxcore/shared_key_cache.h b/protocols/Tox/libtox/src/toxcore/shared_key_cache.h new file mode 100644 index 0000000000..97d8b7a4a8 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/shared_key_cache.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2022 The TokTok team. + */ + +#ifndef C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H +#define C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H + +#include <stdint.h> // uint*_t + +#include "crypto_core.h" +#include "mono_time.h" + +/** + * This implements a cache for shared keys, since key generation is expensive. + */ + +typedef struct Shared_Key_Cache Shared_Key_Cache; + +/** + * @brief Initializes a new shared key cache. + * @param time Time object for retrieving current time. + * @param self_secret_key Our own secret key of length CRYPTO_SECRET_KEY_SIZE, + * it must not change during the lifetime of the cache. + * @param timeout Number of milliseconds, after which a key should be evicted. + * @param keys_per_slot There are 256 slots, this controls how many keys are stored per slot and the size of the cache. + * @return nullptr on error. + */ +non_null() +Shared_Key_Cache *shared_key_cache_new(const Mono_Time *time, + const uint8_t *self_secret_key, + uint64_t timeout, uint8_t keys_per_slot); + +/** + * @brief Deletes the cache and frees all resources. + * @param cache Cache to delete or nullptr. + */ +nullable(1) +void shared_key_cache_free(Shared_Key_Cache *cache); + +/** + * @brief Looks up a key from the cache or computes it if it didn't exist yet. + * @param cache Cache to perform the lookup on. + * @param public_key Public key, used for the lookup and computation. + * + * @return The shared key of length CRYPTO_SHARED_KEY_SIZE, matching the public key and our secret key. + * @return nullptr on error. + */ +non_null() +const uint8_t* shared_key_cache_lookup(Shared_Key_Cache *cache, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]); + +#endif // C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H diff --git a/protocols/Tox/libtox/src/toxcore/state.h b/protocols/Tox/libtox/src/toxcore/state.h index 716286d6d1..0cc188d50d 100644 --- a/protocols/Tox/libtox/src/toxcore/state.h +++ b/protocols/Tox/libtox/src/toxcore/state.h @@ -34,6 +34,7 @@ typedef enum State_Type { STATE_TYPE_NAME = 4, STATE_TYPE_STATUSMESSAGE = 5, STATE_TYPE_STATUS = 6, + STATE_TYPE_GROUPS = 7, STATE_TYPE_TCP_RELAY = 10, STATE_TYPE_PATH_NODE = 11, STATE_TYPE_CONFERENCES = 20, diff --git a/protocols/Tox/libtox/src/toxcore/tox.c b/protocols/Tox/libtox/src/toxcore/tox.c index 079f404079..3f7dcc439d 100644 --- a/protocols/Tox/libtox/src/toxcore/tox.c +++ b/protocols/Tox/libtox/src/toxcore/tox.c @@ -19,6 +19,8 @@ #include "Messenger.h" #include "ccompat.h" #include "group.h" +#include "group_chats.h" +#include "group_moderation.h" #include "logger.h" #include "mono_time.h" #include "network.h" @@ -54,6 +56,10 @@ static_assert(TOX_MAX_NAME_LENGTH == MAX_NAME_LENGTH, "TOX_MAX_NAME_LENGTH is assumed to be equal to MAX_NAME_LENGTH"); static_assert(TOX_MAX_STATUS_MESSAGE_LENGTH == MAX_STATUSMESSAGE_LENGTH, "TOX_MAX_STATUS_MESSAGE_LENGTH is assumed to be equal to MAX_STATUSMESSAGE_LENGTH"); +static_assert(TOX_GROUP_MAX_MESSAGE_LENGTH == GROUP_MAX_MESSAGE_LENGTH, + "TOX_GROUP_MAX_MESSAGE_LENGTH is assumed to be equal to GROUP_MAX_MESSAGE_LENGTH"); +static_assert(TOX_MAX_CUSTOM_PACKET_SIZE == MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE, + "TOX_MAX_CUSTOM_PACKET_SIZE is assumed to be equal to MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE"); struct Tox_Userdata { Tox *tox; @@ -317,8 +323,8 @@ static void tox_dht_get_nodes_response_handler(const DHT *dht, const Node_format Ip_Ntoa ip_str; tox_data->tox->dht_get_nodes_response_callback( - tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), - tox_data->user_data); + tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), + tox_data->user_data); } static m_friend_lossy_packet_cb tox_friend_lossy_packet_handler; @@ -353,6 +359,218 @@ static void tox_friend_lossless_packet_handler(Messenger *m, uint32_t friend_num } } +#ifndef VANILLA_NACL +non_null(1, 4) nullable(6) +static void tox_group_peer_name_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *name, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_name_callback != nullptr) { + tox_data->tox->group_peer_name_callback(tox_data->tox, group_number, peer_id, name, length, tox_data->user_data); + } +} + +non_null(1) nullable(5) +static void tox_group_peer_status_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int status, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_status_callback != nullptr) { + tox_data->tox->group_peer_status_callback(tox_data->tox, group_number, peer_id, (Tox_User_Status)status, + tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_topic_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, + size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_topic_callback != nullptr) { + tox_data->tox->group_topic_callback(tox_data->tox, group_number, peer_id, topic, length, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_topic_lock_handler(const Messenger *m, uint32_t group_number, unsigned int topic_lock, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_topic_lock_callback != nullptr) { + tox_data->tox->group_topic_lock_callback(tox_data->tox, group_number, (Tox_Group_Topic_Lock)topic_lock, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_voice_state_handler(const Messenger *m, uint32_t group_number, unsigned int voice_state, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_voice_state_callback != nullptr) { + tox_data->tox->group_voice_state_callback(tox_data->tox, group_number, (Tox_Group_Voice_State)voice_state, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_peer_limit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_limit, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_limit_callback != nullptr) { + tox_data->tox->group_peer_limit_callback(tox_data->tox, group_number, peer_limit, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_privacy_state_handler(const Messenger *m, uint32_t group_number, unsigned int privacy_state, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_privacy_state_callback != nullptr) { + tox_data->tox->group_privacy_state_callback(tox_data->tox, group_number, (Tox_Group_Privacy_State)privacy_state, + tox_data->user_data); + } +} + +non_null(1) nullable(3, 5) +static void tox_group_password_handler(const Messenger *m, uint32_t group_number, const uint8_t *password, + size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_password_callback != nullptr) { + tox_data->tox->group_password_callback(tox_data->tox, group_number, password, length, tox_data->user_data); + } +} + +non_null(1, 5) nullable(8) +static void tox_group_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *message, size_t length, uint32_t message_id, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_message_callback != nullptr) { + tox_data->tox->group_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, length, + message_id, tox_data->user_data); + } +} + +non_null(1, 5) nullable(7) +static void tox_group_private_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int type, const uint8_t *message, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_private_message_callback != nullptr) { + tox_data->tox->group_private_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, + length, + tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_custom_packet_callback != nullptr) { + tox_data->tox->group_custom_packet_callback(tox_data->tox, group_number, peer_id, data, length, tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_private_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_custom_private_packet_callback != nullptr) { + tox_data->tox->group_custom_private_packet_callback(tox_data->tox, group_number, peer_id, data, length, + tox_data->user_data); + } +} + +non_null(1, 3, 5) nullable(7) +static void tox_group_invite_handler(const Messenger *m, uint32_t friend_number, const uint8_t *invite_data, + size_t length, const uint8_t *group_name, size_t group_name_length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_invite_callback != nullptr) { + tox_data->tox->group_invite_callback(tox_data->tox, friend_number, invite_data, length, group_name, group_name_length, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_peer_join_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_join_callback != nullptr) { + tox_data->tox->group_peer_join_callback(tox_data->tox, group_number, peer_id, tox_data->user_data); + } +} + +non_null(1, 5) nullable(7, 9) +static void tox_group_peer_exit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int exit_type, const uint8_t *name, size_t name_length, + const uint8_t *part_message, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_exit_callback != nullptr) { + tox_data->tox->group_peer_exit_callback(tox_data->tox, group_number, peer_id, (Tox_Group_Exit_Type) exit_type, name, + name_length, + part_message, length, tox_data->user_data); + } +} + +non_null(1) nullable(3) +static void tox_group_self_join_handler(const Messenger *m, uint32_t group_number, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_self_join_callback != nullptr) { + tox_data->tox->group_self_join_callback(tox_data->tox, group_number, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_join_fail_handler(const Messenger *m, uint32_t group_number, unsigned int fail_type, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_join_fail_callback != nullptr) { + tox_data->tox->group_join_fail_callback(tox_data->tox, group_number, (Tox_Group_Join_Fail)fail_type, + tox_data->user_data); + } +} + +non_null(1) nullable(6) +static void tox_group_moderation_handler(const Messenger *m, uint32_t group_number, uint32_t source_peer_number, + uint32_t target_peer_number, unsigned int mod_type, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_moderation_callback != nullptr) { + tox_data->tox->group_moderation_callback(tox_data->tox, group_number, source_peer_number, target_peer_number, + (Tox_Group_Mod_Event)mod_type, + tox_data->user_data); + } +} +#endif bool tox_version_is_compatible(uint32_t major, uint32_t minor, uint32_t patch) { @@ -522,6 +740,7 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) const Tox_System *sys = tox_options_get_operating_system(opts); const Tox_System default_system = tox_default_system(); + if (sys == nullptr) { sys = &default_system; } @@ -553,7 +772,8 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) const char *const proxy_host = tox_options_get_proxy_host(opts); - if (proxy_host == nullptr || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) { + if (proxy_host == nullptr + || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) { SET_ERROR_PARAMETER(error, TOX_ERR_NEW_PROXY_BAD_HOST); // TODO(irungentoo): TOX_ERR_NEW_PROXY_NOT_FOUND if domain. tox_options_free(default_options); @@ -671,6 +891,27 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) custom_lossy_packet_registerhandler(tox->m, tox_friend_lossy_packet_handler); custom_lossless_packet_registerhandler(tox->m, tox_friend_lossless_packet_handler); +#ifndef VANILLA_NACL + m_callback_group_invite(tox->m, tox_group_invite_handler); + gc_callback_message(tox->m, tox_group_message_handler); + gc_callback_private_message(tox->m, tox_group_private_message_handler); + gc_callback_custom_packet(tox->m, tox_group_custom_packet_handler); + gc_callback_custom_private_packet(tox->m, tox_group_custom_private_packet_handler); + gc_callback_moderation(tox->m, tox_group_moderation_handler); + gc_callback_nick_change(tox->m, tox_group_peer_name_handler); + gc_callback_status_change(tox->m, tox_group_peer_status_handler); + gc_callback_topic_change(tox->m, tox_group_topic_handler); + gc_callback_peer_limit(tox->m, tox_group_peer_limit_handler); + gc_callback_privacy_state(tox->m, tox_group_privacy_state_handler); + gc_callback_topic_lock(tox->m, tox_group_topic_lock_handler); + gc_callback_password(tox->m, tox_group_password_handler); + gc_callback_peer_join(tox->m, tox_group_peer_join_handler); + gc_callback_peer_exit(tox->m, tox_group_peer_exit_handler); + gc_callback_self_join(tox->m, tox_group_self_join_handler); + gc_callback_rejected(tox->m, tox_group_join_fail_handler); + gc_callback_voice_state(tox->m, tox_group_voice_state_handler); +#endif + tox_options_free(default_options); tox_unlock(tox); @@ -714,9 +955,9 @@ size_t tox_get_savedata_size(const Tox *tox) assert(tox != nullptr); tox_lock(tox); const size_t ret = 2 * sizeof(uint32_t) - + messenger_size(tox->m) - + conferences_size(tox->m->conferences_object) - + end_size(); + + messenger_size(tox->m) + + conferences_size(tox->m->conferences_object) + + end_size(); tox_unlock(tox); return ret; } @@ -749,7 +990,8 @@ void tox_get_savedata(const Tox *tox, uint8_t *savedata) } non_null(5) nullable(1, 2, 4, 6) -static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, IP_Port **root, Tox_Err_Bootstrap *error) +static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, + IP_Port **root, Tox_Err_Bootstrap *error) { assert(tox != nullptr); assert(root != nullptr); @@ -766,7 +1008,7 @@ static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const int32_t count = net_getipport(host, root, TOX_SOCK_DGRAM); - if (count == -1) { + if (count < 1) { LOGGER_DEBUG(tox->m->log, "could not resolve bootstrap node '%s'", host); net_freeipport(*root); SET_ERROR_PARAMETER(error, TOX_ERR_BOOTSTRAP_BAD_HOST); @@ -879,6 +1121,7 @@ Tox_Connection tox_self_get_connection_status(const Tox *tox) } LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + return TOX_CONNECTION_NONE; } @@ -2580,3 +2823,1760 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error) tox_unlock(tox); return 0; } + +/* GROUPCHAT FUNCTIONS */ + +#ifndef VANILLA_NACL +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback) +{ + assert(tox != nullptr); + tox->group_invite_callback = callback; +} + +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback) +{ + assert(tox != nullptr); + tox->group_message_callback = callback; +} + +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback) +{ + assert(tox != nullptr); + tox->group_private_message_callback = callback; +} + +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback) +{ + assert(tox != nullptr); + tox->group_custom_packet_callback = callback; +} + +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback) +{ + assert(tox != nullptr); + tox->group_custom_private_packet_callback = callback; +} + +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback) +{ + assert(tox != nullptr); + tox->group_moderation_callback = callback; +} + +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_name_callback = callback; +} + +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_status_callback = callback; +} + +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback) +{ + assert(tox != nullptr); + tox->group_topic_callback = callback; +} + +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback) +{ + assert(tox != nullptr); + tox->group_privacy_state_callback = callback; +} + +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback) +{ + assert(tox != nullptr); + tox->group_topic_lock_callback = callback; +} + +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback) +{ + assert(tox != nullptr); + tox->group_voice_state_callback = callback; +} + +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_limit_callback = callback; +} + +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback) +{ + assert(tox != nullptr); + tox->group_password_callback = callback; +} + +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_join_callback = callback; +} + +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_exit_callback = callback; +} + +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback) +{ + assert(tox != nullptr); + tox->group_self_join_callback = callback; +} + +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback) +{ + assert(tox != nullptr); + tox->group_join_fail_callback = callback; +} + +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, + size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_group_add(tox->m->group_handler, (Group_Privacy_State) privacy_state, + group_name, group_name_length, name, name_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_TOO_LONG); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_EMPTY); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_INIT); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_STATE); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_ANNOUNCE); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, + const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_group_join(tox->m->group_handler, chat_id, name, name_length, password, password_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_INIT); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_BAD_CHAT_ID); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_TOO_LONG); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_EMPTY); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_PASSWORD); + return UINT32_MAX; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_CORE); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_OK); + + const bool ret = chat->connection_state == CS_CONNECTED || chat->connection_state == CS_CONNECTING; + tox_unlock(tox); + + return ret; +} + +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED); + tox_unlock(tox); + return false; + } + + + const bool ret = gc_disconnect_from_group(tox->m->group_handler, chat); + + tox_unlock(tox); + + if (!ret) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_OK); + + return true; +} + +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_rejoin_group(tox->m->group_handler, chat); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_CORE); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, + Tox_Err_Group_Leave *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_group_exit(tox->m->group_handler, chat, part_message, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_FAIL_SEND); + return true; /* the group was still successfully deleted */ + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, + Tox_Err_Group_Self_Name_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_self_nick(tox->m, group_number, name, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_INVALID); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const size_t ret = gc_get_self_nick_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + gc_get_self_nick(chat, name); + tox_unlock(tox); + + return true; +} + +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, + Tox_Err_Group_Self_Status_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_self_status(tox->m, group_number, (Group_Peer_Status) status); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_User_Status) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const uint8_t status = gc_get_self_status(chat); + tox_unlock(tox); + + return (Tox_User_Status)status; +} + +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Role) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const Group_Role role = gc_get_self_role(chat); + tox_unlock(tox); + + return (Tox_Group_Role)role; +} + +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const uint32_t ret = gc_get_self_peer_id(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, + Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + gc_get_self_public_key(chat, public_key); + tox_unlock(tox); + + return true; +} + +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + const int ret = gc_get_peer_nick_size(chat, peer_id); + tox_unlock(tox); + + if (ret == -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return -1; + } else { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return ret; + } +} + +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const bool ret = gc_get_peer_nick(chat, peer_id, name); + tox_unlock(tox); + + if (!ret) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return true; +} + +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_User_Status) - 1; + } + + const uint8_t ret = gc_get_status(chat, peer_id); + tox_unlock(tox); + + if (ret == UINT8_MAX) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return (Tox_User_Status) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_User_Status)ret; +} + +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Role) - 1; + } + + const uint8_t ret = gc_get_role(chat, peer_id); + tox_unlock(tox); + + if (ret == (uint8_t) -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return (Tox_Group_Role) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_Group_Role)ret; +} + +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_get_peer_public_key_by_peer_id(chat, peer_id, public_key); + tox_unlock(tox); + + if (ret == -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return true; +} + +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return TOX_CONNECTION_NONE; + } + + const unsigned int ret = gc_get_peer_connection_status(chat, peer_id); + tox_unlock(tox); + + if (ret == 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return TOX_CONNECTION_NONE; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_Connection)ret; +} + +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, + Tox_Err_Group_Topic_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_set_topic(chat, topic, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_topic_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + gc_get_topic(chat, topic); + tox_unlock(tox); + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + return true; +} + +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_group_name_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + gc_get_group_name(chat, group_name); + tox_unlock(tox); + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + return true; +} + +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + gc_get_chat_id(chat, chat_id); + tox_unlock(tox); + + return true; +} + +uint32_t tox_group_get_number_groups(const Tox *tox) +{ + assert(tox != nullptr); + + tox_lock(tox); + const uint32_t ret = gc_count_groups(tox->m->group_handler); + tox_unlock(tox); + + return ret; +} + +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Privacy_State) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const uint8_t state = gc_get_privacy_state(chat); + tox_unlock(tox); + + return (Tox_Group_Privacy_State)state; +} + +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Topic_Lock) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const Group_Topic_Lock topic_lock = gc_get_topic_lock_state(chat); + tox_unlock(tox); + + return (Tox_Group_Topic_Lock)topic_lock; +} + +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Voice_State) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const Group_Voice_State voice_state = gc_get_voice_state(chat); + tox_unlock(tox); + + return (Tox_Group_Voice_State)voice_state; +} + +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const uint16_t ret = gc_get_max_peers(chat); + tox_unlock(tox); + + return ret; +} + +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_password_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + gc_get_password(chat, password); + tox_unlock(tox); + + return true; +} + +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, + size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_message(chat, message, length, type, message_id); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_private_message(chat, peer_id, type, message, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, + size_t length, Tox_Err_Group_Send_Custom_Packet *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_custom_packet(chat, lossless, data, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, + const uint8_t *data, size_t length, + Tox_Err_Group_Send_Custom_Private_Packet *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_custom_private_packet(chat, lossless, peer_id, data, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, + Tox_Err_Group_Invite_Friend *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED); + tox_unlock(tox); + return false; + } + + if (!friend_is_valid(tox->m, friend_number)) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_invite_friend(tox->m->group_handler, chat, friend_number, send_group_invite_packet); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *name, size_t name_length, const uint8_t *password, + size_t password_length, Tox_Err_Group_Invite_Accept *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_accept_invite(tox->m->group_handler, friend_number, invite_data, length, name, name_length, password, + password_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD); + return UINT32_MAX; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_CORE); + return UINT32_MAX; + } + + case -7: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + Tox_Err_Group_Founder_Set_Password *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_founder_set_password(chat, password, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + Tox_Err_Group_Founder_Set_Privacy_State *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_privacy_state(tox->m, group_number, (Group_Privacy_State) privacy_state); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, + Tox_Err_Group_Founder_Set_Topic_Lock *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_topic_lock(tox->m, group_number, (Group_Topic_Lock) topic_lock); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + Tox_Err_Group_Founder_Set_Voice_State *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_voice_state(tox->m, group_number, (Group_Voice_State)voice_state); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, + Tox_Err_Group_Founder_Set_Peer_Limit *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_founder_set_max_peers(chat, max_peers); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, + Tox_Err_Group_Set_Ignore *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_set_ignore(chat, peer_id, ignore); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, + Tox_Err_Group_Mod_Set_Role *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_peer_role(tox->m, group_number, peer_id, (Group_Role) role); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Mod_Kick_Peer *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_kick_peer(tox->m, group_number, peer_id); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +#endif /* VANILLA_NACL */ + diff --git a/protocols/Tox/libtox/src/toxcore/tox.h b/protocols/Tox/libtox/src/toxcore/tox.h index e8d55c6689..fd1901f69c 100644 --- a/protocols/Tox/libtox/src/toxcore/tox.h +++ b/protocols/Tox/libtox/src/toxcore/tox.h @@ -3267,6 +3267,2192 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error); /** @} */ +/******************************************************************************* + * + * :: Group chats + * + ******************************************************************************/ + + + + +/******************************************************************************* + * + * :: Group chat numeric constants + * + ******************************************************************************/ + + + +/** @{ + * Maximum length of a group topic. + */ +#define TOX_GROUP_MAX_TOPIC_LENGTH 512 + +uint32_t tox_group_max_topic_length(void); + +/** + * Maximum length of a peer part message. + */ +#define TOX_GROUP_MAX_PART_LENGTH 128 + +uint32_t tox_group_max_part_length(void); + +/** + * Maximum length of a group text message. + */ +#define TOX_GROUP_MAX_MESSAGE_LENGTH 1372 + +uint32_t tox_group_max_message_length(void); + +/** + * Maximum length of a group custom lossy packet. + */ +#define TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH 500 + +uint32_t tox_group_max_custom_lossy_packet_length(void); + +/** + * Maximum length of a group custom lossless packet. + */ +#define TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH 1373 + +uint32_t tox_group_max_custom_lossless_packet_length(void); + +/** + * Maximum length of a group name. + */ +#define TOX_GROUP_MAX_GROUP_NAME_LENGTH 48 + +uint32_t tox_group_max_group_name_length(void); + +/** + * Maximum length of a group password. + */ +#define TOX_GROUP_MAX_PASSWORD_SIZE 32 + +uint32_t tox_group_max_password_size(void); + +/** + * Number of bytes in a group Chat ID. + */ +#define TOX_GROUP_CHAT_ID_SIZE 32 + +uint32_t tox_group_chat_id_size(void); + +/** + * Size of a peer public key. + */ +#define TOX_GROUP_PEER_PUBLIC_KEY_SIZE 32 + +uint32_t tox_group_peer_public_key_size(void); + + +/******************************************************************************* + * + * :: Group chat state enumerators + * + ******************************************************************************/ + + + +/** + * Represents the group privacy state. + */ +typedef enum Tox_Group_Privacy_State { + + /** + * The group is considered to be public. Anyone may join the group using the Chat ID. + * + * If the group is in this state, even if the Chat ID is never explicitly shared + * with someone outside of the group, information including the Chat ID, IP addresses, + * and peer ID's (but not Tox ID's) is visible to anyone with access to a node + * storing a DHT entry for the given group. + */ + TOX_GROUP_PRIVACY_STATE_PUBLIC, + + /** + * The group is considered to be private. The only way to join the group is by having + * someone in your contact list send you an invite. + * + * If the group is in this state, no group information (mentioned above) is present in the DHT; + * the DHT is not used for any purpose at all. If a public group is set to private, + * all DHT information related to the group will expire shortly. + */ + TOX_GROUP_PRIVACY_STATE_PRIVATE, + +} Tox_Group_Privacy_State; + + +/** + * Represents the state of the group topic lock. + */ +typedef enum Tox_Group_Topic_Lock { + + /** + * The topic lock is enabled. Only peers with the founder and moderator roles may set the topic. + */ + TOX_GROUP_TOPIC_LOCK_ENABLED, + + /** + * The topic lock is disabled. All peers except those with the observer role may set the topic. + */ + TOX_GROUP_TOPIC_LOCK_DISABLED, + +} Tox_Group_Topic_Lock; + +/** + * Represents the group voice state, which determines which Group Roles have permission to speak + * in the group chat. The voice state does not have any effect private messages or topic setting. + */ +typedef enum Tox_Group_Voice_State { + /** + * All group roles above Observer have permission to speak. + */ + TOX_GROUP_VOICE_STATE_ALL, + + /** + * Moderators and Founders have permission to speak. + */ + TOX_GROUP_VOICE_STATE_MODERATOR, + + /** + * Only the founder may speak. + */ + TOX_GROUP_VOICE_STATE_FOUNDER, +} Tox_Group_Voice_State; + +/** + * Represents group roles. + * + * Roles are hierarchical in that each role has a set of privileges plus all the privileges + * of the roles below it. + */ +typedef enum Tox_Group_Role { + + /** + * May kick all other peers as well as set their role to anything (except founder). + * Founders may also set the group password, toggle the privacy state, and set the peer limit. + */ + TOX_GROUP_ROLE_FOUNDER, + + /** + * May kick and set the user and observer roles for peers below this role. + * May also set the group topic. + */ + TOX_GROUP_ROLE_MODERATOR, + + /** + * May communicate with other peers normally. + */ + TOX_GROUP_ROLE_USER, + + /** + * May observe the group and ignore peers; may not communicate with other peers or with the group. + */ + TOX_GROUP_ROLE_OBSERVER, + +} Tox_Group_Role; + + + +/******************************************************************************* + * + * :: Group chat instance management + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_New { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_NEW_OK, + + /** + * name exceeds TOX_MAX_NAME_LENGTH or group_name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. + */ + TOX_ERR_GROUP_NEW_TOO_LONG, + + /** + * name or group_name is NULL or length is zero. + */ + TOX_ERR_GROUP_NEW_EMPTY, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_NEW_INIT, + + /** + * The group state failed to initialize. This usually indicates that something went wrong + * related to cryptographic signing. + */ + TOX_ERR_GROUP_NEW_STATE, + + /** + * The group failed to announce to the DHT. This indicates a network related error. + */ + TOX_ERR_GROUP_NEW_ANNOUNCE, + +} Tox_Err_Group_New; + + +/** + * Creates a new group chat. + * + * This function creates a new group chat object and adds it to the chats array. + * + * The caller of this function has Founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * @param privacy_state The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + * the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + * Otherwise a friend invite will be required to join the group. + * @param group_name The name of the group. The name must be non-NULL. + * @param group_name_length The length of the group name. This must be greater than zero and no larger than + * TOX_GROUP_MAX_GROUP_NAME_LENGTH. + * @param name The name of the peer creating the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, + size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error); + +typedef enum Tox_Err_Group_Join { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_JOIN_OK, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_JOIN_INIT, + + /** + * The chat_id pointer is set to NULL or a group with chat_id already exists. This usually + * happens if the client attempts to create multiple sessions for the same group. + */ + TOX_ERR_GROUP_JOIN_BAD_CHAT_ID, + + /** + * name is NULL or name_length is zero. + */ + TOX_ERR_GROUP_JOIN_EMPTY, + + /** + * name exceeds TOX_MAX_NAME_LENGTH. + */ + TOX_ERR_GROUP_JOIN_TOO_LONG, + + /** + * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_JOIN_PASSWORD, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_JOIN_CORE, + +} Tox_Err_Group_Join; + + +/** + * Joins a group chat with specified Chat ID. + * + * This function creates a new group chat object, adds it to the chats array, and sends + * a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + * found a join attempt will be initiated. + * + * @param chat_id The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If length is equal to zero, + * the password parameter is ignored. length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, + const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error); + +typedef enum Tox_Err_Group_Is_Connected { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_IS_CONNECTED_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND, + +} Tox_Err_Group_Is_Connected; + + +/** + * Returns true if the group chat is currently connected or attempting to connect to other peers + * in the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error); + +typedef enum Tox_Err_Group_Disconnect { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_DISCONNECT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND, + + /** + * The group is already disconnected. + */ + TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED, +} Tox_Err_Group_Disconnect; + + +/** + * Disconnects from a group chat while retaining the group state and credentials. + * + * Returns true if we successfully disconnect from the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error); + +typedef enum Tox_Err_Group_Reconnect { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_RECONNECT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_RECONNECT_CORE, + +} Tox_Err_Group_Reconnect; + + +/** + * Reconnects to a group. + * + * This function disconnects from all peers in the group, then attempts to reconnect with the group. + * The caller's state is not changed (i.e. name, status, role, chat public key etc.). + * + * @param group_number The group number of the group we wish to reconnect to. + * + * @return true on success. + */ +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error); + +typedef enum Tox_Err_Group_Leave { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_LEAVE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_PART_LENGTH. + */ + TOX_ERR_GROUP_LEAVE_TOO_LONG, + + /** + * The parting packet failed to send. + */ + TOX_ERR_GROUP_LEAVE_FAIL_SEND, +} Tox_Err_Group_Leave; + + +/** + * Leaves a group. + * + * This function sends a parting packet containing a custom (non-obligatory) message to all + * peers in a group, and deletes the group from the chat array. All group state information is permanently + * lost, including keys and role credentials. + * + * @param group_number The group number of the group we wish to leave. + * @param part_message The parting message to be sent to all the peers. Set to NULL if we do not wish to + * send a parting message. + * @param length The length of the parting message. Set to 0 if we do not wish to send a parting message. + * + * @return true if the group chat instance is successfully deleted. + */ +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, + Tox_Err_Group_Leave *error); + + +/******************************************************************************* + * + * :: Group user-visible client information (nickname/status/role/public key) + * + ******************************************************************************/ + + + +/** + * General error codes for self state get and size functions. + */ +typedef enum Tox_Err_Group_Self_Query { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_QUERY_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND, + +} Tox_Err_Group_Self_Query; + + +/** + * Error codes for self name setting. + */ +typedef enum Tox_Err_Group_Self_Name_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_NAME_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND, + + /** + * Name length exceeded TOX_MAX_NAME_LENGTH. + */ + TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG, + + /** + * The length given to the set function is zero or name is a NULL pointer. + */ + TOX_ERR_GROUP_SELF_NAME_SET_INVALID, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Name_Set; + + +/** + * Set the client's nickname for the group instance designated by the given group number. + * + * Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + * pointer, the function call will fail. + * + * @param name A byte array containing the new nickname. + * @param length The size of the name byte array. + * + * @return true on success. + */ +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, + Tox_Err_Group_Self_Name_Set *error); + +/** + * Return the length of the client's current nickname for the group instance designated + * by group_number as passed to tox_group_self_set_name. + * + * If no nickname was set before calling this function, the name is empty, + * and this function returns 0. + * + * @see threading for concurrency implications. + */ +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the nickname set by tox_group_self_set_name to a byte array. + * + * If no nickname was set before calling this function, the name is empty, + * and this function has no effect. + * + * Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + * + * @param name A valid memory location large enough to hold the nickname. + * If this parameter is NULL, the function has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error); + +/** + * Error codes for self status setting. + */ +typedef enum Tox_Err_Group_Self_Status_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Status_Set; + + +/** + * Set the client's status for the group instance. Status must be a Tox_User_Status. + * + * @return true on success. + */ +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, + Tox_Err_Group_Self_Status_Set *error); + +/** + * returns the client's status for the group instance on success. + * return value is unspecified on failure. + */ +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's role for the group instance on success. + * return value is unspecified on failure. + */ +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's peer id for the group instance on success. + * return value is unspecified on failure. + */ +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the client's group public key designated by the given group number to a byte array. + * + * This key will be permanently tied to the client's identity for this particular group until + * the client explicitly leaves the group. This key is the only way for other peers to reliably + * identify the client across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + * + * @param public_key A valid memory region large enough to store the public key. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, + Tox_Err_Group_Self_Query *error); + + +/******************************************************************************* + * + * :: Peer-specific group state queries. + * + ******************************************************************************/ + + + +/** + * Error codes for peer info queries. + */ +typedef enum Tox_Err_Group_Peer_Query { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_PEER_QUERY_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND, + +} Tox_Err_Group_Peer_Query; + + +/** + * Return the length of the peer's name. If the group number or ID is invalid, the + * return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name length we want to retrieve. + * + * The return value is equal to the `length` argument received by the last + * `group_peer_name` callback. + */ +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Write the name of the peer designated by the given ID to a byte + * array. + * + * Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last + * `group_peer_name` callback. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name we wish to retrieve. + * @param name A valid memory region large enough to store the friend's name. + * + * @return true on success. + */ +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's user status (away/busy/...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose status we wish to query. + * + * The status returned is equal to the last status received through the + * `group_peer_status` callback. + */ +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's role (user/moderator/founder...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose role we wish to query. + * + * The role returned is equal to the last role received through the + * `group_moderation` callback. + */ +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the type of connection we have established with a peer. + * + * This function will return an error if called on ourselves. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose connection status we wish to query. + */ +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Write the group public key with the designated peer_id for the designated group number to public_key. + * + * This key will be permanently tied to a particular peer until they explicitly leave the group and is + * the only way to reliably identify the same peer across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. If `public_key` is null + * this function has no effect. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose public key we wish to retrieve. + * @param public_key A valid memory region large enough to store the public key. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, + Tox_Err_Group_Peer_Query *error); + +/** + * @param group_number The group number of the group the name change is intended for. + * @param peer_id The ID of the peer who has changed their name. + * @param name The name data. + * @param length The length of the name. + */ +typedef void tox_group_peer_name_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *name, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_peer_name` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their nickname. + */ +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback); + +/** + * @param group_number The group number of the group the status change is intended for. + * @param peer_id The ID of the peer who has changed their status. + * @param status The new status of the peer. + */ +typedef void tox_group_peer_status_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_User_Status status, + void *user_data); + + +/** + * Set the callback for the `group_peer_status` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their status. + */ +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback); + + +/******************************************************************************* + * + * :: Group chat state queries and events. + * + ******************************************************************************/ + + + +/** + * General error codes for group state get and size functions. + */ +typedef enum Tox_Err_Group_State_Queries { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_STATE_QUERIES_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND, + +} Tox_Err_Group_State_Queries; + + +/** + * Error codes for group topic setting. + */ +typedef enum Tox_Err_Group_Topic_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_TOPIC_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND, + + /** + * Topic length exceeded TOX_GROUP_MAX_TOPIC_LENGTH. + */ + TOX_ERR_GROUP_TOPIC_SET_TOO_LONG, + + /** + * The caller does not have the required permissions to set the topic. + */ + TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS, + + /** + * The packet could not be created. This error is usually related to cryptographic signing. + */ + TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED, + +} Tox_Err_Group_Topic_Set; + + +/** + * Set the group topic and broadcast it to the rest of the group. + * + * topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + * topic is set to NULL, the topic will be unset. + * + * @return true on success. + */ +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, + Tox_Err_Group_Topic_Set *error); + +/** + * Return the length of the group topic. If the group number is invalid, the + * return value is unspecified. + * + * The return value is equal to the `length` argument received by the last + * `group_topic` callback. + */ +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the topic designated by the given group number to a byte array. + * + * Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last + * `group_topic` callback. + * + * @param topic A valid memory region large enough to store the topic. + * If this parameter is NULL, this function has no effect. + * + * @return true on success. + */ +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the topic change is intended for. + * @param peer_id The ID of the peer who changed the topic. If the peer who set the topic + * is not present in our peer list this value will be set to 0. + * @param topic The topic data. + * @param length The topic length. + */ +typedef void tox_group_topic_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, size_t length, + void *user_data); + + +/** + * Set the callback for the `group_topic` event. Pass NULL to unset. + * + * This event is triggered when a peer changes the group topic. + */ +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback); + +/** + * Return the length of the group name. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the name of the group designated by the given group number to a byte array. + * + * Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + * + * @param group_name A valid memory region large enough to store the group name. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error); + +/** + * Write the Chat ID designated by the given group number to a byte array. + * + * `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. + * + * @param chat_id A valid memory region large enough to store the Chat ID. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error); + +/** + * Return the number of groups in the Tox chats array. + */ +uint32_t tox_group_get_number_groups(const Tox *tox); + +/** + * Return the privacy state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_privacy_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the privacy state is intended for. + * @param privacy_state The new privacy state. + */ +typedef void tox_group_privacy_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the privacy state. + */ +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback); + +/** + * Return the voice state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last `group_voice_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the voice state change is intended for. + * @param voice_state The new voice state. + */ +typedef void tox_group_voice_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the voice state. + */ +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback); + +/** + * Return the topic lock status of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_topic_lock` callback. + * + * @see the `Group chat founder contols` section for the respective set function. + */ +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the topic lock has changed. + * @param topic_lock The new topic lock state. + */ +typedef void tox_group_topic_lock_cb(Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, void *user_data); + + + +/** + * Set the callback for the `group_topic_lock` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the topic lock status. + */ +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback); + +/** + * Return the maximum number of peers allowed for the group designated by the given group number. + * If the group number is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_peer_limit` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the peer limit has changed. + * @param peer_limit The new peer limit for the group. + */ +typedef void tox_group_peer_limit_cb(Tox *tox, uint32_t group_number, uint32_t peer_limit, void *user_data); + + +/** + * Set the callback for the `group_peer_limit` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the maximum peer limit. + */ +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback); + +/** + * Return the length of the group password. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the password for the group designated by the given group number to a byte array. + * + * Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + * + * The data received is equal to the data received by the last + * `group_password` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + * + * @param password A valid memory region large enough to store the group password. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the password has changed. + * @param password The new group password. + * @param length The length of the password. + */ +typedef void tox_group_password_cb(Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + void *user_data); + + +/** + * Set the callback for the `group_password` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the group password. + */ +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback); + + +/******************************************************************************* + * + * :: Group chat message sending + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Send_Message { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_MESSAGE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. + */ + TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_MESSAGE_EMPTY, + + /** + * The message type is invalid. + */ + TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED, + +} Tox_Err_Group_Send_Message; + + +/** + * Send a text chat message to the group. + * + * This function creates a group message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param type Message type (normal, action, ...). + * @param message A non-NULL pointer to the first element of a byte array + * containing the message text. + * @param length Length of the message to be sent. + * @param message_id A pointer to a uint32_t. The message_id of this message will be returned + * unless the parameter is NULL, in which case the returned parameter value will be undefined. + * If this function returns false the returned parameter `message_id` value will also be undefined. + * + * @return true on success. + */ +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, + size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error); + +typedef enum Tox_Err_Group_Send_Private_Message { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND, + + /** + * The peer ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED, + + /** + * The message type is invalid. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE, + +} Tox_Err_Group_Send_Private_Message; + + +/** + * Send a text chat message to the specified peer in the specified group. + * + * This function creates a group private message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer the message is intended for. + * @param message A non-NULL pointer to the first element of a byte array + * containing the message text. + * @param length Length of the message to be sent. + * + * @return true on success. + */ +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error); + +typedef enum Tox_Err_Group_Send_Custom_Packet { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH if the + * packet was lossy, or TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH if the + * packet was lossless. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Packet; + + +/** + * Send a custom packet to the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The message length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, + size_t length, + Tox_Err_Group_Send_Custom_Packet *error); + + +typedef enum Tox_Err_Group_Send_Custom_Private_Packet { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH if the + * packet was lossy, or TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH if the + * packet was lossless. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY, + + /** + * The peer ID passed did no designate a valid peer. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Private_Packet; + +/** + * Send a custom private packet to a designated peer in the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The packet length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, + const uint8_t *data, size_t length, + Tox_Err_Group_Send_Custom_Private_Packet *error); + + +/******************************************************************************* + * + * :: Group chat message receiving + * + ******************************************************************************/ + + + +/** + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer who sent the message. + * @param type The type of message (normal, action, ...). + * @param message The message data. + * @param message_id A pseudo message id that clients can use to uniquely identify this group message. + * @param length The length of the message. + */ +typedef void tox_group_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, uint32_t message_id, void *user_data); + + +/** + * Set the callback for the `group_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group message. + */ +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback); + +/** + * @param group_number The group number of the group the private message is intended for. + * @param peer_id The ID of the peer who sent the private message. + * @param message The message data. + * @param length The length of the message. + */ +typedef void tox_group_private_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, void *user_data); + + +/** + * Set the callback for the `group_private_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a private message. + */ +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom packet. + */ +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_private_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_private_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom private packet. + */ +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback); + + +/******************************************************************************* + * + * :: Group chat inviting and join/part events + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Invite_Friend { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_INVITE_FRIEND_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND, + + /** + * The friend number passed did not designate a valid friend. + */ + TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND, + + /** + * Creation of the invite packet failed. This indicates a network related error. + */ + TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED, + +} Tox_Err_Group_Invite_Friend; + + +/** + * Invite a friend to a group. + * + * This function creates an invite request packet and pushes it to the send queue. + * + * @param group_number The group number of the group the message is intended for. + * @param friend_number The friend number of the friend the invite is intended for. + * + * @return true on success. + */ +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, + Tox_Err_Group_Invite_Friend *error); + +typedef enum Tox_Err_Group_Invite_Accept { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_OK, + + /** + * The invite data is not in the expected format. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED, + + /** + * name exceeds TOX_MAX_NAME_LENGTH + */ + TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG, + + /** + * name is NULL or name_length is zero. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY, + + /** + * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_CORE, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND, + +} Tox_Err_Group_Invite_Accept; + + +/** + * Accept an invite to a group chat that the client previously received from a friend. The invite + * is only valid while the inviter is present in the group. + * + * @param invite_data The invite data received from the `group_invite` event. + * @param length The length of the invite data. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If password_length is equal to zero, the password + * parameter will be ignored. password_length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return the group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *name, size_t name_length, const uint8_t *password, size_t password_length, + Tox_Err_Group_Invite_Accept *error); + +/** + * @param friend_number The friend number of the contact who sent the invite. + * @param invite_data The invite data. + * @param length The length of invite_data. + */ +typedef void tox_group_invite_cb(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *user_data); + + +/** + * Set the callback for the `group_invite` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group invite from a friend. The client must store + * invite_data which is used to join the group via tox_group_invite_accept. + */ +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback); + +/** + * @param group_number The group number of the group in which a new peer has joined. + * @param peer_id The permanent ID of the new peer. This id should not be relied on for + * client behaviour and should be treated as a random value. + */ +typedef void tox_group_peer_join_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data); + + +/** + * Set the callback for the `group_peer_join` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self joins the group. + */ +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback); + +/** + * Represents peer exit events. These should be used with the `group_peer_exit` event. + */ +typedef enum Tox_Group_Exit_Type { + + /** + * The peer has quit the group. + */ + TOX_GROUP_EXIT_TYPE_QUIT, + + /** + * Your connection with this peer has timed out. + */ + TOX_GROUP_EXIT_TYPE_TIMEOUT, + + /** + * Your connection with this peer has been severed. + */ + TOX_GROUP_EXIT_TYPE_DISCONNECTED, + + /** + * Your connection with all peers has been severed. This will occur when you are kicked from + * a group, rejoin a group, or manually disconnect from a group. + */ + TOX_GROUP_EXIT_TYPE_SELF_DISCONNECTED, + + /** + * The peer has been kicked. + */ + TOX_GROUP_EXIT_TYPE_KICK, + + /** + * The peer provided invalid group sync information. + */ + TOX_GROUP_EXIT_TYPE_SYNC_ERROR, + +} Tox_Group_Exit_Type; + + +/** + * @param group_number The group number of the group in which a peer has left. + * @param peer_id The ID of the peer who left the group. This ID no longer designates a valid peer + * and cannot be used for API calls. + * @param exit_type The type of exit event. One of Tox_Group_Exit_Type. + * @param name The nickname of the peer who left the group. + * @param name_length The length of the peer name. + * @param part_message The parting message data. + * @param part_message_length The length of the parting message. + */ +typedef void tox_group_peer_exit_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *name, size_t name_length, const uint8_t *part_message, size_t part_message_length, void *user_data); + + +/** + * Set the callback for the `group_peer_exit` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self exits the group. + */ +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback); + +/** + * @param group_number The group number of the group that the client has joined. + */ +typedef void tox_group_self_join_cb(Tox *tox, uint32_t group_number, void *user_data); + + +/** + * Set the callback for the `group_self_join` event. Pass NULL to unset. + * + * This event is triggered when the client has successfully joined a group. Use this to initialize + * any group information the client may need. + */ +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback); + +/** + * Represents types of failed group join attempts. These are used in the tox_callback_group_rejected + * callback when a peer fails to join a group. + */ +typedef enum Tox_Group_Join_Fail { + + /** + * The group peer limit has been reached. + */ + TOX_GROUP_JOIN_FAIL_PEER_LIMIT, + + /** + * You have supplied an invalid password. + */ + TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD, + + /** + * The join attempt failed due to an unspecified error. This often occurs when the group is + * not found in the DHT. + */ + TOX_GROUP_JOIN_FAIL_UNKNOWN, + +} Tox_Group_Join_Fail; + + +/** + * @param group_number The group number of the group for which the join has failed. + * @param fail_type The type of group rejection. + */ +typedef void tox_group_join_fail_cb(Tox *tox, uint32_t group_number, Tox_Group_Join_Fail fail_type, void *user_data); + + +/** + * Set the callback for the `group_join_fail` event. Pass NULL to unset. + * + * This event is triggered when the client fails to join a group. + */ +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback); + + +/******************************************************************************* + * + * :: Group chat founder controls (these only work for the group founder) + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Founder_Set_Password { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the password. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS, + + /** + * Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND, + + /** + * The function failed to allocate enough memory for the operation. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Password; + + +/** + * Set or unset the group password. + * + * This function sets the groups password, creates a new group shared state including the change, + * and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the password. + * @param password The password we want to set. Set password to NULL to unset the password. + * @param length The length of the password. length must be no longer than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return true on success. + */ +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + Tox_Err_Group_Founder_Set_Password *error); + +typedef enum Tox_Err_Group_Founder_Set_Topic_Lock { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND, + + /** + * Tox_Group_Topic_Lock is an invalid type. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID, + + /** + * The caller does not have the required permissions to set the topic lock. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS, + + /** + * The topic lock could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Topic_Lock; + + +/** + * Set the group topic lock state. + * + * This function sets the group's topic lock state to enabled or disabled, creates a new shared + * state including the change, and distributes it to the rest of the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * @param group_number The group number of the group for which we wish to change the topic lock state. + * @param topic_lock The state we wish to set the topic lock to. + * + * @return true on success. + */ +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, + Tox_Err_Group_Founder_Set_Topic_Lock *error); + +typedef enum Tox_Err_Group_Founder_Set_Voice_State { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the privacy state. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS, + + /** + * The voice state could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Voice_State; + +/** + * Set the group voice state. + * + * This function sets the group's voice state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the voice state. + * @param voice_state The voice state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + Tox_Err_Group_Founder_Set_Voice_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Privacy_State { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the privacy state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS, + + /** + * The privacy state could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Privacy_State; + +/** + * Set the group privacy state. + * + * This function sets the group's privacy state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the privacy state. + * @param privacy_state The privacy state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + Tox_Err_Group_Founder_Set_Privacy_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Peer_Limit { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the peer limit. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS, + + /** + * The peer limit could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Peer_Limit; + + +/** + * Set the group peer limit. + * + * This function sets a limit for the number of peers who may be in the group, creates a new + * group shared state including the change, and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the peer limit. + * @param max_peers The maximum number of peers to allow in the group. + * + * @return true on success. + */ +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, + Tox_Err_Group_Founder_Set_Peer_Limit *error); + + +/******************************************************************************* + * + * :: Group chat moderation + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Set_Ignore { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SET_IGNORE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND, + + /** + * The caller attempted to ignore himself. + */ + TOX_ERR_GROUP_SET_IGNORE_SELF, + +} Tox_Err_Group_Set_Ignore; + + +/** + * Ignore or unignore a peer. + * + * @param group_number The group number of the group in which you wish to ignore a peer. + * @param peer_id The ID of the peer who shall be ignored or unignored. + * @param ignore True to ignore the peer, false to unignore the peer. + * + * @return true on success. + */ +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, + Tox_Err_Group_Set_Ignore *error); + +typedef enum Tox_Err_Group_Mod_Set_Role { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. Note: you cannot set your own role. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions for this action. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS, + + /** + * The role assignment is invalid. This will occur if you try to set a peer's role to + * the role they already have. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT, + + /** + * The role was not successfully set. This may occur if the packet failed to send, or + * if the role limit has been reached. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION, + + /** + * The caller attempted to set their own role. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_SELF, + +} Tox_Err_Group_Mod_Set_Role; + + +/** + * Set a peer's role. + * + * This function will first remove the peer's previous role and then assign them a new role. + * It will also send a packet to the rest of the group, requesting that they perform + * the role reassignment. Note: peers cannot be set to the founder role. + * + * @param group_number The group number of the group the in which you wish set the peer's role. + * @param peer_id The ID of the peer whose role you wish to set. + * @param role The role you wish to set the peer to. + * + * @return true on success. + */ +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, + Tox_Err_Group_Mod_Set_Role *error); + +typedef enum Tox_Err_Group_Mod_Kick_Peer { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions for this action. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS, + + /** + * The peer could not be kicked from the group. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND, + + /** + * The caller attempted to set their own role. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_SELF, + +} Tox_Err_Group_Mod_Kick_Peer; + + +/** + * Kick a peer. + * + * This function will remove a peer from the caller's peer list and send a packet to all + * group members requesting them to do the same. Note: This function will not trigger + * the `group_peer_exit` event for the caller. + * + * @param group_number The group number of the group the action is intended for. + * @param peer_id The ID of the peer who will be kicked. + * + * @return true on success. + */ +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Mod_Kick_Peer *error); + +/** + * Represents moderation events. These should be used with the `group_moderation` event. + */ +typedef enum Tox_Group_Mod_Event { + + /** + * A peer has been kicked from the group. + */ + TOX_GROUP_MOD_EVENT_KICK, + + /** + * A peer as been given the observer role. + */ + TOX_GROUP_MOD_EVENT_OBSERVER, + + /** + * A peer has been given the user role. + */ + TOX_GROUP_MOD_EVENT_USER, + + /** + * A peer has been given the moderator role. + */ + TOX_GROUP_MOD_EVENT_MODERATOR, + +} Tox_Group_Mod_Event; + + +/** + * @param group_number The group number of the group the event is intended for. + * @param source_peer_id The ID of the peer who initiated the event. + * @param target_peer_id The ID of the peer who is the target of the event. + * @param mod_type The type of event. + */ +typedef void tox_group_moderation_cb(Tox *tox, uint32_t group_number, uint32_t source_peer_id, uint32_t target_peer_id, + Tox_Group_Mod_Event mod_type, void *user_data); + + +/** + * Set the callback for the `group_moderation` event. Pass NULL to unset. + * + * This event is triggered when a moderator or founder executes a moderation event, with + * the exception of the peer who initiates the event. It is also triggered when the + * observer and moderator lists are silently modified (this may occur during group syncing). + * + * If either peer id does not designate a valid peer in the group chat, the client should + * manually update all peer roles. + */ +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback); + +/** @} */ + /** @} */ #ifdef __cplusplus diff --git a/protocols/Tox/libtox/src/toxcore/tox_api.c b/protocols/Tox/libtox/src/toxcore/tox_api.c index 00051e497b..61c248c30d 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_api.c +++ b/protocols/Tox/libtox/src/toxcore/tox_api.c @@ -88,6 +88,42 @@ uint32_t tox_max_hostname_length(void) { return TOX_MAX_HOSTNAME_LENGTH; } +uint32_t tox_group_max_topic_length(void) +{ + return TOX_GROUP_MAX_TOPIC_LENGTH; +} +uint32_t tox_group_max_part_length(void) +{ + return TOX_GROUP_MAX_PART_LENGTH; +} +uint32_t tox_group_max_message_length(void) +{ + return TOX_GROUP_MAX_MESSAGE_LENGTH; +} +uint32_t tox_group_max_custom_lossy_packet_length(void) +{ + return TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH; +} +uint32_t tox_group_max_custom_lossless_packet_length(void) +{ + return TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH; +} +uint32_t tox_group_max_group_name_length(void) +{ + return TOX_GROUP_MAX_GROUP_NAME_LENGTH; +} +uint32_t tox_group_max_password_size(void) +{ + return TOX_GROUP_MAX_PASSWORD_SIZE; +} +uint32_t tox_group_chat_id_size(void) +{ + return TOX_GROUP_CHAT_ID_SIZE; +} +uint32_t tox_group_peer_public_key_size(void) +{ + return TOX_GROUP_PEER_PUBLIC_KEY_SIZE; +} uint32_t tox_dht_node_ip_string_size(void) { return TOX_DHT_NODE_IP_STRING_SIZE; diff --git a/protocols/Tox/libtox/src/toxcore/tox_dispatch.c b/protocols/Tox/libtox/src/toxcore/tox_dispatch.c index 4b4546e4a4..5427851470 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_dispatch.c +++ b/protocols/Tox/libtox/src/toxcore/tox_dispatch.c @@ -47,9 +47,11 @@ Tox_Dispatch *tox_dispatch_new(Tox_Err_Dispatch_New *error) *dispatch = (Tox_Dispatch) { nullptr }; + if (error != nullptr) { *error = TOX_ERR_DISPATCH_NEW_OK; } + return dispatch; } diff --git a/protocols/Tox/libtox/src/toxcore/tox_struct.h b/protocols/Tox/libtox/src/toxcore/tox_struct.h index 22d1c54a27..8b95d83bbc 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_struct.h +++ b/protocols/Tox/libtox/src/toxcore/tox_struct.h @@ -44,6 +44,24 @@ struct Tox { tox_dht_get_nodes_response_cb *dht_get_nodes_response_callback; tox_friend_lossy_packet_cb *friend_lossy_packet_callback_per_pktid[UINT8_MAX + 1]; tox_friend_lossless_packet_cb *friend_lossless_packet_callback_per_pktid[UINT8_MAX + 1]; + tox_group_peer_name_cb *group_peer_name_callback; + tox_group_peer_status_cb *group_peer_status_callback; + tox_group_topic_cb *group_topic_callback; + tox_group_privacy_state_cb *group_privacy_state_callback; + tox_group_topic_lock_cb *group_topic_lock_callback; + tox_group_voice_state_cb *group_voice_state_callback; + tox_group_peer_limit_cb *group_peer_limit_callback; + tox_group_password_cb *group_password_callback; + tox_group_message_cb *group_message_callback; + tox_group_private_message_cb *group_private_message_callback; + tox_group_custom_packet_cb *group_custom_packet_callback; + tox_group_custom_private_packet_cb *group_custom_private_packet_callback; + tox_group_invite_cb *group_invite_callback; + tox_group_peer_join_cb *group_peer_join_callback; + tox_group_peer_exit_cb *group_peer_exit_callback; + tox_group_self_join_cb *group_self_join_callback; + tox_group_join_fail_cb *group_join_fail_callback; + tox_group_moderation_cb *group_moderation_callback; void *toxav_object; // workaround to store a ToxAV object (setter and getter functions are available) }; diff --git a/protocols/Tox/libtox/src/toxcore/util.c b/protocols/Tox/libtox/src/toxcore/util.c index c70c3a76f5..402977a680 100644 --- a/protocols/Tox/libtox/src/toxcore/util.c +++ b/protocols/Tox/libtox/src/toxcore/util.c @@ -139,12 +139,12 @@ uint32_t jenkins_one_at_a_time_hash(const uint8_t *key, size_t len) for (uint32_t i = 0; i < len; ++i) { hash += key[i]; - hash += hash << 10; + hash += (uint32_t)((uint64_t)hash << 10); hash ^= hash >> 6; } - hash += hash << 3; + hash += (uint32_t)((uint64_t)hash << 3); hash ^= hash >> 11; - hash += hash << 15; + hash += (uint32_t)((uint64_t)hash << 15); return hash; } |