fix(chat): verify existing JOIN before signing local profile updates
This commit is contained in:
1 parent
3195167ff3
commit
6efe3000d0
3 files changed
+42
-12
No files matched your search
+11
-3
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user