diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index fb6ce509..dfe282cd 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -300,17 +300,25 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch * out/sz>0 обязательны;отсутствие узла и ошибка оставляют пустую строку (если буфер допустим). */ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz); -// Ссылка на описание avatars/.json внутри дерева своего UID/канала. server=0 удаляет ссылку. +/* Ссылка avatars/.json внутри дерева своего UID/канала;server=0 удаляет ссылку,file остаётся обязательной NUL-строкой. + * 0 — committed/семантически прежнее,-1 — отказ;чужая транзакция запрещена. Storage-роль сервера проверяется под блокировкой. */ int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file); +/* Построить {name:string},включая Unicode/control escaping;0/-1,при ошибке out не меняется. */ int chat_core_name_userinfo(const char* name, char* out, size_t capacity); +/* Прочитать собственный подписанный профиль:0/-1,при отказе все выходы сохраняются. + * Проверяет SQL,размеры,JOIN/update-подписи и целый плоский JSON с уникальными строковыми полями. + * out требует capacity>=длина+1,signature64,timestamp — версия update_ts в секундах/монотонных шагах. */ 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); /* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ. - * Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle. - * Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */ + * Остальные подписанные поля сохраняются;неподписанные старые атрибуты не авторизуются новым обновлением. + * Имя/аватар меняются своими API. Чтение/проверка/версия/запись/Merkle защищены одним BEGIN IMMEDIATE. + * Семантический no-op сохраняет подпись/версию независимо от порядка ключей/escaping;ошибка не меняет БД/Merkle. + * Уведомления идут после commit,без SQLite cursor. Вызов в uasync вне чужой транзакции; + * неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. */ struct chat_profile_option { const char* key; const char* value; }; int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_profile_option* options, size_t count); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index b3141fb5..496d491d 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -96,16 +96,30 @@ int chat_core_name_userinfo(const char* name, char* out, size_t capacity) { p.text[p.used++] = '}'; p.text[p.used] = 0; memcpy(out, p.text, p.used + 1); return 0; } +/* Локальная запись member_sync доверяет caller:перед правкой проверяем прежний JOIN явно. */ +static int profile_verify_join(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t join[64], uint64_t timestamp) { + uint8_t x[32], ed[32], body[112]; + if (topo_node_sqlite_channel_get(CC(inst)->db, channel, NULL, 0, NULL, x, ed, NULL)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: channel keys unavailable ch=%s", channel); return -1; + } + int bytes = member_sync_build_join_msg(x, ed, inst->node_id, inst->my_keys.public_key, timestamp, body, sizeof(body)); + if (bytes < 0 || sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)bytes, join) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored JOIN invalid ch=%s", channel); return -1; + } + return 0; +} + 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 = inst ? CC(inst) : NULL; - if (!cc || !cc->initialized || !cc->db || !chat_channel_id(channel) || !out || !capacity || !signature || !timestamp) { + if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !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,join_sig,ed25519_pubkey,source FROM \"%s\" WHERE node_id=?", table); + snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts,join_sig,ed25519_pubkey,source,join_ts 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; + char profile[MS_USERINFO_MAX + 1]; uint8_t update[64], join[64], body[MS_USERINFO_MAX + 73]; uint64_t version, join_ts; 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); } @@ -114,15 +128,18 @@ int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, ch 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; + sqlite3_column_type(st, 1) != SQLITE_BLOB || sqlite3_column_bytes(st, 1) != 64 || !sig || + sqlite3_column_type(st, 3) != SQLITE_BLOB || sqlite3_column_bytes(st, 3) != 64 || !joined || + sqlite3_column_type(st, 4) != SQLITE_BLOB || sqlite3_column_bytes(st, 4) != 32 || !key || memcmp(key, inst->my_ed25519_pubkey, 32) || + sqlite3_column_type(st, 2) != SQLITE_INTEGER || sqlite3_column_int64(st, 2) <= 0 || sqlite3_column_int(st, 5) || + sqlite3_column_type(st, 6) != SQLITE_INTEGER || sqlite3_column_int64(st, 6) <= 0) 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); + version = (uint64_t)sqlite3_column_int64(st, 2); join_ts = (uint64_t)sqlite3_column_int64(st, 6); 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 || + if (length < 0 || profile_verify_join(inst, channel, join, join_ts) || + 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; } @@ -173,6 +190,7 @@ static int profile_read_current(struct UTUN_INSTANCE* inst, const char* channel, memcpy(out->json, json, (size_t)bytes + 1); memcpy(out->join, join, 64); found = 1; done: stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + if (found && profile_verify_join(inst, channel, out->join, out->join_ts)) return -1; if (found && out->signed_profile) { uint8_t body[MS_USERINFO_MAX + 73]; int bytes = member_sync_build_update_msg(out->join, out->version, out->json, body, sizeof(body)); @@ -343,7 +361,9 @@ void chat_core_set_my_avatar_trampoline(void* arg) { struct chat_avatar_req* q = arg; if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: null avatar request"); return; } char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)q->group); - int rc = chat_core_set_my_avatar(q->inst, ch, q->server, q->file); + int rc = -1; + if (!memchr(q->file, 0, sizeof(q->file))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: unterminated avatar request"); + else rc = chat_core_set_my_avatar(q->inst, ch, q->server, q->file); uint8_t data[17]; mf_put64(data, q->id); mf_put64(data + 8, q->group); data[16] = rc ? 1 : 0; chat_event_post(q->inst, CHAT_EVT_AVATAR_RESULT, data, sizeof(data)); u_free(q); diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index 861737eb..e6abca94 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -210,6 +210,8 @@ int member_sync_build_join_msg(const uint8_t* ch_x25519, const uint8_t* ch_ed25 * MS_APPLY_STALE (локальная версия новее; её доставит обратный проход). * При внешней транзакции caller после commit вызывает member_sync_record_committed * для изменённой записи; до commit автоматических уведомлений нет. + * Отказ ROLLBACK TO не выполняет RELEASE частичных изменений. Владелец внешней транзакции + * при ошибке откатывает её целиком;собственная транзакция пытается полный ROLLBACK,а при его отказе запрашивает stop/DB close. * Возвращает битовую маску MS_APPLY_* (0 — без изменения индекса), <0 — ошибка. */ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,