diff options
Diffstat (limited to 'protocols/Tox/libtox/src')
52 files changed, 16995 insertions, 663 deletions
diff --git a/protocols/Tox/libtox/src/toxcore/DHT.c b/protocols/Tox/libtox/src/toxcore/DHT.c index 8aa161102b..91f0e0ae47 100644 --- a/protocols/Tox/libtox/src/toxcore/DHT.c +++ b/protocols/Tox/libtox/src/toxcore/DHT.c @@ -18,6 +18,7 @@  #include "mono_time.h"  #include "network.h"  #include "ping.h" +#include "shared_key_cache.h"  #include "state.h"  #include "util.h" @@ -43,6 +44,13 @@  /** Number of get node requests to send to quickly find close nodes. */  #define MAX_BOOTSTRAP_TIMES 5 +// TODO(sudden6): find out why we need multiple callbacks and if we really need 32 +#define DHT_FRIEND_MAX_LOCKS 32 + +/* Settings for the shared key cache */ +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 +  typedef struct DHT_Friend_Callback {      dht_ip_cb *ip_callback;      void *data; @@ -61,7 +69,8 @@ struct DHT_Friend {      /* Symmetric NAT hole punching stuff. */      NAT         nat; -    uint16_t lock_count; +    /* Each set bit represents one installed callback */ +    uint32_t lock_flags;      DHT_Friend_Callback callbacks[DHT_FRIEND_MAX_LOCKS];      Node_format to_bootstrap[MAX_SENT_NODES]; @@ -71,6 +80,8 @@ struct DHT_Friend {  static const DHT_Friend empty_dht_friend = {{0}};  const Node_format empty_node_format = {{0}}; +static_assert(sizeof (empty_dht_friend.lock_flags) * 8 == DHT_FRIEND_MAX_LOCKS, "Bitfield size and number of locks don't match"); +  typedef struct Cryptopacket_Handler {      cryptopacket_handler_cb *function;      void *object; @@ -101,8 +112,8 @@ struct DHT {      uint32_t       loaded_num_nodes;      unsigned int   loaded_nodes_index; -    Shared_Keys shared_keys_recv; -    Shared_Keys shared_keys_sent; +    Shared_Key_Cache *shared_keys_recv; +    Shared_Key_Cache *shared_keys_sent;      struct Ping   *ping;      Ping_Array    *dht_ping_array; @@ -200,12 +211,6 @@ static IP_Port ip_port_normalize(const IP_Port *ip_port)      return res;  } -/** @brief Compares pk1 and pk2 with pk. - * - * @retval 0 if both are same distance. - * @retval 1 if pk1 is closer. - * @retval 2 if pk2 is closer. - */  int id_closest(const uint8_t *pk, const uint8_t *pk1, const uint8_t *pk2)  {      for (size_t i = 0; i < CRYPTO_PUBLIC_KEY_SIZE; ++i) { @@ -250,98 +255,25 @@ unsigned int bit_by_bit_cmp(const uint8_t *pk1, const uint8_t *pk2)  }  /** - * Shared key generations are costly, it is therefore smart to store commonly used - * ones so that they can be re-used later without being computed again. - * - * If a shared key is already in shared_keys, copy it to shared_key. - * Otherwise generate it into shared_key and copy it to shared_keys - */ -void get_shared_key(const Mono_Time *mono_time, Shared_Keys *shared_keys, uint8_t *shared_key, -                    const uint8_t *secret_key, const uint8_t *public_key) -{ -    uint32_t num = -1; -    uint32_t curr = 0; - -    for (uint32_t i = 0; i < MAX_KEYS_PER_SLOT; ++i) { -        const int index = public_key[30] * MAX_KEYS_PER_SLOT + i; -        Shared_Key *const key = &shared_keys->keys[index]; - -        if (key->stored) { -            if (pk_equal(public_key, key->public_key)) { -                memcpy(shared_key, key->shared_key, CRYPTO_SHARED_KEY_SIZE); -                ++key->times_requested; -                key->time_last_requested = mono_time_get(mono_time); -                return; -            } - -            if (num != 0) { -                if (mono_time_is_timeout(mono_time, key->time_last_requested, KEYS_TIMEOUT)) { -                    num = 0; -                    curr = index; -                } else if (num > key->times_requested) { -                    num = key->times_requested; -                    curr = index; -                } -            } -        } else if (num != 0) { -            num = 0; -            curr = index; -        } -    } - -    encrypt_precompute(public_key, secret_key, shared_key); - -    if (num != UINT32_MAX) { -        Shared_Key *const key = &shared_keys->keys[curr]; -        key->stored = true; -        key->times_requested = 1; -        memcpy(key->public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); -        memcpy(key->shared_key, shared_key, CRYPTO_SHARED_KEY_SIZE); -        key->time_last_requested = mono_time_get(mono_time); -    } -} - -/**   * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key   * for packets that we receive.   */ -void dht_get_shared_key_recv(DHT *dht, uint8_t *shared_key, const uint8_t *public_key) +const uint8_t *dht_get_shared_key_recv(DHT *dht, const uint8_t *public_key)  { -    get_shared_key(dht->mono_time, &dht->shared_keys_recv, shared_key, dht->self_secret_key, public_key); +    return shared_key_cache_lookup(dht->shared_keys_recv, public_key);  }  /**   * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key   * for packets that we send.   */ -void dht_get_shared_key_sent(DHT *dht, uint8_t *shared_key, const uint8_t *public_key) +const uint8_t *dht_get_shared_key_sent(DHT *dht, const uint8_t *public_key)  { -    get_shared_key(dht->mono_time, &dht->shared_keys_sent, shared_key, dht->self_secret_key, public_key); +    return shared_key_cache_lookup(dht->shared_keys_sent, public_key);  }  #define CRYPTO_SIZE (1 + CRYPTO_PUBLIC_KEY_SIZE * 2 + CRYPTO_NONCE_SIZE) -/** - * @brief Create a request to peer. - * - * Packs the data and sender public key and encrypts the packet. - * - * @param[in] send_public_key public key of the sender. - * @param[in] send_secret_key secret key of the sender. - * @param[out] packet an array of @ref MAX_CRYPTO_REQUEST_SIZE big. - * @param[in] recv_public_key public key of the receiver. - * @param[in] data represents the data we send with the request. - * @param[in] data_length the length of the data. - * @param[in] request_id the id of the request (32 = friend request, 254 = ping request). - * - * @attention Constraints: - * @code - * sizeof(packet) >= MAX_CRYPTO_REQUEST_SIZE - * @endcode - * - * @retval -1 on failure. - * @return the length of the created packet on success. - */  int create_request(const Random *rng, const uint8_t *send_public_key, const uint8_t *send_secret_key,                     uint8_t *packet, const uint8_t *recv_public_key,                     const uint8_t *data, uint32_t data_length, uint8_t request_id) @@ -375,28 +307,6 @@ int create_request(const Random *rng, const uint8_t *send_public_key, const uint      return len + CRYPTO_SIZE;  } -/** - * @brief Decrypts and unpacks a DHT request packet. - * - * Puts the senders public key in the request in @p public_key, the data from - * the request in @p data. - * - * @param[in] self_public_key public key of the receiver (us). - * @param[in] self_secret_key secret key of the receiver (us). - * @param[out] public_key public key of the sender, copied from the input packet. - * @param[out] data decrypted request data, copied from the input packet, must - *   have room for @ref MAX_CRYPTO_REQUEST_SIZE bytes. - * @param[in] packet is the request packet. - * @param[in] packet_length length of the packet. - * - * @attention Constraints: - * @code - * sizeof(data) >= MAX_CRYPTO_REQUEST_SIZE - * @endcode - * - * @retval -1 if not valid request. - * @return the length of the unpacked data. - */  int handle_request(const uint8_t *self_public_key, const uint8_t *self_secret_key, uint8_t *public_key, uint8_t *data,                     uint8_t *request_id, const uint8_t *packet, uint16_t packet_length)  { @@ -435,9 +345,6 @@ int handle_request(const uint8_t *self_public_key, const uint8_t *self_secret_ke      return len1;  } -/** @return packet size of packed node with ip_family on success. - * @retval -1 on failure. - */  int packed_node_size(Family ip_family)  {      if (net_family_is_ipv4(ip_family) || net_family_is_tcp_ipv4(ip_family)) { @@ -452,13 +359,6 @@ int packed_node_size(Family ip_family)  } -/** @brief Pack an IP_Port structure into data of max size length. - * - * Packed_length is the offset of data currently packed. - * - * @return size of packed IP_Port data on success. - * @retval -1 on failure. - */  int pack_ip_port(const Logger *logger, uint8_t *data, uint16_t length, const IP_Port *ip_port)  {      if (data == nullptr) { @@ -514,11 +414,6 @@ int pack_ip_port(const Logger *logger, uint8_t *data, uint16_t length, const IP_      }  } -/** @brief Encrypt plain and write resulting DHT packet into packet with max size length. - * - * @return size of packet on success. - * @retval -1 on failure. - */  int dht_create_packet(const Random *rng, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE],                        const uint8_t *shared_key, const uint8_t type,                        const uint8_t *plain, size_t plain_length, @@ -554,13 +449,6 @@ int dht_create_packet(const Random *rng, const uint8_t public_key[CRYPTO_PUBLIC_      return 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + encrypted_length;  } -/** @brief Unpack IP_Port structure from data of max size length into ip_port. - * - * len_processed is the offset of data currently unpacked. - * - * @return size of unpacked ip_port on success. - * @retval -1 on failure. - */  int unpack_ip_port(IP_Port *ip_port, const uint8_t *data, uint16_t length, bool tcp_enabled)  {      if (data == nullptr) { @@ -621,11 +509,6 @@ int unpack_ip_port(IP_Port *ip_port, const uint8_t *data, uint16_t length, bool      }  } -/** @brief Pack number of nodes into data of maxlength length. - * - * @return length of packed nodes on success. - * @retval -1 on failure. - */  int pack_nodes(const Logger *logger, uint8_t *data, uint16_t length, const Node_format *nodes, uint16_t number)  {      uint32_t packed_length = 0; @@ -655,13 +538,6 @@ int pack_nodes(const Logger *logger, uint8_t *data, uint16_t length, const Node_      return packed_length;  } -/** @brief Unpack data of length into nodes of size max_num_nodes. - * Put the length of the data processed in processed_data_len. - * tcp_enabled sets if TCP nodes are expected (true) or not (false). - * - * @return number of unpacked nodes on success. - * @retval -1 on failure. - */  int unpack_nodes(Node_format *nodes, uint16_t max_num_nodes, uint16_t *processed_data_len, const uint8_t *data,                   uint16_t length, bool tcp_enabled)  { @@ -1057,8 +933,7 @@ static bool send_announce_ping(DHT *dht, const uint8_t *public_key, const IP_Por                                              public_key, CRYPTO_PUBLIC_KEY_SIZE);      memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, &ping_id, sizeof(ping_id)); -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    dht_get_shared_key_sent(dht, shared_key, public_key); +    const uint8_t *shared_key = dht_get_shared_key_sent(dht, public_key);      uint8_t request[1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + sizeof(plain) + CRYPTO_MAC_SIZE]; @@ -1086,8 +961,7 @@ static int handle_data_search_response(void *object, const IP_Port *source,      VLA(uint8_t, plain, plain_len);      const uint8_t *public_key = packet + 1; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    dht_get_shared_key_recv(dht, shared_key, public_key); +    const uint8_t *shared_key = dht_get_shared_key_recv(dht, public_key);      if (decrypt_data_symmetric(shared_key,                                 packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1419,8 +1293,9 @@ uint32_t addto_lists(DHT *dht, const IP_Port *ip_port, const uint8_t *public_key          return used;      } -    for (uint32_t i = 0; i < friend_foundip->lock_count; ++i) { -        if (friend_foundip->callbacks[i].ip_callback != nullptr) { +    for (uint32_t i = 0; i < DHT_FRIEND_MAX_LOCKS; ++i) { +        const bool has_lock = (friend_foundip->lock_flags & (UINT32_C(1) << i)) > 0; +        if (has_lock && friend_foundip->callbacks[i].ip_callback != nullptr) {              friend_foundip->callbacks[i].ip_callback(friend_foundip->callbacks[i].data,                      friend_foundip->callbacks[i].number, &ipp_copy);          } @@ -1515,15 +1390,12 @@ bool dht_getnodes(DHT *dht, const IP_Port *ip_port, const uint8_t *public_key, c      memcpy(plain, client_id, CRYPTO_PUBLIC_KEY_SIZE);      memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, &ping_id, sizeof(ping_id)); -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    dht_get_shared_key_sent(dht, shared_key, public_key); +    const uint8_t *shared_key = dht_get_shared_key_sent(dht, public_key);      const int len = dht_create_packet(dht->rng,                                        dht->self_public_key, shared_key, NET_PACKET_GET_NODES,                                        plain, sizeof(plain), data, sizeof(data)); -    crypto_memzero(shared_key, sizeof(shared_key)); -      if (len != sizeof(data)) {          LOGGER_ERROR(dht->log, "getnodes packet encryption failed");          return false; @@ -1598,9 +1470,7 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p      }      uint8_t plain[CRYPTO_NODE_SIZE]; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - -    dht_get_shared_key_recv(dht, shared_key, packet + 1); +    const uint8_t *shared_key = dht_get_shared_key_recv(dht, packet + 1);      const int len = decrypt_data_symmetric(                          shared_key,                          packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1609,7 +1479,6 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p                          plain);      if (len != CRYPTO_NODE_SIZE) { -        crypto_memzero(shared_key, sizeof(shared_key));          return 1;      } @@ -1617,8 +1486,6 @@ static int handle_getnodes(void *object, const IP_Port *source, const uint8_t *p      ping_add(dht->ping, packet + 1, source); -    crypto_memzero(shared_key, sizeof(shared_key)); -      return 0;  } @@ -1663,8 +1530,7 @@ static bool handle_sendnodes_core(void *object, const IP_Port *source, const uin      }      VLA(uint8_t, plain, 1 + data_size + sizeof(uint64_t)); -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    dht_get_shared_key_sent(dht, shared_key, packet + 1); +    const uint8_t *shared_key = dht_get_shared_key_sent(dht, packet + 1);      const int len = decrypt_data_symmetric(                          shared_key,                          packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -1672,8 +1538,6 @@ static bool handle_sendnodes_core(void *object, const IP_Port *source, const uin                          1 + data_size + sizeof(uint64_t) + CRYPTO_MAC_SIZE,                          plain); -    crypto_memzero(shared_key, sizeof(shared_key)); -      if ((unsigned int)len != SIZEOF_VLA(plain)) {          return false;      } @@ -1745,35 +1609,75 @@ static int handle_sendnodes_ipv6(void *object, const IP_Port *source, const uint  /*----------------------------------------------------------------------------------*/  /*------------------------END of packet handling functions--------------------------*/ -non_null(1) nullable(2, 3, 5) -static void dht_friend_lock(DHT_Friend *const dht_friend, dht_ip_cb *ip_callback, -                            void *data, int32_t number, uint16_t *lock_count) +non_null(1) nullable(2, 3) +static uint32_t dht_friend_lock(DHT_Friend *const dht_friend, dht_ip_cb *ip_callback, +                            void *data, int32_t number)  { -    const uint16_t lock_num = dht_friend->lock_count; -    ++dht_friend->lock_count; +    // find first free slot +    uint8_t lock_num; +    uint32_t lock_token = 0; +    for (lock_num = 0; lock_num < DHT_FRIEND_MAX_LOCKS; ++lock_num) { +        lock_token = UINT32_C(1) << lock_num; +        if ((dht_friend->lock_flags & lock_token) == 0) { +            break; +        } +    } + +    // One of the conditions would be enough, but static analyzers don't get that +    if (lock_token == 0 || lock_num == DHT_FRIEND_MAX_LOCKS) { +        return 0; +    } + +    // Claim that slot +    dht_friend->lock_flags |= lock_token; +      dht_friend->callbacks[lock_num].ip_callback = ip_callback;      dht_friend->callbacks[lock_num].data = data;      dht_friend->callbacks[lock_num].number = number; -    if (lock_count != nullptr) { -        *lock_count = lock_num + 1; +    return lock_token; +} + +non_null() +static void dht_friend_unlock(DHT_Friend *const dht_friend, uint32_t lock_token) +{ +    // If this triggers, there was a double free +    assert((lock_token & dht_friend->lock_flags) > 0); + +    // find used slot +    uint8_t lock_num; +    for (lock_num = 0; lock_num < DHT_FRIEND_MAX_LOCKS; ++lock_num) { +        if (((UINT32_C(1) << lock_num) & lock_token) > 0) { +            break; +        }      } + +    if (lock_num == DHT_FRIEND_MAX_LOCKS) { +        // Gracefully handle double unlock +        return; +    } + +    // Clear the slot +    dht_friend->lock_flags &= ~lock_token; + +    dht_friend->callbacks[lock_num].ip_callback = nullptr; +    dht_friend->callbacks[lock_num].data = nullptr; +    dht_friend->callbacks[lock_num].number = 0;  }  int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, -                  void *data, int32_t number, uint16_t *lock_count) +                  void *data, int32_t number, uint32_t *lock_token)  {      const uint32_t friend_num = index_of_friend_pk(dht->friends_list, dht->num_friends, public_key);      if (friend_num != UINT32_MAX) { /* Is friend already in DHT? */          DHT_Friend *const dht_friend = &dht->friends_list[friend_num]; +        const uint32_t tmp_lock_token = dht_friend_lock(dht_friend, ip_callback, data, number); -        if (dht_friend->lock_count == DHT_FRIEND_MAX_LOCKS) { +        if (tmp_lock_token == 0) {              return -1;          } -        dht_friend_lock(dht_friend, ip_callback, data, number, lock_count); -          return 0;      } @@ -1791,7 +1695,8 @@ int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback,      dht_friend->nat.nat_ping_id = random_u64(dht->rng);      ++dht->num_friends; -    dht_friend_lock(dht_friend, ip_callback, data, number, lock_count); +    *lock_token = dht_friend_lock(dht_friend, ip_callback, data, number); +    assert(*lock_token != 0); // Friend was newly allocated      dht_friend->num_to_bootstrap = get_close_nodes(dht, dht_friend->public_key, dht_friend->to_bootstrap, net_family_unspec(),                                     true, false); @@ -1799,7 +1704,7 @@ int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback,      return 0;  } -int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count) +int dht_delfriend(DHT *dht, const uint8_t *public_key, uint32_t lock_token)  {      const uint32_t friend_num = index_of_friend_pk(dht->friends_list, dht->num_friends, public_key); @@ -1808,13 +1713,9 @@ int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count)      }      DHT_Friend *const dht_friend = &dht->friends_list[friend_num]; -    --dht_friend->lock_count; - -    if (dht_friend->lock_count > 0 && lock_count > 0) { /* DHT friend is still in use.*/ -        --lock_count; -        dht_friend->callbacks[lock_count].ip_callback = nullptr; -        dht_friend->callbacks[lock_count].data = nullptr; -        dht_friend->callbacks[lock_count].number = 0; +    dht_friend_unlock(dht_friend, lock_token); +    if (dht_friend->lock_flags > 0) { +        /* DHT friend is still in use.*/          return 0;      } @@ -1855,7 +1756,7 @@ int dht_getfriendip(const DHT *dht, const uint8_t *public_key, IP_Port *ip_port)      const DHT_Friend *const frnd = &dht->friends_list[friend_index];      const uint32_t client_index = index_of_client_pk(frnd->client_list, MAX_FRIEND_CLIENTS, public_key); -    if (client_index == -1) { +    if (client_index == UINT32_MAX) {          return 0;      } @@ -2059,11 +1960,6 @@ int dht_bootstrap_from_address(DHT *dht, const char *address, bool ipv6enabled,      return 0;  } -/** @brief Send the given packet to node with public_key. - * - * @return number of bytes sent. - * @retval -1 if failure. - */  int route_packet(const DHT *dht, const uint8_t *public_key, const uint8_t *packet, uint16_t length)  {      for (uint32_t i = 0; i < LCLIENT_LIST; ++i) { @@ -2752,6 +2648,15 @@ DHT *new_dht(const Logger *log, const Random *rng, const Network *ns, Mono_Time      crypto_new_keypair(rng, dht->self_public_key, dht->self_secret_key); +    dht->shared_keys_recv = shared_key_cache_new(mono_time, dht->self_secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); +    dht->shared_keys_sent = shared_key_cache_new(mono_time, dht->self_secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + +    if (dht->shared_keys_recv == nullptr || dht->shared_keys_sent == nullptr) { +        kill_dht(dht); +        return nullptr; +    } + +      dht->dht_ping_array = ping_array_new(DHT_PING_ARRAY_SIZE, PING_TIMEOUT);      if (dht->dht_ping_array == nullptr) { @@ -2765,7 +2670,8 @@ DHT *new_dht(const Logger *log, const Random *rng, const Network *ns, Mono_Time          crypto_new_keypair(rng, random_public_key_bytes, random_secret_key_bytes); -        if (dht_addfriend(dht, random_public_key_bytes, nullptr, nullptr, 0, nullptr) != 0) { +        uint32_t token; // We don't intend to delete these ever, but need to pass the token +        if (dht_addfriend(dht, random_public_key_bytes, nullptr, nullptr, 0, &token) != 0) {              kill_dht(dht);              return nullptr;          } @@ -2813,12 +2719,12 @@ void kill_dht(DHT *dht)      networking_registerhandler(dht->net, NET_PACKET_LAN_DISCOVERY, nullptr, nullptr);      cryptopacket_registerhandler(dht, CRYPTO_PACKET_NAT_PING, nullptr, nullptr); +    shared_key_cache_free(dht->shared_keys_recv); +    shared_key_cache_free(dht->shared_keys_sent);      ping_array_kill(dht->dht_ping_array);      ping_kill(dht->ping);      free(dht->friends_list);      free(dht->loaded_nodes_list); -    crypto_memzero(&dht->shared_keys_recv, sizeof(dht->shared_keys_recv)); -    crypto_memzero(&dht->shared_keys_sent, sizeof(dht->shared_keys_sent));      crypto_memzero(dht->self_secret_key, sizeof(dht->self_secret_key));      free(dht);  } @@ -2929,11 +2835,6 @@ void dht_save(const DHT *dht, uint8_t *data)  /** Bootstrap from this number of nodes every time `dht_connect_after_load()` is called */  #define SAVE_BOOTSTAP_FREQUENCY 8 -/** @brief Start sending packets after DHT loaded_friends_list and loaded_clients_list are set. - * - * @retval 0 if successful - * @retval -1 otherwise - */  int dht_connect_after_load(DHT *dht)  {      if (dht == nullptr) { @@ -3003,11 +2904,6 @@ static State_Load_Status dht_load_state_callback(void *outer, const uint8_t *dat      return STATE_LOAD_STATUS_CONTINUE;  } -/** @brief Load the DHT from data of size size. - * - * @retval -1 if failure. - * @retval 0 if success. - */  int dht_load(DHT *dht, const uint8_t *data, uint32_t length)  {      const uint32_t cookie_len = sizeof(uint32_t); @@ -3066,16 +2962,6 @@ bool dht_non_lan_connected(const DHT *dht)      return false;  } -/** @brief Copies our own ip_port structure to `dest`. - * - * WAN addresses take priority over LAN addresses. - * - * This function will zero the `dest` buffer before use. - * - * @retval 0 if our ip port can't be found (this usually means we're not connected to the DHT). - * @retval 1 if IP is a WAN address. - * @retval 2 if IP is a LAN address. - */  unsigned int ipport_self_copy(const DHT *dht, IP_Port *dest)  {      ipport_reset(dest); diff --git a/protocols/Tox/libtox/src/toxcore/DHT.h b/protocols/Tox/libtox/src/toxcore/DHT.h index 34ecf9dd0b..86ac4f9d40 100644 --- a/protocols/Tox/libtox/src/toxcore/DHT.h +++ b/protocols/Tox/libtox/src/toxcore/DHT.h @@ -22,6 +22,20 @@  extern "C" {  #endif +/* Encryption and signature keys definition */ +#define ENC_PUBLIC_KEY_SIZE CRYPTO_PUBLIC_KEY_SIZE +#define ENC_SECRET_KEY_SIZE CRYPTO_SECRET_KEY_SIZE +#define SIG_PUBLIC_KEY_SIZE CRYPTO_SIGN_PUBLIC_KEY_SIZE +#define SIG_SECRET_KEY_SIZE CRYPTO_SIGN_SECRET_KEY_SIZE + +/* Size of the group chat_id */ +#define CHAT_ID_SIZE SIG_PUBLIC_KEY_SIZE + +/* Extended keys for group chats */ +#define EXT_SECRET_KEY_SIZE (ENC_SECRET_KEY_SIZE + SIG_SECRET_KEY_SIZE) +#define EXT_PUBLIC_KEY_SIZE (ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE) + +  /* Maximum size of a signature (may be smaller) */  #define SIGNATURE_SIZE CRYPTO_SIGNATURE_SIZE  /** Maximum number of clients stored per friend. */ @@ -172,8 +186,6 @@ typedef struct NAT {      uint64_t    nat_ping_timestamp;  } NAT; -#define DHT_FRIEND_MAX_LOCKS 32 -  typedef struct Node_format {      uint8_t     public_key[CRYPTO_PUBLIC_KEY_SIZE];      IP_Port     ip_port; @@ -242,24 +254,6 @@ non_null(1, 4) nullable(3)  int unpack_nodes(Node_format *nodes, uint16_t max_num_nodes, uint16_t *processed_data_len, const uint8_t *data,                   uint16_t length, bool tcp_enabled); - -/*----------------------------------------------------------------------------------*/ -/* struct to store some shared keys so we don't have to regenerate them for each request. */ -#define MAX_KEYS_PER_SLOT 4 -#define KEYS_TIMEOUT 600 - -typedef struct Shared_Key { -    uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    uint32_t times_requested; -    bool stored; -    uint64_t time_last_requested; -} Shared_Key; - -typedef struct Shared_Keys { -    Shared_Key keys[256 * MAX_KEYS_PER_SLOT]; -} Shared_Keys; -  /*----------------------------------------------------------------------------------*/  typedef int cryptopacket_handler_cb(void *object, const IP_Port *ip_port, const uint8_t *source_pubkey, @@ -284,30 +278,18 @@ non_null() const uint8_t *dht_get_friend_public_key(const DHT *dht, uint32_t fri  /*----------------------------------------------------------------------------------*/  /** - * Shared key generations are costly, it is therefore smart to store commonly used - * ones so that they can be re-used later without being computed again. - * - * If a shared key is already in shared_keys, copy it to shared_key. - * Otherwise generate it into shared_key and copy it to shared_keys - */ -non_null() -void get_shared_key( -    const Mono_Time *mono_time, Shared_Keys *shared_keys, uint8_t *shared_key, -    const uint8_t *secret_key, const uint8_t *public_key); - -/**   * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key   * for packets that we receive.   */  non_null() -void dht_get_shared_key_recv(DHT *dht, uint8_t *shared_key, const uint8_t *public_key); +const uint8_t *dht_get_shared_key_recv(DHT *dht, const uint8_t *public_key);  /**   * Copy shared_key to encrypt/decrypt DHT packet from public_key into shared_key   * for packets that we send.   */  non_null() -void dht_get_shared_key_sent(DHT *dht, uint8_t *shared_key, const uint8_t *public_key); +const uint8_t *dht_get_shared_key_sent(DHT *dht, const uint8_t *public_key);  /**   * Sends a getnodes request to `ip_port` with the public key `public_key` for nodes @@ -327,29 +309,34 @@ non_null(1) nullable(2)  void dht_callback_get_nodes_response(DHT *dht, dht_get_nodes_response_cb *function);  /** @brief Add a new friend to the friends list. - * public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. + * @param public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long.   * - * ip_callback is the callback of a function that will be called when the ip address + * @param ip_callback is the callback of a function that will be called when the ip address   * is found along with arguments data and number. + * @param data User data for the callback + * @param number Will be passed to ip_callback   * - * lock_count will be set to a non zero number that must be passed to `dht_delfriend()` + * @param lock_token will be set to a non zero number that must be passed to `dht_delfriend()`   * to properly remove the callback.   *   * @retval 0 if success.   * @retval -1 if failure (friends list is full).   */ -non_null(1, 2) nullable(3, 4, 6) +non_null(1, 2, 6) nullable(3, 4)  int dht_addfriend(DHT *dht, const uint8_t *public_key, dht_ip_cb *ip_callback, -                  void *data, int32_t number, uint16_t *lock_count); +                  void *data, int32_t number, uint32_t *lock_token);  /** @brief Delete a friend from the friends list.   * public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. + * @param dht The DHT object + * @param public_key The public key of the friend + * @param lock_token The token received by dht_addfriend(...)   *   * @retval 0 if success.   * @retval -1 if failure (public_key not in friends list).   */  non_null() -int dht_delfriend(DHT *dht, const uint8_t *public_key, uint16_t lock_count); +int dht_delfriend(DHT *dht, const uint8_t *public_key, uint32_t lock_token);  /** @brief Get ip of friend.   * diff --git a/protocols/Tox/libtox/src/toxcore/LAN_discovery.c b/protocols/Tox/libtox/src/toxcore/LAN_discovery.c index 82c02b59e9..ef44d3b552 100644 --- a/protocols/Tox/libtox/src/toxcore/LAN_discovery.c +++ b/protocols/Tox/libtox/src/toxcore/LAN_discovery.c @@ -339,7 +339,8 @@ bool ip_is_lan(const IP *ip)  } -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port) +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, +                        uint16_t port)  {      if (broadcast == nullptr) {          return false; diff --git a/protocols/Tox/libtox/src/toxcore/LAN_discovery.h b/protocols/Tox/libtox/src/toxcore/LAN_discovery.h index 85ea9478f4..5d9c83335b 100644 --- a/protocols/Tox/libtox/src/toxcore/LAN_discovery.h +++ b/protocols/Tox/libtox/src/toxcore/LAN_discovery.h @@ -24,7 +24,8 @@ typedef struct Broadcast_Info Broadcast_Info;   * @return true on success, false on failure.   */  non_null() -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port); +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, +                        uint16_t port);  /**   * Discovers broadcast devices and IP addresses. diff --git a/protocols/Tox/libtox/src/toxcore/Messenger.c b/protocols/Tox/libtox/src/toxcore/Messenger.c index 6042fd725b..118c7d946c 100644 --- a/protocols/Tox/libtox/src/toxcore/Messenger.c +++ b/protocols/Tox/libtox/src/toxcore/Messenger.c @@ -14,7 +14,10 @@  #include <string.h>  #include <time.h> +#include "DHT.h"  #include "ccompat.h" +#include "group_chats.h" +#include "group_onion_announce.h"  #include "logger.h"  #include "mono_time.h"  #include "network.h" @@ -26,6 +29,16 @@ static_assert(MAX_CONCURRENT_FILE_PIPES <= UINT8_MAX + 1,  static const Friend empty_friend = {{0}}; +/** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +bool friend_is_valid(const Messenger *m, int32_t friendnumber) +{ +    return (uint32_t)friendnumber < m->numfriends && m->friendlist[friendnumber].status != 0; +} +  /** @brief Set the size of the friend list to numfriends.   *   * @retval -1 if realloc fails. @@ -107,15 +120,11 @@ void getaddress(const Messenger *m, uint8_t *address)  }  non_null() -static bool send_online_packet(Messenger *m, int32_t friendnumber) +static bool send_online_packet(Messenger *m, int friendcon_id)  { -    if (!m_friend_exists(m, friendnumber)) { -        return false; -    } -      uint8_t packet = PACKET_ID_ONLINE; -    return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, -                             m->friendlist[friendnumber].friendcon_id), &packet, sizeof(packet), false) != -1; +    return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, friendcon_id), &packet, +                             sizeof(packet), false) != -1;  }  non_null() @@ -174,7 +183,7 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta              }              if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { -                send_online_packet(m, i); +                send_online_packet(m, friendcon_id);              }              return i; @@ -184,6 +193,20 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta      return FAERR_NOMEM;  } +non_null() +static int32_t m_add_friend_contact_norequest(Messenger *m, const uint8_t *real_pk) +{ +    if (getfriend_id(m, real_pk) != -1) { +        return FAERR_ALREADYSENT; +    } + +    if (pk_equal(real_pk, nc_get_self_public_key(m->net_crypto))) { +        return FAERR_OWNKEY; +    } + +    return init_new_friend(m, real_pk, FRIEND_CONFIRMED); +} +  /**   * Add a friend.   * @@ -268,10 +291,6 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u  int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk)  { -    if (getfriend_id(m, real_pk) != -1) { -        return FAERR_ALREADYSENT; -    } -      if (!public_key_valid(real_pk)) {          return FAERR_BADCHECKSUM;      } @@ -280,7 +299,7 @@ int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk)          return FAERR_OWNKEY;      } -    return init_new_friend(m, real_pk, FRIEND_CONFIRMED); +    return m_add_friend_contact_norequest(m, real_pk);  }  non_null() @@ -344,6 +363,53 @@ static int friend_received_packet(const Messenger *m, int32_t friendnumber, uint                                  m->friendlist[friendnumber].friendcon_id), number);  } +bool m_create_group_connection(Messenger *m, GC_Chat *chat) +{ +    random_bytes(m->rng, chat->m_group_public_key, CRYPTO_PUBLIC_KEY_SIZE); +    const int friendcon_id = new_friend_connection(m->fr_c, chat->m_group_public_key); + +    if (friendcon_id == -1) { +        return false; +    } + +    const Friend_Conn *connection = get_conn(m->fr_c, friendcon_id); + +    if (connection == nullptr) { +        return false; +    } + +    chat->friend_connection_id = friendcon_id; + +    if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { +        send_online_packet(m, friendcon_id); +    } + +    const int onion_friend_number = friend_conn_get_onion_friendnum(connection); +    Onion_Friend *onion_friend = onion_get_friend(m->onion_c, (uint16_t)onion_friend_number); + +    onion_friend_set_gc_public_key(onion_friend, get_chat_id(chat->chat_public_key)); +    onion_friend_set_gc_data(onion_friend, nullptr, 0); + +    return true; +} + +/** + * Kills the friend connection for a groupchat. + */ +void m_kill_group_connection(Messenger *m, const GC_Chat *chat) +{ +    remove_request_received(m->fr, chat->m_group_public_key); + +    friend_connection_callbacks(m->fr_c, chat->friend_connection_id, MESSENGER_CALLBACK_INDEX, nullptr, +                                nullptr, nullptr, nullptr, 0); + +    if (friend_con_connected(m->fr_c, chat->friend_connection_id) == FRIENDCONN_STATUS_CONNECTED) { +        send_offline_packet(m, chat->friend_connection_id); +    } + +    kill_friend_connection(m->fr_c, chat->friend_connection_id); +} +  non_null(1) nullable(3)  static int do_receipts(Messenger *m, int32_t friendnumber, void *userdata)  { @@ -991,6 +1057,11 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function      m->conference_invite = function;  } +/** @brief the callback for group invites. */ +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function) +{ +    m->group_invite = function; +}  /** @brief Send a conference invite packet.   * @@ -1002,6 +1073,17 @@ bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, con      return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_CONFERENCE, data, length, false);  } + +/** @brief Send a group invite packet. + * + * @retval true if success + */ +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length) +{ +    return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_GROUPCHAT, data, length, false); +} + +  /*** FILE SENDING */ @@ -1922,12 +2004,13 @@ static void check_friend_request_timed_out(Messenger *m, uint32_t i, uint64_t t,      }  } +non_null(1) nullable(4)  static int m_handle_status(void *object, int i, bool status, void *userdata)  {      Messenger *m = (Messenger *)object;      if (status) { /* Went online. */ -        send_online_packet(m, i); +        send_online_packet(m, m->friendlist[i].friendcon_id);      } else { /* Went offline. */          if (m->friendlist[i].status == FRIEND_ONLINE) {              set_friend_status(m, i, FRIEND_CONFIRMED, userdata); @@ -1937,321 +2020,396 @@ static int m_handle_status(void *object, int i, bool status, void *userdata)      return 0;  } -static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t len, void *userdata) +non_null(1, 3) nullable(5) +static int m_handle_packet_offline(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata)  { -    if (len == 0) { -        return -1; +    if (data_length == 0) { +    	set_friend_status(m, i, FRIEND_CONFIRMED, userdata);      } -    Messenger *m = (Messenger *)object; -    const uint8_t packet_id = temp[0]; -    const uint8_t *data = temp + 1; -    const uint16_t data_length = len - 1; +    return 0; +} -    if (m->friendlist[i].status != FRIEND_ONLINE) { -        if (packet_id == PACKET_ID_ONLINE && len == 1) { -            set_friend_status(m, i, FRIEND_ONLINE, userdata); -            send_online_packet(m, i); -        } else { -            return -1; -        } +non_null(1, 3) nullable(5) +static int m_handle_packet_nickname(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length > MAX_NAME_LENGTH) { +        return 0;      } -    switch (packet_id) { -        case PACKET_ID_OFFLINE: { -            if (data_length > 0) { -                break; -            } - -            set_friend_status(m, i, FRIEND_CONFIRMED, userdata); -            break; -        } +    /* Make sure the NULL terminator is present. */ +    VLA(uint8_t, data_terminated, data_length + 1); +    memcpy(data_terminated, data, data_length); +    data_terminated[data_length] = 0; -        case PACKET_ID_NICKNAME: { -            if (data_length > MAX_NAME_LENGTH) { -                break; -            } +    /* inform of namechange before we overwrite the old name */ +    if (m->friend_namechange != nullptr) { +        m->friend_namechange(m, i, data_terminated, data_length, userdata); +    } -            /* Make sure the NULL terminator is present. */ -            VLA(uint8_t, data_terminated, data_length + 1); -            memcpy(data_terminated, data, data_length); -            data_terminated[data_length] = 0; +    memcpy(m->friendlist[i].name, data_terminated, data_length); +    m->friendlist[i].name_length = data_length; -            /* inform of namechange before we overwrite the old name */ -            if (m->friend_namechange != nullptr) { -                m->friend_namechange(m, i, data_terminated, data_length, userdata); -            } +    return 0; +} -            memcpy(m->friendlist[i].name, data_terminated, data_length); -            m->friendlist[i].name_length = data_length; +non_null(1, 3) nullable(5) +static int m_handle_packet_statusmessage(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length > MAX_STATUSMESSAGE_LENGTH) { +        return 0; +    } -            break; -        } +    /* Make sure the NULL terminator is present. */ +    VLA(uint8_t, data_terminated, data_length + 1); +    memcpy(data_terminated, data, data_length); +    data_terminated[data_length] = 0; -        case PACKET_ID_STATUSMESSAGE: { -            if (data_length > MAX_STATUSMESSAGE_LENGTH) { -                break; -            } +    if (m->friend_statusmessagechange != nullptr) { +        m->friend_statusmessagechange(m, i, data_terminated, data_length, userdata); +    } -            /* Make sure the NULL terminator is present. */ -            VLA(uint8_t, data_terminated, data_length + 1); -            memcpy(data_terminated, data, data_length); -            data_terminated[data_length] = 0; +    set_friend_statusmessage(m, i, data_terminated, data_length); -            if (m->friend_statusmessagechange != nullptr) { -                m->friend_statusmessagechange(m, i, data_terminated, data_length, userdata); -            } +    return 0; +} -            set_friend_statusmessage(m, i, data_terminated, data_length); -            break; -        } +non_null(1, 3) nullable(5) +static int m_handle_packet_userstatus(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length != 1) { +        return 0; +    } -        case PACKET_ID_USERSTATUS: { -            if (data_length != 1) { -                break; -            } +    const Userstatus status = (Userstatus)data[0]; -            const Userstatus status = (Userstatus)data[0]; +    if (status >= USERSTATUS_INVALID) { +        return 0; +    } -            if (status >= USERSTATUS_INVALID) { -                break; -            } +    if (m->friend_userstatuschange != nullptr) { +        m->friend_userstatuschange(m, i, status, userdata); +    } -            if (m->friend_userstatuschange != nullptr) { -                m->friend_userstatuschange(m, i, status, userdata); -            } +    set_friend_userstatus(m, i, status); -            set_friend_userstatus(m, i, status); -            break; -        } +    return 0; +} -        case PACKET_ID_TYPING: { -            if (data_length != 1) { -                break; -            } +non_null(1, 3) nullable(5) +static int m_handle_packet_typing(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length != 1) { +        return 0; +    } -            const bool typing = data[0] != 0; +    const bool typing = data[0] != 0; -            set_friend_typing(m, i, typing); +    set_friend_typing(m, i, typing); -            if (m->friend_typingchange != nullptr) { -                m->friend_typingchange(m, i, typing, userdata); -            } +    if (m->friend_typingchange != nullptr) { +        m->friend_typingchange(m, i, typing, userdata); +    } -            break; -        } +    return 0; +} -        case PACKET_ID_MESSAGE: // fall-through -        case PACKET_ID_ACTION: { -            if (data_length == 0) { -                break; -            } +non_null(1, 3) nullable(6) +static int m_handle_packet_message(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, const Message_Type message_type, void *userdata) +{ +    if (data_length == 0) { +        return 0; +    } -            const uint8_t *message = data; -            const uint16_t message_length = data_length; +    const uint8_t *message = data; +    const uint16_t message_length = data_length; -            /* Make sure the NULL terminator is present. */ -            VLA(uint8_t, message_terminated, message_length + 1); -            memcpy(message_terminated, message, message_length); -            message_terminated[message_length] = 0; -            const uint8_t type = packet_id - PACKET_ID_MESSAGE; +    /* Make sure the NULL terminator is present. */ +    VLA(uint8_t, message_terminated, message_length + 1); +    memcpy(message_terminated, message, message_length); +    message_terminated[message_length] = 0; -            if (m->friend_message != nullptr) { -                m->friend_message(m, i, type, message_terminated, message_length, userdata); -            } +    if (m->friend_message != nullptr) { +        m->friend_message(m, i, message_type, message_terminated, message_length, userdata); +    } -            break; -        } +    return 0; +} -        case PACKET_ID_INVITE_CONFERENCE: { -            if (data_length == 0) { -                break; -            } +non_null(1, 3) nullable(5) +static int m_handle_packet_invite_conference(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length == 0) { +        return 0; +    } -            if (m->conference_invite != nullptr) { -                m->conference_invite(m, i, data, data_length, userdata); -            } +    if (m->conference_invite != nullptr) { +        m->conference_invite(m, i, data, data_length, userdata); +    } -            break; -        } +    return 0; +} -        case PACKET_ID_FILE_SENDREQUEST: { -            const unsigned int head_length = 1 + sizeof(uint32_t) + sizeof(uint64_t) + FILE_ID_LENGTH; +non_null(1, 3) nullable(5) +static int m_handle_packet_file_sendrequest(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    const unsigned int head_length = 1 + sizeof(uint32_t) + sizeof(uint64_t) + FILE_ID_LENGTH; -            if (data_length < head_length) { -                break; -            } +    if (data_length < head_length) { +        return 0; +    } -            const uint8_t filenumber = data[0]; +    const uint8_t filenumber = data[0];  #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES -            if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { -                break; -            } +    if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { +        return 0; +    }  #endif -            uint64_t filesize; -            uint32_t file_type; -            const uint16_t filename_length = data_length - head_length; +    uint64_t filesize; +    uint32_t file_type; +    const uint16_t filename_length = data_length - head_length; -            if (filename_length > MAX_FILENAME_LENGTH) { -                break; -            } +    if (filename_length > MAX_FILENAME_LENGTH) { +        return 0; +    } -            memcpy(&file_type, data + 1, sizeof(file_type)); -            file_type = net_ntohl(file_type); +    memcpy(&file_type, data + 1, sizeof(file_type)); +    file_type = net_ntohl(file_type); -            net_unpack_u64(data + 1 + sizeof(uint32_t), &filesize); -            struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; +    net_unpack_u64(data + 1 + sizeof(uint32_t), &filesize); +    struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; -            if (ft->status != FILESTATUS_NONE) { -                break; -            } +    if (ft->status != FILESTATUS_NONE) { +        return 0; +    } -            ft->status = FILESTATUS_NOT_ACCEPTED; -            ft->size = filesize; -            ft->transferred = 0; -            ft->paused = FILE_PAUSE_NOT; -            memcpy(ft->id, data + 1 + sizeof(uint32_t) + sizeof(uint64_t), FILE_ID_LENGTH); +    ft->status = FILESTATUS_NOT_ACCEPTED; +    ft->size = filesize; +    ft->transferred = 0; +    ft->paused = FILE_PAUSE_NOT; +    memcpy(ft->id, data + 1 + sizeof(uint32_t) + sizeof(uint64_t), FILE_ID_LENGTH); -            VLA(uint8_t, filename_terminated, filename_length + 1); -            const uint8_t *filename = nullptr; +    VLA(uint8_t, filename_terminated, filename_length + 1); +    const uint8_t *filename = nullptr; -            if (filename_length > 0) { -                /* Force NULL terminate file name. */ -                memcpy(filename_terminated, data + head_length, filename_length); -                filename_terminated[filename_length] = 0; -                filename = filename_terminated; -            } +    if (filename_length > 0) { +        /* Force NULL terminate file name. */ +        memcpy(filename_terminated, data + head_length, filename_length); +        filename_terminated[filename_length] = 0; +        filename = filename_terminated; +    } -            uint32_t real_filenumber = filenumber; -            real_filenumber += 1; -            real_filenumber <<= 16; +    uint32_t real_filenumber = filenumber; +    real_filenumber += 1; +    real_filenumber <<= 16; -            if (m->file_sendrequest != nullptr) { -                m->file_sendrequest(m, i, real_filenumber, file_type, filesize, filename, filename_length, -                                    userdata); -            } +    if (m->file_sendrequest != nullptr) { +        m->file_sendrequest(m, i, real_filenumber, file_type, filesize, filename, filename_length, +                            userdata); +    } -            break; -        } +    return 0; +} -        case PACKET_ID_FILE_CONTROL: { -            if (data_length < 3) { -                break; -            } +non_null(1, 3) nullable(5) +static int m_handle_packet_file_control(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length < 3) { +        return 0; +    } -            // On the other side, "outbound" is "inbound", i.e. if they send 1, -            // that means "inbound" on their side, but we call it "outbound" -            // here. -            const bool outbound = data[0] == 1; -            uint8_t filenumber = data[1]; -            const uint8_t control_type = data[2]; +    // On the other side, "outbound" is "inbound", i.e. if they send 1, +    // that means "inbound" on their side, but we call it "outbound" +    // here. +    const bool outbound = data[0] == 1; +    const uint8_t filenumber = data[1]; +    const uint8_t control_type = data[2];  #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES -            if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { -                break; -            } +    if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { +        return 0; +    }  #endif -            if (handle_filecontrol(m, i, outbound, filenumber, control_type, data + 3, data_length - 3, userdata) == -1) { -                // TODO(iphydf): Do something different here? Right now, this -                // check is pointless. -                break; -            } +    if (handle_filecontrol(m, i, outbound, filenumber, control_type, data + 3, data_length - 3, userdata) == -1) { +        // TODO(iphydf): Do something different here? Right now, this +        // check is pointless. +        return 0; +    } -            break; -        } +    return 0; +} -        case PACKET_ID_FILE_DATA: { -            if (data_length < 1) { -                break; -            } +non_null(1, 3) nullable(5) +static int m_handle_packet_file_data(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length < 1) { +        return 0; +    } -            uint8_t filenumber = data[0]; +    const uint8_t filenumber = data[0];  #if UINT8_MAX >= MAX_CONCURRENT_FILE_PIPES -            if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { -                break; -            } +    if (filenumber >= MAX_CONCURRENT_FILE_PIPES) { +        return 0; +    }  #endif -            struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; +    struct File_Transfers *ft = &m->friendlist[i].file_receiving[filenumber]; -            if (ft->status != FILESTATUS_TRANSFERRING) { -                break; -            } +    if (ft->status != FILESTATUS_TRANSFERRING) { +        return 0; +    } -            uint64_t position = ft->transferred; -            uint32_t real_filenumber = filenumber; -            real_filenumber += 1; -            real_filenumber <<= 16; -            uint16_t file_data_length = data_length - 1; -            const uint8_t *file_data; +    uint64_t position = ft->transferred; +    uint32_t real_filenumber = filenumber; +    real_filenumber += 1; +    real_filenumber <<= 16; +    uint16_t file_data_length = data_length - 1; +    const uint8_t *file_data; -            if (file_data_length == 0) { -                file_data = nullptr; -            } else { -                file_data = data + 1; -            } +    if (file_data_length == 0) { +        file_data = nullptr; +    } else { +        file_data = data + 1; +    } -            /* Prevent more data than the filesize from being passed to clients. */ -            if ((ft->transferred + file_data_length) > ft->size) { -                file_data_length = ft->size - ft->transferred; -            } +    /* Prevent more data than the filesize from being passed to clients. */ +    if ((ft->transferred + file_data_length) > ft->size) { +        file_data_length = ft->size - ft->transferred; +    } -            if (m->file_filedata != nullptr) { -                m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); -            } +    if (m->file_filedata != nullptr) { +        m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); +    } -            ft->transferred += file_data_length; +    ft->transferred += file_data_length; -            if (file_data_length > 0 && (ft->transferred >= ft->size || file_data_length != MAX_FILE_DATA_SIZE)) { -                file_data_length = 0; -                file_data = nullptr; -                position = ft->transferred; +    if (file_data_length > 0 && (ft->transferred >= ft->size || file_data_length != MAX_FILE_DATA_SIZE)) { +        file_data_length = 0; +        file_data = nullptr; +        position = ft->transferred; -                /* Full file received. */ -                if (m->file_filedata != nullptr) { -                    m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); -                } -            } +        /* Full file received. */ +        if (m->file_filedata != nullptr) { +            m->file_filedata(m, i, real_filenumber, position, file_data, file_data_length, userdata); +        } +    } -            /* Data is zero, filetransfer is over. */ -            if (file_data_length == 0) { -                ft->status = FILESTATUS_NONE; -            } +    /* Data is zero, filetransfer is over. */ +    if (file_data_length == 0) { +        ft->status = FILESTATUS_NONE; +    } -            break; -        } +    return 0; +} -        case PACKET_ID_MSI: { -            if (data_length == 0) { -                break; -            } +non_null(1, 3) nullable(5) +static int m_handle_packet_msi(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +    if (data_length == 0) { +        return 0; +    } -            if (m->msi_packet != nullptr) { -                m->msi_packet(m, i, data, data_length, m->msi_packet_userdata); -            } +    if (m->msi_packet != nullptr) { +        m->msi_packet(m, i, data, data_length, m->msi_packet_userdata); +    } -            break; -        } +    return 0; +} -        default: { -            handle_custom_lossless_packet(object, i, temp, len, userdata); -            break; +non_null(1, 3) nullable(5) +static int m_handle_packet_invite_groupchat(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +#ifndef VANILLA_NACL + +    // first two bytes are messenger packet type and group invite type +    if (data_length < 2 + GC_JOIN_DATA_LENGTH) { +        return 0; +    } + +    const uint8_t invite_type = data[1]; +    const uint8_t *join_data = data + 2; +    const uint32_t join_data_len = data_length - 2; + +    if (m->group_invite != nullptr && data[1] == GROUP_INVITE && data_length != 2 + GC_JOIN_DATA_LENGTH) { +        if (group_not_added(m->group_handler, join_data, join_data_len)) { +            m->group_invite(m, i, join_data, GC_JOIN_DATA_LENGTH, +                            join_data + GC_JOIN_DATA_LENGTH, join_data_len - GC_JOIN_DATA_LENGTH, userdata);          } +    } else if (invite_type == GROUP_INVITE_ACCEPTED) { +        handle_gc_invite_accepted_packet(m->group_handler, i, join_data, join_data_len); +    } else if (invite_type == GROUP_INVITE_CONFIRMATION) { +        handle_gc_invite_confirmed_packet(m->group_handler, i, join_data, join_data_len);      } +#endif // VANILLA_NACL +      return 0;  } +non_null(1, 3) nullable(5) +static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t len, void *userdata) +{ +    if (len == 0) { +        return -1; +    } + +    Messenger *m = (Messenger *)object; +    const uint8_t packet_id = temp[0]; +    const uint8_t *data = temp + 1; +    const uint16_t data_length = len - 1; + +    if (m->friendlist[i].status != FRIEND_ONLINE) { +        if (packet_id == PACKET_ID_ONLINE && len == 1) { +            set_friend_status(m, i, FRIEND_ONLINE, userdata); +            send_online_packet(m, m->friendlist[i].friendcon_id); +        } else { +            return -1; +        } +    } + +    switch (packet_id) { +        // TODO(Green-Sky): now all return 0 on error AND success, make errors errors? +        case PACKET_ID_OFFLINE: +            return m_handle_packet_offline(m, i, data, data_length, userdata); +        case PACKET_ID_NICKNAME: +            return m_handle_packet_nickname(m, i, data, data_length, userdata); +        case PACKET_ID_STATUSMESSAGE: +            return m_handle_packet_statusmessage(m, i, data, data_length, userdata); +        case PACKET_ID_USERSTATUS: +            return m_handle_packet_userstatus(m, i, data, data_length, userdata); +        case PACKET_ID_TYPING: +            return m_handle_packet_typing(m, i, data, data_length, userdata); +        case PACKET_ID_MESSAGE: +            return m_handle_packet_message(m, i, data, data_length, MESSAGE_NORMAL, userdata); +        case PACKET_ID_ACTION: +            return m_handle_packet_message(m, i, data, data_length, MESSAGE_ACTION, userdata); +        case PACKET_ID_INVITE_CONFERENCE: +            return m_handle_packet_invite_conference(m, i, data, data_length, userdata); +        case PACKET_ID_FILE_SENDREQUEST: +            return m_handle_packet_file_sendrequest(m, i, data, data_length, userdata); +        case PACKET_ID_FILE_CONTROL: +            return m_handle_packet_file_control(m, i, data, data_length, userdata); +        case PACKET_ID_FILE_DATA: +            return m_handle_packet_file_data(m, i, data, data_length, userdata); +        case PACKET_ID_MSI: +            return m_handle_packet_msi(m, i, data, data_length, userdata); +	    case PACKET_ID_INVITE_GROUPCHAT: +	        return m_handle_packet_invite_groupchat(m, i, data, data_length, userdata); +    } + +    return handle_custom_lossless_packet(object, i, temp, len, userdata); +} +  non_null(1) nullable(2)  static void do_friends(Messenger *m, void *userdata)  { @@ -2370,6 +2528,89 @@ uint32_t messenger_run_interval(const Messenger *m)      return crypto_interval;  } +/** @brief Attempts to create a DHT announcement for a group chat with our connection info. An + * announcement can only be created if we either have a UDP or TCP connection to the network. + * + * @retval true if success. + */ +#ifndef VANILLA_NACL +non_null() +static bool self_announce_group(const Messenger *m, GC_Chat *chat, Onion_Friend *onion_friend) +{ +    GC_Public_Announce announce = {{{{{0}}}}}; + +    const bool ip_port_is_set = chat->self_udp_status != SELF_UDP_STATUS_NONE; +    const int tcp_num = tcp_copy_connected_relays(chat->tcp_conn, announce.base_announce.tcp_relays, +                        GCA_MAX_ANNOUNCED_TCP_RELAYS); + +    if (tcp_num == 0 && !ip_port_is_set) { +        onion_friend_set_gc_data(onion_friend, nullptr, 0); +        return false; +    } + +    announce.base_announce.tcp_relays_count = (uint8_t)tcp_num; +    announce.base_announce.ip_port_is_set = (uint8_t)(ip_port_is_set ? 1 : 0); + +    if (ip_port_is_set) { +        memcpy(&announce.base_announce.ip_port, &chat->self_ip_port, sizeof(IP_Port)); +    } + +    memcpy(announce.base_announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); +    memcpy(announce.chat_public_key, get_chat_id(chat->chat_public_key), ENC_PUBLIC_KEY_SIZE); + +    uint8_t gc_data[GCA_MAX_DATA_LENGTH]; +    const int length = gca_pack_public_announce(m->log, gc_data, GCA_MAX_DATA_LENGTH, &announce); + +    if (length <= 0) { +        onion_friend_set_gc_data(onion_friend, nullptr, 0); +        return false; +    } + +    if (gca_add_announce(m->mono_time, m->group_announce, &announce) == nullptr) { +        onion_friend_set_gc_data(onion_friend, nullptr, 0); +        return false; +    } + +    onion_friend_set_gc_data(onion_friend, gc_data, (uint16_t)length); +    chat->update_self_announces = false; +    chat->last_time_self_announce = mono_time_get(chat->mono_time); + +    if (tcp_num > 0) { +        pk_copy(chat->announced_tcp_relay_pk, announce.base_announce.tcp_relays[0].public_key); +    } else { +        memset(chat->announced_tcp_relay_pk, 0, sizeof(chat->announced_tcp_relay_pk)); +    } + +    LOGGER_DEBUG(chat->log, "Published group announce. TCP relays: %d, UDP status: %d", tcp_num, +                 chat->self_udp_status); +    return true; +} + +non_null() +static void do_gc_onion_friends(const Messenger *m) +{ +    const uint16_t num_friends = onion_get_friend_count(m->onion_c); + +    for (uint16_t i = 0; i < num_friends; ++i) { +        Onion_Friend *onion_friend = onion_get_friend(m->onion_c, i); + +        if (!onion_friend_is_groupchat(onion_friend)) { +            continue; +        } + +        GC_Chat *chat = gc_get_group_by_public_key(m->group_handler, onion_friend_get_gc_public_key(onion_friend)); + +        if (chat == nullptr) { +            continue; +        } + +        if (chat->update_self_announces) { +            self_announce_group(m, chat, onion_friend); +        } +    } +} +#endif  // VANILLA_NACL +  /** @brief The main loop that needs to be run at least 20 times per second. */  void do_messenger(Messenger *m, void *userdata)  { @@ -2406,6 +2647,11 @@ void do_messenger(Messenger *m, void *userdata)      do_onion_client(m->onion_c);      do_friend_connections(m->fr_c, userdata);      do_friends(m, userdata); +#ifndef VANILLA_NACL +    do_gc(m->group_handler, userdata); +    do_gca(m->mono_time, m->group_announce); +    do_gc_onion_friends(m); +#endif      m_connection_status_callback(m, userdata);      if (mono_time_get(m->mono_time) > m->lastdump + DUMPING_CLIENTS_FRIENDS_EVERY_N_SECONDS) { @@ -2886,6 +3132,101 @@ static State_Load_Status friends_list_load(Messenger *m, const uint8_t *data, ui      return STATE_LOAD_STATUS_CONTINUE;  } +#ifndef VANILLA_NACL +non_null() +static void pack_groupchats(const GC_Session *c, Bin_Pack *bp) +{ +    assert(bp != nullptr && c != nullptr); +    bin_pack_array(bp, gc_count_groups(c)); + +    for (uint32_t i = 0; i < c->chats_index; ++i) { // this loop must match the one in gc_count_groups() +        const GC_Chat *chat = &c->chats[i]; + +        if (!gc_group_is_valid(chat)) { +            continue; +        } + +        gc_group_save(chat, bp); +    } +} + +non_null() +static bool pack_groupchats_handler(Bin_Pack *bp, const void *obj) +{ +    pack_groupchats((const GC_Session *)obj, bp); +    return true;  // TODO(iphydf): Return bool from pack functions. +} + +non_null() +static uint32_t saved_groups_size(const Messenger *m) +{ +    GC_Session *c = m->group_handler; +    return bin_pack_obj_size(pack_groupchats_handler, c); +} + +non_null() +static uint8_t *groups_save(const Messenger *m, uint8_t *data) +{ +    const GC_Session *c = m->group_handler; + +    const uint32_t num_groups = gc_count_groups(c); + +    if (num_groups == 0) { +        return data; +    } + +    const uint32_t len = m_plugin_size(m, STATE_TYPE_GROUPS); + +    if (len == 0) { +        return data; +    } + +    data = state_write_section_header(data, STATE_COOKIE_TYPE, len, STATE_TYPE_GROUPS); + +    if (!bin_pack_obj(pack_groupchats_handler, c, data, len)) { +        LOGGER_FATAL(m->log, "failed to pack group chats into buffer of length %u", len); +        return data; +    } + +    data += len; + +    LOGGER_DEBUG(m->log, "Saved %u groups (length %u)", num_groups, len); + +    return data; +} + +non_null() +static State_Load_Status groups_load(Messenger *m, const uint8_t *data, uint32_t length) +{ +    Bin_Unpack *bu = bin_unpack_new(data, length); +    if (bu == nullptr) { +        LOGGER_ERROR(m->log, "failed to allocate binary unpacker"); +        return STATE_LOAD_STATUS_ERROR; +    } + +    uint32_t num_groups; +    if (!bin_unpack_array(bu, &num_groups)) { +        LOGGER_ERROR(m->log, "msgpack failed to unpack groupchats array: expected array"); +        bin_unpack_free(bu); +        return STATE_LOAD_STATUS_ERROR; +    } + +    LOGGER_DEBUG(m->log, "Loading %u groups (length %u)", num_groups, length); + +    for (uint32_t i = 0; i < num_groups; ++i) { +        const int group_number = gc_group_load(m->group_handler, bu); + +        if (group_number < 0) { +            LOGGER_WARNING(m->log, "Failed to load group %u", i); +        } +    } + +    bin_unpack_free(bu); + +    return STATE_LOAD_STATUS_CONTINUE; +} +#endif /* VANILLA_NACL */ +  // name state plugin  non_null()  static uint32_t name_size(const Messenger *m) @@ -3072,6 +3413,9 @@ static void m_register_default_plugins(Messenger *m)      m_register_state_plugin(m, STATE_TYPE_STATUSMESSAGE, status_message_size, load_status_message,                              save_status_message);      m_register_state_plugin(m, STATE_TYPE_STATUS, status_size, load_status, save_status); +#ifndef VANILLA_NACL +    m_register_state_plugin(m, STATE_TYPE_GROUPS, saved_groups_size, groups_load, groups_save); +#endif      m_register_state_plugin(m, STATE_TYPE_TCP_RELAY, tcp_relay_size, load_tcp_relays, save_tcp_relays);      m_register_state_plugin(m, STATE_TYPE_PATH_NODE, path_node_size, load_path_nodes, save_path_nodes);  } @@ -3244,6 +3588,21 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network          return nullptr;      } +#ifndef VANILLA_NACL +    m->group_announce = new_gca_list(); + +    if (m->group_announce == nullptr) { +        kill_net_crypto(m->net_crypto); +        kill_dht(m->dht); +        kill_networking(m->net); +        friendreq_kill(m->fr); +        logger_kill(m->log); +        free(m); +        return nullptr; +    } + +#endif /* VANILLA_NACL */ +      if (options->dht_announcements_enabled) {          m->forwarding = new_forwarding(m->log, m->rng, m->mono_time, m->dht);          m->announce = new_announcements(m->log, m->rng, m->mono_time, m->forwarding); @@ -3259,10 +3618,35 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network      if ((options->dht_announcements_enabled && (m->forwarding == nullptr || m->announce == nullptr)) ||              m->onion == nullptr || m->onion_a == nullptr || m->onion_c == nullptr || m->fr_c == nullptr) { +        kill_onion(m->onion); +        kill_onion_announce(m->onion_a); +        kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL +        kill_gca(m->group_announce); +#endif /* VANILLA_NACL */          kill_friend_connections(m->fr_c); +        kill_announcements(m->announce); +        kill_forwarding(m->forwarding); +        kill_net_crypto(m->net_crypto); +        kill_dht(m->dht); +        kill_networking(m->net); +        friendreq_kill(m->fr); +        logger_kill(m->log); +        free(m); +        return nullptr; +    } + +#ifndef VANILLA_NACL +    gca_onion_init(m->group_announce, m->onion_a); + +    m->group_handler = new_dht_groupchats(m); + +    if (m->group_handler == nullptr) {          kill_onion(m->onion);          kill_onion_announce(m->onion_a);          kill_onion_client(m->onion_c); +        kill_gca(m->group_announce); +        kill_friend_connections(m->fr_c);          kill_announcements(m->announce);          kill_forwarding(m->forwarding);          kill_net_crypto(m->net_crypto); @@ -3274,15 +3658,23 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network          return nullptr;      } +#endif /* VANILLA_NACL */ +      if (options->tcp_server_port != 0) {          m->tcp_server = new_TCP_server(m->log, m->rng, m->ns, options->ipv6enabled, 1, &options->tcp_server_port,                                         dht_get_self_secret_key(m->dht), m->onion, m->forwarding);          if (m->tcp_server == nullptr) { -            kill_friend_connections(m->fr_c);              kill_onion(m->onion);              kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL +            kill_dht_groupchats(m->group_handler); +#endif +            kill_friend_connections(m->fr_c);              kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL +            kill_gca(m->group_announce); +#endif              kill_announcements(m->announce);              kill_forwarding(m->forwarding);              kill_net_crypto(m->net_crypto); @@ -3332,10 +3724,16 @@ void kill_messenger(Messenger *m)          kill_TCP_server(m->tcp_server);      } -    kill_friend_connections(m->fr_c);      kill_onion(m->onion);      kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL +    kill_dht_groupchats(m->group_handler); +#endif +    kill_friend_connections(m->fr_c);      kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL +    kill_gca(m->group_announce); +#endif      kill_announcements(m->announce);      kill_forwarding(m->forwarding);      kill_net_crypto(m->net_crypto); diff --git a/protocols/Tox/libtox/src/toxcore/Messenger.h b/protocols/Tox/libtox/src/toxcore/Messenger.h index 4abd7905d0..8279815277 100644 --- a/protocols/Tox/libtox/src/toxcore/Messenger.h +++ b/protocols/Tox/libtox/src/toxcore/Messenger.h @@ -15,6 +15,8 @@  #include "forwarding.h"  #include "friend_connection.h"  #include "friend_requests.h" +#include "group_announce.h" +#include "group_common.h"  #include "logger.h"  #include "net_crypto.h"  #include "state.h" @@ -35,7 +37,11 @@ typedef enum Message_Type {      MESSAGE_ACTION,  } Message_Type; +// TODO(Jfreegman, Iphy): Remove this before merge +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED  typedef struct Messenger Messenger; +#endif  // MESSENGER_DEFINED  // Returns the size of the data  typedef uint32_t m_state_size_cb(const Messenger *m); @@ -191,6 +197,8 @@ typedef void m_friend_connectionstatuschange_internal_cb(Messenger *m, uint32_t          uint8_t connection_status, void *user_data);  typedef void m_conference_invite_cb(Messenger *m, uint32_t friend_number, const uint8_t *cookie, uint16_t length,                                      void *user_data); +typedef void m_group_invite_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *data, size_t length, +                               const uint8_t *group_name, size_t group_name_length, void *userdata);  typedef void m_msi_packet_cb(Messenger *m, uint32_t friend_number, const uint8_t *data, uint16_t length,                               void *user_data);  typedef int m_lossy_rtp_packet_cb(Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t len, void *object); @@ -269,6 +277,9 @@ struct Messenger {      uint64_t lastdump;      uint8_t is_receiving_file; +    GC_Session *group_handler; +    GC_Announces_List *group_announce; +      bool has_added_relays; // If the first connection has occurred in do_messenger      uint16_t num_loaded_relays; @@ -288,6 +299,8 @@ struct Messenger {      struct Group_Chats *conferences_object; /* Set by new_groupchats()*/      m_conference_invite_cb *conference_invite; +    m_group_invite_cb *group_invite; +      m_file_recv_cb *file_sendrequest;      m_file_recv_control_cb *file_filecontrol;      m_file_recv_chunk_cb *file_filedata; @@ -306,6 +319,14 @@ struct Messenger {  };  /** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +non_null() +bool friend_is_valid(const Messenger *m, int32_t friendnumber); + +/**   * Format: `[real_pk (32 bytes)][nospam number (4 bytes)][checksum (2 bytes)]`   *   * @param[out] address FRIEND_ADDRESS_SIZE byte address to give to others. @@ -348,6 +369,19 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u  non_null()  int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk); +/** @brief Initializes the friend connection and onion connection for a groupchat. + * + * @retval true on success. + */ +non_null() +bool m_create_group_connection(Messenger *m, GC_Chat *chat); + +/* + * Kills the friend connection for a groupchat. + */ +non_null() +void m_kill_group_connection(Messenger *m, const GC_Chat *chat); +  /** @return the friend number associated to that public key.   * @retval -1 if no such friend.   */ @@ -589,6 +623,12 @@ non_null() void m_callback_core_connection(Messenger *m, m_self_connection_statu  non_null(1) nullable(2)  void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function); +/* Set the callback for group invites. + */ +non_null(1) nullable(2) +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function); + +  /** @brief Send a conference invite packet.   *   * return true on success @@ -597,6 +637,17 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function  non_null()  bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, const uint8_t *data, uint16_t length); +/* Send a group invite packet. + * + *  WARNING: Return-value semantics are different than for + *  send_conference_invite_packet(). + * + *  return true on success + */ +non_null() +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length); + +  /*** FILE SENDING */ @@ -678,7 +729,8 @@ int file_seek(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uin   * @retval -7 if wrong position.   */  non_null(1) nullable(5) -int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, const uint8_t *data, uint16_t length); +int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, +                   const uint8_t *data, uint16_t length);  /*** A/V related */ diff --git a/protocols/Tox/libtox/src/toxcore/TCP_common.c b/protocols/Tox/libtox/src/toxcore/TCP_common.c index 349e35fda7..a42faced1a 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_common.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_common.c @@ -278,7 +278,7 @@ int read_packet_TCP_secure_connection(          return -1;      } -    VLA(uint8_t, data_encrypted, *next_packet_length); +    VLA(uint8_t, data_encrypted, (int) *next_packet_length);      const int len_packet = read_TCP_packet(logger, ns, sock, data_encrypted, *next_packet_length, ip_port);      if (len_packet == -1) { @@ -286,7 +286,7 @@ int read_packet_TCP_secure_connection(      }      if (len_packet != *next_packet_length) { -        LOGGER_ERROR(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length); +        LOGGER_WARNING(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length);          return 0;      } diff --git a/protocols/Tox/libtox/src/toxcore/TCP_common.h b/protocols/Tox/libtox/src/toxcore/TCP_common.h index 88c0cb665a..78d1623d8a 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_common.h +++ b/protocols/Tox/libtox/src/toxcore/TCP_common.h @@ -23,6 +23,20 @@ void wipe_priority_list(TCP_Priority_List *p);  #define NUM_RESERVED_PORTS 16  #define NUM_CLIENT_CONNECTIONS (256 - NUM_RESERVED_PORTS) +#ifdef USE_TEST_NETWORK +#define TCP_PACKET_FORWARD_REQUEST 11 +#define TCP_PACKET_FORWARDING 10 +#define TCP_PACKET_ROUTING_REQUEST  9 +#define TCP_PACKET_ROUTING_RESPONSE 8 +#define TCP_PACKET_CONNECTION_NOTIFICATION 7 +#define TCP_PACKET_DISCONNECT_NOTIFICATION 6 +#define TCP_PACKET_PING 5 +#define TCP_PACKET_PONG 4 +#define TCP_PACKET_OOB_SEND 3 +#define TCP_PACKET_OOB_RECV 2 +#define TCP_PACKET_ONION_REQUEST  1 +#define TCP_PACKET_ONION_RESPONSE 0 +#else  #define TCP_PACKET_ROUTING_REQUEST  0  #define TCP_PACKET_ROUTING_RESPONSE 1  #define TCP_PACKET_CONNECTION_NOTIFICATION 2 @@ -35,6 +49,7 @@ void wipe_priority_list(TCP_Priority_List *p);  #define TCP_PACKET_ONION_RESPONSE 9  #define TCP_PACKET_FORWARD_REQUEST 10  #define TCP_PACKET_FORWARDING 11 +#endif  // test network  #define TCP_HANDSHAKE_PLAIN_SIZE (CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE)  #define TCP_SERVER_HANDSHAKE_SIZE (CRYPTO_NONCE_SIZE + TCP_HANDSHAKE_PLAIN_SIZE + CRYPTO_MAC_SIZE) diff --git a/protocols/Tox/libtox/src/toxcore/TCP_connection.c b/protocols/Tox/libtox/src/toxcore/TCP_connection.c index c67df1b2eb..4c71f366b3 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_connection.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_connection.c @@ -277,7 +277,6 @@ static TCP_con *get_tcp_connection(const TCP_Connections *tcp_c, int tcp_connect      return &tcp_c->tcp_connections[tcp_connections_number];  } -/** Returns the number of connected TCP relays */  uint32_t tcp_connected_relays_count(const TCP_Connections *tcp_c)  {      uint32_t count = 0; @@ -635,6 +634,11 @@ static int find_tcp_connection_relay(const TCP_Connections *tcp_c, const uint8_t      return -1;  } +bool tcp_relay_is_valid(const TCP_Connections *tcp_c, const uint8_t *relay_pk) +{ +    return find_tcp_connection_relay(tcp_c, relay_pk) != -1; +} +  /** @brief Create a new TCP connection to public_key.   *   * public_key must be the counterpart to the secret key that the other peer used with `new_tcp_connections()`. diff --git a/protocols/Tox/libtox/src/toxcore/TCP_connection.h b/protocols/Tox/libtox/src/toxcore/TCP_connection.h index 65ead4d87f..0434a73eb0 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_connection.h +++ b/protocols/Tox/libtox/src/toxcore/TCP_connection.h @@ -78,10 +78,14 @@ const uint8_t *tcp_connections_public_key(const TCP_Connections *tcp_c);  non_null()  uint32_t tcp_connections_count(const TCP_Connections *tcp_c); -/** Returns the number of connected TCP relays */ +/** @brief Returns the number of connected TCP relays. */  non_null()  uint32_t tcp_connected_relays_count(const TCP_Connections *tcp_c); +/** @brief Returns true if we know of a valid TCP relay with the passed public key. */ +non_null() +bool tcp_relay_is_valid(const TCP_Connections *tcp_c, const uint8_t *relay_pk); +  /** @brief Send a packet to the TCP connection.   *   * return -1 on failure. diff --git a/protocols/Tox/libtox/src/toxcore/TCP_server.c b/protocols/Tox/libtox/src/toxcore/TCP_server.c index f571a312d2..ecd557d1b2 100644 --- a/protocols/Tox/libtox/src/toxcore/TCP_server.c +++ b/protocols/Tox/libtox/src/toxcore/TCP_server.c @@ -166,7 +166,7 @@ static void free_accepted_connection_array(TCP_Server *tcp_server)      tcp_server->size_accepted_connections = 0;  } -/**  +/**   * @return index corresponding to connection with peer on success   * @retval -1 on failure.   */ @@ -388,7 +388,7 @@ non_null()  static int send_routing_response(const Logger *logger, TCP_Secure_Connection *con, uint8_t rpid,                                   const uint8_t *public_key)  { -    uint8_t data[1 + 1 + CRYPTO_PUBLIC_KEY_SIZE]; +    uint8_t data[2 + CRYPTO_PUBLIC_KEY_SIZE];      data[0] = TCP_PACKET_ROUTING_RESPONSE;      data[1] = rpid;      memcpy(data + 2, public_key, CRYPTO_PUBLIC_KEY_SIZE); @@ -1033,12 +1033,15 @@ TCP_Server *new_TCP_server(const Logger *logger, const Random *rng, const Networ  non_null()  static void do_TCP_accept_new(TCP_Server *tcp_server)  { -    for (uint32_t i = 0; i < tcp_server->num_listening_socks; ++i) { -        Socket sock; +    for (uint32_t sock_idx = 0; sock_idx < tcp_server->num_listening_socks; ++sock_idx) { + +        for (uint32_t connection_idx = 0; connection_idx < MAX_INCOMING_CONNECTIONS; ++connection_idx) { +            const Socket sock = net_accept(tcp_server->ns, tcp_server->socks_listening[sock_idx]); -        do { -            sock = net_accept(tcp_server->ns, tcp_server->socks_listening[i]); -        } while (accept_connection(tcp_server, sock) != -1); +            if (accept_connection(tcp_server, sock) == -1) { +                break; +            } +        }      }  }  #endif diff --git a/protocols/Tox/libtox/src/toxcore/announce.c b/protocols/Tox/libtox/src/toxcore/announce.c index 7914d19615..647bd21ea3 100644 --- a/protocols/Tox/libtox/src/toxcore/announce.c +++ b/protocols/Tox/libtox/src/toxcore/announce.c @@ -14,9 +14,14 @@  #include "LAN_discovery.h"  #include "ccompat.h" +#include "shared_key_cache.h"  #include "timed_auth.h"  #include "util.h" +// Settings for the shared key cache +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 +  uint8_t announce_response_of_request_type(uint8_t request_type)  {      switch (request_type) { @@ -53,7 +58,7 @@ struct Announcements {      const uint8_t *public_key;      const uint8_t *secret_key; -    Shared_Keys shared_keys; +    Shared_Key_Cache *shared_keys;      uint8_t hmac_key[CRYPTO_HMAC_KEY_SIZE];      int32_t synch_offset; @@ -429,10 +434,13 @@ static int create_reply_plain_store_announce_request(Announcements *announce,      }      VLA(uint8_t, plain, plain_len); -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    get_shared_key(announce->mono_time, &announce->shared_keys, shared_key, -                   announce->secret_key, data_public_key); +    const uint8_t* shared_key = shared_key_cache_lookup(announce->shared_keys, data_public_key); + +    if (shared_key == nullptr) { +        /* Error looking up/deriving the shared key */ +        return -1; +    }      if (decrypt_data_symmetric(shared_key,                                 data + CRYPTO_PUBLIC_KEY_SIZE, @@ -549,9 +557,7 @@ static int create_reply(Announcements *announce, const IP_Port *source,      }      VLA(uint8_t, plain, plain_len); -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; - -    dht_get_shared_key_recv(announce->dht, shared_key, data + 1); +    const uint8_t *shared_key = dht_get_shared_key_recv(announce->dht, data + 1);      if (decrypt_data_symmetric(shared_key,                                 data + 1 + CRYPTO_PUBLIC_KEY_SIZE, @@ -652,6 +658,11 @@ Announcements *new_announcements(const Logger *log, const Random *rng, const Mon      announce->public_key = dht_get_self_public_key(announce->dht);      announce->secret_key = dht_get_self_secret_key(announce->dht);      new_hmac_key(announce->rng, announce->hmac_key); +    announce->shared_keys = shared_key_cache_new(mono_time, announce->secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); +    if (announce->shared_keys == nullptr) { +        free(announce); +        return nullptr; +    }      announce->start_time = mono_time_get(announce->mono_time); @@ -677,7 +688,7 @@ void kill_announcements(Announcements *announce)      networking_registerhandler(announce->net, NET_PACKET_STORE_ANNOUNCE_REQUEST, nullptr, nullptr);      crypto_memzero(announce->hmac_key, CRYPTO_HMAC_KEY_SIZE); -    crypto_memzero(&announce->shared_keys, sizeof(Shared_Keys)); +    shared_key_cache_free(announce->shared_keys);      for (uint32_t i = 0; i < ANNOUNCE_BUCKETS * ANNOUNCE_BUCKET_SIZE; ++i) {          if (announce->entries[i].data != nullptr) { diff --git a/protocols/Tox/libtox/src/toxcore/bin_pack.c b/protocols/Tox/libtox/src/toxcore/bin_pack.c index ca8940ee44..3575803aed 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_pack.c +++ b/protocols/Tox/libtox/src/toxcore/bin_pack.c @@ -127,6 +127,11 @@ bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length)      return cmp_write_bin(&bp->ctx, data, length);  } +bool bin_pack_nil(Bin_Pack *bp) +{ +    return cmp_write_nil(&bp->ctx); +} +  bool bin_pack_bin_marker(Bin_Pack *bp, uint32_t size)  {      return cmp_write_bin_marker(&bp->ctx, size); @@ -159,3 +164,4 @@ bool bin_pack_bin_b(Bin_Pack *bp, const uint8_t *data, uint32_t length)  {      return bp->ctx.write(&bp->ctx, data, length) == length;  } + diff --git a/protocols/Tox/libtox/src/toxcore/bin_pack.h b/protocols/Tox/libtox/src/toxcore/bin_pack.h index 542f533b88..51646c088a 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_pack.h +++ b/protocols/Tox/libtox/src/toxcore/bin_pack.h @@ -90,9 +90,10 @@ non_null() bool bin_pack_u16(Bin_Pack *bp, uint16_t val);  non_null() bool bin_pack_u32(Bin_Pack *bp, uint32_t val);  /** @brief Pack a `uint64_t` as MessagePack positive integer. */  non_null() bool bin_pack_u64(Bin_Pack *bp, uint64_t val); +/** @brief Pack an empty array member as a MessagePack nil value. */ +non_null() bool bin_pack_nil(Bin_Pack *bp);  /** @brief Pack a byte array as MessagePack bin. */  non_null() bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length); -  /** @brief Start packing a custom binary representation.   *   * A call to this function must be followed by exactly `size` bytes packed by functions below. diff --git a/protocols/Tox/libtox/src/toxcore/bin_unpack.c b/protocols/Tox/libtox/src/toxcore/bin_unpack.c index e4daec3a22..ff591ca87a 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_unpack.c +++ b/protocols/Tox/libtox/src/toxcore/bin_unpack.c @@ -104,6 +104,11 @@ bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val)      return cmp_read_ulong(&bu->ctx, val);  } +bool bin_unpack_nil(Bin_Unpack *bu) +{ +    return cmp_read_nil(&bu->ctx); +} +  bool bin_unpack_bin(Bin_Unpack *bu, uint8_t **data_ptr, uint32_t *data_length_ptr)  {      uint32_t bin_size; diff --git a/protocols/Tox/libtox/src/toxcore/bin_unpack.h b/protocols/Tox/libtox/src/toxcore/bin_unpack.h index c6ead96e1b..bd4d8785c1 100644 --- a/protocols/Tox/libtox/src/toxcore/bin_unpack.h +++ b/protocols/Tox/libtox/src/toxcore/bin_unpack.h @@ -60,6 +60,9 @@ non_null() bool bin_unpack_u16(Bin_Unpack *bu, uint16_t *val);  non_null() bool bin_unpack_u32(Bin_Unpack *bu, uint32_t *val);  /** @brief Unpack a MessagePack positive int into a `uint64_t`. */  non_null() bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val); +/** @brief Unpack a Messagepack nil value. */ +non_null() bool bin_unpack_nil(Bin_Unpack *bu); +  /** @brief Unpack a MessagePack bin into a newly allocated byte array.   *   * Allocates a new byte array and stores it into `data_ptr` with its length stored in diff --git a/protocols/Tox/libtox/src/toxcore/crypto_core.c b/protocols/Tox/libtox/src/toxcore/crypto_core.c index 1ec2cee5fd..14025252c4 100644 --- a/protocols/Tox/libtox/src/toxcore/crypto_core.c +++ b/protocols/Tox/libtox/src/toxcore/crypto_core.c @@ -176,7 +176,7 @@ bool crypto_memunlock(void *data, size_t length)  #endif  } -bool pk_equal(const uint8_t *pk1, const uint8_t *pk2) +bool pk_equal(const uint8_t pk1[CRYPTO_PUBLIC_KEY_SIZE], const uint8_t pk2[CRYPTO_PUBLIC_KEY_SIZE])  {  #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION      // Hope that this is better for the fuzzer @@ -186,7 +186,7 @@ bool pk_equal(const uint8_t *pk1, const uint8_t *pk2)  #endif  } -void pk_copy(uint8_t *dest, const uint8_t *src) +void pk_copy(uint8_t dest[CRYPTO_PUBLIC_KEY_SIZE], const uint8_t src[CRYPTO_PUBLIC_KEY_SIZE])  {      memcpy(dest, src, CRYPTO_PUBLIC_KEY_SIZE);  } @@ -472,7 +472,7 @@ int32_t crypto_new_keypair(const Random *rng, uint8_t *public_key, uint8_t *secr  #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION      random_bytes(rng, secret_key, CRYPTO_SECRET_KEY_SIZE);      memset(public_key, 0, CRYPTO_PUBLIC_KEY_SIZE);  // Make MSAN happy -    crypto_scalarmult_curve25519_base(public_key, secret_key); +    crypto_derive_public_key(public_key, secret_key);      return 0;  #else      return crypto_box_keypair(public_key, secret_key); @@ -484,7 +484,7 @@ void crypto_derive_public_key(uint8_t *public_key, const uint8_t *secret_key)      crypto_scalarmult_curve25519_base(public_key, secret_key);  } -void new_hmac_key(const Random *rng, uint8_t *key) +void new_hmac_key(const Random *rng, uint8_t key[CRYPTO_HMAC_KEY_SIZE])  {      random_bytes(rng, key, CRYPTO_HMAC_KEY_SIZE);  } @@ -492,13 +492,22 @@ void new_hmac_key(const Random *rng, uint8_t *key)  void crypto_hmac(uint8_t auth[CRYPTO_HMAC_SIZE], const uint8_t key[CRYPTO_HMAC_KEY_SIZE], const uint8_t *data,                   size_t length)  { +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +    memcpy(auth, key, 16); +    memcpy(auth + 16, data, length < 16 ? length : 16); +#else      crypto_auth(auth, data, length, key); +#endif  }  bool crypto_hmac_verify(const uint8_t auth[CRYPTO_HMAC_SIZE], const uint8_t key[CRYPTO_HMAC_KEY_SIZE],                          const uint8_t *data, size_t length)  { +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +    return memcmp(auth, key, 16) == 0 && memcmp(auth + 16, data, length < 16 ? length : 16) == 0; +#else      return crypto_auth_verify(auth, data, length, key) == 0; +#endif  }  void crypto_sha256(uint8_t *hash, const uint8_t *data, size_t length) diff --git a/protocols/Tox/libtox/src/toxcore/crypto_core.h b/protocols/Tox/libtox/src/toxcore/crypto_core.h index 0aaadeacf1..3b9f27d677 100644 --- a/protocols/Tox/libtox/src/toxcore/crypto_core.h +++ b/protocols/Tox/libtox/src/toxcore/crypto_core.h @@ -358,7 +358,7 @@ int32_t decrypt_data(const uint8_t *public_key, const uint8_t *secret_key, const  /**   * @brief Fast encrypt/decrypt operations.   * - * Use if this is not a one-time communication. @ref encrypt_precompute does the + * Use if this is not a one-time communication. `encrypt_precompute` does the   * shared-key generation once so it does not have to be performed on every   * encrypt/decrypt.   */ diff --git a/protocols/Tox/libtox/src/toxcore/friend_connection.c b/protocols/Tox/libtox/src/toxcore/friend_connection.c index 9e5dee7cbd..7b8537c720 100644 --- a/protocols/Tox/libtox/src/toxcore/friend_connection.c +++ b/protocols/Tox/libtox/src/toxcore/friend_connection.c @@ -31,7 +31,7 @@ struct Friend_Conn {      uint8_t real_public_key[CRYPTO_PUBLIC_KEY_SIZE];      uint8_t dht_temp_pk[CRYPTO_PUBLIC_KEY_SIZE]; -    uint16_t dht_lock; +    uint32_t dht_lock_token;      IP_Port dht_ip_port;      uint64_t dht_pk_lastrecv;      uint64_t dht_ip_port_lastrecv; @@ -197,7 +197,7 @@ Friend_Conn *get_conn(const Friend_Connections *fr_c, int friendcon_id)      return &fr_c->conns[friendcon_id];  } -/**  +/**   * @return friendcon_id corresponding to the real public key on success.   * @retval -1 on failure.   */ @@ -377,16 +377,15 @@ static void change_dht_pk(Friend_Connections *fr_c, int friendcon_id, const uint      friend_con->dht_pk_lastrecv = mono_time_get(fr_c->mono_time); -    if (friend_con->dht_lock > 0) { -        if (dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock) != 0) { +    if (friend_con->dht_lock_token > 0) { +        if (dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token) != 0) {              LOGGER_ERROR(fr_c->logger, "a. Could not delete dht peer. Please report this.");              return;          } - -        friend_con->dht_lock = 0; +        friend_con->dht_lock_token = 0;      } -    dht_addfriend(fr_c->dht, dht_public_key, dht_ip_callback, fr_c, friendcon_id, &friend_con->dht_lock); +    dht_addfriend(fr_c->dht, dht_public_key, dht_ip_callback, fr_c, friendcon_id, &friend_con->dht_lock_token);      memcpy(friend_con->dht_temp_pk, dht_public_key, CRYPTO_PUBLIC_KEY_SIZE);  } @@ -609,7 +608,7 @@ static int friend_new_connection(Friend_Connections *fr_c, int friendcon_id)      }      /* If dht_temp_pk does not contains a pk. */ -    if (friend_con->dht_lock == 0) { +    if (friend_con->dht_lock_token == 0) {          return -1;      } @@ -838,8 +837,9 @@ int kill_friend_connection(Friend_Connections *fr_c, int friendcon_id)      onion_delfriend(fr_c->onion_c, friend_con->onion_friendnum);      crypto_kill(fr_c->net_crypto, friend_con->crypt_connection_id); -    if (friend_con->dht_lock > 0) { -        dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock); +    if (friend_con->dht_lock_token > 0) { +        dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token); +        friend_con->dht_lock_token = 0;      }      return wipe_friend_conn(fr_c, friendcon_id); @@ -882,7 +882,8 @@ int send_friend_request_packet(Friend_Connections *fr_c, int friendcon_id, uint3      if (friend_con->status == FRIENDCONN_STATUS_CONNECTED) {          packet[0] = PACKET_ID_FRIEND_REQUESTS; -        return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), false) != -1 ? 1 : 0; +        return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), +                                 false) != -1 ? 1 : 0;      }      packet[0] = CRYPTO_PACKET_FRIEND_REQ; @@ -967,9 +968,9 @@ void do_friend_connections(Friend_Connections *fr_c, void *userdata)          if (friend_con != nullptr) {              if (friend_con->status == FRIENDCONN_STATUS_CONNECTING) {                  if (friend_con->dht_pk_lastrecv + FRIEND_DHT_TIMEOUT < temp_time) { -                    if (friend_con->dht_lock > 0) { -                        dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock); -                        friend_con->dht_lock = 0; +                    if (friend_con->dht_lock_token > 0) { +                        dht_delfriend(fr_c->dht, friend_con->dht_temp_pk, friend_con->dht_lock_token); +                        friend_con->dht_lock_token = 0;                          memset(friend_con->dht_temp_pk, 0, CRYPTO_PUBLIC_KEY_SIZE);                      }                  } @@ -978,7 +979,7 @@ void do_friend_connections(Friend_Connections *fr_c, void *userdata)                      friend_con->dht_ip_port.ip.family = net_family_unspec();                  } -                if (friend_con->dht_lock > 0) { +                if (friend_con->dht_lock_token > 0) {                      if (friend_new_connection(fr_c, i) == 0) {                          set_direct_ip_port(fr_c->net_crypto, friend_con->crypt_connection_id, &friend_con->dht_ip_port, false);                          connect_to_saved_tcp_relays(fr_c, i, MAX_FRIEND_TCP_CONNECTIONS / 2); /* Only fill it half up. */ diff --git a/protocols/Tox/libtox/src/toxcore/group_announce.c b/protocols/Tox/libtox/src/toxcore/group_announce.c index 83dd3acf54..896b043dbd 100644 --- a/protocols/Tox/libtox/src/toxcore/group_announce.c +++ b/protocols/Tox/libtox/src/toxcore/group_announce.c @@ -186,7 +186,7 @@ static int gca_unpack_announce(const Logger *log, const uint8_t *data, uint16_t      memcpy(announce->peer_public_key, data + offset, ENC_PUBLIC_KEY_SIZE);      offset += ENC_PUBLIC_KEY_SIZE; -    announce->ip_port_is_set = data[offset] == 1; +    net_unpack_bool(&data[offset], &announce->ip_port_is_set);      ++offset;      announce->tcp_relays_count = data[offset]; diff --git a/protocols/Tox/libtox/src/toxcore/group_announce.h b/protocols/Tox/libtox/src/toxcore/group_announce.h index 11bba7d530..801363d6b2 100644 --- a/protocols/Tox/libtox/src/toxcore/group_announce.h +++ b/protocols/Tox/libtox/src/toxcore/group_announce.h @@ -20,7 +20,7 @@ extern "C" {  /* The maximum number of announces to save for a particular group chat. */  #define GCA_MAX_SAVED_ANNOUNCES_PER_GC 16 -/* Maximum number of TCP relays that can be in an annoucne. */ +/* Maximum number of TCP relays that can be in an announce. */  #define GCA_MAX_ANNOUNCED_TCP_RELAYS 1  /* Maximum number of announces we can send in an announce response. */ diff --git a/protocols/Tox/libtox/src/toxcore/group_chats.c b/protocols/Tox/libtox/src/toxcore/group_chats.c new file mode 100644 index 0000000000..f9cb2d18fb --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_chats.c @@ -0,0 +1,8410 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_chats.h" + +#include <assert.h> + +#ifndef VANILLA_NACL +#include <sodium.h> +#endif + +#include <string.h> + +#include "DHT.h" +#include "LAN_discovery.h" +#include "Messenger.h" +#include "TCP_connection.h" +#include "ccompat.h" +#include "friend_connection.h" +#include "group_common.h" +#include "group_moderation.h" +#include "group_pack.h" +#include "mono_time.h" +#include "network.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/* The minimum size of a plaintext group handshake packet */ +#define GC_MIN_HS_PACKET_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) + +/* The minimum size of an encrypted group handshake packet. */ +#define GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE +\ +                                          GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) + +/* Size of a group's shared state in packed format */ +#define GC_PACKED_SHARED_STATE_SIZE (EXT_PUBLIC_KEY_SIZE + sizeof(uint16_t) + MAX_GC_GROUP_NAME_SIZE +\ +                                     sizeof(uint16_t) + 1 + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE +\ +                                     MOD_MODERATION_HASH_SIZE + sizeof(uint32_t) + sizeof(uint32_t) + 1) + +/* Minimum size of a topic packet; includes topic length, public signature key, topic version and checksum */ +#define GC_MIN_PACKED_TOPIC_INFO_SIZE (sizeof(uint16_t) + SIG_PUBLIC_KEY_SIZE + sizeof(uint32_t) + sizeof(uint16_t)) + +#define GC_SHARED_STATE_ENC_PACKET_SIZE (SIGNATURE_SIZE + GC_PACKED_SHARED_STATE_SIZE) + +/* Header information attached to all broadcast messages: broadcast_type */ +#define GC_BROADCAST_ENC_HEADER_SIZE 1 + +/* Size of a group packet message ID */ +#define GC_MESSAGE_ID_BYTES sizeof(uint64_t) + +/* Size of a lossless ack packet */ +#define GC_LOSSLESS_ACK_PACKET_SIZE (GC_MESSAGE_ID_BYTES + 1) + +/* Smallest possible size of an encrypted lossless payload. + * + * Data includes the message_id, group packet type, and the nonce and MAC for decryption. + */ +#define GC_MIN_LOSSLESS_PAYLOAD_SIZE (GC_MESSAGE_ID_BYTES + CRYPTO_NONCE_SIZE + 1 + CRYPTO_MAC_SIZE) + +/* Smallest possible size of a lossy group packet */ +#define GC_MIN_LOSSY_PAYLOAD_SIZE (GC_MIN_LOSSLESS_PAYLOAD_SIZE - GC_MESSAGE_ID_BYTES) + +/* Maximum number of bytes to pad packets with. + * + * Packets are padded with a random number of zero bytes between zero and this value in order to hide + * the true length of the message, which reduces the amount of metadata leaked through packet analysis. + * + * Note: This behaviour was copied from the toxcore encryption implementation in net_crypto.c. + */ +#define GC_MAX_PACKET_PADDING 8 + +/* Minimum size of a ping packet, which contains the peer count, peer list checksum, shared state version, + * sanctions list version, sanctions list checksum, topic version, and topic checksum + */ +#define GC_PING_PACKET_MIN_DATA_SIZE ((sizeof(uint16_t) * 4) + (sizeof(uint32_t) * 3)) + +/* How often in seconds we can send a group sync request packet */ +#define GC_SYNC_REQUEST_LIMIT (GC_PING_TIMEOUT + 1) + +/* How often in seconds we can send the peer list to any peer in the group in a sync response */ +#define GC_SYNC_RESPONSE_PEER_LIST_LIMIT 3 + +/* How often in seconds we try to handshake with an unconfirmed peer */ +#define GC_SEND_HANDSHAKE_INTERVAL 3 + +/* How often in seconds we rotate session encryption keys with a peer */ +#define GC_KEY_ROTATION_TIMEOUT (5 * 60) + +/* How often in seconds we try to reconnect to peers that recently timed out */ +#define GC_TIMED_OUT_RECONN_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT * 3) + +/* How long in seconds before we stop trying to reconnect with a timed out peer */ +#define GC_TIMED_OUT_STALE_TIMEOUT (60 * 15) + +/* The value the topic lock is set to when the topic lock is enabled. */ +#define GC_TOPIC_LOCK_ENABLED 0 + +static_assert(GCC_BUFFER_SIZE <= UINT16_MAX, +              "GCC_BUFFER_SIZE must be <= UINT16_MAX)"); + +static_assert(MAX_GC_PACKET_CHUNK_SIZE < MAX_GC_PACKET_SIZE, +              "MAX_GC_PACKET_CHUNK_SIZE must be < MAX_GC_PACKET_SIZE"); + +// size of a lossless handshake packet - lossless packets can't/shouldn't be split up +static_assert(MAX_GC_PACKET_CHUNK_SIZE >= 171, +              "MAX_GC_PACKET_CHUNK_SIZE must be >= 171"); + +// group_moderation constants assume this is the max packet size. +static_assert(MAX_GC_PACKET_SIZE >= 50000, +              "MAX_GC_PACKET_SIZE doesn't match constants in group_moderation.h"); + +static_assert(MAX_GC_PACKET_SIZE <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE, +              "MAX_GC_PACKET_SIZE must be <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE"); + +/** Types of broadcast messages. */ +typedef enum Group_Message_Type { +    GC_MESSAGE_TYPE_NORMAL = 0x00, +    GC_MESSAGE_TYPE_ACTION = 0x01, +} Group_Message_Type; + +/** Types of handshake request packets. */ +typedef enum Group_Handshake_Packet_Type { +    GH_REQUEST  = 0x00,  // Requests a handshake +    GH_RESPONSE = 0x01,  // Responds to a handshake request +} Group_Handshake_Packet_Type; + +/** Types of handshake requests (within a handshake request packet). */ +typedef enum Group_Handshake_Request_Type { +    HS_INVITE_REQUEST     = 0x00,   // Requests an invite to the group +    HS_PEER_INFO_EXCHANGE = 0x01,   // Requests a peer info exchange +} Group_Handshake_Request_Type; + +/** These bitmasks determine what group state info a peer is requesting in a sync request */ +typedef enum Group_Sync_Flags { +    GF_PEERS      = (1 << 0), // 1 +    GF_TOPIC      = (1 << 1), // 2 +    GF_STATE      = (1 << 2), // 4 +} Group_Sync_Flags; + +non_null() static bool self_gc_is_founder(const GC_Chat *chat); +non_null() static bool group_number_valid(const GC_Session *c, int group_number); +non_null() static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number); +non_null() static void group_delete(GC_Session *c, GC_Chat *chat); +non_null() static void group_cleanup(GC_Session *c, GC_Chat *chat); +non_null() static bool group_exists(const GC_Session *c, const uint8_t *chat_id); +non_null() static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat); +non_null(1, 2) nullable(4) +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata); +non_null() static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, +        uint8_t *secret_key); +non_null() static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs); +non_null() static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer); + +static const GC_Chat empty_gc_chat = {nullptr}; + +non_null() +static void kill_group_friend_connection(const GC_Session *c, const GC_Chat *chat) +{ +    if (chat->friend_connection_id != -1) { +        m_kill_group_connection(c->messenger, chat); +    } +} + +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type) +{ +    assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + +    const uint16_t min_header_size = packet_type == NET_PACKET_GC_LOSSY +                                     ? GC_MIN_LOSSY_PAYLOAD_SIZE +                                     : GC_MIN_LOSSLESS_PAYLOAD_SIZE; +    const uint16_t header_size = ENC_PUBLIC_KEY_SIZE + GC_MAX_PACKET_PADDING + min_header_size; + +    assert(length <= UINT16_MAX - header_size); + +    return length + header_size; +} + +/** Return true if `peer_number` is our own. */ +static bool peer_number_is_self(int peer_number) +{ +    return peer_number == 0; +} + +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number) +{ +    return peer_number >= 0 && peer_number < (int)chat->numpeers; +} + +non_null() +static GC_Peer *get_gc_peer(const GC_Chat *chat, int peer_number) +{ +    if (!gc_peer_number_is_valid(chat, peer_number)) { +        return nullptr; +    } + +    return &chat->group[peer_number]; +} + +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number) +{ +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return nullptr; +    } + +    return &peer->gconn; +} + +/** Returns the amount of empty padding a packet of designated length should have. */ +static uint16_t group_packet_padding_length(uint16_t length) +{ +    return (MAX_GC_PACKET_CHUNK_SIZE - length) % GC_MAX_PACKET_PADDING; +} + +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick) +{ +    if (nick != nullptr) { +        const GC_Peer *peer = get_gc_peer(chat, 0); +        assert(peer != nullptr); +        assert(peer->nick_length > 0); + +        memcpy(nick, peer->nick, peer->nick_length); +    } +} + +uint16_t gc_get_self_nick_size(const GC_Chat *chat) +{ +    const GC_Peer *peer = get_gc_peer(chat, 0); +    assert(peer != nullptr); + +    return peer->nick_length; +} + +/** @brief Sets self nick to `nick`. + * + * Returns false if `nick` is null or `length` is greater than MAX_GC_NICK_SIZE. + */ +non_null() +static bool self_gc_set_nick(const GC_Chat *chat, const uint8_t *nick, uint16_t length) +{ +    if (nick == nullptr || length > MAX_GC_NICK_SIZE) { +        return false; +    } + +    GC_Peer *peer = get_gc_peer(chat, 0); +    assert(peer != nullptr); + +    memcpy(peer->nick, nick, length); +    peer->nick_length = length; + +    return true; +} + +Group_Role gc_get_self_role(const GC_Chat *chat) +{ + +    const GC_Peer *peer = get_gc_peer(chat, 0); +    assert(peer != nullptr); + +    return peer->role; +} + +/** Sets self role. If role is invalid this function has no effect. */ +non_null() +static void self_gc_set_role(const GC_Chat *chat, Group_Role role) +{ +    if (role <= GR_OBSERVER) { +        GC_Peer *peer = get_gc_peer(chat, 0); +        assert(peer != nullptr); + +        peer->role = role; +    } +} + +uint8_t gc_get_self_status(const GC_Chat *chat) +{ +    const GC_Peer *peer = get_gc_peer(chat, 0); +    assert(peer != nullptr); + +    return peer->status; +} + +/** Sets self status. If status is invalid this function has no effect. */ +non_null() +static void self_gc_set_status(const GC_Chat *chat, Group_Peer_Status status) +{ +    if (status == GS_NONE || status == GS_AWAY || status == GS_BUSY) { +        GC_Peer *peer = get_gc_peer(chat, 0); +        assert(peer != nullptr); +        peer->status = status; +        return; +    } + +    LOGGER_WARNING(chat->log, "Attempting to set user status with invalid status: %u", (uint8_t)status); +} + +uint32_t gc_get_self_peer_id(const GC_Chat *chat) +{ +    const GC_Peer *peer = get_gc_peer(chat, 0); +    assert(peer != nullptr); + +    return peer->peer_id; +} + +/** Sets self confirmed status. */ +non_null() +static void self_gc_set_confirmed(const GC_Chat *chat, bool confirmed) +{ +    GC_Connection *gconn = get_gc_connection(chat, 0); +    assert(gconn != nullptr); + +    gconn->confirmed = confirmed; +} + +/** Returns true if self has the founder role */ +non_null() +static bool self_gc_is_founder(const GC_Chat *chat) +{ +    return gc_get_self_role(chat) == GR_FOUNDER; +} + +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key) +{ +    if (public_key != nullptr) { +        memcpy(public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); +    } +} + +/** @brief Sets self extended public key to `ext_public_key`. + * + * If `ext_public_key` is null this function has no effect. + */ +non_null() +static void self_gc_set_ext_public_key(const GC_Chat *chat, const uint8_t *ext_public_key) +{ +    if (ext_public_key != nullptr) { +        GC_Connection *gconn = get_gc_connection(chat, 0); +        assert(gconn != nullptr); +        memcpy(gconn->addr.public_key, ext_public_key, EXT_PUBLIC_KEY_SIZE); +    } +} + +/** + * Return true if `peer` has permission to speak according to the `voice_state`. + */ +non_null() +static bool peer_has_voice(const GC_Peer *peer, Group_Voice_State voice_state) +{ +    const Group_Role role = peer->role; + +    switch (voice_state) { +        case GV_ALL: +            return role <= GR_USER; + +        case GV_MODS: +            return role <= GR_MODERATOR; + +        case GV_FOUNDER: +            return role == GR_FOUNDER; + +        default: +            return false; +    } +} + +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed) +{ +    uint16_t packed_len = 0; +    uint16_t count = 0; + +    for (uint32_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { +        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + +        if (!saved_peer_is_valid(saved_peer)) { +            continue; +        } + +        int packed_ipp_len = 0; +        int packed_tcp_len = 0; + +        if (ipport_isset(&saved_peer->ip_port)) { +            if (packed_len > length) { +                return -1; +            } + +            packed_ipp_len = pack_ip_port(chat->log, data + packed_len, length - packed_len, &saved_peer->ip_port); + +            if (packed_ipp_len > 0) { +                packed_len += packed_ipp_len; +            } +        } + +        if (ipport_isset(&saved_peer->tcp_relay.ip_port)) { +            if (packed_len > length) { +                return -1; +            } + +            packed_tcp_len = pack_nodes(chat->log, data + packed_len, length - packed_len, &saved_peer->tcp_relay, 1); + +            if (packed_tcp_len > 0) { +                packed_len += packed_tcp_len; +            } +        } + +        if (packed_len + ENC_PUBLIC_KEY_SIZE > length) { +            return -1; +        } + +        if (packed_tcp_len > 0 || packed_ipp_len > 0) { +            memcpy(data + packed_len, chat->saved_peers[i].public_key, ENC_PUBLIC_KEY_SIZE); +            packed_len += ENC_PUBLIC_KEY_SIZE; +            ++count; +        } else { +            LOGGER_WARNING(chat->log, "Failed to pack saved peer"); +        } +    } + +    if (processed != nullptr) { +        *processed = packed_len; +    } + +    return count; +} + +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length) +{ +    uint16_t count = 0; +    uint16_t unpacked_len = 0; + +    for (size_t i = 0; unpacked_len < length; ++i) { +        GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + +        const int ipp_len = unpack_ip_port(&saved_peer->ip_port, data + unpacked_len, length - unpacked_len, false); + +        if (ipp_len > 0) { +            unpacked_len += ipp_len; +        } + +        if (unpacked_len > length) { +            return -1; +        } + +        uint16_t tcp_len_processed = 0; +        const int tcp_len = unpack_nodes(&saved_peer->tcp_relay, 1, &tcp_len_processed, data + unpacked_len, +                                         length - unpacked_len, true); + +        if (tcp_len == 1 && tcp_len_processed > 0) { +            unpacked_len += tcp_len_processed; +        } else if (ipp_len <= 0) { +            LOGGER_WARNING(chat->log, "Failed to unpack saved peer: Invalid connection info."); +            return -1; +        } + +        if (unpacked_len + ENC_PUBLIC_KEY_SIZE > length) { +            return -1; +        } + +        if (tcp_len > 0 || ipp_len > 0) { +            memcpy(saved_peer->public_key, data + unpacked_len, ENC_PUBLIC_KEY_SIZE); +            unpacked_len += ENC_PUBLIC_KEY_SIZE; +            ++count; +        } else { +            LOGGER_ERROR(chat->log, "Unpacked peer with bad connection info"); +            return -1; +        } +    } + +    return count; +} + +/** Returns true if chat privacy state is set to public. */ +non_null() +static bool is_public_chat(const GC_Chat *chat) +{ +    return chat->shared_state.privacy_state == GI_PUBLIC; +} + +/** Returns true if group is password protected */ +non_null() +static bool chat_is_password_protected(const GC_Chat *chat) +{ +    return chat->shared_state.password_length > 0; +} + +/** Returns true if `password` matches the current group password. */ +non_null() +static bool validate_password(const GC_Chat *chat, const uint8_t *password, uint16_t length) +{ +    if (length > MAX_GC_PASSWORD_SIZE) { +        return false; +    } + +    if (length != chat->shared_state.password_length) { +        return false; +    } + +    return memcmp(chat->shared_state.password, password, length) == 0; +} + +/** @brief Returns the chat object that contains a peer with a public key equal to `id`. + * + * `id` must be at least ENC_PUBLIC_KEY_SIZE bytes in length. + */ +non_null() +static GC_Chat *get_chat_by_id(const GC_Session *c, const uint8_t *id) +{ +    if (c == nullptr) { +        return nullptr; +    } + +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        GC_Chat *chat = &c->chats[i]; + +        if (chat->connection_state == CS_NONE) { +            continue; +        } + +        if (memcmp(id, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { +            return chat; +        } + +        if (get_peer_number_of_enc_pk(chat, id, false) != -1) { +            return chat; +        } +    } + +    return nullptr; +} + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key) +{ +    return jenkins_one_at_a_time_hash(public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** @brief Sets the sum of the public_key_hash of all confirmed peers. + * + * Must be called every time a peer is confirmed or deleted. + */ +non_null() +static void set_gc_peerlist_checksum(GC_Chat *chat) +{ +    uint16_t sum = 0; + +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (gconn->confirmed) { +            sum += gconn->public_key_hash; +        } +    } + +    chat->peers_checksum = sum; +} + +/** Returns a checksum of the topic currently set in `topic_info`. */ +non_null() +static uint16_t get_gc_topic_checksum(const GC_TopicInfo *topic_info) +{ +    return data_checksum(topic_info->topic, topic_info->length); +} + +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed) +{ +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (gconn->pending_delete) { +            continue; +        } + +        if (confirmed && !gconn->confirmed) { +            continue; +        } + +        if (memcmp(gconn->addr.public_key, public_enc_key, ENC_PUBLIC_KEY_SIZE) == 0) { +            return i; +        } +    } + +    return -1; +} + +/** @brief Check if peer associated with `public_sig_key` is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + */ +non_null() +static int get_peer_number_of_sig_pk(const GC_Chat *chat, const uint8_t *public_sig_key) +{ +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (memcmp(get_sig_pk(gconn->addr.public_key), public_sig_key, SIG_PUBLIC_KEY_SIZE) == 0) { +            return i; +        } +    } + +    return -1; +} + +non_null() +static bool gc_get_enc_pk_from_sig_pk(const GC_Chat *chat, uint8_t *public_key, const uint8_t *public_sig_key) +{ +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        const uint8_t *full_pk = gconn->addr.public_key; + +        if (memcmp(public_sig_key, get_sig_pk(full_pk), SIG_PUBLIC_KEY_SIZE) == 0) { +            memcpy(public_key, get_enc_key(full_pk), ENC_PUBLIC_KEY_SIZE); +            return true; +        } +    } + +    return false; +} + +non_null() +static GC_Connection *random_gc_connection(const GC_Chat *chat) +{ +    if (chat->numpeers <= 1) { +        return nullptr; +    } + +    const uint32_t base = random_range_u32(chat->rng, chat->numpeers - 1); + +    for (uint32_t i = 0; i < chat->numpeers - 1; ++i) { +        const uint32_t index = 1 + (base + i) % (chat->numpeers - 1); +        GC_Connection *rand_gconn = get_gc_connection(chat, index); + +        if (rand_gconn == nullptr) { +            return nullptr; +        } + +        if (!rand_gconn->pending_delete && rand_gconn->confirmed) { +            return rand_gconn; +        } +    } + +    return nullptr; +} + +/** @brief Returns the peer number associated with peer_id. + * Returns -1 if peer_id is invalid. + */ +non_null() +static int get_peer_number_of_peer_id(const GC_Chat *chat, uint32_t peer_id) +{ +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        if (chat->group[i].peer_id == peer_id) { +            return i; +        } +    } + +    return -1; +} + +/** @brief Returns a unique peer ID. + * Returns UINT32_MAX if all possible peer ID's are taken. + * + * These ID's are permanently assigned to a peer when they join the group and should be + * considered arbitrary values. + */ +non_null() +static uint32_t get_new_peer_id(const GC_Chat *chat) +{ +    for (uint32_t i = 0; i < UINT32_MAX - 1; ++i) { +        if (get_peer_number_of_peer_id(chat, i) == -1) { +            return i; +        } +    } + +    return UINT32_MAX; +} + +/** @brief Sets the password for the group (locally only). + * + * Return true on success. + */ +non_null(1) nullable(2) +static bool set_gc_password_local(GC_Chat *chat, const uint8_t *passwd, uint16_t length) +{ +    if (length > MAX_GC_PASSWORD_SIZE) { +        return false; +    } + +    if (passwd == nullptr || length == 0) { +        chat->shared_state.password_length = 0; +        memset(chat->shared_state.password, 0, MAX_GC_PASSWORD_SIZE); +    } else { +        chat->shared_state.password_length = length; +        crypto_memlock(chat->shared_state.password, sizeof(chat->shared_state.password)); +        memcpy(chat->shared_state.password, passwd, length); +    } + +    return true; +} + +/** @brief Sets the local shared state to `version`. + * + * This should always be called instead of setting the variables manually. + */ +non_null() +static void set_gc_shared_state_version(GC_Chat *chat, uint32_t version) +{ +    chat->shared_state.version = version; +    chat->moderation.shared_state_version = version; +} + +/** @brief Expands the chat_id into the extended chat public key (encryption key + signature key). + * + * @param dest must have room for EXT_PUBLIC_KEY_SIZE bytes. + * + * Return true on success. + */ +non_null() +static bool expand_chat_id(uint8_t *dest, const uint8_t *chat_id) +{ +    assert(dest != nullptr); + +    const int ret = crypto_sign_ed25519_pk_to_curve25519(dest, chat_id); +    memcpy(dest + ENC_PUBLIC_KEY_SIZE, chat_id, SIG_PUBLIC_KEY_SIZE); + +    return ret != -1; +} + +/** Copies peer connect info from `gconn` to `addr`. */ +non_null() +static void copy_gc_saved_peer(const Random *rng, const GC_Connection *gconn, GC_SavedPeerInfo *addr) +{ +    if (!gcc_copy_tcp_relay(rng, &addr->tcp_relay, gconn)) { +        addr->tcp_relay = (Node_format) { +            0 +        }; +    } + +    addr->ip_port = gconn->addr.ip_port; +    memcpy(addr->public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** Return true if `saved_peer` has either a valid IP_Port or a valid TCP relay. */ +non_null() +static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer) +{ +    return ipport_isset(&saved_peer->ip_port) || ipport_isset(&saved_peer->tcp_relay.ip_port); +} + +/** @brief Returns the index of the saved peers entry for `public_key`. + * Returns -1 if key is not found. + */ +non_null() +static int saved_peer_index(const GC_Chat *chat, const uint8_t *public_key) +{ +    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { +        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + +        if (memcmp(saved_peer->public_key, public_key, ENC_PUBLIC_KEY_SIZE) == 0) { +            return i; +        } +    } + +    return -1; +} + +/** @brief Returns the index of the first vacant entry in saved peers list. + * + * If `public_key` is non-null and already exists in the list, its index will be returned. + * + * A vacant entry is an entry that does not have either an IP_port or tcp relay set (invalid), + * or an entry containing info on a peer that is not presently online (offline). + * + * Invalid entries are given priority over offline entries. + * + * Returns -1 if there are no vacant indices. + */ +non_null(1) nullable(2) +static int saved_peers_get_new_index(const GC_Chat *chat, const uint8_t *public_key) +{ +    if (public_key != nullptr) { +        const int idx = saved_peer_index(chat, public_key); + +        if (idx != -1) { +            return idx; +        } +    } + +    // first check for invalid spots +    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { +        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + +        if (!saved_peer_is_valid(saved_peer)) { +            return i; +        } +    } + +    // now look for entries with offline peers +    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { +        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + +        const int peernumber = get_peer_number_of_enc_pk(chat, saved_peer->public_key, true); + +        if (peernumber < 0) { +            return i; +        } +    } + +    return -1; +} + +/** @brief Attempts to add `gconn` to the saved peer list. + * + * If an entry already exists it will be updated. + * + * Older peers will only be overwritten if the peer is no longer + * present in the chat. This gives priority to more stable connections. + * + * This function should be called every time a new peer joins the group. + */ +non_null() +static void add_gc_saved_peers(GC_Chat *chat, const GC_Connection *gconn) +{ +    const int idx = saved_peers_get_new_index(chat, gconn->addr.public_key); + +    if (idx == -1) { +        return; +    } + +    GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; +    copy_gc_saved_peer(chat->rng, gconn, saved_peer); +} + +/** @brief Finds the first vacant spot in the saved peers list and fills it with a present + * peer who isn't already in the list. + * + * This function should be called after a confirmed peer exits the group. + */ +non_null() +static void refresh_gc_saved_peers(GC_Chat *chat) +{ +    const int idx = saved_peers_get_new_index(chat, nullptr); + +    if (idx == -1) { +        return; +    } + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        if (gconn == nullptr) { +            continue; +        } + +        if (!gconn->confirmed) { +            continue; +        } + +        if (saved_peer_index(chat, gconn->addr.public_key) == -1) { +            GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; +            copy_gc_saved_peer(chat->rng, gconn, saved_peer); +            return; +        } +    } +} + +/** Returns the number of confirmed peers in peerlist. */ +non_null() +static uint16_t get_gc_confirmed_numpeers(const GC_Chat *chat) +{ +    uint16_t count = 0; + +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (gconn->confirmed) { +            ++count; +        } +    } + +    return count; +} + +non_null() static bool sign_gc_shared_state(GC_Chat *chat); +non_null() static bool broadcast_gc_mod_list(const GC_Chat *chat); +non_null() static bool broadcast_gc_shared_state(const GC_Chat *chat); +non_null() static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, +        const uint8_t *sanction_data, uint16_t length, bool add_obs); + +/** Returns true if peer designated by `peer_number` is in the sanctions list as an observer. */ +non_null() +static bool peer_is_observer(const GC_Chat *chat, uint32_t peer_number) +{ +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    return sanctions_list_is_observer(&chat->moderation, get_enc_key(gconn->addr.public_key)); +} + +/** Returns true if peer designated by `peer_number` is the group founder. */ +non_null() +static bool peer_is_founder(const GC_Chat *chat, uint32_t peer_number) +{ + +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    return memcmp(chat->shared_state.founder_public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE) == 0; +} + +/** Returns true if peer designated by `peer_number` is in the moderator list or is the founder. */ +non_null() +static bool peer_is_moderator(const GC_Chat *chat, uint32_t peer_number) +{ +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    if (peer_is_founder(chat, peer_number)) { +        return false; +    } + +    return mod_list_verify_sig_pk(&chat->moderation, get_sig_pk(gconn->addr.public_key)); +} + +/** @brief Iterates through the peerlist and updates group roles according to the + * current group state. + * + * Also updates the roles checksum. If any role conflicts exist the shared state + * version is set to zero in order to force a sync update. + * + * This should be called every time the moderator list or sanctions list changes, + * and after a new peer is marked as confirmed. + */ +non_null() +static void update_gc_peer_roles(GC_Chat *chat) +{ +    chat->roles_checksum = 0; +    bool conflicts = false; + +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        if (gconn == nullptr) { +            continue; +        } + +        if (!gconn->confirmed) { +            continue; +        } + +        const uint8_t first_byte = gconn->addr.public_key[0]; +        const bool is_founder = peer_is_founder(chat, i); + +        if (is_founder) { +            chat->group[i].role = GR_FOUNDER; +            chat->roles_checksum += GR_FOUNDER + first_byte; +            continue; +        } + +        const bool is_observer  = peer_is_observer(chat, i); +        const bool is_moderator = peer_is_moderator(chat, i); +        const bool is_user = !(is_founder || is_moderator || is_observer); + +        if (is_observer && is_moderator) { +            conflicts = true; +        } + +        if (is_user) { +            chat->group[i].role = GR_USER; +            chat->roles_checksum += GR_USER + first_byte; +            continue; +        } + +        if (is_moderator) { +            chat->group[i].role = GR_MODERATOR; +            chat->roles_checksum += GR_MODERATOR + first_byte; +            continue; +        } + +        if (is_observer) { +            chat->group[i].role = GR_OBSERVER; +            chat->roles_checksum += GR_OBSERVER + first_byte; +            continue; +        } +    } + +    if (conflicts && !self_gc_is_founder(chat)) { +        set_gc_shared_state_version(chat, 0);  // need a new shared state +    } +} + +/** @brief Removes the first found offline mod from the mod list. + * + * Broadcasts the shared state and moderator list on success, as well as the updated + * sanctions list if necessary. + * + * TODO(Jfreegman): Make this smarter in who to remove (e.g. the mod who hasn't been seen online in the longest time) + * + * Returns false on failure. + */ +non_null() +static bool prune_gc_mod_list(GC_Chat *chat) +{ +    if (chat->moderation.num_mods == 0) { +        return true; +    } + +    uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; +    bool pruned_mod = false; + +    for (uint16_t i = 0; i < chat->moderation.num_mods; ++i) { +        if (get_peer_number_of_sig_pk(chat, chat->moderation.mod_list[i]) == -1) { +            memcpy(public_sig_key, chat->moderation.mod_list[i], SIG_PUBLIC_KEY_SIZE); + +            if (!mod_list_remove_index(&chat->moderation, i)) { +                continue; +            } + +            pruned_mod = true; +            break; +        } +    } + +    return pruned_mod +           && mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash) +           && sign_gc_shared_state(chat) +           && broadcast_gc_shared_state(chat) +           && broadcast_gc_mod_list(chat) +           && update_gc_sanctions_list(chat, public_sig_key) +           && update_gc_topic(chat, public_sig_key); +} + +/** @brief Removes the first found offline sanctioned peer from the sanctions list and sends the + * event to the rest of the group. + * + * @retval false on failure or if no presently sanctioned peer is offline. + */ +non_null() +static bool prune_gc_sanctions_list(GC_Chat *chat) +{ +    if (chat->moderation.num_sanctions == 0) { +        return true; +    } + +    const Mod_Sanction *sanction = nullptr; +    uint8_t target_ext_pk[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + +    for (uint16_t i = 0; i < chat->moderation.num_sanctions; ++i) { +        const int peer_number = get_peer_number_of_enc_pk(chat, chat->moderation.sanctions[i].target_public_enc_key, true); + +        if (peer_number == -1) { +            sanction = &chat->moderation.sanctions[i]; +            memcpy(target_ext_pk, sanction->target_public_enc_key, ENC_PUBLIC_KEY_SIZE); +            memcpy(target_ext_pk + ENC_PUBLIC_KEY_SIZE, sanction->setter_public_sig_key, SIG_PUBLIC_KEY_SIZE); +            break; +        } +    } + +    if (sanction == nullptr) { +        return false; +    } + +    if (!sanctions_list_remove_observer(&chat->moderation, sanction->target_public_enc_key, nullptr)) { +        LOGGER_WARNING(chat->log, "Failed to remove entry from observer list"); +        return false; +    } + +    sanction = nullptr; + +    uint8_t data[MOD_SANCTIONS_CREDS_SIZE]; +    const uint16_t length = sanctions_creds_pack(&chat->moderation.sanctions_creds, data); + +    if (length != MOD_SANCTIONS_CREDS_SIZE) { +        LOGGER_ERROR(chat->log, "Failed to pack credentials (invalid length: %u)", length); +        return false; +    } + +    if (!send_gc_set_observer(chat, target_ext_pk, data, length, false)) { +        LOGGER_WARNING(chat->log, "Failed to broadcast set observer"); +        return false; +    } + +    return true; +} + +/** @brief Size of peer data that we pack for transfer (nick length must be accounted for separately). + * packed data consists of: nick length, nick, and status. + */ +#define PACKED_GC_PEER_SIZE (sizeof(uint16_t) + MAX_GC_NICK_SIZE + sizeof(uint8_t)) + +/** @brief Packs peer info into data of maxlength length. + * + * Return length of packed peer on success. + * Return -1 on failure. + */ +non_null() +static int pack_gc_peer(uint8_t *data, uint16_t length, const GC_Peer *peer) +{ +    if (PACKED_GC_PEER_SIZE > length) { +        return -1; +    } + +    uint32_t packed_len = 0; + +    net_pack_u16(data + packed_len, peer->nick_length); +    packed_len += sizeof(uint16_t); +    memcpy(data + packed_len, peer->nick, MAX_GC_NICK_SIZE); +    packed_len += MAX_GC_NICK_SIZE; +    memcpy(data + packed_len, &peer->status, sizeof(uint8_t)); +    packed_len += sizeof(uint8_t); + +    return packed_len; +} + +/** @brief Unpacks peer info of size length into peer. + * + * Returns the length of processed data on success. + * Returns -1 on failure. + */ +non_null() +static int unpack_gc_peer(GC_Peer *peer, const uint8_t *data, uint16_t length) +{ +    if (PACKED_GC_PEER_SIZE > length) { +        return -1; +    } + +    uint16_t len_processed = 0; + +    net_unpack_u16(data + len_processed, &peer->nick_length); +    len_processed += sizeof(uint16_t); +    peer->nick_length = min_u16(MAX_GC_NICK_SIZE, peer->nick_length); +    memcpy(peer->nick, data + len_processed, MAX_GC_NICK_SIZE); +    len_processed += MAX_GC_NICK_SIZE; +    memcpy(&peer->status, data + len_processed, sizeof(uint8_t)); +    len_processed += sizeof(uint8_t); + +    return len_processed; +} + +/** @brief Packs shared_state into data. + * + * @param data must have room for at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_shared_state(uint8_t *data, uint16_t length, const GC_SharedState *shared_state) +{ +    if (length < GC_PACKED_SHARED_STATE_SIZE) { +        return 0; +    } + +    const uint8_t privacy_state = shared_state->privacy_state; +    const uint8_t voice_state = shared_state->voice_state; + +    uint16_t packed_len = 0; + +    // version is always first +    net_pack_u32(data + packed_len, shared_state->version); +    packed_len += sizeof(uint32_t); + +    memcpy(data + packed_len, shared_state->founder_public_key, EXT_PUBLIC_KEY_SIZE); +    packed_len += EXT_PUBLIC_KEY_SIZE; +    net_pack_u16(data + packed_len, shared_state->maxpeers); +    packed_len += sizeof(uint16_t); +    net_pack_u16(data + packed_len, shared_state->group_name_len); +    packed_len += sizeof(uint16_t); +    memcpy(data + packed_len, shared_state->group_name, MAX_GC_GROUP_NAME_SIZE); +    packed_len += MAX_GC_GROUP_NAME_SIZE; +    memcpy(data + packed_len, &privacy_state, sizeof(uint8_t)); +    packed_len += sizeof(uint8_t); +    net_pack_u16(data + packed_len, shared_state->password_length); +    packed_len += sizeof(uint16_t); +    memcpy(data + packed_len, shared_state->password, MAX_GC_PASSWORD_SIZE); +    packed_len += MAX_GC_PASSWORD_SIZE; +    memcpy(data + packed_len, shared_state->mod_list_hash, MOD_MODERATION_HASH_SIZE); +    packed_len += MOD_MODERATION_HASH_SIZE; +    net_pack_u32(data + packed_len, shared_state->topic_lock); +    packed_len += sizeof(uint32_t); +    memcpy(data + packed_len, &voice_state, sizeof(uint8_t)); +    packed_len += sizeof(uint8_t); + +    return packed_len; +} + +/** @brief Unpacks shared state data into shared_state. + * + * @param data must contain at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns the length of processed data. + */ +non_null() +static uint16_t unpack_gc_shared_state(GC_SharedState *shared_state, const uint8_t *data, uint16_t length) +{ +    if (length < GC_PACKED_SHARED_STATE_SIZE) { +        return 0; +    } + +    uint16_t len_processed = 0; + +    // version is always first +    net_unpack_u32(data + len_processed, &shared_state->version); +    len_processed += sizeof(uint32_t); + +    memcpy(shared_state->founder_public_key, data + len_processed, EXT_PUBLIC_KEY_SIZE); +    len_processed += EXT_PUBLIC_KEY_SIZE; +    net_unpack_u16(data + len_processed, &shared_state->maxpeers); +    len_processed += sizeof(uint16_t); +    net_unpack_u16(data + len_processed, &shared_state->group_name_len); +    shared_state->group_name_len = min_u16(shared_state->group_name_len, MAX_GC_GROUP_NAME_SIZE); +    len_processed += sizeof(uint16_t); +    memcpy(shared_state->group_name, data + len_processed, MAX_GC_GROUP_NAME_SIZE); +    len_processed += MAX_GC_GROUP_NAME_SIZE; + +    uint8_t privacy_state; +    memcpy(&privacy_state, data + len_processed, sizeof(uint8_t)); +    len_processed += sizeof(uint8_t); + +    net_unpack_u16(data + len_processed, &shared_state->password_length); +    len_processed += sizeof(uint16_t); +    memcpy(shared_state->password, data + len_processed, MAX_GC_PASSWORD_SIZE); +    len_processed += MAX_GC_PASSWORD_SIZE; +    memcpy(shared_state->mod_list_hash, data + len_processed, MOD_MODERATION_HASH_SIZE); +    len_processed += MOD_MODERATION_HASH_SIZE; +    net_unpack_u32(data + len_processed, &shared_state->topic_lock); +    len_processed += sizeof(uint32_t); + +    uint8_t voice_state; +    memcpy(&voice_state, data + len_processed, sizeof(uint8_t)); +    len_processed += sizeof(uint8_t); + +    shared_state->voice_state = (Group_Voice_State)voice_state; +    shared_state->privacy_state = (Group_Privacy_State)privacy_state; + +    return len_processed; +} + +/** @brief Packs topic info into data. + * + * @param data must have room for at least topic length + GC_MIN_PACKED_TOPIC_INFO_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_topic_info(uint8_t *data, uint16_t length, const GC_TopicInfo *topic_info) +{ +    if (length < topic_info->length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { +        return 0; +    } + +    uint16_t packed_len = 0; + +    net_pack_u32(data + packed_len, topic_info->version); +    packed_len += sizeof(uint32_t); +    net_pack_u16(data + packed_len, topic_info->checksum); +    packed_len += sizeof(uint16_t); +    net_pack_u16(data + packed_len, topic_info->length); +    packed_len += sizeof(uint16_t); +    memcpy(data + packed_len, topic_info->topic, topic_info->length); +    packed_len += topic_info->length; +    memcpy(data + packed_len, topic_info->public_sig_key, SIG_PUBLIC_KEY_SIZE); +    packed_len += SIG_PUBLIC_KEY_SIZE; + +    return packed_len; +} + +/** @brief Unpacks topic info into `topic_info`. + * + * Returns -1 on failure. + * Returns the length of the processed data on success. + */ +non_null() +static int unpack_gc_topic_info(GC_TopicInfo *topic_info, const uint8_t *data, uint16_t length) +{ +    if (length < sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t)) { +        return -1; +    } + +    uint16_t len_processed = 0; + +    net_unpack_u32(data + len_processed, &topic_info->version); +    len_processed += sizeof(uint32_t); +    net_unpack_u16(data + len_processed, &topic_info->checksum); +    len_processed += sizeof(uint16_t); +    net_unpack_u16(data + len_processed, &topic_info->length); +    len_processed += sizeof(uint16_t); + +    if (topic_info->length > MAX_GC_TOPIC_SIZE) { +        topic_info->length = MAX_GC_TOPIC_SIZE; +    } + +    if (length - len_processed < topic_info->length + SIG_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    if (topic_info->length > 0) { +        memcpy(topic_info->topic, data + len_processed, topic_info->length); +        len_processed += topic_info->length; +    } + +    memcpy(topic_info->public_sig_key, data + len_processed, SIG_PUBLIC_KEY_SIZE); +    len_processed += SIG_PUBLIC_KEY_SIZE; + +    return len_processed; +} + +/** @brief Creates a shared state packet and puts it in data. + * Packet includes self pk hash, shared state signature, and packed shared state info. + * data must have room for at least GC_SHARED_STATE_ENC_PACKET_SIZE bytes. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_shared_state_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ +    if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { +        return -1; +    } + +    memcpy(data, chat->shared_state_sig, SIGNATURE_SIZE); +    const uint16_t header_len = SIGNATURE_SIZE; + +    const uint16_t packed_len = pack_gc_shared_state(data + header_len, length - header_len, &chat->shared_state); + +    if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { +        return -1; +    } + +    return (int)(header_len + packed_len); +} + +/** @brief Creates a signature for the group's shared state in packed form. + * + * This function only works for the Founder. + * + * Returns true on success and increments the shared state version. + */ +non_null() +static bool sign_gc_shared_state(GC_Chat *chat) +{ +    if (!self_gc_is_founder(chat)) { +        LOGGER_ERROR(chat->log, "Failed to sign shared state (invalid permission)"); +        return false; +    } + +    if (chat->shared_state.version != UINT32_MAX) { +        /* improbable, but an overflow would break everything */ +        set_gc_shared_state_version(chat, chat->shared_state.version + 1); +    } else { +        LOGGER_WARNING(chat->log, "Shared state version wraparound"); +    } + +    uint8_t shared_state[GC_PACKED_SHARED_STATE_SIZE]; +    const uint16_t packed_len = pack_gc_shared_state(shared_state, sizeof(shared_state), &chat->shared_state); + +    if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { +        set_gc_shared_state_version(chat, chat->shared_state.version - 1); +        LOGGER_ERROR(chat->log, "Failed to pack shared state"); +        return false; +    } + +    const int ret = crypto_sign_detached(chat->shared_state_sig, nullptr, shared_state, packed_len, +                                         get_sig_sk(chat->chat_secret_key)); + +    if (ret != 0) { +        set_gc_shared_state_version(chat, chat->shared_state.version - 1); +        LOGGER_ERROR(chat->log, "Failed to sign shared state (%d)", ret); +        return false; +    } + +    return true; +} + +/** @brief Decrypts data using the shared key associated with `gconn`. + * + * The packet payload should begin with a nonce. + * + * @param message_id should be set to NULL for lossy packets. + * + * Returns length of the plaintext data on success. + * Return -1 if encrypted payload length is invalid. + * Return -2 on decryption failure. + * Return -3 if plaintext payload length is invalid. + */ +non_null(1, 2, 3, 5, 6) nullable(4) +static int group_packet_unwrap(const Logger *log, const GC_Connection *gconn, uint8_t *data, uint64_t *message_id, +                               uint8_t *packet_type, const uint8_t *packet, uint16_t length) +{ +    if (length <= CRYPTO_NONCE_SIZE) { +        LOGGER_FATAL(log, "Invalid packet length: %u", length); +        return -1; +    } + +    uint8_t *plain = (uint8_t *)malloc(length); + +    if (plain == nullptr) { +        LOGGER_ERROR(log, "Failed to allocate memory for plain data buffer"); +        return -1; +    } + +    int plain_len = decrypt_data_symmetric(gconn->session_shared_key, packet, packet + CRYPTO_NONCE_SIZE, +                                           length - CRYPTO_NONCE_SIZE, plain); + +    if (plain_len <= 0) { +        free(plain); +        return plain_len == 0 ? -3 : -2; +    } + +    const int min_plain_len = message_id != nullptr ? 1 + GC_MESSAGE_ID_BYTES : 1; + +    /* remove padding */ +    const uint8_t *real_plain = plain; + +    while (real_plain[0] == 0) { +        ++real_plain; +        --plain_len; + +        if (plain_len < min_plain_len) { +            free(plain); +            return -3; +        } +    } + +    uint32_t header_len = sizeof(uint8_t); +    *packet_type = real_plain[0]; +    plain_len -= sizeof(uint8_t); + +    if (message_id != nullptr) { +        net_unpack_u64(real_plain + sizeof(uint8_t), message_id); +        plain_len -= GC_MESSAGE_ID_BYTES; +        header_len += GC_MESSAGE_ID_BYTES; +    } + +    memcpy(data, real_plain + header_len, plain_len); + +    free(plain); + +    return plain_len; +} + +int group_packet_wrap( +    const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, +    uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, +    uint8_t gp_packet_type, uint8_t net_packet_type) +{ +    const uint16_t padding_len = group_packet_padding_length(length); +    const uint16_t min_packet_size = net_packet_type == NET_PACKET_GC_LOSSLESS +                                     ? length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + GC_MESSAGE_ID_BYTES + 1 +                                     : length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + 1; + +    if (min_packet_size > packet_size) { +        LOGGER_ERROR(log, "Invalid packet buffer size: %u", packet_size); +        return -1; +    } + +    if (length > MAX_GC_PACKET_CHUNK_SIZE) { +        LOGGER_ERROR(log, "Packet payload size (%u) exceeds maximum (%u)", length, MAX_GC_PACKET_CHUNK_SIZE); +        return -1; +    } + +    uint8_t *plain = (uint8_t *)malloc(packet_size); + +    if (plain == nullptr) { +        return -1; +    } + +    assert(padding_len < packet_size); + +    memset(plain, 0, padding_len); + +    uint16_t enc_header_len = sizeof(uint8_t); +    plain[padding_len] = gp_packet_type; + +    if (net_packet_type == NET_PACKET_GC_LOSSLESS) { +        net_pack_u64(plain + padding_len + sizeof(uint8_t), message_id); +        enc_header_len += GC_MESSAGE_ID_BYTES; +    } + +    if (length > 0 && data != nullptr) { +        memcpy(plain + padding_len + enc_header_len, data, length); +    } + +    uint8_t nonce[CRYPTO_NONCE_SIZE]; +    random_nonce(rng, nonce); + +    const uint16_t plain_len = padding_len + enc_header_len + length; +    const uint16_t encrypt_buf_size = plain_len + CRYPTO_MAC_SIZE; + +    uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + +    if (encrypt == nullptr) { +        free(plain); +        return -2; +    } + +    const int enc_len = encrypt_data_symmetric(shared_key, nonce, plain, plain_len, encrypt); + +    free(plain); + +    if (enc_len != encrypt_buf_size) { +        LOGGER_ERROR(log, "encryption failed. packet type: 0x%02x, enc_len: %d", gp_packet_type, enc_len); +        free(encrypt); +        return -3; +    } + +    packet[0] = net_packet_type; +    memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); +    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); +    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + +    free(encrypt); + +    return 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Sends a lossy packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null() +static bool send_lossy_group_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, +                                    uint16_t length, uint8_t packet_type) +{ +    assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + +    if (!gconn->handshaked || gconn->pending_delete) { +        return false; +    } + +    if (data == nullptr || length == 0) { +        return false; +    } + +    const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSY); +    uint8_t *packet = (uint8_t *)malloc(packet_size); + +    if (packet == nullptr) { +        return false; +    } + +    const int len = group_packet_wrap( +                        chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, +                        packet_size, data, length, 0, packet_type, NET_PACKET_GC_LOSSY); + +    if (len < 0) { +        LOGGER_ERROR(chat->log, "Failed to encrypt packet (type: 0x%02x, error: %d)", packet_type, len); +        free(packet); +        return false; +    } + +    const bool ret = gcc_send_packet(chat, gconn, packet, (uint16_t)len); + +    free(packet); + +    return ret; +} + +/** @brief Sends a lossless packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null(1, 2) nullable(3) +static bool send_lossless_group_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, +                                       uint8_t packet_type) +{ +    assert(length <= MAX_GC_PACKET_SIZE); + +    if (!gconn->handshaked || gconn->pending_delete) { +        return false; +    } + +    if (length > MAX_GC_PACKET_CHUNK_SIZE) { +        return gcc_send_lossless_packet_fragments(chat, gconn, data, length, packet_type); +    } + +    return gcc_send_lossless_packet(chat, gconn, data, length, packet_type) == 0; +} + +/** @brief Sends a group sync request to peer. + * + * Returns true on success or if sync request timeout has not expired. + */ +non_null() +static bool send_gc_sync_request(GC_Chat *chat, GC_Connection *gconn, uint16_t sync_flags) +{ +    if (!mono_time_is_timeout(chat->mono_time, chat->last_sync_request, GC_SYNC_REQUEST_LIMIT)) { +        return true; +    } + +    chat->last_sync_request = mono_time_get(chat->mono_time); + +    uint8_t data[(sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE]; +    uint16_t length = sizeof(uint16_t); + +    net_pack_u16(data, sync_flags); + +    if (chat_is_password_protected(chat)) { +        net_pack_u16(data + length, chat->shared_state.password_length); +        length += sizeof(uint16_t); + +        memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); +        length += MAX_GC_PASSWORD_SIZE; +    } + +    return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_REQUEST); +} + +/** @brief Sends a sync response packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool send_gc_sync_response(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_RESPONSE); +} + +non_null() static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, +        uint8_t request_type, uint8_t join_type); +non_null() static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Unpacks a sync announce. + * + * If the announced peer is not already in our peer list, we attempt to + * initiate a peer info exchange with them. + * + * Return true on success (whether or not the peer was added). + */ +non_null() +static bool unpack_gc_sync_announce(GC_Chat *chat, const uint8_t *data, const uint16_t length) +{ +    GC_Announce announce = {0}; + +    const int unpacked_announces = gca_unpack_announces_list(chat->log, data, length, &announce, 1); + +    if (unpacked_announces <= 0) { +        LOGGER_WARNING(chat->log, "Failed to unpack announces: %d", unpacked_announces); +        return false; +    } + +    if (memcmp(announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { +        LOGGER_WARNING(chat->log, "Attempted to unpack our own announce"); +        return true; +    } + +    if (!gca_is_valid_announce(&announce)) { +        LOGGER_WARNING(chat->log, "got invalid announce"); +        return false; +    } + +    const IP_Port *ip_port = announce.ip_port_is_set ? &announce.ip_port : nullptr; +    const int new_peer_number = peer_add(chat, ip_port, announce.peer_public_key); + +    if (new_peer_number == -1) { +        LOGGER_ERROR(chat->log, "peer_add() failed"); +        return false; +    } + +    if (new_peer_number == -2) {  // peer already added +        return true; +    } + +    if (new_peer_number > 0) { +        GC_Connection *new_gconn = get_gc_connection(chat, new_peer_number); + +        if (new_gconn == nullptr) { +            return false; +        } + +        uint32_t added_tcp_relays = 0; + +        for (uint8_t i = 0; i < announce.tcp_relays_count; ++i) { +            const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, new_gconn->tcp_connection_num, +                                       &announce.tcp_relays[i].ip_port, +                                       announce.tcp_relays[i].public_key); + +            if (add_tcp_result == -1) { +                continue; +            } + +            if (gcc_save_tcp_relay(chat->rng, new_gconn, &announce.tcp_relays[i]) == 0) { +                ++added_tcp_relays; +            } +        } + +        if (!announce.ip_port_is_set && added_tcp_relays == 0) { +            gcc_mark_for_deletion(new_gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +            LOGGER_ERROR(chat->log, "Sync error: Invalid peer connection info"); +            return false; +        } + +        new_gconn->pending_handshake_type = HS_PEER_INFO_EXCHANGE; + +        return true; +    } + +    LOGGER_FATAL(chat->log, "got impossible return value %d", new_peer_number); + +    return false; +} + +/** @brief Handles a sync response packet. + * + * Note: This function may change peer numbers. + * + * Return 0 on success. + * Return -1 if the group is full or the peer failed to unpack. + * Return -2 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_sync_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                                   uint16_t length, void *userdata) +{ +    if (chat->connection_state == CS_CONNECTED && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers +            && !peer_is_founder(chat, peer_number)) { +        return -1; +    } + +    if (length > 0) { +        if (!unpack_gc_sync_announce(chat, data, length)) { +            return -1; +        } +    } + +    chat->connection_state = CS_CONNECTED; + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -2; +    } + +    if (!send_gc_peer_exchange(chat, gconn)) { +        LOGGER_WARNING(chat->log, "Failed to send peer exchange on sync response"); +    } + +    if (c->self_join != nullptr && chat->time_connected == 0) { +        c->self_join(c->messenger, chat->group_number, userdata); +        chat->time_connected = mono_time_get(chat->mono_time); +    } + +    return 0; +} + +non_null() static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key); +non_null() static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Creates a sync announce for peer designated by `gconn` and puts it in `announce`, which + * must be zeroed by the caller. + * + * Returns true if announce was successfully created. + */ +non_null() +static bool create_sync_announce(const GC_Chat *chat, const GC_Connection *gconn, uint32_t peer_number, +                                 GC_Announce *announce) +{ +    if (chat == nullptr || gconn == nullptr) { +        return false; +    } + +    if (gconn->tcp_relays_count > 0) { +        if (gcc_copy_tcp_relay(chat->rng, &announce->tcp_relays[0], gconn)) { +            announce->tcp_relays_count = 1; +        } +    } + +    get_gc_peer_public_key(chat, peer_number, announce->peer_public_key); + +    if (gcc_ip_port_is_set(gconn)) { +        announce->ip_port = gconn->addr.ip_port; +        announce->ip_port_is_set = true; +    } + +    return true; +} + +non_null() +static bool sync_response_send_peers(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, bool first_sync) +{ +    // Always respond to a peer's first sync request +    if (!first_sync && !mono_time_is_timeout(chat->mono_time, +            chat->last_sync_response_peer_list, +            GC_SYNC_RESPONSE_PEER_LIST_LIMIT)) { +        return true; +    } + +    uint8_t *response = (uint8_t *)malloc(MAX_GC_PACKET_CHUNK_SIZE); + +    if (response == nullptr) { +        return false; +    } + +    size_t num_announces = 0; + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        const GC_Connection *peer_gconn = get_gc_connection(chat, i); + +        if (peer_gconn == nullptr || !peer_gconn->confirmed) { +            continue; +        } + +        if (peer_gconn->public_key_hash == gconn->public_key_hash || i == peer_number) { +            continue; +        } + +        GC_Announce announce = {0}; + +        if (!create_sync_announce(chat, peer_gconn, i, &announce)) { +            continue; +        } + +        const int packed_length = gca_pack_announce(chat->log, response, MAX_GC_PACKET_CHUNK_SIZE, &announce); + +        if (packed_length <= 0) { +            LOGGER_WARNING(chat->log, "Failed to pack announce: %d", packed_length); +            continue; +        } + +        if (!send_gc_sync_response(chat, gconn, response, packed_length)) { +            LOGGER_WARNING(chat->log, "Failed to send peer announce info"); +            continue; +        } + +        ++num_announces; +    } + +    free(response); + +    if (num_announces == 0) { +        // we send an empty sync response even if we didn't send any peers as an acknowledgement +        if (!send_gc_sync_response(chat, gconn, nullptr, 0)) { +            LOGGER_WARNING(chat->log, "Failed to send peer announce info"); +            return false; +        } +    } else { +        chat->last_sync_response_peer_list = mono_time_get(chat->mono_time); +    } + +    return true; +} + +/** @brief Sends group state specified by `sync_flags` peer designated by `peer_number`. + * + * Return true on success. + */ +non_null() +static bool sync_response_send_state(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, +                                     uint16_t sync_flags) +{ +    const bool first_sync = gconn->last_sync_response == 0; + +    // Do not change the order of these four send calls. See: https://toktok.ltd/spec.html#sync_request-0xf8 +    if ((sync_flags & GF_STATE) > 0 && chat->shared_state.version > 0) { +        if (!send_peer_shared_state(chat, gconn)) { +            LOGGER_WARNING(chat->log, "Failed to send shared state"); +            return false; +        } + +        if (!send_peer_mod_list(chat, gconn)) { +            LOGGER_WARNING(chat->log, "Failed to send mod list"); +            return false; +        } + +        if (!send_peer_sanctions_list(chat, gconn)) { +            LOGGER_WARNING(chat->log, "Failed to send sanctions list"); +            return false; +        } + +        gconn->last_sync_response = mono_time_get(chat->mono_time); +    } + +    if ((sync_flags & GF_TOPIC) > 0 && chat->time_connected > 0 && chat->topic_info.version > 0) { +        if (!send_peer_topic(chat, gconn)) { +            LOGGER_WARNING(chat->log, "Failed to send topic"); +            return false; +        } + +        gconn->last_sync_response = mono_time_get(chat->mono_time); +    } + +    if ((sync_flags & GF_PEERS) > 0) { +        if (!sync_response_send_peers(chat, gconn, peer_number, first_sync)) { +            return false; +        } + +        gconn->last_sync_response = mono_time_get(chat->mono_time); +    } + +    return true; +} + +/** @brief Handles a sync request packet and sends a response containing the peer list. + * + * May send additional group info in separate packets, including the topic, shared state, mod list, + * and sanctions list, if respective sync flags are set. + * + * If the group is password protected the password in the request data must first be verified. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if password is invalid. + * Return -3 if we fail to send a response packet. + * Return -4 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_sync_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -4; +    } + +    if (length < sizeof(uint16_t)) { +        return -1; +    } + +    if (chat->numpeers <= 1) { +        return 0; +    } + +    if (chat->shared_state.version == 0) { +        LOGGER_DEBUG(chat->log, "Got sync request with uninitialized state"); +        return 0; +    } + +    if (!mono_time_is_timeout(chat->mono_time, gconn->last_sync_response, GC_PING_TIMEOUT)) { +        LOGGER_DEBUG(chat->log, "sync request rate limit for peer %d", peer_number); +        return 0; +    } + +    uint16_t sync_flags; +    net_unpack_u16(data, &sync_flags); + +    if (chat_is_password_protected(chat)) { +        if (length < (sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE) { +            return -2; +        } + +        uint16_t password_length; +        net_unpack_u16(data + sizeof(uint16_t), &password_length); + +        const uint8_t *password = data + (sizeof(uint16_t) * 2); + +        if (!validate_password(chat, password, password_length)) { +            LOGGER_DEBUG(chat->log, "Invalid password"); +            return -2; +        } +    } + +    if (!sync_response_send_state(chat, gconn, peer_number, sync_flags)) { +        return -3; +    } + +    return 0; +} + +non_null() static void copy_self(const GC_Chat *chat, GC_Peer *peer); +non_null() static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Shares our TCP relays with peer and adds shared relays to our connection with them. + * + * Returns true on success or if we're not connected to any TCP relays. + */ +non_null() +static bool send_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn) +{ + +    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; +    uint8_t data[GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6]; + +    const uint32_t n = tcp_copy_connected_relays_index(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, +                       gconn->tcp_relay_share_index); + +    if (n == 0) { +        return true; +    } + +    gconn->tcp_relay_share_index += GCC_MAX_TCP_SHARED_RELAYS; + +    for (uint32_t i = 0; i < n; ++i) { +        add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_relays[i].ip_port, +                                 tcp_relays[i].public_key); +    } + +    const int nodes_len = pack_nodes(chat->log, data, sizeof(data), tcp_relays, n); + +    if (nodes_len <= 0 || (uint32_t)nodes_len > sizeof(data)) { +        LOGGER_ERROR(chat->log, "Failed to pack tcp relays (nodes_len: %d)", nodes_len); +        return false; +    } + +    if (!send_lossless_group_packet(chat, gconn, data, (uint16_t)nodes_len, GP_TCP_RELAYS)) { +        LOGGER_ERROR(chat->log, "Failed to send tcp relays"); +        return false; +    } + +    return true; +} + +/** @brief Adds a peer's shared TCP relays to our connection with them. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contains invalid data. + */ +non_null() +static int handle_gc_tcp_relays(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    if (length == 0) { +        return -1; +    } + +    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; +    const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, nullptr, data, length, true); + +    if (num_nodes <= 0) { +        return -2; +    } + +    for (int i = 0; i < num_nodes; ++i) { +        const Node_format *tcp_node = &tcp_relays[i]; + +        if (add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_node->ip_port, +                                     tcp_node->public_key) == 0) { +            gcc_save_tcp_relay(chat->rng, gconn, tcp_node); + +            if (gconn->tcp_relays_count == 1) { +                add_gc_saved_peers(chat, gconn);  // make sure we save at least one tcp relay +            } +        } +    } + +    return 0; +} + +/** @brief Send invite request to peer_number. + * + * If the group requires a password, the packet will + * contain the password supplied by the invite requestor. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_request(const GC_Chat *chat, GC_Connection *gconn) +{ +    uint16_t length = 0; +    uint8_t data[sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE]; + +    if (chat_is_password_protected(chat)) { +        net_pack_u16(data, chat->shared_state.password_length); +        length += sizeof(uint16_t); + +        memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); +        length += MAX_GC_PASSWORD_SIZE; +    } + +    return send_lossless_group_packet(chat, gconn, data, length, GP_INVITE_REQUEST); +} + +non_null() +static bool send_gc_invite_response(const GC_Chat *chat, GC_Connection *gconn) +{ +    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_INVITE_RESPONSE); +} + +/** @brief Handles an invite response packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if we fail to send a sync request. + */ +non_null() +static int handle_gc_invite_response(GC_Chat *chat, GC_Connection *gconn) +{ +    const uint16_t flags = GF_PEERS | GF_TOPIC | GF_STATE; + +    if (!send_gc_sync_request(chat, gconn, flags)) { +        return -1; +    } + +    return 0; +} + +/** + * @brief Handles an invite response reject packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_invite_response_reject(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, +        void *userdata) +{ +    if (length < sizeof(uint8_t)) { +        return -1; +    } + +    if (chat->connection_state == CS_CONNECTED) { +        return 0; +    } + +    if (gc_get_self_role(chat) == GR_FOUNDER) { +        return 0; +    } + +    uint8_t type = data[0]; + +    if (type >= GJ_INVALID) { +        type = GJ_INVITE_FAILED; +    } + +    chat->connection_state = CS_DISCONNECTED; + +    if (c->rejected != nullptr) { +        c->rejected(c->messenger, chat->group_number, type, userdata); +    } + +    return 0; +} + +/** @brief Sends an invite response rejection packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_response_reject(const GC_Chat *chat, const GC_Connection *gconn, uint8_t type) +{ +    if (type >= GJ_INVALID) { +        type = GJ_INVITE_FAILED; +    } + +    uint8_t data[1]; +    data[0] = type; +    const uint16_t length = 1; + +    return send_lossy_group_packet(chat, gconn, data, length, GP_INVITE_RESPONSE_REJECT); +} + +/** @brief Handles an invite request and verifies that the correct password has been supplied + * if the group is password protected. + * + * Return 0 if invite request is successfully handled. + * Return -1 if the group is full. + * Return -2 if the supplied password is invalid. + * Return -3 if we fail to send an invite response. + * Return -4 if peer_number does not designate a valid peer. + */ +non_null() +static int handle_gc_invite_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ +    if (chat->shared_state.version == 0) {  // we aren't synced yet; ignore request +        return 0; +    } + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -4; +    } + +    int ret = -1; + +    uint8_t invite_error; + +    if (get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers && !peer_is_founder(chat, peer_number)) { +        invite_error = GJ_GROUP_FULL; +        goto FAILED_INVITE; +    } + +    if (chat_is_password_protected(chat)) { +        invite_error = GJ_INVALID_PASSWORD; +        ret = -2; + +        if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { +            goto FAILED_INVITE; +        } + +        uint16_t password_length; +        net_unpack_u16(data, &password_length); + +        const uint8_t *password = data + sizeof(uint16_t); + +        if (!validate_password(chat, password, password_length)) { +            goto FAILED_INVITE; +        } +    } + +    if (!send_gc_invite_response(chat, gconn)) { +        return -3; +    } + +    return 0; + +FAILED_INVITE: +    send_gc_invite_response_reject(chat, gconn, invite_error); +    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + +    return ret; +} + +/** @brief Sends a lossless packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossless_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (gconn->confirmed) { +            send_lossless_group_packet(chat, gconn, data, length, type); +        } +    } +} + +/** @brief Sends a lossy packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossy_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); + +        assert(gconn != nullptr); + +        if (gconn->confirmed) { +            send_lossy_group_packet(chat, gconn, data, length, type); +        } +    } +} + +/** @brief Creates packet with broadcast header info followed by data of length. + * + * Returns length of packet including header. + */ +non_null(3) nullable(1) +static uint16_t make_gc_broadcast_header(const uint8_t *data, uint16_t length, uint8_t *packet, uint8_t bc_type) +{ +    packet[0] = bc_type; +    const uint16_t header_len = sizeof(uint8_t); + +    if (data != nullptr && length > 0) { +        memcpy(packet + header_len, data, length); +    } + +    return length + header_len; +} + +/** @brief sends a group broadcast packet to all confirmed peers. + * + * Returns true on success. + */ +non_null(1) nullable(2) +static bool send_gc_broadcast_message(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t bc_type) +{ +    if (length + GC_BROADCAST_ENC_HEADER_SIZE > MAX_GC_PACKET_SIZE) { +        LOGGER_ERROR(chat->log, "Failed to broadcast message: invalid length %u", length); +        return false; +    } + +    uint8_t *packet = (uint8_t *)malloc(length + GC_BROADCAST_ENC_HEADER_SIZE); + +    if (packet == nullptr) { +        return false; +    } + +    const uint16_t packet_len = make_gc_broadcast_header(data, length, packet, bc_type); + +    send_gc_lossless_packet_all_peers(chat, packet, packet_len, GP_BROADCAST); + +    free(packet); + +    return true; +} + +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat); + +/** @brief Compares the supplied values with our own state and returns the appropriate + * sync flags for a sync request. + */ +non_null() +static uint16_t get_sync_flags(const GC_Chat *chat, uint16_t peers_checksum, uint16_t peer_count, +                               uint32_t sstate_version, uint32_t screds_version, uint16_t roles_checksum, +                               uint32_t topic_version, uint16_t topic_checksum) +{ +    uint16_t sync_flags = 0; + +    if (peers_checksum != chat->peers_checksum && peer_count >= get_gc_confirmed_numpeers(chat)) { +        sync_flags |= GF_PEERS; +    } + +    if (sstate_version > 0) { +        const uint16_t self_roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; + +        if ((sstate_version > chat->shared_state.version || screds_version > chat->moderation.sanctions_creds.version) +                || (screds_version == chat->moderation.sanctions_creds.version +                    && roles_checksum != self_roles_checksum)) { +            sync_flags |= GF_STATE; +        } +    } + +    if (group_topic_lock_enabled(chat)) { +        if (topic_version > chat->topic_info.version || +                (topic_version == chat->topic_info.version && topic_checksum > chat->topic_info.checksum)) { +            sync_flags |= GF_TOPIC; +        } +    } else if (topic_checksum > chat->topic_info.checksum) { +        sync_flags |= GF_TOPIC; +    } + +    return sync_flags; +} + +/** @brief Compares a peer's group sync info that we received in a ping packet to our own. + * + * If their info appears to be more recent than ours we send them a sync request. + * + * This function should only be called from `handle_gc_ping()`. + * + * Returns true if a sync request packet is successfully sent. + */ +non_null() +static bool do_gc_peer_state_sync(GC_Chat *chat, GC_Connection *gconn, const uint8_t *sync_data, +                                  const uint16_t length) +{ +    if (length < GC_PING_PACKET_MIN_DATA_SIZE) { +        return false; +    } + +    uint16_t peers_checksum; +    uint16_t peer_count; +    uint32_t sstate_version; +    uint32_t screds_version; +    uint16_t roles_checksum; +    uint32_t topic_version; +    uint16_t topic_checksum; + +    size_t unpacked_len = 0; + +    net_unpack_u16(sync_data, &peers_checksum); +    unpacked_len += sizeof(uint16_t); + +    net_unpack_u16(sync_data + unpacked_len, &peer_count); +    unpacked_len += sizeof(uint16_t); + +    net_unpack_u32(sync_data + unpacked_len, &sstate_version); +    unpacked_len += sizeof(uint32_t); + +    net_unpack_u32(sync_data + unpacked_len, &screds_version); +    unpacked_len += sizeof(uint32_t); + +    net_unpack_u16(sync_data + unpacked_len, &roles_checksum); +    unpacked_len += sizeof(uint16_t); + +    net_unpack_u32(sync_data + unpacked_len, &topic_version); +    unpacked_len += sizeof(uint32_t); + +    net_unpack_u16(sync_data + unpacked_len, &topic_checksum); +    unpacked_len += sizeof(uint16_t); + +    if (unpacked_len != GC_PING_PACKET_MIN_DATA_SIZE) { +        LOGGER_FATAL(chat->log, "Unpacked length is impossible (%zu)", unpacked_len); +        return false; +    } + +    const uint16_t sync_flags = get_sync_flags(chat, peers_checksum, peer_count, sstate_version, screds_version, +                                roles_checksum, topic_version, topic_checksum); + +    if (sync_flags > 0) { +        return send_gc_sync_request(chat, gconn, sync_flags); +    } + +    return false; +} + +/** @brief Handles a ping packet. + * + * The packet contains sync information including peer's peer list checksum, + * shared state version, topic version, and sanction credentials version. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size or peer is not confirmed. + */ +non_null() +static int handle_gc_ping(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    if (length < GC_PING_PACKET_MIN_DATA_SIZE) { +        return -1; +    } + +    if (!gconn->confirmed) { +        return -1; +    } + +    do_gc_peer_state_sync(chat, gconn, data, length); + +    if (length > GC_PING_PACKET_MIN_DATA_SIZE) { +        IP_Port ip_port; +        memset(&ip_port, 0, sizeof(IP_Port)); + +        if (unpack_ip_port(&ip_port, data + GC_PING_PACKET_MIN_DATA_SIZE, +                           length - GC_PING_PACKET_MIN_DATA_SIZE, false) > 0) { +            gcc_set_ip_port(gconn, &ip_port); +            add_gc_saved_peers(chat, gconn); +        } +    } + +    return 0; +} + +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status) +{ +    const GC_Session *c = m->group_handler; +    const GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    self_gc_set_status(chat, status); + +    uint8_t data[1]; +    data[0] = gc_get_self_status(chat); + +    if (!send_gc_broadcast_message(chat, data, 1, GM_STATUS)) { +        return -2; +    } + +    return 0; +} + +/** @brief Handles a status broadcast from `peer`. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid length. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_status(const GC_Session *c, const GC_Chat *chat, GC_Peer *peer, const uint8_t *data, +                            uint16_t length, void *userdata) +{ +    if (length < sizeof(uint8_t)) { +        return -1; +    } + +    const Group_Peer_Status status = (Group_Peer_Status)data[0]; + +    if (status > GS_BUSY) { +        LOGGER_WARNING(chat->log, "Received invalid status %u", status); +        return 0; +    } + +    peer->status = status; + +    if (c->status_change != nullptr) { +        c->status_change(c->messenger, chat->group_number, peer->peer_id, status, userdata); +    } + +    return 0; +} + +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return UINT8_MAX; +    } + +    return peer->status; +} + +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return UINT8_MAX; +    } + +    return peer->role; +} + +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest) +{ +    if (dest != nullptr) { +        memcpy(dest, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); +    } +} + +/** @brief Sends self peer info to `gconn`. + * + * If the group is password protected the request will contain the group + * password, which the recipient will validate in the respective + * group message handler. + * + * Returns true on success. + */ +non_null() +static bool send_self_to_peer(const GC_Chat *chat, GC_Connection *gconn) +{ +    GC_Peer *self = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + +    if (self == nullptr) { +        return false; +    } + +    copy_self(chat, self); + +    const uint16_t data_size = PACKED_GC_PEER_SIZE + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE; +    uint8_t *data = (uint8_t *)malloc(data_size); + +    if (data == nullptr) { +        free(self); +        return false; +    } + +    uint16_t length = 0; + +    if (chat_is_password_protected(chat)) { +        net_pack_u16(data, chat->shared_state.password_length); +        length += sizeof(uint16_t); + +        memcpy(data + sizeof(uint16_t), chat->shared_state.password, MAX_GC_PASSWORD_SIZE); +        length += MAX_GC_PASSWORD_SIZE; +    } + +    const int packed_len = pack_gc_peer(data + length, data_size - length, self); +    length += packed_len; + +    free(self); + +    if (packed_len <= 0) { +        LOGGER_DEBUG(chat->log, "pack_gc_peer failed in handle_gc_peer_info_request_request %d", packed_len); +        free(data); +        return false; +    } + +    const bool ret = send_lossless_group_packet(chat, gconn, data, length, GP_PEER_INFO_RESPONSE); + +    free(data); + +    return ret; +} + +/** @brief Handles a peer info request packet. + * + * Return 0 on success. + * Return -1 if unconfirmed peer is trying to join a full group. + * Return -2 if response fails. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_peer_info_request(const GC_Chat *chat, uint32_t peer_number) +{ +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -3; +    } + +    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers +            && !peer_is_founder(chat, peer_number)) { +        return -1; +    } + +    if (!send_self_to_peer(chat, gconn)) { +        return -2; +    } + +    return 0; +} + +/** @brief Sends a peer info request to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn) +{ +    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_PEER_INFO_REQUEST); +} + +/** @brief Do peer info exchange with peer designated by `gconn`. + * + * This function sends two packets to a peer. The first packet is a peer info response containing our own info, + * and the second packet is a peer info request. + * + * Return false if either packet fails to send. + */ +static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn) +{ +    return send_self_to_peer(chat, gconn) && send_gc_peer_info_request(chat, gconn); +} + +/** @brief Updates peer's info, validates their group role, and sets them as a confirmed peer. + * If the group is password protected the password must first be validated. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if group number is invalid. + * Return -3 if peer number is invalid. + * Return -4 if unconfirmed peer is trying to join a full group. + * Return -5 if supplied group password is invalid. + * Return -6 if we fail to add the peer to the peer list. + * Return -7 if peer's role cannot be validated. + * Return -8 if malloc fails. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_peer_info_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, +                                        const uint8_t *data, uint16_t length, void *userdata) +{ +    if (length < PACKED_GC_PEER_SIZE) { +        return -1; +    } + +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -3; +    } + +    GC_Connection *gconn = &peer->gconn; + +    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers +            && !peer_is_founder(chat, peer_number)) { +        return -4; +    } + +    uint16_t unpacked_len = 0; + +    if (chat_is_password_protected(chat)) { +        if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { +            return -5; +        } + +        uint16_t password_length; +        net_unpack_u16(data, &password_length); +        unpacked_len += sizeof(uint16_t); + +        if (!validate_password(chat, data + unpacked_len, password_length)) { +            return -5; +        } + +        unpacked_len += MAX_GC_PASSWORD_SIZE; +    } + +    if (length <= unpacked_len) { +        return -1; +    } + +    GC_Peer *peer_info = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + +    if (peer_info == nullptr) { +        return -8; +    } + +    if (unpack_gc_peer(peer_info, data + unpacked_len, length - unpacked_len) == -1) { +        LOGGER_ERROR(chat->log, "unpack_gc_peer() failed"); +        free(peer_info); +        return -6; +    } + +    if (peer_update(chat, peer_info, peer_number) == -1) { +        LOGGER_WARNING(chat->log, "peer_update() failed"); +        free(peer_info); +        return -6; +    } + +    free(peer_info); + +    const bool was_confirmed = gconn->confirmed; +    gconn->confirmed = true; + +    update_gc_peer_roles(chat); + +    add_gc_saved_peers(chat, gconn); + +    set_gc_peerlist_checksum(chat); + +    if (c->peer_join != nullptr && !was_confirmed) { +        c->peer_join(c->messenger, chat->group_number, peer->peer_id, userdata); +    } + +    return 0; +} + +/** @brief Sends the group shared state and its signature to peer_number. + * + * Returns true on success. + */ +non_null() +static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn) +{ +    if (chat->shared_state.version == 0) { +        return false; +    } + +    uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; +    const int length = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + +    if (length != GC_SHARED_STATE_ENC_PACKET_SIZE) { +        return false; +    } + +    return send_lossless_group_packet(chat, gconn, packet, (uint16_t)length, GP_SHARED_STATE); +} + +/** @brief Sends the group shared state and signature to all confirmed peers. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_shared_state(const GC_Chat *chat) +{ +    uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; +    const int packet_len = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + +    if (packet_len != GC_SHARED_STATE_ENC_PACKET_SIZE) { +        return false; +    } + +    send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SHARED_STATE); + +    return true; +} + +/** @brief Helper function for `do_gc_shared_state_changes()`. + * + * If the privacy state has been set to private, we kill our group's connection to the DHT. + * Otherwise, we create a new connection with the DHT and flag an announcement. + */ +non_null(1, 2) nullable(3) +static void do_privacy_state_change(const GC_Session *c, GC_Chat *chat, void *userdata) +{ +    if (is_public_chat(chat)) { +        if (!m_create_group_connection(c->messenger, chat)) { +            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); +        } else { +            chat->update_self_announces = true; +            chat->join_type = HJ_PUBLIC; +        } +    } else { +        kill_group_friend_connection(c, chat); +        cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); +        chat->join_type = HJ_PRIVATE; +    } + +    if (c->privacy_state != nullptr) { +        c->privacy_state(c->messenger, chat->group_number, chat->shared_state.privacy_state, userdata); +    } +} + +/** + * Compares old_shared_state with the chat instance's current shared state and triggers the + * appropriate callbacks depending on what pieces of state information changed. Also + * handles DHT announcement/removal if the privacy state changed. + * + * The initial retrieval of the shared state on group join will be ignored by this function. + */ +non_null(1, 2, 3) nullable(4) +static void do_gc_shared_state_changes(const GC_Session *c, GC_Chat *chat, const GC_SharedState *old_shared_state, +                                       void *userdata) +{ +    /* Max peers changed */ +    if (chat->shared_state.maxpeers != old_shared_state->maxpeers && c->peer_limit != nullptr) { +        c->peer_limit(c->messenger, chat->group_number, chat->shared_state.maxpeers, userdata); +    } + +    /* privacy state changed */ +    if (chat->shared_state.privacy_state != old_shared_state->privacy_state) { +        do_privacy_state_change(c, chat, userdata); +    } + +    /* password changed */ +    if (chat->shared_state.password_length != old_shared_state->password_length +            || memcmp(chat->shared_state.password, old_shared_state->password, old_shared_state->password_length) != 0) { + +        if (c->password != nullptr) { +            c->password(c->messenger, chat->group_number, chat->shared_state.password, +                        chat->shared_state.password_length, userdata); +        } +    } + +    /* topic lock state changed */ +    if (chat->shared_state.topic_lock != old_shared_state->topic_lock && c->topic_lock != nullptr) { +        const Group_Topic_Lock lock_state = group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; +        c->topic_lock(c->messenger, chat->group_number, lock_state, userdata); +    } + +    /* voice state changed */ +    if (chat->shared_state.voice_state != old_shared_state->voice_state && c->voice_state != nullptr) { +        c->voice_state(c->messenger, chat->group_number, chat->shared_state.voice_state, userdata); +    } +} + +/** @brief Sends a sync request to a random peer in the group with the specificed sync flags. + * + * Return true on success. + */ +non_null() +static bool send_gc_random_sync_request(GC_Chat *chat, uint16_t sync_flags) +{ +    GC_Connection *rand_gconn = random_gc_connection(chat); + +    if (rand_gconn == nullptr) { +        return false; +    } + +    return send_gc_sync_request(chat, rand_gconn, sync_flags); +} + +/** @brief Returns true if all shared state values are legal. */ +non_null() +static bool validate_gc_shared_state(const GC_SharedState *state) +{ +    return state->maxpeers > 0 +           && state->password_length <= MAX_GC_PASSWORD_SIZE +           && state->group_name_len > 0 +           && state->group_name_len <= MAX_GC_GROUP_NAME_SIZE +           && state->privacy_state <= GI_PRIVATE +           && state->voice_state <= GV_FOUNDER; +} + +/** @brief Handles a shared state error and attempts to send a sync request to a random peer. + * + * Return 0 if error is currectly handled. + * Return -1 on failure. + */ +non_null() +static int handle_gc_shared_state_error(GC_Chat *chat, GC_Connection *gconn) +{ +    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); + +    if (chat->shared_state.version == 0) { +        chat->connection_state = CS_CONNECTING; +        return 0; +    } + +    if (chat->numpeers <= 1) { +        return 0; +    } + +    if (!send_gc_random_sync_request(chat, GF_STATE)) { +        return -1; +    } + +    return 0; +} + +/** @brief Handles a shared state packet and validates the new shared state. + * + * Return 0 if packet is successfully handled. + * Return -1 if packet is invalid and this is not successfully handled. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_shared_state(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, +                                  uint16_t length, void *userdata) +{ +    if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { +        return handle_gc_shared_state_error(chat, gconn); +    } + +    const uint8_t *signature = data; +    const uint8_t *ss_data = data + SIGNATURE_SIZE; +    const uint16_t ss_length = length - SIGNATURE_SIZE; + +    if (crypto_sign_verify_detached(signature, ss_data, GC_PACKED_SHARED_STATE_SIZE, +                                    get_sig_pk(chat->chat_public_key)) == -1) { +        LOGGER_DEBUG(chat->log, "Failed to validate shared state signature"); +        return handle_gc_shared_state_error(chat, gconn); +    } + +    uint32_t version; +    net_unpack_u32(ss_data, &version);  // version is the first 4 bytes of shared state data payload + +    if (version == 0 || version < chat->shared_state.version) { +        LOGGER_DEBUG(chat->log, "Invalid shared state version (got %u, expected >= %u)", +                     version, chat->shared_state.version); +        return 0; +    } + +    GC_SharedState old_shared_state = chat->shared_state; +    GC_SharedState new_shared_state; + +    if (unpack_gc_shared_state(&new_shared_state, ss_data, ss_length) == 0) { +        LOGGER_WARNING(chat->log, "Failed to unpack shared state"); +        return 0; +    } + +    if (!validate_gc_shared_state(&new_shared_state)) { +        LOGGER_WARNING(chat->log, "Failed to validate shared state"); +        return 0; +    } + +    if (chat->shared_state.version == 0) {  // init founder public sig key in moderation object +        memcpy(chat->moderation.founder_public_sig_key, +               get_sig_pk(new_shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); +    } + +    chat->shared_state = new_shared_state; + +    memcpy(chat->shared_state_sig, signature, sizeof(chat->shared_state_sig)); + +    set_gc_shared_state_version(chat, chat->shared_state.version); + +    do_gc_shared_state_changes(c, chat, &old_shared_state, userdata); + +    return 0; +} + +/** @brief Validates `data` containing a moderation list and unpacks it into the + * shared state of `chat`. + * + * Return 1 if data is valid but mod list doesn't match shared state. + * Return 0 if data is valid. + * Return -1 if data is invalid. + */ +non_null() +static int validate_unpack_mod_list(GC_Chat *chat, const uint8_t *data, uint16_t length, uint16_t num_mods) +{ +    if (num_mods > MOD_MAX_NUM_MODERATORS) { +        return -1; +    } + +    uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE] = {0}; + +    if (length > 0) { +        mod_list_get_data_hash(mod_list_hash, data, length); +    } + +    // we make sure that this mod list's hash matches the one we got in our last shared state update +    if (chat->shared_state.version > 0 +            && memcmp(mod_list_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE) != 0) { +        LOGGER_WARNING(chat->log, "failed to validate mod list hash"); +        return 1; +    } + +    if (mod_list_unpack(&chat->moderation, data, length, num_mods) == -1) { +        LOGGER_WARNING(chat->log, "failed to unpack mod list"); +        return -1; +    } + +    return 0; +} + +/** @brief Handles new mod_list and compares its hash against the mod_list_hash in the shared state. + * + * If the new list fails validation, we attempt to send a sync request to a random peer. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contained invalid data or validation failed. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_mod_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, void *userdata) +{ +    if (length < sizeof(uint16_t)) { +        return -1; +    } + +    // only the founder can modify the list; the founder can never be out of sync +    if (self_gc_is_founder(chat)) { +        return 0; +    } + +    uint16_t num_mods; +    net_unpack_u16(data, &num_mods); + +    const int unpack_ret = validate_unpack_mod_list(chat, data + sizeof(uint16_t), length - sizeof(uint16_t), num_mods); + +    if (unpack_ret == 0) { +        update_gc_peer_roles(chat); + +        if (chat->connection_state == CS_CONNECTED && c->moderation != nullptr) { +            c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_MOD, userdata); +        } + +        return 0; +    } + +    if (unpack_ret == 1) { +        return 0; +    } + +    // unpack/validation failed: handle error + +    if (chat->shared_state.version == 0) { +        chat->connection_state = CS_CONNECTING; +        return -2; +    } + +    if (chat->numpeers <= 1) { +        return 0; +    } + +    send_gc_random_sync_request(chat, GF_STATE); + +    return 0; +} + +/** @brief Handles a sanctions list validation error and attempts to send a sync request to a random peer. + * + * Return 0 on success. + * Return -1 on failure. + */ +non_null() +static int handle_gc_sanctions_list_error(GC_Chat *chat) +{ +    if (chat->moderation.sanctions_creds.version > 0) { +        return 0; +    } + +    if (chat->shared_state.version == 0) { +        chat->connection_state = CS_CONNECTING; +        return 0; +    } + +    if (chat->numpeers <= 1) { +        return 0; +    } + +    if (!send_gc_random_sync_request(chat, GF_STATE)) { +        return -1; +    } + +    return 0; +} + +/** @brief Handles a sanctions list packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to gracefully handle a sanctions list error. + * Return -2 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_sanctions_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, +                                    void *userdata) +{ +    if (length < sizeof(uint16_t)) { +        return -2; +    } + +    uint16_t num_sanctions; +    net_unpack_u16(data, &num_sanctions); + +    if (num_sanctions > MOD_MAX_NUM_SANCTIONS) { +        LOGGER_DEBUG(chat->log, "num_sanctions: %u exceeds maximum", num_sanctions); +        return handle_gc_sanctions_list_error(chat); +    } + +    Mod_Sanction_Creds creds; + +    Mod_Sanction *sanctions = (Mod_Sanction *)calloc(num_sanctions, sizeof(Mod_Sanction)); + +    if (sanctions == nullptr) { +        return -1; +    } + +    const int unpacked_num = sanctions_list_unpack(sanctions, &creds, num_sanctions, data + sizeof(uint16_t), +                             length - sizeof(uint16_t), nullptr); + +    if (unpacked_num != num_sanctions) { +        LOGGER_WARNING(chat->log, "Failed to unpack sanctions list: %d", unpacked_num); +        free(sanctions); +        return handle_gc_sanctions_list_error(chat); +    } + +    if (!sanctions_list_check_integrity(&chat->moderation, &creds, sanctions, num_sanctions)) { +        LOGGER_WARNING(chat->log, "Sanctions list failed integrity check"); +        free(sanctions); +        return handle_gc_sanctions_list_error(chat); +    } + +    if (creds.version < chat->moderation.sanctions_creds.version) { +        free(sanctions); +        return 0; +    } + +    // this may occur if two mods change the sanctions list at the exact same time +    if (creds.version == chat->moderation.sanctions_creds.version +            && creds.checksum <= chat->moderation.sanctions_creds.checksum) { +        free(sanctions); +        return 0; +    } + +    sanctions_list_cleanup(&chat->moderation); + +    chat->moderation.sanctions_creds = creds; +    chat->moderation.sanctions = sanctions; +    chat->moderation.num_sanctions = num_sanctions; + +    update_gc_peer_roles(chat); + +    if (chat->connection_state == CS_CONNECTED) { +        if (c->moderation != nullptr) { +            c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_OBSERVER, userdata); +        } +    } + +    return 0; +} + +/** @brief Makes a mod_list packet. + * + * Returns length of packet data on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_mod_list_packet(const GC_Chat *chat, uint8_t *data, uint32_t maxlen, uint16_t mod_list_size) +{ +    if (maxlen < sizeof(uint16_t) + mod_list_size) { +        return -1; +    } + +    net_pack_u16(data, chat->moderation.num_mods); +    const uint16_t length = sizeof(uint16_t) + mod_list_size; + +    if (mod_list_size > 0) { +        uint8_t *packed_mod_list = (uint8_t *)malloc(mod_list_size); + +        if (packed_mod_list == nullptr) { +            return -1; +        } + +        mod_list_pack(&chat->moderation, packed_mod_list); +        memcpy(data + sizeof(uint16_t), packed_mod_list, mod_list_size); + +        free(packed_mod_list); +    } + +    return length; +} + +/** @brief Sends the moderator list to peer. + * + * Return true on success. + */ +non_null() +static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn) +{ +    const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; +    const uint16_t length = sizeof(uint16_t) + mod_list_size; +    uint8_t *packet = (uint8_t *)malloc(length); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + +    if (packet_len != length) { +        free(packet); +        return false; +    } + +    const bool ret = send_lossless_group_packet(chat, gconn, packet, length, GP_MOD_LIST); + +    free(packet); + +    return ret; +} + +/** @brief Makes a sanctions list packet. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_sanctions_list_packet(const GC_Chat *chat, uint8_t *data, uint16_t maxlen) +{ +    if (maxlen < sizeof(uint16_t)) { +        return -1; +    } + +    net_pack_u16(data, chat->moderation.num_sanctions); +    const uint16_t length = sizeof(uint16_t); + +    const int packed_len = sanctions_list_pack(data + length, maxlen - length, chat->moderation.sanctions, +                           chat->moderation.num_sanctions, &chat->moderation.sanctions_creds); + +    if (packed_len < 0) { +        return -1; +    } + +    return (int)(length + packed_len); +} + +/** @brief Sends the sanctions list to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn) +{ +    if (chat->moderation.sanctions_creds.version == 0) { +        return true; +    } + +    const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + +                                 sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + +    uint8_t *packet = (uint8_t *)malloc(packet_size); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + +    if (packet_len == -1) { +        free(packet); +        return false; +    } + +    const bool ret = send_lossless_group_packet(chat, gconn, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + +    free(packet); + +    return ret; +} + +/** @brief Sends the sanctions list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_sanctions_list(const GC_Chat *chat) +{ +    const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + +                                 sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + +    uint8_t *packet = (uint8_t *)malloc(packet_size); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + +    if (packet_len == -1) { +        free(packet); +        return false; +    } + +    send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + +    free(packet); + +    return true; +} + +/** @brief Re-signs all sanctions list entries signed by public_sig_key and broadcasts + * the updated sanctions list to all group peers. + * + * Returns true on success. + */ +non_null() +static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key) +{ +    const uint16_t num_replaced = sanctions_list_replace_sig(&chat->moderation, public_sig_key); + +    if (num_replaced == 0) { +        return true; +    } + +    return broadcast_gc_sanctions_list(chat); +} + +/** @brief Sends mod_list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_mod_list(const GC_Chat *chat) +{ +    const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; +    const uint16_t length = sizeof(uint16_t) + mod_list_size; +    uint8_t *packet = (uint8_t *)malloc(length); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + +    if (packet_len != length) { +        free(packet); +        return false; +    } + +    send_gc_lossless_packet_all_peers(chat, packet, length, GP_MOD_LIST); + +    free(packet); + +    return true; +} + +/** @brief Sends a parting signal to the group. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the packet failed to send. + */ +non_null(1) nullable(2) +static int send_gc_self_exit(const GC_Chat *chat, const uint8_t *partmessage, uint16_t length) +{ +    if (length > MAX_GC_PART_MESSAGE_SIZE) { +        return -1; +    } + +    if (!send_gc_broadcast_message(chat, partmessage, length, GM_PEER_EXIT)) { +        return -2; +    } + +    return 0; +} + +/** @brief Handles a peer exit broadcast. */ +non_null(1, 2) nullable(3) +static void handle_gc_peer_exit(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    if (length > MAX_GC_PART_MESSAGE_SIZE) { +        length = MAX_GC_PART_MESSAGE_SIZE; +    } + +    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_QUIT, data, length); +} + +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length) +{ +    const GC_Session *c = m->group_handler; +    const GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    if (length > MAX_GC_NICK_SIZE) { +        return -2; +    } + +    if (length == 0 || nick == nullptr) { +        return -3; +    } + +    if (!self_gc_set_nick(chat, nick, length)) { +        return -2; +    } + +    if (!send_gc_broadcast_message(chat, nick, length, GM_NICK)) { +        return -4; +    } + +    return 0; +} + +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return false; +    } + +    if (name != nullptr) { +        memcpy(name, peer->nick, peer->nick_length); +    } + +    return true; +} + +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -1; +    } + +    return peer->nick_length; +} + +/** @brief Handles a nick change broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 on failure. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_nick(const GC_Session *c, GC_Chat *chat, GC_Peer *peer, const uint8_t *nick, +                          uint16_t length,  void *userdata) +{ +    /* If this happens malicious behaviour is highly suspect */ +    if (length == 0 || length > MAX_GC_NICK_SIZE) { +        GC_Connection *gconn = &peer->gconn; +        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); +        LOGGER_WARNING(chat->log, "Invalid nick length for nick: %s (%u)", nick, length); +        return -1; +    } + +    memcpy(peer->nick, nick, length); +    peer->nick_length = length; + +    if (c->nick_change != nullptr) { +        c->nick_change(c->messenger, chat->group_number, peer->peer_id, nick, length, userdata); +    } + +    return 0; +} + +/** @brief Copies peer_number's public key to `public_key`. + * + * Returns 0 on success. + * Returns -1 if peer_number is invalid. + * Returns -2 if `public_key` is null. + */ +non_null() +static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key) +{ +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -1; +    } + +    if (public_key == nullptr) { +        return -2; +    } + +    memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + +    return 0; +} + +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -1; +    } + +    if (public_key == nullptr) { +        return -2; +    } + +    memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + +    return 0; +} + +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    if (peer_number_is_self(peer_number)) {  // we cannot have a connection with ourselves +        return 0; +    } + +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return 0; +    } + +    if (gcc_conn_is_direct(chat->mono_time, gconn)) { +        return 2; +    } + +    return 1; +} + +/** @brief Creates a topic packet and puts it in data. + * + * Packet includes the topic, topic length, public signature key of the + * setter, topic version, and the signature. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_topic_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ +    if (length < SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { +        return -1; +    } + +    memcpy(data, chat->topic_sig, SIGNATURE_SIZE); +    uint16_t data_length = SIGNATURE_SIZE; + +    const uint16_t packed_len = pack_gc_topic_info(data + data_length, length - data_length, &chat->topic_info); +    data_length += packed_len; + +    if (packed_len != chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { +        return -1; +    } + +    return data_length; +} + +/** @brief Sends the group topic to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn) +{ +    const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; +    uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + +    if (packet_len != packet_buf_size) { +        free(packet); +        return false; +    } + +    if (!send_lossless_group_packet(chat, gconn, packet, packet_buf_size, GP_TOPIC)) { +        free(packet); +        return false; +    } + +    free(packet); + +    return true; +} + +/** + * @brief Initiates a session key rotation with peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_peer_key_rotation_request(const GC_Chat *chat, GC_Connection *gconn) +{ +    // Only the peer closest to the chat_id sends requests. This is to prevent both peers from sending +    // requests at the same time and ending up with a different resulting shared key +    if (!gconn->self_is_closer) { +        // if this peer hasn't sent us a rotation request in a reasonable timeframe we drop their connection +        if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT + GC_PING_TIMEOUT)) { +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); +        } + +        return true; +    } + +    uint8_t packet[1 + ENC_PUBLIC_KEY_SIZE]; + +    net_pack_bool(&packet[0], false); // request type + +    create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + +    // copy new session public key to packet +    memcpy(packet + 1, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); + +    if (!send_lossless_group_packet(chat, gconn, packet, sizeof(packet), GP_KEY_ROTATION)) { +        return false; +    } + +    gconn->pending_key_rotation_request = true; + +    return true; +} + +/** @brief Sends the group topic to all group members. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_topic(const GC_Chat *chat) +{ +    const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; +    uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + +    if (packet == nullptr) { +        return false; +    } + +    const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + +    if (packet_len != packet_buf_size) { +        free(packet); +        return false; +    } + +    send_gc_lossless_packet_all_peers(chat, packet, packet_buf_size, GP_TOPIC); + +    free(packet); + +    return true; +} + +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length) +{ +    if (length > MAX_GC_TOPIC_SIZE) { +        return -1; +    } + +    const bool topic_lock_enabled = group_topic_lock_enabled(chat); + +    if (topic_lock_enabled && gc_get_self_role(chat) > GR_MODERATOR) { +        return -2; +    } + +    if (gc_get_self_role(chat) > GR_USER) { +        return -2; +    } + +    const GC_TopicInfo old_topic_info = chat->topic_info; + +    uint8_t old_topic_sig[SIGNATURE_SIZE]; +    memcpy(old_topic_sig, chat->topic_sig, SIGNATURE_SIZE); + +    // TODO(jfreegman): improbable, but an overflow would break topic setting +    if (chat->topic_info.version == UINT32_MAX) { +        return -3; +    } + +    // only increment topic version when lock is enabled +    if (topic_lock_enabled) { +        ++chat->topic_info.version; +    } + +    chat->topic_info.length = length; + +    if (length > 0) { +        assert(topic != nullptr); +        memcpy(chat->topic_info.topic, topic, length); +    } else { +        memset(chat->topic_info.topic, 0, sizeof(chat->topic_info.topic)); +    } + +    memcpy(chat->topic_info.public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + +    chat->topic_info.checksum = get_gc_topic_checksum(&chat->topic_info); + +    const uint16_t packet_buf_size = length + GC_MIN_PACKED_TOPIC_INFO_SIZE; +    uint8_t *packed_topic = (uint8_t *)malloc(packet_buf_size); + +    if (packed_topic == nullptr) { +        return -3; +    } + +    int err = -3; + +    const uint16_t packed_len = pack_gc_topic_info(packed_topic, packet_buf_size, &chat->topic_info); + +    if (packed_len != packet_buf_size) { +        goto ON_ERROR; +    } + +    if (crypto_sign_detached(chat->topic_sig, nullptr, packed_topic, packet_buf_size, +                             get_sig_sk(chat->self_secret_key)) == -1) { +        goto ON_ERROR; +    } + +    if (!broadcast_gc_topic(chat)) { +        err = -4; +        goto ON_ERROR; +    } + +    chat->topic_prev_checksum = old_topic_info.checksum; +    chat->topic_time_set = mono_time_get(chat->mono_time); + +    free(packed_topic); +    return 0; + +ON_ERROR: +    chat->topic_info = old_topic_info; +    memcpy(chat->topic_sig, old_topic_sig, SIGNATURE_SIZE); +    free(packed_topic); +    return err; +} + +void gc_get_topic(const GC_Chat *chat, uint8_t *topic) +{ +    if (topic != nullptr) { +        memcpy(topic, chat->topic_info.topic, chat->topic_info.length); +    } +} + +uint16_t gc_get_topic_size(const GC_Chat *chat) +{ +    return chat->topic_info.length; +} + +/** + * If public_sig_key is equal to the key of the topic setter, replaces topic credentials + * and re-broadcasts the updated topic info to the group. + * + * Returns true on success + */ +non_null() +static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key) +{ +    if (memcmp(public_sig_key, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) != 0) { +        return true; +    } + +    return gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) == 0; +} + +/** @brief Validates `topic_info`. + * + * Return true if topic info is valid. + */ +non_null() +static bool handle_gc_topic_validate(const GC_Chat *chat, const GC_Peer *peer, const GC_TopicInfo *topic_info, +                                     bool topic_lock_enabled) +{ +    if (topic_info->checksum != get_gc_topic_checksum(topic_info)) { +        LOGGER_WARNING(chat->log, "received invalid topic checksum"); +        return false; +    } + +    if (topic_lock_enabled) { +        if (!mod_list_verify_sig_pk(&chat->moderation, topic_info->public_sig_key)) { +            LOGGER_DEBUG(chat->log, "Invalid topic signature (bad credentials)"); +            return false; +        } + +        if (topic_info->version < chat->topic_info.version) { +            return false; +        } +    } else { +        uint8_t public_enc_key[ENC_PUBLIC_KEY_SIZE]; + +        if (gc_get_enc_pk_from_sig_pk(chat, public_enc_key, topic_info->public_sig_key)) { +            if (sanctions_list_is_observer(&chat->moderation, public_enc_key)) { +                LOGGER_DEBUG(chat->log, "Invalid topic signature (sanctioned peer attempted to change topic)"); +                return false; +            } +        } + +        if (topic_info->version == chat->shared_state.topic_lock) { +            // always accept topic on initial connection +            if (!mono_time_is_timeout(chat->mono_time, chat->time_connected, GC_PING_TIMEOUT)) { +                return true; +            } + +            if (chat->topic_prev_checksum == topic_info->checksum && +                    !mono_time_is_timeout(chat->mono_time, chat->topic_time_set, GC_CONFIRMED_PEER_TIMEOUT)) { +                LOGGER_DEBUG(chat->log, "Topic reversion (probable sync error)"); +                return false; +            } + +            return true; +        } + +        // the topic version should never change when the topic lock is disabled except when +        // the founder changes the topic prior to enabling the lock +        if (!(peer->role == GR_FOUNDER && topic_info->version == chat->shared_state.topic_lock + 1)) { +            LOGGER_ERROR(chat->log, "topic version %u differs from topic lock %u", topic_info->version, +                         chat->shared_state.topic_lock); +            return false; +        } +    } + +    return true; +} + +/** @brief Handles a topic packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_topic(const GC_Session *c, GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, +                           uint16_t length, void *userdata) +{ +    if (length < SIGNATURE_SIZE + GC_MIN_PACKED_TOPIC_INFO_SIZE) { +        return -1; +    } + +    const uint16_t old_checksum = chat->topic_info.checksum; + +    GC_TopicInfo topic_info; + +    if (unpack_gc_topic_info(&topic_info, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE) == -1) { +        LOGGER_WARNING(chat->log, "failed to unpack topic"); +        return 0; +    } + +    const uint8_t *signature = data; + +    if (crypto_sign_verify_detached(signature, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE, +                                    topic_info.public_sig_key) == -1) { +        LOGGER_WARNING(chat->log, "failed to verify topic signature"); +        return 0; +    } + +    const bool topic_lock_enabled = group_topic_lock_enabled(chat); + +    if (!handle_gc_topic_validate(chat, peer, &topic_info, topic_lock_enabled)) { +        return 0; +    } + +    // prevents sync issues from triggering the callback needlessly +    const bool skip_callback = chat->topic_info.length == topic_info.length +                               && memcmp(chat->topic_info.topic, topic_info.topic, topic_info.length) == 0; + +    chat->topic_prev_checksum = old_checksum; +    chat->topic_time_set = mono_time_get(chat->mono_time); +    chat->topic_info = topic_info; +    memcpy(chat->topic_sig, signature, SIGNATURE_SIZE); + +    if (!skip_callback && chat->connection_state == CS_CONNECTED && c->topic_change != nullptr) { +        const int setter_peer_number = get_peer_number_of_sig_pk(chat, topic_info.public_sig_key); +        const uint32_t peer_id = setter_peer_number >= 0 ? chat->group[setter_peer_number].peer_id : 0; + +        c->topic_change(c->messenger, chat->group_number, peer_id, topic_info.topic, topic_info.length, userdata); +    } + +    return 0; +} + +/** @brief Handles a key exchange packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if length is invalid. + * Return -2 if we fail to create a new session keypair. + * Return -3 if response packet fails to send. + */ +non_null() +static int handle_gc_key_exchange(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    if (length < 1 + ENC_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    bool is_response; +    net_unpack_bool(&data[0], &is_response); + +    const uint8_t *sender_public_session_key = data + 1; + +    if (is_response) { +        if (!gconn->pending_key_rotation_request) { +            LOGGER_WARNING(chat->log, "got unsolicited key rotation response from peer %u", gconn->public_key_hash); +            return 0; +        } + +        // now that we have response we can compute our new shared key and begin using it +        gcc_make_session_shared_key(gconn, sender_public_session_key); + +        gconn->pending_key_rotation_request = false; + +        return 0; +    } + +    // key generation is pretty cpu intensive so we make sure a peer can't DOS us by spamming requests +    if (!mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT / 2)) { +        return 0; +    } + +    uint8_t response[1 + ENC_PUBLIC_KEY_SIZE]; +    uint8_t new_session_pk[ENC_PUBLIC_KEY_SIZE]; +    uint8_t new_session_sk[ENC_SECRET_KEY_SIZE]; + +    net_pack_bool(&response[0], true); + +    crypto_memlock(new_session_sk, sizeof(new_session_sk)); + +    create_gc_session_keypair(chat->log, chat->rng, new_session_pk, new_session_sk); + +    memcpy(response + 1, new_session_pk, ENC_PUBLIC_KEY_SIZE); + +    if (!send_lossless_group_packet(chat, gconn, response, sizeof(response), GP_KEY_ROTATION)) { +        return -3; +    } + +    // save new keys and compute new shared key AFTER sending response packet with old key +    memcpy(gconn->session_public_key, new_session_pk, sizeof(gconn->session_public_key)); +    memcpy(gconn->session_secret_key, new_session_sk, sizeof(gconn->session_secret_key)); + +    gcc_make_session_shared_key(gconn, sender_public_session_key); + +    crypto_memunlock(new_session_sk, sizeof(new_session_sk)); + +    gconn->last_key_rotation = mono_time_get(chat->mono_time); + +    return 0; +} + +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name) +{ +    if (group_name != nullptr) { +        memcpy(group_name, chat->shared_state.group_name, chat->shared_state.group_name_len); +    } +} + +uint16_t gc_get_group_name_size(const GC_Chat *chat) +{ +    return chat->shared_state.group_name_len; +} + +void gc_get_password(const GC_Chat *chat, uint8_t *password) +{ +    if (password != nullptr) { +        memcpy(password, chat->shared_state.password, chat->shared_state.password_length); +    } +} + +uint16_t gc_get_password_size(const GC_Chat *chat) +{ +    return chat->shared_state.password_length; +} + +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length) +{ +    if (!self_gc_is_founder(chat)) { +        return -1; +    } + +    uint8_t *oldpasswd = nullptr; +    const uint16_t oldlen = chat->shared_state.password_length; + +    if (oldlen > 0) { +        oldpasswd = (uint8_t *)malloc(oldlen); + +        if (oldpasswd == nullptr) { +            return -4; +        } + +        memcpy(oldpasswd, chat->shared_state.password, oldlen); +    } + +    if (!set_gc_password_local(chat, password, password_length)) { +        free(oldpasswd); +        return -2; +    } + +    if (!sign_gc_shared_state(chat)) { +        set_gc_password_local(chat, oldpasswd, oldlen); +        free(oldpasswd); +        return -2; +    } + +    free(oldpasswd); + +    if (!broadcast_gc_shared_state(chat)) { +        return -3; +    } + +    return 0; +} + +/** @brief Validates change to moderator list and either adds or removes peer from our moderator list. + * + * Return target's peer number on success. + * Return -1 on packet handle failure. + * Return -2 if target peer is not online. + * Return -3 if target peer is not a valid role (probably indicates sync issues). + * Return -4 on validation failure. + */ +non_null() +static int validate_unpack_gc_set_mod(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length, +                                      bool add_mod) +{ +    int target_peer_number; +    uint8_t mod_data[MOD_LIST_ENTRY_SIZE]; + +    if (add_mod) { +        if (length < 1 + MOD_LIST_ENTRY_SIZE) { +            return -1; +        } + +        memcpy(mod_data, data + 1, MOD_MODERATION_HASH_SIZE); +        target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + +        if (!gc_peer_number_is_valid(chat, target_peer_number)) { +            return -2; +        } + +        const Group_Role target_role = chat->group[target_peer_number].role; + +        if (target_role != GR_USER) { +            return -3; +        } + +        if (!mod_list_add_entry(&chat->moderation, mod_data)) { +            return -4; +        } +    } else { +        memcpy(mod_data, data + 1, SIG_PUBLIC_KEY_SIZE); +        target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + +        if (!gc_peer_number_is_valid(chat, target_peer_number)) { +            return -2; +        } + +        const Group_Role target_role = chat->group[target_peer_number].role; + +        if (target_role != GR_MODERATOR) { +            return -3; +        } + +        if (!mod_list_remove_entry(&chat->moderation, mod_data)) { +            return -4; +        } +    } + +    update_gc_peer_roles(chat); + +    return target_peer_number; +} + +/** @brief Handles a moderator set broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_mod(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                             uint16_t length, void *userdata) +{ +    if (length < 1 + SIG_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + +    if (setter_peer == nullptr) { +        return -3; +    } + +    if (setter_peer->role != GR_FOUNDER) { +        return 0; +    } + +    bool add_mod; +    net_unpack_bool(&data[0], &add_mod); + +    const int target_peer_number = validate_unpack_gc_set_mod(chat, peer_number, data, length, add_mod); + +    if (target_peer_number == -1) { +        return -2; +    } + +    const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + +    if (target_peer == nullptr) { +        return 0; +    } + +    if (c->moderation != nullptr) { +        c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, +                      add_mod ? MV_MOD : MV_USER, userdata); +    } + +    return 0; +} + +/** @brief Sends a set mod broadcast to the group. + * + * Return true on success. + */ +non_null() +static bool send_gc_set_mod(const GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ +    const uint16_t length = 1 + SIG_PUBLIC_KEY_SIZE; +    uint8_t *data = (uint8_t *)malloc(length); + +    if (data == nullptr) { +        return false; +    } + +    net_pack_bool(&data[0], add_mod); + +    memcpy(data + 1, get_sig_pk(gconn->addr.public_key), SIG_PUBLIC_KEY_SIZE); + +    if (!send_gc_broadcast_message(chat, data, length, GM_SET_MOD)) { +        free(data); +        return false; +    } + +    free(data); + +    return true; +} + +/** + * Adds or removes the peer designated by gconn from moderator list if `add_mod` is true or false respectively. + * Re-signs and re-distributes an updated mod_list hash. + * + * Returns true on success. + */ +non_null() +static bool founder_gc_set_moderator(GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ +    if (!self_gc_is_founder(chat)) { +        return false; +    } + +    if (add_mod) { +        if (chat->moderation.num_mods >= MOD_MAX_NUM_MODERATORS) { +            if (!prune_gc_mod_list(chat)) { +                return false; +            } +        } + +        if (!mod_list_add_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { +            return false; +        } +    } else { +        if (!mod_list_remove_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { +            return false; +        } + +        if (!update_gc_sanctions_list(chat,  get_sig_pk(gconn->addr.public_key)) +                || !update_gc_topic(chat, get_sig_pk(gconn->addr.public_key))) { +            return false; +        } +    } + +    uint8_t old_hash[MOD_MODERATION_HASH_SIZE]; +    memcpy(old_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); + +    if (!mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash)) { +        return false; +    } + +    if (!sign_gc_shared_state(chat) || !broadcast_gc_shared_state(chat)) { +        memcpy(chat->shared_state.mod_list_hash, old_hash, MOD_MODERATION_HASH_SIZE); +        return false; +    } + +    return send_gc_set_mod(chat, gconn, add_mod); +} + +/** @brief Validates `data` containing a change for the sanction list and unpacks it + * into the sanctions list for `chat`. + * + * if `add_obs` is true we're adding an observer to the list. + * + * Return 1 if sanctions list is not modified. + * Return 0 if data is valid and sanctions list is successfully modified. + * Return -1 if data is invalid format. + */ +non_null() +static int validate_unpack_observer_entry(GC_Chat *chat, const uint8_t *data, uint16_t length, +        const uint8_t *public_key, bool add_obs) +{ +    Mod_Sanction_Creds creds; + +    if (add_obs) { +        Mod_Sanction sanction; + +        if (sanctions_list_unpack(&sanction, &creds, 1, data, length, nullptr) != 1) { +            return -1; +        } + +        // this may occur if two mods change the sanctions list at the exact same time +        if (creds.version == chat->moderation.sanctions_creds.version +                && creds.checksum <= chat->moderation.sanctions_creds.checksum) { +            return 1; +        } + +        if (sanctions_list_entry_exists(&chat->moderation, &sanction) +                || !sanctions_list_add_entry(&chat->moderation, &sanction, &creds)) { +            return -1; +        } +    } else { +        if (length < MOD_SANCTIONS_CREDS_SIZE) { +            return -1; +        } + +        if (sanctions_creds_unpack(&creds, data) != MOD_SANCTIONS_CREDS_SIZE) { +            return -1; +        } + +        if (creds.version == chat->moderation.sanctions_creds.version +                && creds.checksum <= chat->moderation.sanctions_creds.checksum) { +            return 1; +        } + +        if (!sanctions_list_is_observer(&chat->moderation, public_key) +                || !sanctions_list_remove_observer(&chat->moderation, public_key, &creds)) { +            return 1; +        } +    } + +    return 0; +} + +/** @brief Handles a set observer broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_observer(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                                  uint16_t length, void *userdata) +{ +    if (length <= 1 + EXT_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + +    if (setter_peer == nullptr) { +        return -3; +    } + +    if (setter_peer->role > GR_MODERATOR) { +        LOGGER_DEBUG(chat->log, "peer with insufficient permissions tried to modify sanctions list"); +        return 0; +    } + +    bool add_obs; +    net_unpack_bool(&data[0], &add_obs); + +    const uint8_t *public_key = data + 1; + +    const int target_peer_number = get_peer_number_of_enc_pk(chat, public_key, false); + +    if (target_peer_number >= 0 && (uint32_t)target_peer_number == peer_number) { +        return -2; +    } + +    const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + +    if (target_peer != nullptr) { +        if ((add_obs && target_peer->role != GR_USER) || (!add_obs && target_peer->role != GR_OBSERVER)) { +            return 0; +        } +    } + +    const int ret = validate_unpack_observer_entry(chat, +                    data + 1 + EXT_PUBLIC_KEY_SIZE, +                    length - 1 - EXT_PUBLIC_KEY_SIZE, +                    public_key, add_obs); + +    if (ret == -1) { +        return -2; +    } + + +    if (ret == 1) { +        return 0; +    } + +    update_gc_peer_roles(chat); + +    if (target_peer != nullptr) { +        if (c->moderation != nullptr) { +            c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, +                          add_obs ? MV_OBSERVER : MV_USER, userdata); +        } +    } + +    return 0; +} + +/** @brief Broadcasts observer role data to the group. + * + * Returns true on success. + */ +non_null() +static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, const uint8_t *sanction_data, +                                 uint16_t length, bool add_obs) +{ +    const uint16_t packet_len = 1 + EXT_PUBLIC_KEY_SIZE + length; +    uint8_t *packet = (uint8_t *)malloc(packet_len); + +    if (packet == nullptr) { +        return false; +    } + +    net_pack_bool(&packet[0], add_obs); + +    memcpy(packet + 1, target_ext_pk, EXT_PUBLIC_KEY_SIZE); +    memcpy(packet + 1 + EXT_PUBLIC_KEY_SIZE, sanction_data, length); + +    if (!send_gc_broadcast_message(chat, packet, packet_len, GM_SET_OBSERVER)) { +        free(packet); +        return false; +    } + +    free(packet); + +    return true; +} + +/** @brief Adds or removes peer_number from the observer list if add_obs is true or false respectively. + * Broadcasts this change to the entire group. + * + * Returns true on success. + */ +non_null() +static bool mod_gc_set_observer(GC_Chat *chat, uint32_t peer_number, bool add_obs) +{ +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    if (gc_get_self_role(chat) >= GR_USER) { +        return false; +    } + +    uint8_t sanction_data[MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_CREDS_SIZE]; +    uint16_t length = 0; + +    if (add_obs) { +        if (chat->moderation.num_sanctions >= MOD_MAX_NUM_SANCTIONS) { +            if (!prune_gc_sanctions_list(chat)) { +                return false; +            } +        } + +        // if sanctioned peer set the topic we need to overwrite his signature and redistribute +        // topic info +        const int setter_peer_number = get_peer_number_of_sig_pk(chat, chat->topic_info.public_sig_key); + +        if (setter_peer_number >= 0 && (uint32_t)setter_peer_number == peer_number) { +            if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { +                return false; +            } +        } + +        Mod_Sanction sanction; + +        if (!sanctions_list_make_entry(&chat->moderation, gconn->addr.public_key, &sanction, SA_OBSERVER)) { +            LOGGER_WARNING(chat->log, "sanctions_list_make_entry failed in mod_gc_set_observer"); +            return false; +        } + +        const int packed_len = sanctions_list_pack(sanction_data, sizeof(sanction_data), &sanction, 1, +                               &chat->moderation.sanctions_creds); + +        if (packed_len == -1) { +            return false; +        } + +        length += packed_len; +    } else { +        if (!sanctions_list_remove_observer(&chat->moderation, gconn->addr.public_key, nullptr)) { +            LOGGER_WARNING(chat->log, "failed to remove sanction"); +            return false; +        } + +        const uint16_t packed_len = sanctions_creds_pack(&chat->moderation.sanctions_creds, sanction_data); + +        if (packed_len != MOD_SANCTIONS_CREDS_SIZE) { +            return false; +        } + +        length += packed_len; +    } + +    if (length > sizeof(sanction_data)) { +        LOGGER_FATAL(chat->log, "Invalid sanction data length: %u", length); +        return false; +    } + +    update_gc_peer_roles(chat); + +    return send_gc_set_observer(chat, gconn->addr.public_key, sanction_data, length, add_obs); +} + +/** @brief Sets the role of `peer_number` to `new_role`. If necessary this function will first + * remove the peer's current role before applying the new one. + * + * Return true on success. + */ +non_null() +static bool apply_new_gc_role(GC_Chat *chat, uint32_t peer_number, Group_Role current_role, Group_Role new_role) +{ +    const GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    switch (current_role) { +        case GR_MODERATOR: { +            if (!founder_gc_set_moderator(chat, gconn, false)) { +                return false; +            } + +            update_gc_peer_roles(chat); + +            if (new_role == GR_OBSERVER) { +                return mod_gc_set_observer(chat, peer_number, true); +            } + +            break; +        } + +        case GR_OBSERVER: { +            if (!mod_gc_set_observer(chat, peer_number, false)) { +                return false; +            } + +            update_gc_peer_roles(chat); + +            if (new_role == GR_MODERATOR) { +                return founder_gc_set_moderator(chat, gconn, true); +            } + +            break; +        } + +        case GR_USER: { +            if (new_role == GR_MODERATOR) { +                return founder_gc_set_moderator(chat, gconn, true); +            } else if (new_role == GR_OBSERVER) { +                return mod_gc_set_observer(chat, peer_number, true); +            } + +            break; +        } + +        case GR_FOUNDER: + +        // Intentional fallthrough +        default: { +            return false; +        } +    } + +    return true; +} + +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role) +{ +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    const GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -2; +    } + +    const GC_Connection *gconn = &peer->gconn; + +    if (!gconn->confirmed) { +        return -2; +    } + +    const Group_Role current_role = peer->role; + +    if (new_role == GR_FOUNDER || peer->role == new_role) { +        return -4; +    } + +    if (peer_number_is_self(peer_number)) { +        return -6; +    } + +    if (current_role == GR_FOUNDER || gc_get_self_role(chat) >= GR_USER) { +        return -3; +    } + +    // moderators can't demote moderators or promote peers to moderator +    if (!self_gc_is_founder(chat) && (new_role == GR_MODERATOR || current_role == GR_MODERATOR)) { +        return -3; +    } + +    if (!apply_new_gc_role(chat, peer_number, current_role, new_role)) { +        return -5; +    } + +    update_gc_peer_roles(chat); + +    return 0; +} + +/** @brief Return true if topic lock is enabled */ +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat) +{ +    return chat->shared_state.topic_lock == GC_TOPIC_LOCK_ENABLED; +} + +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat) +{ +    return chat->shared_state.privacy_state; +} + +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat) +{ +    return group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; +} + +Group_Voice_State gc_get_voice_state(const GC_Chat *chat) +{ +    return chat->shared_state.voice_state; +} + +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state) +{ +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    if (new_lock_state > TL_DISABLED) { +        return -2; +    } + +    if (!self_gc_is_founder(chat)) { +        return -3; +    } + +    if (chat->connection_state <= CS_DISCONNECTED) { +        return -4; +    } + +    const Group_Topic_Lock old_lock_state = gc_get_topic_lock_state(chat); + +    if (new_lock_state == old_lock_state) { +        return 0; +    } + +    const uint32_t old_topic_lock = chat->shared_state.topic_lock; + +    // If we're enabling the lock the founder needs to sign the current topic and re-broadcast +    // it with a new version. This needs to happen before we re-broadcast the shared state because +    // if it fails we don't want to enable the topic lock with an invalid topic signature or version. +    if (new_lock_state == TL_ENABLED) { +        chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; + +        if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { +            chat->shared_state.topic_lock = old_topic_lock; +            return -6; +        } +    } else { +        chat->shared_state.topic_lock = chat->topic_info.version; +    } + +    if (!sign_gc_shared_state(chat)) { +        chat->shared_state.topic_lock = old_topic_lock; +        return -5; +    } + +    if (!broadcast_gc_shared_state(chat)) { +        return -6; +    } + +    return 0; +} + +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state) +{ +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    if (!self_gc_is_founder(chat)) { +        return -2; +    } + +    if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { +        return -3; +    } + +    const Group_Voice_State old_voice_state = chat->shared_state.voice_state; + +    if (new_voice_state == old_voice_state) { +        return 0; +    } + +    chat->shared_state.voice_state = new_voice_state; + +    if (!sign_gc_shared_state(chat)) { +        chat->shared_state.voice_state = old_voice_state; +        return -4; +    } + +    if (!broadcast_gc_shared_state(chat)) { +        return -5; +    } + +    return 0; +} + +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state) +{ +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    if (!self_gc_is_founder(chat)) { +        return -2; +    } + +    if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { +        return -3; +    } + +    const Group_Privacy_State old_privacy_state = chat->shared_state.privacy_state; + +    if (new_privacy_state == old_privacy_state) { +        return 0; +    } + +    chat->shared_state.privacy_state = new_privacy_state; + +    if (!sign_gc_shared_state(chat)) { +        chat->shared_state.privacy_state = old_privacy_state; +        return -4; +    } + +    if (new_privacy_state == GI_PRIVATE) { +        cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); +        kill_group_friend_connection(c, chat); +        chat->join_type = HJ_PRIVATE; +    } else { +        if (!m_create_group_connection(c->messenger, chat)) { +            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); +        } else { +            chat->update_self_announces = true; +            chat->join_type = HJ_PUBLIC; +        } +    } + +    if (!broadcast_gc_shared_state(chat)) { +        return -5; +    } + +    return 0; +} + +uint16_t gc_get_max_peers(const GC_Chat *chat) +{ +    return chat->shared_state.maxpeers; +} + +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers) +{ +    if (!self_gc_is_founder(chat)) { +        return -1; +    } + +    const uint16_t old_maxpeers = chat->shared_state.maxpeers; + +    if (max_peers == chat->shared_state.maxpeers) { +        return 0; +    } + +    chat->shared_state.maxpeers = max_peers; + +    if (!sign_gc_shared_state(chat)) { +        chat->shared_state.maxpeers = old_maxpeers; +        return -2; +    } + +    if (!broadcast_gc_shared_state(chat)) { +        return -3; +    } + +    return 0; +} + +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, uint32_t *message_id) +{ +    if (length > MAX_GC_MESSAGE_SIZE) { +        return -1; +    } + +    if (message == nullptr || length == 0) { +        return -2; +    } + +    if (type != GC_MESSAGE_TYPE_NORMAL && type != GC_MESSAGE_TYPE_ACTION) { +        return -3; +    } + +    const GC_Peer *self = get_gc_peer(chat, 0); +    assert(self != nullptr); + +    if (gc_get_self_role(chat) >= GR_OBSERVER || !peer_has_voice(self, chat->shared_state.voice_state)) { +        return -4; +    } + +    const uint8_t packet_type = type == GC_MESSAGE_TYPE_NORMAL ? GM_PLAIN_MESSAGE : GM_ACTION_MESSAGE; + +    const uint16_t length_raw = length + GC_MESSAGE_PSEUDO_ID_SIZE; +    uint8_t *message_raw = (uint8_t *)malloc(length_raw); + +    if (message_raw == nullptr) { +        return -5; +    } + +    const uint32_t pseudo_msg_id = random_u32(chat->rng); + +    net_pack_u32(message_raw, pseudo_msg_id); +    memcpy(message_raw + GC_MESSAGE_PSEUDO_ID_SIZE, message, length); + +    if (!send_gc_broadcast_message(chat, message_raw, length_raw, packet_type)) { +        free(message_raw); +        return -5; +    } + +    if (message_id != nullptr) { +        *message_id = pseudo_msg_id; +    } + +    free(message_raw); +    return 0; +} + +/** @brief Handles a message broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, +                             uint16_t length, uint8_t type, void *userdata) +{ +    if (data == nullptr || length > MAX_GC_MESSAGE_RAW_SIZE || length <= GC_MESSAGE_PSEUDO_ID_SIZE) { +        return -1; +    } + +    if (peer->ignore || peer->role >= GR_OBSERVER || !peer_has_voice(peer, chat->shared_state.voice_state)) { +        return 0; +    } + +    if (type != GM_PLAIN_MESSAGE && type != GM_ACTION_MESSAGE) { +        LOGGER_WARNING(chat->log, "received invalid message type: %u", type); +        return 0; +    } + +    const uint8_t cb_type = (type == GM_PLAIN_MESSAGE) ? MESSAGE_NORMAL : MESSAGE_ACTION; + +    uint32_t pseudo_msg_id; +    net_unpack_u32(data, &pseudo_msg_id); + +    if (c->message != nullptr) { +        c->message(c->messenger, chat->group_number, peer->peer_id, cb_type, data + GC_MESSAGE_PSEUDO_ID_SIZE, +                   length - GC_MESSAGE_PSEUDO_ID_SIZE, pseudo_msg_id, userdata); +    } + +    return 0; +} + +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, +                            uint16_t length) +{ +    if (length > MAX_GC_MESSAGE_SIZE) { +        return -1; +    } + +    if (message == nullptr || length == 0) { +        return -2; +    } + +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -3; +    } + +    if (type > MESSAGE_ACTION) { +        return -4; +    } + +    if (gc_get_self_role(chat) >= GR_OBSERVER) { +        return -5; +    } + +    uint8_t *message_with_type = (uint8_t *)malloc(length + 1); + +    if (message_with_type == nullptr) { +        return -6; +    } + +    message_with_type[0] = type; +    memcpy(message_with_type + 1, message, length); + +    uint8_t *packet = (uint8_t *)malloc(length + 1 + GC_BROADCAST_ENC_HEADER_SIZE); + +    if (packet == nullptr) { +        free(message_with_type); +        return -6; +    } + +    const uint16_t packet_len = make_gc_broadcast_header(message_with_type, length + 1, packet, GM_PRIVATE_MESSAGE); + +    free(message_with_type); + +    if (!send_lossless_group_packet(chat, gconn, packet, packet_len, GP_BROADCAST)) { +        free(packet); +        return -6; +    } + +    free(packet); + +    return 0; +} + +/** @brief Handles a private message. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_private_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, +                                     uint16_t length, void *userdata) +{ +    if (data == nullptr || length > MAX_GC_MESSAGE_SIZE || length <= 1) { +        return -1; +    } + +    if (peer->ignore || peer->role >= GR_OBSERVER) { +        return 0; +    } + +    const uint8_t message_type = data[0]; + +    if (message_type > MESSAGE_ACTION) { +        LOGGER_WARNING(chat->log, "Received invalid private message type: %u", message_type); +        return 0; +    } + +    if (c->private_message != nullptr) { +        c->private_message(c->messenger, chat->group_number, peer->peer_id, message_type, data + 1, length - 1, userdata); +    } + +    return 0; +} + +/** @brief Returns false if a custom packet is too large. */ +static bool custom_gc_packet_length_is_valid(uint16_t length, bool lossless) +{ +    if (lossless) { +        if (length > MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE) { +            return false; +        } +    } else { +        if (length > MAX_GC_CUSTOM_LOSSY_PACKET_SIZE) { +            return false; +        } +    } + +    return true; +} + +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, +                                  uint16_t length) +{ +    if (!custom_gc_packet_length_is_valid(length, lossless)) { +        return -1; +    } + +    if (message == nullptr || length == 0) { +        return -2; +    } + +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -3; +    } + +    if (gc_get_self_role(chat) >= GR_OBSERVER) { +        return -4; +    } + +    bool ret; + +    if (lossless) { +        ret = send_lossless_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); +    } else { +        ret = send_lossy_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); +    } + +    return ret ? 0 : -5; +} + + + +/** @brief Handles a custom private packet. + * + * @retval 0 if packet is handled correctly. + * @retval -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_custom_private_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, +        const uint8_t *data, uint16_t length, bool lossless, void *userdata) +{ +    if (!custom_gc_packet_length_is_valid(length, lossless)) { +        return -1; +    } + +    if (data == nullptr || length == 0) { +        return -1; +    } + +    if (peer->ignore || peer->role >= GR_OBSERVER) { +        return 0; +    } + +    if (c->custom_private_packet != nullptr) { +        c->custom_private_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); +    } + +    return 0; +} + +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length) +{ +    if (!custom_gc_packet_length_is_valid(length, lossless)) { +        return -1; +    } + +    if (data == nullptr || length == 0) { +        return -2; +    } + +    if (gc_get_self_role(chat) >= GR_OBSERVER) { +        return -3; +    } + +    if (lossless) { +        send_gc_lossless_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); +    } else { +        send_gc_lossy_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); +    } + +    return 0; +} + +/** @brief Handles a custom packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_custom_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, +                                   uint16_t length, bool lossless, void *userdata) +{ +    if (!custom_gc_packet_length_is_valid(length, lossless)) { +        return -1; +    } + +    if (data == nullptr || length == 0) { +        return -1; +    } + +    if (peer->ignore || peer->role >= GR_OBSERVER) { +        return 0; +    } + +    if (c->custom_packet != nullptr) { +        c->custom_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); +    } + +    return 0; +} + +/** @brief Handles a peer kick broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_kick_peer(const GC_Session *c, GC_Chat *chat, const GC_Peer *setter_peer, const uint8_t *data, +                               uint16_t length, void *userdata) +{ +    if (length < ENC_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    if (setter_peer->role >= GR_USER) { +        return 0; +    } + +    const uint8_t *target_pk = data; + +    const int target_peer_number = get_peer_number_of_enc_pk(chat, target_pk, false); +    GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + +    if (target_peer != nullptr) { +        if (target_peer->role != GR_USER) { +            return 0; +        } +    } + +    if (peer_number_is_self(target_peer_number)) { +        assert(target_peer != nullptr); + +        for (uint32_t i = 1; i < chat->numpeers; ++i) { +            GC_Connection *gconn = get_gc_connection(chat, i); +            assert(gconn != nullptr); + +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); +        } + +        chat->connection_state = CS_DISCONNECTED; + +        if (c->moderation != nullptr) { +            c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, +                          MV_KICK, userdata); +        } + +        return 0; +    } + +    if (target_peer == nullptr) {   /** we don't need to/can't kick a peer that isn't in our peerlist */ +        return 0; +    } + +    gcc_mark_for_deletion(&target_peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_KICKED, nullptr, 0); + +    if (c->moderation != nullptr) { +        c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, MV_KICK, userdata); +    } + +    return 0; +} + +/** @brief Sends a packet to instruct all peers to remove gconn from their peerlist. + * + * Returns true on success. + */ +non_null() +static bool send_gc_kick_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ +    uint8_t packet[ENC_PUBLIC_KEY_SIZE]; +    memcpy(packet, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + +    return send_gc_broadcast_message(chat, packet, ENC_PUBLIC_KEY_SIZE, GM_KICK_PEER); +} + +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id) +{ +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    if (peer_number_is_self(peer_number)) { +        return -6; +    } + +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -2; +    } + +    if (gc_get_self_role(chat) >= GR_USER || peer->role == GR_FOUNDER) { +        return -3; +    } + +    if (!self_gc_is_founder(chat) && peer->role == GR_MODERATOR) { +        return -3; +    } + +    if (peer->role == GR_MODERATOR || peer->role == GR_OBSERVER) { +        // this first removes peer from any lists they're on and broadcasts new lists to group +        if (gc_set_peer_role(c->messenger, chat->group_number, peer_id, GR_USER) < 0) { +            return -4; +        } +    } + +    if (!send_gc_kick_peer(chat, &peer->gconn)) { +        return -5; +    } + +    gcc_mark_for_deletion(&peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_NO_CALLBACK, nullptr, 0); + +    return 0; +} + +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type) +{ +    if (gconn->pending_delete) { +        return true; +    } + +    if (type == GR_ACK_REQ) { +        const uint64_t tm = mono_time_get(chat->mono_time); + +        if (gconn->last_requested_packet_time == tm) { +            return true; +        } + +        gconn->last_requested_packet_time = tm; +    } else if (type != GR_ACK_RECV) { +        return false; +    } + +    uint8_t data[GC_LOSSLESS_ACK_PACKET_SIZE]; +    data[0] = (uint8_t) type; +    net_pack_u64(data + 1, message_id); + +    return send_lossy_group_packet(chat, gconn, data, GC_LOSSLESS_ACK_PACKET_SIZE, GP_MESSAGE_ACK); +} + +/** @brief Handles a lossless message acknowledgement. + * + * If the type is GR_ACK_RECV we remove the packet from our + * send array. If the type is GR_ACK_REQ we re-send the packet + * associated with the requested message_id. + * + * Returns 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if we failed to handle the ack (may be caused by connection issues). + * Return -3 if we failed to re-send a requested packet. + */ +non_null() +static int handle_gc_message_ack(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ +    if (length < GC_LOSSLESS_ACK_PACKET_SIZE) { +        return -1; +    } + +    uint64_t message_id; +    net_unpack_u64(data + 1, &message_id); + +    const Group_Message_Ack_Type type = (Group_Message_Ack_Type) data[0]; + +    if (type == GR_ACK_RECV) { +        if (!gcc_handle_ack(chat->log, gconn, message_id)) { +            return -2; +        } + +        return 0; +    } + +    if (type != GR_ACK_REQ) { +        return 0; +    } + +    const uint64_t tm = mono_time_get(chat->mono_time); +    const uint16_t idx = gcc_get_array_index(message_id); + +    /* re-send requested packet */ +    if (gconn->send_array[idx].message_id == message_id) { +        if (gcc_encrypt_and_send_lossless_packet(chat, gconn, gconn->send_array[idx].data, +                gconn->send_array[idx].data_length, +                gconn->send_array[idx].message_id, +                gconn->send_array[idx].packet_type)) { +            gconn->send_array[idx].last_send_try = tm; +            LOGGER_DEBUG(chat->log, "Re-sent requested packet %llu", (unsigned long long)message_id); +        } else { +            return -3; +        } +    } + +    return 0; +} + +/** @brief Sends a handshake response ack to peer. + * + * Return true on success. + */ +non_null() +static bool send_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ +    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_HS_RESPONSE_ACK); +} + +/** @brief Handles a handshake response ack. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to respond with an invite request. + */ +non_null() +static int handle_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ +    gconn->handshaked = true;  // has to be true before we can send a lossless packet + +    if (!send_gc_invite_request(chat, gconn)) { +        gconn->handshaked = false; +        return -1; +    } + +    return 0; +} + +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore) +{ +    const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -1; +    } + +    if (peer_number_is_self(peer_number)) { +        return -2; +    } + +    peer->ignore = ignore; + +    return 0; +} + +/** @brief Handles a broadcast packet. + * + * Returns 0 if packet is handled correctly. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_broadcast(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                               uint16_t length, void *userdata) +{ +    if (length < GC_BROADCAST_ENC_HEADER_SIZE) { +        return -1; +    } + +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return -1; +    } + +    GC_Connection *gconn = &peer->gconn; + +    if (!gconn->confirmed) { +        return -1; +    } + +    const uint8_t broadcast_type = data[0]; + +    const uint16_t m_len = length - 1; +    const uint8_t *message = data + 1; + +    int ret = 0; + +    switch (broadcast_type) { +        case GM_STATUS: { +            ret = handle_gc_status(c, chat, peer, message, m_len, userdata); +            break; +        } + +        case GM_NICK: { +            ret = handle_gc_nick(c, chat, peer, message, m_len, userdata); +            break; +        } + +        case GM_ACTION_MESSAGE: + +        // intentional fallthrough +        case GM_PLAIN_MESSAGE: { +            ret = handle_gc_message(c, chat, peer, message, m_len, broadcast_type, userdata); +            break; +        } + +        case GM_PRIVATE_MESSAGE: { +            ret = handle_gc_private_message(c, chat, peer, message, m_len, userdata); +            break; +        } + +        case GM_PEER_EXIT: { +            handle_gc_peer_exit(chat, gconn, message, m_len); +            ret = 0; +            break; +        } + +        case GM_KICK_PEER: { +            ret = handle_gc_kick_peer(c, chat, peer, message, m_len, userdata); +            break; +        } + +        case GM_SET_MOD: { +            ret = handle_gc_set_mod(c, chat, peer_number, message, m_len, userdata); +            break; +        } + +        case GM_SET_OBSERVER: { +            ret = handle_gc_set_observer(c, chat, peer_number, message, m_len, userdata); +            break; +        } + +        default: { +            LOGGER_DEBUG(chat->log, "Received an invalid broadcast type 0x%02x", broadcast_type); +            break; +        } +    } + +    if (ret < 0) { +        LOGGER_DEBUG(chat->log, "Broadcast handle error %d: type: 0x%02x, peernumber: %u", +                     ret, broadcast_type, peer_number); +        return -1; +    } + +    return 0; +} + +/** @brief Decrypts data of size `length` using self secret key and sender's public key. + * + * The packet payload should begin with a nonce. + * + * Returns length of plaintext data on success. + * Return -1 if length is invalid. + * Return -2 if decryption fails. + */ +non_null() +static int unwrap_group_handshake_packet(const Logger *log, const uint8_t *self_sk, const uint8_t *sender_pk, +        uint8_t *plain, size_t plain_size, const uint8_t *packet, uint16_t length) +{ +    if (length <= CRYPTO_NONCE_SIZE) { +        LOGGER_FATAL(log, "Invalid handshake packet length %u", length); +        return -1; +    } + +    const int plain_len = decrypt_data(sender_pk, self_sk, packet, packet + CRYPTO_NONCE_SIZE, +                                       length - CRYPTO_NONCE_SIZE, plain); + +    if (plain_len < 0 || (uint32_t)plain_len != plain_size) { +        LOGGER_DEBUG(log, "decrypt handshake request failed: len: %d, size: %zu", plain_len, plain_size); +        return -2; +    } + +    return plain_len; +} + +/** @brief Encrypts data of length using the peer's shared key a new nonce. + * + * Adds plaintext header consisting of: packet identifier, target public encryption key, + * self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if packet size is invalid. + * Return -2 on malloc failure. + * Return -3 if encryption fails. + */ +non_null() +static int wrap_group_handshake_packet( +    const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *self_sk, +    const uint8_t *target_pk, uint8_t *packet, uint32_t packet_size, +    const uint8_t *data, uint16_t length) +{ +    if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { +        LOGGER_FATAL(log, "Invalid packet size: %u", packet_size); +        return -1; +    } + +    uint8_t nonce[CRYPTO_NONCE_SIZE]; +    random_nonce(rng, nonce); + +    const size_t encrypt_buf_size = length + CRYPTO_MAC_SIZE; +    uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + +    if (encrypt == nullptr) { +        return -2; +    } + +    const int enc_len = encrypt_data(target_pk, self_sk, nonce, data, length, encrypt); + +    if (enc_len < 0 || (size_t)enc_len != encrypt_buf_size) { +        LOGGER_ERROR(log, "Failed to encrypt group handshake packet (len: %d)", enc_len); +        free(encrypt); +        return -3; +    } + +    packet[0] = NET_PACKET_GC_HANDSHAKE; +    memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); +    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, target_pk, ENC_PUBLIC_KEY_SIZE); +    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); +    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + +    free(encrypt); + +    return 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Makes, wraps and encrypts a group handshake packet (both request and response are the same format). + * + * Packet contains the packet header, handshake type, self public encryption key, self public signature key, + * request type, and a single TCP relay node. + * + * Returns length of encrypted packet on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_handshake_packet(const GC_Chat *chat, const GC_Connection *gconn, uint8_t handshake_type, +                                    uint8_t request_type, uint8_t join_type, uint8_t *packet, size_t packet_size, +                                    const Node_format *node) +{ +    if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { +        LOGGER_FATAL(chat->log, "invalid packet size: %zu", packet_size); +        return -1; +    } + +    if (chat == nullptr || gconn == nullptr || node == nullptr) { +        return -1; +    } + +    uint8_t data[GC_MIN_HS_PACKET_PAYLOAD_SIZE + sizeof(Node_format)]; + +    uint16_t length = sizeof(uint8_t); + +    data[0] = handshake_type; +    memcpy(data + length, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); +    length += ENC_PUBLIC_KEY_SIZE; +    memcpy(data + length, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); +    length += SIG_PUBLIC_KEY_SIZE; +    memcpy(data + length, &request_type, sizeof(uint8_t)); +    length += sizeof(uint8_t); +    memcpy(data + length, &join_type, sizeof(uint8_t)); +    length += sizeof(uint8_t); + +    int nodes_size = pack_nodes(chat->log, data + length, sizeof(Node_format), node, MAX_SENT_GC_NODES); + +    if (nodes_size > 0) { +        length += nodes_size; +    } else { +        nodes_size = 0; +    } + +    const int enc_len = wrap_group_handshake_packet( +                            chat->log, chat->rng, chat->self_public_key, chat->self_secret_key, +                            gconn->addr.public_key, packet, (uint16_t)packet_size, data, length); + +    if (enc_len != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + nodes_size) { +        LOGGER_WARNING(chat->log, "Failed to wrap handshake packet: %d", enc_len); +        return -1; +    } + +    return enc_len; +} + +/** @brief Sends a handshake packet to `gconn`. + * + * Handshake_type should be GH_REQUEST or GH_RESPONSE. + * + * Returns true on success. + */ +non_null() +static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, +                                     uint8_t request_type, uint8_t join_type) +{ +    if (gconn == nullptr) { +        return false; +    } + +    Node_format node; +    memset(&node, 0, sizeof(node)); + +    if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) { +        LOGGER_TRACE(chat->log, "Failed to copy TCP relay during handshake (%u TCP relays)", gconn->tcp_relays_count); +    } + +    uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; +    const int length = make_gc_handshake_packet(chat, gconn, handshake_type, request_type, join_type, packet, +                       sizeof(packet), &node); + +    if (length < 0) { +        return false; +    } + +    const bool try_tcp_fallback = gconn->handshake_attempts % 2 == 1 && gconn->tcp_relays_count > 0; +    ++gconn->handshake_attempts; + +    int ret = -1; + +    if (!try_tcp_fallback && gcc_direct_conn_is_possible(chat, gconn)) { +        ret = sendpacket(chat->net, &gconn->addr.ip_port, packet, (uint16_t)length); +    } + +    if (ret != length && gconn->tcp_relays_count == 0) { +        LOGGER_WARNING(chat->log, "UDP handshake failed and no TCP relays to fall back on"); +        return false; +    } + +    // Send a TCP handshake if UDP fails, or if UDP succeeded last time but we never got a response +    if (gconn->tcp_relays_count > 0 && (ret != length || try_tcp_fallback)) { +        if (send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, (uint16_t)length) == -1) { +            LOGGER_DEBUG(chat->log, "Send handshake packet failed. Type 0x%02x", request_type); +            return false; +        } +    } + +    if (gconn->is_pending_handshake_response) { +        gcc_set_send_message_id(gconn, 3);  // handshake response is always second packet +    }  else { +        gcc_set_send_message_id(gconn, 2);  // handshake request is always first packet +    } + +    return true; +} + +/** @brief Sends an out-of-band TCP handshake request packet to `gconn`. + * + * Return true on success. + */ +static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn) +{ +    if (gconn == nullptr) { +        return false; +    } + +    Node_format node; +    memset(&node, 0, sizeof(node)); + +    if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) { +        LOGGER_WARNING(chat->log, "Failed to copy TCP relay"); +        return false; +    } + +    uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; +    const int length = make_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type, +                       packet, sizeof(packet), &node); + +    if (length < 0) { +        LOGGER_WARNING(chat->log, "Failed to make handshake packet"); +        return false; +    } + +    return tcp_send_oob_packet_using_relay(chat->tcp_conn, gconn->oob_relay_pk, gconn->addr.public_key, +                                           packet, (uint16_t)length) == 0; +} + +/** @brief Handles a handshake response packet and takes appropriate action depending on the value of request_type. + * + * This function assumes the length has already been validated. + * + * Returns peer_number of new connected peer on success. + * Returns -1 on failure. + */ +non_null() +static int handle_gc_handshake_response(const GC_Chat *chat, const uint8_t *sender_pk, const uint8_t *data, +                                        uint16_t length) +{ +    // this should be checked at lower level; this is a redundant defense check. Ideally we should +    // guarantee that this can never happen in the future. +    if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1) { +        LOGGER_FATAL(chat->log, "Invalid handshake response size (%u)", length); +        return -1; +    } + +    const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + +    if (peer_number == -1) { +        return -1; +    } + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -1; +    } + +    const uint8_t *sender_session_pk = data; + +    gcc_make_session_shared_key(gconn, sender_session_pk); + +    set_sig_pk(gconn->addr.public_key, data + ENC_PUBLIC_KEY_SIZE); + +    gcc_set_recv_message_id(gconn, 2);  // handshake response is always second packet + +    gconn->handshaked = true; + +    send_gc_hs_response_ack(chat, gconn); + +    const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + +    switch (request_type) { +        case HS_INVITE_REQUEST: { +            if (!send_gc_invite_request(chat, gconn)) { +                return -1; +            } + +            break; +        } + +        case HS_PEER_INFO_EXCHANGE: { +            if (!send_gc_peer_exchange(chat, gconn)) { +                return -1; +            } + +            break; +        } + +        default: { +            return -1; +        } +    } + +    return peer_number; +} + +/** @brief Sends a handshake response packet of type `request_type` to `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_handshake_response(const GC_Chat *chat, GC_Connection *gconn) +{ +    return send_gc_handshake_packet(chat, gconn, GH_RESPONSE, gconn->pending_handshake_type, 0); +} + +/** @brief Handles handshake request packets. + * + * Peer is added to peerlist and a lossless connection is established. + * + * This function assumes the length has already been validated. + * + * Return new peer's peer_number on success. + * Return -1 on failure. + */ +#define GC_NEW_PEER_CONNECTION_LIMIT 10 +non_null(1, 3, 4) nullable(2) +static int handle_gc_handshake_request(GC_Chat *chat, const IP_Port *ipp, const uint8_t *sender_pk, +                                       const uint8_t *data, uint16_t length) +{ +    // this should be checked at lower level; this is a redundant defense check. Ideally we should +    // guarantee that this can never happen in the future. +    if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) { +        LOGGER_FATAL(chat->log, "Invalid length (%u)", length); +        return -1; +    } + +    if (chat->connection_state <= CS_DISCONNECTED) { +        LOGGER_DEBUG(chat->log, "Handshake request ignored; state is disconnected"); +        return -1; +    } + +    if (chat->connection_O_metre >= GC_NEW_PEER_CONNECTION_LIMIT) { +        chat->block_handshakes = true; +        LOGGER_DEBUG(chat->log, "Handshake overflow. Blocking handshakes."); +        return -1; +    } + +    ++chat->connection_O_metre; + +    const uint8_t *public_sig_key = data + ENC_PUBLIC_KEY_SIZE; + +    const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; +    const uint8_t join_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1]; + +    int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); +    const bool is_new_peer = peer_number < 0; + +    if (is_new_peer) { +        peer_number = peer_add(chat, ipp, sender_pk); + +        if (peer_number < 0) { +            LOGGER_WARNING(chat->log, "Failed to add peer during handshake request"); +            return -1; +        } +    } else  { +        GC_Connection *gconn = get_gc_connection(chat, peer_number); + +        if (gconn == nullptr) { +            LOGGER_WARNING(chat->log, "Invalid peer number"); +            return -1; +        } + +        if (gconn->handshaked) { +            gconn->handshaked = false; +            LOGGER_DEBUG(chat->log, "Handshaked peer sent a handshake request"); +            return -1; +        } + +        // peers sent handshake request at same time so the closer peer becomes the requestor +        // and ignores the request packet while further peer continues on with the response +        if (gconn->self_is_closer) { +            LOGGER_DEBUG(chat->log, "Simultaneous handshake requests; other peer is closer"); +            return 0; +        } +    } + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        LOGGER_DEBUG(chat->log, "Peer connection invalid"); +        return -1; +    } + +    gcc_set_ip_port(gconn, ipp); + +    Node_format node[GCA_MAX_ANNOUNCED_TCP_RELAYS]; +    const int processed = ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1; + +    const int nodes_count = unpack_nodes(node, GCA_MAX_ANNOUNCED_TCP_RELAYS, nullptr, +                                         data + processed, length - processed, true); + +    if (nodes_count <= 0 && ipp == nullptr) { +        if (is_new_peer) { +            LOGGER_WARNING(chat->log, "Broken tcp relay for new peer"); +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +        } + +        return -1; +    } + +    if (nodes_count > 0) { +        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, +                                   &node->ip_port, node->public_key); + +        if (add_tcp_result < 0 && is_new_peer && ipp == nullptr) { +            LOGGER_WARNING(chat->log, "Broken tcp relay for new peer"); +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +            return -1; +        } + +        if (add_tcp_result == 0) { +            gcc_save_tcp_relay(chat->rng, gconn, node); +        } +    } + +    const uint8_t *sender_session_pk = data; + +    gcc_make_session_shared_key(gconn, sender_session_pk); + +    set_sig_pk(gconn->addr.public_key, public_sig_key); + +    if (join_type == HJ_PUBLIC && !is_public_chat(chat)) { +        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +        LOGGER_DEBUG(chat->log, "Ignoring invalid invite request"); +        return -1; +    } + +    gcc_set_recv_message_id(gconn, 1);  // handshake request is always first packet + +    gconn->is_pending_handshake_response = true; +    gconn->pending_handshake_type = request_type; + +    return peer_number; +} + +/** @brief Handles handshake request and handshake response packets. + * + * Returns the peer_number of the connecting peer on success. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(3, 7) +static int handle_gc_handshake_packet(GC_Chat *chat, const uint8_t *sender_pk, const IP_Port *ipp, +                                      const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ +    if (length < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { +        return -1; +    } + +    const size_t data_buf_size = length - CRYPTO_NONCE_SIZE - CRYPTO_MAC_SIZE; +    uint8_t *data = (uint8_t *)malloc(data_buf_size); + +    if (data == nullptr) { +        return -1; +    } + +    const int plain_len = unwrap_group_handshake_packet(chat->log, chat->self_secret_key, sender_pk, data, +                          data_buf_size, packet, length); + +    if (plain_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE)  { +        LOGGER_DEBUG(chat->log, "Failed to unwrap handshake packet (probably a stale request using an old key)"); +        free(data); +        return -1; +    } + +    const uint8_t handshake_type = data[0]; + +    const uint8_t *real_data = data + 1; +    const uint16_t real_len = (uint16_t)plain_len - 1; + +    int peer_number; + +    if (handshake_type == GH_REQUEST) { +        peer_number = handle_gc_handshake_request(chat, ipp, sender_pk, real_data, real_len); +    } else if (handshake_type == GH_RESPONSE) { +        peer_number = handle_gc_handshake_response(chat, sender_pk, real_data, real_len); +    } else { +        free(data); +        return -1; +    } + +    free(data); + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -1; +    } + +    if (direct_conn) { +        gconn->last_received_direct_time = mono_time_get(chat->mono_time); +    } + +    return peer_number; +} + +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                               uint16_t length, uint8_t packet_type, void *userdata) +{ +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return false; +    } + +    GC_Connection *gconn = &peer->gconn; + +    int ret; + +    switch (packet_type) { +        case GP_BROADCAST: { +            ret = handle_gc_broadcast(c, chat, peer_number, data, length, userdata); +            break; +        } + +        case GP_PEER_INFO_REQUEST: { +            ret = handle_gc_peer_info_request(chat, peer_number); +            break; +        } + +        case GP_PEER_INFO_RESPONSE: { +            ret = handle_gc_peer_info_response(c, chat, peer_number, data, length, userdata); +            break; +        } + +        case GP_SYNC_REQUEST: { +            ret = handle_gc_sync_request(chat, peer_number, data, length); +            break; +        } + +        case GP_SYNC_RESPONSE: { +            ret = handle_gc_sync_response(c, chat, peer_number, data, length, userdata); +            break; +        } + +        case GP_INVITE_REQUEST: { +            ret = handle_gc_invite_request(chat, peer_number, data, length); +            break; +        } + +        case GP_INVITE_RESPONSE: { +            ret = handle_gc_invite_response(chat, gconn); +            break; +        } + +        case GP_TOPIC: { +            ret = handle_gc_topic(c, chat, peer, data, length, userdata); +            break; +        } + +        case GP_SHARED_STATE: { +            ret = handle_gc_shared_state(c, chat, gconn, data, length, userdata); +            break; +        } + +        case GP_MOD_LIST: { +            ret = handle_gc_mod_list(c, chat, data, length, userdata); +            break; +        } + +        case GP_SANCTIONS_LIST: { +            ret = handle_gc_sanctions_list(c, chat, data, length, userdata); +            break; +        } + +        case GP_HS_RESPONSE_ACK: { +            ret = handle_gc_hs_response_ack(chat, gconn); +            break; +        } + +        case GP_TCP_RELAYS: { +            ret = handle_gc_tcp_relays(chat, gconn, data, length); +            break; +        } + +        case GP_KEY_ROTATION: { +            ret = handle_gc_key_exchange(chat, gconn, data, length); +            break; +        } + +        case GP_CUSTOM_PACKET: { +            ret = handle_gc_custom_packet(c, chat, peer, data, length, true, userdata); +            break; +        } + +        case GP_CUSTOM_PRIVATE_PACKET: { +            ret = handle_gc_custom_private_packet(c, chat, peer, data, length, true, userdata); +            break; +        } + +        default: { +            LOGGER_DEBUG(chat->log, "Handling invalid lossless group packet type 0x%02x", packet_type); +            return false; +        } +    } + +    if (ret < 0) { +        LOGGER_DEBUG(chat->log, "Lossless packet handle error %d: type: 0x%02x, peernumber: %d", +                     ret, packet_type, peer_number); +        return false; +    } + +    peer = get_gc_peer(chat, peer_number); + +    if (peer != nullptr) { +        peer->gconn.last_requested_packet_time = mono_time_get(chat->mono_time); +    } + +    return true; +} + +/** @brief Handles a packet fragment. + * + * If the fragment is the last one in a sequence we send an ack. Otherwise we + * store the fragment in the receive array and wait for the next segment. + * + * Segments must be processed in correct sequence, and we cannot handle + * non-fragment packets while a sequence is incomplete. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 4) nullable(5, 9) +static bool handle_gc_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, +                                      const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, +                                      void *userdata) +{ +    if (gconn->last_chunk_id != 0 && message_id != gconn->last_chunk_id + 1) { +        return gc_send_message_ack(chat, gconn, gconn->last_chunk_id + 1, GR_ACK_REQ); +    } + +    if (gconn->last_chunk_id == 0 && message_id != gconn->received_message_id + 1) { +        return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); +    } + +    const int frag_ret = gcc_handle_packet_fragment(c, chat, peer_number, gconn, data, length, packet_type, +                         message_id, userdata); + +    if (frag_ret == -1) { +        return false; +    } + +    if (frag_ret == 0) { +        gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); +    } + +    gconn->last_received_packet_time = mono_time_get(chat->mono_time); + +    return true; +} + +/** @brief Handles lossless groupchat packets. + * + * This function assumes the length has already been validated. + * + * Returns true if packet is successfully handled. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossless_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, +                                      const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ +    if (length < GC_MIN_LOSSLESS_PAYLOAD_SIZE) { +        return false; +    } + +    int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    if (gconn->pending_delete) { +        return true; +    } + +    uint8_t *data = (uint8_t *)malloc(length); + +    if (data == nullptr) { +        LOGGER_DEBUG(chat->log, "Failed to allocate memory for packet data buffer"); +        return false; +    } + +    uint8_t packet_type; +    uint64_t message_id; + +    const int len = group_packet_unwrap(chat->log, gconn, data, &message_id, &packet_type, packet, length); + +    if (len < 0) { +        Ip_Ntoa ip_str; +        LOGGER_DEBUG(chat->log, "Failed to unwrap lossless packet from %s:%d: %d", +                     net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len); +        free(data); +        return false; +    } + +    if (!gconn->handshaked && (packet_type != GP_HS_RESPONSE_ACK && packet_type != GP_INVITE_REQUEST)) { +        LOGGER_DEBUG(chat->log, "Got lossless packet type 0x%02x from unconfirmed peer", packet_type); +        free(data); +        return false; +    } + +    const bool is_invite_packet = packet_type == GP_INVITE_REQUEST || packet_type == GP_INVITE_RESPONSE +                                  || packet_type == GP_INVITE_RESPONSE_REJECT; + +    if (message_id == 3 && is_invite_packet && gconn->received_message_id <= 1) { +        // we missed initial handshake request. Drop this packet and wait for another handshake request. +        LOGGER_DEBUG(chat->log, "Missed handshake packet, type: 0x%02x", packet_type); +        free(data); +        return false; +    } + +    const int lossless_ret = gcc_handle_received_message(chat->log, chat->mono_time, gconn, data, (uint16_t) len, +                             packet_type, message_id, direct_conn); + +    if (packet_type == GP_INVITE_REQUEST && !gconn->handshaked) {  // Both peers sent request at same time +        free(data); +        return true; +    } + +    if (lossless_ret < 0) { +        LOGGER_DEBUG(chat->log, "failed to handle packet %llu (type: 0x%02x, id: %llu)", +                     (unsigned long long)message_id, packet_type, (unsigned long long)message_id); +        free(data); +        return false; +    } + +    /* Duplicate packet */ +    if (lossless_ret == 0) { +        free(data); +        return gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); +    } + +    /* request missing packet */ +    if (lossless_ret == 1) { +        LOGGER_TRACE(chat->log, "received out of order packet from peer %u. expected %llu, got %llu", peer_number, +                     (unsigned long long)gconn->received_message_id + 1, (unsigned long long)message_id); +        free(data); +        return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); +    } + +    /* handle packet fragment */ +    if (lossless_ret == 3) { +        const bool frag_ret = handle_gc_packet_fragment(c, chat, peer_number, gconn, data, (uint16_t)len, packet_type, +                              message_id, userdata); +        free(data); +        return frag_ret; +    } + +    const bool ret = handle_gc_lossless_helper(c, chat, peer_number, data, (uint16_t)len, packet_type, userdata); + +    free(data); + +    if (!ret) { +        return false; +    } + +    /* peer number can change from peer add operations in packet handlers */ +    peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); +    gconn = get_gc_connection(chat, peer_number); + +    if (gconn != nullptr && lossless_ret == 2) { +        gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); +    } + +    return true; +} + +/** @brief Handles lossy groupchat message packets. + * + * This function assumes the length has already been validated. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossy_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, +                                   const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ +    if (length < GC_MIN_LOSSY_PAYLOAD_SIZE) { +        return false; +    } + +    const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return false; +    } + +    GC_Connection *gconn = &peer->gconn; + +    if (!gconn->handshaked || gconn->pending_delete) { +        LOGGER_DEBUG(chat->log, "Got lossy packet from invalid peer"); +        return false; +    } + +    uint8_t *data = (uint8_t *)malloc(length); + +    if (data == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); +        return false; +    } + +    uint8_t packet_type; + +    const int len = group_packet_unwrap(chat->log, gconn, data, nullptr, &packet_type, packet, length); + +    if (len <= 0) { +        Ip_Ntoa ip_str; +        LOGGER_DEBUG(chat->log, "Failed to unwrap lossy packet from %s:%d: %d", +                     net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len); +        free(data); +        return false; +    } + +    int ret = -1; +    const uint16_t payload_len = (uint16_t)len; + +    switch (packet_type) { +        case GP_MESSAGE_ACK: { +            ret = handle_gc_message_ack(chat, gconn, data, payload_len); +            break; +        } + +        case GP_PING: { +            ret = handle_gc_ping(chat, gconn, data, payload_len); +            break; +        } + +        case GP_INVITE_RESPONSE_REJECT: { +            ret = handle_gc_invite_response_reject(c, chat, data, payload_len, userdata); +            break; +        } + +        case GP_CUSTOM_PACKET: { +            ret = handle_gc_custom_packet(c, chat, peer, data, payload_len, false, userdata); +            break; +        } + +        case GP_CUSTOM_PRIVATE_PACKET: { +            ret = handle_gc_custom_private_packet(c, chat, peer, data, payload_len, false, userdata); +            break; +        } + +        default: { +            LOGGER_WARNING(chat->log, "Warning: handling invalid lossy group packet type 0x%02x", packet_type); +            free(data); +            return false; +        } +    } + +    free(data); + +    if (ret < 0) { +        LOGGER_DEBUG(chat->log, "Lossy packet handle error %d: type: 0x%02x, peernumber %d", ret, packet_type, +                     peer_number); +        return false; +    } + +    const uint64_t tm = mono_time_get(chat->mono_time); + +    if (direct_conn) { +        gconn->last_received_direct_time = tm; +    } + +    gconn->last_received_packet_time = tm; + +    return true; +} + +/** @brief Return true if group is either connected or attempting to connect. */ +non_null() +static bool group_can_handle_packets(const GC_Chat *chat) +{ +    const GC_Conn_State state = chat->connection_state; +    return state == CS_CONNECTING || state == CS_CONNECTED; +} + +/** @brief Sends a group packet to appropriate handler function. + * + * Returns non-negative value on success. + * Returns -1 on failure. + */ +#define MIN_TCP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 3) nullable(5) +static int handle_gc_tcp_packet(void *object, int id, const uint8_t *packet, uint16_t length, void *userdata) +{ +    const Messenger *m = (Messenger *)object; + +    if (m == nullptr) { +        return -1; +    } + +    if (length <= MIN_TCP_PACKET_SIZE) { +        LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, +                       MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); +        return -1; +    } + +    if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { +        LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, +                       MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); +        return -1; +    } + +    const uint8_t packet_type = packet[0]; + +    const uint8_t *sender_pk = packet + 1; + +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = nullptr; + +    if (packet_type == NET_PACKET_GC_HANDSHAKE) { +        chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); +    } else { +        chat = get_chat_by_id(c, sender_pk); +    } + +    if (chat == nullptr) { +        return -1; +    } + +    if (!group_can_handle_packets(chat)) { +        return -1; +    } + +    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; +    uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; + +    switch (packet_type) { +        case NET_PACKET_GC_LOSSLESS: { +            if (!handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { +                return -1; +            } + +            return 0; +        } + +        case NET_PACKET_GC_LOSSY: { +            if (!handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { +                return -1; +            } + +            return 0; +        } + +        case NET_PACKET_GC_HANDSHAKE: { +            // handshake packets have an extra public key in plaintext header +            if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { +                return -1; +            } + +            payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; +            payload = payload + ENC_PUBLIC_KEY_SIZE; + +            return handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata); +        } + +        default: { +            return -1; +        } +    } +} + +non_null(1, 2, 4) nullable(6) +static int handle_gc_tcp_oob_packet(void *object, const uint8_t *public_key, unsigned int tcp_connections_number, +                                    const uint8_t *packet, uint16_t length, void *userdata) +{ +    const Messenger *m = (Messenger *)object; + +    if (m == nullptr) { +        return -1; +    } + +    if (length <= GC_MIN_HS_PACKET_PAYLOAD_SIZE) { +        LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, +                       GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); +        return -1; +    } + +    if (length > MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { +        LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, +                       GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); +        return -1; +    } + +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + +    if (chat == nullptr) { +        return -1; +    } + +    if (!group_can_handle_packets(chat)) { +        return -1; +    } + +    const uint8_t packet_type = packet[0]; + +    if (packet_type != NET_PACKET_GC_HANDSHAKE) { +        return -1; +    } + +    const uint8_t *sender_pk = packet + 1; + +    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE; +    const uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE - ENC_PUBLIC_KEY_SIZE; + +    if (payload_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { +        return -1; +    } + +    if (handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata) == -1) { +        return -1; +    } + +    return 0; +} + +#define MIN_UDP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 2, 3) nullable(5) +static int handle_gc_udp_packet(void *object, const IP_Port *ipp, const uint8_t *packet, uint16_t length, +                                void *userdata) +{ +    const Messenger *m = (Messenger *)object; + +    if (m == nullptr) { +        return -1; +    } + +    if (length <= MIN_UDP_PACKET_SIZE) { +        LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, +                       MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); +        return -1; +    } + +    if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { +        LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, +                       MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); +        return -1; +    } + +    const uint8_t packet_type = packet[0]; +    const uint8_t *sender_pk = packet + 1; + +    const GC_Session *c = m->group_handler; +    GC_Chat *chat = nullptr; + +    if (packet_type == NET_PACKET_GC_HANDSHAKE) { +        chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); +    } else { +        chat = get_chat_by_id(c, sender_pk); +    } + +    if (chat == nullptr) { +        return -1; +    } + +    if (!group_can_handle_packets(chat)) { +        return -1; +    } + +    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; +    uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; +    bool ret = false; + +    switch (packet_type) { +        case NET_PACKET_GC_LOSSLESS: { +            ret = handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, true, userdata); +            break; +        } + +        case NET_PACKET_GC_LOSSY: { +            ret = handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, true, userdata); +            break; +        } + +        case NET_PACKET_GC_HANDSHAKE: { +            // handshake packets have an extra public key in plaintext header +            if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { +                return -1; +            } + +            payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; +            payload = payload + ENC_PUBLIC_KEY_SIZE; + +            ret = handle_gc_handshake_packet(chat, sender_pk, ipp, payload, payload_len, true, userdata) != -1; +            break; +        } + +        default: { +            return -1; +        } +    } + +    return ret ? 0 : -1; +} + +void gc_callback_message(const Messenger *m, gc_message_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->message = function; +} + +void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->private_message = function; +} + +void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->custom_packet = function; +} + +void gc_callback_custom_private_packet(const Messenger *m, gc_custom_private_packet_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->custom_private_packet = function; +} + +void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->moderation = function; +} + +void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->nick_change = function; +} + +void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->status_change = function; +} + +void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->topic_change = function; +} + +void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->topic_lock = function; +} + +void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->voice_state = function; +} + +void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->peer_limit = function; +} + +void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->privacy_state = function; +} + +void gc_callback_password(const Messenger *m, gc_password_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->password = function; +} + +void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->peer_join = function; +} + +void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->peer_exit = function; +} + +void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->self_join = function; +} + +void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function) +{ +    GC_Session *c = m->group_handler; +    c->rejected = function; +} + +/** @brief Deletes peer_number from group. + * + * `no_callback` should be set to true if the `peer_exit` callback + * should not be triggered. + * + * Return true on success. + */ +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata) +{ +    GC_Peer *peer = get_gc_peer(chat, peer_number); + +    if (peer == nullptr) { +        return false; +    } + +    // We need to save some peer info for the callback before deleting it +    const bool peer_confirmed = peer->gconn.confirmed; +    const uint32_t peer_id = peer->peer_id; +    uint8_t nick[MAX_GC_NICK_SIZE]; +    const uint16_t nick_length = peer->nick_length; +    const GC_Exit_Info exit_info = peer->gconn.exit_info; + +    assert(nick_length <= MAX_GC_NICK_SIZE); +    memcpy(nick, peer->nick, nick_length); + +    gcc_peer_cleanup(&peer->gconn); + +    --chat->numpeers; + +    if (chat->numpeers != peer_number) { +        chat->group[peer_number] = chat->group[chat->numpeers]; +    } + +    chat->group[chat->numpeers] = (GC_Peer) { +        0 +    }; + +    GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, chat->numpeers * sizeof(GC_Peer)); + +    if (tmp_group == nullptr) { +        return false; +    } + +    chat->group = tmp_group; + +    set_gc_peerlist_checksum(chat); + +    if (peer_confirmed) { +        refresh_gc_saved_peers(chat); +    } + +    if (exit_info.exit_type != GC_EXIT_TYPE_NO_CALLBACK && c->peer_exit != nullptr && peer_confirmed) { +        c->peer_exit(c->messenger, chat->group_number, peer_id, exit_info.exit_type, nick, +                     nick_length, exit_info.part_message, exit_info.length, userdata); +    } + +    return true; +} + +/** @brief Updates peer_number with info from `peer` and validates peer data. + * + * Returns peer_number on success. + * Returns -1 on failure. + */ +static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number) +{ +    if (peer->nick_length == 0) { +        return -1; +    } + +    if (peer->status > GS_BUSY) { +        return -1; +    } + +    if (peer->role > GR_OBSERVER) { +        return -1; +    } + +    GC_Peer *curr_peer = get_gc_peer(chat, peer_number); +    assert(curr_peer != nullptr); + +    curr_peer->status = peer->status; +    curr_peer->nick_length = peer->nick_length; + +    memcpy(curr_peer->nick, peer->nick, peer->nick_length); + +    return peer_number; +} + +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key) +{ +    if (get_peer_number_of_enc_pk(chat, public_key, false) != -1) { +        return -2; +    } + +    const uint32_t peer_id = get_new_peer_id(chat); + +    if (peer_id == UINT32_MAX) { +        LOGGER_WARNING(chat->log, "Failed to add peer: all peer ID's are taken?"); +        return -1; +    } + +    const int peer_number = chat->numpeers; +    int tcp_connection_num = -1; + +    if (peer_number > 0) {  // we don't need a connection to ourself +        tcp_connection_num = new_tcp_connection_to(chat->tcp_conn, public_key, 0); + +        if (tcp_connection_num == -1) { +            LOGGER_WARNING(chat->log, "Failed to init tcp connection for peer %d", peer_number); +        } +    } + +    GC_Message_Array_Entry *send = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); +    GC_Message_Array_Entry *recv = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); + +    if (send == nullptr || recv == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for gconn buffers"); + +        if (tcp_connection_num != -1) { +            kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); +        } + +        free(send); +        free(recv); +        return -1; +    } + +    GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, (chat->numpeers + 1) * sizeof(GC_Peer)); + +    if (tmp_group == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for group realloc"); + +        if (tcp_connection_num != -1) { +            kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); +        } + +        free(send); +        free(recv); +        return -1; +    } + +    ++chat->numpeers; +    chat->group = tmp_group; + +    chat->group[peer_number] = (GC_Peer) { +        0 +    }; + +    GC_Connection *gconn = &chat->group[peer_number].gconn; + +    gconn->send_array = send; +    gconn->recv_array = recv; + +    gcc_set_ip_port(gconn, ipp); +    chat->group[peer_number].role = GR_USER; +    chat->group[peer_number].peer_id = peer_id; +    chat->group[peer_number].ignore = false; + +    crypto_memlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); + +    create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + +    if (peer_number > 0) { +        memcpy(gconn->addr.public_key, public_key, ENC_PUBLIC_KEY_SIZE);  // we get the sig key in the handshake +    } else { +        memcpy(gconn->addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); +    } + +    const uint64_t tm = mono_time_get(chat->mono_time); + +    gcc_set_send_message_id(gconn, 1); +    gconn->public_key_hash = gc_get_pk_jenkins_hash(public_key); +    gconn->last_received_packet_time = tm; +    gconn->last_key_rotation = tm; +    gconn->tcp_connection_num = tcp_connection_num; +    gconn->last_sent_ip_time = tm; +    gconn->last_sent_ping_time = tm - (GC_PING_TIMEOUT / 2) + (peer_number % (GC_PING_TIMEOUT / 2)); +    gconn->self_is_closer = id_closest(get_chat_id(chat->chat_public_key), +                                       get_enc_key(chat->self_public_key), +                                       get_enc_key(gconn->addr.public_key)) == 1; +    return peer_number; +} + +/** @brief Copies own peer data to `peer`. */ +non_null() +static void copy_self(const GC_Chat *chat, GC_Peer *peer) +{ +    *peer = (GC_Peer) { +        0 +    }; + +    peer->status = gc_get_self_status(chat); +    gc_get_self_nick(chat, peer->nick); +    peer->nick_length = gc_get_self_nick_size(chat); +    peer->role = gc_get_self_role(chat); +} + +/** @brief Returns true if we haven't received a ping from this peer after n seconds. + * n depends on whether or not the peer has been confirmed. + */ +non_null() +static bool peer_timed_out(const Mono_Time *mono_time, const GC_Connection *gconn) +{ +    return mono_time_is_timeout(mono_time, gconn->last_received_packet_time, gconn->confirmed +                                ? GC_CONFIRMED_PEER_TIMEOUT +                                : GC_UNCONFIRMED_PEER_TIMEOUT); +} + +/** @brief Attempts to send pending handshake packets to peer designated by `gconn`. + * + * One request of each type can be sent per `GC_SEND_HANDSHAKE_INTERVAL` seconds. + * + * Return true on success. + */ +non_null() +static bool send_pending_handshake(const GC_Chat *chat, GC_Connection *gconn) +{ +    if (chat == nullptr || gconn == nullptr) { +        return false; +    } + +    if (gconn->is_pending_handshake_response) { +        if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_response, GC_SEND_HANDSHAKE_INTERVAL)) { +            return true; +        } + +        gconn->last_handshake_response = mono_time_get(chat->mono_time); + +        return send_gc_handshake_response(chat, gconn); +    } + +    if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_request, GC_SEND_HANDSHAKE_INTERVAL)) { +        return true; +    } + +    gconn->last_handshake_request = mono_time_get(chat->mono_time); + +    if (gconn->is_oob_handshake) { +        return send_gc_oob_handshake_request(chat, gconn); +    } + +    return send_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type); +} + +#define GC_TCP_RELAY_SEND_INTERVAL (60 * 3) +non_null(1, 2) nullable(3) +static void do_peer_connections(const GC_Session *c, GC_Chat *chat, void *userdata) +{ +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        if (gconn->pending_delete) { +            continue; +        } + +        if (peer_timed_out(chat->mono_time, gconn)) { +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); +            continue; +        } + +        gcc_resend_packets(chat, gconn); + +        if (gconn->tcp_relays_count > 0 && +                mono_time_is_timeout(chat->mono_time, gconn->last_sent_tcp_relays_time, GC_TCP_RELAY_SEND_INTERVAL)) { +            if (gconn->confirmed) { +                send_gc_tcp_relays(chat, gconn); +                gconn->last_sent_tcp_relays_time = mono_time_get(chat->mono_time); +            } +        } + +        gcc_check_recv_array(c, chat, gconn, i, userdata);   // may change peer numbers +    } +} + +/** @brief Executes pending handshakes for peers. + * + * If our peerlist is empty we periodically try to + * load peers from our saved peers list and initiate handshake requests with them. + */ +#define LOAD_PEERS_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT + 10) +non_null() +static void do_handshakes(GC_Chat *chat) +{ +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        if (gconn->handshaked || gconn->pending_delete) { +            continue; +        } + +        send_pending_handshake(chat, gconn); +    } + +    if (chat->numpeers <= 1) { +        const uint64_t tm = mono_time_get(chat->mono_time); + +        if (mono_time_is_timeout(chat->mono_time, chat->last_time_peers_loaded, LOAD_PEERS_TIMEOUT)) { +            load_gc_peers(chat, chat->saved_peers, GC_MAX_SAVED_PEERS); +            chat->last_time_peers_loaded = tm; +        } +    } +} + +/** @brief Adds `gconn` to the group timeout list. */ +non_null() +static void add_gc_peer_timeout_list(GC_Chat *chat, const GC_Connection *gconn) +{ +    const size_t idx = chat->timeout_list_index; +    const uint64_t tm = mono_time_get(chat->mono_time); + +    copy_gc_saved_peer(chat->rng, gconn, &chat->timeout_list[idx].addr); + +    chat->timeout_list[idx].last_seen = tm; +    chat->timeout_list[idx].last_reconn_try = 0; +    chat->timeout_list_index = (idx + 1) % MAX_GC_SAVED_TIMEOUTS; +} + +non_null(1, 2) nullable(3) +static void do_peer_delete(const GC_Session *c, GC_Chat *chat, void *userdata) +{ +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        if (gconn->pending_delete) { +            const GC_Exit_Info *exit_info = &gconn->exit_info; + +            if (exit_info->exit_type == GC_EXIT_TYPE_TIMEOUT && gconn->confirmed) { +                add_gc_peer_timeout_list(chat, gconn); +            } + +            if (!peer_delete(c, chat, i, userdata)) { +                LOGGER_ERROR(chat->log, "Failed to delete peer %u", i); +            } + +            if (i >= chat->numpeers) { +                break; +            } +        } +    } +} + +/** @brief Constructs and sends a ping packet to `gconn` containing info needed for group syncing + * and connection maintenance. + * + * Return true on success. + */ +non_null() +static bool ping_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ +    const uint16_t buf_size = GC_PING_PACKET_MIN_DATA_SIZE + sizeof(IP_Port); +    uint8_t *data = (uint8_t *)malloc(buf_size); + +    if (data == nullptr) { +        return false; +    } + +    const uint16_t roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; +    uint16_t packed_len = 0; + +    net_pack_u16(data, chat->peers_checksum); +    packed_len += sizeof(uint16_t); + +    net_pack_u16(data + packed_len, get_gc_confirmed_numpeers(chat)); +    packed_len += sizeof(uint16_t); + +    net_pack_u32(data + packed_len, chat->shared_state.version); +    packed_len += sizeof(uint32_t); + +    net_pack_u32(data + packed_len, chat->moderation.sanctions_creds.version); +    packed_len += sizeof(uint32_t); + +    net_pack_u16(data + packed_len, roles_checksum); +    packed_len += sizeof(uint16_t); + +    net_pack_u32(data + packed_len, chat->topic_info.version); +    packed_len += sizeof(uint32_t); + +    net_pack_u16(data + packed_len, chat->topic_info.checksum); +    packed_len += sizeof(uint16_t); + +    if (packed_len != GC_PING_PACKET_MIN_DATA_SIZE) { +        LOGGER_FATAL(chat->log, "Packed length is impossible"); +    } + +    if (chat->self_udp_status == SELF_UDP_STATUS_WAN && !gcc_conn_is_direct(chat->mono_time, gconn) +            && mono_time_is_timeout(chat->mono_time, gconn->last_sent_ip_time, GC_SEND_IP_PORT_INTERVAL)) { + +        const int packed_ipp_len = pack_ip_port(chat->log, data + buf_size - sizeof(IP_Port), sizeof(IP_Port), +                                                &chat->self_ip_port); + +        if (packed_ipp_len > 0) { +            packed_len += packed_ipp_len; +        } +    } + +    if (!send_lossy_group_packet(chat, gconn, data, packed_len, GP_PING)) { +        free(data); +        return true; +    } + +    free(data); + +    return false; +} + +/** + * Sends a ping packet to peers that haven't been pinged in at least GC_PING_TIMEOUT seconds, and + * a key rotation request to peers with whom we haven't refreshed keys in at least GC_KEY_ROTATION_TIMEOUT + * seconds. + * + * Ping packet always includes your confirmed peer count, a peer list checksum, your shared state and sanctions + * list version for syncing purposes. We also occasionally try to send our own IP info to peers that we + * do not have a direct connection with. + */ +#define GC_DO_PINGS_INTERVAL 2 +non_null() +static void do_gc_ping_and_key_rotation(GC_Chat *chat) +{ +    if (!mono_time_is_timeout(chat->mono_time, chat->last_ping_interval, GC_DO_PINGS_INTERVAL)) { +        return; +    } + +    const uint64_t tm = mono_time_get(chat->mono_time); + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        if (!gconn->confirmed) { +            continue; +        } + +        if (mono_time_is_timeout(chat->mono_time, gconn->last_sent_ping_time, GC_PING_TIMEOUT)) { +            if (ping_peer(chat, gconn)) { +                gconn->last_sent_ping_time = tm; +            } +        } + +        if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT)) { +            if (send_peer_key_rotation_request(chat, gconn)) { +                gconn->last_key_rotation = tm; +            } +        } +    } + +    chat->last_ping_interval = tm; +} + +non_null() +static void do_new_connection_cooldown(GC_Chat *chat) +{ +    if (chat->connection_O_metre == 0) { +        return; +    } + +    const uint64_t tm = mono_time_get(chat->mono_time); + +    if (chat->connection_cooldown_timer < tm) { +        chat->connection_cooldown_timer = tm; +        --chat->connection_O_metre; + +        if (chat->connection_O_metre == 0 && chat->block_handshakes) { +            chat->block_handshakes = false; +            LOGGER_DEBUG(chat->log, "Unblocking handshakes"); +        } +    } +} + +#define TCP_RELAYS_CHECK_INTERVAL 10 +non_null(1, 2) nullable(3) +static void do_gc_tcp(const GC_Session *c, GC_Chat *chat, void *userdata) +{ +    if (chat->tcp_conn == nullptr || !group_can_handle_packets(chat)) { +        return; +    } + +    do_tcp_connections(chat->log, chat->tcp_conn, userdata); + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        const GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        const bool tcp_set = !gcc_conn_is_direct(chat->mono_time, gconn); +        set_tcp_connection_to_status(chat->tcp_conn, gconn->tcp_connection_num, tcp_set); +    } + +    if (mono_time_is_timeout(chat->mono_time, chat->last_checked_tcp_relays, TCP_RELAYS_CHECK_INTERVAL) +            && tcp_connected_relays_count(chat->tcp_conn) != chat->connected_tcp_relays) { +        add_tcp_relays_to_chat(c, chat); +        chat->connected_tcp_relays = tcp_connected_relays_count(chat->tcp_conn); +        chat->last_checked_tcp_relays = mono_time_get(chat->mono_time); +    } +} + +/** + * Updates our TCP and UDP connection status and flags a new announcement if our connection has + * changed and we have either a UDP or TCP connection. + */ +#define GC_SELF_CONNECTION_CHECK_INTERVAL 5  // how often in seconds we should run this function +#define GC_SELF_REFRESH_ANNOUNCE_INTERVAL (60 * 20)  // how often in seconds we force refresh our group announcement +non_null() +static void do_self_connection(const GC_Session *c, GC_Chat *chat) +{ +    if (!mono_time_is_timeout(chat->mono_time, chat->last_self_announce_check, GC_SELF_CONNECTION_CHECK_INTERVAL)) { +        return; +    } + +    const unsigned int self_udp_status = ipport_self_copy(c->messenger->dht, &chat->self_ip_port); +    const bool udp_change = (chat->self_udp_status != self_udp_status) && (self_udp_status != SELF_UDP_STATUS_NONE); + +    // We flag a group announce if our UDP status has changed since last run, or if our last announced TCP +    // relay is no longer valid. Additionally, we will always flag an announce in the specified interval +    // regardless of the prior conditions. Private groups are never announced. +    if (is_public_chat(chat) && +            ((udp_change || !tcp_relay_is_valid(chat->tcp_conn, chat->announced_tcp_relay_pk)) +             || mono_time_is_timeout(chat->mono_time, chat->last_time_self_announce, GC_SELF_REFRESH_ANNOUNCE_INTERVAL))) { +        chat->update_self_announces = true; +    } + +    chat->self_udp_status = (Self_UDP_Status) self_udp_status; +    chat->last_self_announce_check = mono_time_get(chat->mono_time); +} + +/** @brief Attempts to initiate a new connection with peers in the timeout list. + * + * This function is not used for public groups as the DHT and group sync mechanism + * should automatically do this for us. + */ +#define TIMED_OUT_RECONN_INTERVAL 2 +non_null() +static void do_timed_out_reconn(GC_Chat *chat) +{ +    if (is_public_chat(chat)) { +        return; +    } + +    if (!mono_time_is_timeout(chat->mono_time, chat->last_timed_out_reconn_try, TIMED_OUT_RECONN_INTERVAL)) { +        return; +    } + +    const uint64_t curr_time = mono_time_get(chat->mono_time); + +    for (size_t i = 0; i < MAX_GC_SAVED_TIMEOUTS; ++i) { +        GC_TimedOutPeer *timeout = &chat->timeout_list[i]; + +        if (timeout->last_seen == 0 || timeout->last_seen == curr_time) { +            continue; +        } + +        if (mono_time_is_timeout(chat->mono_time, timeout->last_seen, GC_TIMED_OUT_STALE_TIMEOUT) +                || get_peer_number_of_enc_pk(chat, timeout->addr.public_key, true) != -1) { +            *timeout = (GC_TimedOutPeer) { +                {{ +                        0 +                    } +                } +            }; +            continue; +        } + +        if (mono_time_is_timeout(chat->mono_time, timeout->last_reconn_try, GC_TIMED_OUT_RECONN_TIMEOUT)) { +            if (load_gc_peers(chat, &timeout->addr, 1) != 1) { +                LOGGER_WARNING(chat->log, "Failed to load timed out peer"); +            } + +            timeout->last_reconn_try = curr_time; +        } +    } + +    chat->last_timed_out_reconn_try = curr_time; +} + +void do_gc(GC_Session *c, void *userdata) +{ +    if (c == nullptr) { +        return; +    } + +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        GC_Chat *chat = &c->chats[i]; + +        const GC_Conn_State state = chat->connection_state; + +        if (state == CS_NONE) { +            continue; +        } + +        if (state != CS_DISCONNECTED) { +            do_peer_connections(c, chat, userdata); +            do_gc_tcp(c, chat, userdata); +            do_handshakes(chat); +            do_self_connection(c, chat); +        } + +        if (chat->connection_state == CS_CONNECTED) { +            do_gc_ping_and_key_rotation(chat); +            do_timed_out_reconn(chat); +        } + +        do_new_connection_cooldown(chat); +        do_peer_delete(c, chat, userdata); +    } +} + +/** @brief Set the size of the groupchat list to n. + * + * Return true on success. + */ +non_null() +static bool realloc_groupchats(GC_Session *c, uint32_t n) +{ +    if (n == 0) { +        free(c->chats); +        c->chats = nullptr; +        return true; +    } + +    GC_Chat *temp = (GC_Chat *)realloc(c->chats, n * sizeof(GC_Chat)); + +    if (temp == nullptr) { +        return false; +    } + +    c->chats = temp; +    return true; +} + +non_null() +static int get_new_group_index(GC_Session *c) +{ +    if (c == nullptr) { +        return -1; +    } + +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        if (c->chats[i].connection_state == CS_NONE) { +            return i; +        } +    } + +    if (!realloc_groupchats(c, c->chats_index + 1)) { +        return -1; +    } + +    const int new_index = c->chats_index; + +    c->chats[new_index] = empty_gc_chat; + +    memset(&c->chats[new_index].saved_invites, -1, sizeof(c->chats[new_index].saved_invites)); + +    ++c->chats_index; + +    return new_index; +} + +/** Attempts to associate new TCP relays with our group connection. */ +static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat) +{ +    const Messenger *m = c->messenger; + +    const uint32_t num_relays = tcp_connections_count(nc_get_tcp_c(m->net_crypto)); + +    if (num_relays == 0) { +        return; +    } + +    Node_format *tcp_relays = (Node_format *)calloc(num_relays, sizeof(Node_format)); + +    if (tcp_relays == nullptr) { +        return; +    } + +    const uint32_t num_copied = tcp_copy_connected_relays(nc_get_tcp_c(m->net_crypto), tcp_relays, (uint16_t)num_relays); + +    for (uint32_t i = 0; i < num_copied; ++i) { +        add_tcp_relay_global(chat->tcp_conn, &tcp_relays[i].ip_port, tcp_relays[i].public_key); +    } + +    free(tcp_relays); +} + +non_null() +static bool init_gc_tcp_connection(const GC_Session *c, GC_Chat *chat) +{ +    const Messenger *m = c->messenger; + +    chat->tcp_conn = new_tcp_connections(chat->log, chat->rng, m->ns, chat->mono_time, chat->self_secret_key, +                                         &m->options.proxy_info); + +    if (chat->tcp_conn == nullptr) { +        return false; +    } + +    add_tcp_relays_to_chat(c, chat); + +    set_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_packet, c->messenger); +    set_oob_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_oob_packet, c->messenger); + +    return true; +} + +/** Initializes default shared state values. */ +non_null() +static void init_gc_shared_state(GC_Chat *chat, const Group_Privacy_State privacy_state) +{ +    chat->shared_state.maxpeers = MAX_GC_PEERS_DEFAULT; +    chat->shared_state.privacy_state = privacy_state; +    chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; +    chat->shared_state.voice_state = GV_ALL; +} + +/** @brief Initializes the group shared state for the founder. + * + * Return true on success. + */ +non_null() +static bool init_gc_shared_state_founder(GC_Chat *chat, Group_Privacy_State privacy_state, const uint8_t *group_name, +        uint16_t name_length) +{ +    memcpy(chat->shared_state.founder_public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); +    memcpy(chat->shared_state.group_name, group_name, name_length); +    chat->shared_state.group_name_len = name_length; +    chat->shared_state.privacy_state = privacy_state; + +    return sign_gc_shared_state(chat); +} + +/** @brief Initializes shared state for moderation object. + * + * This must be called before any moderation + * or sanctions related operations. + */ +non_null() +static void init_gc_moderation(GC_Chat *chat) +{ +    memcpy(chat->moderation.founder_public_sig_key, +           get_sig_pk(chat->shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); +    memcpy(chat->moderation.self_public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); +    memcpy(chat->moderation.self_secret_sig_key, get_sig_pk(chat->self_secret_key), SIG_SECRET_KEY_SIZE); +    chat->moderation.shared_state_version = chat->shared_state.version; +    chat->moderation.log = chat->log; +} + +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat); + +non_null() +static int create_new_group(GC_Session *c, const uint8_t *nick, size_t nick_length, bool founder, +                            const Group_Privacy_State privacy_state) +{ +    if (nick == nullptr || nick_length == 0) { +        return -1; +    } + +    if (nick_length > MAX_GC_NICK_SIZE) { +        return -1; +    } + +    const int group_number = get_new_group_index(c); + +    if (group_number == -1) { +        return -1; +    } + +    Messenger *m = c->messenger; +    GC_Chat *chat = &c->chats[group_number]; + +    chat->log = m->log; +    chat->rng = m->rng; + +    const uint64_t tm = mono_time_get(m->mono_time); + +    chat->group_number = group_number; +    chat->numpeers = 0; +    chat->connection_state = CS_CONNECTING; +    chat->net = m->net; +    chat->mono_time = m->mono_time; +    chat->last_ping_interval = tm; +    chat->friend_connection_id = -1; + +    if (!create_new_chat_ext_keypair(chat)) { +        LOGGER_ERROR(chat->log, "Failed to create extended keypair"); +        group_delete(c, chat); +        return -1; +    } + +    if (!init_gc_tcp_connection(c, chat)) { +        group_delete(c, chat); +        return -1; +    } + +    if (peer_add(chat, nullptr, chat->self_public_key) != 0) {    /* you are always peer_number/index 0 */ +        group_delete(c, chat); +        return -1; +    } + +    if (!self_gc_set_nick(chat, nick, (uint16_t)nick_length)) { +        group_delete(c, chat); +        return -1; +    } + +    self_gc_set_status(chat, GS_NONE); +    self_gc_set_role(chat, founder ? GR_FOUNDER : GR_USER); +    self_gc_set_confirmed(chat, true); +    self_gc_set_ext_public_key(chat, chat->self_public_key); + +    init_gc_shared_state(chat, privacy_state); +    init_gc_moderation(chat); + +    return group_number; +} + +/** @brief Inits the sanctions list credentials. + * + * This should be called by the group founder on creation. + * + * This function must be called after `init_gc_moderation()`. + * + * Return true on success. + */ +non_null() +static bool init_gc_sanctions_creds(GC_Chat *chat) +{ +    return sanctions_list_make_creds(&chat->moderation); +} + +/** @brief Attempts to add `num_addrs` peers from `addrs` to our peerlist and initiate invite requests + * for all of them. + * + * Returns the number of peers successfully loaded. + */ +static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs) +{ +    size_t count = 0; + +    for (size_t i = 0; i < num_addrs; ++i) { +        if (!saved_peer_is_valid(&addrs[i])) { +            continue; +        } + +        const bool ip_port_is_set = ipport_isset(&addrs[i].ip_port); +        const IP_Port *ip_port = ip_port_is_set ? &addrs[i].ip_port : nullptr; + +        const int peer_number = peer_add(chat, ip_port, addrs[i].public_key); + +        GC_Connection *gconn = get_gc_connection(chat, peer_number); + +        if (gconn == nullptr) { +            continue; +        } + +        add_tcp_relay_global(chat->tcp_conn, &addrs[i].tcp_relay.ip_port, addrs[i].tcp_relay.public_key); + +        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, +                                   &addrs[i].tcp_relay.ip_port, +                                   addrs[i].tcp_relay.public_key); + +        if (add_tcp_result == -1 && !ip_port_is_set) { +            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +            continue; +        } + +        if (add_tcp_result == 0) { +            const int save_tcp_result = gcc_save_tcp_relay(chat->rng, gconn, &addrs[i].tcp_relay); + +            if (save_tcp_result == -1) { +                gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); +                continue; +            } + +            memcpy(gconn->oob_relay_pk, addrs[i].tcp_relay.public_key, CRYPTO_PUBLIC_KEY_SIZE); +        } + +        const uint64_t tm = mono_time_get(chat->mono_time); + +        gconn->is_oob_handshake = !gcc_direct_conn_is_possible(chat, gconn); +        gconn->is_pending_handshake_response = false; +        gconn->pending_handshake_type = HS_INVITE_REQUEST; +        gconn->last_received_packet_time = tm; +        gconn->last_key_rotation = tm; + +        ++count; +    } + +    update_gc_peer_roles(chat); + +    return count; +} + +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp) +{ +    gc_save_pack_group(chat, bp); +} + +int gc_group_load(GC_Session *c, Bin_Unpack *bu) +{ +    const int group_number = get_new_group_index(c); + +    if (group_number < 0) { +        return -1; +    } + +    const uint64_t tm = mono_time_get(c->messenger->mono_time); + +    Messenger *m = c->messenger; +    GC_Chat *chat = &c->chats[group_number]; + +    chat->group_number = group_number; +    chat->numpeers = 0; +    chat->net = m->net; +    chat->mono_time = m->mono_time; +    chat->log = m->log; +    chat->rng = m->rng; +    chat->last_ping_interval = tm; +    chat->friend_connection_id = -1; + +    if (!gc_load_unpack_group(chat, bu)) { +        LOGGER_ERROR(chat->log, "Failed to unpack group"); +        return -1; +    } + +    init_gc_moderation(chat); + +    if (!init_gc_tcp_connection(c, chat)) { +        LOGGER_ERROR(chat->log, "Failed to init tcp connection"); +        return -1; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        return group_number; +    } + +    if (is_public_chat(chat)) { +        if (!m_create_group_connection(m, chat)) { +            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); +        } +    } + +    return group_number; +} + +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, +                 uint16_t group_name_length, +                 const uint8_t *nick, size_t nick_length) +{ +    if (group_name_length > MAX_GC_GROUP_NAME_SIZE) { +        return -1; +    } + +    if (nick_length > MAX_GC_NICK_SIZE) { +        return -1; +    } + +    if (group_name_length == 0 || group_name == nullptr) { +        return -2; +    } + +    if (nick_length == 0 || nick == nullptr) { +        return -2; +    } + +    const int group_number = create_new_group(c, nick, nick_length, true, privacy_state); + +    if (group_number == -1) { +        return -3; +    } + +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -3; +    } + +    crypto_memlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); + +    create_extended_keypair(chat->chat_public_key, chat->chat_secret_key); + +    if (!init_gc_shared_state_founder(chat, privacy_state, group_name, group_name_length)) { +        group_delete(c, chat); +        return -4; +    } + +    init_gc_moderation(chat); + +    if (!init_gc_sanctions_creds(chat)) { +        group_delete(c, chat); +        return -4; +    } + +    if (gc_set_topic(chat, nullptr, 0) != 0) { +        group_delete(c, chat); +        return -4; +    } + +    chat->join_type = HJ_PRIVATE; +    chat->connection_state = CS_CONNECTED; +    chat->time_connected = mono_time_get(c->messenger->mono_time); + +    if (is_public_chat(chat)) { +        if (!m_create_group_connection(c->messenger, chat)) { +            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); +            group_delete(c, chat); +            return -5; +        } + +        chat->join_type = HJ_PUBLIC; +    } + +    update_gc_peer_roles(chat); + +    return group_number; +} + +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, +                  uint16_t passwd_len) +{ +    if (chat_id == nullptr || group_exists(c, chat_id) || getfriend_id(c->messenger, chat_id) != -1) { +        return -2; +    } + +    if (nick_length > MAX_GC_NICK_SIZE) { +        return -3; +    } + +    if (nick == nullptr || nick_length == 0) { +        return -4; +    } + +    const int group_number = create_new_group(c, nick, nick_length, false, GI_PUBLIC); + +    if (group_number == -1) { +        return -1; +    } + +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -1; +    } + +    if (!expand_chat_id(chat->chat_public_key, chat_id)) { +        group_delete(c, chat); +        return -1; +    } + +    chat->connection_state = CS_CONNECTING; + +    if (passwd != nullptr && passwd_len > 0) { +        if (!set_gc_password_local(chat, passwd, passwd_len)) { +            group_delete(c, chat); +            return -5; +        } +    } + +    if (!m_create_group_connection(c->messenger, chat)) { +        group_delete(c, chat); +        return -6; +    } + +    update_gc_peer_roles(chat); + +    return group_number; +} + +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat) +{ +    if (c == nullptr || chat == nullptr) { +        return false; +    } + +    chat->connection_state = CS_DISCONNECTED; + +    send_gc_broadcast_message(chat, nullptr, 0, GM_PEER_EXIT); + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); +    } + +    return true; +} + +int gc_rejoin_group(GC_Session *c, GC_Chat *chat) +{ +    if (c == nullptr || chat == nullptr) { +        return -1; +    } + +    chat->time_connected = 0; + +    if (group_can_handle_packets(chat)) { +        send_gc_self_exit(chat, nullptr, 0); +    } + +    for (uint32_t i = 1; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); +    } + +    if (is_public_chat(chat)) { +        kill_group_friend_connection(c, chat); + +        if (!m_create_group_connection(c->messenger, chat)) { +            LOGGER_WARNING(chat->log, "Failed to create new messenger connection for group"); +            return -2; +        } + +        chat->update_self_announces = true; +    } + +    chat->connection_state = CS_CONNECTING; + +    return 0; +} + +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length) +{ +    if (length < CHAT_ID_SIZE) { +        return false; +    } + +    return !group_exists(c, chat_id); +} + +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, +                     gc_send_group_invite_packet_cb *callback) +{ +    if (!friend_is_valid(c->messenger, friend_number)) { +        return -1; +    } + +    const uint16_t group_name_length = chat->shared_state.group_name_len; + +    assert(group_name_length <= MAX_GC_GROUP_NAME_SIZE); + +    uint8_t *packet = (uint8_t *)malloc(2 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE + group_name_length); + +    if (packet == nullptr) { +        return -1; +    } + +    packet[0] = GP_FRIEND_INVITE; +    packet[1] = GROUP_INVITE; + +    memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); +    uint16_t length = 2 + CHAT_ID_SIZE; + +    memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); +    length += ENC_PUBLIC_KEY_SIZE; + + +    memcpy(packet + length, chat->shared_state.group_name, group_name_length); +    length += group_name_length; + +    assert(length <= MAX_GC_PACKET_SIZE); + +    if (!callback(c->messenger, friend_number, packet, length)) { +        free(packet); +        return -2; +    } + +    free(packet); + +    chat->saved_invites[chat->saved_invites_index] = friend_number; +    chat->saved_invites_index = (chat->saved_invites_index + 1) % MAX_GC_SAVED_INVITES; + +    return 0; +} + +/** @brief Sends an invite accepted packet to `friend_number`. + * + * Return 0 on success. + * Return -1 if `friend_number` does not designate a valid friend. + * Return -2 if `chat `is null. + * Return -3 if packet failed to send. + */ +non_null() +static int send_gc_invite_accepted_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number) +{ +    if (!friend_is_valid(m, friend_number)) { +        return -1; +    } + +    if (chat == nullptr) { +        return -2; +    } + +    uint8_t packet[1 + 1 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE]; +    packet[0] = GP_FRIEND_INVITE; +    packet[1] = GROUP_INVITE_ACCEPTED; + +    memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); +    uint16_t length = 2 + CHAT_ID_SIZE; + +    memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); +    length += ENC_PUBLIC_KEY_SIZE; + +    if (!send_group_invite_packet(m, friend_number, packet, length)) { +        LOGGER_ERROR(chat->log, "Failed to send group invite packet."); +        return -3; +    } + +    return 0; +} + +/** @brief Sends an invite confirmed packet to friend designated by `friend_number`. + * + * `data` must contain the group's Chat ID, the sender's public encryption key, + * and either the sender's packed IP_Port, or at least one packed TCP node that + * the sender can be connected to through (or both). + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_confirmed_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number, +        const uint8_t *data, uint16_t length) +{ +    if (!friend_is_valid(m, friend_number)) { +        return false; +    } + +    if (chat == nullptr) { +        return false; +    } + +    if (length > MAX_GC_PACKET_SIZE) { +        return false; +    } + +    const uint16_t packet_length = 2 + length; +    uint8_t *packet = (uint8_t *)malloc(packet_length); + +    if (packet == nullptr) { +        return false; +    } + +    packet[0] = GP_FRIEND_INVITE; +    packet[1] = GROUP_INVITE_CONFIRMATION; + +    memcpy(packet + 2, data, length); + +    if (!send_group_invite_packet(m, friend_number, packet, packet_length)) { +        free(packet); +        return false; +    } + +    free(packet); + +    return true; +} + +/** @brief Adds `num_nodes` tcp relays from `tcp_relays` to tcp relays list associated with `gconn` + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn, const Node_format *tcp_relays, +                                  size_t num_nodes) +{ +    uint32_t relays_added = 0; + +    for (size_t i = 0; i < num_nodes; ++i) { +        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, +                                   gconn->tcp_connection_num, &tcp_relays[i].ip_port, +                                   tcp_relays[i].public_key); + +        if (add_tcp_result == 0) { +            if (gcc_save_tcp_relay(chat->rng, gconn, &tcp_relays[i]) == 0) { +                ++relays_added; +            } +        } +    } + +    return relays_added; +} + +non_null() +static bool copy_friend_ip_port_to_gconn(const Messenger *m, int friend_number, GC_Connection *gconn) +{ +    if (!friend_is_valid(m, friend_number)) { +        return false; +    } + +    const Friend *f = &m->friendlist[friend_number]; +    const int friend_connection_id = f->friendcon_id; +    const Friend_Conn *connection = get_conn(m->fr_c, friend_connection_id); + +    if (connection == nullptr) { +        return false; +    } + +    const IP_Port *friend_ip_port = friend_conn_get_dht_ip_port(connection); + +    if (!ipport_isset(friend_ip_port)) { +        return false; +    } + +    gconn->addr.ip_port = *friend_ip_port; + +    return true; +} + +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ +    if (length < GC_JOIN_DATA_LENGTH) { +        return -1; +    } + +    if (!friend_is_valid(c->messenger, friend_number)) { +        return -4; +    } + +    uint8_t chat_id[CHAT_ID_SIZE]; +    uint8_t invite_chat_pk[ENC_PUBLIC_KEY_SIZE]; + +    memcpy(chat_id, data, CHAT_ID_SIZE); +    memcpy(invite_chat_pk, data + CHAT_ID_SIZE, ENC_PUBLIC_KEY_SIZE); + +    const GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + +    if (chat == nullptr) { +        return -2; +    } + +    const int peer_number = get_peer_number_of_enc_pk(chat, invite_chat_pk, false); + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return -3; +    } + +    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; +    const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, +                                       nullptr, data + ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE, +                                       length - GC_JOIN_DATA_LENGTH, true); + +    const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(c->messenger, friend_number, gconn); + +    uint32_t tcp_relays_added = 0; + +    if (num_nodes > 0) { +        tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_nodes); +    } else { +        LOGGER_WARNING(chat->log, "Invite confirm packet did not contain any TCP relays"); +    } + +    if (tcp_relays_added == 0 && !copy_ip_port_result) { +        LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); +        return -5; +    } + +    gconn->pending_handshake_type = HS_INVITE_REQUEST; + +    return 0; +} + +/** Return true if we have a pending sent invite for our friend designated by `friend_number`. */ +non_null() +static bool friend_was_invited(const Messenger *m, GC_Chat *chat, int friend_number) +{ +    for (size_t i = 0; i < MAX_GC_SAVED_INVITES; ++i) { +        if (chat->saved_invites[i] == friend_number) { +            chat->saved_invites[i] = -1; +            return friend_is_valid(m, friend_number); +        } +    } + +    return false; +} + +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ +    if (length < GC_JOIN_DATA_LENGTH) { +        return false; +    } + +    const Messenger *m = c->messenger; + +    const uint8_t *chat_id = data; + +    GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + +    if (chat == nullptr || !group_can_handle_packets(chat)) { +        return false; +    } + +    const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + +    const int peer_number = peer_add(chat, nullptr, invite_chat_pk); + +    if (!friend_was_invited(m, chat, friend_number)) { +        return false; +    } + +    GC_Connection *gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return false; +    } + +    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; +    const uint32_t num_tcp_relays = tcp_copy_connected_relays(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS); + +    const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(m, friend_number, gconn); + +    if (num_tcp_relays == 0 && !copy_ip_port_result) { +        return false; +    } + +    uint16_t len = GC_JOIN_DATA_LENGTH; +    uint8_t out_data[GC_JOIN_DATA_LENGTH + (GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6)]; + +    memcpy(out_data, chat_id, CHAT_ID_SIZE); +    memcpy(out_data + CHAT_ID_SIZE, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + +    if (num_tcp_relays > 0) { +        const uint32_t tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_tcp_relays); + +        if (tcp_relays_added == 0 && !copy_ip_port_result) { +            LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); +            return false; +        } + +        const int nodes_len = pack_nodes(chat->log, out_data + len, sizeof(out_data) - len, tcp_relays, +                                         (uint16_t)num_tcp_relays); + +        if (nodes_len <= 0 && !copy_ip_port_result) { +            return false; +        } + +        len += nodes_len; +    } + +    return send_gc_invite_confirmed_packet(m, chat, friend_number, out_data, len); +} + +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, +                     size_t nick_length, const uint8_t *passwd, uint16_t passwd_len) +{ +    if (length < CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE) { +        return -1; +    } + +    if (nick_length > MAX_GC_NICK_SIZE) { +        return -3; +    } + +    if (nick == nullptr || nick_length == 0) { +        return -4; +    } + +    if (!friend_is_valid(c->messenger, friend_number)) { +        return -6; +    } + +    const uint8_t *chat_id = data; +    const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + +    const int group_number = create_new_group(c, nick, nick_length, false, GI_PUBLIC); + +    if (group_number == -1) { +        return -2; +    } + +    GC_Chat *chat = gc_get_group(c, group_number); + +    if (chat == nullptr) { +        return -2; +    } + +    if (!expand_chat_id(chat->chat_public_key, chat_id)) { +        group_delete(c, chat); +        return -2; +    } + +    if (passwd != nullptr && passwd_len > 0) { +        if (!set_gc_password_local(chat, passwd, passwd_len)) { +            group_delete(c, chat); +            return -5; +        } +    } + +    const int peer_id = peer_add(chat, nullptr, invite_chat_pk); + +    if (peer_id < 0) { +        return -2; +    } + +    chat->join_type = HJ_PRIVATE; + +    if (send_gc_invite_accepted_packet(c->messenger, chat, friend_number) != 0) { +        return -7; +    } + +    return group_number; +} + +non_null(1, 3) nullable(5) +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, +        size_t data_length, void *user_data); + +GC_Session *new_dht_groupchats(Messenger *m) +{ +    if (m == nullptr) { +        return nullptr; +    } + +    GC_Session *c = (GC_Session *)calloc(1, sizeof(GC_Session)); + +    if (c == nullptr) { +        return nullptr; +    } + +    c->messenger = m; +    c->announces_list = m->group_announce; + +    networking_registerhandler(m->net, NET_PACKET_GC_LOSSLESS, &handle_gc_udp_packet, m); +    networking_registerhandler(m->net, NET_PACKET_GC_LOSSY, &handle_gc_udp_packet, m); +    networking_registerhandler(m->net, NET_PACKET_GC_HANDSHAKE, &handle_gc_udp_packet, m); +    onion_group_announce_register(m->onion_c, gc_handle_announce_response_callback, c); + +    return c; +} + +static void group_cleanup(GC_Session *c, GC_Chat *chat) +{ +    kill_group_friend_connection(c, chat); + +    mod_list_cleanup(&chat->moderation); +    sanctions_list_cleanup(&chat->moderation); + +    if (chat->tcp_conn != nullptr) { +        kill_tcp_connections(chat->tcp_conn); +    } + +    gcc_cleanup(chat); + +    if (chat->group != nullptr) { +        free(chat->group); +        chat->group = nullptr; +    } + +    crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); +    crypto_memunlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); +    crypto_memunlock(chat->shared_state.password, sizeof(chat->shared_state.password)); +} + +/** Deletes chat from group chat array and cleans up. */ +static void group_delete(GC_Session *c, GC_Chat *chat) +{ +    if (c == nullptr || chat == nullptr) { +        if (chat != nullptr) { +            LOGGER_ERROR(chat->log, "Null pointer"); +        } + +        return; +    } + +    group_cleanup(c, chat); + +    c->chats[chat->group_number] = empty_gc_chat; + +    uint32_t i; + +    for (i = c->chats_index; i > 0; --i) { +        if (c->chats[i - 1].connection_state != CS_NONE) { +            break; +        } +    } + +    if (c->chats_index != i) { +        c->chats_index = i; + +        if (!realloc_groupchats(c, c->chats_index)) { +            LOGGER_ERROR(chat->log, "Failed to reallocate groupchats array"); +        } +    } +} + +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length) +{ +    const int ret =  group_can_handle_packets(chat) ? send_gc_self_exit(chat, message, length) : 0; +    group_delete(c, chat); +    return ret; +} + +void kill_dht_groupchats(GC_Session *c) +{ +    if (c == nullptr) { +        return; +    } + +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        GC_Chat *chat = &c->chats[i]; + +        if (chat->connection_state == CS_NONE) { +            continue; +        } + +        if (group_can_handle_packets(chat)) { +            send_gc_self_exit(chat, nullptr, 0); +        } + +        group_cleanup(c, chat); +    } + +    networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSY, nullptr, nullptr); +    networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSLESS, nullptr, nullptr); +    networking_registerhandler(c->messenger->net, NET_PACKET_GC_HANDSHAKE, nullptr, nullptr); +    onion_group_announce_register(c->messenger->onion_c, nullptr, nullptr); + +    free(c->chats); +    free(c); +} + +bool gc_group_is_valid(const GC_Chat *chat) +{ +    return chat->connection_state != CS_NONE && chat->shared_state.version > 0; +} + +/** Return true if `group_number` designates an active group in session `c`. */ +static bool group_number_valid(const GC_Session *c, int group_number) +{ +    if (group_number < 0 || group_number >= c->chats_index) { +        return false; +    } + +    if (c->chats == nullptr) { +        return false; +    } + +    return c->chats[group_number].connection_state != CS_NONE; +} + +uint32_t gc_count_groups(const GC_Session *c) +{ +    uint32_t count = 0; + +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        const GC_Chat *chat = &c->chats[i]; + +        if (gc_group_is_valid(chat)) { +            ++count; +        } +    } + +    return count; +} + +GC_Chat *gc_get_group(const GC_Session *c, int group_number) +{ +    if (!group_number_valid(c, group_number)) { +        return nullptr; +    } + +    return &c->chats[group_number]; +} + +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key) +{ +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        GC_Chat *chat = &c->chats[i]; + +        if (chat->connection_state == CS_NONE) { +            continue; +        } + +        if (memcmp(public_key, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE) == 0) { +            return chat; +        } +    } + +    return nullptr; +} + +/** Return True if chat_id exists in the session chat array */ +static bool group_exists(const GC_Session *c, const uint8_t *chat_id) +{ +    for (uint32_t i = 0; i < c->chats_index; ++i) { +        const GC_Chat *chat = &c->chats[i]; + +        if (chat->connection_state == CS_NONE) { +            continue; +        } + +        if (memcmp(get_chat_id(chat->chat_public_key), chat_id, CHAT_ID_SIZE) == 0) { +            return true; +        } +    } + +    return false; +} + +/** Creates a new 32-byte session encryption keypair and puts the results in `public_key` and `secret_key`. */ +static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, uint8_t *secret_key) +{ +    if (crypto_new_keypair(rng, public_key, secret_key) != 0) { +        LOGGER_FATAL(log, "Failed to create group session keypair"); +    } +} + +/** + * Creates a new 64-byte extended keypair for `chat` and puts results in `self_public_key` + * and `self_secret_key` buffers. The first 32-bytes of the generated keys are used for + * encryption, while the remaining 32-bytes are used for signing. + * + * Return false if key generation fails. + */ +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat) +{ +    crypto_memlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + +    if (!create_extended_keypair(chat->self_public_key, chat->self_secret_key)) { +        crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); +        return false; +    } + +    return true; +} + +/** @brief Handles a group announce onion response. + * + * Return true on success. + */ +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, +        size_t data_length, void *user_data) +{ +    const GC_Session *c = (GC_Session *)user_data; + +    if (c == nullptr) { +        return false; +    } + +    if (sendback_num == 0) { +        return false; +    } + +    GC_Announce announces[GCA_MAX_SENT_ANNOUNCES]; +    const uint8_t *gc_public_key = onion_friend_get_gc_public_key_num(onion_c, sendback_num - 1); +    GC_Chat *chat = gc_get_group_by_public_key(c, gc_public_key); + +    if (chat == nullptr) { +        return false; +    } + +    const int gc_announces_count = gca_unpack_announces_list(chat->log, data, data_length, +                                   announces, GCA_MAX_SENT_ANNOUNCES); + +    if (gc_announces_count == -1) { +        return false; +    } + +    const int added_peers = gc_add_peers_from_announces(chat, announces, gc_announces_count); + +    return added_peers >= 0; +} + +/** @brief Adds TCP relays from `announce` to the TCP relays list for `gconn`. + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays_from_announce(const GC_Chat *chat, GC_Connection *gconn, const GC_Announce *announce) +{ +    uint32_t added_relays = 0; + +    for (uint8_t j = 0; j < announce->tcp_relays_count; ++j) { +        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, +                                   &announce->tcp_relays[j].ip_port, +                                   announce->tcp_relays[j].public_key); + +        if (add_tcp_result == -1) { +            continue; +        } + +        if (gcc_save_tcp_relay(chat->rng, gconn, &announce->tcp_relays[j]) == -1) { +            continue; +        } + +        if (added_relays == 0) { +            memcpy(gconn->oob_relay_pk, announce->tcp_relays[j].public_key, CRYPTO_PUBLIC_KEY_SIZE); +        } + +        ++added_relays; +    } + +    return added_relays; +} + +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count) +{ +    if (chat == nullptr || announces == nullptr) { +        return -1; +    } + +    if (!is_public_chat(chat)) { +        return 0; +    } + +    int added_peers = 0; + +    for (uint8_t i = 0; i < gc_announces_count; ++i) { +        const GC_Announce *announce = &announces[i]; + +        if (!gca_is_valid_announce(announce)) { +            continue; +        } + +        const bool ip_port_set = announce->ip_port_is_set; +        const IP_Port *ip_port = ip_port_set ? &announce->ip_port : nullptr; +        const int peer_number = peer_add(chat, ip_port, announce->peer_public_key); + +        GC_Connection *gconn = get_gc_connection(chat, peer_number); + +        if (gconn == nullptr) { +            continue; +        } + +        const uint32_t added_tcp_relays = add_gc_tcp_relays_from_announce(chat, gconn, announce); + +        if (!ip_port_set && added_tcp_relays == 0) { +            LOGGER_ERROR(chat->log, "Got invalid announcement: %u relays, IPP set: %d", +                         added_tcp_relays, ip_port_set); +            continue; +        } + +        gconn->pending_handshake_type = HS_INVITE_REQUEST; + +        if (!ip_port_set) { +            gconn->is_oob_handshake = true; +        } + +        ++added_peers; +    } + +    return added_peers; +} +#endif  // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_chats.h b/protocols/Tox/libtox/src/toxcore/group_chats.h new file mode 100644 index 0000000000..821e629a64 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_chats.h @@ -0,0 +1,782 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CHATS_H +#define GROUP_CHATS_H + +#include <stdbool.h> +#include <stdint.h> + +#include "TCP_connection.h" +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_announce.h" +#include "group_common.h" +#include "group_connection.h" +#include "logger.h" + +#define GC_PING_TIMEOUT 12 +#define GC_SEND_IP_PORT_INTERVAL (GC_PING_TIMEOUT * 5) +#define GC_CONFIRMED_PEER_TIMEOUT (GC_PING_TIMEOUT * 4 + 10) +#define GC_UNCONFIRMED_PEER_TIMEOUT GC_PING_TIMEOUT + +#define GC_JOIN_DATA_LENGTH (ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE) + +/** Group topic lock states. */ +typedef enum Group_Topic_Lock { +    TL_ENABLED  = 0x00,  // Only the Founder and moderators may set the topic +    TL_DISABLED = 0x01,  // Anyone except Observers may set the topic +} Group_Topic_Lock; + +/** Group moderation events. */ +typedef enum Group_Moderation_Event { +    MV_KICK      = 0x00,  // A peer has been kicked +    MV_OBSERVER  = 0x01,  // A peer has been demoted to Observer +    MV_USER      = 0x02,  // A peer has been demoted or promoted to User +    MV_MOD       = 0x03,  // A peer has been promoted to or demoted from Moderator +} Group_Moderation_Event; + +/** Messenger level group invite types */ +typedef enum Group_Invite_Message_Type { +    GROUP_INVITE              = 0x00,  // Peer has initiated an invite +    GROUP_INVITE_ACCEPTED     = 0x01,  // Peer has accepted the invite +    GROUP_INVITE_CONFIRMATION = 0x02,  // Peer has confirmed the accepted invite +} Group_Invite_Message_Type; + +/** Group join rejection types. */ +typedef enum Group_Join_Rejected { +    GJ_GROUP_FULL       = 0x00, +    GJ_INVALID_PASSWORD = 0x01, +    GJ_INVITE_FAILED    = 0x02, +    GJ_INVALID          = 0x03, +} Group_Join_Rejected; + +/** Group broadcast packet types */ +typedef enum Group_Broadcast_Type { +    GM_STATUS          = 0x00,  // Peer changed their status +    GM_NICK            = 0x01,  // Peer changed their nickname +    GM_PLAIN_MESSAGE   = 0x02,  // Peer sent a normal message +    GM_ACTION_MESSAGE  = 0x03,  // Peer sent an action message +    GM_PRIVATE_MESSAGE = 0x04,  // Peer sent a private message +    GM_PEER_EXIT       = 0x05,  // Peer left the group +    GM_KICK_PEER       = 0x06,  // Peer was kicked from the group +    GM_SET_MOD         = 0x07,  // Peer was promoted to or demoted from Moderator role +    GM_SET_OBSERVER    = 0x08,  // Peer was demoted to or promoted from Observer role +} Group_Broadcast_Type; + +/*** + * Group packet types. + * + * For a detailed spec, see docs/DHT_Group_Chats_Packet_Spec.md + */ +typedef enum Group_Packet_Type { +    /* lossy packets (ID 0 is reserved) */ +    GP_PING                     = 0x01, +    GP_MESSAGE_ACK              = 0x02, +    GP_INVITE_RESPONSE_REJECT   = 0x03, + +    /* lossless packets */ +    GP_CUSTOM_PRIVATE_PACKET    = 0xee, +    GP_FRAGMENT                 = 0xef, +    GP_KEY_ROTATION             = 0xf0, +    GP_TCP_RELAYS               = 0xf1, +    GP_CUSTOM_PACKET            = 0xf2, +    GP_BROADCAST                = 0xf3, +    GP_PEER_INFO_REQUEST        = 0xf4, +    GP_PEER_INFO_RESPONSE       = 0xf5, +    GP_INVITE_REQUEST           = 0xf6, +    GP_INVITE_RESPONSE          = 0xf7, +    GP_SYNC_REQUEST             = 0xf8, +    GP_SYNC_RESPONSE            = 0xf9, +    GP_TOPIC                    = 0xfa, +    GP_SHARED_STATE             = 0xfb, +    GP_MOD_LIST                 = 0xfc, +    GP_SANCTIONS_LIST           = 0xfd, +    GP_FRIEND_INVITE            = 0xfe, +    GP_HS_RESPONSE_ACK          = 0xff, +} Group_Packet_Type; + +/** Lossless message acknowledgement types. */ +typedef enum Group_Message_Ack_Type { +    GR_ACK_RECV    = 0x00,  // indicates a message has been received +    GR_ACK_REQ     = 0x01,  // indicates a message needs to be re-sent +} Group_Message_Ack_Type; + +/** @brief Returns the GC_Connection object associated with `peer_number`. + * Returns null if peer_number does not designate a valid peer. + */ +non_null() +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number); + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +non_null() +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key); + +/** @brief Check if peer with the public encryption key is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + * + * If `confirmed` is true the peer number will only be returned if the peer is confirmed. + */ +non_null() +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed); + +/** @brief Encrypts `data` of size `length` using the peer's shared key and a new nonce. + * + * Adds encrypted header consisting of: packet type, message_id (only for lossless packets). + * Adds plaintext header consisting of: packet identifier, self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if plaintext length is invalid. + * Return -2 if malloc fails. + * Return -3 if encryption fails. + */ +non_null(1, 2, 3, 4, 5) nullable(7) +int group_packet_wrap( +    const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, +    uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, +    uint8_t gp_packet_type, uint8_t net_packet_type); + +/** @brief Returns the size of a wrapped/encrypted packet with a plain size of `length`. + * + * `packet_type` should be either NET_PACKET_GC_LOSSY or NET_PACKET_GC_LOSSLESS. + */ +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type); + +/** @brief Sends a plain message or an action, depending on type. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * `message_id` should either point to a uint32_t or be NULL. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the message type is invalid. + * Returns -4 if the sender does not have permission to speak. + * Returns -5 if the packet fails to send. + */ +non_null(1, 2, 3, 4) nullable(5) +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, +                    uint32_t *message_id); + +/** @brief Sends a private message to peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the peer_id is invalid. + * Returns -4 if the message type is invalid. + * Returns -5 if the sender has the observer role. + * Returns -6 if the packet fails to send. + */ +non_null() +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, +                            uint16_t length); + +/** @brief Sends a custom packet to the group. If lossless is true, the packet will be lossless. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the sender has the observer role. + */ +non_null() +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length); + +/** @brief Sends a custom private packet to the peer designated by peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * @retval 0 on success. + * @retval -1 if the message is too long. + * @retval -2 if the message pointer is NULL or length is zero. + * @retval -3 if the supplied peer_id does not designate a valid peer. + * @retval -4 if the sender has the observer role. + * @retval -5 if the packet fails to send. + */ +non_null() +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, +                                  uint16_t length); + +/** @brief Sets ignore for peer_id. + * + * Returns 0 on success. + * Returns -1 if the peer_id is invalid. + * Returns -2 if the caller attempted to ignore himself. + */ +non_null() +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore); + +/** @brief Sets the group topic and broadcasts it to the group. + * + * If `length` is equal to zero the topic will be unset. + * + * Returns 0 on success. + * Returns -1 if the topic is too long (must be `<= MAX_GC_TOPIC_SIZE`). + * Returns -2 if the caller does not have the required permissions to set the topic. + * Returns -3 if the packet cannot be created or signing fails. + * Returns -4 if the packet fails + */ +non_null(1) nullable(2) +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length); + +/** @brief Copies the group topic to `topic`. If topic is null this function has no effect. + * + * Call `gc_get_topic_size` to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last topic callback. + */ +non_null(1) nullable(2) +void gc_get_topic(const GC_Chat *chat, uint8_t *topic); + +/** @brief Returns the topic length. + * + * The return value is equal to the `length` agument received by the last topic callback. + */ +non_null() +uint16_t gc_get_topic_size(const GC_Chat *chat); + +/** @brief Copies group name to `group_name`. If `group_name` is null this function has no effect. + * + * Call `gc_get_group_name_size` to determine the allocation size for the `group_name` + * parameter. + */ +non_null() +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name); + +/** @brief Returns the group name length. */ +non_null() +uint16_t gc_get_group_name_size(const GC_Chat *chat); + +/** @brief Copies the group password to password. + * + * If password is null this function has no effect. + * + * Call the `gc_get_password_size` function to determine the allocation size for + * the `password` buffer. + * + * The data received is equal to the data received by the last password callback. + */ +non_null() +void gc_get_password(const GC_Chat *chat, uint8_t *password); + +/** @brief Returns the group password length. */ +non_null() +uint16_t gc_get_password_size(const GC_Chat *chat); + +/** @brief Returns the group privacy state. + * + * The value returned is equal to the data receieved by the last privacy_state callback. + */ +non_null() +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat); + +/** @brief Returns the group topic lock state. + * + * The value returned is equal to the data received by the last last topic_lock callback. + */ +non_null() +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat); + +/** @brief Returns the group voice state. + * + * The value returned is equal to the data received by the last voice_state callback. + */ +non_null() +Group_Voice_State gc_get_voice_state(const GC_Chat *chat); + +/** @brief Returns the group peer limit. + * + * The value returned is equal to the data receieved by the last peer_limit callback. + */ +non_null() +uint16_t gc_get_max_peers(const GC_Chat *chat); + +/** @brief Sets your own nick to `nick`. + * + * `length` cannot exceed MAX_GC_NICK_SIZE. if `length` is zero or `name` is a + * null pointer the function call will fail. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the length is too long. + * Returns -3 if the length is zero or nick is a NULL pointer. + * Returns -4 if the packet fails to send. + */ +non_null() +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length); + +/** @brief Copies your own name to `nick`. + * + * If `nick` is null this function has no effect. + */ +non_null() +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick); + +/** @brief Return your own nick length. + * + * If no nick was set before calling this function it will return 0. + */ +non_null() +uint16_t gc_get_self_nick_size(const GC_Chat *chat); + +/** @brief Returns your own group role. */ +non_null() +Group_Role gc_get_self_role(const GC_Chat *chat); + +/** @brief Return your own status. */ +non_null() +uint8_t gc_get_self_status(const GC_Chat *chat); + +/** @brief Returns your own peer id. */ +non_null() +uint32_t gc_get_self_peer_id(const GC_Chat *chat); + +/** @brief Copies self public key to `public_key`. + * + * If `public_key` is null this function has no effect. + * + * This key is permanently tied to our identity for `chat` until we explicitly + * exit the group. This key is the only way for other peers to reliably identify + * us across client restarts. + */ +non_null(1) nullable(2) +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key); + +/** @brief Copies nick designated by `peer_id` to `name`. + * + * Call `gc_get_peer_nick_size` to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last nick_change callback. + * + * Returns true on success. + * Returns false if peer_id is invalid. + */ +non_null(1) nullable(3) +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name); + +/** @brief Returns the length of the nick for the peer designated by `peer_id`. + * Returns -1 if peer_id is invalid. + * + * The value returned is equal to the `length` argument received by the last + * nick_change callback. + */ +non_null() +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Copies peer_id's public key to `public_key`. + * + * This key is permanently tied to the peer's identity for `chat` until they explicitly + * exit the group. This key is the only way for to reliably identify the given peer + * across client restarts. + * + * `public_key` shold have room for at least ENC_PUBLIC_KEY_SIZE bytes. + * + * Returns 0 on success. + * Returns -1 if peer_id is invalid or doesn't correspond to a valid peer connection. + * Returns -2 if `public_key` is null. + */ +non_null(1) nullable(3) +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key); + +/** @brief Gets the connection status for peer associated with `peer_id`. + * + * Returns 2 if we have a direct (UDP) connection with a peer. + * Returns 1 if we have an indirect (TCP) connection with a peer. + * Returns 0 if peer_id is invalid or corresponds to ourselves. + * + * Note: Return values must correspond to Tox_Connection enum in API. + */ +non_null() +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the caller's status to `status`. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the packet failed to send. + */ +non_null() +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status); + +/** @brief Returns the status of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The status returned is equal to the last status received through the status_change + * callback. + */ +non_null() +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Returns the group role of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The role returned is equal to the last role received through the moderation callback. + */ +non_null() +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the role of peer_id. role must be one of: GR_MODERATOR, GR_USER, GR_OBSERVER + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if caller does not have sufficient permissions for the action. + * Returns -4 if the role assignment is invalid. + * Returns -5 if the role failed to be set. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role); + +/** @brief Sets the group password and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If `password` is null or `password_length` is 0 the password will be unset for the group. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for the action. + * Returns -2 if the password is too long. + * Returns -3 if the packet failed to send. + * Returns -4 if malloc failed. + */ +non_null(1) nullable(2) +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length); + +/** @brief Sets the topic lock and distributes the new shared state to the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if `topic_lock` is an invalid type. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the group is disconnected. + * Returns -5 if the topic lock could not be set. + * Returns -6 if the packet failed to send. + */ +non_null() +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state); + +/** @brief Sets the group privacy state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the privacy state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state); + +/** @brief Sets the group voice state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the voice state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state); + +/** @brief Sets the peer limit to maxpeers and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for this action. + * Returns -2 if the peer limit could not be set. + * Returns -3 if the packet failed to send. + */ +non_null() +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers); + +/** @brief Removes peer designated by `peer_id` from peer list and sends a broadcast instructing + * all other peers to remove the peer from their peerlist as well. + * + * This function will not trigger the peer_exit callback for the caller. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the action failed. + * Returns -5 if the packet failed to send. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id); + +/** @brief Copies the chat_id to dest. If dest is null this function has no effect. + * + * `dest` should have room for at least CHAT_ID_SIZE bytes. + */ +non_null(1) nullable(2) +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest); + + +/** Group callbacks */ +non_null(1) nullable(2) void gc_callback_message(const Messenger *m, gc_message_cb *function); +non_null(1) nullable(2) void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function); +non_null(1) nullable(2) void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function); +non_null(1) nullable(2) void gc_callback_custom_private_packet(const Messenger *m, +        gc_custom_private_packet_cb *function); +non_null(1) nullable(2) void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function); +non_null(1) nullable(2) void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function); +non_null(1) nullable(2) void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function); +non_null(1) nullable(2) void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function); +non_null(1) nullable(2) void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function); +non_null(1) nullable(2) void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function); +non_null(1) nullable(2) void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function); +non_null(1) nullable(2) void gc_callback_password(const Messenger *m, gc_password_cb *function); +non_null(1) nullable(2) void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function); +non_null(1) nullable(2) void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function); +non_null(1) nullable(2) void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function); +non_null(1) nullable(2) void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function); +non_null(1) nullable(2) void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function); + +/** @brief The main loop. Should be called with every Messenger iteration. */ +non_null(1) nullable(2) +void do_gc(GC_Session *c, void *userdata); + +/** + * Make sure that DHT is initialized before calling this. + * Returns a NULL pointer on failure. + */ +nullable(1) +GC_Session *new_dht_groupchats(Messenger *m); + +/** @brief Cleans up groupchat structures and calls `gc_group_exit()` for every group chat */ +nullable(1) +void kill_dht_groupchats(GC_Session *c); + +/** @brief Loads a previously saved group and attempts to join it. + * + * `bu` is the packed group info. + * + * Returns group_number on success. + * Returns -1 on failure. + */ +non_null() +int gc_group_load(GC_Session *c, Bin_Unpack *bu); + +/** + * @brief Saves info from `chat` to `bp` in binary format. + */ +non_null() +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp); + +/** @brief Creates a new group and adds it to the group sessions group array. + * + * The caller of this function has founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * Return -1 if the nick or group name is too long. + * Return -2 if the nick or group name is empty. + * Return -3 if the the group object fails to initialize. + * Return -4 if the group state fails to initialize. + * Return -5 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, +                 uint16_t group_name_length, +                 const uint8_t *nick, size_t nick_length); + +/** @brief Joins a group designated by `chat_id`. + * + * This function creates a new GC_Chat object, adds it to the chats array, and sends a DHT + * announcement to find peers in the group associated with `chat_id`. Once a peer has been + * found a join attempt will be initiated. + * + * If the group is not password protected password should be set to NULL and password_length should be 0. + * + * Return group_number on success. + * Return -1 if the group object fails to initialize. + * Return -2 if chat_id is NULL or a group with chat_id already exists in the chats array. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the group password. + * Return -6 if the Messenger friend connection fails to initialize. + */ +non_null(1, 2, 3) nullable(5) +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, +                  uint16_t passwd_len); + +/** @brief Disconnects from all peers in a group but saves the group state for later use. + * + * Return true on sucess. + * Return false if the group handler object or chat object is null. + */ +non_null() +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat); + +/** @brief Disconnects from all peers in a group and attempts to reconnect. + * + * All self state and credentials are retained. + * + * Returns 0 on success. + * Returns -1 if the group handler object or chat object is null. + * Returns -2 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_rejoin_group(GC_Session *c, GC_Chat *chat); + +/** @brief Joins a group using the invite data received in a friend's group invite. + * + * The invite is only valid while the inviter is present in the group. + * + * Return group_number on success. + * Return -1 if the invite data is malformed. + * Return -2 if the group object fails to initialize. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the password. + * Return -6 if friend doesn't exist. + * Return -7 if sending packet failed. + */ +non_null(1, 3, 5) nullable(7) +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, +                     size_t nick_length, const uint8_t *passwd, uint16_t passwd_len); + +typedef bool gc_send_group_invite_packet_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *packet, +        uint16_t length); + +/** @brief Invites friend designated by `friendnumber` to chat. + * Packet includes: Type, chat_id, TCP node or packed IP_Port. + * + * Return 0 on success. + * Return -1 if friendnumber does not exist. + * Return -2 on failure to create the invite data. + * Return -3 if the packet fails to send. + */ +non_null() +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, +                     gc_send_group_invite_packet_cb *callback); + +/** @brief Leaves a group and sends an exit broadcast packet with an optional parting message. + * + * All group state is permanently lost, including keys and roles. + * + * Return 0 on success. + * Return -1 if the parting message is too long. + * Return -2 if the parting message failed to send. + */ +non_null(1, 2) nullable(3) +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length); + +/** @brief Returns true if `chat` is a valid group chat. + * + * A valid group chat constitutes an initialized chat instance with a non-zero shared state version. + * The shared state version will be non-zero either if a peer has created the group, or if + * they have ever successfully connected to the group. + */ +non_null() +bool gc_group_is_valid(const GC_Chat *chat); + +/** @brief Returns the number of active groups in `c`. */ +non_null() +uint32_t gc_count_groups(const GC_Session *c); + +/** @brief Returns true if peer_number exists */ +non_null() +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number); + +/** @brief Return group_number's GC_Chat pointer on success + * Return NULL on failure + */ +non_null() +GC_Chat *gc_get_group(const GC_Session *c, int group_number); + +/** @brief Sends a lossy message acknowledgement to peer associated with `gconn`. + * + * If `type` is GR_ACK_RECV we send a read-receipt for read_id's packet. If `type` is GR_ACK_REQ + * we send a request for the respective id's packet. + * + * Requests are limited to one per second per peer. + * + * @retval true on success. + */ +non_null() +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type); + +/** @brief Helper function for `handle_gc_lossless_packet()`. + * + * Note: This function may modify the peer list and change peer numbers. + * + * @retval true if packet is successfully handled. + */ +non_null(1, 2) nullable(4, 7) +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, +                               uint16_t length, uint8_t packet_type, void *userdata); + +/** @brief Handles an invite accept packet. + * + * @retval true on success. + */ +non_null() +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Return true if `chat_id` is not present in our group sessions array. + * + * `length` must be at least CHAT_ID_SIZE bytes in length. + */ +non_null() +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length); + +/** @brief Handles an invite confirmed packet. + * + * Return 0 on success. + * Return -1 if length is invalid. + * Return -2 if data contains invalid chat_id. + * Return -3 if data contains invalid peer info. + * Return -4 if `friend_number` does not designate a valid friend. + * Return -5 if data contains invalid connection info. + */ +non_null() +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Returns the group designated by `public_key`. + * Returns null if group does not exist. + */ +non_null() +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key); + +/** @brief Attempts to add peers from `announces` to our peer list and initiate an invite request. + * + * Returns the number of peers added on success. + * Returns -1 on failure. + */ +non_null() +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count); + +#endif  // GROUP_CHATS_H diff --git a/protocols/Tox/libtox/src/toxcore/group_common.h b/protocols/Tox/libtox/src/toxcore/group_common.h new file mode 100644 index 0000000000..ef901e8a3a --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_common.h @@ -0,0 +1,409 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2022 The TokTok team. + */ + +/** + * Common groupchat data structures. + */ + +#ifndef GROUP_COMMON_H +#define GROUP_COMMON_H + +#include <stdbool.h> +#include <stdint.h> + +#include "DHT.h" +#include "TCP_connection.h" +#include "group_moderation.h" + +#define MAX_GC_PART_MESSAGE_SIZE 128 +#define MAX_GC_NICK_SIZE 128 +#define MAX_GC_TOPIC_SIZE 512 +#define MAX_GC_GROUP_NAME_SIZE 48 +#define GC_MESSAGE_PSEUDO_ID_SIZE 4 +#define GROUP_MAX_MESSAGE_LENGTH  1372 + +/* Max size of a packet chunk. Packets larger than this must be split up. + * + * For an explanation on why this value was chosen, see the following link: https://archive.ph/vsCOG + */ +#define MAX_GC_PACKET_CHUNK_SIZE 500 + +#define MAX_GC_MESSAGE_SIZE GROUP_MAX_MESSAGE_LENGTH +#define MAX_GC_MESSAGE_RAW_SIZE (MAX_GC_MESSAGE_SIZE + GC_MESSAGE_PSEUDO_ID_SIZE) +#define MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE 1373 +#define MAX_GC_CUSTOM_LOSSY_PACKET_SIZE MAX_GC_PACKET_CHUNK_SIZE +#define MAX_GC_PASSWORD_SIZE 32 +#define MAX_GC_SAVED_INVITES 10 +#define MAX_GC_PEERS_DEFAULT 100 +#define MAX_GC_SAVED_TIMEOUTS 12 +#define GC_MAX_SAVED_PEERS 100 +#define GC_SAVED_PEER_SIZE (ENC_PUBLIC_KEY_SIZE + sizeof(Node_format) + sizeof(IP_Port)) + +/* Max size of a complete encrypted packet including headers. */ +#define MAX_GC_PACKET_SIZE (MAX_GC_PACKET_CHUNK_SIZE * 100) + +/* Max number of messages to store in the send/recv arrays */ +#define GCC_BUFFER_SIZE 8192 + +/** Self UDP status. Must correspond to return values from `ipport_self_copy()`. */ +typedef enum Self_UDP_Status { +    SELF_UDP_STATUS_NONE = 0x00, +    SELF_UDP_STATUS_WAN  = 0x01, +    SELF_UDP_STATUS_LAN  = 0x02, +} Self_UDP_Status; + +/** Group exit types. */ +typedef enum Group_Exit_Type { +    GC_EXIT_TYPE_QUIT              = 0x00,  // Peer left the group +    GC_EXIT_TYPE_TIMEOUT           = 0x01,  // Peer connection timed out +    GC_EXIT_TYPE_DISCONNECTED      = 0x02,  // Peer diconnected from group +    GC_EXIT_TYPE_SELF_DISCONNECTED = 0x03,  // Self disconnected from group +    GC_EXIT_TYPE_KICKED            = 0x04,  // Peer was kicked from the group +    GC_EXIT_TYPE_SYNC_ERR          = 0x05,  // Peer failed to sync with the group +    GC_EXIT_TYPE_NO_CALLBACK       = 0x06,  // The peer exit callback should not be triggered +} Group_Exit_Type; + +typedef struct GC_Exit_Info { +    uint8_t  part_message[MAX_GC_PART_MESSAGE_SIZE]; +    uint16_t length; +    Group_Exit_Type exit_type; +} GC_Exit_Info; + +typedef struct GC_PeerAddress { +    uint8_t     public_key[EXT_PUBLIC_KEY_SIZE]; +    IP_Port     ip_port; +} GC_PeerAddress; + +typedef struct GC_Message_Array_Entry { +    uint8_t *data; +    uint16_t data_length; +    uint8_t  packet_type; +    uint64_t message_id; +    uint64_t time_added; +    uint64_t last_send_try; +} GC_Message_Array_Entry; + +typedef struct GC_Connection { +    uint64_t send_message_id;   /* message_id of the next message we send to peer */ + +    uint16_t send_array_start;   /* send_array index of oldest item */ +    GC_Message_Array_Entry *send_array; + +    uint64_t received_message_id;   /* message_id of peer's last message to us */ +    GC_Message_Array_Entry *recv_array; + +    uint64_t    last_chunk_id;  /* The message ID of the last packet fragment we received */ + +    GC_PeerAddress   addr;   /* holds peer's extended real public key and ip_port */ +    uint32_t    public_key_hash;   /* Jenkins one at a time hash of peer's real encryption public key */ + +    uint8_t     session_public_key[ENC_PUBLIC_KEY_SIZE];   /* self session public key for this peer */ +    uint8_t     session_secret_key[ENC_SECRET_KEY_SIZE];   /* self session secret key for this peer */ +    uint8_t     session_shared_key[CRYPTO_SHARED_KEY_SIZE];  /* made with our session sk and peer's session pk */ + +    int         tcp_connection_num; +    uint64_t    last_sent_tcp_relays_time;  /* the last time we attempted to send this peer our tcp relays */ +    uint16_t    tcp_relay_share_index; +    uint64_t    last_received_direct_time;   /* the last time we received a direct UDP packet from this connection */ +    uint64_t    last_sent_ip_time;  /* the last time we sent our ip info to this peer in a ping packet */ + +    Node_format connected_tcp_relays[MAX_FRIEND_TCP_CONNECTIONS]; +    uint16_t    tcp_relays_count; + +    uint64_t    last_received_packet_time;  /* The last time we successfully processed any packet from this peer */ +    uint64_t    last_requested_packet_time;  /* The last time we requested a missing packet from this peer */ +    uint64_t    last_sent_ping_time; +    uint64_t    last_sync_response;  /* the last time we sent this peer a sync response */ +    uint8_t     oob_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; +    bool        self_is_closer; /* true if we're "closer" to the chat_id than this peer (uses real pk's) */ + +    bool        confirmed;  /* true if this peer has given us their info */ +    bool        handshaked;  /* true if we've successfully handshaked with this peer */ +    uint16_t    handshake_attempts; +    uint64_t    last_handshake_request; +    uint64_t    last_handshake_response; +    uint8_t     pending_handshake_type; +    bool        is_pending_handshake_response; +    bool        is_oob_handshake; + +    uint64_t    last_key_rotation;  /* the last time we rotated session keys for this peer */ +    bool        pending_key_rotation_request; + +    bool        pending_delete;  /* true if this peer has been marked for deletion */ +    GC_Exit_Info exit_info; +} GC_Connection; + +/*** + * Group roles. Roles are hierarchical in that each role has a set of privileges plus + * all the privileges of the roles below it. + */ +typedef enum Group_Role { +    /** Group creator. All-powerful. Cannot be demoted or kicked. */ +    GR_FOUNDER   = 0x00, + +    /** +     * May promote or demote peers below them to any role below them. +     * May also kick peers below them and set the topic. +     */ +    GR_MODERATOR = 0x01, + +    /** may interact normally with the group. */ +    GR_USER      = 0x02, + +    /** May not interact with the group but may observe. */ +    GR_OBSERVER  = 0x03, +} Group_Role; + +typedef enum Group_Peer_Status { +    GS_NONE    = 0x00, +    GS_AWAY    = 0x01, +    GS_BUSY    = 0x02, +} Group_Peer_Status; + +/** + * Group voice states. The state determines which Group Roles have permission to speak. + */ +typedef enum Group_Voice_State { +    /** Every group role except Observers may speak. */ +    GV_ALL       = 0x00, + +    /** Only Moderators and the Founder may speak. */ +    GV_MODS      = 0x01, + +    /** Only the Founder may speak. */ +    GV_FOUNDER   = 0x02, +} Group_Voice_State; + +/** Group connection states. */ +typedef enum GC_Conn_State { +    CS_NONE         = 0x00,  // Indicates a group is not initialized +    CS_DISCONNECTED = 0x01,  // Not receiving or sending any packets +    CS_CONNECTING   = 0x02,  // Attempting to establish a connection with peers in the group +    CS_CONNECTED    = 0x03,  // Has successfully received a sync response from a peer in the group +} GC_Conn_State; + +/** Group privacy states. */ +typedef enum Group_Privacy_State { +    GI_PUBLIC   = 0x00,  // Anyone with the chat ID may join the group +    GI_PRIVATE  = 0x01,  // Peers may only join the group via a friend invite +} Group_Privacy_State; + +/** Handshake join types. */ +typedef enum Group_Handshake_Join_Type { +    HJ_PUBLIC = 0x00,   // Indicates the group was joined via the DHT +    HJ_PRIVATE = 0x01,  // Indicates the group was joined via private friend invite +} Group_Handshake_Join_Type; + +typedef struct GC_SavedPeerInfo { +    uint8_t     public_key[ENC_PUBLIC_KEY_SIZE]; +    Node_format tcp_relay; +    IP_Port     ip_port; +} GC_SavedPeerInfo; + +/** Holds info about peers who recently timed out */ +typedef struct GC_TimedOutPeer { +    GC_SavedPeerInfo addr; +    uint64_t    last_seen;  // the time the peer disconnected +    uint64_t    last_reconn_try;  // the last time we tried to establish a new connection +} GC_TimedOutPeer; + +typedef struct GC_Peer { +    /* Below state is sent to other peers in peer info exchange */ +    uint8_t       nick[MAX_GC_NICK_SIZE]; +    uint16_t      nick_length; +    uint8_t       status; + +    /* Below state is local only */ +    Group_Role    role; +    uint32_t      peer_id;    // permanent ID (used for the public API) +    bool          ignore; + +    GC_Connection gconn; +} GC_Peer; + +typedef struct GC_SharedState { +    uint32_t    version; +    uint8_t     founder_public_key[EXT_PUBLIC_KEY_SIZE]; +    uint16_t    maxpeers; +    uint16_t    group_name_len; +    uint8_t     group_name[MAX_GC_GROUP_NAME_SIZE]; +    Group_Privacy_State privacy_state;   // GI_PUBLIC (uses DHT) or GI_PRIVATE (invite only) +    uint16_t    password_length; +    uint8_t     password[MAX_GC_PASSWORD_SIZE]; +    uint8_t     mod_list_hash[MOD_MODERATION_HASH_SIZE]; +    uint32_t    topic_lock; // equal to GC_TOPIC_LOCK_ENABLED when lock is enabled +    Group_Voice_State voice_state; +} GC_SharedState; + +typedef struct GC_TopicInfo { +    uint32_t    version; +    uint16_t    length; +    uint16_t    checksum;  // used for syncing problems. the checksum with the highest value gets priority. +    uint8_t     topic[MAX_GC_TOPIC_SIZE]; +    uint8_t     public_sig_key[SIG_PUBLIC_KEY_SIZE];  // Public signature key of the topic setter +} GC_TopicInfo; + +typedef struct GC_Chat { +    Mono_Time       *mono_time; +    const Logger    *log; +    const Random    *rng; + +    uint32_t        connected_tcp_relays; +    Self_UDP_Status self_udp_status; +    IP_Port         self_ip_port; + +    Networking_Core *net; +    TCP_Connections *tcp_conn; + +    uint64_t        last_checked_tcp_relays; +    Group_Handshake_Join_Type join_type; + +    GC_Peer         *group; +    Moderation      moderation; + +    GC_Conn_State   connection_state; + +    GC_SharedState  shared_state; +    uint8_t         shared_state_sig[SIGNATURE_SIZE];  // signed by founder using the chat secret key + +    GC_TopicInfo    topic_info; +    uint8_t         topic_sig[SIGNATURE_SIZE];  // signed by the peer who set the current topic +    uint16_t        topic_prev_checksum;  // checksum of the previous topic +    uint64_t        topic_time_set; + +    uint16_t    peers_checksum;  // sum of the public key hash of every confirmed peer in the group +    uint16_t    roles_checksum;  // sum of every confirmed peer's role plus the first byte of their public key + +    uint32_t    numpeers; +    int         group_number; + +    uint8_t     chat_public_key[EXT_PUBLIC_KEY_SIZE];  // the chat_id is the sig portion +    uint8_t     chat_secret_key[EXT_SECRET_KEY_SIZE];  // only used by the founder + +    uint8_t     self_public_key[EXT_PUBLIC_KEY_SIZE]; +    uint8_t     self_secret_key[EXT_SECRET_KEY_SIZE]; + +    uint64_t    time_connected; +    uint64_t    last_ping_interval; +    uint64_t    last_sync_request;  // The last time we sent a sync request to any peer +    uint64_t    last_sync_response_peer_list;  // The last time we sent the peer list to any peer +    uint64_t    last_time_peers_loaded; + +    /* keeps track of frequency of new inbound connections */ +    uint8_t     connection_O_metre; +    uint64_t    connection_cooldown_timer; +    bool        block_handshakes; + +    int32_t     saved_invites[MAX_GC_SAVED_INVITES]; +    uint8_t     saved_invites_index; + +    /** A list of recently seen peers in case we disconnect from a private group. +     * Peers are added once they're confirmed, and only if there are vacant +     * spots (older connections get priority). An entry is removed only when the list +     * is full, its respective peer goes offline, and an online peer who isn't yet +     * present in the list can be added. +     */ +    GC_SavedPeerInfo saved_peers[GC_MAX_SAVED_PEERS]; + +    GC_TimedOutPeer timeout_list[MAX_GC_SAVED_TIMEOUTS]; +    size_t      timeout_list_index; +    uint64_t    last_timed_out_reconn_try;  // the last time we tried to reconnect to timed out peers + +    bool        update_self_announces;  // true if we should try to update our announcements +    uint64_t    last_self_announce_check;  // the last time we checked if we should update our announcements +    uint64_t    last_time_self_announce;  // the last time we announced the group +    uint8_t     announced_tcp_relay_pk[CRYPTO_PUBLIC_KEY_SIZE];  // The pk of the last TCP relay we announced + +    uint8_t     m_group_public_key[CRYPTO_PUBLIC_KEY_SIZE];  // public key for group's messenger friend connection +    int         friend_connection_id;  // identifier for group's messenger friend connection +} GC_Chat; + +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED +typedef struct Messenger Messenger; +#endif /* MESSENGER_DEFINED */ + +typedef void gc_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, +                           const uint8_t *data, size_t length, uint32_t message_id, void *user_data); +typedef void gc_private_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, +                                   const uint8_t *data, size_t length, void *user_data); +typedef void gc_custom_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, +                                 size_t length, void *user_data); +typedef void gc_custom_private_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, +        const uint8_t *data, +        size_t length, void *user_data); +typedef void gc_moderation_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, uint32_t target_peer, +                              unsigned int mod_event, void *user_data); +typedef void gc_nick_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, +                               size_t length, void *user_data); +typedef void gc_status_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int status, +                                 void *user_data); +typedef void gc_topic_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, +                                size_t length, void *user_data); +typedef void gc_topic_lock_cb(const Messenger *m, uint32_t group_number, unsigned int topic_lock, void *user_data); +typedef void gc_voice_state_cb(const Messenger *m, uint32_t group_number, unsigned int voice_state, void *user_data); +typedef void gc_peer_limit_cb(const Messenger *m, uint32_t group_number, uint32_t max_peers, void *user_data); +typedef void gc_privacy_state_cb(const Messenger *m, uint32_t group_number, unsigned int state, void *user_data); +typedef void gc_password_cb(const Messenger *m, uint32_t group_number, const uint8_t *data, size_t length, +                            void *user_data); +typedef void gc_peer_join_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data); +typedef void gc_peer_exit_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int exit_type, +                             const uint8_t *nick, size_t nick_len, const uint8_t *data, size_t length, void *user_data); +typedef void gc_self_join_cb(const Messenger *m, uint32_t group_number, void *user_data); +typedef void gc_rejected_cb(const Messenger *m, uint32_t group_number, unsigned int type, void *user_data); + +typedef struct GC_Session { +    Messenger                 *messenger; +    GC_Chat                   *chats; +    struct GC_Announces_List  *announces_list; + +    uint32_t     chats_index; + +    gc_message_cb *message; +    gc_private_message_cb *private_message; +    gc_custom_packet_cb *custom_packet; +    gc_custom_private_packet_cb *custom_private_packet; +    gc_moderation_cb *moderation; +    gc_nick_change_cb *nick_change; +    gc_status_change_cb *status_change; +    gc_topic_change_cb *topic_change; +    gc_topic_lock_cb *topic_lock; +    gc_voice_state_cb *voice_state; +    gc_peer_limit_cb *peer_limit; +    gc_privacy_state_cb *privacy_state; +    gc_password_cb *password; +    gc_peer_join_cb *peer_join; +    gc_peer_exit_cb *peer_exit; +    gc_self_join_cb *self_join; +    gc_rejected_cb *rejected; +} GC_Session; + +/** @brief Adds a new peer to group_number's peer list. + * + * Return peer_number on success. + * Return -1 on failure. + * Return -2 if a peer with public_key is already in our peerlist. + */ +non_null(1, 3) nullable(2) +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key); + +/** @brief Unpacks saved peers from `data` of size `length` into `chat`. + * + * Returns the number of unpacked peers on success. + * Returns -1 on failure. + */ +non_null() +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length); + +/** @brief Packs all valid entries from saved peerlist into `data`. + * + * If `processed` is non-null it will be set to the length of the packed data. + * + * Return the number of packed saved peers on success. + * Return -1 if buffer is too small. + */ +non_null(1, 2) nullable(4) +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed); + +#endif  // GROUP_COMMON_H diff --git a/protocols/Tox/libtox/src/toxcore/group_connection.c b/protocols/Tox/libtox/src/toxcore/group_connection.c new file mode 100644 index 0000000000..86c353c00c --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_connection.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_connection.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "DHT.h" +#include "ccompat.h" +#include "crypto_core.h" +#include "group_chats.h" +#include "group_common.h" +#include "mono_time.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/** Seconds since last direct UDP packet was received before the connection is considered dead */ +#define GCC_UDP_DIRECT_TIMEOUT (GC_PING_TIMEOUT + 4) + +/** Returns true if array entry does not contain an active packet. */ +non_null() +static bool array_entry_is_empty(const GC_Message_Array_Entry *array_entry) +{ +    assert(array_entry != nullptr); +    return array_entry->time_added == 0; +} + +/** @brief Clears an array entry. */ +non_null() +static void clear_array_entry(GC_Message_Array_Entry *const array_entry) +{ +    if (array_entry->data != nullptr) { +        free(array_entry->data); +    } + +    *array_entry = (GC_Message_Array_Entry) { +        nullptr +    }; +} + +/** + * Clears every send array message from queue starting at the index designated by + * `start_id` and ending at `end_id`, and sets the send_message_id for `gconn` + * to `start_id`. + */ +non_null() +static void clear_send_queue_id_range(GC_Connection *gconn, uint64_t start_id, uint64_t end_id) +{ +    const uint16_t start_idx = gcc_get_array_index(start_id); +    const uint16_t end_idx = gcc_get_array_index(end_id); + +    for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { +        GC_Message_Array_Entry *entry = &gconn->send_array[i]; +        clear_array_entry(entry); +    } + +    gconn->send_message_id = start_id; +} + +uint16_t gcc_get_array_index(uint64_t message_id) +{ +    return message_id % GCC_BUFFER_SIZE; +} + +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id) +{ +    gconn->send_message_id = id; +    gconn->send_array_start = id % GCC_BUFFER_SIZE; +} + +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id) +{ +    gconn->received_message_id = id; +} + +/** @brief Puts packet data in array_entry. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool create_array_entry(const Mono_Time *mono_time, GC_Message_Array_Entry *array_entry, const uint8_t *data, +                               uint16_t length, uint8_t packet_type, uint64_t message_id) +{ +    if (length > 0) { +        if (data == nullptr) { +            return false; +        } + +        array_entry->data = (uint8_t *)malloc(sizeof(uint8_t) * length); + +        if (array_entry->data == nullptr) { +            return false; +        } + +        memcpy(array_entry->data, data, length); +    } + +    const uint64_t tm = mono_time_get(mono_time); + +    array_entry->data_length = length; +    array_entry->packet_type = packet_type; +    array_entry->message_id = message_id; +    array_entry->time_added = tm; +    array_entry->last_send_try = tm; + +    return true; +} + +/** @brief Adds data of length to gconn's send_array. + * + * Returns true on success and increments gconn's send_message_id. + */ +non_null(1, 2, 3) nullable(4) +static bool add_to_send_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, const uint8_t *data, +                              uint16_t length, uint8_t packet_type) +{ +    /* check if send_array is full */ +    if ((gconn->send_message_id % GCC_BUFFER_SIZE) == (uint16_t)(gconn->send_array_start - 1)) { +        LOGGER_DEBUG(log, "Send array overflow"); +        return false; +    } + +    const uint16_t idx = gcc_get_array_index(gconn->send_message_id); +    GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + +    if (!array_entry_is_empty(array_entry)) { +        LOGGER_DEBUG(log, "Send array entry isn't empty"); +        return false; +    } + +    if (!create_array_entry(mono_time, array_entry, data, length, packet_type, gconn->send_message_id)) { +        LOGGER_WARNING(log, "Failed to create array entry"); +        return false; +    } + +    ++gconn->send_message_id; + +    return true; +} + +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, +                             uint8_t packet_type) +{ +    const uint64_t message_id = gconn->send_message_id; + +    if (!add_to_send_array(chat->log, chat->mono_time, gconn, data, length, packet_type)) { +        LOGGER_WARNING(chat->log, "Failed to add payload to send array: (type: 0x%02x, length: %d)", packet_type, length); +        return -1; +    } + +    if (!gcc_encrypt_and_send_lossless_packet(chat, gconn, data, length, message_id, packet_type)) { +        LOGGER_DEBUG(chat->log, "Failed to send payload: (type: 0x%02x, length: %d)", packet_type, length); +        return -2; +    } + +    return 0; +} + + +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, +                                        uint16_t length, uint8_t packet_type) +{ +    if (length <= MAX_GC_PACKET_CHUNK_SIZE || data == nullptr) { +        LOGGER_FATAL(chat->log, "invalid length or null data pointer"); +        return false; +    } + +    const uint16_t start_id = gconn->send_message_id; + +    // First packet segment is comprised of packet type + first chunk of payload +    uint8_t chunk[MAX_GC_PACKET_CHUNK_SIZE]; +    chunk[0] = packet_type; +    memcpy(chunk + 1, data, MAX_GC_PACKET_CHUNK_SIZE - 1); + +    if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, MAX_GC_PACKET_CHUNK_SIZE, GP_FRAGMENT)) { +        return false; +    } + +    uint16_t processed = MAX_GC_PACKET_CHUNK_SIZE - 1; + +    // The rest of the segments are added in chunks +    while (length > processed) { +        const uint16_t chunk_len = min_u16(MAX_GC_PACKET_CHUNK_SIZE, length - processed); + +        memcpy(chunk, data + processed, chunk_len); +        processed += chunk_len; + +        if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, chunk_len, GP_FRAGMENT)) { +            clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); +            return false; +        } +    } + +    // empty packet signals the end of the sequence +    if (!add_to_send_array(chat->log, chat->mono_time, gconn, nullptr, 0, GP_FRAGMENT)) { +        clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); +        return false; +    } + +    const uint16_t start_idx = gcc_get_array_index(start_id); +    const uint16_t end_idx = gcc_get_array_index(gconn->send_message_id); + +    for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { +        GC_Message_Array_Entry *entry = &gconn->send_array[i]; + +        if (array_entry_is_empty(entry)) { +            LOGGER_FATAL(chat->log, "array entry for packet chunk is empty"); +            return false; +        } + +        assert(entry->packet_type == GP_FRAGMENT); + +        gcc_encrypt_and_send_lossless_packet(chat, gconn, entry->data, entry->data_length, +                                             entry->message_id, entry->packet_type); +    } + +    return true; +} + +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id) +{ +    uint16_t idx = gcc_get_array_index(message_id); +    GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + +    if (array_entry_is_empty(array_entry)) { +        return true; +    } + +    if (array_entry->message_id != message_id) {  // wrap-around indicates a connection problem +        LOGGER_DEBUG(log, "Wrap-around on message %llu", (unsigned long long)message_id); +        return false; +    } + +    clear_array_entry(array_entry); + +    /* Put send_array_start in proper position */ +    if (idx == gconn->send_array_start) { +        const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + +        while (array_entry_is_empty(&gconn->send_array[idx]) && gconn->send_array_start != end) { +            gconn->send_array_start = (gconn->send_array_start + 1) % GCC_BUFFER_SIZE; +            idx = (idx + 1) % GCC_BUFFER_SIZE; +        } +    } + +    return true; +} + +bool gcc_ip_port_is_set(const GC_Connection *gconn) +{ +    return ipport_isset(&gconn->addr.ip_port); +} + +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp) +{ +    if (ipp != nullptr && ipport_isset(ipp)) { +        gconn->addr.ip_port = *ipp; +    } +} + +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn) +{ +    if (gconn == nullptr || tcp_node == nullptr) { +        return false; +    } + +    if (gconn->tcp_relays_count == 0) { +        return false; +    } + +    const uint32_t rand_idx = random_range_u32(rng, gconn->tcp_relays_count); + +    if (!ipport_isset(&gconn->connected_tcp_relays[rand_idx].ip_port)) { +        return false; +    } + +    *tcp_node = gconn->connected_tcp_relays[rand_idx]; + +    return true; +} + +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node) +{ +    if (gconn == nullptr || tcp_node == nullptr) { +        return -1; +    } + +    if (!ipport_isset(&tcp_node->ip_port)) { +        return -1; +    } + +    for (uint16_t i = 0; i < gconn->tcp_relays_count; ++i) { +        if (pk_equal(gconn->connected_tcp_relays[i].public_key, tcp_node->public_key)) { +            return -2; +        } +    } + +    uint32_t idx = gconn->tcp_relays_count; + +    if (gconn->tcp_relays_count >= MAX_FRIEND_TCP_CONNECTIONS) { +        idx = random_range_u32(rng, gconn->tcp_relays_count); +    } else { +        ++gconn->tcp_relays_count; +    } + +    gconn->connected_tcp_relays[idx] = *tcp_node; + +    return 0; +} + +/** @brief Stores `data` of length `length` in the receive array for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3) nullable(4) +static bool store_in_recv_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, +                                const uint8_t *data, +                                uint16_t length, uint8_t packet_type, uint64_t message_id) +{ +    const uint16_t idx = gcc_get_array_index(message_id); +    GC_Message_Array_Entry *ary_entry = &gconn->recv_array[idx]; + +    if (!array_entry_is_empty(ary_entry)) { +        LOGGER_DEBUG(log, "Recv array is not empty"); +        return false; +    } + +    if (!create_array_entry(mono_time, ary_entry, data, length, packet_type, message_id)) { +        LOGGER_WARNING(log, "Failed to create array entry"); +        return false; +    } + +    return true; +} + +/** + * Reassembles a fragmented packet sequence ending with the data in the receive + * array at slot `message_id - 1` and starting with the last found slot containing + * a GP_FRAGMENT packet when searching backwards in the array. + * + * The fully reassembled packet is stored in `payload`, which must be passed as a + * null pointer, and must be free'd by the caller. + * + * Return the length of the fully reassembled packet on success. + * Return 0 on failure. + */ +non_null(1, 3) nullable(2) +static uint16_t reassemble_packet(const Logger *log, GC_Connection *gconn, uint8_t **payload, uint64_t message_id) +{ +    uint16_t end_idx = gcc_get_array_index(message_id - 1); +    uint16_t start_idx = end_idx; +    uint16_t packet_length = 0; + +    GC_Message_Array_Entry *entry = &gconn->recv_array[end_idx]; + +    // search backwards in recv array until we find an empty slot or a non-fragment packet type +    while (!array_entry_is_empty(entry) && entry->packet_type == GP_FRAGMENT) { +        assert(entry->data != nullptr); +        assert(entry->data_length <= MAX_GC_PACKET_CHUNK_SIZE); + +        const uint16_t diff = packet_length + entry->data_length; + +        assert(diff > packet_length);  // overflow check +        packet_length = diff; + +        if (packet_length > MAX_GC_PACKET_SIZE) { +            LOGGER_ERROR(log, "Payload of size %u exceeded max packet size", packet_length);  // should never happen +            return 0; +        } + +        start_idx = start_idx > 0 ? start_idx - 1 : GCC_BUFFER_SIZE - 1; +        entry = &gconn->recv_array[start_idx]; + +        if (start_idx == end_idx) { +            LOGGER_ERROR(log, "Packet reassemble wrap-around"); +            return 0; +        } +    } + +    if (packet_length == 0) { +        return 0; +    } + +    assert(*payload == nullptr); +    *payload = (uint8_t *)malloc(packet_length); + +    if (*payload == nullptr) { +        LOGGER_ERROR(log, "Failed to allocate %u bytes for payload buffer", packet_length); +        return 0; +    } + +    start_idx = (start_idx + 1) % GCC_BUFFER_SIZE; +    end_idx = (end_idx + 1) % GCC_BUFFER_SIZE; + +    uint16_t processed = 0; + +    for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { +        entry = &gconn->recv_array[i]; + +        assert(processed + entry->data_length <= packet_length); +        memcpy(*payload + processed, entry->data, entry->data_length); +        processed += entry->data_length; + +        clear_array_entry(entry); +    } + +    return processed; +} + +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, +                               GC_Connection *gconn, const uint8_t *chunk, uint16_t length, uint8_t packet_type, +                               uint64_t message_id, void *userdata) +{ +    if (length > 0) { +        if (!store_in_recv_array(chat->log, chat->mono_time, gconn, chunk, length, packet_type, message_id)) { +            return -1; +        } + +        gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); +        gconn->last_chunk_id = message_id; + +        return 1; +    } + +    uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; +    memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + +    uint8_t *payload = nullptr; +    const uint16_t processed_len = reassemble_packet(chat->log, gconn, &payload, message_id); + +    if (processed_len == 0) { +        free(payload); +        return -1; +    } + +    if (!handle_gc_lossless_helper(c, chat, peer_number, payload + 1, processed_len - 1, payload[0], userdata)) { +        free(payload); +        return -1; +    } + +    /* peer number can change from peer add operations in packet handlers */ +    peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); +    gconn = get_gc_connection(chat, peer_number); + +    if (gconn == nullptr) { +        return 0; +    } + +    gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); +    gconn->last_chunk_id = 0; + +    free(payload); + +    return 0; +} + +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, +                                const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, +                                bool direct_conn) +{ +    if (direct_conn) { +        gconn->last_received_direct_time = mono_time_get(mono_time); +    } + +    /* Appears to be a duplicate packet so we discard it */ +    if (message_id < gconn->received_message_id + 1) { +        return 0; +    } + +    if (packet_type == GP_FRAGMENT) { // we handle packet fragments as a special case +        return 3; +    } + +    /* we're missing an older message from this peer so we store it in recv_array */ +    if (message_id > gconn->received_message_id + 1) { +        if (!store_in_recv_array(log, mono_time, gconn, data, length, packet_type, message_id)) { +            return -1; +        } + +        return 1; +    } + +    gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + +    return 2; +} + +/** @brief Handles peer_number's array entry with appropriate handler and clears it from array. + * + * This function increments the received message ID for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3, 5) nullable(6) +static bool process_recv_array_entry(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, +                                     GC_Message_Array_Entry *const array_entry, void *userdata) +{ +    uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; +    memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + +    const bool ret = handle_gc_lossless_helper(c, chat, peer_number, array_entry->data, array_entry->data_length, +                     array_entry->packet_type, userdata); + +    /* peer number can change from peer add operations in packet handlers */ +    peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); +    gconn = get_gc_connection(chat, peer_number); + +    clear_array_entry(array_entry); + +    if (gconn == nullptr) { +        return true; +    } + +    if (!ret) { +        gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_REQ); +        return false; +    } + +    gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_RECV); + +    gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + +    return true; +} + +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, +                          void *userdata) +{ +    if (gconn->last_chunk_id != 0) {  // dont check array if we have an unfinished fragment sequence +        return; +    } + +    const uint16_t idx = (gconn->received_message_id + 1) % GCC_BUFFER_SIZE; +    GC_Message_Array_Entry *const array_entry = &gconn->recv_array[idx]; + +    if (!array_entry_is_empty(array_entry)) { +        process_recv_array_entry(c, chat, gconn, peer_number, array_entry, userdata); +    } +} + +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn) +{ +    const uint64_t tm = mono_time_get(chat->mono_time); +    const uint16_t start = gconn->send_array_start; +    const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + +    GC_Message_Array_Entry *array_entry = &gconn->send_array[start]; + +    if (array_entry_is_empty(array_entry)) { +        return; +    } + +    if (mono_time_is_timeout(chat->mono_time, array_entry->time_added, GC_CONFIRMED_PEER_TIMEOUT)) { +        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); +        LOGGER_DEBUG(chat->log, "Send array stuck; timing out peer"); +        return; +    } + +    for (uint16_t i = start; i != end; i = (i + 1) % GCC_BUFFER_SIZE) { +        array_entry = &gconn->send_array[i]; + +        if (array_entry_is_empty(array_entry)) { +            continue; +        } + +        if (tm == array_entry->last_send_try) { +            continue; +        } + +        const uint64_t delta = array_entry->last_send_try - array_entry->time_added; +        array_entry->last_send_try = tm; + +        /* if this occurrs less than once per second this won't be reliable */ +        if (delta > 1 && is_power_of_2(delta)) { +            gcc_encrypt_and_send_lossless_packet(chat, gconn, array_entry->data, array_entry->data_length, +                                                 array_entry->message_id, array_entry->packet_type); +        } +    } +} + +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length) +{ +    if (packet == nullptr || length == 0) { +        return false; +    } + +    bool direct_send_attempt = false; + +    if (gcc_direct_conn_is_possible(chat, gconn)) { +        if (gcc_conn_is_direct(chat->mono_time, gconn)) { +            return (uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length; +        } + +        if ((uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length) { +            direct_send_attempt = true; +        } +    } + +    const int ret = send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, length); +    return ret == 0 || direct_send_attempt; +} + +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, +        uint16_t length, uint64_t message_id, uint8_t packet_type) +{ +    const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSLESS); +    uint8_t *packet = (uint8_t *)malloc(packet_size); + +    if (packet == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); +        return false; +    } + +    const int enc_len = group_packet_wrap( +                            chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, +                            packet_size, data, length, message_id, packet_type, NET_PACKET_GC_LOSSLESS); + +    if (enc_len < 0) { +        LOGGER_ERROR(chat->log, "Failed to wrap packet (type: 0x%02x, error: %d)", packet_type, enc_len); +        free(packet); +        return false; +    } + +    if (!gcc_send_packet(chat, gconn, packet, (uint16_t)enc_len)) { +        LOGGER_DEBUG(chat->log, "Failed to send packet (type: 0x%02x, enc_len: %d)", packet_type, enc_len); +        free(packet); +        return false; +    } + +    free(packet); + +    return true; +} + +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk) +{ +    encrypt_precompute(sender_pk, gconn->session_secret_key, gconn->session_shared_key); +} + +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn) +{ +    return GCC_UDP_DIRECT_TIMEOUT + gconn->last_received_direct_time > mono_time_get(mono_time); +} + +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn) +{ +    return !net_family_is_unspec(gconn->addr.ip_port.ip.family) && !net_family_is_unspec(net_family(chat->net)); +} + +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, +                           const uint8_t *part_message, uint16_t length) +{ +    if (gconn == nullptr) { +        return; +    } + +    if (gconn->pending_delete) { +        return; +    } + +    gconn->pending_delete = true; +    gconn->exit_info.exit_type = type; + +    kill_tcp_connection_to(tcp_conn, gconn->tcp_connection_num); + +    if (length > 0 && length <= MAX_GC_PART_MESSAGE_SIZE  && part_message != nullptr) { +        memcpy(gconn->exit_info.part_message, part_message, length); +        gconn->exit_info.length = length; +    } +} + +void gcc_peer_cleanup(GC_Connection *gconn) +{ +    for (size_t i = 0; i < GCC_BUFFER_SIZE; ++i) { +        free(gconn->send_array[i].data); +        free(gconn->recv_array[i].data); +    } + +    free(gconn->recv_array); +    free(gconn->send_array); + +    crypto_memunlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); +    crypto_memunlock(gconn->session_shared_key, sizeof(gconn->session_shared_key)); +    crypto_memzero(gconn, sizeof(GC_Connection)); +} + +void gcc_cleanup(const GC_Chat *chat) +{ +    for (uint32_t i = 0; i < chat->numpeers; ++i) { +        GC_Connection *gconn = get_gc_connection(chat, i); +        assert(gconn != nullptr); + +        gcc_peer_cleanup(gconn); +    } +} + +#endif // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_connection.h b/protocols/Tox/libtox/src/toxcore/group_connection.h new file mode 100644 index 0000000000..2202c7ab36 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_connection.h @@ -0,0 +1,189 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CONNECTION_H +#define GROUP_CONNECTION_H + +#include "group_common.h" + +/* Max number of TCP relays we share with a peer on handshake */ +#define GCC_MAX_TCP_SHARED_RELAYS 3 + +/** Marks a peer for deletion. If gconn is null or already marked for deletion this function has no effect. */ +non_null(1, 2) nullable(4) +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, +                           const uint8_t *part_message, uint16_t length); + +/** @brief Decides if message need to be put in recv_array or immediately handled. + * + * Return 3 if message is in correct sequence and is a fragment packet. + * Return 2 if message is in correct sequence and may be handled immediately. + * Return 1 if packet is out of sequence and added to recv_array. + * Return 0 if message is a duplicate. + * Return -1 on failure + */ +non_null(1, 2, 3) nullable(4) +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, +                                const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, +                                bool direct_conn); + +/** @brief Handles a packet fragment. + * + * If the fragment is incomplete, it gets stored in the recv + * array. Otherwise the segment is re-assembled into a complete + * payload and processed. + * + * Return 1 if fragment is successfully handled and is not the end of the sequence. + * Return 0 if fragment is the end of a sequence and successfully handled. + * Return -1 on failure. + */ +non_null(1, 2, 4) nullable(5, 9) +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, +                               const uint8_t *chunk, uint16_t length, uint8_t packet_type,  uint64_t message_id, +                               void *userdata); + +/** @brief Return array index for message_id */ +uint16_t gcc_get_array_index(uint64_t message_id); + +/** @brief Removes send_array item with message_id. + * + * Return true on success. + */ +non_null() +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id); + +/** @brief Sets the send_message_id and send_array_start for `gconn` to `id`. + * + * This should only be used to initialize a new lossless connection. + */ +non_null() +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id); + +/** @brief Sets the received_message_id for `gconn` to `id`. */ +non_null() +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id); + +/** + * @brief Returns true if the ip_port is set for gconn. + */ +non_null() +bool gcc_ip_port_is_set(const GC_Connection *gconn); + +/** + * @brief Sets the ip_port for gconn to ipp. + * + * If ipp is not set this function has no effect. + */ +non_null(1) nullable(2) +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp); + +/** @brief Copies a random TCP relay node from gconn to tcp_node. + * + * Return true on success. + */ +non_null() +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn); + +/** @brief Saves tcp_node to gconn's list of connected tcp relays. + * + * If relays list is full a random node is overwritten with the new node. + * + * Return 0 on success. + * Return -1 on failure. + * Return -2 if node is already in list. + */ +non_null() +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node); + +/** @brief Checks for and handles messages that are in proper sequence in gconn's recv_array. + * This should always be called after a new packet is successfully handled. + */ +non_null(1, 2, 3) nullable(5) +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, +                          void *userdata); + +/** @brief Attempts to re-send lossless packets that have not yet received an ack. */ +non_null() +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn); + +/** + * Uses public encryption key `sender_pk` and the shared secret key associated with `gconn` + * to generate a shared 32-byte encryption key that can be used by the owners of both keys for symmetric + * encryption and decryption. + * + * Puts the result in the shared session key buffer for `gconn`, which must have room for + * CRYPTO_SHARED_KEY_SIZE bytes. This resulting shared key should be treated as a secret key. + */ +non_null() +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk); + +/** @brief Return true if we have a direct connection with `gconn`. */ +non_null() +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn); + +/** @brief Return true if a direct UDP connection is possible with `gconn`. */ +non_null() +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Sends a packet to the peer associated with gconn. + * + * This is a lower level function that does not encrypt or wrap the packet. + * + * Return true on success. + */ +non_null() +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length); + +/** @brief Sends a lossless packet to `gconn` comprised of `data` of size `length`. + * + * This function will add the packet to the lossless send array, encrypt/wrap it using the + * shared key associated with `gconn`, and send it over the wire. + * + * Return 0 on success. + * Return -1 if the packet couldn't be added to the send array. + * Return -2 if the packet failed to be encrypted or failed to send. + */ +non_null(1, 2) nullable(3) +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, +                             uint8_t packet_type); + +/** @brief Splits a lossless packet up into fragments, wraps each fragment in a GP_FRAGMENT + * header, encrypts them, and send them in succession. + * + * This function will first try to add each packet fragment to the send array as an atomic + * unit. If any chunk fails to be added the process will be reversed and an error will be + * returned. Otherwise it will then try to send all the fragments in succession. + * + * Return true if all fragments are successfully added to the send array. + */ +non_null() +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, +                                        uint16_t length, uint8_t packet_type); + + +/** @brief Encrypts `data` of `length` bytes, designated by `message_id`, using the shared key + * associated with `gconn` and sends lossless packet over the wire. + * + * This function does not add the packet to the send array. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, +        uint16_t length, uint64_t message_id, uint8_t packet_type); + +/** @brief Called when a peer leaves the group. */ +non_null() +void gcc_peer_cleanup(GC_Connection *gconn); + +/** @brief Called on group exit. */ +non_null() +void gcc_cleanup(const GC_Chat *chat); + +#endif  // GROUP_CONNECTION_H diff --git a/protocols/Tox/libtox/src/toxcore/group_moderation.c b/protocols/Tox/libtox/src/toxcore/group_moderation.c index dea38a648b..b16c397cbe 100644 --- a/protocols/Tox/libtox/src/toxcore/group_moderation.c +++ b/protocols/Tox/libtox/src/toxcore/group_moderation.c @@ -27,6 +27,10 @@ static_assert(MOD_MAX_NUM_SANCTIONS * MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_C                "MOD_MAX_NUM_SANCTIONS must be able to fit inside the maximum allowed payload size");  static_assert(MOD_MAX_NUM_MODERATORS * MOD_LIST_ENTRY_SIZE <= MAX_PACKET_SIZE_NO_HEADERS,                "MOD_MAX_NUM_MODERATORS must be able to fit insize the maximum allowed payload size"); +static_assert(MOD_MAX_NUM_MODERATORS <= MOD_MAX_NUM_MODERATORS_LIMIT, +              "MOD_MAX_NUM_MODERATORS must be <= MOD_MAX_NUM_MODERATORS_LIMIT"); +static_assert(MOD_MAX_NUM_SANCTIONS <= MOD_MAX_NUM_SANCTIONS_LIMIT, +              "MOD_MAX_NUM_SANCTIONS must be <= MOD_MAX_NUM_SANCTIONS_LIMIT");  uint16_t mod_list_packed_size(const Moderation *moderation)  { @@ -398,7 +402,7 @@ int sanctions_list_unpack(Mod_Sanction *sanctions, Mod_Sanction_Creds *creds, ui   */  non_null(4) nullable(1)  static bool sanctions_list_make_hash(const Mod_Sanction *sanctions, uint32_t new_version, uint16_t num_sanctions, -                                    uint8_t *hash) +                                     uint8_t *hash)  {      if (num_sanctions == 0 || sanctions == nullptr) {          memset(hash, 0, MOD_SANCTION_HASH_SIZE); @@ -568,7 +572,7 @@ bool sanctions_list_check_integrity(const Moderation *moderation, const Mod_Sanc  }  /** @brief Validates a sanctions list if credentials are supplied. If successful, - *   or if no credentials are supplid, assigns new sanctions list and credentials + *   or if no credentials are supplied, assigns new sanctions list and credentials   *   to moderation object.   *   * @param moderation The moderation object being operated on. diff --git a/protocols/Tox/libtox/src/toxcore/group_moderation.h b/protocols/Tox/libtox/src/toxcore/group_moderation.h index 36b44a42f0..8b2a73869a 100644 --- a/protocols/Tox/libtox/src/toxcore/group_moderation.h +++ b/protocols/Tox/libtox/src/toxcore/group_moderation.h @@ -36,9 +36,17 @@ extern "C" {  /* The max size of a groupchat packet with 100 bytes reserved for header data */  #define MAX_PACKET_SIZE_NO_HEADERS 49900 -/* These values must take into account the maximum allowed packet size and headers. */ -#define MOD_MAX_NUM_MODERATORS (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) -#define MOD_MAX_NUM_SANCTIONS  (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) +/* The maximum possible number of moderators that can be sent in a group packet sequence. */ +#define MOD_MAX_NUM_MODERATORS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) + +/* The maximum number of moderators that we allow in a group: 100 */ +#define MOD_MAX_NUM_MODERATORS       ((MOD_MAX_NUM_MODERATORS_LIMIT / 16) + 3) + +/* The maximum number of sanctions that be sent in a group packet sequence. */ +#define MOD_MAX_NUM_SANCTIONS_LIMIT  (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) + +/* The maximum number of sanctions that we allow in a group: 30 */ +#define MOD_MAX_NUM_SANCTIONS        (MOD_MAX_NUM_SANCTIONS_LIMIT / 12)  typedef enum Mod_Sanction_Type {      SA_OBSERVER = 0x00, diff --git a/protocols/Tox/libtox/src/toxcore/group_onion_announce.c b/protocols/Tox/libtox/src/toxcore/group_onion_announce.c new file mode 100644 index 0000000000..b797770e52 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_onion_announce.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#include "group_onion_announce.h" + +#include <assert.h> +#include <string.h> + +#include "ccompat.h" + +static_assert(GCA_ANNOUNCE_MAX_SIZE <= ONION_MAX_EXTRA_DATA_SIZE, +              "GC_Announce does not fit into the onion packet extra data"); + +static pack_extra_data_cb pack_group_announces; +non_null() +static int pack_group_announces(void *object, const Logger *logger, const Mono_Time *mono_time, +                                uint8_t num_nodes, uint8_t *plain, uint16_t plain_size, +                                uint8_t *response, uint16_t response_size, uint16_t offset) +{ +    GC_Announces_List *gc_announces_list = (GC_Announces_List *)object; +    GC_Public_Announce public_announce; + +    if (gca_unpack_public_announce(logger, plain, plain_size, +                                   &public_announce) == -1) { +        LOGGER_WARNING(logger, "Failed to unpack public group announce"); +        return -1; +    } + +    const GC_Peer_Announce *new_announce = gca_add_announce(mono_time, gc_announces_list, &public_announce); + +    if (new_announce == nullptr) { +        LOGGER_ERROR(logger, "Failed to add group announce"); +        return -1; +    } + +    GC_Announce gc_announces[GCA_MAX_SENT_ANNOUNCES]; +    const int num_ann = gca_get_announces(gc_announces_list, +                                          gc_announces, +                                          GCA_MAX_SENT_ANNOUNCES, +                                          public_announce.chat_public_key, +                                          new_announce->base_announce.peer_public_key); + +    if (num_ann < 0) { +        LOGGER_ERROR(logger, "failed to get group announce"); +        return -1; +    } + +    assert(num_ann <= UINT8_MAX); + +    size_t announces_length = 0; + +    if (gca_pack_announces_list(logger, response + offset, response_size - offset, gc_announces, (uint8_t)num_ann, +                                &announces_length) != num_ann) { +        LOGGER_WARNING(logger, "Failed to pack group announces list"); +        return -1; +    } + +    return announces_length; +} + +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a) +{ +    onion_announce_extra_data_callback(onion_a, GCA_MAX_SENT_ANNOUNCES * sizeof(GC_Announce), pack_group_announces, +                                       group_announce); +} + +#ifndef VANILLA_NACL + +int create_gca_announce_request( +    const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, +    const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, +    const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, +    const uint8_t *gc_data, uint16_t gc_data_length) +{ +    if (max_packet_length < ONION_ANNOUNCE_REQUEST_MAX_SIZE || gc_data_length == 0) { +        return -1; +    } + +    uint8_t plain[ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + +                                     ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + GCA_ANNOUNCE_MAX_SIZE]; +    uint8_t *position_in_plain = plain; +    const size_t encrypted_size = sizeof(plain) - GCA_ANNOUNCE_MAX_SIZE + gc_data_length; + +    memcpy(plain, ping_id, ONION_PING_ID_SIZE); +    position_in_plain += ONION_PING_ID_SIZE; + +    memcpy(position_in_plain, client_id, CRYPTO_PUBLIC_KEY_SIZE); +    position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + +    memcpy(position_in_plain, data_public_key, CRYPTO_PUBLIC_KEY_SIZE); +    position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + +    memcpy(position_in_plain, &sendback_data, sizeof(sendback_data)); +    position_in_plain += sizeof(sendback_data); + +    memcpy(position_in_plain, gc_data, gc_data_length); + +    packet[0] = NET_PACKET_ANNOUNCE_REQUEST; +    random_nonce(rng, packet + 1); +    memcpy(packet + 1 + CRYPTO_NONCE_SIZE, public_key, CRYPTO_PUBLIC_KEY_SIZE); + +    const int len = encrypt_data(dest_client_id, secret_key, packet + 1, plain, +                                 encrypted_size, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE); + +    const uint32_t full_length = (uint32_t)len + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE; + +    if (full_length != ONION_ANNOUNCE_REQUEST_MIN_SIZE + gc_data_length) { +        return -1; +    } + +    return full_length; +} +#endif  // VANILLA_NACL diff --git a/protocols/Tox/libtox/src/toxcore/group_onion_announce.h b/protocols/Tox/libtox/src/toxcore/group_onion_announce.h new file mode 100644 index 0000000000..5c6d64ec59 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_onion_announce.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#ifndef C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H +#define C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H + +#include "group_announce.h" +#include "onion_announce.h" + +non_null() +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a); + +non_null() +int create_gca_announce_request( +    const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, +    const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, +    const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, +    const uint8_t *gc_data, uint16_t gc_data_length); + +#endif  // C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H diff --git a/protocols/Tox/libtox/src/toxcore/group_pack.c b/protocols/Tox/libtox/src/toxcore/group_pack.c new file mode 100644 index 0000000000..ecdd965610 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_pack.c @@ -0,0 +1,423 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#include "group_pack.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "ccompat.h" +#include "util.h" + +non_null() +static bool load_unpack_state_values(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 8)) { +        LOGGER_ERROR(chat->log, "Group state values array malformed"); +        return false; +    } + +    bool manually_disconnected = false; +    uint8_t privacy_state = 0; +    uint8_t voice_state = 0; + +    if (!(bin_unpack_bool(bu, &manually_disconnected) +            && bin_unpack_u16(bu, &chat->shared_state.group_name_len) +            && bin_unpack_u08(bu, &privacy_state) +            && bin_unpack_u16(bu, &chat->shared_state.maxpeers) +            && bin_unpack_u16(bu, &chat->shared_state.password_length) +            && bin_unpack_u32(bu, &chat->shared_state.version) +            && bin_unpack_u32(bu, &chat->shared_state.topic_lock) +            && bin_unpack_u08(bu, &voice_state))) { +        LOGGER_ERROR(chat->log, "Failed to unpack state value"); +        return false; +    } + +    chat->connection_state = manually_disconnected ? CS_DISCONNECTED : CS_CONNECTING; +    chat->shared_state.privacy_state = (Group_Privacy_State)privacy_state; +    chat->shared_state.voice_state = (Group_Voice_State)voice_state; + +    // we always load saved groups as private in case the group became private while we were offline. +    // this will have no detrimental effect if the group is public, as the correct privacy +    // state will be set via sync. +    chat->join_type = HJ_PRIVATE; + +    return true; +} + +non_null() +static bool load_unpack_state_bin(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 5)) { +        LOGGER_ERROR(chat->log, "Group state binary array malformed"); +        return false; +    } + +    if (!(bin_unpack_bin_fixed(bu, chat->shared_state_sig, SIGNATURE_SIZE) +            && bin_unpack_bin_fixed(bu, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE) +            && bin_unpack_bin_fixed(bu, chat->shared_state.group_name, chat->shared_state.group_name_len) +            && bin_unpack_bin_fixed(bu, chat->shared_state.password, chat->shared_state.password_length) +            && bin_unpack_bin_fixed(bu, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE))) { +        LOGGER_ERROR(chat->log, "Failed to unpack state binary data"); +        return false; +    } + +    return true; +} + +non_null() +static bool load_unpack_topic_info(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 6)) { +        LOGGER_ERROR(chat->log, "Group topic array malformed"); +        return false; +    } + +    if (!(bin_unpack_u32(bu, &chat->topic_info.version) +            && bin_unpack_u16(bu, &chat->topic_info.length) +            && bin_unpack_u16(bu, &chat->topic_info.checksum) +            && bin_unpack_bin_fixed(bu, chat->topic_info.topic, chat->topic_info.length) +            && bin_unpack_bin_fixed(bu, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) +            && bin_unpack_bin_fixed(bu, chat->topic_sig, SIGNATURE_SIZE))) { +        LOGGER_ERROR(chat->log, "Failed to unpack topic info"); +        return false; +    } + +    return true; +} + +non_null() +static bool load_unpack_mod_list(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 2)) { +        LOGGER_ERROR(chat->log, "Group mod list array malformed"); +        return false; +    } + +    if (!bin_unpack_u16(bu, &chat->moderation.num_mods)) { +        LOGGER_ERROR(chat->log, "Failed to unpack mod list value"); +        return false; +    } + +    if (chat->moderation.num_mods == 0) { +        bin_unpack_nil(bu); +        return true; +    } + +    if (chat->moderation.num_mods > MOD_MAX_NUM_MODERATORS) { +        LOGGER_ERROR(chat->log, "moderation count %u exceeds maximum %u", chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); +        return false; +    } + +    uint8_t *packed_mod_list = (uint8_t *)malloc(chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE); + +    if (packed_mod_list == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for packed mod list"); +        return false; +    } + +    const size_t packed_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + +    if (!bin_unpack_bin_fixed(bu, packed_mod_list, packed_size)) { +        LOGGER_ERROR(chat->log, "Failed to unpack mod list binary data"); +        free(packed_mod_list); +        return false; +    } + +    if (mod_list_unpack(&chat->moderation, packed_mod_list, packed_size, chat->moderation.num_mods) == -1) { +        LOGGER_ERROR(chat->log, "Failed to unpack mod list info"); +        free(packed_mod_list); +        return false; +    } + +    free(packed_mod_list); + +    return true; +} + +non_null() +static bool load_unpack_keys(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 4)) { +        LOGGER_ERROR(chat->log, "Group keys array malformed"); +        return false; +    } + +    if (!(bin_unpack_bin_fixed(bu, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE) +            && bin_unpack_bin_fixed(bu, chat->chat_secret_key, EXT_SECRET_KEY_SIZE) +            && bin_unpack_bin_fixed(bu, chat->self_public_key, EXT_PUBLIC_KEY_SIZE) +            && bin_unpack_bin_fixed(bu, chat->self_secret_key, EXT_SECRET_KEY_SIZE))) { +        LOGGER_ERROR(chat->log, "Failed to unpack keys"); +        return false; +    } + +    return true; +} + +non_null() +static bool load_unpack_self_info(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 4)) { +        LOGGER_ERROR(chat->log, "Group self info array malformed"); +        return false; +    } + +    uint8_t self_nick[MAX_GC_NICK_SIZE]; +    uint16_t self_nick_len = 0; +    uint8_t self_role = GR_USER; +    uint8_t self_status = GS_NONE; + +    if (!(bin_unpack_u16(bu, &self_nick_len) +            && bin_unpack_u08(bu, &self_role) +            && bin_unpack_u08(bu, &self_status))) { +        LOGGER_ERROR(chat->log, "Failed to unpack self values"); +        return false; +    } + +    assert(self_nick_len <= MAX_GC_NICK_SIZE); + +    if (!bin_unpack_bin_fixed(bu, self_nick, self_nick_len)) { +        LOGGER_ERROR(chat->log, "Failed to unpack self nick bytes"); +        return false; +    } + +    // we have to add ourself before setting self info +    if (peer_add(chat, nullptr, chat->self_public_key) != 0) { +        LOGGER_ERROR(chat->log, "Failed to add self to peer list"); +        return false; +    } + +    assert(chat->numpeers > 0); + +    GC_Peer *self = &chat->group[0]; + +    memcpy(self->gconn.addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); +    memcpy(self->nick, self_nick, self_nick_len); +    self->nick_length = self_nick_len; +    self->role = (Group_Role)self_role; +    self->status = (Group_Peer_Status)self_status; +    self->gconn.confirmed = true; + +    return true; +} + +non_null() +static bool load_unpack_saved_peers(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 2)) { +        LOGGER_ERROR(chat->log, "Group saved peers array malformed"); +        return false; +    } + +    // Saved peers +    uint16_t saved_peers_size = 0; + +    if (!bin_unpack_u16(bu, &saved_peers_size)) { +        LOGGER_ERROR(chat->log, "Failed to unpack saved peers value"); +        return false; +    } + +    if (saved_peers_size == 0) { +        bin_unpack_nil(bu); +        return true; +    } + +    uint8_t *saved_peers = (uint8_t *)malloc(saved_peers_size * GC_SAVED_PEER_SIZE); + +    if (saved_peers == nullptr) { +        LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peer list"); +        return false; +    } + +    if (!bin_unpack_bin_fixed(bu, saved_peers, saved_peers_size)) { +        LOGGER_ERROR(chat->log, "Failed to unpack saved peers binary data"); +        free(saved_peers); +        return false; +    } + +    if (unpack_gc_saved_peers(chat, saved_peers, saved_peers_size) == -1) { +        LOGGER_ERROR(chat->log, "Failed to unpack saved peers");  // recoverable error +    } + +    free(saved_peers); + +    return true; +} + +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu) +{ +    if (!bin_unpack_array_fixed(bu, 7)) { +        LOGGER_ERROR(chat->log, "Group info array malformed"); +        return false; +    } + +    return load_unpack_state_values(chat, bu) +           && load_unpack_state_bin(chat, bu) +           && load_unpack_topic_info(chat, bu) +           && load_unpack_mod_list(chat, bu) +           && load_unpack_keys(chat, bu) +           && load_unpack_self_info(chat, bu) +           && load_unpack_saved_peers(chat, bu); +} + +non_null() +static void save_pack_state_values(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 8); +    bin_pack_bool(bp, chat->connection_state == CS_DISCONNECTED); // 1 +    bin_pack_u16(bp, chat->shared_state.group_name_len); // 2 +    bin_pack_u08(bp, chat->shared_state.privacy_state); // 3 +    bin_pack_u16(bp, chat->shared_state.maxpeers); // 4 +    bin_pack_u16(bp, chat->shared_state.password_length); // 5 +    bin_pack_u32(bp, chat->shared_state.version); // 6 +    bin_pack_u32(bp, chat->shared_state.topic_lock); // 7 +    bin_pack_u08(bp, chat->shared_state.voice_state); // 8 +} + +non_null() +static void save_pack_state_bin(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 5); + +    bin_pack_bin(bp, chat->shared_state_sig, SIGNATURE_SIZE); // 1 +    bin_pack_bin(bp, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE); // 2 +    bin_pack_bin(bp, chat->shared_state.group_name, chat->shared_state.group_name_len); // 3 +    bin_pack_bin(bp, chat->shared_state.password, chat->shared_state.password_length); // 4 +    bin_pack_bin(bp, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); // 5 +} + +non_null() +static void save_pack_topic_info(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 6); + +    bin_pack_u32(bp, chat->topic_info.version); // 1 +    bin_pack_u16(bp, chat->topic_info.length); // 2 +    bin_pack_u16(bp, chat->topic_info.checksum); // 3 +    bin_pack_bin(bp, chat->topic_info.topic, chat->topic_info.length); // 4 +    bin_pack_bin(bp, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE); // 5 +    bin_pack_bin(bp, chat->topic_sig, SIGNATURE_SIZE); // 6 +} + +non_null() +static void save_pack_mod_list(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 2); + +    const uint16_t num_mods = min_u16(chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); + +    if (num_mods == 0) { +        bin_pack_u16(bp, num_mods); // 1 +        bin_pack_nil(bp); // 2 +        return; +    } + +    uint8_t *packed_mod_list = (uint8_t *)malloc(num_mods * MOD_LIST_ENTRY_SIZE); + +    // we can still recover without the mod list +    if (packed_mod_list == nullptr) { +        bin_pack_u16(bp, 0); // 1 +        bin_pack_nil(bp); // 2 +        LOGGER_ERROR(chat->log, "Failed to allocate memory for moderation list"); +        return; +    } + +    bin_pack_u16(bp, num_mods); // 1 + +    mod_list_pack(&chat->moderation, packed_mod_list); + +    const size_t packed_size = num_mods * MOD_LIST_ENTRY_SIZE; + +    bin_pack_bin(bp, packed_mod_list, packed_size); // 2 + +    free(packed_mod_list); +} + +non_null() +static void save_pack_keys(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 4); + +    bin_pack_bin(bp, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE); // 1 +    bin_pack_bin(bp, chat->chat_secret_key, EXT_SECRET_KEY_SIZE); // 2 +    bin_pack_bin(bp, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); // 3 +    bin_pack_bin(bp, chat->self_secret_key, EXT_SECRET_KEY_SIZE); // 4 +} + +non_null() +static void save_pack_self_info(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 4); + +    const GC_Peer *self = &chat->group[0]; + +    assert(self->nick_length <= MAX_GC_NICK_SIZE); + +    bin_pack_u16(bp, self->nick_length); // 1 +    bin_pack_u08(bp, (uint8_t)self->role); // 2 +    bin_pack_u08(bp, (uint8_t)self->status); // 3 +    bin_pack_bin(bp, self->nick, self->nick_length); // 4 +} + +non_null() +static void save_pack_saved_peers(const GC_Chat *chat, Bin_Pack *bp) +{ +    bin_pack_array(bp, 2); + +    uint8_t *saved_peers = (uint8_t *)malloc(GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE); + +    // we can still recover without the saved peers list +    if (saved_peers == nullptr) { +        bin_pack_u16(bp, 0); // 1 +        bin_pack_nil(bp); // 2 +        LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peers list"); +        return; +    } + +    uint16_t packed_size = 0; +    const int count = pack_gc_saved_peers(chat, saved_peers, GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE, &packed_size); + +    if (count < 0) { +        LOGGER_ERROR(chat->log, "Failed to pack saved peers"); +    } + +    bin_pack_u16(bp, packed_size); // 1 + +    if (packed_size == 0) { +        bin_pack_nil(bp); // 2 +        free(saved_peers); +        return; +    } + +    bin_pack_bin(bp, saved_peers, packed_size); // 2 + +    free(saved_peers); +} + +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp) +{ +    if (chat->numpeers == 0) { +        LOGGER_ERROR(chat->log, "Failed to pack group: numpeers is 0"); +        return; +    } + +    bin_pack_array(bp, 7); + +    save_pack_state_values(chat, bp); // 1 +    save_pack_state_bin(chat, bp); // 2 +    save_pack_topic_info(chat, bp); // 3 +    save_pack_mod_list(chat, bp); // 4 +    save_pack_keys(chat, bp); // 5 +    save_pack_self_info(chat, bp); // 6 +    save_pack_saved_peers(chat, bp); // 7 +} diff --git a/protocols/Tox/libtox/src/toxcore/group_pack.h b/protocols/Tox/libtox/src/toxcore/group_pack.h new file mode 100644 index 0000000000..ae831ac708 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/group_pack.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#ifndef GROUP_PACK_H +#define GROUP_PACK_H + +#include <stdbool.h> + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_common.h" + +/** + * Packs group data from `chat` into `mp` in binary format. Parallel to the + * `gc_load_unpack_group` function. + */ +non_null() +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp); + +/** + * Unpacks binary group data from `obj` into `chat`. Parallel to the `gc_save_pack_group` + * function. + * + * Return true if unpacking is successful. + */ +non_null() +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu); + +#endif // GROUP_PACK_H diff --git a/protocols/Tox/libtox/src/toxcore/net_crypto.c b/protocols/Tox/libtox/src/toxcore/net_crypto.c index c34a45734c..71f6e39e64 100644 --- a/protocols/Tox/libtox/src/toxcore/net_crypto.c +++ b/protocols/Tox/libtox/src/toxcore/net_crypto.c @@ -223,15 +223,15 @@ static int create_cookie_request(const Net_Crypto *c, uint8_t *packet, const uin      memcpy(plain, c->self_public_key, CRYPTO_PUBLIC_KEY_SIZE);      memcpy(plain + CRYPTO_PUBLIC_KEY_SIZE, padding, CRYPTO_PUBLIC_KEY_SIZE);      memcpy(plain + (CRYPTO_PUBLIC_KEY_SIZE * 2), &number, sizeof(uint64_t)); - -    dht_get_shared_key_sent(c->dht, shared_key, dht_public_key); +    const uint8_t *tmp_shared_key = dht_get_shared_key_sent(c->dht, dht_public_key); +    memcpy(shared_key, tmp_shared_key, CRYPTO_SHARED_KEY_SIZE);      uint8_t nonce[CRYPTO_NONCE_SIZE];      random_nonce(c->rng, nonce);      packet[0] = NET_PACKET_COOKIE_REQUEST;      memcpy(packet + 1, dht_get_self_public_key(c->dht), CRYPTO_PUBLIC_KEY_SIZE);      memcpy(packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE);      const int len = encrypt_data_symmetric(shared_key, nonce, plain, sizeof(plain), -                                     packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); +                                           packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE);      if (len != COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE) {          return -1; @@ -341,10 +341,11 @@ static int handle_cookie_request(const Net_Crypto *c, uint8_t *request_plain, ui      }      memcpy(dht_public_key, packet + 1, CRYPTO_PUBLIC_KEY_SIZE); -    dht_get_shared_key_sent(c->dht, shared_key, dht_public_key); +    const uint8_t *tmp_shared_key = dht_get_shared_key_sent(c->dht, dht_public_key); +    memcpy(shared_key, tmp_shared_key, CRYPTO_SHARED_KEY_SIZE);      const int len = decrypt_data_symmetric(shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, -                                     packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, -                                     request_plain); +                                           packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, +                                           request_plain);      if (len != COOKIE_REQUEST_PLAIN_LENGTH) {          return -1; @@ -488,7 +489,7 @@ static int create_crypto_handshake(const Net_Crypto *c, uint8_t *packet, const u      random_nonce(c->rng, packet + 1 + COOKIE_LENGTH);      const int len = encrypt_data(peer_real_pk, c->self_secret_key, packet + 1 + COOKIE_LENGTH, plain, sizeof(plain), -                           packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); +                                 packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE);      if (len != HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE)) {          return -1; @@ -541,8 +542,8 @@ static bool handle_crypto_handshake(const Net_Crypto *c, uint8_t *nonce, uint8_t      uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH];      const int len = decrypt_data(cookie_plain, c->self_secret_key, packet + 1 + COOKIE_LENGTH, -                           packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, -                           HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); +                                 packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, +                                 HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain);      if (len != sizeof(plain)) {          return false; @@ -1273,7 +1274,7 @@ static int handle_data_packet(const Net_Crypto *c, int crypt_connection_id, uint      const uint16_t diff = num - num_cur_nonce;      increment_nonce_number(nonce, diff);      const int len = decrypt_data_symmetric(conn->shared_key, nonce, packet + 1 + sizeof(uint16_t), -                                     length - (1 + sizeof(uint16_t)), data); +                                           length - (1 + sizeof(uint16_t)), data);      if ((unsigned int)len != length - crypto_packet_overhead) {          return -1; @@ -1515,7 +1516,7 @@ static void connection_kill(Net_Crypto *c, int crypt_connection_id, void *userda      if (conn->connection_status_callback != nullptr) {          conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, -                false, userdata); +                                         false, userdata);      }      while (true) { /* TODO(irungentoo): is this really the best way to do this? */ @@ -1602,7 +1603,7 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const          if (conn->connection_status_callback != nullptr) {              conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, -                    true, userdata); +                                             true, userdata);          }      } @@ -1616,8 +1617,8 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const          }          const int requested = handle_request_packet(c->mono_time, &conn->send_array, -                                              real_data, real_length, -                                              &rtt_calc_time, rtt_time); +                              real_data, real_length, +                              &rtt_calc_time, rtt_time);          if (requested == -1) {              return -1; @@ -2657,7 +2658,7 @@ static void send_crypto_packets(Net_Crypto *c)                                                       &conn->recv_array) + 1.0) / (conn->packet_recv_rate + 1.0));                  const double request_packet_interval2 = ((CRYPTO_PACKET_MIN_RATE / conn->packet_recv_rate) * -                                                   (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL; +                                                        (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL;                  if (request_packet_interval2 < request_packet_interval) {                      request_packet_interval = request_packet_interval2; @@ -2750,7 +2751,7 @@ static void send_crypto_packets(Net_Crypto *c)                                                   PACKET_COUNTER_AVERAGE_INTERVAL));                      const double min_speed_request = 1000.0 * (((double)(total_sent + total_resent)) / ( -                            (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL)); +                                                         (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL));                      if (min_speed < CRYPTO_PACKET_MIN_RATE) {                          min_speed = CRYPTO_PACKET_MIN_RATE; diff --git a/protocols/Tox/libtox/src/toxcore/net_crypto.h b/protocols/Tox/libtox/src/toxcore/net_crypto.h index f6e062f3be..0c3dfd0d45 100644 --- a/protocols/Tox/libtox/src/toxcore/net_crypto.h +++ b/protocols/Tox/libtox/src/toxcore/net_crypto.h @@ -59,6 +59,7 @@  #define PACKET_ID_FILE_SENDREQUEST 80  #define PACKET_ID_FILE_CONTROL 81  #define PACKET_ID_FILE_DATA 82 +#define PACKET_ID_INVITE_GROUPCHAT 95  #define PACKET_ID_INVITE_CONFERENCE 96  #define PACKET_ID_ONLINE_PACKET 97  #define PACKET_ID_DIRECT_CONFERENCE 98 diff --git a/protocols/Tox/libtox/src/toxcore/network.c b/protocols/Tox/libtox/src/toxcore/network.c index 35893b73dc..8544496c1c 100644 --- a/protocols/Tox/libtox/src/toxcore/network.c +++ b/protocols/Tox/libtox/src/toxcore/network.c @@ -655,6 +655,121 @@ static uint32_t data_1(uint16_t buflen, const uint8_t *buffer)      return data;  } +static const char *net_packet_type_name(Net_Packet_Type type) +{ +    switch (type) { +        case NET_PACKET_PING_REQUEST: +            return "PING_REQUEST"; + +        case NET_PACKET_PING_RESPONSE: +            return "PING_RESPONSE"; + +        case NET_PACKET_GET_NODES: +            return "GET_NODES"; + +        case NET_PACKET_SEND_NODES_IPV6: +            return "SEND_NODES_IPV6"; + +        case NET_PACKET_COOKIE_REQUEST: +            return "COOKIE_REQUEST"; + +        case NET_PACKET_COOKIE_RESPONSE: +            return "COOKIE_RESPONSE"; + +        case NET_PACKET_CRYPTO_HS: +            return "CRYPTO_HS"; + +        case NET_PACKET_CRYPTO_DATA: +            return "CRYPTO_DATA"; + +        case NET_PACKET_CRYPTO: +            return "CRYPTO"; + +        case NET_PACKET_GC_HANDSHAKE: +            return "GC_HANDSHAKE"; + +        case NET_PACKET_GC_LOSSLESS: +            return "GC_LOSSLESS"; + +        case NET_PACKET_GC_LOSSY: +            return "GC_LOSSY"; + +        case NET_PACKET_LAN_DISCOVERY: +            return "LAN_DISCOVERY"; + +        case NET_PACKET_ONION_SEND_INITIAL: +            return "ONION_SEND_INITIAL"; + +        case NET_PACKET_ONION_SEND_1: +            return "ONION_SEND_1"; + +        case NET_PACKET_ONION_SEND_2: +            return "ONION_SEND_2"; + +        case NET_PACKET_ANNOUNCE_REQUEST_OLD: +            return "ANNOUNCE_REQUEST_OLD"; + +        case NET_PACKET_ANNOUNCE_RESPONSE_OLD: +            return "ANNOUNCE_RESPONSE_OLD"; + +        case NET_PACKET_ONION_DATA_REQUEST: +            return "ONION_DATA_REQUEST"; + +        case NET_PACKET_ONION_DATA_RESPONSE: +            return "ONION_DATA_RESPONSE"; + +        case NET_PACKET_ANNOUNCE_REQUEST: +            return "ANNOUNCE_REQUEST"; + +        case NET_PACKET_ANNOUNCE_RESPONSE: +            return "ANNOUNCE_RESPONSE"; + +        case NET_PACKET_ONION_RECV_3: +            return "ONION_RECV_3"; + +        case NET_PACKET_ONION_RECV_2: +            return "ONION_RECV_2"; + +        case NET_PACKET_ONION_RECV_1: +            return "ONION_RECV_1"; + +        case NET_PACKET_FORWARD_REQUEST: +            return "FORWARD_REQUEST"; + +        case NET_PACKET_FORWARDING: +            return "FORWARDING"; + +        case NET_PACKET_FORWARD_REPLY: +            return "FORWARD_REPLY"; + +        case NET_PACKET_DATA_SEARCH_REQUEST: +            return "DATA_SEARCH_REQUEST"; + +        case NET_PACKET_DATA_SEARCH_RESPONSE: +            return "DATA_SEARCH_RESPONSE"; + +        case NET_PACKET_DATA_RETRIEVE_REQUEST: +            return "DATA_RETRIEVE_REQUEST"; + +        case NET_PACKET_DATA_RETRIEVE_RESPONSE: +            return "DATA_RETRIEVE_RESPONSE"; + +        case NET_PACKET_STORE_ANNOUNCE_REQUEST: +            return "STORE_ANNOUNCE_REQUEST"; + +        case NET_PACKET_STORE_ANNOUNCE_RESPONSE: +            return "STORE_ANNOUNCE_RESPONSE"; + +        case BOOTSTRAP_INFO_PACKET_ID: +            return "BOOTSTRAP_INFO"; + +        case NET_PACKET_MAX: +            return "MAX"; +    } + +    return "<unknown>"; +} +  non_null()  static void loglogdata(const Logger *log, const char *message, const uint8_t *buffer,                         uint16_t buflen, const IP_Port *ip_port, long res) @@ -663,21 +778,24 @@ static void loglogdata(const Logger *log, const char *message, const uint8_t *bu          Ip_Ntoa ip_str;          const int error = net_error();          char *strerror = net_new_strerror(error); -        LOGGER_TRACE(log, "[%2u] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", -                     buffer[0], message, min_u16(buflen, 999), 'E', +        LOGGER_TRACE(log, "[%02x = %-20s] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", +                     buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, +                     min_u16(buflen, 999), 'E',                       net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), error,                       strerror, data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]);          net_kill_strerror(strerror);      } else if ((res > 0) && ((size_t)res <= buflen)) {          Ip_Ntoa ip_str; -        LOGGER_TRACE(log, "[%2u] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", -                     buffer[0], message, min_u16(res, 999), (size_t)res < buflen ? '<' : '=', +        LOGGER_TRACE(log, "[%02x = %-20s] %s %3u%c %s:%u (%u: %s) | %08x%08x...%02x", +                     buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, +                     min_u16(res, 999), (size_t)res < buflen ? '<' : '=',                       net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), 0, "OK",                       data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]);      } else { /* empty or overwrite */          Ip_Ntoa ip_str; -        LOGGER_TRACE(log, "[%2u] %s %lu%c%u %s:%u (%u: %s) | %08x%08x...%02x", -                     buffer[0], message, res, res == 0 ? '!' : '>', buflen, +        LOGGER_TRACE(log, "[%02x = %-20s] %s %lu%c%u %s:%u (%u: %s) | %08x%08x...%02x", +                     buffer[0], net_packet_type_name((Net_Packet_Type)buffer[0]), message, +                     res, res == 0 ? '!' : '>', buflen,                       net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), 0, "OK",                       data_0(buflen, buffer), data_1(buflen, buffer), buffer[buflen - 1]);      } @@ -1616,7 +1734,7 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port)      } else {          Ip_Ntoa ip_str;          LOGGER_ERROR(log, "cannot connect to %s:%d which is neither IPv4 nor IPv6", -                     net_ip_ntoa(&ip_port->ip, &ip_str), ip_port->port); +                     net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port));          return false;      } @@ -1638,7 +1756,7 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port)          if (!should_ignore_connect_error(error)) {              char *net_strerror = net_new_strerror(error);              LOGGER_ERROR(log, "failed to connect to %s:%d: %d (%s)", -                         net_ip_ntoa(&ip_port->ip, &ip_str), ip_port->port, error, net_strerror); +                         net_ip_ntoa(&ip_port->ip, &ip_str), net_ntohs(ip_port->port), error, net_strerror);              net_kill_strerror(net_strerror);              return false;          } @@ -1649,18 +1767,6 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port)  int32_t net_getipport(const char *node, IP_Port **res, int tox_type)  { -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -    if ((true)) { -        *res = (IP_Port *)calloc(1, sizeof(IP_Port)); -        assert(*res != nullptr); -        IP_Port *ip_port = *res; -        ip_port->ip.ip.v4.uint32 = 0x7F000003; // 127.0.0.3 -        ip_port->ip.family = *make_tox_family(AF_INET); - -        return 1; -    } -#endif -      // Try parsing as IP address first.      IP_Port parsed = {{{0}}}; @@ -1676,6 +1782,18 @@ int32_t net_getipport(const char *node, IP_Port **res, int tox_type)          return 1;      } +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +    if ((true)) { +        *res = (IP_Port *)calloc(1, sizeof(IP_Port)); +        assert(*res != nullptr); +        IP_Port *ip_port = *res; +        ip_port->ip.ip.v4.uint32 = net_htonl(0x7F000003); // 127.0.0.3 +        ip_port->ip.family = *make_tox_family(AF_INET); + +        return 1; +    } +#endif +      // It's not an IP address, so now we try doing a DNS lookup.      struct addrinfo *infos;      const int ret = getaddrinfo(node, nullptr, nullptr, &infos); @@ -1814,6 +1932,12 @@ uint16_t net_ntohs(uint16_t hostshort)      return ntohs(hostshort);  } +size_t net_pack_bool(uint8_t *bytes, bool v) +{ +    bytes[0] = v ? 1 : 0; +    return 1; +} +  size_t net_pack_u16(uint8_t *bytes, uint16_t v)  {      bytes[0] = (v >> 8) & 0xff; @@ -1837,6 +1961,12 @@ size_t net_pack_u64(uint8_t *bytes, uint64_t v)      return p - bytes;  } +size_t net_unpack_bool(const uint8_t *bytes, bool *v) +{ +    *v = bytes[0] != 0; +    return 1; +} +  size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v)  {      const uint8_t hi = bytes[0]; diff --git a/protocols/Tox/libtox/src/toxcore/network.h b/protocols/Tox/libtox/src/toxcore/network.h index a1fc6a74fc..ffc04ed6e4 100644 --- a/protocols/Tox/libtox/src/toxcore/network.h +++ b/protocols/Tox/libtox/src/toxcore/network.h @@ -109,10 +109,9 @@ typedef enum Net_Packet_Type {      NET_PACKET_CRYPTO               = 0x24, /* Encrypted data packet ID. */      NET_PACKET_LAN_DISCOVERY        = 0x25, /* LAN discovery packet ID. */ -    // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats -    // NET_PACKET_GC_HANDSHAKE         = 0x62, /* Group chat handshake packet ID */ -    // NET_PACKET_GC_LOSSLESS          = 0x63, /* Group chat lossless packet ID */ -    // NET_PACKET_GC_LOSSY             = 0x64, /* Group chat lossy packet ID */ +    NET_PACKET_GC_HANDSHAKE         = 0x62, /* Group chat handshake packet ID */ +    NET_PACKET_GC_LOSSLESS          = 0x63, /* Group chat lossless packet ID */ +    NET_PACKET_GC_LOSSY             = 0x64, /* Group chat lossy packet ID */      /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */      NET_PACKET_ONION_SEND_INITIAL   = 0x8f, @@ -131,6 +130,17 @@ typedef enum Net_Packet_Type {      NET_PACKET_ONION_RECV_2         = 0x9c,      NET_PACKET_ONION_RECV_1         = 0x9d, +    NET_PACKET_FORWARD_REQUEST      = 0x9e, +    NET_PACKET_FORWARDING           = 0x9f, +    NET_PACKET_FORWARD_REPLY        = 0xa0, + +    NET_PACKET_DATA_SEARCH_REQUEST     = 0xa1, +    NET_PACKET_DATA_SEARCH_RESPONSE    = 0xa2, +    NET_PACKET_DATA_RETRIEVE_REQUEST   = 0xa3, +    NET_PACKET_DATA_RETRIEVE_RESPONSE  = 0xa4, +    NET_PACKET_STORE_ANNOUNCE_REQUEST  = 0xa5, +    NET_PACKET_STORE_ANNOUNCE_RESPONSE = 0xa6, +      BOOTSTRAP_INFO_PACKET_ID        = 0xf1, /* Only used for bootstrap nodes */      NET_PACKET_MAX                  = 0xff, /* This type must remain within a single uint8. */ @@ -148,10 +158,9 @@ typedef enum Net_Packet_Type {      NET_PACKET_CRYPTO               = 0x20, /* Encrypted data packet ID. */      NET_PACKET_LAN_DISCOVERY        = 0x21, /* LAN discovery packet ID. */ -    // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats -    // NET_PACKET_GC_HANDSHAKE         = 0x5a, /* Group chat handshake packet ID */ -    // NET_PACKET_GC_LOSSLESS          = 0x5b, /* Group chat lossless packet ID */ -    // NET_PACKET_GC_LOSSY             = 0x5c, /* Group chat lossy packet ID */ +    NET_PACKET_GC_HANDSHAKE         = 0x5a, /* Group chat handshake packet ID */ +    NET_PACKET_GC_LOSSLESS          = 0x5b, /* Group chat lossless packet ID */ +    NET_PACKET_GC_LOSSY             = 0x5c, /* Group chat lossy packet ID */      /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */      NET_PACKET_ONION_SEND_INITIAL   = 0x80, @@ -304,6 +313,8 @@ uint32_t net_ntohl(uint32_t hostlong);  uint16_t net_ntohs(uint16_t hostshort);  non_null() +size_t net_pack_bool(uint8_t *bytes, bool v); +non_null()  size_t net_pack_u16(uint8_t *bytes, uint16_t v);  non_null()  size_t net_pack_u32(uint8_t *bytes, uint32_t v); @@ -311,6 +322,8 @@ non_null()  size_t net_pack_u64(uint8_t *bytes, uint64_t v);  non_null() +size_t net_unpack_bool(const uint8_t *bytes, bool *v); +non_null()  size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v);  non_null()  size_t net_unpack_u32(const uint8_t *bytes, uint32_t *v); @@ -531,8 +544,9 @@ bool net_connect(const Logger *log, Socket sock, const IP_Port *ip_port);   * Skip all addresses with socktype != type (use type = -1 to get all addresses)   * To correctly deallocate array memory use `net_freeipport()`   * - * return number of elements in res array - * and -1 on error. + * @return number of elements in res array. + * @retval 0 if res array empty. + * @retval -1 on error.   */  non_null()  int32_t net_getipport(const char *node, IP_Port **res, int tox_type); diff --git a/protocols/Tox/libtox/src/toxcore/onion.c b/protocols/Tox/libtox/src/toxcore/onion.c index d7e3f02665..63ff6d8520 100644 --- a/protocols/Tox/libtox/src/toxcore/onion.c +++ b/protocols/Tox/libtox/src/toxcore/onion.c @@ -27,6 +27,11 @@  #define KEY_REFRESH_INTERVAL (2 * 60 * 60) + +// Settings for the shared key cache +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 +  /** Change symmetric keys every 2 hours to make paths expire eventually. */  non_null()  static void change_symmetric_key(Onion *onion) @@ -314,9 +319,14 @@ static int handle_send_initial(void *object, const IP_Port *source, const uint8_      change_symmetric_key(onion);      uint8_t plain[ONION_MAX_PACKET_SIZE]; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    get_shared_key(onion->mono_time, &onion->shared_keys_1, shared_key, dht_get_self_secret_key(onion->dht), -                   packet + 1 + CRYPTO_NONCE_SIZE); +    const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; +    const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_1, public_key); + +    if (shared_key == nullptr) { +        /* Error looking up/deriving the shared key */ +        return 1; +    } +      const int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE,                                       length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE), plain); @@ -385,9 +395,14 @@ static int handle_send_1(void *object, const IP_Port *source, const uint8_t *pac      change_symmetric_key(onion);      uint8_t plain[ONION_MAX_PACKET_SIZE]; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    get_shared_key(onion->mono_time, &onion->shared_keys_2, shared_key, dht_get_self_secret_key(onion->dht), -                   packet + 1 + CRYPTO_NONCE_SIZE); +    const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; +    const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_2, public_key); + +    if (shared_key == nullptr) { +        /* Error looking up/deriving the shared key */ +        return 1; +    } +      int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE,                                       length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + RETURN_1), plain); @@ -443,9 +458,14 @@ static int handle_send_2(void *object, const IP_Port *source, const uint8_t *pac      change_symmetric_key(onion);      uint8_t plain[ONION_MAX_PACKET_SIZE]; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    get_shared_key(onion->mono_time, &onion->shared_keys_3, shared_key, dht_get_self_secret_key(onion->dht), -                   packet + 1 + CRYPTO_NONCE_SIZE); +    const uint8_t *public_key = packet + 1 + CRYPTO_NONCE_SIZE; +    const uint8_t *shared_key = shared_key_cache_lookup(onion->shared_keys_3, public_key); + +    if (shared_key == nullptr) { +        /* Error looking up/deriving the shared key */ +        return 1; +    } +      int len = decrypt_data_symmetric(shared_key, packet + 1, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE,                                       length - (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + RETURN_2), plain); @@ -457,7 +477,8 @@ static int handle_send_2(void *object, const IP_Port *source, const uint8_t *pac      const uint8_t packet_id = plain[SIZE_IPPORT]; -    if (packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && packet_id != NET_PACKET_ONION_DATA_REQUEST) { +    if (packet_id != NET_PACKET_ANNOUNCE_REQUEST && packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && +            packet_id != NET_PACKET_ONION_DATA_REQUEST) {          return 1;      } @@ -507,7 +528,8 @@ static int handle_recv_3(void *object, const IP_Port *source, const uint8_t *pac      const uint8_t packet_id = packet[1 + RETURN_3]; -    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { +    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && +            packet_id != NET_PACKET_ONION_DATA_RESPONSE) {          return 1;      } @@ -555,7 +577,8 @@ static int handle_recv_2(void *object, const IP_Port *source, const uint8_t *pac      const uint8_t packet_id = packet[1 + RETURN_2]; -    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { +    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && +            packet_id != NET_PACKET_ONION_DATA_RESPONSE) {          return 1;      } @@ -603,7 +626,8 @@ static int handle_recv_1(void *object, const IP_Port *source, const uint8_t *pac      const uint8_t packet_id = packet[1 + RETURN_1]; -    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { +    if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && +            packet_id != NET_PACKET_ONION_DATA_RESPONSE) {          return 1;      } @@ -664,6 +688,19 @@ Onion *new_onion(const Logger *log, const Mono_Time *mono_time, const Random *rn      new_symmetric_key(rng, onion->secret_symmetric_key);      onion->timestamp = mono_time_get(onion->mono_time); +    const uint8_t *secret_key = dht_get_self_secret_key(dht); +    onion->shared_keys_1 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); +    onion->shared_keys_2 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); +    onion->shared_keys_3 = shared_key_cache_new(mono_time, secret_key, KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); + +    if (onion->shared_keys_1 == nullptr || +        onion->shared_keys_2 == nullptr || +        onion->shared_keys_3 == nullptr) { +        kill_onion(onion); +        return nullptr; +    } + +      networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_INITIAL, &handle_send_initial, onion);      networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_1, &handle_send_1, onion);      networking_registerhandler(onion->net, NET_PACKET_ONION_SEND_2, &handle_send_2, onion); @@ -691,5 +728,9 @@ void kill_onion(Onion *onion)      crypto_memzero(onion->secret_symmetric_key, sizeof(onion->secret_symmetric_key)); +    shared_key_cache_free(onion->shared_keys_1); +    shared_key_cache_free(onion->shared_keys_2); +    shared_key_cache_free(onion->shared_keys_3); +      free(onion);  } diff --git a/protocols/Tox/libtox/src/toxcore/onion.h b/protocols/Tox/libtox/src/toxcore/onion.h index 3da2137c44..5c8f920b44 100644 --- a/protocols/Tox/libtox/src/toxcore/onion.h +++ b/protocols/Tox/libtox/src/toxcore/onion.h @@ -12,6 +12,7 @@  #include "DHT.h"  #include "logger.h"  #include "mono_time.h" +#include "shared_key_cache.h"  typedef int onion_recv_1_cb(void *object, const IP_Port *dest, const uint8_t *data, uint16_t length); @@ -24,9 +25,9 @@ typedef struct Onion {      uint8_t secret_symmetric_key[CRYPTO_SYMMETRIC_KEY_SIZE];      uint64_t timestamp; -    Shared_Keys shared_keys_1; -    Shared_Keys shared_keys_2; -    Shared_Keys shared_keys_3; +    Shared_Key_Cache *shared_keys_1; +    Shared_Key_Cache *shared_keys_2; +    Shared_Key_Cache *shared_keys_3;      onion_recv_1_cb *recv_1_function;      void *callback_object; diff --git a/protocols/Tox/libtox/src/toxcore/onion_announce.c b/protocols/Tox/libtox/src/toxcore/onion_announce.c index f12e560837..fb04d21f56 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_announce.c +++ b/protocols/Tox/libtox/src/toxcore/onion_announce.c @@ -16,6 +16,7 @@  #include "LAN_discovery.h"  #include "ccompat.h"  #include "mono_time.h" +#include "shared_key_cache.h"  #include "util.h"  #define PING_ID_TIMEOUT ONION_ANNOUNCE_TIMEOUT @@ -31,6 +32,10 @@  #define ONION_MINIMAL_SIZE (ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE * 2 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH) +/* Settings for the shared key cache */ +#define MAX_KEYS_PER_SLOT 4 +#define KEYS_TIMEOUT 600 +  static_assert(ONION_PING_ID_SIZE == CRYPTO_PUBLIC_KEY_SIZE,                "announce response packets assume that ONION_PING_ID_SIZE is equal to CRYPTO_PUBLIC_KEY_SIZE"); @@ -51,7 +56,7 @@ struct Onion_Announce {      Onion_Announce_Entry entries[ONION_ANNOUNCE_MAX_ENTRIES];      uint8_t hmac_key[CRYPTO_HMAC_KEY_SIZE]; -    Shared_Keys shared_keys_recv; +    Shared_Key_Cache *shared_keys_recv;      uint16_t extra_data_max_size;      pack_extra_data_cb *extra_data_callback; @@ -346,7 +351,6 @@ non_null()  static int add_to_entries(Onion_Announce *onion_a, const IP_Port *ret_ip_port, const uint8_t *public_key,                            const uint8_t *data_public_key, const uint8_t *ret)  { -      int pos = in_entries(onion_a, public_key);      if (pos == -1) { @@ -427,9 +431,12 @@ static int handle_announce_request_common(      pack_extra_data_cb *pack_extra_data_callback)  {      const uint8_t *packet_public_key = packet + 1 + CRYPTO_NONCE_SIZE; -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -    get_shared_key(onion_a->mono_time, &onion_a->shared_keys_recv, shared_key, dht_get_self_secret_key(onion_a->dht), -                   packet_public_key); +    const uint8_t *shared_key = shared_key_cache_lookup(onion_a->shared_keys_recv, packet_public_key); + +    if (shared_key == nullptr) { +        /* Error looking up/deriving the shared key */ +        return 1; +    }      uint8_t *plain = (uint8_t *)malloc(plain_size); @@ -654,13 +661,16 @@ Onion_Announce *new_onion_announce(const Logger *log, const Random *rng, const M      onion_a->extra_data_object = nullptr;      new_hmac_key(rng, onion_a->hmac_key); +    onion_a->shared_keys_recv = shared_key_cache_new(mono_time, dht_get_self_secret_key(dht), KEYS_TIMEOUT, MAX_KEYS_PER_SLOT); +    if (onion_a->shared_keys_recv == nullptr) { +        kill_onion_announce(onion_a); +        return nullptr; +    } +      networking_registerhandler(onion_a->net, NET_PACKET_ANNOUNCE_REQUEST, &handle_announce_request, onion_a);      networking_registerhandler(onion_a->net, NET_PACKET_ANNOUNCE_REQUEST_OLD, &handle_announce_request_old, onion_a);      networking_registerhandler(onion_a->net, NET_PACKET_ONION_DATA_REQUEST, &handle_data_request, onion_a); -    // TODO(Jfreegman): Remove this when we merge the rest of new groupchats -    onion_announce_extra_data_callback(onion_a, 0, nullptr, nullptr); -      return onion_a;  } @@ -675,6 +685,7 @@ void kill_onion_announce(Onion_Announce *onion_a)      networking_registerhandler(onion_a->net, NET_PACKET_ONION_DATA_REQUEST, nullptr, nullptr);      crypto_memzero(onion_a->hmac_key, CRYPTO_HMAC_KEY_SIZE); +    shared_key_cache_free(onion_a->shared_keys_recv);      free(onion_a);  } diff --git a/protocols/Tox/libtox/src/toxcore/onion_announce.h b/protocols/Tox/libtox/src/toxcore/onion_announce.h index 8cf1f49539..24303abccb 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_announce.h +++ b/protocols/Tox/libtox/src/toxcore/onion_announce.h @@ -20,6 +20,7 @@  #define ONION_ANNOUNCE_SENDBACK_DATA_LENGTH (sizeof(uint64_t)) +#define MAX_SENT_GC_NODES 1  #define ONION_ANNOUNCE_REQUEST_MIN_SIZE (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_MAC_SIZE)  #define ONION_ANNOUNCE_REQUEST_MAX_SIZE (ONION_ANNOUNCE_REQUEST_MIN_SIZE + ONION_MAX_EXTRA_DATA_SIZE) @@ -125,7 +126,7 @@ typedef int pack_extra_data_cb(void *object, const Logger *logger, const Mono_Ti                                 uint8_t num_nodes, uint8_t *plain, uint16_t plain_size,                                 uint8_t *response, uint16_t response_size, uint16_t offset); -non_null(1) nullable(3, 4) +non_null()  void onion_announce_extra_data_callback(Onion_Announce *onion_a, uint16_t extra_data_max_size,                                          pack_extra_data_cb *extra_data_callback, void *extra_data_object); diff --git a/protocols/Tox/libtox/src/toxcore/onion_client.c b/protocols/Tox/libtox/src/toxcore/onion_client.c index 91168c661c..ff63473e10 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_client.c +++ b/protocols/Tox/libtox/src/toxcore/onion_client.c @@ -15,6 +15,7 @@  #include "LAN_discovery.h"  #include "ccompat.h" +#include "group_onion_announce.h"  #include "mono_time.h"  #include "util.h" @@ -54,7 +55,7 @@ typedef struct Last_Pinged {      uint64_t    timestamp;  } Last_Pinged; -typedef struct Onion_Friend { +struct Onion_Friend {      bool is_valid;      bool is_online; @@ -87,7 +88,12 @@ typedef struct Onion_Friend {      onion_dht_pk_cb *dht_pk_callback;      void *dht_pk_callback_object;      uint32_t dht_pk_callback_number; -} Onion_Friend; + +    uint8_t  gc_data[GCA_MAX_DATA_LENGTH]; +    uint8_t  gc_public_key[ENC_PUBLIC_KEY_SIZE]; +    uint16_t gc_data_length; +    bool     is_groupchat; +};  static const Onion_Friend empty_onion_friend = {false}; @@ -138,8 +144,51 @@ struct Onion_Client {      unsigned int onion_connected;      bool udp_connected; + +    onion_group_announce_cb *group_announce_response; +    void *group_announce_response_user_data;  }; +uint16_t onion_get_friend_count(const Onion_Client *const onion_c) +{ +    return onion_c->num_friends; +} + +Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num) +{ +    return &onion_c->friends_list[friend_num]; +} + +const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend) +{ +    return onion_friend->gc_public_key; +} + +const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num) +{ +    return onion_c->friends_list[num].gc_public_key; +} + +void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key) +{ +    memcpy(onion_friend->gc_public_key, public_key, ENC_PUBLIC_KEY_SIZE); +} + +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length) +{ +    if (gc_data_length > 0 && gc_data != nullptr) { +        memcpy(onion_friend->gc_data, gc_data, gc_data_length); +    } + +    onion_friend->gc_data_length = gc_data_length; +    onion_friend->is_groupchat = true; +} + +bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend) +{ +    return onion_friend->is_groupchat; +} +  DHT *onion_get_dht(const Onion_Client *onion_c)  {      return onion_c->dht; @@ -351,8 +400,8 @@ static bool path_timed_out(const Mono_Time *mono_time, const Onion_Client_Paths      const uint64_t timeout = is_new ? ONION_PATH_FIRST_TIMEOUT : ONION_PATH_TIMEOUT;      return (onion_paths->last_path_used_times[pathnum] >= ONION_PATH_MAX_NO_RESPONSE_USES -             && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) -            || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME); +            && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) +           || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME);  }  /** should node be considered to have timed out */ @@ -360,8 +409,8 @@ non_null()  static bool onion_node_timed_out(const Onion_Node *node, const Mono_Time *mono_time)  {      return node->timestamp == 0 -            || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS -                && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT)); +           || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS +               && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT));  }  /** @brief Create a new path or use an old suitable one (if pathnum is valid) @@ -600,19 +649,35 @@ static int client_send_announce_request(Onion_Client *onion_c, uint32_t num, con          ping_id = zero_ping_id;      } -    uint8_t request[ONION_ANNOUNCE_REQUEST_SIZE]; +    uint8_t request[ONION_ANNOUNCE_REQUEST_MAX_SIZE];      int len;      if (num == 0) {          len = create_announce_request( -                onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), -                nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), -                onion_c->temp_public_key, sendback); +                  onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), +                  nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), +                  onion_c->temp_public_key, sendback);      } else { -        len = create_announce_request( -                onion_c->rng, request, sizeof(request), dest_pubkey, onion_c->friends_list[num - 1].temp_public_key, -                onion_c->friends_list[num - 1].temp_secret_key, ping_id, -                onion_c->friends_list[num - 1].real_public_key, zero_ping_id, sendback); +        Onion_Friend *onion_friend = &onion_c->friends_list[num - 1]; + +        if (onion_friend->gc_data_length == 0) { // contact is a friend +            len = create_announce_request( +                      onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, +                      onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, +                      zero_ping_id, sendback); +        } else { // contact is a gc +#ifndef VANILLA_NACL +            onion_friend->is_groupchat = true; + +            len = create_gca_announce_request( +                      onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, +                      onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, +                      zero_ping_id, sendback, onion_friend->gc_data, +                      onion_friend->gc_data_length); +#else +            return -1; +#endif  // VANILLA_NACL +        }      }      if (len == -1) { @@ -852,6 +917,16 @@ static int client_ping_nodes(Onion_Client *onion_c, uint32_t num, const Node_for  }  non_null() +static bool handle_group_announce_response(Onion_Client *onion_c, uint32_t num, const uint8_t *plain, size_t plain_size) +{ +    if (onion_c->group_announce_response == nullptr) { +        return true; +    } + +    return onion_c->group_announce_response(onion_c, num, plain, plain_size, onion_c->group_announce_response_user_data); +} + +non_null(1, 2, 3) nullable(5)  static int handle_announce_response(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length,                                      void *userdata)  { @@ -861,6 +936,95 @@ static int handle_announce_response(void *object, const IP_Port *source, const u          return 1;      } +    uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; +    IP_Port ip_port; +    uint32_t path_num; +    const uint32_t num = check_sendback(onion_c, packet + 1, public_key, &ip_port, &path_num); + +    if (num > onion_c->num_friends) { +        return 1; +    } + +    uint8_t plain[1 + ONION_PING_ID_SIZE + ONION_ANNOUNCE_RESPONSE_MAX_SIZE - ONION_ANNOUNCE_RESPONSE_MIN_SIZE]; +    const int plain_size = 1 + ONION_PING_ID_SIZE + length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE; +    int len; + +    if (num == 0) { +        len = decrypt_data(public_key, nc_get_self_secret_key(onion_c->c), +                           packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, +                           packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, +                           length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); +    } else { +        if (!onion_c->friends_list[num - 1].is_valid) { +            return 1; +        } + +        len = decrypt_data(public_key, onion_c->friends_list[num - 1].temp_secret_key, +                           packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, +                           packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, +                           length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); +    } + +    if ((uint32_t)len != plain_size) { +        return 1; +    } + +    const uint32_t path_used = set_path_timeouts(onion_c, num, path_num); + +    if (client_add_to_list(onion_c, num, public_key, &ip_port, plain[0], plain + 1, path_used) == -1) { +        return 1; +    } + +    uint16_t len_nodes = 0; +    const uint8_t nodes_count = plain[1 + ONION_PING_ID_SIZE]; + +    if (nodes_count > 0) { +        if (nodes_count > MAX_SENT_NODES) { +            return 1; +        } + +        Node_format nodes[MAX_SENT_NODES]; +        const int num_nodes = unpack_nodes(nodes, nodes_count, &len_nodes, plain + 2 + ONION_PING_ID_SIZE, +                                           plain_size - 2 - ONION_PING_ID_SIZE, false); + +        if (num_nodes < 0) { +            return 1; +        } + +        if (client_ping_nodes(onion_c, num, nodes, num_nodes, source) == -1) { +            return 1; +        } +    } + +    if (len_nodes + 1 < length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE) { +        const uint16_t offset = 2 + ONION_PING_ID_SIZE + len_nodes; + +        if (plain_size < offset) { +            return 1; +        } + +        if (!handle_group_announce_response(onion_c, num, plain + offset, plain_size - offset)) { +            return 1; +        } +    } + +    // TODO(irungentoo): LAN vs non LAN ips?, if we are connected only to LAN, are we offline? +    onion_c->last_packet_recv = mono_time_get(onion_c->mono_time); + +    return 0; +} + +/* TODO(jfreegman): DEPRECATE */ +non_null(1, 2, 3) nullable(5) +static int handle_announce_response_old(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, +                                        void *userdata) +{ +    Onion_Client *onion_c = (Onion_Client *)object; + +    if (length < ONION_ANNOUNCE_RESPONSE_MIN_SIZE || length > ONION_ANNOUNCE_RESPONSE_MAX_SIZE) { +        return 1; +    } +      const uint16_t len_nodes = length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE;      uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; @@ -1038,10 +1202,14 @@ static int handle_tcp_onion(void *object, const uint8_t *data, uint16_t length,      IP_Port ip_port = {{{0}}};      ip_port.ip.family = net_family_tcp_server(); -    if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { +    if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE) {          return handle_announce_response(object, &ip_port, data, length, userdata);      } +    if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { +        return handle_announce_response_old(object, &ip_port, data, length, userdata); +    } +      if (data[0] == NET_PACKET_ONION_DATA_RESPONSE) {          return handle_data_response(object, &ip_port, data, length, userdata);      } @@ -1118,8 +1286,8 @@ int send_onion_data(Onion_Client *onion_c, int friend_num, const uint8_t *data,          uint8_t o_packet[ONION_MAX_PACKET_SIZE];          len = create_data_request( -                onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, -                node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet)); +                  onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, +                  node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet));          if (len == -1) {              continue; @@ -1167,8 +1335,8 @@ static int send_dht_dhtpk(const Onion_Client *onion_c, int friend_num, const uin      uint8_t packet_data[MAX_CRYPTO_REQUEST_SIZE];      len = create_request( -            onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, -            onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK); +              onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, +              onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK);      assert(len <= UINT16_MAX);      const Packet packet = {packet_data, (uint16_t)len}; @@ -1237,7 +1405,8 @@ static int send_dhtpk_announce(Onion_Client *onion_c, uint16_t friend_num, uint8      int nodes_len = 0;      if (num_nodes != 0) { -        nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, nodes, num_nodes); +        nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, +                               nodes, num_nodes);          if (nodes_len <= 0) {              return -1; @@ -1344,7 +1513,8 @@ int onion_addfriend(Onion_Client *onion_c, const uint8_t *public_key)      onion_c->friends_list[index].is_valid = true;      memcpy(onion_c->friends_list[index].real_public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); -    crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, onion_c->friends_list[index].temp_secret_key); +    crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, +                       onion_c->friends_list[index].temp_secret_key);      return index;  } @@ -1674,6 +1844,12 @@ void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_ha      onion_c->onion_data_handlers[byte].object = object;  } +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data) +{ +    onion_c->group_announce_response = func; +    onion_c->group_announce_response_user_data = user_data; +} +  #define ANNOUNCE_INTERVAL_NOT_ANNOUNCED 3  #define ANNOUNCE_INTERVAL_ANNOUNCED ONION_NODE_PING_INTERVAL @@ -1925,7 +2101,8 @@ Onion_Client *new_onion_client(const Logger *logger, const Random *rng, const Mo      onion_c->c = c;      new_symmetric_key(rng, onion_c->secret_symmetric_key);      crypto_new_keypair(rng, onion_c->temp_public_key, onion_c->temp_secret_key); -    networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response, onion_c); +    networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, &handle_announce_response, onion_c); +    networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response_old, onion_c);      networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, &handle_data_response, onion_c);      oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, &handle_dhtpk_announce, onion_c);      cryptopacket_registerhandler(onion_c->dht, CRYPTO_PACKET_DHTPK, &handle_dht_dhtpk, onion_c); @@ -1942,6 +2119,7 @@ void kill_onion_client(Onion_Client *onion_c)      ping_array_kill(onion_c->announce_ping_array);      realloc_onion_friends(onion_c, 0); +    networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, nullptr, nullptr);      networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, nullptr, nullptr);      networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, nullptr, nullptr);      oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, nullptr, nullptr); diff --git a/protocols/Tox/libtox/src/toxcore/onion_client.h b/protocols/Tox/libtox/src/toxcore/onion_client.h index 2a30005c69..23a48ef5e6 100644 --- a/protocols/Tox/libtox/src/toxcore/onion_client.h +++ b/protocols/Tox/libtox/src/toxcore/onion_client.h @@ -43,6 +43,8 @@  #define MAX_PATH_NODES 32 +#define GCA_MAX_DATA_LENGTH GCA_PUBLIC_ANNOUNCE_MAX_SIZE +  /**   * If no announce response packets are received within this interval tox will   * be considered offline. We give time for a node to be pinged often enough @@ -195,6 +197,13 @@ typedef int oniondata_handler_cb(void *object, const uint8_t *source_pubkey, con  non_null(1) nullable(3, 4)  void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_handler_cb *cb, void *object); +typedef bool onion_group_announce_cb(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, +                                     size_t data_length, void *user_data); + +/** Function to call when the onion gets a group announce response. */ +non_null(1) nullable(2, 3) +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data); +  non_null()  void do_onion_client(Onion_Client *onion_c); @@ -217,4 +226,15 @@ typedef enum Onion_Connection_Status {  non_null()  Onion_Connection_Status onion_connection_status(const Onion_Client *onion_c); +typedef struct Onion_Friend Onion_Friend; + +non_null() uint16_t onion_get_friend_count(const Onion_Client *const onion_c); +non_null() Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num); +non_null() const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend); +non_null() const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num); +non_null() void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key); +non_null(1) nullable(2) +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length); +non_null() bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend); +  #endif diff --git a/protocols/Tox/libtox/src/toxcore/ping.c b/protocols/Tox/libtox/src/toxcore/ping.c index 91a780b123..f44777b8ee 100644 --- a/protocols/Tox/libtox/src/toxcore/ping.c +++ b/protocols/Tox/libtox/src/toxcore/ping.c @@ -53,10 +53,9 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key          return;      } -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE];      // generate key to encrypt ping_id with recipient privkey -    dht_get_shared_key_sent(ping->dht, shared_key, public_key); +    const uint8_t *shared_key = dht_get_shared_key_sent(ping->dht, public_key);      // Generate random ping_id.      uint8_t data[PING_DATA_SIZE];      pk_copy(data, public_key); @@ -64,7 +63,6 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key      ping_id = ping_array_add(ping->ping_array, ping->mono_time, ping->rng, data, sizeof(data));      if (ping_id == 0) { -        crypto_memzero(shared_key, sizeof(shared_key));          return;      } @@ -82,8 +80,6 @@ void ping_send_request(Ping *ping, const IP_Port *ipp, const uint8_t *public_key                                  ping_plain, sizeof(ping_plain),                                  pk + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); -    crypto_memzero(shared_key, sizeof(shared_key)); -      if (rc != PING_PLAIN_SIZE + CRYPTO_MAC_SIZE) {          return;      } @@ -139,11 +135,11 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_          return 1;      } -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; +    const uint8_t *shared_key = dht_get_shared_key_recv(dht, packet + 1); +      uint8_t ping_plain[PING_PLAIN_SIZE];      // Decrypt ping_id -    dht_get_shared_key_recv(dht, shared_key, packet + 1);      const int rc = decrypt_data_symmetric(shared_key,                                            packet + 1 + CRYPTO_PUBLIC_KEY_SIZE,                                            packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, @@ -151,12 +147,10 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_                                            ping_plain);      if (rc != sizeof(ping_plain)) { -        crypto_memzero(shared_key, sizeof(shared_key));          return 1;      }      if (ping_plain[0] != NET_PACKET_PING_REQUEST) { -        crypto_memzero(shared_key, sizeof(shared_key));          return 1;      } @@ -166,8 +160,6 @@ static int handle_ping_request(void *object, const IP_Port *source, const uint8_      ping_send_response(ping, source, packet + 1, ping_id, shared_key);      ping_add(ping, packet + 1, source); -    crypto_memzero(shared_key, sizeof(shared_key)); -      return 0;  } @@ -188,10 +180,8 @@ static int handle_ping_response(void *object, const IP_Port *source, const uint8          return 1;      } -    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; -      // generate key to encrypt ping_id with recipient privkey -    dht_get_shared_key_sent(ping->dht, shared_key, packet + 1); +    const uint8_t *shared_key = dht_get_shared_key_sent(ping->dht, packet + 1);      uint8_t ping_plain[PING_PLAIN_SIZE];      // Decrypt ping_id @@ -201,8 +191,6 @@ static int handle_ping_response(void *object, const IP_Port *source, const uint8                                  PING_PLAIN_SIZE + CRYPTO_MAC_SIZE,                                  ping_plain); -    crypto_memzero(shared_key, sizeof(shared_key)); -      if (rc != sizeof(ping_plain)) {          return 1;      } diff --git a/protocols/Tox/libtox/src/toxcore/shared_key_cache.c b/protocols/Tox/libtox/src/toxcore/shared_key_cache.c new file mode 100644 index 0000000000..7846ae4f69 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/shared_key_cache.c @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2022 The TokTok team. + */ + +#include "shared_key_cache.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h>     // calloc(...) +#include <string.h>     // memcpy(...) + +#include "ccompat.h" +#include "crypto_core.h" +#include "mono_time.h" + +typedef struct Shared_Key { +    uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; +    uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; +    uint64_t time_last_requested; +} Shared_Key; + +struct Shared_Key_Cache { +    Shared_Key *keys; +    const uint8_t* self_secret_key; +    uint64_t timeout; /** After this time (in seconds), a key is erased on the next housekeeping cycle */ +    const Mono_Time *time; +    uint8_t keys_per_slot; +}; + +non_null() +static bool shared_key_is_empty(const Shared_Key *k) { +    assert(k != nullptr); +    /* +     * Since time can never be 0, we use that to determine if a key slot is empty. +     * Additionally this allows us to use crypto_memzero and leave the slot in a valid state. +     */ +    return k->time_last_requested == 0; +} + +non_null() +static void shared_key_set_empty(Shared_Key *k) { +    crypto_memzero(k, sizeof (Shared_Key)); +    assert(shared_key_is_empty(k)); +} + +Shared_Key_Cache *shared_key_cache_new(const Mono_Time *time, const uint8_t *self_secret_key, uint64_t timeout, uint8_t keys_per_slot) +{ +    if (time == nullptr || self_secret_key == nullptr || timeout == 0 || keys_per_slot == 0) { +        return nullptr; +    } + +    // Time must not be zero, since we use that as special value for empty slots +    if (mono_time_get(time) == 0) { +        // Fail loudly in debug environments +        assert(false); +        return nullptr; +    } + +    Shared_Key_Cache *res = (Shared_Key_Cache *)calloc(1, sizeof (Shared_Key_Cache)); +    if (res == nullptr) { +        return nullptr; +    } + +    res->self_secret_key = self_secret_key; +    res->time = time; +    res->keys_per_slot = keys_per_slot; +    // We take one byte from the public key for each bucket and store keys_per_slot elements there +    const size_t cache_size = 256 * keys_per_slot; +    res->keys = (Shared_Key *)calloc(cache_size, sizeof (Shared_Key)); + +    if (res->keys == nullptr) { +        free(res); +        return nullptr; +    } + +    crypto_memlock(res->keys, cache_size * sizeof (Shared_Key)); + +    return res; +} + +void shared_key_cache_free(Shared_Key_Cache *cache) +{ +    if (cache == nullptr) { +        return; +    } + +    const size_t cache_size = 256 * cache->keys_per_slot; +    // Don't leave key material in memory +    crypto_memzero(cache->keys, cache_size * sizeof (Shared_Key)); +    crypto_memunlock(cache->keys, cache_size * sizeof (Shared_Key)); +    free(cache->keys); +    free(cache); +} + +/* NOTE: On each lookup housekeeping is performed to evict keys that did timeout. */ +const uint8_t *shared_key_cache_lookup(Shared_Key_Cache *cache, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]) +{ +    // caching the time is not necessary, but calls to mono_time_get(...) are not free +    const uint64_t cur_time = mono_time_get(cache->time); +    // We can't use the first and last bytes because they are masked in curve25519. Selected 8 for good alignment. +    const uint8_t bucket_idx = public_key[8]; +    Shared_Key* bucket_start = &cache->keys[bucket_idx*cache->keys_per_slot]; + +    const uint8_t* found = nullptr; + +    // Perform lookup +    for(size_t i = 0; i < cache->keys_per_slot; ++i) { +        if (shared_key_is_empty(&bucket_start[i])) { +            continue; +        } + +        if (pk_equal(public_key, bucket_start[i].public_key)) { +            found = bucket_start[i].shared_key; +            bucket_start[i].time_last_requested = cur_time; +            break; +        } +    } + +    // Perform housekeeping for this bucket +    for (size_t i = 0; i < cache->keys_per_slot; ++i) { +        if (shared_key_is_empty(&bucket_start[i])) { +            continue; +        } + +        const bool timed_out = (bucket_start[i].time_last_requested + cache->timeout) < cur_time; +        if (timed_out) { +            shared_key_set_empty(&bucket_start[i]); +        } +    } + +    if (found == nullptr) { +        // Insert into cache + +        uint64_t oldest_timestamp = UINT64_MAX; +        size_t oldest_index = 0; + +        /* +         *  Find least recently used entry, unused entries are prioritised, +         *  because their time_last_requested field is zeroed. +         */ +        for (size_t i = 0; i < cache->keys_per_slot; ++i) { +            if (bucket_start[i].time_last_requested < oldest_timestamp) { +                oldest_timestamp = bucket_start[i].time_last_requested; +                oldest_index = i; +            } +        } + +        // Compute the shared key for the cache +        if (encrypt_precompute(public_key, cache->self_secret_key, bucket_start[oldest_index].shared_key) != 0) { +            // Don't put anything in the cache on error +            return nullptr; +        } + +        // update cache entry +        memcpy(bucket_start[oldest_index].public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); +        bucket_start[oldest_index].time_last_requested = cur_time; +        found = bucket_start[oldest_index].shared_key; +    } + +    return found; +} diff --git a/protocols/Tox/libtox/src/toxcore/shared_key_cache.h b/protocols/Tox/libtox/src/toxcore/shared_key_cache.h new file mode 100644 index 0000000000..97d8b7a4a8 --- /dev/null +++ b/protocols/Tox/libtox/src/toxcore/shared_key_cache.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2022 The TokTok team. + */ + +#ifndef C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H +#define C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H + +#include <stdint.h>     // uint*_t + +#include "crypto_core.h" +#include "mono_time.h" + +/** + * This implements a cache for shared keys, since key generation is expensive. + */ + +typedef struct Shared_Key_Cache Shared_Key_Cache; + +/** + * @brief Initializes a new shared key cache. + * @param time Time object for retrieving current time. + * @param self_secret_key Our own secret key of length CRYPTO_SECRET_KEY_SIZE, + * it must not change during the lifetime of the cache. + * @param timeout Number of milliseconds, after which a key should be evicted. + * @param keys_per_slot There are 256 slots, this controls how many keys are stored per slot and the size of the cache. + * @return nullptr on error. + */ +non_null() +Shared_Key_Cache *shared_key_cache_new(const Mono_Time *time, +                                       const uint8_t *self_secret_key, +                                       uint64_t timeout, uint8_t keys_per_slot); + +/** + * @brief Deletes the cache and frees all resources. + * @param cache Cache to delete or nullptr. + */ +nullable(1) +void shared_key_cache_free(Shared_Key_Cache *cache); + +/** + * @brief Looks up a key from the cache or computes it if it didn't exist yet. + * @param cache Cache to perform the lookup on. + * @param public_key Public key, used for the lookup and computation. + * + * @return The shared key of length CRYPTO_SHARED_KEY_SIZE, matching the public key and our secret key. + * @return nullptr on error. + */ +non_null() +const uint8_t* shared_key_cache_lookup(Shared_Key_Cache *cache, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]); + +#endif // C_TOXCORE_TOXCORE_SHARED_KEY_CACHE_H diff --git a/protocols/Tox/libtox/src/toxcore/state.h b/protocols/Tox/libtox/src/toxcore/state.h index 716286d6d1..0cc188d50d 100644 --- a/protocols/Tox/libtox/src/toxcore/state.h +++ b/protocols/Tox/libtox/src/toxcore/state.h @@ -34,6 +34,7 @@ typedef enum State_Type {      STATE_TYPE_NAME          = 4,      STATE_TYPE_STATUSMESSAGE = 5,      STATE_TYPE_STATUS        = 6, +    STATE_TYPE_GROUPS        = 7,      STATE_TYPE_TCP_RELAY     = 10,      STATE_TYPE_PATH_NODE     = 11,      STATE_TYPE_CONFERENCES   = 20, diff --git a/protocols/Tox/libtox/src/toxcore/tox.c b/protocols/Tox/libtox/src/toxcore/tox.c index 079f404079..3f7dcc439d 100644 --- a/protocols/Tox/libtox/src/toxcore/tox.c +++ b/protocols/Tox/libtox/src/toxcore/tox.c @@ -19,6 +19,8 @@  #include "Messenger.h"  #include "ccompat.h"  #include "group.h" +#include "group_chats.h" +#include "group_moderation.h"  #include "logger.h"  #include "mono_time.h"  #include "network.h" @@ -54,6 +56,10 @@ static_assert(TOX_MAX_NAME_LENGTH == MAX_NAME_LENGTH,                "TOX_MAX_NAME_LENGTH is assumed to be equal to MAX_NAME_LENGTH");  static_assert(TOX_MAX_STATUS_MESSAGE_LENGTH == MAX_STATUSMESSAGE_LENGTH,                "TOX_MAX_STATUS_MESSAGE_LENGTH is assumed to be equal to MAX_STATUSMESSAGE_LENGTH"); +static_assert(TOX_GROUP_MAX_MESSAGE_LENGTH == GROUP_MAX_MESSAGE_LENGTH, +              "TOX_GROUP_MAX_MESSAGE_LENGTH is assumed to be equal to GROUP_MAX_MESSAGE_LENGTH"); +static_assert(TOX_MAX_CUSTOM_PACKET_SIZE == MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE, +              "TOX_MAX_CUSTOM_PACKET_SIZE is assumed to be equal to MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE");  struct Tox_Userdata {      Tox *tox; @@ -317,8 +323,8 @@ static void tox_dht_get_nodes_response_handler(const DHT *dht, const Node_format      Ip_Ntoa ip_str;      tox_data->tox->dht_get_nodes_response_callback( -            tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), -            tox_data->user_data); +        tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), +        tox_data->user_data);  }  static m_friend_lossy_packet_cb tox_friend_lossy_packet_handler; @@ -353,6 +359,218 @@ static void tox_friend_lossless_packet_handler(Messenger *m, uint32_t friend_num      }  } +#ifndef VANILLA_NACL +non_null(1, 4) nullable(6) +static void tox_group_peer_name_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +                                        const uint8_t *name, size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_peer_name_callback != nullptr) { +        tox_data->tox->group_peer_name_callback(tox_data->tox, group_number, peer_id, name, length, tox_data->user_data); +    } +} + +non_null(1) nullable(5) +static void tox_group_peer_status_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +        unsigned int status, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_peer_status_callback != nullptr) { +        tox_data->tox->group_peer_status_callback(tox_data->tox, group_number, peer_id, (Tox_User_Status)status, +                tox_data->user_data); +    } +} + +non_null(1, 4) nullable(6) +static void tox_group_topic_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, +                                    size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_topic_callback != nullptr) { +        tox_data->tox->group_topic_callback(tox_data->tox, group_number, peer_id, topic, length, tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_topic_lock_handler(const Messenger *m, uint32_t group_number, unsigned int topic_lock, +        void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_topic_lock_callback != nullptr) { +        tox_data->tox->group_topic_lock_callback(tox_data->tox, group_number, (Tox_Group_Topic_Lock)topic_lock, +                tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_voice_state_handler(const Messenger *m, uint32_t group_number, unsigned int voice_state, +        void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_voice_state_callback != nullptr) { +        tox_data->tox->group_voice_state_callback(tox_data->tox, group_number, (Tox_Group_Voice_State)voice_state, +                tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_peer_limit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_limit, +        void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_peer_limit_callback != nullptr) { +        tox_data->tox->group_peer_limit_callback(tox_data->tox, group_number, peer_limit, tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_privacy_state_handler(const Messenger *m, uint32_t group_number, unsigned int privacy_state, +        void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_privacy_state_callback != nullptr) { +        tox_data->tox->group_privacy_state_callback(tox_data->tox, group_number, (Tox_Group_Privacy_State)privacy_state, +                tox_data->user_data); +    } +} + +non_null(1) nullable(3, 5) +static void tox_group_password_handler(const Messenger *m, uint32_t group_number, const uint8_t *password, +                                       size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_password_callback != nullptr) { +        tox_data->tox->group_password_callback(tox_data->tox, group_number, password, length, tox_data->user_data); +    } +} + +non_null(1, 5) nullable(8) +static void tox_group_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, +                                      const uint8_t *message, size_t length, uint32_t message_id, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_message_callback != nullptr) { +        tox_data->tox->group_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, length, +                                              message_id, tox_data->user_data); +    } +} + +non_null(1, 5) nullable(7) +static void tox_group_private_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +        unsigned int type, const uint8_t *message, size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_private_message_callback != nullptr) { +        tox_data->tox->group_private_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, +                length, +                tox_data->user_data); +    } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +        const uint8_t *data, size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_custom_packet_callback != nullptr) { +        tox_data->tox->group_custom_packet_callback(tox_data->tox, group_number, peer_id, data, length, tox_data->user_data); +    } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_private_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +        const uint8_t *data, size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_custom_private_packet_callback != nullptr) { +        tox_data->tox->group_custom_private_packet_callback(tox_data->tox, group_number, peer_id, data, length, +                tox_data->user_data); +    } +} + +non_null(1, 3, 5) nullable(7) +static void tox_group_invite_handler(const Messenger *m, uint32_t friend_number, const uint8_t *invite_data, +                                     size_t length, const uint8_t *group_name, size_t group_name_length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_invite_callback != nullptr) { +        tox_data->tox->group_invite_callback(tox_data->tox, friend_number, invite_data, length, group_name, group_name_length, +                                             tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_peer_join_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_peer_join_callback != nullptr) { +        tox_data->tox->group_peer_join_callback(tox_data->tox, group_number, peer_id, tox_data->user_data); +    } +} + +non_null(1, 5) nullable(7, 9) +static void tox_group_peer_exit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, +                                        unsigned int exit_type, const uint8_t *name, size_t name_length, +                                        const uint8_t *part_message, size_t length, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_peer_exit_callback != nullptr) { +        tox_data->tox->group_peer_exit_callback(tox_data->tox, group_number, peer_id, (Tox_Group_Exit_Type) exit_type, name, +                                                name_length, +                                                part_message, length, tox_data->user_data); +    } +} + +non_null(1) nullable(3) +static void tox_group_self_join_handler(const Messenger *m, uint32_t group_number, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_self_join_callback != nullptr) { +        tox_data->tox->group_self_join_callback(tox_data->tox, group_number, tox_data->user_data); +    } +} + +non_null(1) nullable(4) +static void tox_group_join_fail_handler(const Messenger *m, uint32_t group_number, unsigned int fail_type, +                                        void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_join_fail_callback != nullptr) { +        tox_data->tox->group_join_fail_callback(tox_data->tox, group_number, (Tox_Group_Join_Fail)fail_type, +                                                tox_data->user_data); +    } +} + +non_null(1) nullable(6) +static void tox_group_moderation_handler(const Messenger *m, uint32_t group_number, uint32_t source_peer_number, +        uint32_t target_peer_number, unsigned int mod_type, void *user_data) +{ +    struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + +    if (tox_data->tox->group_moderation_callback != nullptr) { +        tox_data->tox->group_moderation_callback(tox_data->tox, group_number, source_peer_number, target_peer_number, +                (Tox_Group_Mod_Event)mod_type, +                tox_data->user_data); +    } +} +#endif  bool tox_version_is_compatible(uint32_t major, uint32_t minor, uint32_t patch)  { @@ -522,6 +740,7 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error)      const Tox_System *sys = tox_options_get_operating_system(opts);      const Tox_System default_system = tox_default_system(); +      if (sys == nullptr) {          sys = &default_system;      } @@ -553,7 +772,8 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error)          const char *const proxy_host = tox_options_get_proxy_host(opts); -        if (proxy_host == nullptr || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) { +        if (proxy_host == nullptr +                || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) {              SET_ERROR_PARAMETER(error, TOX_ERR_NEW_PROXY_BAD_HOST);              // TODO(irungentoo): TOX_ERR_NEW_PROXY_NOT_FOUND if domain.              tox_options_free(default_options); @@ -671,6 +891,27 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error)      custom_lossy_packet_registerhandler(tox->m, tox_friend_lossy_packet_handler);      custom_lossless_packet_registerhandler(tox->m, tox_friend_lossless_packet_handler); +#ifndef VANILLA_NACL +    m_callback_group_invite(tox->m, tox_group_invite_handler); +    gc_callback_message(tox->m, tox_group_message_handler); +    gc_callback_private_message(tox->m, tox_group_private_message_handler); +    gc_callback_custom_packet(tox->m, tox_group_custom_packet_handler); +    gc_callback_custom_private_packet(tox->m, tox_group_custom_private_packet_handler); +    gc_callback_moderation(tox->m, tox_group_moderation_handler); +    gc_callback_nick_change(tox->m, tox_group_peer_name_handler); +    gc_callback_status_change(tox->m, tox_group_peer_status_handler); +    gc_callback_topic_change(tox->m, tox_group_topic_handler); +    gc_callback_peer_limit(tox->m, tox_group_peer_limit_handler); +    gc_callback_privacy_state(tox->m, tox_group_privacy_state_handler); +    gc_callback_topic_lock(tox->m, tox_group_topic_lock_handler); +    gc_callback_password(tox->m, tox_group_password_handler); +    gc_callback_peer_join(tox->m, tox_group_peer_join_handler); +    gc_callback_peer_exit(tox->m, tox_group_peer_exit_handler); +    gc_callback_self_join(tox->m, tox_group_self_join_handler); +    gc_callback_rejected(tox->m, tox_group_join_fail_handler); +    gc_callback_voice_state(tox->m, tox_group_voice_state_handler); +#endif +      tox_options_free(default_options);      tox_unlock(tox); @@ -714,9 +955,9 @@ size_t tox_get_savedata_size(const Tox *tox)      assert(tox != nullptr);      tox_lock(tox);      const size_t ret = 2 * sizeof(uint32_t) -                 + messenger_size(tox->m) -                 + conferences_size(tox->m->conferences_object) -                 + end_size(); +                       + messenger_size(tox->m) +                       + conferences_size(tox->m->conferences_object) +                       + end_size();      tox_unlock(tox);      return ret;  } @@ -749,7 +990,8 @@ void tox_get_savedata(const Tox *tox, uint8_t *savedata)  }  non_null(5) nullable(1, 2, 4, 6) -static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, IP_Port **root, Tox_Err_Bootstrap *error) +static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, +                                      IP_Port **root, Tox_Err_Bootstrap *error)  {      assert(tox != nullptr);      assert(root != nullptr); @@ -766,7 +1008,7 @@ static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port,      const int32_t count = net_getipport(host, root, TOX_SOCK_DGRAM); -    if (count == -1) { +    if (count < 1) {          LOGGER_DEBUG(tox->m->log, "could not resolve bootstrap node '%s'", host);          net_freeipport(*root);          SET_ERROR_PARAMETER(error, TOX_ERR_BOOTSTRAP_BAD_HOST); @@ -879,6 +1121,7 @@ Tox_Connection tox_self_get_connection_status(const Tox *tox)      }      LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); +    return TOX_CONNECTION_NONE;  } @@ -2580,3 +2823,1760 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error)      tox_unlock(tox);      return 0;  } + +/* GROUPCHAT FUNCTIONS */ + +#ifndef VANILLA_NACL +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_invite_callback = callback; +} + +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_message_callback = callback; +} + +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_private_message_callback = callback; +} + +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_custom_packet_callback = callback; +} + +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_custom_private_packet_callback = callback; +} + +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_moderation_callback = callback; +} + +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_peer_name_callback = callback; +} + +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_peer_status_callback = callback; +} + +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_topic_callback = callback; +} + +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_privacy_state_callback = callback; +} + +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_topic_lock_callback = callback; +} + +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_voice_state_callback = callback; +} + +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_peer_limit_callback = callback; +} + +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_password_callback = callback; +} + +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_peer_join_callback = callback; +} + +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_peer_exit_callback = callback; +} + +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_self_join_callback = callback; +} + +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback) +{ +    assert(tox != nullptr); +    tox->group_join_fail_callback = callback; +} + +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, +                       size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_group_add(tox->m->group_handler, (Group_Privacy_State) privacy_state, +                                 group_name, group_name_length, name, name_length); +    tox_unlock(tox); + +    if (ret >= 0) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_OK); +        return ret; +    } + +    switch (ret) { +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_TOO_LONG); +            return UINT32_MAX; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_EMPTY); +            return UINT32_MAX; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_INIT); +            return UINT32_MAX; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_STATE); +            return UINT32_MAX; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_ANNOUNCE); +            return UINT32_MAX; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return UINT32_MAX; +} + +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, +                        const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_group_join(tox->m->group_handler, chat_id, name, name_length, password, password_length); +    tox_unlock(tox); + +    if (ret >= 0) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_OK); +        return ret; +    } + +    switch (ret) { +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_INIT); +            return UINT32_MAX; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_BAD_CHAT_ID); +            return UINT32_MAX; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_TOO_LONG); +            return UINT32_MAX; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_EMPTY); +            return UINT32_MAX; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_PASSWORD); +            return UINT32_MAX; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_CORE); +            return UINT32_MAX; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return UINT32_MAX; +} + +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_OK); + +    const bool ret = chat->connection_state == CS_CONNECTED || chat->connection_state == CS_CONNECTING; +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + + +    const bool ret = gc_disconnect_from_group(tox->m->group_handler, chat); + +    tox_unlock(tox); + +    if (!ret) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_OK); + +    return true; +} + +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_rejoin_group(tox->m->group_handler, chat); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_CORE); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, +                     Tox_Err_Group_Leave *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_group_exit(tox->m->group_handler, chat, part_message, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_FAIL_SEND); +            return true;   /* the group was still successfully deleted */ +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, +                             Tox_Err_Group_Self_Name_Set *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_set_self_nick(tox->m, group_number, name, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_INVALID); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    const size_t ret = gc_get_self_nick_size(chat); +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    gc_get_self_nick(chat, name); +    tox_unlock(tox); + +    return true; +} + +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, +                               Tox_Err_Group_Self_Status_Set *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_set_self_status(tox->m, group_number, (Group_Peer_Status) status); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_User_Status) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    const uint8_t status = gc_get_self_status(chat); +    tox_unlock(tox); + +    return (Tox_User_Status)status; +} + +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_Group_Role) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    const Group_Role role = gc_get_self_role(chat); +    tox_unlock(tox); + +    return (Tox_Group_Role)role; +} + +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    const uint32_t ret = gc_get_self_peer_id(chat); +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, +                                   Tox_Err_Group_Self_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + +    gc_get_self_public_key(chat, public_key); +    tox_unlock(tox); + +    return true; +} + +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                                    Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    const int ret = gc_get_peer_nick_size(chat, peer_id); +    tox_unlock(tox); + +    if (ret == -1) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return -1; +    } else { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +        return ret; +    } +} + +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, +                             Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const bool ret = gc_get_peer_nick(chat, peer_id, name); +    tox_unlock(tox); + +    if (!ret) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +    return true; +} + +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, +        Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_User_Status) - 1; +    } + +    const uint8_t ret = gc_get_status(chat, peer_id); +    tox_unlock(tox); + +    if (ret == UINT8_MAX) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return (Tox_User_Status) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +    return (Tox_User_Status)ret; +} + +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                                       Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_Group_Role) - 1; +    } + +    const uint8_t ret = gc_get_role(chat, peer_id); +    tox_unlock(tox); + +    if (ret == (uint8_t) -1) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return (Tox_Group_Role) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +    return (Tox_Group_Role)ret; +} + +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, +                                   Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_get_peer_public_key_by_peer_id(chat, peer_id, public_key); +    tox_unlock(tox); + +    if (ret == -1) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +    return true; +} + +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, +        Tox_Err_Group_Peer_Query *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return TOX_CONNECTION_NONE; +    } + +    const unsigned int ret = gc_get_peer_connection_status(chat, peer_id); +    tox_unlock(tox); + +    if (ret == 0) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); +        return TOX_CONNECTION_NONE; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); +    return (Tox_Connection)ret; +} + +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, +                         Tox_Err_Group_Topic_Set *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_set_topic(chat, topic, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const size_t ret = gc_get_topic_size(chat); +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    gc_get_topic(chat, topic); +    tox_unlock(tox); + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); +    return true; +} + +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const size_t ret = gc_get_group_name_size(chat); +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    gc_get_group_name(chat, group_name); +    tox_unlock(tox); + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    return true; +} + +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); +    gc_get_chat_id(chat, chat_id); +    tox_unlock(tox); + +    return true; +} + +uint32_t tox_group_get_number_groups(const Tox *tox) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const uint32_t ret = gc_count_groups(tox->m->group_handler); +    tox_unlock(tox); + +    return ret; +} + +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, +        Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_Group_Privacy_State) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const uint8_t state = gc_get_privacy_state(chat); +    tox_unlock(tox); + +    return (Tox_Group_Privacy_State)state; +} + +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_Group_Topic_Lock) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const Group_Topic_Lock topic_lock = gc_get_topic_lock_state(chat); +    tox_unlock(tox); + +    return (Tox_Group_Topic_Lock)topic_lock; +} + +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, +        Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return (Tox_Group_Voice_State) - 1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const Group_Voice_State voice_state = gc_get_voice_state(chat); +    tox_unlock(tox); + +    return (Tox_Group_Voice_State)voice_state; +} + +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const uint16_t ret = gc_get_max_peers(chat); +    tox_unlock(tox); + +    return ret; +} + +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return -1; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    const size_t ret = gc_get_password_size(chat); +    tox_unlock(tox); + +    return ret; +} + +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, +                            Tox_Err_Group_State_Queries *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + +    gc_get_password(chat, password); +    tox_unlock(tox); + +    return true; +} + +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, +                            size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_send_message(chat, message, length, type, message_id); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_EMPTY); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, +                                    const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_send_private_message(chat, peer_id, type, message, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS); +            return false; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, +                                  size_t length, Tox_Err_Group_Send_Custom_Packet *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_send_custom_packet(chat, lossless, data, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, +        const uint8_t *data, size_t length, +        Tox_Err_Group_Send_Custom_Private_Packet *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_send_custom_private_packet(chat, lossless, peer_id, data, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, +                             Tox_Err_Group_Invite_Friend *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    if (!friend_is_valid(tox->m, friend_number)) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_invite_friend(tox->m->group_handler, chat, friend_number, send_group_invite_packet); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, +                                 const uint8_t *name, size_t name_length, const uint8_t *password, +                                 size_t password_length, Tox_Err_Group_Invite_Accept *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_accept_invite(tox->m->group_handler, friend_number, invite_data, length, name, name_length, password, +                                     password_length); +    tox_unlock(tox); + +    if (ret >= 0) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_OK); +        return ret; +    } + +    switch (ret) { +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE); +            return UINT32_MAX; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED); +            return UINT32_MAX; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG); +            return UINT32_MAX; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY); +            return UINT32_MAX; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD); +            return UINT32_MAX; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_CORE); +            return UINT32_MAX; +        } + +        case -7: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND); +            return UINT32_MAX; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return UINT32_MAX; +} + +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, +                                    Tox_Err_Group_Founder_Set_Password *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_founder_set_password(chat, password, length); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, +        Tox_Err_Group_Founder_Set_Privacy_State *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_founder_set_privacy_state(tox->m, group_number, (Group_Privacy_State) privacy_state); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, +                                      Tox_Err_Group_Founder_Set_Topic_Lock *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_founder_set_topic_lock(tox->m, group_number, (Group_Topic_Lock) topic_lock); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET); +            return false; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, +                                       Tox_Err_Group_Founder_Set_Voice_State *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_founder_set_voice_state(tox->m, group_number, (Group_Voice_State)voice_state); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, +                                      Tox_Err_Group_Founder_Set_Peer_Limit *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    if (chat->connection_state == CS_DISCONNECTED) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_founder_set_max_peers(chat, max_peers); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, +                          Tox_Err_Group_Set_Ignore *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + +    if (chat == nullptr) { +        SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND); +        tox_unlock(tox); +        return false; +    } + +    const int ret = gc_set_ignore(chat, peer_id, ignore); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_SELF); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, +                            Tox_Err_Group_Mod_Set_Role *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_set_peer_role(tox->m, group_number, peer_id, (Group_Role) role); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION); +            return false; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_SELF); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                             Tox_Err_Group_Mod_Kick_Peer *error) +{ +    assert(tox != nullptr); + +    tox_lock(tox); +    const int ret = gc_kick_peer(tox->m, group_number, peer_id); +    tox_unlock(tox); + +    switch (ret) { +        case 0: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_OK); +            return true; +        } + +        case -1: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND); +            return false; +        } + +        case -2: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND); +            return false; +        } + +        case -3: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS); +            return false; +        } + +        case -4: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION); +            return false; +        } + +        case -5: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND); +            return false; +        } + +        case -6: { +            SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_SELF); +            return false; +        } +    } + +    /* can't happen */ +    LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + +    return false; +} + +#endif /* VANILLA_NACL */ + diff --git a/protocols/Tox/libtox/src/toxcore/tox.h b/protocols/Tox/libtox/src/toxcore/tox.h index e8d55c6689..fd1901f69c 100644 --- a/protocols/Tox/libtox/src/toxcore/tox.h +++ b/protocols/Tox/libtox/src/toxcore/tox.h @@ -3267,6 +3267,2192 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error);  /** @} */ +/******************************************************************************* + * + * :: Group chats + * + ******************************************************************************/ + + + + +/******************************************************************************* + * + * :: Group chat numeric constants + * + ******************************************************************************/ + + + +/** @{ + * Maximum length of a group topic. + */ +#define TOX_GROUP_MAX_TOPIC_LENGTH     512 + +uint32_t tox_group_max_topic_length(void); + +/** + * Maximum length of a peer part message. + */ +#define TOX_GROUP_MAX_PART_LENGTH      128 + +uint32_t tox_group_max_part_length(void); + +/** + * Maximum length of a group text message. + */ +#define TOX_GROUP_MAX_MESSAGE_LENGTH    1372 + +uint32_t tox_group_max_message_length(void); + +/** + * Maximum length of a group custom lossy packet. + */ +#define TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH 500 + +uint32_t tox_group_max_custom_lossy_packet_length(void); + +/** + * Maximum length of a group custom lossless packet. + */ +#define TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH 1373 + +uint32_t tox_group_max_custom_lossless_packet_length(void); + +/** + * Maximum length of a group name. + */ +#define TOX_GROUP_MAX_GROUP_NAME_LENGTH 48 + +uint32_t tox_group_max_group_name_length(void); + +/** + * Maximum length of a group password. + */ +#define TOX_GROUP_MAX_PASSWORD_SIZE    32 + +uint32_t tox_group_max_password_size(void); + +/** + * Number of bytes in a group Chat ID. + */ +#define TOX_GROUP_CHAT_ID_SIZE         32 + +uint32_t tox_group_chat_id_size(void); + +/** + * Size of a peer public key. + */ +#define TOX_GROUP_PEER_PUBLIC_KEY_SIZE 32 + +uint32_t tox_group_peer_public_key_size(void); + + +/******************************************************************************* + * + * :: Group chat state enumerators + * + ******************************************************************************/ + + + +/** + * Represents the group privacy state. + */ +typedef enum Tox_Group_Privacy_State { + +    /** +     * The group is considered to be public. Anyone may join the group using the Chat ID. +     * +     * If the group is in this state, even if the Chat ID is never explicitly shared +     * with someone outside of the group, information including the Chat ID, IP addresses, +     * and peer ID's (but not Tox ID's) is visible to anyone with access to a node +     * storing a DHT entry for the given group. +     */ +    TOX_GROUP_PRIVACY_STATE_PUBLIC, + +    /** +     * The group is considered to be private. The only way to join the group is by having +     * someone in your contact list send you an invite. +     * +     * If the group is in this state, no group information (mentioned above) is present in the DHT; +     * the DHT is not used for any purpose at all. If a public group is set to private, +     * all DHT information related to the group will expire shortly. +     */ +    TOX_GROUP_PRIVACY_STATE_PRIVATE, + +} Tox_Group_Privacy_State; + + +/** + * Represents the state of the group topic lock. + */ +typedef enum Tox_Group_Topic_Lock { + +    /** +     * The topic lock is enabled. Only peers with the founder and moderator roles may set the topic. +     */ +    TOX_GROUP_TOPIC_LOCK_ENABLED, + +    /** +     * The topic lock is disabled. All peers except those with the observer role may set the topic. +     */ +    TOX_GROUP_TOPIC_LOCK_DISABLED, + +} Tox_Group_Topic_Lock; + +/** + * Represents the group voice state, which determines which Group Roles have permission to speak + * in the group chat. The voice state does not have any effect private messages or topic setting. + */ +typedef enum Tox_Group_Voice_State { +    /** +     * All group roles above Observer have permission to speak. +     */ +    TOX_GROUP_VOICE_STATE_ALL, + +    /** +     * Moderators and Founders have permission to speak. +     */ +    TOX_GROUP_VOICE_STATE_MODERATOR, + +    /** +     * Only the founder may speak. +     */ +    TOX_GROUP_VOICE_STATE_FOUNDER, +} Tox_Group_Voice_State; + +/** + * Represents group roles. + * + * Roles are hierarchical in that each role has a set of privileges plus all the privileges + * of the roles below it. + */ +typedef enum Tox_Group_Role { + +    /** +     * May kick all other peers as well as set their role to anything (except founder). +     * Founders may also set the group password, toggle the privacy state, and set the peer limit. +     */ +    TOX_GROUP_ROLE_FOUNDER, + +    /** +     * May kick and set the user and observer roles for peers below this role. +     * May also set the group topic. +     */ +    TOX_GROUP_ROLE_MODERATOR, + +    /** +     * May communicate with other peers normally. +     */ +    TOX_GROUP_ROLE_USER, + +    /** +     * May observe the group and ignore peers; may not communicate with other peers or with the group. +     */ +    TOX_GROUP_ROLE_OBSERVER, + +} Tox_Group_Role; + + + +/******************************************************************************* + * + * :: Group chat instance management + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_New { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_NEW_OK, + +    /** +     * name exceeds TOX_MAX_NAME_LENGTH or group_name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. +     */ +    TOX_ERR_GROUP_NEW_TOO_LONG, + +    /** +     * name or group_name is NULL or length is zero. +     */ +    TOX_ERR_GROUP_NEW_EMPTY, + +    /** +     * The group instance failed to initialize. +     */ +    TOX_ERR_GROUP_NEW_INIT, + +    /** +     * The group state failed to initialize. This usually indicates that something went wrong +     * related to cryptographic signing. +     */ +    TOX_ERR_GROUP_NEW_STATE, + +    /** +     * The group failed to announce to the DHT. This indicates a network related error. +     */ +    TOX_ERR_GROUP_NEW_ANNOUNCE, + +} Tox_Err_Group_New; + + +/** + * Creates a new group chat. + * + * This function creates a new group chat object and adds it to the chats array. + * + * The caller of this function has Founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * @param privacy_state The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + *   the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + *   Otherwise a friend invite will be required to join the group. + * @param group_name The name of the group. The name must be non-NULL. + * @param group_name_length The length of the group name. This must be greater than zero and no larger than + *   TOX_GROUP_MAX_GROUP_NAME_LENGTH. + * @param name The name of the peer creating the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + *   than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, +                       size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error); + +typedef enum Tox_Err_Group_Join { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_JOIN_OK, + +    /** +     * The group instance failed to initialize. +     */ +    TOX_ERR_GROUP_JOIN_INIT, + +    /** +     * The chat_id pointer is set to NULL or a group with chat_id already exists. This usually +     * happens if the client attempts to create multiple sessions for the same group. +     */ +    TOX_ERR_GROUP_JOIN_BAD_CHAT_ID, + +    /** +     * name is NULL or name_length is zero. +     */ +    TOX_ERR_GROUP_JOIN_EMPTY, + +    /** +     * name exceeds TOX_MAX_NAME_LENGTH. +     */ +    TOX_ERR_GROUP_JOIN_TOO_LONG, + +    /** +     * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. +     */ +    TOX_ERR_GROUP_JOIN_PASSWORD, + +    /** +     * There was a core error when initiating the group. +     */ +    TOX_ERR_GROUP_JOIN_CORE, + +} Tox_Err_Group_Join; + + +/** + * Joins a group chat with specified Chat ID. + * + * This function creates a new group chat object, adds it to the chats array, and sends + * a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + * found a join attempt will be initiated. + * + * @param chat_id The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If length is equal to zero, + *   the password parameter is ignored. length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + *   than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, +                        const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error); + +typedef enum Tox_Err_Group_Is_Connected { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_IS_CONNECTED_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND, + +} Tox_Err_Group_Is_Connected; + + +/** + * Returns true if the group chat is currently connected or attempting to connect to other peers + * in the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error); + +typedef enum Tox_Err_Group_Disconnect { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_DISCONNECT_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND, + +    /** +     * The group is already disconnected. +     */ +    TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED, +} Tox_Err_Group_Disconnect; + + +/** + * Disconnects from a group chat while retaining the group state and credentials. + * + * Returns true if we successfully disconnect from the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error); + +typedef enum Tox_Err_Group_Reconnect { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_RECONNECT_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND, + +    /** +     * There was a core error when initiating the group. +     */ +    TOX_ERR_GROUP_RECONNECT_CORE, + +} Tox_Err_Group_Reconnect; + + +/** + * Reconnects to a group. + * + * This function disconnects from all peers in the group, then attempts to reconnect with the group. + * The caller's state is not changed (i.e. name, status, role, chat public key etc.). + * + * @param group_number The group number of the group we wish to reconnect to. + * + * @return true on success. + */ +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error); + +typedef enum Tox_Err_Group_Leave { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_LEAVE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND, + +    /** +     * Message length exceeded TOX_GROUP_MAX_PART_LENGTH. +     */ +    TOX_ERR_GROUP_LEAVE_TOO_LONG, + +    /** +     * The parting packet failed to send. +     */ +    TOX_ERR_GROUP_LEAVE_FAIL_SEND, +} Tox_Err_Group_Leave; + + +/** + * Leaves a group. + * + * This function sends a parting packet containing a custom (non-obligatory) message to all + * peers in a group, and deletes the group from the chat array. All group state information is permanently + * lost, including keys and role credentials. + * + * @param group_number The group number of the group we wish to leave. + * @param part_message The parting message to be sent to all the peers. Set to NULL if we do not wish to + *   send a parting message. + * @param length The length of the parting message. Set to 0 if we do not wish to send a parting message. + * + * @return true if the group chat instance is successfully deleted. + */ +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, +                     Tox_Err_Group_Leave *error); + + +/******************************************************************************* + * + * :: Group user-visible client information (nickname/status/role/public key) + * + ******************************************************************************/ + + + +/** + * General error codes for self state get and size functions. + */ +typedef enum Tox_Err_Group_Self_Query { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SELF_QUERY_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND, + +} Tox_Err_Group_Self_Query; + + +/** + * Error codes for self name setting. + */ +typedef enum Tox_Err_Group_Self_Name_Set { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SELF_NAME_SET_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND, + +    /** +     * Name length exceeded TOX_MAX_NAME_LENGTH. +     */ +    TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG, + +    /** +     * The length given to the set function is zero or name is a NULL pointer. +     */ +    TOX_ERR_GROUP_SELF_NAME_SET_INVALID, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Name_Set; + + +/** + * Set the client's nickname for the group instance designated by the given group number. + * + * Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + * pointer, the function call will fail. + * + * @param name A byte array containing the new nickname. + * @param length The size of the name byte array. + * + * @return true on success. + */ +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, +                             Tox_Err_Group_Self_Name_Set *error); + +/** + * Return the length of the client's current nickname for the group instance designated + * by group_number as passed to tox_group_self_set_name. + * + * If no nickname was set before calling this function, the name is empty, + * and this function returns 0. + * + * @see threading for concurrency implications. + */ +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the nickname set by tox_group_self_set_name to a byte array. + * + * If no nickname was set before calling this function, the name is empty, + * and this function has no effect. + * + * Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + * + * @param name A valid memory location large enough to hold the nickname. + *   If this parameter is NULL, the function has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error); + +/** + * Error codes for self status setting. + */ +typedef enum Tox_Err_Group_Self_Status_Set { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SELF_STATUS_SET_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Status_Set; + + +/** + * Set the client's status for the group instance. Status must be a Tox_User_Status. + * + * @return true on success. + */ +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, +                               Tox_Err_Group_Self_Status_Set *error); + +/** + * returns the client's status for the group instance on success. + * return value is unspecified on failure. + */ +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's role for the group instance on success. + * return value is unspecified on failure. + */ +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's peer id for the group instance on success. + * return value is unspecified on failure. + */ +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the client's group public key designated by the given group number to a byte array. + * + * This key will be permanently tied to the client's identity for this particular group until + * the client explicitly leaves the group. This key is the only way for other peers to reliably + * identify the client across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + * + * @param public_key A valid memory region large enough to store the public key. + *   If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, +                                   Tox_Err_Group_Self_Query *error); + + +/******************************************************************************* + * + * :: Peer-specific group state queries. + * + ******************************************************************************/ + + + +/** + * Error codes for peer info queries. + */ +typedef enum Tox_Err_Group_Peer_Query { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_PEER_QUERY_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND, + +    /** +     * The ID passed did not designate a valid peer. +     */ +    TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND, + +} Tox_Err_Group_Peer_Query; + + +/** + * Return the length of the peer's name. If the group number or ID is invalid, the + * return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name length we want to retrieve. + * + * The return value is equal to the `length` argument received by the last + * `group_peer_name` callback. + */ +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                                    Tox_Err_Group_Peer_Query *error); + +/** + * Write the name of the peer designated by the given ID to a byte + * array. + * + * Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last + * `group_peer_name` callback. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name we wish to retrieve. + * @param name A valid memory region large enough to store the friend's name. + * + * @return true on success. + */ +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, +                             Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's user status (away/busy/...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose status we wish to query. + * + * The status returned is equal to the last status received through the + * `group_peer_status` callback. + */ +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, +        Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's role (user/moderator/founder...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose role we wish to query. + * + * The role returned is equal to the last role received through the + * `group_moderation` callback. + */ +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                                       Tox_Err_Group_Peer_Query *error); + +/** + * Return the type of connection we have established with a peer. + * + * This function will return an error if called on ourselves. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose connection status we wish to query. + */ +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, +        Tox_Err_Group_Peer_Query *error); + +/** + * Write the group public key with the designated peer_id for the designated group number to public_key. + * + * This key will be permanently tied to a particular peer until they explicitly leave the group and is + * the only way to reliably identify the same peer across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. If `public_key` is null + * this function has no effect. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose public key we wish to retrieve. + * @param public_key A valid memory region large enough to store the public key. + *   If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, +                                   Tox_Err_Group_Peer_Query *error); + +/** + * @param group_number The group number of the group the name change is intended for. + * @param peer_id The ID of the peer who has changed their name. + * @param name The name data. + * @param length The length of the name. + */ +typedef void tox_group_peer_name_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *name, +                                    size_t length, void *user_data); + + +/** + * Set the callback for the `group_peer_name` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their nickname. + */ +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback); + +/** + * @param group_number The group number of the group the status change is intended for. + * @param peer_id The ID of the peer who has changed their status. + * @param status The new status of the peer. + */ +typedef void tox_group_peer_status_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_User_Status status, +                                      void *user_data); + + +/** + * Set the callback for the `group_peer_status` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their status. + */ +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback); + + +/******************************************************************************* + * + * :: Group chat state queries and events. + * + ******************************************************************************/ + + + +/** + * General error codes for group state get and size functions. + */ +typedef enum Tox_Err_Group_State_Queries { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_STATE_QUERIES_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND, + +} Tox_Err_Group_State_Queries; + + +/** + * Error codes for group topic setting. + */ +typedef enum Tox_Err_Group_Topic_Set { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_TOPIC_SET_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND, + +    /** +     * Topic length exceeded TOX_GROUP_MAX_TOPIC_LENGTH. +     */ +    TOX_ERR_GROUP_TOPIC_SET_TOO_LONG, + +    /** +     * The caller does not have the required permissions to set the topic. +     */ +    TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS, + +    /** +     * The packet could not be created. This error is usually related to cryptographic signing. +     */ +    TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED, + +} Tox_Err_Group_Topic_Set; + + +/** + * Set the group topic and broadcast it to the rest of the group. + * + * topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + * topic is set to NULL, the topic will be unset. + * + * @return true on success. + */ +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, +                         Tox_Err_Group_Topic_Set *error); + +/** + * Return the length of the group topic. If the group number is invalid, the + * return value is unspecified. + * + * The return value is equal to the `length` argument received by the last + * `group_topic` callback. + */ +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the topic designated by the given group number to a byte array. + * + * Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last + * `group_topic` callback. + * + * @param topic A valid memory region large enough to store the topic. + *   If this parameter is NULL, this function has no effect. + * + * @return true on success. + */ +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the topic change is intended for. + * @param peer_id The ID of the peer who changed the topic. If the peer who set the topic + *   is not present in our peer list this value will be set to 0. + * @param topic The topic data. + * @param length The topic length. + */ +typedef void tox_group_topic_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, size_t length, +                                void *user_data); + + +/** + * Set the callback for the `group_topic` event. Pass NULL to unset. + * + * This event is triggered when a peer changes the group topic. + */ +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback); + +/** + * Return the length of the group name. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the name of the group designated by the given group number to a byte array. + * + * Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + * + * @param group_name A valid memory region large enough to store the group name. + *   If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error); + +/** + * Write the Chat ID designated by the given group number to a byte array. + * + * `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. + * + * @param chat_id A valid memory region large enough to store the Chat ID. + *   If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error); + +/** + * Return the number of groups in the Tox chats array. + */ +uint32_t tox_group_get_number_groups(const Tox *tox); + +/** + * Return the privacy state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_privacy_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, +        Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the privacy state is intended for. + * @param privacy_state The new privacy state. + */ +typedef void tox_group_privacy_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, +                                        void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the privacy state. + */ +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback); + +/** + * Return the voice state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last `group_voice_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, +        Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the voice state change is intended for. + * @param voice_state The new voice state. + */ +typedef void tox_group_voice_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, +                                      void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the voice state. + */ +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback); + +/** + * Return the topic lock status of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_topic_lock` callback. + * + * @see the `Group chat founder contols` section for the respective set function. + */ +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, +        Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the topic lock has changed. + * @param topic_lock The new topic lock state. + */ +typedef void tox_group_topic_lock_cb(Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, void *user_data); + + + +/** + * Set the callback for the `group_topic_lock` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the topic lock status. + */ +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback); + +/** + * Return the maximum number of peers allowed for the group designated by the given group number. + * If the group number is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_peer_limit` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the peer limit has changed. + * @param peer_limit The new peer limit for the group. + */ +typedef void tox_group_peer_limit_cb(Tox *tox, uint32_t group_number, uint32_t peer_limit, void *user_data); + + +/** + * Set the callback for the `group_peer_limit` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the maximum peer limit. + */ +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback); + +/** + * Return the length of the group password. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the password for the group designated by the given group number to a byte array. + * + * Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + * + * The data received is equal to the data received by the last + * `group_password` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + * + * @param password A valid memory region large enough to store the group password. + *   If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, +                            Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the password has changed. + * @param password The new group password. + * @param length The length of the password. + */ +typedef void tox_group_password_cb(Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, +                                   void *user_data); + + +/** + * Set the callback for the `group_password` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the group password. + */ +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback); + + +/******************************************************************************* + * + * :: Group chat message sending + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Send_Message { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND, + +    /** +     * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG, + +    /** +     * The message pointer is null or length is zero. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_EMPTY, + +    /** +     * The message type is invalid. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE, + +    /** +     * The caller does not have the required permissions to send group messages. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS, + +    /** +     * Packet failed to send. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED, + +} Tox_Err_Group_Send_Message; + + +/** + * Send a text chat message to the group. + * + * This function creates a group message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param type Message type (normal, action, ...). + * @param message A non-NULL pointer to the first element of a byte array + *   containing the message text. + * @param length Length of the message to be sent. + * @param message_id A pointer to a uint32_t. The message_id of this message will be returned + *   unless the parameter is NULL, in which case the returned parameter value will be undefined. + *   If this function returns false the returned parameter `message_id` value will also be undefined. + * + * @return true on success. + */ +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, +                            size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error); + +typedef enum Tox_Err_Group_Send_Private_Message { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND, + +    /** +     * The peer ID passed did not designate a valid peer. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND, + +    /** +     * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG, + +    /** +     * The message pointer is null or length is zero. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY, + +    /** +     * The caller does not have the required permissions to send group messages. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS, + +    /** +     * Packet failed to send. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED, + +    /** +     * The message type is invalid. +     */ +    TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE, + +} Tox_Err_Group_Send_Private_Message; + + +/** + * Send a text chat message to the specified peer in the specified group. + * + * This function creates a group private message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer the message is intended for. + * @param message A non-NULL pointer to the first element of a byte array + *   containing the message text. + * @param length Length of the message to be sent. + * + * @return true on success. + */ +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, +                                    const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error); + +typedef enum Tox_Err_Group_Send_Custom_Packet { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND, + +    /** +     * Message length exceeded TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH if the +     * packet was lossy, or TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH if the +     * packet was lossless. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG, + +    /** +     * The message pointer is null or length is zero. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY, + +    /** +     * The caller does not have the required permissions to send group messages. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Packet; + + +/** + * Send a custom packet to the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The message length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, +                                  size_t length, +                                  Tox_Err_Group_Send_Custom_Packet *error); + + +typedef enum Tox_Err_Group_Send_Custom_Private_Packet { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND, + +    /** +     * Message length exceeded TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH if the +     * packet was lossy, or TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH if the +     * packet was lossless. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG, + +    /** +     * The message pointer is null or length is zero. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY, + +    /** +     * The peer ID passed did no designate a valid peer. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND, + +    /** +     * The caller does not have the required permissions to send group messages. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Private_Packet; + +/** + * Send a custom private packet to a designated peer in the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The packet length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, +        const uint8_t *data, size_t length, +        Tox_Err_Group_Send_Custom_Private_Packet *error); + + +/******************************************************************************* + * + * :: Group chat message receiving + * + ******************************************************************************/ + + + +/** + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer who sent the message. + * @param type The type of message (normal, action, ...). + * @param message The message data. + * @param message_id A pseudo message id that clients can use to uniquely identify this group message. + * @param length The length of the message. + */ +typedef void tox_group_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, +                                  const uint8_t *message, size_t length, uint32_t message_id, void *user_data); + + +/** + * Set the callback for the `group_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group message. + */ +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback); + +/** + * @param group_number The group number of the group the private message is intended for. + * @param peer_id The ID of the peer who sent the private message. + * @param message The message data. + * @param length The length of the message. + */ +typedef void tox_group_private_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, +        const uint8_t *message, size_t length, void *user_data); + + +/** + * Set the callback for the `group_private_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a private message. + */ +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, +                                        size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom packet. + */ +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_private_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, +        size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_private_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom private packet. + */ +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback); + + +/******************************************************************************* + * + * :: Group chat inviting and join/part events + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Invite_Friend { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND, + +    /** +     * The friend number passed did not designate a valid friend. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND, + +    /** +     * Creation of the invite packet failed. This indicates a network related error. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL, + +    /** +     * Packet failed to send. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED, + +} Tox_Err_Group_Invite_Friend; + + +/** + * Invite a friend to a group. + * + * This function creates an invite request packet and pushes it to the send queue. + * + * @param group_number The group number of the group the message is intended for. + * @param friend_number The friend number of the friend the invite is intended for. + * + * @return true on success. + */ +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, +                             Tox_Err_Group_Invite_Friend *error); + +typedef enum Tox_Err_Group_Invite_Accept { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_OK, + +    /** +     * The invite data is not in the expected format. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE, + +    /** +     * The group instance failed to initialize. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED, + +    /** +     * name exceeds TOX_MAX_NAME_LENGTH +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG, + +    /** +     * name is NULL or name_length is zero. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY, + +    /** +     * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD, + +    /** +     * There was a core error when initiating the group. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_CORE, + +    /** +     * Packet failed to send. +     */ +    TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND, + +} Tox_Err_Group_Invite_Accept; + + +/** + * Accept an invite to a group chat that the client previously received from a friend. The invite + * is only valid while the inviter is present in the group. + * + * @param invite_data The invite data received from the `group_invite` event. + * @param length The length of the invite data. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + *   than TOX_MAX_NAME_LENGTH. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If password_length is equal to zero, the password + *   parameter will be ignored. password_length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return the group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, +                                 const uint8_t *name, size_t name_length, const uint8_t *password, size_t password_length, +                                 Tox_Err_Group_Invite_Accept *error); + +/** + * @param friend_number The friend number of the contact who sent the invite. + * @param invite_data The invite data. + * @param length The length of invite_data. + */ +typedef void tox_group_invite_cb(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, +                                 const uint8_t *group_name, size_t group_name_length, void *user_data); + + +/** + * Set the callback for the `group_invite` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group invite from a friend. The client must store + * invite_data which is used to join the group via tox_group_invite_accept. + */ +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback); + +/** + * @param group_number The group number of the group in which a new peer has joined. + * @param peer_id The permanent ID of the new peer. This id should not be relied on for + * client behaviour and should be treated as a random value. + */ +typedef void tox_group_peer_join_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data); + + +/** + * Set the callback for the `group_peer_join` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self joins the group. + */ +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback); + +/** + * Represents peer exit events. These should be used with the `group_peer_exit` event. + */ +typedef enum Tox_Group_Exit_Type { + +    /** +     * The peer has quit the group. +     */ +    TOX_GROUP_EXIT_TYPE_QUIT, + +    /** +     * Your connection with this peer has timed out. +     */ +    TOX_GROUP_EXIT_TYPE_TIMEOUT, + +    /** +     * Your connection with this peer has been severed. +     */ +    TOX_GROUP_EXIT_TYPE_DISCONNECTED, + +    /** +     * Your connection with all peers has been severed. This will occur when you are kicked from +     * a group, rejoin a group, or manually disconnect from a group. +     */ +    TOX_GROUP_EXIT_TYPE_SELF_DISCONNECTED, + +    /** +     * The peer has been kicked. +     */ +    TOX_GROUP_EXIT_TYPE_KICK, + +    /** +     * The peer provided invalid group sync information. +     */ +    TOX_GROUP_EXIT_TYPE_SYNC_ERROR, + +} Tox_Group_Exit_Type; + + +/** + * @param group_number The group number of the group in which a peer has left. + * @param peer_id The ID of the peer who left the group. This ID no longer designates a valid peer + *   and cannot be used for API calls. + * @param exit_type The type of exit event. One of Tox_Group_Exit_Type. + * @param name The nickname of the peer who left the group. + * @param name_length The length of the peer name. + * @param part_message The parting message data. + * @param part_message_length The length of the parting message. + */ +typedef void tox_group_peer_exit_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Exit_Type exit_type, +                                    const uint8_t *name, size_t name_length, const uint8_t *part_message, size_t part_message_length, void *user_data); + + +/** + * Set the callback for the `group_peer_exit` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self exits the group. + */ +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback); + +/** + * @param group_number The group number of the group that the client has joined. + */ +typedef void tox_group_self_join_cb(Tox *tox, uint32_t group_number, void *user_data); + + +/** + * Set the callback for the `group_self_join` event. Pass NULL to unset. + * + * This event is triggered when the client has successfully joined a group. Use this to initialize + * any group information the client may need. + */ +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback); + +/** + * Represents types of failed group join attempts. These are used in the tox_callback_group_rejected + * callback when a peer fails to join a group. + */ +typedef enum Tox_Group_Join_Fail { + +    /** +     * The group peer limit has been reached. +     */ +    TOX_GROUP_JOIN_FAIL_PEER_LIMIT, + +    /** +     * You have supplied an invalid password. +     */ +    TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD, + +    /** +     * The join attempt failed due to an unspecified error. This often occurs when the group is +     * not found in the DHT. +     */ +    TOX_GROUP_JOIN_FAIL_UNKNOWN, + +} Tox_Group_Join_Fail; + + +/** + * @param group_number The group number of the group for which the join has failed. + * @param fail_type The type of group rejection. + */ +typedef void tox_group_join_fail_cb(Tox *tox, uint32_t group_number, Tox_Group_Join_Fail fail_type, void *user_data); + + +/** + * Set the callback for the `group_join_fail` event. Pass NULL to unset. + * + * This event is triggered when the client fails to join a group. + */ +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback); + + +/******************************************************************************* + * + * :: Group chat founder controls (these only work for the group founder) + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Founder_Set_Password { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND, + +    /** +     * The caller does not have the required permissions to set the password. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS, + +    /** +     * Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND, + +    /** +     * The function failed to allocate enough memory for the operation. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Password; + + +/** + * Set or unset the group password. + * + * This function sets the groups password, creates a new group shared state including the change, + * and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the password. + * @param password The password we want to set. Set password to NULL to unset the password. + * @param length The length of the password. length must be no longer than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return true on success. + */ +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, +                                    Tox_Err_Group_Founder_Set_Password *error); + +typedef enum Tox_Err_Group_Founder_Set_Topic_Lock { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND, + +    /** +     * Tox_Group_Topic_Lock is an invalid type. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID, + +    /** +     * The caller does not have the required permissions to set the topic lock. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS, + +    /** +     * The topic lock could not be set. This may occur due to an error related to +     * cryptographic signing of the new shared state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Topic_Lock; + + +/** + * Set the group topic lock state. + * + * This function sets the group's topic lock state to enabled or disabled, creates a new shared + * state including the change, and distributes it to the rest of the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * @param group_number The group number of the group for which we wish to change the topic lock state. + * @param topic_lock The state we wish to set the topic lock to. + * + * @return true on success. + */ +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, +                                      Tox_Err_Group_Founder_Set_Topic_Lock *error); + +typedef enum Tox_Err_Group_Founder_Set_Voice_State { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND, + +    /** +     * The caller does not have the required permissions to set the privacy state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS, + +    /** +     * The voice state could not be set. This may occur due to an error related to +     * cryptographic signing of the new shared state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Voice_State; + +/** + * Set the group voice state. + * + * This function sets the group's voice state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the voice state. + * @param voice_state The voice state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, +                                       Tox_Err_Group_Founder_Set_Voice_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Privacy_State { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND, + +    /** +     * The caller does not have the required permissions to set the privacy state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS, + +    /** +     * The privacy state could not be set. This may occur due to an error related to +     * cryptographic signing of the new shared state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Privacy_State; + +/** + * Set the group privacy state. + * + * This function sets the group's privacy state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the privacy state. + * @param privacy_state The privacy state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, +        Tox_Err_Group_Founder_Set_Privacy_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Peer_Limit { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND, + +    /** +     * The caller does not have the required permissions to set the peer limit. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS, + +    /** +     * The peer limit could not be set. This may occur due to an error related to +     * cryptographic signing of the new shared state. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND, + +    /** +     * The group is disconnected. +     */ +    TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Peer_Limit; + + +/** + * Set the group peer limit. + * + * This function sets a limit for the number of peers who may be in the group, creates a new + * group shared state including the change, and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the peer limit. + * @param max_peers The maximum number of peers to allow in the group. + * + * @return true on success. + */ +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, +                                      Tox_Err_Group_Founder_Set_Peer_Limit *error); + + +/******************************************************************************* + * + * :: Group chat moderation + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Set_Ignore { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_SET_IGNORE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND, + +    /** +     * The ID passed did not designate a valid peer. +     */ +    TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND, + +    /** +     * The caller attempted to ignore himself. +     */ +    TOX_ERR_GROUP_SET_IGNORE_SELF, + +} Tox_Err_Group_Set_Ignore; + + +/** + * Ignore or unignore a peer. + * + * @param group_number The group number of the group in which you wish to ignore a peer. + * @param peer_id The ID of the peer who shall be ignored or unignored. + * @param ignore True to ignore the peer, false to unignore the peer. + * + * @return true on success. + */ +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, +                          Tox_Err_Group_Set_Ignore *error); + +typedef enum Tox_Err_Group_Mod_Set_Role { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND, + +    /** +     * The ID passed did not designate a valid peer. Note: you cannot set your own role. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND, + +    /** +     * The caller does not have the required permissions for this action. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS, + +    /** +     * The role assignment is invalid. This will occur if you try to set a peer's role to +     * the role they already have. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT, + +    /** +     * The role was not successfully set. This may occur if the packet failed to send, or +     * if the role limit has been reached. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION, + +    /** +     * The caller attempted to set their own role. +     */ +    TOX_ERR_GROUP_MOD_SET_ROLE_SELF, + +} Tox_Err_Group_Mod_Set_Role; + + +/** + * Set a peer's role. + * + * This function will first remove the peer's previous role and then assign them a new role. + * It will also send a packet to the rest of the group, requesting that they perform + * the role reassignment. Note: peers cannot be set to the founder role. + * + * @param group_number The group number of the group the in which you wish set the peer's role. + * @param peer_id The ID of the peer whose role you wish to set. + * @param role The role you wish to set the peer to. + * + * @return true on success. + */ +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, +                            Tox_Err_Group_Mod_Set_Role *error); + +typedef enum Tox_Err_Group_Mod_Kick_Peer { + +    /** +     * The function returned successfully. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_OK, + +    /** +     * The group number passed did not designate a valid group. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND, + +    /** +     * The ID passed did not designate a valid peer. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND, + +    /** +     * The caller does not have the required permissions for this action. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS, + +    /** +     * The peer could not be kicked from the group. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION, + +    /** +     * The packet failed to send. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND, + +    /** +     * The caller attempted to set their own role. +     */ +    TOX_ERR_GROUP_MOD_KICK_PEER_SELF, + +} Tox_Err_Group_Mod_Kick_Peer; + + +/** + * Kick a peer. + * + * This function will remove a peer from the caller's peer list and send a packet to all + * group members requesting them to do the same. Note: This function will not trigger + * the `group_peer_exit` event for the caller. + * + * @param group_number The group number of the group the action is intended for. + * @param peer_id The ID of the peer who will be kicked. + * + * @return true on success. + */ +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, +                             Tox_Err_Group_Mod_Kick_Peer *error); + +/** + * Represents moderation events. These should be used with the `group_moderation` event. + */ +typedef enum Tox_Group_Mod_Event { + +    /** +     * A peer has been kicked from the group. +     */ +    TOX_GROUP_MOD_EVENT_KICK, + +    /** +     * A peer as been given the observer role. +     */ +    TOX_GROUP_MOD_EVENT_OBSERVER, + +    /** +     * A peer has been given the user role. +     */ +    TOX_GROUP_MOD_EVENT_USER, + +    /** +     * A peer has been given the moderator role. +     */ +    TOX_GROUP_MOD_EVENT_MODERATOR, + +} Tox_Group_Mod_Event; + + +/** + * @param group_number The group number of the group the event is intended for. + * @param source_peer_id The ID of the peer who initiated the event. + * @param target_peer_id The ID of the peer who is the target of the event. + * @param mod_type The type of event. + */ +typedef void tox_group_moderation_cb(Tox *tox, uint32_t group_number, uint32_t source_peer_id, uint32_t target_peer_id, +                                     Tox_Group_Mod_Event mod_type, void *user_data); + + +/** + * Set the callback for the `group_moderation` event. Pass NULL to unset. + * + * This event is triggered when a moderator or founder executes a moderation event, with + * the exception of the peer who initiates the event. It is also triggered when the + * observer and moderator lists are silently modified (this may occur during group syncing). + * + * If either peer id does not designate a valid peer in the group chat, the client should + * manually update all peer roles. + */ +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback); + +/** @} */ +  /** @} */  #ifdef __cplusplus diff --git a/protocols/Tox/libtox/src/toxcore/tox_api.c b/protocols/Tox/libtox/src/toxcore/tox_api.c index 00051e497b..61c248c30d 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_api.c +++ b/protocols/Tox/libtox/src/toxcore/tox_api.c @@ -88,6 +88,42 @@ uint32_t tox_max_hostname_length(void)  {      return TOX_MAX_HOSTNAME_LENGTH;  } +uint32_t tox_group_max_topic_length(void) +{ +    return TOX_GROUP_MAX_TOPIC_LENGTH; +} +uint32_t tox_group_max_part_length(void) +{ +    return TOX_GROUP_MAX_PART_LENGTH; +} +uint32_t tox_group_max_message_length(void) +{ +    return TOX_GROUP_MAX_MESSAGE_LENGTH; +} +uint32_t tox_group_max_custom_lossy_packet_length(void) +{ +    return TOX_GROUP_MAX_CUSTOM_LOSSY_PACKET_LENGTH; +} +uint32_t tox_group_max_custom_lossless_packet_length(void) +{ +    return TOX_GROUP_MAX_CUSTOM_LOSSLESS_PACKET_LENGTH; +} +uint32_t tox_group_max_group_name_length(void) +{ +    return TOX_GROUP_MAX_GROUP_NAME_LENGTH; +} +uint32_t tox_group_max_password_size(void) +{ +    return TOX_GROUP_MAX_PASSWORD_SIZE; +} +uint32_t tox_group_chat_id_size(void) +{ +    return TOX_GROUP_CHAT_ID_SIZE; +} +uint32_t tox_group_peer_public_key_size(void) +{ +    return TOX_GROUP_PEER_PUBLIC_KEY_SIZE; +}  uint32_t tox_dht_node_ip_string_size(void)  {      return TOX_DHT_NODE_IP_STRING_SIZE; diff --git a/protocols/Tox/libtox/src/toxcore/tox_dispatch.c b/protocols/Tox/libtox/src/toxcore/tox_dispatch.c index 4b4546e4a4..5427851470 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_dispatch.c +++ b/protocols/Tox/libtox/src/toxcore/tox_dispatch.c @@ -47,9 +47,11 @@ Tox_Dispatch *tox_dispatch_new(Tox_Err_Dispatch_New *error)      *dispatch = (Tox_Dispatch) {          nullptr      }; +      if (error != nullptr) {          *error = TOX_ERR_DISPATCH_NEW_OK;      } +      return dispatch;  } diff --git a/protocols/Tox/libtox/src/toxcore/tox_struct.h b/protocols/Tox/libtox/src/toxcore/tox_struct.h index 22d1c54a27..8b95d83bbc 100644 --- a/protocols/Tox/libtox/src/toxcore/tox_struct.h +++ b/protocols/Tox/libtox/src/toxcore/tox_struct.h @@ -44,6 +44,24 @@ struct Tox {      tox_dht_get_nodes_response_cb *dht_get_nodes_response_callback;      tox_friend_lossy_packet_cb *friend_lossy_packet_callback_per_pktid[UINT8_MAX + 1];      tox_friend_lossless_packet_cb *friend_lossless_packet_callback_per_pktid[UINT8_MAX + 1]; +    tox_group_peer_name_cb *group_peer_name_callback; +    tox_group_peer_status_cb *group_peer_status_callback; +    tox_group_topic_cb *group_topic_callback; +    tox_group_privacy_state_cb *group_privacy_state_callback; +    tox_group_topic_lock_cb *group_topic_lock_callback; +    tox_group_voice_state_cb *group_voice_state_callback; +    tox_group_peer_limit_cb *group_peer_limit_callback; +    tox_group_password_cb *group_password_callback; +    tox_group_message_cb *group_message_callback; +    tox_group_private_message_cb *group_private_message_callback; +    tox_group_custom_packet_cb *group_custom_packet_callback; +    tox_group_custom_private_packet_cb *group_custom_private_packet_callback; +    tox_group_invite_cb *group_invite_callback; +    tox_group_peer_join_cb *group_peer_join_callback; +    tox_group_peer_exit_cb *group_peer_exit_callback; +    tox_group_self_join_cb *group_self_join_callback; +    tox_group_join_fail_cb *group_join_fail_callback; +    tox_group_moderation_cb *group_moderation_callback;      void *toxav_object; // workaround to store a ToxAV object (setter and getter functions are available)  }; diff --git a/protocols/Tox/libtox/src/toxcore/util.c b/protocols/Tox/libtox/src/toxcore/util.c index c70c3a76f5..402977a680 100644 --- a/protocols/Tox/libtox/src/toxcore/util.c +++ b/protocols/Tox/libtox/src/toxcore/util.c @@ -139,12 +139,12 @@ uint32_t jenkins_one_at_a_time_hash(const uint8_t *key, size_t len)      for (uint32_t i = 0; i < len; ++i) {          hash += key[i]; -        hash += hash << 10; +        hash += (uint32_t)((uint64_t)hash << 10);          hash ^= hash >> 6;      } -    hash += hash << 3; +    hash += (uint32_t)((uint64_t)hash << 3);      hash ^= hash >> 11; -    hash += hash << 15; +    hash += (uint32_t)((uint64_t)hash << 15);      return hash;  }  | 
