fix(chat): verify existing JOIN before signing local profile updates

This commit is contained in:
evgeny committed 2026-10-27 06:27:01 +01:00
1 parent 3195167ff3
commit 6efe3000d0
3 files changed
+42 -12

No files matched your search

+11 -3
View File
@@ -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/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку.
/* Ссылка avatars/<sha256>.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);
+29 -9
View File
@@ -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);
+2
View File
@@ -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,