diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index 0a8be441..61e60309 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -30,6 +30,7 @@ #include #include +#include #include "../../lib/platform_compat.h" /* ─── хелперы ─── */ @@ -379,6 +380,47 @@ static int json_read_text(sqlite3_stmt* st, int column, const char** out) { *out = text; return SQLITE_OK; } +int chat_json_strings(sqlite3* db, const char* text, size_t limit, + int (*visit)(const char* key, const char* value, void* arg), void* arg) { + if (!db || !text || limit > INT_MAX || strnlen(text, limit + 1) > limit) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: invalid arguments/size limit=%zu", limit); return -1; + } + sqlite3_stmt* st = NULL; const char* stage = "validate prepare"; + int rc = sqlite3_prepare_v2(db, "SELECT json_valid(?1),CASE WHEN json_valid(?1) THEN json_type(?1) END", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_ROW) goto error; + const char* type = NULL; rc = json_read_text(st, 1, &type); if (rc != SQLITE_OK) goto error; + if (!sqlite3_column_int(st, 0) || !type || strcmp(type, "object")) { stage = "not a valid object"; rc = SQLITE_CONSTRAINT; goto error; } + rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + stage = "field schema"; + rc = sqlite3_prepare_v2(db, "SELECT count(*),count(DISTINCT key),coalesce(sum(type<>'text'),0) FROM json_each(?1)", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_ROW) goto error; + if (sqlite3_column_int64(st, 0) != sqlite3_column_int64(st, 1) || sqlite3_column_int64(st, 2)) { + stage = "duplicate/nonstring fields"; rc = SQLITE_CONSTRAINT; goto error; + } + rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + stage = "fields"; + rc = sqlite3_prepare_v2(db, "SELECT key,value FROM json_each(?1)", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC); + if (rc != SQLITE_OK) goto error; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + const char *key, *value; rc = json_read_text(st, 0, &key); if (rc == SQLITE_OK) rc = json_read_text(st, 1, &value); + if (rc != SQLITE_OK) goto error; + if (!key || !value || (visit && visit(key, value, arg))) { stage = "field callback"; rc = SQLITE_CONSTRAINT; goto error; } + } + if (rc != SQLITE_DONE) goto error; + stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + return 0; +error: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(db)); + rc = sqlite3_finalize(st); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: cleanup finalize rc=%d", rc); + return -1; +} + /* Сохранённый канал может ещё не иметь history-таблицы при отложенной runtime-подготовке. */ static int json_message_count(sqlite3* db, const char* channel, sqlite3_int64* count) { char table[80], sql[128]; msg_table_name(channel, table, sizeof(table)); sqlite3_stmt* st = NULL; diff --git a/src/chat/chat_core_priv.h b/src/chat/chat_core_priv.h index edfc97bb..b4bc10a8 100644 --- a/src/chat/chat_core_priv.h +++ b/src/chat/chat_core_priv.h @@ -124,6 +124,12 @@ void peers_table_name(const char* ch_id, char* buf, size_t sz); * Использующий модуль логирует ошибку с контекстом своей операции. */ uint64_t chat_channel_id(const char* text); +/* Проверить весь JSON-object<=limit:уникальные ключи,только строковые значения,без скрытого NUL. + * SQLite заимствована;0/-1 с логом,callback может быть NULL. Callback получает строки до следующего step, + * строит только временный результат и возвращает0/-1;при поздней ошибке caller отбрасывает весь результат. */ +int chat_json_strings(sqlite3* db, const char* text, size_t limit, + int (*visit)(const char* key, const char* value, void* arg), void* arg); + /* Байты подписи описания:канонический ID+NUL,имя<=255байт+NUL,owner native-u64,X25519/Ed25519 public[32]. * Возвращает длину/-1;аргументы обязательны,при отказе out не меняется. Общий формат create/JOIN/invite. */ int chat_channel_description_data(const char* channel, const char* name, uint64_t owner, diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 23f04cd3..953a3e31 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -104,26 +104,40 @@ int chat_core_name_userinfo(const char* name, char* out, size_t capacity) { int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity, uint8_t signature[64], uint64_t* timestamp) { - struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->db || !channel || !out || !signature || !timestamp) { + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + if (!cc || !cc->initialized || !cc->db || !chat_channel_id(channel) || !out || !capacity || !signature || !timestamp) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid read request"); return -1; } char table[80], sql[256]; peers_table_name(channel, table, sizeof(table)); - snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts FROM \"%s\" WHERE node_id=?", table); - sqlite3_stmt* st = NULL; int result = -1; - if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); - if (sqlite3_step(st) == SQLITE_ROW) { - const char* json = (const char*)sqlite3_column_text(st, 0); size_t size = json ? strlen(json) : 0; - if (json && size <= MS_USERINFO_MAX && size < capacity && sqlite3_column_bytes(st, 1) == 64 && sqlite3_column_int64(st, 2) > 0) { - memcpy(out, json, size + 1); memcpy(signature, sqlite3_column_blob(st, 1), 64); - *timestamp = (uint64_t)sqlite3_column_int64(st, 2); result = 0; - } - } - sqlite3_finalize(st); + snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts,join_sig,ed25519_pubkey,source FROM \"%s\" WHERE node_id=?", table); + sqlite3_stmt* st = NULL; const char* stage = "prepare"; + char profile[MS_USERINFO_MAX + 1]; uint8_t update[64], join[64], body[MS_USERINFO_MAX + 73]; uint64_t version; + int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) { stage = "bind"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); } + if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); } + if (rc != SQLITE_ROW) goto error; + stage = "fields"; rc = SQLITE_CORRUPT; + int bytes = sqlite3_column_bytes(st, 0); const char* json = (const char*)sqlite3_column_text(st, 0); + const void *sig = sqlite3_column_blob(st, 1), *joined = sqlite3_column_blob(st, 3), *key = sqlite3_column_blob(st, 4); + if (sqlite3_column_type(st, 0) != SQLITE_TEXT || !json || bytes > MS_USERINFO_MAX || strlen(json) != (size_t)bytes || + sqlite3_column_bytes(st, 1) != 64 || !sig || sqlite3_column_bytes(st, 3) != 64 || !joined || + sqlite3_column_bytes(st, 4) != 32 || !key || memcmp(key, inst->my_ed25519_pubkey, 32) || + sqlite3_column_int64(st, 2) <= 0 || sqlite3_column_int(st, 5)) goto error; + if ((size_t)bytes >= capacity) { stage = "capacity"; rc = SQLITE_TOOBIG; goto error; } + memcpy(profile, json, (size_t)bytes + 1); memcpy(update, sig, 64); memcpy(join, joined, 64); + version = (uint64_t)sqlite3_column_int64(st, 2); + stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + int length = member_sync_build_update_msg(join, version, profile, body, sizeof(body)); + if (length < 0 || sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)length, update) != SC_OK || + chat_json_strings(cc->db, profile, MS_USERINFO_MAX, NULL, NULL)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored signature/JSON invalid channel=%s", channel); return -1; } - if (result) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: signed local profile unavailable channel=%s capacity=%zu", channel, capacity); - return result; + memcpy(out, profile, (size_t)bytes + 1); memcpy(signature, update, 64); *timestamp = version; return 0; +error: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read channel=%s stage=%s rc=%d capacity=%zu: %s", channel, stage, rc, capacity, sqlite3_errmsg(cc->db)); + rc = sqlite3_finalize(st); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read cleanup finalize rc=%d", rc); + return -1; } /* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */