diff --git a/src/chat/chat_event.c b/src/chat/chat_event.c index d5c03e42..f4430e62 100644 --- a/src/chat/chat_event.c +++ b/src/chat/chat_event.c @@ -17,7 +17,7 @@ #endif void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler) { - if (!inst) return; + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: handler requires instance"); return; } #ifdef __ANDROID__ __android_log_print(ANDROID_LOG_INFO, "utun-evt", "chat_event_set_handler: handler=%p old=%p", (void*)handler, (void*)inst->chat_event_handler); @@ -26,7 +26,11 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha } void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { - if (inst && inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; } + if (!inst || type <= 0 || len < 0 || (len && !data)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: invalid event instance=%p type=%d bytes=%d data=%p", + (void*)inst, type, len, (const void*)data); return; + } + if (inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; } static const char* names[] = { [1]="MSG_RECEIVED", [2]="CONNECT_RESULT", [3]="NEW_PEER", @@ -64,7 +68,8 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : NULL; if (!n) n = "?"; DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len); - if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len); + if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE && type != CHAT_EVT_INVITE_LINK_READY) + log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len); } /* Идентификатор связывает результат с конкретной отправкой, независимо от текущего канала GUI. */ diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 2993d00e..731fbb03 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -6,7 +6,8 @@ * post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет. * data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия. * - * Без обработчика — данные логируются через DEBUG_DEBUG + log_dump; LOCAL_INVITE с ключом не дампится. + * Без обработчика — DEBUG_DEBUG + log_dump; LOCAL_INVITE/INVITE_LINK_READY с секретами не дампятся. + * Обработчик может удалить канал; stop/destroy сервиса выполняются вне callbacks. * Все вызовы — из uasync-потока. */ @@ -61,7 +62,7 @@ extern "C" { #define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */ #define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */ #define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */ -#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1][text:var] */ +#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1] */ #define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][generation:8][JSON:utf8] — история указанного поколения */ #define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */ #define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ @@ -101,7 +102,8 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha // ── Отправка событий из ядра ── -// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*. +// Синхронно передать type>0 и len>=0 байт data; inst обязателен,data может быть NULL только при len=0. +// Неверные аргументы логируются без доставки;формат CHAT_EVT_* проверяет получатель,память остаётся отправителю. void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); // Сообщить этап подготовки: state=0 начало,1 готово,2 ошибка; target — ID без префикса, is_dm добавляет "dm:". diff --git a/src/chat/chat_join.c b/src/chat/chat_join.c index 6f44a967..a691ac7e 100644 --- a/src/chat/chat_join.c +++ b/src/chat/chat_join.c @@ -8,6 +8,7 @@ #include "chat_join.h" #include "member_sync.h" #include "chat_event.h" +#include "chat_sync.h" #include "../utun_instance.h" #include "../routing_layer/topo_node_sqlite.h" @@ -59,11 +60,11 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) { } static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) { - _keys_purge_expired(inst); - if (!channel_id || !join_key || !inviter_id) { + if (!inst || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID); return -1; } + _keys_purge_expired(inst); for (struct join_key_entry* e = inst->join_keys; e; e = e->next) { if (e->channel_id != channel_id || e->join_key != join_key) continue; if (e->inviter_id != inviter_id) { @@ -103,13 +104,17 @@ static void _keys_free_all(struct UTUN_INSTANCE* inst) { int chat_join_serialize_member(const struct join_member_data* m, uint8_t* out, size_t out_sz, size_t* out_len) { - if (!m || !out || !out_len || !m->x25519 || !m->ed25519) return -1; + if (!m || !out || !out_len || !m->x25519 || !m->ed25519) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid member serialization arguments"); return -1; + } size_t ul = strnlen(m->userinfo, sizeof(m->userinfo)); if (ul > MS_USERINFO_MAX) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "chat_join: profile exceeds limit bytes=%zu", ul); return -1; } size_t need = 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2 + ul; - if (need > out_sz) return -1; + if (need > out_sz) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: member buffer too small need=%zu capacity=%zu", need, out_sz); return -1; + } size_t off = 0; memcpy(out + off, &m->node_id, 8); off += 8; memcpy(out + off, m->x25519, 32); off += 32; @@ -125,8 +130,11 @@ int chat_join_serialize_member(const struct join_member_data* m, return 0; } -int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m) { - if (!data || !m || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) return -1; +int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* output) { + if (!data || !output || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid/short member block bytes=%zu", len); return -1; + } + struct join_member_data parsed, *m = &parsed; memset(m, 0, sizeof(*m)); size_t off = 0; memcpy(&m->node_id, data + off, 8); off += 8; @@ -142,6 +150,7 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d } memcpy(m->userinfo, data + off, ul); m->userinfo[ul] = '\0'; + *output = parsed; return 0; } @@ -150,6 +159,9 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t from_node, uint64_t join_key, const uint8_t* member_data, size_t member_len) { + if (!inst || inst->join_stopping || !group_id || group_id > INT64_MAX || !from_node) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid/stopped process request", CJ_ID); return; + } struct join_member_data m; if (chat_join_parse_member(member_data, member_len, &m) != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request parse failed ch=%llu from=%016llx", CJ_ID, (unsigned long long)group_id, (unsigned long long)from_node); @@ -188,6 +200,18 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, return; } + /* Локальная запись доверенная: до неё проверяем также профиль и уже известный ключ идентичности. */ + struct ms_member_rec record = {.node_id = m.node_id, .x25519 = m.x25519, .ed25519 = m.ed25519, + .join_sig = m.join_sig, .join_ts = m.join_ts, .update_sig = m.update_sig, .update_ts = m.update_ts, + .userinfo = m.userinfo}; + uint8_t trusted_ed[32]; + int lookup = topo_node_sqlite_get_ed25519_pubkey(db, m.node_id, trusted_ed); + if (lookup == TOPO_SQLITE_LOOKUP_ERROR || (lookup == 0 && memcmp(trusted_ed, m.ed25519, sizeof(trusted_ed))) || + member_sync_verify_local_record(db, ch_id, &record) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member validation failed ch=%s node=%016llx lookup=%d", CJ_ID, + ch_id, (unsigned long long)m.node_id, lookup); return; + } + /* 3. добавить мембера + подписать его pubkey (дерево приглашений) */ uint8_t tree_sig[64]; if (member_sync_sign_pubkey(inst->my_ed25519_privkey, m.x25519, tree_sig) != 0) { @@ -204,22 +228,8 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, return; } if (rc & MS_APPLY_CHANGED) { - /* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */ - uint64_t gid = strtoull(ch_id, NULL, 10); - struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); - if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) { - struct ETCP_CONN* jc = instance_find_conn(inst, m.node_id); - if (jc) { - topo_group_new_conn(g, jc); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: joiner 0x%016llx added to group ch=%s for BGP", - CJ_ID, (unsigned long long)m.node_id, ch_id); - } - } - /* уведомить GUI инвайтера об успешном присоединении джойнера */ - uint8_t cevt[20]; memcpy(cevt, &m.node_id, 8); - int r = 0; memcpy(cevt + 8, &r, 4); - memcpy(cevt + 12, &group_id, 8); - chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, cevt, 20); + /* member_sync уже запускает сессию после commit; GUI получает общий формат результата. */ + chat_sync_join_result(inst, 0, group_id, m.node_id, CHAT_JOIN_OK); } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me", @@ -354,7 +364,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { /* ── Жизненный цикл ── */ int chat_join_init(struct UTUN_INSTANCE* inst) { - if (!inst) return -1; + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: init without instance", CJ_ID); return -1; } inst->join_stopping = 0; if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID); @@ -382,7 +392,7 @@ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, u struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) { - if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) { + if (!inst || !inst->ua || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !callback) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID); return NULL; } @@ -415,6 +425,9 @@ struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst } uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { + if (!inst || !channel_id || channel_id > INT64_MAX || !join_key) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key lookup", CJ_ID); return 0; + } struct join_key_entry* e = _keys_find(inst, channel_id, join_key); return e ? e->inviter_id : 0; } @@ -422,7 +435,9 @@ uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_i int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_id, uint64_t join_key, const struct join_member_data* m) { - if (!inst || !m || inviter_id == 0) return -1; + if (!inst || inst->join_stopping || !m || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid forward arguments", CJ_ID); return -1; + } uint8_t mbuf[MS_USERINFO_MAX + 256]; size_t mlen = 0; if (chat_join_serialize_member(m, mbuf, sizeof(mbuf), &mlen) != 0) { diff --git a/src/chat/chat_join.h b/src/chat/chat_join.h index e9f9fb77..ca189137 100644 --- a/src/chat/chat_join.h +++ b/src/chat/chat_join.h @@ -15,7 +15,7 @@ * │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J) * │ │ валиден ключ → только pending, БЕЗ BGP new_conn * │◀── REQUEST_FWD ───────────┤ │ - * │ verif join_sig, node_id=derive(x25519) │ + * │ verif JOIN,профиля,известного Ed25519,node_id=derive(x25519) │ * │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join")) * │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │ * │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C @@ -115,11 +115,14 @@ struct join_member_data { char userinfo[MS_USERINFO_MAX + 1]; /* JSON, null-terminated */ }; -/* Сериализовать member-блок (без join_key). Возвращает 0 и *out_len, -1 если не влез. */ +/* Сериализовать member-блок (без join_key). 0 и *out_len,-1 при неверных аргументах/размере. + * На отказе out/out_len сохраняются; подписи NULL кодируются 64 нулями,ключи обязательны. + * Числа native,кроме userinfo_len:u16BE;userinfo<=1024 байт без NUL на проводе. */ int chat_join_serialize_member(const struct join_member_data* m, uint8_t* out, size_t out_sz, size_t* out_len); -/* Разобрать member-блок. Указатели ссылаются на data (не копируют). 0=ок, -1=ошибка. */ +/* Разобрать весь member-блок:проверить длину/отсутствие скрытого NUL. 0/-1,на отказе m сохраняется. + * Ключи/подписи заимствованы у data,userinfo копируется;криптография здесь не проверяется. */ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m); /* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */ @@ -147,7 +150,9 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id, const struct join_member_data* m); /* Инвайтер (локально, когда connection==инвайтер, или из recv REQUEST_FWD): - * верификация join_sig + добавление мембера + подпись дерева. */ + * Проверка ключа,идентичности,JOIN,подписи профиля при наличии и известного Ed25519; + * затем доверенная локальная запись с подписью приглашения. Join-only допускается. + * Подключение группы инициирует member_sync после commit;при изменении CONNECT_RESULT имеет request_id=0. */ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t from_node, uint64_t join_key, const uint8_t* member_data, size_t member_len); diff --git a/tests/test_chat_join.c b/tests/test_chat_join.c index 641b1f54..52c644e5 100644 --- a/tests/test_chat_join.c +++ b/tests/test_chat_join.c @@ -7,10 +7,11 @@ // Задача 2 (интеграция): два инстанса A(инвайтер) и C(connection) — регистрация join-ключа // через etcp_router, после сходимости BGP lookup_inviter(C) == A.node_id. // -// chat_core/chat_sync (глобальные синглтоны) НЕ используются: конфиг без секции [chatserver], +// Сервис chat_core/chat_sync не запускается: конфиг без секции [chatserver], // member_sync/chat_join инициализируются вручную. #include "chat_join.h" +#include "chat_event.h" #include "../routing_layer/etcp_router.h" #include "../routing_layer/route_crypto.h" #include "../transport_layer/etcp_api.h" @@ -31,6 +32,7 @@ #include "test_net.h" #include +#include #define OPENSSL_API_COMPAT 0x10100000L #include #include @@ -58,6 +60,14 @@ static char g_tdir[256] = "/tmp/utun_cj_XXXXXX"; static char g_cfg[MAX_NODES][256]; static char g_db_dir[MAX_NODES][320]; static uint64_t g_group_id = 0; +static unsigned join_results; +static void check_join_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + (void)inst; + if (type != CHAT_EVT_CONNECT_RESULT) return; + uint64_t request; int32_t result; + assert(len == 28); memcpy(&request, data + 20, 8); memcpy(&result, data + 8, 4); + assert(request == 0 && result == 0); join_results++; +} /* ── крипто-хелперы ── */ @@ -415,6 +425,7 @@ static void t_process_request(void) { /* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */ TEST("process_request: valid join_sig accepted"); { + chat_event_set_handler(inst, check_join_event); struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); uint8_t buf[512]; size_t len = 0; serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); @@ -427,8 +438,22 @@ static void t_process_request(void) { rec.x25519 = j.x_pub; rec.signature = sig; rec.signed_by = sb; int vs = (gs == 0) ? member_sync_verify_signature(db, CH_COMP, &rec) : -99; - if (in_ch == 1 && gs == 0 && sb == inst->node_id && vs == 1) OK(); + if (in_ch == 1 && gs == 0 && sb == inst->node_id && vs == 1 && join_results == 1) OK(); else FAIL("in_ch=%d gs=%d sb=%016llx vs=%d", in_ch, gs, (unsigned long long)sb, vs); + chat_event_set_handler(inst, NULL); + } + + TEST("process_request: signed profile accepted"); { + struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); + uint8_t update[64], message[MS_USERINFO_MAX + 73], buf[MS_USERINFO_MAX + 256]; size_t len; + struct join_member_data m = {.node_id = j.node_id, .x25519 = j.x_pub, .ed25519 = j.ed_pub, + .join_sig = j.join_sig, .join_ts = j.join_ts, .update_sig = update, .update_ts = 1}; + strcpy(m.userinfo, "{\"name\":\"signed profile\"}"); + int bytes = member_sync_build_update_msg(j.join_sig, m.update_ts, m.userinfo, message, sizeof(message)); + assert(bytes > 0 && sc_ed25519_sign(j.ed_priv, message, (size_t)bytes, update) == SC_OK); + assert(!chat_join_serialize_member(&m, buf, sizeof(buf), &len)); + chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); + if (topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); } /* 2. невалидный join_sig → отклонён */