/* * chat_join.c — авторизация входа в канал через connection-узел и инвайтера. * Маршрутизируемая (etcp_router) часть: регистрация ключа, форвард запроса, * обработка на инвайтере (верификация + добавление мембера + подпись дерева). * См. описание flow в chat_join.h. */ #include "chat_join.h" #include "member_sync.h" #include "chat_event.h" #include "../utun_instance.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" #include "../routing_layer/etcp_router.h" #include "../routing_layer/route_crypto.h" #include "../transport_layer/etcp_api.h" #include "../transport_layer/etcp.h" #include "../transport_layer/secure_channel.h" #include "../ntp_time.h" #include "../../lib/mem.h" #include "../../lib/ll_queue.h" #include "../../lib/u_async.h" #include "../../lib/debug_config.h" #include "../../lib/platform_compat.h" #include #include #define CJ_ID "chat_join" /* ── Хранилище ключей на connection-узле ── */ struct join_key_entry { struct join_key_entry* next; uint64_t join_key; uint64_t channel_id; uint64_t inviter_id; /* кто пригласил (src KEY_REGISTER) */ uint64_t expiry_tb; /* timebase (0.1ms) истечения */ }; static uint64_t _now_tb(void) { return get_time_tb(); } static void _keys_purge_expired(struct UTUN_INSTANCE* inst) { uint64_t now = _now_tb(); struct join_key_entry** p = &inst->join_keys; while (*p) { if ((*p)->expiry_tb <= now) { struct join_key_entry* r = *p; *p = r->next; DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key expired key=%016llx ch=%llu inviter=%016llx", CJ_ID, (unsigned long long)r->join_key, (unsigned long long)r->channel_id, (unsigned long long)r->inviter_id); u_free(r); } else { p = &(*p)->next; } } } 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) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID); return -1; } 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) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration conflicts with existing inviter ch=%llu", CJ_ID, (unsigned long long)channel_id); return -1; } e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL; return 0; } struct join_key_entry* e = u_malloc(sizeof(*e)); if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return -1; } e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id; e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL; e->next = inst->join_keys; inst->join_keys = e; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds", CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id, (unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS); return 0; } static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { _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) return e; return NULL; } static void _keys_free_all(struct UTUN_INSTANCE* inst) { struct join_key_entry* e = inst->join_keys; while (e) { struct join_key_entry* n = e->next; u_free(e); e = n; } inst->join_keys = NULL; } /* ── Сериализация member-блока ── */ 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; 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; size_t off = 0; memcpy(out + off, &m->node_id, 8); off += 8; memcpy(out + off, m->x25519, 32); off += 32; memcpy(out + off, m->ed25519, 32); off += 32; if (m->join_sig) memcpy(out + off, m->join_sig, 64); else memset(out + off, 0, 64); off += 64; memcpy(out + off, &m->join_ts, 8); off += 8; if (m->update_sig) memcpy(out + off, m->update_sig, 64); else memset(out + off, 0, 64); off += 64; memcpy(out + off, &m->update_ts, 8); off += 8; out[off++] = (uint8_t)(ul >> 8); out[off++] = (uint8_t)ul; if (ul) memcpy(out + off, m->userinfo, ul); off += ul; *out_len = off; 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; memset(m, 0, sizeof(*m)); size_t off = 0; memcpy(&m->node_id, data + off, 8); off += 8; m->x25519 = data + off; off += 32; m->ed25519 = data + off; off += 32; m->join_sig = data + off; off += 64; memcpy(&m->join_ts, data + off, 8); off += 8; m->update_sig = data + off; off += 64; memcpy(&m->update_ts, data + off, 8); off += 8; size_t ul = ((size_t)data[off] << 8) | data[off + 1]; off += 2; if (ul > MS_USERINFO_MAX || off + ul != len || memchr(data + off, 0, ul)) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "chat_join: invalid profile length bytes=%zu payload=%zu", ul, len); return -1; } memcpy(m->userinfo, data + off, ul); m->userinfo[ul] = '\0'; return 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) { 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); return; } char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id); sqlite3* db = inst->topo_sqlite_db; /* 0. верификация join-ключа: принимаем только выданные нами (инвайтером) ключи */ struct join_key_entry* ke = _keys_find(inst, group_id, join_key); if (!ke || ke->inviter_id != inst->node_id) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request invalid join key ch=%s from=%016llx key=%016llx — drop", CJ_ID, ch_id, (unsigned long long)from_node, (unsigned long long)join_key); return; } /* 1. привязка идентичности: node_id = derive(x25519) */ if (sc_derive_node_id_from_pubkey(m.x25519) != m.node_id) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request node_id/x25519 mismatch (forgery) nid=0x%016llx ch=%s", CJ_ID, (unsigned long long)m.node_id, ch_id); return; } /* 2. верификация join_sig: sign(ch_x||ch_ed||node_id||x25519||join_ts) ключом ed25519 */ uint8_t ch_x[32], ch_ed[32]; if (!db || topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, ch_x, ch_ed, NULL) != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request no channel ch=%s", CJ_ID, ch_id); return; } uint8_t vmsg[128]; int vlen = member_sync_build_join_msg(ch_x, ch_ed, m.node_id, m.x25519, m.join_ts, vmsg, (int)sizeof(vmsg)); if (vlen < 0 || sc_ed25519_verify(m.ed25519, vmsg, (size_t)vlen, m.join_sig) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request invalid join_sig nid=0x%016llx ch=%s", CJ_ID, (unsigned long long)m.node_id, ch_id); return; } /* 3. добавить мембера + подписать его pubkey (дерево приглашений) */ uint8_t tree_sig[64]; if (member_sync_sign_pubkey(inst->my_ed25519_privkey, m.x25519, tree_sig) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request sign_pubkey FAILED nid=0x%016llx", CJ_ID, (unsigned long long)m.node_id); return; } int rc = member_sync_put(inst, ch_id, m.node_id, m.x25519, m.ed25519, m.join_sig, m.join_ts, m.update_sig, m.update_ts, m.userinfo, NULL, NULL, 0, inst->node_id, tree_sig); if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request member_sync_put FAILED ch=%s nid=0x%016llx rc=%d", CJ_ID, ch_id, (unsigned long long)m.node_id, rc); 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); } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me", CJ_ID, (unsigned long long)m.node_id, ch_id, (unsigned long long)from_node, (unsigned long long)join_key); } /* Ожидание прикладного ACK: надёжная доставка router сама повторяет потерянные пакеты. */ struct chat_join_registration { struct chat_join_registration* next; struct UTUN_INSTANCE* inst; uint64_t channel_id, target_node_id, join_key; void* timer; chat_join_registered_fn callback; void* arg; }; static int send_key_control(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target, uint64_t key, uint8_t subcmd, uint8_t status) { struct ll_entry* entry = ll_alloc_lldgram(subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK ? 11 : 10); if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control allocation failed", CJ_ID); return -1; } entry->dgram[0] = ETCP_RT_ID_JOIN; entry->len = entry->memlen; entry->dgram[1] = subcmd; memcpy(entry->dgram + 2, &key, 8); if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) entry->dgram[10] = status; int rc = etcp_route_send(inst, channel_id, target, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); if (rc != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control send failed cmd=%02x ch=%llu peer=%016llx rc=%d", CJ_ID, subcmd, (unsigned long long)channel_id, (unsigned long long)target, rc); } return rc; } static void registration_finish(struct chat_join_registration* req, int result) { struct chat_join_registration** p = &req->inst->join_registrations; while (*p && *p != req) p = &(*p)->next; if (*p) *p = req->next; if (req->timer) uasync_cancel_timeout(req->inst->ua, req->timer); if (result == CHAT_JOIN_OK) { if (chat_join_store_local_key(req->inst, req->channel_id, req->join_key) != 0) result = CHAT_JOIN_ERROR; } if (result == CHAT_JOIN_OK || result == CHAT_JOIN_CANCELLED) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration %s ch=%llu peer=%016llx key=%016llx", CJ_ID, result == CHAT_JOIN_OK ? "confirmed" : "cancelled", (unsigned long long)req->channel_id, (unsigned long long)req->target_node_id, (unsigned long long)req->join_key); } else { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration failed ch=%llu peer=%016llx key=%016llx result=%d", CJ_ID, (unsigned long long)req->channel_id, (unsigned long long)req->target_node_id, (unsigned long long)req->join_key, result); } chat_join_registered_fn callback = req->callback; void* arg = req->arg; u_free(req); callback(arg, result); } static void registration_timer(void* arg) { struct chat_join_registration* req = arg; req->timer = NULL; registration_finish(req, req->target_node_id == req->inst->node_id ? CHAT_JOIN_OK : CHAT_JOIN_TIMEOUT); } void chat_join_cancel_registration(struct chat_join_registration* req) { if (req) registration_finish(req, CHAT_JOIN_CANCELLED); } /* ── etcp_router recv ── */ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) { if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return; } struct UTUN_INSTANCE* inst = conn->instance; if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return; } const uint8_t* d = entry->dgram; uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8); uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8); uint8_t required = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED; if (from_node != inst->node_id && (d[ROUTER_SVC_FLAGS_OFF] & required) != required) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: unprotected join packet rejected from=%016llx ch=%llu", CJ_ID, (unsigned long long)from_node, (unsigned long long)group_id); queue_dgram_free(entry); queue_entry_free(entry); return; } const uint8_t* payload = d + ROUTER_SVC_PAYLOAD_OFF; size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF; uint8_t subcmd = payload[0]; DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv subcmd=%02x from=%016llx ch=%llu len=%zu", CJ_ID, subcmd, (unsigned long long)from_node, (unsigned long long)group_id, plen); if (subcmd == JOIN_SUBCMD_KEY_REGISTER) { /* payload: [subcmd:1][join_key:8] */ if (plen != 9) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER len=%zu", CJ_ID, plen); } else { uint64_t key; memcpy(&key, payload + 1, 8); int rc = _keys_put(inst, group_id, key, from_node); send_key_control(inst, group_id, from_node, key, JOIN_SUBCMD_KEY_REGISTER_ACK, rc ? 1 : 0); } } else if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) { if (plen != 10 || payload[9] > 1) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER_ACK len=%zu", CJ_ID, plen); } else { uint64_t key; memcpy(&key, payload + 1, 8); struct chat_join_registration* req = inst->join_registrations; while (req && (req->channel_id != group_id || req->target_node_id != from_node || req->join_key != key)) req = req->next; /* Освобождаем пакет до callback: владелец может закрыть instance. */ int result = payload[9] ? CHAT_JOIN_ERROR : CHAT_JOIN_OK; queue_dgram_free(entry); queue_entry_free(entry); if (req) registration_finish(req, result); else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: late/unmatched registration ACK ch=%llu peer=%016llx", CJ_ID, (unsigned long long)group_id, (unsigned long long)from_node); return; } } else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) { /* payload: [subcmd:1][join_key:8][member...] */ if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } uint64_t join_key; memcpy(&join_key, payload + 1, 8); chat_join_process_request(inst, group_id, from_node, join_key, payload + 1 + 8, plen - 1 - 8); } else { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: unknown subcmd=%02x from=%016llx", CJ_ID, subcmd, (unsigned long long)from_node); } queue_dgram_free(entry); queue_entry_free(entry); } /* ── Жизненный цикл ── */ int chat_join_init(struct UTUN_INSTANCE* inst) { if (!inst) 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); return -1; } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: initialized", CJ_ID); return 0; } void chat_join_destroy(struct UTUN_INSTANCE* inst) { if (!inst) return; inst->join_stopping = 1; if (inst->router_bindings.callbacks[ETCP_RT_ID_JOIN]) etcp_router_unbind(inst, ETCP_RT_ID_JOIN); while (inst->join_registrations) chat_join_cancel_registration(inst->join_registrations); _keys_free_all(inst); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID); } /* ── Публичные операции ── */ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local registration without instance", CJ_ID); return -1; } return _keys_put(inst, channel_id, join_key, inst->node_id); } 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) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID); return NULL; } for (struct chat_join_registration* r = inst->join_registrations; r; r = r->next) { if (r->channel_id == channel_id && r->join_key == join_key) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration already pending ch=%llu", CJ_ID, (unsigned long long)channel_id); return NULL; } } struct chat_join_registration* req = u_calloc(1, sizeof(*req)); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration allocation failed", CJ_ID); return NULL; } req->inst = inst; req->channel_id = channel_id; req->join_key = join_key; req->target_node_id = target_node_id ? target_node_id : inst->node_id; req->callback = callback; req->arg = arg; int local = req->target_node_id == inst->node_id; req->timer = uasync_set_timeout(inst->ua, local ? 0 : JOIN_REGISTER_TIMEOUT_TB, req, registration_timer, "join_register"); if (!req->timer) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration timer allocation failed", CJ_ID); u_free(req); return NULL; } if (!local && send_key_control(inst, channel_id, req->target_node_id, join_key, JOIN_SUBCMD_KEY_REGISTER, 0) != 0) { uasync_cancel_timeout(inst->ua, req->timer); u_free(req); return NULL; } req->next = inst->join_registrations; inst->join_registrations = req; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration pending ch=%llu peer=%016llx key=%016llx", CJ_ID, (unsigned long long)channel_id, (unsigned long long)req->target_node_id, (unsigned long long)join_key); return req; } uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { struct join_key_entry* e = _keys_find(inst, channel_id, join_key); return e ? e->inviter_id : 0; } 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; uint8_t mbuf[MS_USERINFO_MAX + 256]; size_t mlen = 0; if (chat_join_serialize_member(m, mbuf, sizeof(mbuf), &mlen) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward serialize failed", CJ_ID); return -1; } size_t plen = 1 + 8 + mlen; uint8_t* payload = u_malloc(plen); if (!payload) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward payload allocation failed bytes=%zu", CJ_ID, plen); return -1; } payload[0] = JOIN_SUBCMD_REQUEST_FWD; memcpy(payload + 1, &join_key, 8); memcpy(payload + 1 + 8, mbuf, mlen); struct ll_entry* entry = queue_entry_new(0); if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward entry allocation failed", CJ_ID); u_free(payload); return -1; } entry->dgram = u_malloc(1 + plen); if (!entry->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward datagram allocation failed bytes=%zu", CJ_ID, plen + 1); u_free(payload); queue_entry_free(entry); return -1; } entry->dgram[0] = ETCP_RT_ID_JOIN; memcpy(entry->dgram + 1, payload, plen); entry->len = 1 + (uint16_t)plen; u_free(payload); int rc = etcp_route_send(inst, channel_id, inviter_id, entry, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); if (rc != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx", CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)inviter_id); return -1; } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx", CJ_ID, (unsigned long long)channel_id, (unsigned long long)inviter_id, (unsigned long long)m->node_id); return 0; }