summaryrefslogtreecommitdiff
path: root/protocols/Tox/libtox/src
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2023-08-23 19:38:07 +0300
committerGeorge Hazan <george.hazan@gmail.com>2023-08-23 19:38:07 +0300
commitabacfceb23fc41d0b632a7985c2291c02225b432 (patch)
tree186f0c918c8bf671e0bec368f9ff27fd2352d47c /protocols/Tox/libtox/src
parenta8249e73fa598dde2fbde216c7e8f416ebdbf88d (diff)
libtox synced with their master, it could help to fix #3649
Diffstat (limited to 'protocols/Tox/libtox/src')
-rw-r--r--protocols/Tox/libtox/src/toxcore/DHT.c308
-rw-r--r--protocols/Tox/libtox/src/toxcore/DHT.h67
-rw-r--r--protocols/Tox/libtox/src/toxcore/LAN_discovery.c3
-rw-r--r--protocols/Tox/libtox/src/toxcore/LAN_discovery.h3
-rw-r--r--protocols/Tox/libtox/src/toxcore/Messenger.c882
-rw-r--r--protocols/Tox/libtox/src/toxcore/Messenger.h54
-rw-r--r--protocols/Tox/libtox/src/toxcore/TCP_common.c4
-rw-r--r--protocols/Tox/libtox/src/toxcore/TCP_common.h15
-rw-r--r--protocols/Tox/libtox/src/toxcore/TCP_connection.c6
-rw-r--r--protocols/Tox/libtox/src/toxcore/TCP_connection.h6
-rw-r--r--protocols/Tox/libtox/src/toxcore/TCP_server.c17
-rw-r--r--protocols/Tox/libtox/src/toxcore/announce.c27
-rw-r--r--protocols/Tox/libtox/src/toxcore/bin_pack.c6
-rw-r--r--protocols/Tox/libtox/src/toxcore/bin_pack.h3
-rw-r--r--protocols/Tox/libtox/src/toxcore/bin_unpack.c5
-rw-r--r--protocols/Tox/libtox/src/toxcore/bin_unpack.h3
-rw-r--r--protocols/Tox/libtox/src/toxcore/crypto_core.c17
-rw-r--r--protocols/Tox/libtox/src/toxcore/crypto_core.h2
-rw-r--r--protocols/Tox/libtox/src/toxcore/friend_connection.c31
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_announce.c2
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_announce.h2
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_chats.c8410
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_chats.h782
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_common.h409
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_connection.c707
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_connection.h189
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_moderation.c8
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_moderation.h14
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_onion_announce.c115
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_onion_announce.h22
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_pack.c423
-rw-r--r--protocols/Tox/libtox/src/toxcore/group_pack.h35
-rw-r--r--protocols/Tox/libtox/src/toxcore/net_crypto.c33
-rw-r--r--protocols/Tox/libtox/src/toxcore/net_crypto.h1
-rw-r--r--protocols/Tox/libtox/src/toxcore/network.c170
-rw-r--r--protocols/Tox/libtox/src/toxcore/network.h34
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion.c67
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion.h7
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion_announce.c27
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion_announce.h3
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion_client.c222
-rw-r--r--protocols/Tox/libtox/src/toxcore/onion_client.h20
-rw-r--r--protocols/Tox/libtox/src/toxcore/ping.c20
-rw-r--r--protocols/Tox/libtox/src/toxcore/shared_key_cache.c161
-rw-r--r--protocols/Tox/libtox/src/toxcore/shared_key_cache.h51
-rw-r--r--protocols/Tox/libtox/src/toxcore/state.h1
-rw-r--r--protocols/Tox/libtox/src/toxcore/tox.c2016
-rw-r--r--protocols/Tox/libtox/src/toxcore/tox.h2186
-rw-r--r--protocols/Tox/libtox/src/toxcore/tox_api.c36
-rw-r--r--protocols/Tox/libtox/src/toxcore/tox_dispatch.c2
-rw-r--r--protocols/Tox/libtox/src/toxcore/tox_struct.h18
-rw-r--r--protocols/Tox/libtox/src/toxcore/util.c6
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;
}