diff --git a/lib/json_flat.c b/lib/json_flat.c index ace4925e..f8c8e47c 100644 --- a/lib/json_flat.c +++ b/lib/json_flat.c @@ -1,4 +1,6 @@ #include "json_flat.h" +#include "mem.h" +#include "debug_config.h" #include #define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++ @@ -19,6 +21,8 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t case 'n': ch = '\n'; break; case 'r': ch = '\r'; break; case 't': ch = '\t'; break; + case 'b': ch = '\b'; break; + case 'f': ch = '\f'; break; default: return -1; } } @@ -33,20 +37,22 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t return 0; } -static int json_parse_pair(const char** pp, const char* end, +static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity, json_flat_cb cb, void* arg) { const char* p = *pp; SKIP_WS(p, end); CHECK(p, end); if (*p == '}') { *pp = p; return 0; } - char key[128], val[256]; - if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1; + char* key = scratch; + if (json_read_string(&p, end, key, capacity) != 0) return -1; + size_t key_size = strlen(key) + 1; + char* val = scratch + key_size; SKIP_WS(p, end); CHECK(p, end); if (*p != ':') return -1; p++; SKIP_WS(p, end); CHECK(p, end); - if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1; + if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1; - if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; } + if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } } SKIP_WS(p, end); CHECK(p, end); if (*p == ',') p++; @@ -61,13 +67,23 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) { SKIP_WS(p, end); CHECK(p, end); if (*p != '{') return -1; p++; + /* Decoded key + value + two NULs fit within the encoded object size. */ + size_t capacity = strlen(json) + 1; + if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; } + char local[384]; + char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity); + if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; } + int result = 0; for (;;) { - int rc = json_parse_pair(&p, end, cb, arg); - if (rc < 0) return -1; - if (rc > 0) return 0; /* callback stopped */ - SKIP_WS(p, end); CHECK(p, end); - if (*p == '}') return 0; + int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg); + if (rc < 0) { result = -1; break; } + if (rc > 0) break; + SKIP_WS(p, end); + if (p >= end) { result = -1; break; } + if (*p == '}') break; } + if (scratch != local) u_free(scratch); + return result; } struct get_ctx { const char* key; char* out; size_t sz; int found; }; @@ -81,6 +97,7 @@ static int get_cb(const char* k, const char* v, void* arg) { } int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) { + if (!key || (value_out && !value_sz)) return -1; struct get_ctx g = { key, value_out, value_sz, 0 }; if (json_flat_parse(json, get_cb, &g) != 0) return -1; return g.found ? 0 : -1; diff --git a/lib/json_flat.h b/lib/json_flat.h index 5e636497..f99e8d13 100644 --- a/lib/json_flat.h +++ b/lib/json_flat.h @@ -2,7 +2,7 @@ * @file json_flat.h * @brief Парсер плоского JSON без вложенности: {"key":"value",...} * - * Только строковые значения. Без malloc, без зависимостей. + * Только строковые значения. Callback получает полные строки без обрезания. */ #ifndef JSON_FLAT_H @@ -15,6 +15,7 @@ extern "C" { #endif typedef int (*json_flat_cb)(const char* key, const char* value, void* arg); +/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */ int json_flat_parse(const char* json, json_flat_cb cb, void* arg); int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz); diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index cde2e50b..c2bb1f47 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -300,7 +300,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg) { static void json_escape(const char* src, char* dst, size_t dst_sz) { size_t i = 0; const char* s = src; while (*s && i < dst_sz - 2) { - if (*s == '"' || *s == '\\') { if (i + 1 < dst_sz - 2) dst[i++] = '\\'; } + const char* escape = NULL; + switch (*s) { + case '"': escape = "\\\""; break; + case '\\': escape = "\\\\"; break; + case '\n': escape = "\\n"; break; + case '\r': escape = "\\r"; break; + case '\t': escape = "\\t"; break; + case '\b': escape = "\\b"; break; + case '\f': escape = "\\f"; break; + } + if (escape) { + if (i + 2 >= dst_sz) break; + dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue; + } dst[i++] = *s++; } dst[i] = '\0'; @@ -403,7 +416,8 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) { struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1; + if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1; + *out_len = 0; char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); char* w = buf; char* end = buf + buf_size; *w++ = '['; const char* sep = ""; @@ -412,6 +426,12 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch "SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags " "FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl); sqlite3_stmt* st = NULL; + sqlite3_stmt* as = NULL; +#define MEMBERS_APPEND(...) do { \ + int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \ + if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \ + w += written; \ +} while (0) if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } while (sqlite3_step(st) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); @@ -422,7 +442,7 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch int x25519_len = sqlite3_column_bytes(st, 2); int ed25519_len = sqlite3_column_bytes(st, 3); const char* adm_tags = (const char*)sqlite3_column_text(st, 4); - char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[384]; + char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1]; json_escape(name ? name : "", esc_name, sizeof(esc_name)); x25519_hex[0] = ed25519_hex[0] = '\0'; if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]); @@ -437,14 +457,13 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags)); - w += snprintf(w, (size_t)(end - w), + MEMBERS_APPEND( "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d," "\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex); sep = ","; /* addresses */ - sqlite3_stmt* as = NULL; char asql[200]; snprintf(asql, sizeof(asql), "SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol"); if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) { @@ -465,20 +484,24 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7], addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]); else { snprintf(ip_str, sizeof(ip_str), "?"); } - w += snprintf(w, (size_t)(end - w), + MEMBERS_APPEND( "%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}", asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt); asep2 = ","; } - sqlite3_finalize(as); + sqlite3_finalize(as); as = NULL; } - w += snprintf(w, (size_t)(end - w), "]}"); + MEMBERS_APPEND("]}"); } + MEMBERS_APPEND("]"); sqlite3_finalize(st); - *w++ = ']'; - *w = '\0'; *out_len = (size_t)(w - buf); return 0; +overflow: + sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0; + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size); + return -1; +#undef MEMBERS_APPEND } int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) { @@ -500,7 +523,7 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* struct chat_member_tags_key { char key[CHAT_MEMBER_TAGS_KEY_SZ]; - char val[CHAT_MEMBER_TAGS_VAL_SZ]; + char* val; char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */ }; @@ -530,20 +553,19 @@ static int tags_is_append_key(const char* key) { return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0; } -static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) { - uint64_t ts = 0; - const char* eff = tags_append_effective(raw, &ts); - snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff); - (void)ts; -} - static int tags_json_cb(const char* key, const char* json_val, void* arg) { struct chat_member_tags* t = (struct chat_member_tags*)arg; - if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS) return -1; + if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1; + } struct chat_member_tags_key* k = &t->keys[t->key_count]; snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); - snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val); + k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val); k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL; + if (!k->val || (tags_is_append_key(key) && !k->raw)) { + u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k)); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1; + } t->key_count++; return 0; } @@ -563,7 +585,9 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (sqlite3_step(st) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(st, 0); - if (tags && tags[0]) chat_member_tags_replace(t, tags); + if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) { + sqlite3_finalize(st); chat_member_tags_free(t); return NULL; + } } sqlite3_finalize(st); } @@ -575,22 +599,24 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) { if (!t || !key) return -1; if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; } - if (val && strlen(val) >= CHAT_MEMBER_TAGS_VAL_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; } + if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; } int idx = tags_find(t, key); if (!val) { - if (idx >= 0) { u_free(t->keys[idx].raw); t->keys[idx] = t->keys[--t->key_count]; } + if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; } return 0; } + char* copy = u_strdup(val); + if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; } if (idx >= 0) { - snprintf(t->keys[idx].val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); + u_free(t->keys[idx].val); t->keys[idx].val = copy; } else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) { struct chat_member_tags_key* k = &t->keys[t->key_count++]; snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); - snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); + k->val = copy; k->raw = NULL; } else { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1; + u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1; } return 0; } @@ -602,11 +628,17 @@ const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) { } int chat_member_tags_replace(struct chat_member_tags* t, const char* json) { - if (!t || !json) return -1; - for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); - t->key_count = 0; - int rc = json_flat_parse(json, tags_json_cb, t); - if (rc != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; } + if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1; + } + struct chat_member_tags parsed = {0}; + int rc = json_flat_parse(json, tags_json_cb, &parsed); + if (rc != 0) { + for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); } + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; + } + for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); } + t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys)); DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d", CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count); return 0; @@ -617,8 +649,14 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1; } - char json[4096]; int off = 0, ver = 1; -#define TAGS_ADVANCE(n) do { if (off < (int)sizeof(json) - 10) off += (int)(n); else off = (int)sizeof(json); } while(0) + char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1; +#define TAGS_ADVANCE(expr) do { \ + int written = (expr); \ + if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \ + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \ + } \ + off += written; \ +} while (0) int vi = tags_find(t, "ver"); if (vi >= 0) ver = atoi(t->keys[vi].val) + 1; TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver)); @@ -647,7 +685,13 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* } else { out_val = k->val; } - TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", k->key, out_val)); + char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1]; + if (strlen(out_val) > MS_ADM_TAGS_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1; + } + json_escape(k->key, escaped_key, sizeof(escaped_key)); + json_escape(out_val, escaped_value, sizeof(escaped_value)); + TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value)); } TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}")); #undef TAGS_ADVANCE @@ -656,10 +700,9 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1; } - uint8_t sign_msg[8192]; size_t mlen = (size_t)off; - if (mlen + 8 > sizeof(sign_msg)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — json too big", CC_ID); return -1; } - memcpy(sign_msg, json, mlen); - memcpy(sign_msg + mlen, &t->node_id, 8); mlen += 8; + uint8_t sign_msg[MS_ADM_TAGS_MAX + 8]; + int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg)); + if (mlen < 0) return -1; uint8_t sig[64]; if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1; @@ -702,14 +745,14 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* sqlite3_finalize(st); if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s", - CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d", + CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off); return 0; } void chat_member_tags_free(struct chat_member_tags* t) { if (!t) return; - for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); + for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); } u_free(t); } diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 2bbbbf35..2431e965 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -214,7 +214,6 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* #define CHAT_MEMBER_TAGS_MAX_KEYS 16 #define CHAT_MEMBER_TAGS_KEY_SZ 64 -#define CHAT_MEMBER_TAGS_VAL_SZ 256 struct chat_member_tags; diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 1b3f218d..c5e0dee2 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -45,11 +45,23 @@ static uint64_t member_read64(const uint8_t* p) { return n; } +int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity) { + size_t len = tags ? strlen(tags) : 0; + if (!tags || !out || len > MS_ADM_TAGS_MAX || capacity < len + 8) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid owner message node=%016llx bytes=%zu capacity=%zu max=%d", + MS_ID, (unsigned long long)node_id, len, capacity, MS_ADM_TAGS_MAX); + return -1; + } + memcpy(out, tags, len); + memcpy(out + len, &node_id, 8); + return (int)len + 8; +} + static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig, uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) { - uint8_t data[1024]; + uint8_t data[MS_ADM_TAGS_MAX + 1024]; size_t off = 0; member_write64(data + off, node_id); off += 8; memcpy(data + off, x25519, 32); off += 32; @@ -59,13 +71,13 @@ static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const u if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64); off += 64; member_write64(data + off, update_ts); off += 8; size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0; - if (ul > 255 || atl > 255) { + if (ul > 255 || atl > MS_ADM_TAGS_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id); return -1; } data[off++] = (uint8_t)ul; if (ul) { memcpy(data + off, userinfo, ul); off += ul; } - data[off++] = (uint8_t)atl; + data[off++] = (uint8_t)(atl >> 8); data[off++] = (uint8_t)atl; if (atl) { memcpy(data + off, adm_tags, atl); off += atl; } if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64); off += 64; member_write64(data + off, signed_by); off += 8; @@ -237,15 +249,15 @@ static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int afte if (src != 0) continue; if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1) - || (nm && strlen(nm) > 255) || (atags && strlen(atags) > 255)) { + || (nm && strlen(nm) > 255) || (atags && strlen(atags) > MS_ADM_TAGS_MAX)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); sqlite3_finalize(stmt); return -1; } uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; - uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; + size_t atl = atags ? strlen(atags) : 0; uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; - size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64; + size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 2 + atl + 64 + 8 + 64; if (off + need > *len) { if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } *more = 1; @@ -268,7 +280,7 @@ static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int afte } buf[off++] = nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; } - buf[off++] = atl; + buf[off++] = (uint8_t)(atl >> 8); buf[off++] = (uint8_t)atl; if (atl) { memcpy(buf + off, atags, atl); off += atl; } if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; } @@ -364,7 +376,7 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even /* REMOVE не удаляет member (удаление — только битая подпись при верификации) */ - char tags[256] = ""; + char tags[MS_ADM_TAGS_MAX + 1] = ""; char peers_tbl[128]; _peers_table(group->channel_id, peers_tbl, sizeof(peers_tbl)); sqlite3_stmt* st = NULL; char sql[256]; snprintf(sql, sizeof(sql), "SELECT COALESCE(adm_tags,'') FROM \"%s\" WHERE node_id=?", peers_tbl); @@ -377,8 +389,8 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even sqlite3_finalize(st); } - DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags=%s", - MS_ID, event, (unsigned long long)node_id, tags); + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags_bytes=%zu", + MS_ID, event, (unsigned long long)node_id, strlen(tags)); _fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id); } @@ -475,9 +487,9 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — db is NULL", MS_ID); return -1; } int stale = 0, changed = 0; - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags=%s", + DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags_bytes=%zu", MS_ID, ch_id, (unsigned long long)m->node_id, (unsigned long long)m->update_ts, - m->adm_tags ? m->adm_tags : ""); + m->adm_tags ? strlen(m->adm_tags) : 0); char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); @@ -485,7 +497,7 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, int has_local = 0; uint8_t local_join_sig[64] = {0}; uint64_t local_join_ts = 0; uint64_t local_update_ts = 0; - char local_tags[256] = ""; + char local_tags[MS_ADM_TAGS_MAX + 1] = ""; uint64_t local_signed_by = 0; uint8_t local_signature[64] = {0}; int local_src = 0; @@ -601,18 +613,16 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0; uint8_t ch_ed_pub[32] = {0}; if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) { - uint8_t amsg[264]; size_t aoff = 0; - size_t atl2 = strlen(m->adm_tags); - if (atl2 > 191) atl2 = 191; - memcpy(amsg + aoff, m->adm_tags, atl2); aoff += atl2; - memcpy(amsg + aoff, &m->node_id, 8); aoff += 8; + uint8_t amsg[MS_ADM_TAGS_MAX + 8]; + int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg)); + if (aoff < 0) return MT_ERR_DATA; EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); if (pkey) { EVP_MD_CTX* vctx = EVP_MD_CTX_new(); if (vctx) { int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) && (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1); - if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s tags=%s", MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags); + if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s bytes=%zu", MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags)); else block_b_ok = 1; EVP_MD_CTX_free(vctx); } @@ -635,7 +645,7 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s", MS_ID, local_src, (unsigned long long)m->node_id, ch_id); - if (!identity_ok || !block_a_ok || (!is_local && m->adm_tags && m->adm_tags[0] && !block_b_ok)) { + if (!identity_ok || !block_a_ok || (m->adm_tags && m->adm_tags[0] && !block_b_ok)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d", MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok); return MT_ERR_DATA; @@ -762,11 +772,9 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const uint8_t ch_ed_pub[32] = {0}; if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) != 0) return 1; /* нет канального ключа — не можем проверить, не удаляем */ - uint8_t amsg[264]; size_t aoff = 0; - size_t atl2 = strlen(m->adm_tags); - if (atl2 > 191) atl2 = 191; - memcpy(amsg + aoff, m->adm_tags, atl2); aoff += atl2; - memcpy(amsg + aoff, &m->node_id, 8); aoff += 8; + uint8_t amsg[MS_ADM_TAGS_MAX + 8]; + int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg)); + if (aoff < 0) return 0; EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); if (!pkey) return 1; EVP_MD_CTX* vctx = EVP_MD_CTX_new(); @@ -775,8 +783,8 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const if (vctx) EVP_MD_CTX_free(vctx); EVP_PKEY_free(pkey); if (!ok) { - DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s tags=%s", - MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags); + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s bytes=%zu", + MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags)); return 0; } } @@ -877,7 +885,7 @@ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) { struct member_wire { struct ms_member_rec rec; - char userinfo[256], tags[256]; + char userinfo[256], tags[MS_ADM_TAGS_MAX + 1]; uint8_t local_join[64]; }; @@ -898,11 +906,11 @@ static int member_decode(const uint8_t* p, size_t len, struct member_wire* w, si if (len - off < 73) return -1; m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8; uint8_t n = p[off++]; - if (len - off < (size_t)n + 1 || memchr(p + off, 0, n)) return -1; + if (len - off < (size_t)n + 2 || memchr(p + off, 0, n)) return -1; memcpy(w->userinfo, p + off, n); off += n; - n = p[off++]; - if (len - off < (size_t)n + 136 || memchr(p + off, 0, n)) return -1; - memcpy(w->tags, p + off, n); off += n; + size_t tags_len = ((size_t)p[off] << 8) | p[off + 1]; off += 2; + if (tags_len > MS_ADM_TAGS_MAX || len - off < tags_len + 136 || memchr(p + off, 0, tags_len)) return -1; + memcpy(w->tags, p + off, tags_len); off += tags_len; m->adm_tags_sig = p + off; off += 64; m->signed_by = member_read64(p + off); off += 8; m->signature = p + off; off += 64; @@ -918,7 +926,7 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, co DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA; } uint16_t count = ((uint16_t)data[0] << 8) | data[1]; - if (count > (len - 2) / 283) { + if (count > (len - 2) / 284) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA; } size_t off = 2; @@ -1134,7 +1142,8 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, const char* adm_tags, const uint8_t* adm_tags_sig, int storage, uint64_t signed_by, const uint8_t* signature) { if (!inst || !ch_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put — inst=%p ch_id=%s", MS_ID, (void*)inst, ch_id ? ch_id : "(null)"); return -1; } - DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s adm_tags=%s storage=%d signed_by=%016llx", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", adm_tags ? adm_tags : "", storage, (unsigned long long)signed_by); + DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx tags_bytes=%zu storage=%d signed_by=%016llx", + MS_ID, ch_id, (unsigned long long)member_id, adm_tags ? strlen(adm_tags) : 0, storage, (unsigned long long)signed_by); struct ms_member_rec m; memset(&m, 0, sizeof(m)); diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index 0ec6fa4d..55688be8 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -55,6 +55,10 @@ struct sqlite3; #define MS_APPLY_CHANGED 0x01 /* запись и дерево обновлены */ #define MS_APPLY_STALE 0x02 /* локальная версия новее; обратный проход доставит её пиру */ +/* UTF-8 JSON bytes, excluding NUL. A complete record fits in one Merkle page. */ +#define MS_ADM_TAGS_MAX 8192 +int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity); + /* Распарсенный рекорд мембера (два подписанных блока). */ struct ms_member_rec { uint64_t node_id; diff --git a/src/chat/member_sync_doc.md b/src/chat/member_sync_doc.md index 2ed749b3..55699ea8 100644 --- a/src/chat/member_sync_doc.md +++ b/src/chat/member_sync_doc.md @@ -33,10 +33,11 @@ SHA256 по полям: node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8, update_sig:64, update_ts:8, userinfo_len:1, userinfo, -adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. +adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. Целые big-endian; отсутствующие подписи представлены нулями. -Обе строки ограничены 255 байтами. Хеш включает userinfo и дерево приглашений. +userinfo ограничен 255 байтами, adm_tags — 8192 байтами UTF-8 без NUL. Длина adm_tags — uint16 big-endian. +Хеш и подпись владельца покрывают весь adm_tags без обрезания. Хеш включает userinfo и дерево приглашений. Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно. ## Страница на проводе @@ -45,7 +46,7 @@ adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. node_id:8, x25519:32, ed25519:32, flags:1, при HAS_JOIN — join_sig:64 и join_ts:8, update_sig:64, update_ts:8, userinfo_len:1, userinfo, -adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. +adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. Целые big-endian. Встроенные NUL и хвостовые байты запрещены. get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора. diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index ddae1efc..415b48d6 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -588,7 +588,12 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6 sqlite3_bind_int(u, 3, storage); sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags)); sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id); - sqlite3_step(u); sqlite3_finalize(u); + int rc = sqlite3_step(u); sqlite3_finalize(u); + if (rc != SQLITE_DONE) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member_owner_put: ch=%s node=%016llx rc=%d: %s", + channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db)); + return -1; + } return 0; } @@ -751,70 +756,6 @@ int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, return rc; } -int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, - uint8_t* buf, size_t buf_sz, size_t* out_len) { - if (!db || !channel_id || !buf || !out_len) return -1; - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - - sqlite3_stmt* stmt = NULL; - char sql[512]; - snprintf(sql, sizeof(sql), - "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig" - " FROM \"%s\" ORDER BY node_id ASC", peers_tbl); - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - size_t off = 0; - if (off + 2 > buf_sz) { sqlite3_finalize(stmt); return -2; } - uint16_t* cnt_ptr = (uint16_t*)(buf + off); off += 2; - *cnt_ptr = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - uint64_t node_id = (uint64_t)sqlite3_column_int64(stmt, 0); - const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(stmt, 1); - const uint8_t* ed_pub = (const uint8_t*)sqlite3_column_blob(stmt, 2); - const uint8_t* join_sig = (const uint8_t*)sqlite3_column_blob(stmt, 3); - uint64_t join_ts = (uint64_t)sqlite3_column_int64(stmt, 4); - const uint8_t* update_sig = (const uint8_t*)sqlite3_column_blob(stmt, 5); - uint64_t update_ts = (uint64_t)sqlite3_column_int64(stmt, 6); - const char* puserinfo = (const char*)sqlite3_column_text(stmt, 7); - const char* adm_tags_s = (const char*)sqlite3_column_text(stmt, 8); - const uint8_t* adm_tags_sig_b = (const uint8_t*)sqlite3_column_blob(stmt, 9); - if (!x25519 || !ed_pub) continue; - uint8_t nl = puserinfo ? (uint8_t)strnlen(puserinfo, 255) : 0; - uint8_t atl = adm_tags_s ? (uint8_t)strnlen(adm_tags_s, 255) : 0; - - uint8_t flags = (join_sig && join_ts) ? PEERS_FLAG_HAS_JOIN : 0; - size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64; - if (off + peer_needed > buf_sz) { sqlite3_finalize(stmt); return -2; } - memcpy(buf + off, &node_id, 8); off += 8; - memcpy(buf + off, x25519, 32); off += 32; - memcpy(buf + off, ed_pub, 32); off += 32; - buf[off++] = flags; - if (flags & PEERS_FLAG_HAS_JOIN) { - memcpy(buf + off, join_sig, 64); off += 64; - memcpy(buf + off, &join_ts, 8); off += 8; - } - if (update_sig && update_ts) { - memcpy(buf + off, update_sig, 64); off += 64; - memcpy(buf + off, &update_ts, 8); off += 8; - } else { - memset(buf + off, 0, 64); off += 64; - uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8; - } - buf[off++] = nl; - if (nl) { memcpy(buf + off, puserinfo, nl); off += nl; } - buf[off++] = atl; - if (atl) { memcpy(buf + off, adm_tags_s, atl); off += atl; } - if (adm_tags_sig_b && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, adm_tags_sig_b, 64); off += 64; } - else { memset(buf + off, 0, 64); off += 64; } - (*cnt_ptr)++; - } - sqlite3_finalize(stmt); - *out_len = off; - return 0; -} - int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) { if (!db) return -1; sqlite3_stmt* stmt = NULL; diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index 52a8db11..ddea9f8d 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -83,9 +83,6 @@ int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id, int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, const uint8_t* x25519_priv, const uint8_t* ed25519_priv); -int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, - uint8_t* buf, size_t buf_sz, size_t* out_len); - int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online); int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id); int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]); diff --git a/src/routing_layer/topo_node_sqlite_doc.md b/src/routing_layer/topo_node_sqlite_doc.md index 04ae8db9..4c3d5b7e 100644 --- a/src/routing_layer/topo_node_sqlite_doc.md +++ b/src/routing_layer/topo_node_sqlite_doc.md @@ -11,11 +11,11 @@ 3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()` 4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_`) 5. Добавлять участников в канал через `topo_node_sqlite_member_put()` -6. Читать участников канала через `topo_node_sqlite_channel_peers_all()` — возвращает бинарный буфер со всеми участниками (node_id, ключи, подписи, адреса) +6. Читать участников из `peers_`; постраничная сетевая сериализация принадлежит `member_sync`. **Нюансы:** - Имена таблиц `peers_` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_` -- `topo_node_sqlite_channel_peers_all()` формирует компактный бинарный буфер с числом записей в начале (uint16_t) +- Сетевой формат участников описан в `src/chat/member_sync_doc.md`; SQLite-слой не дублирует сериализатор. - Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта - Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN @@ -31,7 +31,6 @@ | `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_` | | `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) | | `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) | -| `topo_node_sqlite_channel_peers_all(...)` | Сериализует всех участников канала в бинарный буфер (count + [node_id\|x25519\|ed25519\|flags\|sigs\|name\|addrs]) | | `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды | | `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online | | `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) | diff --git a/tests/test_media_delivery_chat.c b/tests/test_media_delivery_chat.c index a6d230c4..29153d74 100644 --- a/tests/test_media_delivery_chat.c +++ b/tests/test_media_delivery_chat.c @@ -408,7 +408,7 @@ static int author_publish_message(void) { /* ── фазы ── */ static void setup_chat_group(void) { - uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ch_sig[64] = {0}; + uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ed_priv[32] = {0}, ch_sig[64] = {0}; int i; TEST("generate channel keys"); { @@ -419,6 +419,7 @@ static void setup_chat_group(void) { ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); } if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l); + l = 32; EVP_PKEY_get_raw_private_key(epkey, ed_priv, &l); EVP_MD_CTX* mctx = EVP_MD_CTX_new(); if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey); size_t sl = 64; EVP_DigestSign(mctx, ch_sig, &sl, (const uint8_t*)CH_ID, strlen(CH_ID)); EVP_MD_CTX_free(mctx); } @@ -440,12 +441,19 @@ static void setup_chat_group(void) { } TEST("add 6 members (s1/s2 supernode, st1/st2 storage)"); { - const char* adm[N_NODES] = { NULL, NULL, "{\"supernode\":\"yes\"}", "{\"supernode\":\"yes\"}", "{\"storage\":\"yes\"}", "{\"storage\":\"yes\"}" }; + const char* adm[N_NODES] = { NULL, NULL, "{\"ver\":\"1\",\"supernode\":\"yes\"}", "{\"ver\":\"1\",\"supernode\":\"yes\"}", + "{\"ver\":\"1\",\"storage\":\"yes\"}", "{\"ver\":\"1\",\"storage\":\"yes\"}" }; for (i = 0; i < N_NODES; i++) { for (int m = 0; m < N_NODES; m++) { - member_sync_put(g_inst[i], CH_ID, g_nid[m], + uint8_t sig[64], msg[MS_ADM_TAGS_MAX + 8]; + if (adm[m]) { + int n = member_sync_build_owner_msg(g_nid[m], adm[m], msg, sizeof(msg)); + if (n < 0 || sc_ed25519_sign(ed_priv, msg, n, sig) != SC_OK) { FAIL("owner signing failed"); return; } + } + int rc = member_sync_put(g_inst[i], CH_ID, g_nid[m], g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, - NULL, 0, NULL, 0, "{\"name\":\"node\"}", adm[m], NULL, 0, 0, NULL); + NULL, 0, NULL, 0, "{\"name\":\"node\"}", adm[m], adm[m] ? sig : NULL, 0, 0, NULL); + if (rc < 0) { FAIL("member insertion failed rc=%d", rc); return; } /* заполняем nodes-таблицу (нужно для верификации Ed25519 подписи блоков/автора) */ topo_node_sqlite_node_update_verified(g_inst[i]->topo_sqlite_db, g_nid[m], "node", g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, diff --git a/tests/test_media_delivery_full.c b/tests/test_media_delivery_full.c index 4c659631..e43db8c6 100644 --- a/tests/test_media_delivery_full.c +++ b/tests/test_media_delivery_full.c @@ -114,7 +114,7 @@ static int msend(struct UTUN_INSTANCE* inst, uint64_t dst, const uint8_t* data, /* ── Phase P1: create CHAT group + members on all nodes ── */ static void setup_chat_group(void) { - uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ch_sig[64] = {0}; + uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ed_priv[32] = {0}, ch_sig[64] = {0}; int i; TEST("generate channel keys"); { @@ -125,6 +125,7 @@ static void setup_chat_group(void) { ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); } if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l); + l = 32; EVP_PKEY_get_raw_private_key(epkey, ed_priv, &l); /* sign channel */ EVP_MD_CTX* mctx = EVP_MD_CTX_new(); if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey); @@ -147,15 +148,21 @@ static void setup_chat_group(void) { } TEST("add all 4 members (n1, n2, s1 supernode, s2 supernode)"); { - const char* adm[4] = { NULL, NULL, "supernode=yes", "supernode=yes" }; + const char* adm[4] = { NULL, NULL, "{\"ver\":\"1\",\"supernode\":\"yes\"}", "{\"ver\":\"1\",\"supernode\":\"yes\"}" }; for (i = 0; i < N_NODES; i++) { sqlite3* db = g_inst[i]->topo_sqlite_db; if (!db) continue; for (int m = 0; m < N_NODES; m++) { - member_sync_put(g_inst[i], CH_ID, g_nid[m], + uint8_t sig[64], msg[MS_ADM_TAGS_MAX + 8]; + if (adm[m]) { + int n = member_sync_build_owner_msg(g_nid[m], adm[m], msg, sizeof(msg)); + if (n < 0 || sc_ed25519_sign(ed_priv, msg, n, sig) != SC_OK) { FAIL("owner signing failed"); return; } + } + int rc = member_sync_put(g_inst[i], CH_ID, g_nid[m], g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, NULL, 0, NULL, 0, "{\"name\":\"node\"}", - adm[m], NULL, 0, 0, NULL); + adm[m], adm[m] ? sig : NULL, 0, 0, NULL); + if (rc < 0) { FAIL("member insertion failed rc=%d", rc); return; } } } int ok = 1; diff --git a/tests/test_member_adapter.c b/tests/test_member_adapter.c index fd1b25fd..a1a93610 100644 --- a/tests/test_member_adapter.c +++ b/tests/test_member_adapter.c @@ -20,10 +20,112 @@ static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) { assert(merkle_sync_read_hash(inst, "1001", 0, 0, hash) == 0); } +static void public_key(const uint8_t priv[32], uint8_t pub[32]) { + EVP_PKEY* pk = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32); assert(pk); + size_t len = 32; assert(EVP_PKEY_get_raw_public_key(pk, pub, &len) == 1); EVP_PKEY_free(pk); +} + +static void test_owner_sizes(struct UASYNC* ua) { + struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); + uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0}; + public_key(priv, pub); public_key(owner_priv, owner_pub); + assert(topo_node_sqlite_channel_put(source.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, owner_priv, zero) == 0); + assert(topo_node_sqlite_channel_put(target.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, NULL, zero) == 0); + uint64_t nid = sc_derive_node_id_from_pubkey(x); + uint8_t join[64], tree[64], owner[64], msg[MS_ADM_TAGS_MAX + 8]; + int n = member_sync_build_join_msg(x, owner_pub, nid, x, 1, msg, sizeof(msg)); assert(n > 0); + assert(sc_ed25519_sign(priv, msg, n, join) == SC_OK); + assert(member_sync_sign_pubkey(owner_priv, x, tree) == 0); + char tags[MS_ADM_TAGS_MAX + 2]; + struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub, + .join_sig = join, .join_ts = 1, .userinfo = "", .adm_tags = tags, .adm_tags_sig = owner, .signature = tree }; + const size_t sizes[] = {191, 192, 255, 256, 4096, MS_ADM_TAGS_MAX}; + uint8_t page[MT_PAGE_SIZE], expected[32], actual[32]; + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + size_t len = sizes[i]; + int prefix = snprintf(tags, sizeof(tags), "{\"ver\":\"%zu\",\"pad\":\"", i + 1); + const char* suffix = "\",\"storage\":\"yes\"}"; + size_t tail = len - strlen(suffix); + memset(tags + prefix, 'a', tail - (size_t)prefix); memcpy(tags + tail, suffix, strlen(suffix) + 1); + n = member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)); assert(n == (int)len + 8); + assert(sc_ed25519_sign(owner_priv, msg, n, owner) == SC_OK); + assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED); + assert(member_sync_verify_local_record(source.topo_sqlite_db, "1001", &rec) == 1); + size_t bytes = sizeof(page); uint64_t next; int more; + assert(_member_get_page(&source, "1001", merkle_sync_level_prefix(nid, 5), 0, 0, page, &bytes, &next, &more) == 0); + assert(next == nid && !more && bytes < MT_PAGE_SIZE); + struct member_wire w; size_t used; + assert(member_decode(page + 2, bytes - 2, &w, &used) == 0 && used == bytes - 2 && !strcmp(w.rec.adm_tags, tags)); + assert(_member_apply_items(&target, "1001", 2, page, bytes) == 0); + root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32)); + sqlite3_stmt* st; + assert(sqlite3_prepare_v2(target.topo_sqlite_db, "SELECT adm_tags,storage FROM peers_1001", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == (int)len); + assert(!strcmp((const char*)sqlite3_column_text(st, 0), tags)); + char storage[8]; + assert(json_flat_get((const char*)sqlite3_column_text(st, 0), "storage", storage, sizeof(storage)) == 0); + assert(!strcmp(storage, "yes")); + sqlite3_finalize(st); + size_t flip = tail - 1; tags[flip] = 'b'; + assert(member_sync_apply_record(&target, "1001", 2, &rec) < 0); + assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) < 0); + assert(member_sync_verify_local_record(target.topo_sqlite_db, "1001", &rec) == 0); + root(&target, actual); assert(!memcmp(expected, actual, 32)); + assert(sqlite3_exec(target.topo_sqlite_db, "SAVEPOINT hash_tail", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_member_owner_put(target.topo_sqlite_db, "1001", nid, tags, owner, 0) == 0); + assert(merkle_sync_recompute_path(&target, "1001", nid) == 1); + root(&target, actual); assert(memcmp(expected, actual, 32)); + assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK TO hash_tail; RELEASE hash_tail", NULL, NULL, NULL) == SQLITE_OK); + tags[flip] = 'a'; + assert(_member_apply_items(&target, "1001", 2, page, bytes - 1) < 0); + size_t length_offset = (size_t)(w.rec.adm_tags_sig - page) - len - 2; + page[length_offset] = 0xff; page[length_offset + 1] = 0xff; + assert(_member_apply_items(&target, "1001", 2, page, bytes) < 0); + } + puts("PASS: full owner signatures and wire roundtrip at 191/192/255/256/4096/8192 bytes; suffix tampering rejected"); + + struct chat_core_ctx cc = { .inst = &source, .db = source.topo_sqlite_db, .initialized = 1 }; + source.chat_core = &cc; + char listing[MT_PAGE_SIZE]; size_t listed; + assert(chat_core_get_members_json(&source, "1001", listing, sizeof(listing), &listed) == 0 && listed > MS_ADM_TAGS_MAX); + struct chat_member_tags* edit = chat_member_tags_load(&source, "1001", nid); assert(edit); + const char* pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096); + assert(chat_member_tags_set(edit, "storage", "no") == 0); + assert(chat_member_tags_commit(&source, edit) == 0); + chat_member_tags_free(edit); + edit = chat_member_tags_load(&source, "1001", nid); assert(edit); + pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096); + assert(!strcmp(chat_member_tags_get(edit, "storage"), "no")); + root(&source, expected); + assert(sqlite3_exec(source.topo_sqlite_db, "CREATE TRIGGER reject_owner BEFORE UPDATE OF adm_tags ON peers_1001 " + "BEGIN SELECT RAISE(ABORT,'injected owner failure'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(chat_member_tags_set(edit, "storage", "yes") == 0); + assert(chat_member_tags_commit(&source, edit) < 0); + root(&source, actual); assert(!memcmp(expected, actual, 32)); + assert(sqlite3_exec(source.topo_sqlite_db, "DROP TRIGGER reject_owner", NULL, NULL, NULL) == SQLITE_OK); + memset(tags, 'x', MS_ADM_TAGS_MAX + 1); tags[MS_ADM_TAGS_MAX + 1] = 0; + assert(member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)) < 0); + assert(chat_member_tags_set(edit, "pad", tags) < 0); + tags[MS_ADM_TAGS_MAX] = 0; + assert(chat_member_tags_set(edit, "pad", tags) == 0 && chat_member_tags_commit(&source, edit) < 0); + root(&source, actual); assert(!memcmp(expected, actual, 32)); + assert(chat_member_tags_set(edit, "pad", "quoted\"\\value\n\r\t\b\f") == 0 && chat_member_tags_commit(&source, edit) == 0); + chat_member_tags_free(edit); + edit = chat_member_tags_load(&source, "1001", nid); assert(edit); + assert(!strcmp(chat_member_tags_get(edit, "pad"), "quoted\"\\value\n\r\t\b\f")); chat_member_tags_free(edit); + char small[8]; size_t output; + assert(chat_core_get_members_json(&source, "1001", small, sizeof(small), &output) < 0 && output == 0); + source.chat_core = NULL; + puts("PASS: long JSON edit/reload; escaping; size and SQLite errors preserve data and tree"); + member_sync_destroy(&source); member_sync_destroy(&target); + assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua); + test_owner_sizes(ua); struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua); put(&source, 1); put(&source, 2);