Merge moderation portion of new groupchats codebase

This commit is contained in:
jfreegman 2022-03-30 20:09:50 -04:00
parent 09575dc05b
commit 015305a088
No known key found for this signature in database
GPG Key ID: 3627F3144076AE63
18 changed files with 1788 additions and 37 deletions

View File

@ -260,19 +260,25 @@ set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/friend_requests.c
toxcore/friend_requests.h)
# LAYER 6: Tox messenger
# LAYER 6: DHT based group chats
# ----------------------------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/group_moderation.c
toxcore/group_moderation.h)
# LAYER 7: Tox messenger
# ----------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/Messenger.c
toxcore/Messenger.h)
# LAYER 7: Group chats
# LAYER 8: Conferences
# --------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/group.c
toxcore/group.h)
# LAYER 8: Public API
# LAYER 9: Public API
# -------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/tox_api.c
@ -281,7 +287,7 @@ set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/tox_private.h)
set(toxcore_API_HEADERS ${toxcore_API_HEADERS} ${toxcore_SOURCE_DIR}/toxcore/tox.h^tox)
# LAYER 9: New async events API
# LAYER 10: New async events API
# -------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
third_party/cmp/cmp.c
@ -319,7 +325,7 @@ set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/tox_unpack.h)
set(toxcore_API_HEADERS ${toxcore_API_HEADERS} ${toxcore_SOURCE_DIR}/toxcore/tox_events.h^tox)
# LAYER 10: Dispatch recorded events to callbacks.
# LAYER 11: Dispatch recorded events to callbacks.
# -------------------
set(toxcore_SOURCES ${toxcore_SOURCES}
toxcore/tox_dispatch.c
@ -438,6 +444,7 @@ unit_test(toxav rtp)
unit_test(toxcore DHT)
unit_test(toxcore bin_pack)
unit_test(toxcore crypto_core)
unit_test(toxcore group_moderation)
unit_test(toxcore mono_time)
unit_test(toxcore ping_array)
unit_test(toxcore tox)

View File

@ -11,6 +11,8 @@ CPPCHECK+=("--inconclusive")
CPPCHECK+=("--error-exitcode=1")
# Used for VLA.
CPPCHECK+=("--suppress=allocaCalled")
# False positives on sanctions_copy in group_moderation.c
CPPCHECK+=("--suppress=doubleFree")
# False positives in switch statements.
CPPCHECK+=("--suppress=knownConditionTrueFalse")
# Cppcheck does not need standard library headers to get proper results.

View File

@ -1 +1 @@
8216f2b48b15db02c373766c9e8f898a0cf8ebb26310635b146a118d0bcf1f71 /usr/local/bin/tox-bootstrapd
073710c9592554bbcb8bd094c9d64a987d52a2d1dedf965557b56fe848ddc14b /usr/local/bin/tox-bootstrapd

View File

@ -45,6 +45,7 @@ RUN source .github/scripts/flags-coverage.sh \
-DSTRICT_ABI=ON \
-DAUTOTEST=ON \
-DPROXY_TEST=ON \
-DUSE_IPV6=OFF \
&& cmake --build _build --parallel 8 --target install
WORKDIR /work/_build

View File

@ -15,6 +15,7 @@ sh_test(
args = ["$(locations %s)" % f for f in CIMPLE_FILES] + [
"-Wno-boolean-return",
"-Wno-callback-names",
"-Wno-callgraph",
"-Wno-enum-names",
"-Wno-type-check",
"+RTS",

View File

@ -76,6 +76,7 @@ cc_test(
flaky = True,
deps = [
":crypto_core",
":util",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
@ -163,6 +164,7 @@ cc_library(
],
deps = [
":attributes",
":ccompat",
":crypto_core",
"@pthread",
],
@ -487,6 +489,46 @@ cc_library(
],
)
cc_library(
name = "group_moderation",
srcs = ["group_moderation.c"],
hdrs = ["group_moderation.h"],
deps = [
":DHT",
":ccompat",
":crypto_core",
":logger",
":mono_time",
":network",
":util",
"@libsodium",
],
)
cc_test(
name = "group_moderation_test",
size = "small",
srcs = ["group_moderation_test.cc"],
deps = [
":crypto_core",
":group_moderation",
":logger",
":util",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_fuzz_test(
name = "group_moderation_fuzz_test",
srcs = ["group_moderation_fuzz_test.cc"],
corpus = ["//tools/toktok-fuzzer/corpus:group_moderation_fuzz_test"],
deps = [
":group_moderation",
"//c-toxcore/testing/fuzzing:fuzz_support",
],
)
cc_library(
name = "Messenger",
srcs = ["Messenger.c"],

View File

@ -1800,7 +1800,7 @@ static uint8_t do_ping_and_sendnode_requests(DHT *dht, uint64_t *lastgetnode, co
}
if (num_nodes > 0 && (mono_time_is_timeout(dht->mono_time, *lastgetnode, GET_NODE_INTERVAL)
|| *bootstrap_times < MAX_BOOTSTRAP_TIMES)) {
|| *bootstrap_times < MAX_BOOTSTRAP_TIMES)) {
uint32_t rand_node = random_range_u32(num_nodes);
if ((num_nodes - 1) != rand_node) {
@ -1855,8 +1855,8 @@ static void do_Close(DHT *dht)
dht->num_to_bootstrap = 0;
const uint8_t not_killed = do_ping_and_sendnode_requests(
dht, &dht->close_lastgetnodes, dht->self_public_key, dht->close_clientlist, LCLIENT_LIST, &dht->close_bootstrap_times,
false);
dht, &dht->close_lastgetnodes, dht->self_public_key, dht->close_clientlist, LCLIENT_LIST, &dht->close_bootstrap_times,
false);
if (not_killed != 0) {
return;
@ -2568,7 +2568,8 @@ static int handle_LANdiscovery(void *object, const IP_Port *source, const uint8_
/*----------------------------------------------------------------------------------*/
DHT *new_dht(const Logger *log, Mono_Time *mono_time, Networking_Core *net, bool hole_punching_enabled, bool lan_discovery_enabled)
DHT *new_dht(const Logger *log, Mono_Time *mono_time, Networking_Core *net, bool hole_punching_enabled,
bool lan_discovery_enabled)
{
if (net == nullptr) {
return nullptr;
@ -2761,7 +2762,8 @@ void dht_save(const DHT *dht, uint8_t *data)
}
}
state_write_section_header(old_data, DHT_STATE_COOKIE_TYPE, pack_nodes(dht->log, data, sizeof(Node_format) * num, clients, num), DHT_STATE_TYPE_NODES);
state_write_section_header(old_data, DHT_STATE_COOKIE_TYPE, pack_nodes(dht->log, data, sizeof(Node_format) * num,
clients, num), DHT_STATE_TYPE_NODES);
free(clients);
}

View File

@ -22,6 +22,8 @@
extern "C" {
#endif
/* Maximum size of a signature (may be smaller) */
#define SIGNATURE_SIZE CRYPTO_SIGNATURE_SIZE
/** Maximum number of clients stored per friend. */
#define MAX_FRIEND_CLIENTS 8
@ -64,6 +66,7 @@ extern "C" {
#define CRYPTO_PACKET_DHTPK 156
#define CRYPTO_PACKET_NAT_PING 254 // NAT ping crypto packet ID.
/* Max size of a packed node for IPV4 and IPV6 respectively */
#define PACKED_NODE_SIZE_IP4 (1 + SIZE_IP4 + sizeof(uint16_t) + CRYPTO_PUBLIC_KEY_SIZE)
#define PACKED_NODE_SIZE_IP6 (1 + SIZE_IP6 + sizeof(uint16_t) + CRYPTO_PUBLIC_KEY_SIZE)
@ -467,7 +470,8 @@ int dht_load(DHT *dht, const uint8_t *data, uint32_t length);
/** Initialize DHT. */
non_null()
DHT *new_dht(const Logger *log, Mono_Time *mono_time, Networking_Core *net, bool hole_punching_enabled, bool lan_discovery_enabled);
DHT *new_dht(const Logger *log, Mono_Time *mono_time, Networking_Core *net, bool hole_punching_enabled,
bool lan_discovery_enabled);
non_null()
void kill_dht(DHT *dht);

View File

@ -92,23 +92,6 @@ int getfriendcon_id(const Messenger *m, int32_t friendnumber)
return m->friendlist[friendnumber].friendcon_id;
}
/**
* @return a uint16_t that represents the checksum of address of length len.
*/
non_null()
static uint16_t address_checksum(const uint8_t *address, uint32_t len)
{
uint8_t checksum[2] = {0};
uint16_t check;
for (uint32_t i = 0; i < len; ++i) {
checksum[i % 2] ^= address[i];
}
memcpy(&check, checksum, sizeof(check));
return check;
}
/**
* Format: `[real_pk (32 bytes)][nospam number (4 bytes)][checksum (2 bytes)]`
*
@ -119,7 +102,7 @@ void getaddress(const Messenger *m, uint8_t *address)
pk_copy(address, nc_get_self_public_key(m->net_crypto));
uint32_t nospam = get_nospam(m->fr);
memcpy(address + CRYPTO_PUBLIC_KEY_SIZE, &nospam, sizeof(nospam));
uint16_t checksum = address_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
uint16_t checksum = data_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
memcpy(address + CRYPTO_PUBLIC_KEY_SIZE + sizeof(nospam), &checksum, sizeof(checksum));
}
@ -236,7 +219,7 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u
}
uint16_t check;
const uint16_t checksum = address_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
const uint16_t checksum = data_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
memcpy(&check, address + CRYPTO_PUBLIC_KEY_SIZE + sizeof(uint32_t), sizeof(check));
if (check != checksum) {
@ -2887,7 +2870,7 @@ static State_Load_Status friends_list_load(Messenger *m, const uint8_t *data, ui
uint8_t address[FRIEND_ADDRESS_SIZE];
pk_copy(address, temp.real_pk);
memcpy(address + CRYPTO_PUBLIC_KEY_SIZE, &temp.friendrequest_nospam, sizeof(uint32_t));
uint16_t checksum = address_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
uint16_t checksum = data_checksum(address, FRIEND_ADDRESS_SIZE - sizeof(checksum));
memcpy(address + CRYPTO_PUBLIC_KEY_SIZE + sizeof(uint32_t), &checksum, sizeof(checksum));
m_addfriend(m, address, temp.info, net_ntohs(temp.info_size));
}

View File

@ -33,6 +33,12 @@
#define crypto_box_MACBYTES (crypto_box_ZEROBYTES - crypto_box_BOXZEROBYTES)
#endif
#ifndef VANILLA_NACL
// Need dht because of ENC_SECRET_KEY_SIZE and ENC_PUBLIC_KEY_SIZE
#define ENC_PUBLIC_KEY_SIZE CRYPTO_PUBLIC_KEY_SIZE
#define ENC_SECRET_KEY_SIZE CRYPTO_SECRET_KEY_SIZE
#endif
//!TOKSTYLE-
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
#include "../testing/fuzzing/fuzz_adapter.h"
@ -58,6 +64,33 @@ static_assert(CRYPTO_SHA512_SIZE == crypto_hash_sha512_BYTES,
static_assert(CRYPTO_PUBLIC_KEY_SIZE == 32,
"CRYPTO_PUBLIC_KEY_SIZE is required to be 32 bytes for public_key_eq to work");
#ifndef VANILLA_NACL
static_assert(CRYPTO_SIGNATURE_SIZE == crypto_sign_BYTES,
"CRYPTO_SIGNATURE_SIZE should be equal to crypto_sign_BYTES");
static_assert(CRYPTO_SIGN_PUBLIC_KEY_SIZE == crypto_sign_PUBLICKEYBYTES,
"CRYPTO_SIGN_PUBLIC_KEY_SIZE should be equal to crypto_sign_PUBLICKEYBYTES");
static_assert(CRYPTO_SIGN_SECRET_KEY_SIZE == crypto_sign_SECRETKEYBYTES,
"CRYPTO_SIGN_SECRET_KEY_SIZE should be equal to crypto_sign_SECRETKEYBYTES");
#endif /* VANILLA_NACL */
bool create_extended_keypair(uint8_t *pk, uint8_t *sk)
{
#ifdef VANILLA_NACL
return false;
#else
/* create signature key pair */
crypto_sign_keypair(pk + ENC_PUBLIC_KEY_SIZE, sk + ENC_SECRET_KEY_SIZE);
/* convert public signature key to public encryption key */
const int res1 = crypto_sign_ed25519_pk_to_curve25519(pk, pk + ENC_PUBLIC_KEY_SIZE);
/* convert secret signature key to secret encryption key */
const int res2 = crypto_sign_ed25519_sk_to_curve25519(sk, sk + ENC_SECRET_KEY_SIZE);
return res1 == 0 && res2 == 0;
#endif
}
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
static uint8_t *crypto_malloc(size_t bytes)
{
@ -177,6 +210,26 @@ uint32_t random_range_u32(uint32_t upper_bound)
#endif // VANILLA_NACL
}
bool crypto_signature_create(uint8_t *signature, const uint8_t *message, uint64_t message_length,
const uint8_t *secret_key)
{
#ifdef VANILLA_NACL
return false;
#else
return crypto_sign_detached(signature, nullptr, message, message_length, secret_key) == 0;
#endif // VANILLA_NACL
}
bool crypto_signature_verify(const uint8_t *signature, const uint8_t *message, uint64_t message_length,
const uint8_t *public_key)
{
#ifdef VANILLA_NACL
return false;
#else
return crypto_sign_verify_detached(signature, message, message_length, public_key) == 0;
#endif
}
bool public_key_valid(const uint8_t *public_key)
{
if (public_key[31] >= 128) { /* Last bit of key is always zero. */

View File

@ -19,6 +19,21 @@
extern "C" {
#endif
/**
* The number of bytes in a signature.
*/
#define CRYPTO_SIGNATURE_SIZE 64
/**
* The number of bytes in a Tox public key used for signatures.
*/
#define CRYPTO_SIGN_PUBLIC_KEY_SIZE 32
/**
* The number of bytes in a Tox secret key used for signatures.
*/
#define CRYPTO_SIGN_SECRET_KEY_SIZE 64
/**
* @brief The number of bytes in a Tox public key used for encryption.
*/
@ -60,6 +75,41 @@ extern "C" {
*/
#define CRYPTO_SHA512_SIZE 64
/**
* @brief The number of bytes in an encryption public key used by DHT group chats.
*/
#define ENC_PUBLIC_KEY_SIZE CRYPTO_PUBLIC_KEY_SIZE
/**
* @brief The number of bytes in an encryption secret key used by DHT group chats.
*/
#define ENC_SECRET_KEY_SIZE CRYPTO_SECRET_KEY_SIZE
/**
* @brief The number of bytes in a signature public key.
*/
#define SIG_PUBLIC_KEY_SIZE CRYPTO_SIGN_PUBLIC_KEY_SIZE
/**
* @brief The number of bytes in a signature secret key.
*/
#define SIG_SECRET_KEY_SIZE CRYPTO_SIGN_SECRET_KEY_SIZE
/**
* @brief The number of bytes in a DHT group chat public key identifier.
*/
#define CHAT_ID_SIZE SIG_PUBLIC_KEY_SIZE
/**
* @brief The number of bytes in an extended public key used by DHT group chats.
*/
#define EXT_PUBLIC_KEY_SIZE (ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE)
/**
* @brief The number of bytes in an extended secret key used by DHT group chats.
*/
#define EXT_SECRET_KEY_SIZE (ENC_SECRET_KEY_SIZE + SIG_SECRET_KEY_SIZE)
/**
* @brief A `bzero`-like function which won't be optimised away by the compiler.
*
@ -129,6 +179,36 @@ uint64_t random_u64(void);
*/
uint32_t random_range_u32(uint32_t upper_bound);
/** @brief Cryptographically signs a message using the supplied secret key and puts the resulting signature
* in the supplied buffer.
*
* @param signature The buffer for the resulting signature, which must have room for at
* least CRYPTO_SIGNATURE_SIZE bytes.
* @param message The message being signed.
* @param message_length The length in bytes of the message being signed.
* @param secret_key The secret key used to create the signature. The key should be
* produced by either `create_extended_keypair` or the libsodium function `crypto_sign_keypair`.
*
* @retval true on success.
*/
non_null()
bool crypto_signature_create(uint8_t *signature, const uint8_t *message, uint64_t message_length,
const uint8_t *secret_key);
/** @brief Verifies that the given signature was produced by a given message and public key.
*
* @param signature The signature we wish to verify.
* @param message The message we wish to verify.
* @param message_length The length of the message.
* @param public_key The public key counterpart of the secret key that was used to
* create the signature.
*
* @retval true on success.
*/
non_null()
bool crypto_signature_verify(const uint8_t *signature, const uint8_t *message, uint64_t message_length,
const uint8_t *public_key);
/**
* @brief Fill the given nonce with random bytes.
*/
@ -151,6 +231,17 @@ void random_bytes(uint8_t *bytes, size_t length);
non_null()
bool public_key_valid(const uint8_t *public_key);
/** @brief Creates an extended keypair: curve25519 and ed25519 for encryption and signing
* respectively. The Encryption keys are derived from the signature keys.
*
* @param pk The buffer where the public key will be stored. Must have room for EXT_PUBLIC_KEY_SIZE bytes.
* @param sk The buffer where the secret key will be stored. Must have room for EXT_SECRET_KEY_SIZE bytes.
*
* @retval true on success.
*/
non_null()
bool create_extended_keypair(uint8_t *pk, uint8_t *sk);
/**
* @brief Generate a new random keypair.
*

View File

@ -6,11 +6,17 @@
#include <array>
#include <vector>
#include "util.h"
namespace {
using ExtPublicKey = std::array<uint8_t, EXT_PUBLIC_KEY_SIZE>;
using ExtSecretKey = std::array<uint8_t, EXT_SECRET_KEY_SIZE>;
using Signature = std::array<uint8_t, CRYPTO_SIGNATURE_SIZE>;
using Nonce = std::array<uint8_t, CRYPTO_NONCE_SIZE>;
TEST(CryptoCore, IncrementNonce)
{
using Nonce = std::array<uint8_t, CRYPTO_NONCE_SIZE>;
Nonce nonce{};
increment_nonce(nonce.data());
EXPECT_EQ(
@ -26,7 +32,6 @@ TEST(CryptoCore, IncrementNonce)
TEST(CryptoCore, IncrementNonceNumber)
{
using Nonce = std::array<uint8_t, CRYPTO_NONCE_SIZE>;
Nonce nonce{};
increment_nonce_number(nonce.data(), 0x1F5);
@ -43,4 +48,24 @@ TEST(CryptoCore, IncrementNonceNumber)
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34, 0x5A, 0x62}}));
}
TEST(CryptoCore, Signatures)
{
ExtPublicKey pk;
ExtSecretKey sk;
EXPECT_TRUE(create_extended_keypair(pk.data(), sk.data()));
std::vector<uint8_t> message;
// Try a few different sizes, including empty 0 length message.
for (uint8_t i = 0; i < 100; ++i) {
message.push_back(random_u08());
Signature signature;
EXPECT_TRUE(crypto_signature_create(
signature.data(), message.data(), message.size(), get_sig_sk(sk.data())));
EXPECT_TRUE(crypto_signature_verify(
signature.data(), message.data(), message.size(), get_sig_pk(pk.data())));
}
}
} // namespace

864
toxcore/group_moderation.c Normal file
View File

@ -0,0 +1,864 @@
/* 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_moderation.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ccompat.h"
#include "crypto_core.h"
#include "mono_time.h"
#include "network.h"
#include "util.h"
static_assert(MOD_SANCTIONS_CREDS_SIZE <= MAX_PACKET_SIZE_NO_HEADERS,
"MOD_SANCTIONS_CREDS_SIZE must be <= the maximum allowed payload size");
static_assert(MOD_MAX_NUM_SANCTIONS * MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_CREDS_SIZE <= MAX_PACKET_SIZE_NO_HEADERS,
"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");
uint16_t mod_list_packed_size(const Moderation *moderation)
{
return moderation->num_mods * MOD_LIST_ENTRY_SIZE;
}
int mod_list_unpack(Moderation *moderation, const uint8_t *data, uint16_t length, uint16_t num_mods)
{
if (length < num_mods * MOD_LIST_ENTRY_SIZE) {
return -1;
}
mod_list_cleanup(moderation);
if (num_mods == 0) {
return 0;
}
uint8_t **tmp_list = (uint8_t **)calloc(num_mods, sizeof(uint8_t *));
if (tmp_list == nullptr) {
return -1;
}
uint16_t unpacked_len = 0;
for (uint16_t i = 0; i < num_mods; ++i) {
tmp_list[i] = (uint8_t *)malloc(sizeof(uint8_t) * MOD_LIST_ENTRY_SIZE);
if (tmp_list[i] == nullptr) {
free_uint8_t_pointer_array(tmp_list, i);
return -1;
}
memcpy(tmp_list[i], &data[i * MOD_LIST_ENTRY_SIZE], MOD_LIST_ENTRY_SIZE);
unpacked_len += MOD_LIST_ENTRY_SIZE;
}
moderation->mod_list = tmp_list;
moderation->num_mods = num_mods;
return unpacked_len;
}
void mod_list_pack(const Moderation *moderation, uint8_t *data)
{
for (uint16_t i = 0; i < moderation->num_mods; ++i) {
memcpy(&data[i * MOD_LIST_ENTRY_SIZE], moderation->mod_list[i], MOD_LIST_ENTRY_SIZE);
}
}
void mod_list_get_data_hash(uint8_t *hash, const uint8_t *packed_mod_list, uint16_t length)
{
crypto_sha256(hash, packed_mod_list, length);
}
bool mod_list_make_hash(const Moderation *moderation, uint8_t *hash)
{
if (moderation->num_mods == 0) {
memset(hash, 0, MOD_MODERATION_HASH_SIZE);
return true;
}
const size_t data_buf_size = mod_list_packed_size(moderation);
assert(data_buf_size > 0);
uint8_t *data = (uint8_t *)malloc(data_buf_size);
if (data == nullptr) {
return false;
}
mod_list_pack(moderation, data);
mod_list_get_data_hash(hash, data, data_buf_size);
free(data);
return true;
}
/**
* Returns moderator list index for public_sig_key.
* Returns -1 if key is not in the list.
*/
non_null()
static int mod_list_index_of_sig_pk(const Moderation *moderation, const uint8_t *public_sig_key)
{
for (uint16_t i = 0; i < moderation->num_mods; ++i) {
if (memcmp(moderation->mod_list[i], public_sig_key, SIG_PUBLIC_KEY_SIZE) == 0) {
return i;
}
}
return -1;
}
bool mod_list_verify_sig_pk(const Moderation *moderation, const uint8_t *sig_pk)
{
if (memcmp(moderation->founder_public_sig_key, sig_pk, SIG_PUBLIC_KEY_SIZE) == 0) {
return true;
}
for (uint16_t i = 0; i < moderation->num_mods; ++i) {
if (memcmp(moderation->mod_list[i], sig_pk, SIG_PUBLIC_KEY_SIZE) == 0) {
return true;
}
}
return false;
}
bool mod_list_remove_index(Moderation *moderation, uint16_t index)
{
if (index >= moderation->num_mods) {
return false;
}
if ((moderation->num_mods - 1) == 0) {
mod_list_cleanup(moderation);
return true;
}
--moderation->num_mods;
if (index != moderation->num_mods) {
memcpy(moderation->mod_list[index], moderation->mod_list[moderation->num_mods],
MOD_LIST_ENTRY_SIZE);
}
free(moderation->mod_list[moderation->num_mods]);
moderation->mod_list[moderation->num_mods] = nullptr;
uint8_t **tmp_list = (uint8_t **)realloc(moderation->mod_list, moderation->num_mods * sizeof(uint8_t *));
if (tmp_list == nullptr) {
return false;
}
moderation->mod_list = tmp_list;
return true;
}
bool mod_list_remove_entry(Moderation *moderation, const uint8_t *public_sig_key)
{
if (moderation->num_mods == 0) {
return false;
}
const int idx = mod_list_index_of_sig_pk(moderation, public_sig_key);
if (idx == -1) {
return false;
}
assert(idx <= UINT16_MAX);
return mod_list_remove_index(moderation, (uint16_t)idx);
}
bool mod_list_add_entry(Moderation *moderation, const uint8_t *mod_data)
{
if (moderation->num_mods >= MOD_MAX_NUM_MODERATORS) {
return false;
}
uint8_t **tmp_list = (uint8_t **)realloc(moderation->mod_list, (moderation->num_mods + 1) * sizeof(uint8_t *));
if (tmp_list == nullptr) {
return false;
}
moderation->mod_list = tmp_list;
tmp_list[moderation->num_mods] = (uint8_t *)malloc(sizeof(uint8_t) * MOD_LIST_ENTRY_SIZE);
if (tmp_list[moderation->num_mods] == nullptr) {
return false;
}
memcpy(tmp_list[moderation->num_mods], mod_data, MOD_LIST_ENTRY_SIZE);
++moderation->num_mods;
return true;
}
void mod_list_cleanup(Moderation *moderation)
{
free_uint8_t_pointer_array(moderation->mod_list, moderation->num_mods);
moderation->num_mods = 0;
moderation->mod_list = nullptr;
}
uint16_t sanctions_creds_pack(const Mod_Sanction_Creds *creds, uint8_t *data)
{
uint16_t packed_len = 0;
net_pack_u32(data + packed_len, creds->version);
packed_len += sizeof(uint32_t);
memcpy(data + packed_len, creds->hash, MOD_SANCTION_HASH_SIZE);
packed_len += MOD_SANCTION_HASH_SIZE;
net_pack_u16(data + packed_len, creds->checksum);
packed_len += sizeof(uint16_t);
memcpy(data + packed_len, creds->sig_pk, SIG_PUBLIC_KEY_SIZE);
packed_len += SIG_PUBLIC_KEY_SIZE;
memcpy(data + packed_len, creds->sig, SIGNATURE_SIZE);
packed_len += SIGNATURE_SIZE;
return packed_len;
}
uint16_t sanctions_list_packed_size(uint16_t num_sanctions)
{
return MOD_SANCTION_PACKED_SIZE * num_sanctions;
}
int sanctions_list_pack(uint8_t *data, uint16_t length, const Mod_Sanction *sanctions, uint16_t num_sanctions,
const Mod_Sanction_Creds *creds)
{
assert(sanctions != nullptr || num_sanctions == 0);
assert(sanctions != nullptr || creds != nullptr);
uint16_t packed_len = 0;
for (uint16_t i = 0; i < num_sanctions; ++i) {
if (packed_len + sizeof(uint8_t) + SIG_PUBLIC_KEY_SIZE + TIME_STAMP_SIZE > length) {
return -1;
}
memcpy(data + packed_len, &sanctions[i].type, sizeof(uint8_t));
packed_len += sizeof(uint8_t);
memcpy(data + packed_len, sanctions[i].setter_public_sig_key, SIG_PUBLIC_KEY_SIZE);
packed_len += SIG_PUBLIC_KEY_SIZE;
net_pack_u64(data + packed_len, sanctions[i].time_set);
packed_len += TIME_STAMP_SIZE;
const uint8_t sanctions_type = sanctions[i].type;
if (sanctions_type == SA_OBSERVER) {
if (packed_len + ENC_PUBLIC_KEY_SIZE > length) {
return -1;
}
memcpy(data + packed_len, sanctions[i].target_public_enc_key, ENC_PUBLIC_KEY_SIZE);
packed_len += ENC_PUBLIC_KEY_SIZE;
} else {
return -1;
}
if (packed_len + SIGNATURE_SIZE > length) {
return -1;
}
/* Signature must be packed last */
memcpy(data + packed_len, sanctions[i].signature, SIGNATURE_SIZE);
packed_len += SIGNATURE_SIZE;
}
if (creds == nullptr) {
return packed_len;
}
if (length < packed_len || length - packed_len < MOD_SANCTIONS_CREDS_SIZE) {
return -1;
}
const uint16_t cred_len = sanctions_creds_pack(creds, data + packed_len);
if (cred_len != MOD_SANCTIONS_CREDS_SIZE) {
return -1;
}
return (int)(packed_len + cred_len);
}
uint16_t sanctions_creds_unpack(Mod_Sanction_Creds *creds, const uint8_t *data)
{
uint16_t len_processed = 0;
net_unpack_u32(data + len_processed, &creds->version);
len_processed += sizeof(uint32_t);
memcpy(creds->hash, data + len_processed, MOD_SANCTION_HASH_SIZE);
len_processed += MOD_SANCTION_HASH_SIZE;
net_unpack_u16(data + len_processed, &creds->checksum);
len_processed += sizeof(uint16_t);
memcpy(creds->sig_pk, data + len_processed, SIG_PUBLIC_KEY_SIZE);
len_processed += SIG_PUBLIC_KEY_SIZE;
memcpy(creds->sig, data + len_processed, SIGNATURE_SIZE);
len_processed += SIGNATURE_SIZE;
return len_processed;
}
int sanctions_list_unpack(Mod_Sanction *sanctions, Mod_Sanction_Creds *creds, uint16_t max_sanctions,
const uint8_t *data, uint16_t length, uint16_t *processed_data_len)
{
uint16_t num = 0;
uint16_t len_processed = 0;
while (num < max_sanctions && num < MOD_MAX_NUM_SANCTIONS && len_processed < length) {
if (len_processed + sizeof(uint8_t) + SIG_PUBLIC_KEY_SIZE + TIME_STAMP_SIZE > length) {
return -1;
}
memcpy(&sanctions[num].type, data + len_processed, sizeof(uint8_t));
len_processed += sizeof(uint8_t);
memcpy(sanctions[num].setter_public_sig_key, data + len_processed, SIG_PUBLIC_KEY_SIZE);
len_processed += SIG_PUBLIC_KEY_SIZE;
net_unpack_u64(data + len_processed, &sanctions[num].time_set);
len_processed += TIME_STAMP_SIZE;
if (sanctions[num].type == SA_OBSERVER) {
if (len_processed + ENC_PUBLIC_KEY_SIZE > length) {
return -1;
}
memcpy(sanctions[num].target_public_enc_key, data + len_processed, ENC_PUBLIC_KEY_SIZE);
len_processed += ENC_PUBLIC_KEY_SIZE;
} else {
return -1;
}
if (len_processed + SIGNATURE_SIZE > length) {
return -1;
}
memcpy(sanctions[num].signature, data + len_processed, SIGNATURE_SIZE);
len_processed += SIGNATURE_SIZE;
++num;
}
if (length <= len_processed || length - len_processed < MOD_SANCTIONS_CREDS_SIZE) {
if (length != len_processed) {
return -1;
}
if (processed_data_len != nullptr) {
*processed_data_len = len_processed;
}
return num;
}
const uint16_t creds_len = sanctions_creds_unpack(creds, data + len_processed);
if (creds_len != MOD_SANCTIONS_CREDS_SIZE) {
return -1;
}
if (processed_data_len != nullptr) {
*processed_data_len = len_processed + creds_len;
}
return num;
}
/** @brief Creates a new sanction list hash and puts it in hash.
*
* The hash is derived from the signature of all entries plus the version number.
* hash must have room for at least MOD_SANCTION_HASH_SIZE bytes.
*
* If num_sanctions is 0 the hash is zeroed.
*
* Return true on success.
*/
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)
{
if (num_sanctions == 0 || sanctions == nullptr) {
memset(hash, 0, MOD_SANCTION_HASH_SIZE);
return true;
}
const size_t sig_data_size = num_sanctions * SIGNATURE_SIZE;
const size_t data_buf_size = sig_data_size + sizeof(uint32_t);
// check for integer overflower
if (data_buf_size < num_sanctions) {
return false;
}
uint8_t *data = (uint8_t *)malloc(data_buf_size);
if (data == nullptr) {
return false;
}
for (uint16_t i = 0; i < num_sanctions; ++i) {
memcpy(&data[i * SIGNATURE_SIZE], sanctions[i].signature, SIGNATURE_SIZE);
}
memcpy(&data[sig_data_size], &new_version, sizeof(uint32_t));
crypto_sha256(hash, data, data_buf_size);
free(data);
return true;
}
/** @brief Verifies that sanction contains valid info and was assigned by a current mod or group founder.
*
* Returns true on success.
*/
non_null()
static bool sanctions_list_validate_entry(const Moderation *moderation, const Mod_Sanction *sanction)
{
if (!mod_list_verify_sig_pk(moderation, sanction->setter_public_sig_key)) {
return false;
}
if (sanction->type >= SA_INVALID) {
return false;
}
if (sanction->time_set == 0) {
return false;
}
uint8_t packed_data[MOD_SANCTION_PACKED_SIZE];
const int packed_len = sanctions_list_pack(packed_data, sizeof(packed_data), sanction, 1, nullptr);
if (packed_len <= (int) SIGNATURE_SIZE) {
return false;
}
return crypto_signature_verify(sanction->signature, packed_data, packed_len - SIGNATURE_SIZE,
sanction->setter_public_sig_key);
}
non_null()
static uint16_t sanctions_creds_get_checksum(const Mod_Sanction_Creds *creds)
{
return data_checksum(creds->hash, sizeof(creds->hash));
}
non_null()
static void sanctions_creds_set_checksum(Mod_Sanction_Creds *creds)
{
creds->checksum = sanctions_creds_get_checksum(creds);
}
bool sanctions_list_make_creds(Moderation *moderation)
{
const Mod_Sanction_Creds old_creds = moderation->sanctions_creds;
++moderation->sanctions_creds.version;
memcpy(moderation->sanctions_creds.sig_pk, moderation->self_public_sig_key, SIG_PUBLIC_KEY_SIZE);
uint8_t hash[MOD_SANCTION_HASH_SIZE];
if (!sanctions_list_make_hash(moderation->sanctions, moderation->sanctions_creds.version,
moderation->num_sanctions, hash)) {
moderation->sanctions_creds = old_creds;
return false;
}
memcpy(moderation->sanctions_creds.hash, hash, MOD_SANCTION_HASH_SIZE);
sanctions_creds_set_checksum(&moderation->sanctions_creds);
if (!crypto_signature_create(moderation->sanctions_creds.sig, moderation->sanctions_creds.hash,
MOD_SANCTION_HASH_SIZE, moderation->self_secret_sig_key)) {
moderation->sanctions_creds = old_creds;
return false;
}
return true;
}
/** @brief Validates sanction list credentials.
*
* Verifies that:
* - the public signature key belongs to a mod or the founder
* - the signature for the hash was made by the owner of the public signature key.
* - the received hash matches our own hash of the new sanctions list
* - the received checksum matches the received hash
* - the new version is >= our current version
*
* Returns true on success.
*/
non_null(1, 3) nullable(2)
static bool sanctions_creds_validate(const Moderation *moderation, const Mod_Sanction *sanctions,
const Mod_Sanction_Creds *creds, uint16_t num_sanctions)
{
if (!mod_list_verify_sig_pk(moderation, creds->sig_pk)) {
LOGGER_WARNING(moderation->log, "Invalid credentials signature pk");
return false;
}
uint8_t hash[MOD_SANCTION_HASH_SIZE];
if (!sanctions_list_make_hash(sanctions, creds->version, num_sanctions, hash)) {
return false;
}
if (memcmp(hash, creds->hash, MOD_SANCTION_HASH_SIZE) != 0) {
LOGGER_WARNING(moderation->log, "Invalid credentials hash");
return false;
}
if (creds->checksum != sanctions_creds_get_checksum(creds)) {
LOGGER_WARNING(moderation->log, "Invalid credentials checksum");
return false;
}
if (moderation->shared_state_version > 0) {
if ((creds->version < moderation->sanctions_creds.version)
&& !(creds->version == 0 && moderation->sanctions_creds.version == UINT32_MAX)) {
LOGGER_WARNING(moderation->log, "Invalid version");
return false;
}
}
if (!crypto_signature_verify(creds->sig, hash, MOD_SANCTION_HASH_SIZE, creds->sig_pk)) {
LOGGER_WARNING(moderation->log, "Invalid signature");
return false;
}
return true;
}
bool sanctions_list_check_integrity(const Moderation *moderation, const Mod_Sanction_Creds *creds,
const Mod_Sanction *sanctions, uint16_t num_sanctions)
{
for (uint16_t i = 0; i < num_sanctions; ++i) {
if (!sanctions_list_validate_entry(moderation, &sanctions[i])) {
LOGGER_WARNING(moderation->log, "Invalid entry");
return false;
}
}
return sanctions_creds_validate(moderation, sanctions, creds, num_sanctions);
}
/** @brief Validates a sanctions list if credentials are supplied. If successful,
* or if no credentials are supplid, assigns new sanctions list and credentials
* to moderation object.
*
* @param moderation The moderation object being operated on.
* @param new_sanctions The sanctions list to validate and assign to moderation object.
* @param new_creds The new sanctions credentials to be assigned to moderation object.
* @param num_sanctions The number of sanctions in the sanctions list.
*
* @retval false if sanctions credentials validation fails.
*/
non_null(1, 2) nullable(3)
static bool sanctions_apply_new(Moderation *moderation, Mod_Sanction *new_sanctions,
const Mod_Sanction_Creds *new_creds,
uint16_t num_sanctions)
{
if (new_creds != nullptr) {
if (!sanctions_creds_validate(moderation, new_sanctions, new_creds, num_sanctions)) {
LOGGER_WARNING(moderation->log, "Failed to validate credentials");
return false;
}
moderation->sanctions_creds = *new_creds;
}
sanctions_list_cleanup(moderation);
moderation->sanctions = new_sanctions;
moderation->num_sanctions = num_sanctions;
return true;
}
/** @brief Returns a copy of the sanctions list. The caller is responsible for freeing the
* memory returned by this function.
*/
non_null()
static Mod_Sanction *sanctions_list_copy(const Mod_Sanction *sanctions, uint16_t num_sanctions)
{
Mod_Sanction *copy = (Mod_Sanction *)calloc(num_sanctions, sizeof(Mod_Sanction));
if (copy == nullptr) {
return nullptr;
}
memcpy(copy, sanctions, num_sanctions * sizeof(Mod_Sanction));
return copy;
}
/** @brief Removes index-th sanction list entry.
*
* New credentials will be validated if creds is non-null.
*
* Returns true on success.
*/
non_null(1) nullable(3)
static bool sanctions_list_remove_index(Moderation *moderation, uint16_t index, const Mod_Sanction_Creds *creds)
{
if (index >= moderation->num_sanctions) {
return false;
}
const uint16_t new_num = moderation->num_sanctions - 1;
if (new_num == 0) {
if (creds != nullptr) {
if (!sanctions_creds_validate(moderation, nullptr, creds, 0)) {
return false;
}
moderation->sanctions_creds = *creds;
}
sanctions_list_cleanup(moderation);
return true;
}
/* Operate on a copy of the list in case something goes wrong. */
Mod_Sanction *sanctions_copy = sanctions_list_copy(moderation->sanctions, moderation->num_sanctions);
if (sanctions_copy == nullptr) {
return false;
}
if (index != new_num) {
sanctions_copy[index] = sanctions_copy[new_num];
}
Mod_Sanction *new_list = (Mod_Sanction *)realloc(sanctions_copy, new_num * sizeof(Mod_Sanction));
if (new_list == nullptr) {
free(sanctions_copy);
return false;
}
if (!sanctions_apply_new(moderation, new_list, creds, new_num)) {
free(new_list);
return false;
}
return true;
}
bool sanctions_list_remove_observer(Moderation *moderation, const uint8_t *public_key,
const Mod_Sanction_Creds *creds)
{
for (uint16_t i = 0; i < moderation->num_sanctions; ++i) {
const Mod_Sanction *curr_sanction = &moderation->sanctions[i];
if (curr_sanction->type != SA_OBSERVER) {
continue;
}
if (memcmp(public_key, curr_sanction->target_public_enc_key, ENC_PUBLIC_KEY_SIZE) == 0) {
if (!sanctions_list_remove_index(moderation, i, creds)) {
return false;
}
if (creds == nullptr) {
return sanctions_list_make_creds(moderation);
}
return true;
}
}
return false;
}
bool sanctions_list_is_observer(const Moderation *moderation, const uint8_t *public_key)
{
for (uint16_t i = 0; i < moderation->num_sanctions; ++i) {
const Mod_Sanction *curr_sanction = &moderation->sanctions[i];
if (curr_sanction->type != SA_OBSERVER) {
continue;
}
if (memcmp(curr_sanction->target_public_enc_key, public_key, ENC_PUBLIC_KEY_SIZE) == 0) {
return true;
}
}
return false;
}
bool sanctions_list_entry_exists(const Moderation *moderation, const Mod_Sanction *sanction)
{
if (sanction->type == SA_OBSERVER) {
return sanctions_list_is_observer(moderation, sanction->target_public_enc_key);
}
return false;
}
bool sanctions_list_add_entry(Moderation *moderation, const Mod_Sanction *sanction, const Mod_Sanction_Creds *creds)
{
if (moderation->num_sanctions >= MOD_MAX_NUM_SANCTIONS) {
LOGGER_WARNING(moderation->log, "num_sanctions %d exceeds maximum", moderation->num_sanctions);
return false;
}
if (!sanctions_list_validate_entry(moderation, sanction)) {
LOGGER_ERROR(moderation->log, "Failed to validate sanction");
return false;
}
if (sanctions_list_entry_exists(moderation, sanction)) {
LOGGER_WARNING(moderation->log, "Attempted to add duplicate sanction");
return false;
}
/* Operate on a copy of the list in case something goes wrong. */
Mod_Sanction *sanctions_copy = nullptr;
if (moderation->num_sanctions > 0) {
sanctions_copy = sanctions_list_copy(moderation->sanctions, moderation->num_sanctions);
if (sanctions_copy == nullptr) {
return false;
}
}
const uint16_t index = moderation->num_sanctions;
Mod_Sanction *new_list = (Mod_Sanction *)realloc(sanctions_copy, (index + 1) * sizeof(Mod_Sanction));
if (new_list == nullptr) {
free(sanctions_copy);
return false;
}
new_list[index] = *sanction;
if (!sanctions_apply_new(moderation, new_list, creds, index + 1)) {
free(new_list);
return false;
}
return true;
}
/** @brief Signs packed sanction data.
*
* This function must be called by the owner of the entry's public_sig_key.
*
* Returns true on success.
*/
non_null()
static bool sanctions_list_sign_entry(const Moderation *moderation, Mod_Sanction *sanction)
{
uint8_t packed_data[MOD_SANCTION_PACKED_SIZE];
const int packed_len = sanctions_list_pack(packed_data, sizeof(packed_data), sanction, 1, nullptr);
if (packed_len <= (int) SIGNATURE_SIZE) {
LOGGER_ERROR(moderation->log, "Failed to pack sanctions list: %d", packed_len);
return false;
}
return crypto_signature_create(sanction->signature, packed_data, packed_len - SIGNATURE_SIZE,
moderation->self_secret_sig_key);
}
bool sanctions_list_make_entry(Moderation *moderation, const uint8_t *public_key, Mod_Sanction *sanction,
uint8_t type)
{
*sanction = (Mod_Sanction) {
0
};
if (type == SA_OBSERVER) {
memcpy(sanction->target_public_enc_key, public_key, ENC_PUBLIC_KEY_SIZE);
} else {
LOGGER_ERROR(moderation->log, "Tried to create sanction with invalid type: %u", type);
return false;
}
memcpy(sanction->setter_public_sig_key, moderation->self_public_sig_key, SIG_PUBLIC_KEY_SIZE);
sanction->time_set = (uint64_t)time(nullptr);
sanction->type = type;
if (!sanctions_list_sign_entry(moderation, sanction)) {
LOGGER_ERROR(moderation->log, "Failed to sign sanction");
return false;
}
if (!sanctions_list_add_entry(moderation, sanction, nullptr)) {
return false;
}
if (!sanctions_list_make_creds(moderation)) {
LOGGER_ERROR(moderation->log, "Failed to make credentials for new sanction");
return false;
}
return true;
}
uint16_t sanctions_list_replace_sig(Moderation *moderation, const uint8_t *public_sig_key)
{
uint16_t count = 0;
for (uint16_t i = 0; i < moderation->num_sanctions; ++i) {
if (memcmp(moderation->sanctions[i].setter_public_sig_key, public_sig_key, SIG_PUBLIC_KEY_SIZE) != 0) {
continue;
}
memcpy(moderation->sanctions[i].setter_public_sig_key, moderation->self_public_sig_key, SIG_PUBLIC_KEY_SIZE);
if (!sanctions_list_sign_entry(moderation, &moderation->sanctions[i])) {
LOGGER_ERROR(moderation->log, "Failed to sign sanction");
continue;
}
++count;
}
if (count > 0) {
if (!sanctions_list_make_creds(moderation)) {
return 0;
}
}
return count;
}
void sanctions_list_cleanup(Moderation *moderation)
{
if (moderation->sanctions != nullptr) {
free(moderation->sanctions);
}
moderation->sanctions = nullptr;
moderation->num_sanctions = 0;
}

288
toxcore/group_moderation.h Normal file
View File

@ -0,0 +1,288 @@
/* 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 C_TOXCORE_TOXCORE_GROUP_MODERATION_H
#define C_TOXCORE_TOXCORE_GROUP_MODERATION_H
#include <stdbool.h>
#include <stdint.h>
#include "DHT.h"
#include "logger.h"
#ifdef __cplusplus
extern "C" {
#endif
#define MOD_MODERATION_HASH_SIZE CRYPTO_SHA256_SIZE
#define MOD_LIST_ENTRY_SIZE SIG_PUBLIC_KEY_SIZE
#define MOD_SANCTION_HASH_SIZE CRYPTO_SHA256_SIZE
#define TIME_STAMP_SIZE sizeof(uint64_t)
/* The packed size of a Mod_Sanction_Creds */
#define MOD_SANCTIONS_CREDS_SIZE (sizeof(uint32_t) + MOD_SANCTION_HASH_SIZE + sizeof(uint16_t) +\
SIG_PUBLIC_KEY_SIZE + SIGNATURE_SIZE)
/* The packed size of a Mod_Sanction */
#define MOD_SANCTION_PACKED_SIZE (SIG_PUBLIC_KEY_SIZE + TIME_STAMP_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + SIGNATURE_SIZE)
/* 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)))
typedef enum Mod_Sanction_Type {
SA_OBSERVER = 0x00,
SA_INVALID = 0x01,
} Mod_Sanction_Type;
typedef struct Mod_Sanction_Creds {
uint32_t version;
uint8_t hash[MOD_SANCTION_HASH_SIZE]; // hash of all sanctions list signatures + version
uint16_t checksum; // a sum of the hash
uint8_t sig_pk[SIG_PUBLIC_KEY_SIZE]; // Last mod to have modified the sanctions list
uint8_t sig[SIGNATURE_SIZE]; // signature of hash, signed by sig_pk
} Mod_Sanction_Creds;
/** Holds data pertaining to a peer who has been sanctioned. */
typedef struct Mod_Sanction {
uint8_t setter_public_sig_key[SIG_PUBLIC_KEY_SIZE];
// TODO(Jfreegman): This timestamp can potentially be used to track a user across
// different group chats if they're a moderator and set many sanctions across the
// different groups. This should be addressed in the future.
uint64_t time_set;
uint8_t type;
uint8_t target_public_enc_key[ENC_PUBLIC_KEY_SIZE];
/* Signature of all above packed data signed by the owner of public_sig_key */
uint8_t signature[SIGNATURE_SIZE];
} Mod_Sanction;
typedef struct Moderation {
const Logger *log;
Mod_Sanction *sanctions;
uint16_t num_sanctions;
Mod_Sanction_Creds sanctions_creds;
uint8_t **mod_list; // array of public signature keys of all the mods
uint16_t num_mods;
// copies from parent/sibling chat/shared state objects
uint8_t founder_public_sig_key[SIG_PUBLIC_KEY_SIZE];
uint8_t self_public_sig_key[SIG_PUBLIC_KEY_SIZE];
uint8_t self_secret_sig_key[SIG_SECRET_KEY_SIZE];
uint32_t shared_state_version;
} Moderation;
/** @brief Returns the size in bytes of the packed moderation list. */
non_null()
uint16_t mod_list_packed_size(const Moderation *moderation);
/** @brief Unpacks data into the moderator list.
*
* @param data should contain num_mods entries of size MOD_LIST_ENTRY_SIZE.
*
* Returns length of unpacked data on success.
* Returns -1 on failure.
*/
non_null()
int mod_list_unpack(Moderation *moderation, const uint8_t *data, uint16_t length, uint16_t num_mods);
/** @brief Packs moderator list into data.
* @param data must have room for the number of bytes returned by `mod_list_packed_size`.
*/
non_null()
void mod_list_pack(const Moderation *moderation, uint8_t *data);
/** @brief Creates a new moderator list hash and puts it in `hash`.
*
* @param hash must have room for at least MOD_MODERATION_HASH_SIZE bytes.
*
* If num_mods is 0 the hash is zeroed.
*
* Returns true on sucess.
*/
non_null()
bool mod_list_make_hash(const Moderation *moderation, uint8_t *hash);
/** @brief Puts a sha256 hash of `packed_mod_list` of `length` bytes in `hash`.
*
* @param hash must have room for at least MOD_MODERATION_HASH_SIZE bytes.
*/
non_null()
void mod_list_get_data_hash(uint8_t *hash, const uint8_t *packed_mod_list, uint16_t length);
/** @brief Removes moderator at index-th position in the moderator list.
*
* Returns true on success.
*/
non_null()
bool mod_list_remove_index(Moderation *moderation, uint16_t index);
/** @brief Removes public_sig_key from the moderator list.
*
* Returns true on success.
*/
non_null()
bool mod_list_remove_entry(Moderation *moderation, const uint8_t *public_sig_key);
/** @brief Adds a mod to the moderator list.
*
* @param mod_data must be MOD_LIST_ENTRY_SIZE bytes.
*
* Returns true on success.
*/
non_null()
bool mod_list_add_entry(Moderation *moderation, const uint8_t *mod_data);
/** @return true if the public signature key belongs to a moderator or the founder */
non_null()
bool mod_list_verify_sig_pk(const Moderation *moderation, const uint8_t *sig_pk);
/** @brief Frees all memory associated with the moderator list and sets num_mods to 0. */
nullable(1)
void mod_list_cleanup(Moderation *moderation);
/** @brief Returns the size in bytes of num_sanctions packed sanctions. */
uint16_t sanctions_list_packed_size(uint16_t num_sanctions);
/** @brief Packs sanctions into data. Additionally packs the sanctions credentials into creds.
*
* @param data The byte array being packed. Must have room for the number of bytes returned
* by `sanctions_list_packed_size`.
* @param length The size of the byte array.
* @param sanctions The sanctions list.
* @param num_sanctions The number of sanctions in the sanctions list. This value must be the same
* value used when calling `sanctions_list_packed_size`.
* @param creds The credentials object to fill.
*
* @retval The length of packed data on success.
* @retval -1 on failure.
*/
non_null(1) nullable(3, 5)
int sanctions_list_pack(uint8_t *data, uint16_t length, const Mod_Sanction *sanctions, uint16_t num_sanctions,
const Mod_Sanction_Creds *creds);
/** @brief Unpacks sanctions and new sanctions credentials.
*
* @param sanctions The sanctions array the sanctions data is unpacked into.
* @param creds The creds object the creds data is unpacked into.
* @param max_sanctions The maximum number of sanctions that the sanctions array can hold.
* @param data The packed data array.
* @param length The size of the packed data.
* @param processed_data_len If non-null, will contain the number of processed bytes on success.
*
* @retval The number of unpacked entries on success.
* @retval -1 on failure.
*/
non_null(1, 2, 4) nullable(6)
int sanctions_list_unpack(Mod_Sanction *sanctions, Mod_Sanction_Creds *creds, uint16_t max_sanctions,
const uint8_t *data, uint16_t length, uint16_t *processed_data_len);
/** @brief Packs sanction list credentials into data.
*
* @param data must have room for MOD_SANCTIONS_CREDS_SIZE bytes.
*
* Returns length of packed data.
*/
non_null()
uint16_t sanctions_creds_pack(const Mod_Sanction_Creds *creds, uint8_t *data);
/** @brief Unpacks sanctions credentials into creds from data.
*
* @param data must have room for MOD_SANCTIONS_CREDS_SIZE bytes.
*
* Returns the length of the data processed.
*/
non_null()
uint16_t sanctions_creds_unpack(Mod_Sanction_Creds *creds, const uint8_t *data);
/** @brief Updates sanction list credentials.
*
* Increment version, replace sig_pk with your own, update hash to reflect new
* sanction list, and sign new hash signature.
*
* Returns true on success.
*/
non_null()
bool sanctions_list_make_creds(Moderation *moderation);
/** @brief Validates all sanctions list entries as well as the list itself.
*
* Returns true if all entries are valid.
* Returns false if one or more entries are invalid.
*/
non_null()
bool sanctions_list_check_integrity(const Moderation *moderation, const Mod_Sanction_Creds *creds,
const Mod_Sanction *sanctions, uint16_t num_sanctions);
/** @brief Adds an entry to the sanctions list.
*
* The entry is first validated and the resulting new sanction list is
* compared against the new credentials.
*
* Entries must be unique.
*
* Returns true on success.
*/
non_null(1, 2) nullable(3)
bool sanctions_list_add_entry(Moderation *moderation, const Mod_Sanction *sanction, const Mod_Sanction_Creds *creds);
/** @brief Creates a new sanction entry for `public_key` where type is one of Mod_Sanction_Type.
*
* New entry is signed and placed in the sanctions list.
*
* Returns true on success.
*/
non_null()
bool sanctions_list_make_entry(Moderation *moderation, const uint8_t *public_key, Mod_Sanction *sanction,
uint8_t type);
/** @return true if public key is in the observer list. */
non_null()
bool sanctions_list_is_observer(const Moderation *moderation, const uint8_t *public_key);
/** @return true if sanction already exists in the sanctions list. */
non_null()
bool sanctions_list_entry_exists(const Moderation *moderation, const Mod_Sanction *sanction);
/** @brief Removes observer entry for public key from sanction list.
*
* If creds is NULL we make new credentials (this should only be done by a moderator or founder)
*
* Returns false on failure or if entry was not found.
*/
non_null(1, 2) nullable(3)
bool sanctions_list_remove_observer(Moderation *moderation, const uint8_t *public_key,
const Mod_Sanction_Creds *creds);
/** @brief Replaces all sanctions list signatures made by public_sig_key with the caller's.
*
* This is called whenever the founder demotes a moderator.
*
* Returns the number of entries re-signed.
*/
non_null()
uint16_t sanctions_list_replace_sig(Moderation *moderation, const uint8_t *public_sig_key);
non_null()
void sanctions_list_cleanup(Moderation *moderation);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // C_TOXCORE_TOXCORE_GROUP_MODERATION_H

View File

@ -0,0 +1,38 @@
#include "group_moderation.h"
#include "../testing/fuzzing/fuzz_support.h"
namespace {
void TestModListUnpack(Fuzz_Data input)
{
CONSUME1_OR_RETURN(const uint16_t num_mods, input);
Moderation mods{};
mod_list_unpack(&mods, input.data, input.size, num_mods);
mod_list_cleanup(&mods);
}
void TestSanctionsListUnpack(Fuzz_Data input)
{
Mod_Sanction sanctions[10];
Mod_Sanction_Creds creds;
uint16_t processed_data_len;
sanctions_list_unpack(sanctions, &creds, 10, input.data, input.size, &processed_data_len);
}
void TestSanctionCredsUnpack(Fuzz_Data input)
{
CONSUME_OR_RETURN(const uint8_t *data, input, MOD_SANCTIONS_CREDS_SIZE);
Mod_Sanction_Creds creds;
sanctions_creds_unpack(&creds, data);
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
fuzz_select_target(
data, size, TestModListUnpack, TestSanctionsListUnpack, TestSanctionCredsUnpack);
return 0;
}

View File

@ -0,0 +1,248 @@
#include "group_moderation.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <vector>
#include "crypto_core.h"
#include "logger.h"
#include "util.h"
namespace {
using ExtPublicKey = std::array<uint8_t, EXT_PUBLIC_KEY_SIZE>;
using ExtSecretKey = std::array<uint8_t, EXT_SECRET_KEY_SIZE>;
using ModerationHash = std::array<uint8_t, MOD_MODERATION_HASH_SIZE>;
TEST(ModList, PackedSizeOfEmptyModListIsZero)
{
Moderation mods{};
EXPECT_EQ(mod_list_packed_size(&mods), 0);
uint8_t byte = 1;
mod_list_pack(&mods, &byte);
EXPECT_EQ(byte, 1);
}
TEST(ModList, UnpackingZeroSizeArrayIsNoop)
{
Moderation mods{};
const uint8_t byte = 1;
EXPECT_EQ(mod_list_unpack(&mods, &byte, 0, 0), 0);
}
TEST(ModList, AddRemoveMultipleMods)
{
Moderation mods{};
uint8_t sig_pk1[32] = {1};
uint8_t sig_pk2[32] = {2};
EXPECT_TRUE(mod_list_add_entry(&mods, sig_pk1));
EXPECT_TRUE(mod_list_add_entry(&mods, sig_pk2));
EXPECT_TRUE(mod_list_remove_entry(&mods, sig_pk1));
EXPECT_TRUE(mod_list_remove_entry(&mods, sig_pk2));
}
TEST(ModList, PackingAndUnpackingList)
{
using ModListEntry = std::array<uint8_t, MOD_LIST_ENTRY_SIZE>;
Moderation mods{};
EXPECT_TRUE(mod_list_add_entry(&mods, ModListEntry{}.data()));
std::vector<uint8_t> packed(mod_list_packed_size(&mods));
mod_list_pack(&mods, packed.data());
EXPECT_TRUE(mod_list_remove_entry(&mods, ModListEntry{}.data()));
Moderation mods2{};
EXPECT_EQ(mod_list_unpack(&mods2, packed.data(), packed.size(), 1), packed.size());
EXPECT_TRUE(mod_list_remove_entry(&mods2, ModListEntry{}.data()));
}
TEST(ModList, UnpackingTooManyModsFails)
{
using ModListEntry = std::array<uint8_t, MOD_LIST_ENTRY_SIZE>;
Moderation mods{};
EXPECT_TRUE(mod_list_add_entry(&mods, ModListEntry{}.data()));
std::vector<uint8_t> packed(mod_list_packed_size(&mods));
mod_list_pack(&mods, packed.data());
Moderation mods2{};
EXPECT_EQ(mod_list_unpack(&mods2, packed.data(), packed.size(), 2), -1);
EXPECT_TRUE(mod_list_remove_entry(&mods, ModListEntry{}.data()));
}
TEST(ModList, UnpackingFromEmptyBufferFails)
{
std::vector<uint8_t> packed(1);
Moderation mods{};
EXPECT_EQ(mod_list_unpack(&mods, packed.end().base(), 0, 1), -1);
}
TEST(ModList, HashOfEmptyModListZeroesOutBuffer)
{
Moderation mods{};
// Fill with random data, check that it's zeroed.
ModerationHash hash;
random_bytes(hash.data(), hash.size());
EXPECT_TRUE(mod_list_make_hash(&mods, hash.data()));
EXPECT_EQ(hash, ModerationHash{});
}
TEST(ModList, RemoveIndexFromEmptyModListFails)
{
Moderation mods{};
EXPECT_FALSE(mod_list_remove_index(&mods, 0));
EXPECT_FALSE(mod_list_remove_index(&mods, UINT16_MAX));
}
TEST(ModList, RemoveEntryFromEmptyModListFails)
{
Moderation mods{};
uint8_t sig_pk[32] = {0};
EXPECT_FALSE(mod_list_remove_entry(&mods, sig_pk));
}
TEST(ModList, ModListRemoveIndex)
{
Moderation mods{};
uint8_t sig_pk[32] = {1};
EXPECT_TRUE(mod_list_add_entry(&mods, sig_pk));
EXPECT_TRUE(mod_list_remove_index(&mods, 0));
}
TEST(ModList, CleanupOnEmptyModsIsNoop)
{
Moderation mods{};
mod_list_cleanup(&mods);
}
TEST(ModList, EmptyModListCannotVerifyAnySigPk)
{
Moderation mods{};
uint8_t sig_pk[32] = {1};
EXPECT_FALSE(mod_list_verify_sig_pk(&mods, sig_pk));
}
TEST(ModList, ModListAddVerifyRemoveSigPK)
{
Moderation mods{};
uint8_t sig_pk[32] = {1};
EXPECT_TRUE(mod_list_add_entry(&mods, sig_pk));
EXPECT_TRUE(mod_list_verify_sig_pk(&mods, sig_pk));
EXPECT_TRUE(mod_list_remove_entry(&mods, sig_pk));
EXPECT_FALSE(mod_list_verify_sig_pk(&mods, sig_pk));
}
TEST(ModList, ModListHashCheck)
{
Moderation mods1{};
uint8_t sig_pk1[32] = {1};
std::array<uint8_t, MOD_MODERATION_HASH_SIZE> hash1;
EXPECT_TRUE(mod_list_add_entry(&mods1, sig_pk1));
EXPECT_TRUE(mod_list_make_hash(&mods1, hash1.data()));
EXPECT_TRUE(mod_list_remove_entry(&mods1, sig_pk1));
}
TEST(SanctionsList, PackingIntoUndersizedBufferFails)
{
Mod_Sanction sanctions[1] = {};
std::array<uint8_t, 1> packed;
EXPECT_EQ(sanctions_list_pack(packed.data(), packed.size(), sanctions, 1, nullptr), -1);
uint16_t length = sanctions_list_packed_size(1) - 1;
std::vector<uint8_t> packed2(length);
EXPECT_EQ(sanctions_list_pack(packed2.data(), packed2.size(), sanctions, 1, nullptr), -1);
}
TEST(SanctionsList, PackUnpackSanctionsCreds)
{
Moderation mod{};
std::array<uint8_t, MOD_SANCTIONS_CREDS_SIZE> packed;
EXPECT_EQ(sanctions_creds_pack(&mod.sanctions_creds, packed.data()), MOD_SANCTIONS_CREDS_SIZE);
EXPECT_EQ(
sanctions_creds_unpack(&mod.sanctions_creds, packed.data()), MOD_SANCTIONS_CREDS_SIZE);
}
struct SanctionsListMod : ::testing::Test {
protected:
ExtPublicKey pk;
ExtSecretKey sk;
Logger *log = logger_new();
Moderation mod{};
Mod_Sanction sanctions[2] = {};
const uint8_t sanctioned_pk1[32] = {1};
const uint8_t sanctioned_pk2[32] = {2};
void SetUp() override
{
ASSERT_TRUE(create_extended_keypair(pk.data(), sk.data()));
mod.log = log;
memcpy(mod.self_public_sig_key, get_sig_pk(pk.data()), SIG_PUBLIC_KEY_SIZE);
memcpy(mod.self_secret_sig_key, get_sig_sk(sk.data()), SIG_SECRET_KEY_SIZE);
ASSERT_TRUE(mod_list_add_entry(&mod, get_sig_pk(pk.data())));
EXPECT_FALSE(sanctions_list_check_integrity(&mod, &mod.sanctions_creds, &sanctions[0], 0));
EXPECT_FALSE(sanctions_list_check_integrity(&mod, &mod.sanctions_creds, &sanctions[0], 1));
EXPECT_FALSE(
sanctions_list_check_integrity(&mod, &mod.sanctions_creds, &sanctions[0], UINT16_MAX));
EXPECT_TRUE(sanctions_list_make_entry(&mod, sanctioned_pk1, &sanctions[0], SA_OBSERVER));
EXPECT_TRUE(sanctions_list_check_integrity(
&mod, &mod.sanctions_creds, sanctions, mod.num_sanctions));
EXPECT_TRUE(sanctions_list_make_entry(&mod, sanctioned_pk2, &sanctions[1], SA_OBSERVER));
EXPECT_TRUE(sanctions_list_check_integrity(
&mod, &mod.sanctions_creds, sanctions, mod.num_sanctions));
}
~SanctionsListMod() override
{
EXPECT_TRUE(sanctions_list_remove_observer(&mod, sanctioned_pk1, nullptr));
EXPECT_TRUE(sanctions_list_remove_observer(&mod, sanctioned_pk2, nullptr));
EXPECT_FALSE(sanctions_list_entry_exists(&mod, &sanctions[0]));
EXPECT_FALSE(sanctions_list_entry_exists(&mod, &sanctions[1]));
EXPECT_TRUE(mod_list_remove_entry(&mod, get_sig_pk(pk.data())));
logger_kill(log);
}
};
// TODO(JFreegman): Split this up into smaller subtests
TEST_F(SanctionsListMod, PackUnpackSanction)
{
std::vector<uint8_t> packed(sanctions_list_packed_size(2));
EXPECT_EQ(
sanctions_list_pack(packed.data(), packed.size(), sanctions, 2, nullptr), packed.size());
Mod_Sanction unpacked_sanctions[2] = {};
uint16_t processed_data_len = 0;
EXPECT_EQ(sanctions_list_unpack(unpacked_sanctions, &mod.sanctions_creds, 2, packed.data(),
packed.size(), &processed_data_len),
2);
EXPECT_EQ(processed_data_len, packed.size());
EXPECT_TRUE(sanctions_list_check_integrity(
&mod, &mod.sanctions_creds, unpacked_sanctions, mod.num_sanctions));
EXPECT_TRUE(sanctions_list_entry_exists(&mod, &unpacked_sanctions[0]));
EXPECT_TRUE(sanctions_list_entry_exists(&mod, &unpacked_sanctions[1]));
}
TEST_F(SanctionsListMod, ReplaceSanctionSignatures)
{
EXPECT_EQ(sanctions_list_replace_sig(&mod, mod.self_public_sig_key), mod.num_sanctions);
EXPECT_TRUE(
sanctions_list_check_integrity(&mod, &mod.sanctions_creds, sanctions, mod.num_sanctions));
}
} // namespace

View File

@ -17,10 +17,39 @@
#include <string.h>
#include <time.h>
#include "crypto_core.h" /* for CRYPTO_PUBLIC_KEY_SIZE */
#include "ccompat.h"
#include "crypto_core.h" // for CRYPTO_PUBLIC_KEY_SIZE
bool is_power_of_2(uint64_t x)
{
return x != 0 && (x & (~x + 1)) == x;
}
const uint8_t *get_enc_key(const uint8_t *key)
{
return key;
}
const uint8_t *get_sig_pk(const uint8_t *key)
{
return key + ENC_PUBLIC_KEY_SIZE;
}
void set_sig_pk(uint8_t *key, const uint8_t *sig_pk)
{
memcpy(key + ENC_PUBLIC_KEY_SIZE, sig_pk, SIG_PUBLIC_KEY_SIZE);
}
const uint8_t *get_sig_sk(const uint8_t *key)
{
return key + ENC_SECRET_KEY_SIZE;
}
const uint8_t *get_chat_id(const uint8_t *key)
{
return key + ENC_PUBLIC_KEY_SIZE;
}
/** Equality function for public keys. */
bool pk_equal(const uint8_t *dest, const uint8_t *src)
{
return public_key_eq(dest, src);
@ -31,6 +60,34 @@ void pk_copy(uint8_t *dest, const uint8_t *src)
memcpy(dest, src, CRYPTO_PUBLIC_KEY_SIZE);
}
void free_uint8_t_pointer_array(uint8_t **ary, size_t n_items)
{
if (ary == nullptr) {
return;
}
for (size_t i = 0; i < n_items; ++i) {
if (ary[i] != nullptr) {
free(ary[i]);
}
}
free(ary);
}
uint16_t data_checksum(const uint8_t *data, uint32_t length)
{
uint8_t checksum[2] = {0};
uint16_t check;
for (uint32_t i = 0; i < length; ++i) {
checksum[i % 2] ^= data[i];
}
memcpy(&check, checksum, sizeof(check));
return check;
}
int create_recursive_mutex(pthread_mutex_t *mutex)
{
pthread_mutexattr_t attr;
@ -111,3 +168,19 @@ uint64_t min_u64(uint64_t a, uint64_t b)
{
return a < b ? a : b;
}
uint32_t jenkins_one_at_a_time_hash(const uint8_t *key, size_t len)
{
uint32_t hash = 0;
for (uint32_t i = 0; i < len; ++i) {
hash += key[i];
hash += hash << 10;
hash ^= hash >> 6;
}
hash += hash << 3;
hash ^= hash >> 11;
hash += hash << 15;
return hash;
}

View File

@ -16,18 +16,33 @@
#include <stdint.h>
#include "attributes.h"
#include "crypto_core.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Equality function for public keys. */
bool is_power_of_2(uint64_t x);
/** Functions for groupchat extended keys */
non_null() const uint8_t *get_enc_key(const uint8_t *key);
non_null() const uint8_t *get_sig_pk(const uint8_t *key);
non_null() void set_sig_pk(uint8_t *key, const uint8_t *sig_pk);
non_null() const uint8_t *get_sig_sk(const uint8_t *key);
non_null() const uint8_t *get_chat_id(const uint8_t *key);
/** @brief Equality function for public keys. */
non_null() bool pk_equal(const uint8_t *dest, const uint8_t *src);
/**
* @brief Copy a public key from `src` to `dest`.
*/
non_null() void pk_copy(uint8_t *dest, const uint8_t *src);
/** @brief Frees all pointers in a uint8_t pointer array, as well as the array itself. */
nullable(1)
void free_uint8_t_pointer_array(uint8_t **ary, size_t n_items);
/** Returns -1 if failed or 0 if success */
non_null() int create_recursive_mutex(pthread_mutex_t *mutex);
@ -61,6 +76,20 @@ uint16_t min_u16(uint16_t a, uint16_t b);
uint32_t min_u32(uint32_t a, uint32_t b);
uint64_t min_u64(uint64_t a, uint64_t b);
/** @brief Returns a 32-bit hash of key of size len */
non_null()
uint32_t jenkins_one_at_a_time_hash(const uint8_t *key, size_t len);
/** @brief Computes a checksum of a byte array.
*
* @param data The byte array used to compute the checksum.
* @param length The length in bytes of the passed data.
*
* @retval The resulting checksum.
*/
non_null()
uint16_t data_checksum(const uint8_t *data, uint32_t length);
#ifdef __cplusplus
} // extern "C"
#endif