From e3745f279e29f5207936c6ca922e2e6741776696 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 6 Oct 2026 13:37:18 +0200 Subject: [PATCH] Store signed group avatars without replacing channel keys --- src/chat/chat_core_priv.h | 2 ++ src/chat/group_avatar.h | 30 ++++++++++++++++++++++++++++ src/routing_layer/topo_node_sqlite.c | 16 +++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/chat/group_avatar.h diff --git a/src/chat/chat_core_priv.h b/src/chat/chat_core_priv.h index 78159fd6..ac67a75b 100644 --- a/src/chat/chat_core_priv.h +++ b/src/chat/chat_core_priv.h @@ -77,6 +77,7 @@ static inline size_t b64_decode(const char* src, size_t src_len, uint8_t* dst, s struct ms_props_cbk; /* member_sync.c */ struct ms_apply_cbk; /* member_sync.c */ +struct group_avatar; struct chat_core_ctx { struct UTUN_INSTANCE* inst; @@ -88,6 +89,7 @@ struct chat_core_ctx { /* per-instance sub-module state (was file-scope statics) */ struct ms_props_cbk* props_cbks; /* was member_sync.c g_props_cbks */ struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */ + struct group_avatar* group_avatar; uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */ void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */ int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */ diff --git a/src/chat/group_avatar.h b/src/chat/group_avatar.h new file mode 100644 index 00000000..1e06e0d2 --- /dev/null +++ b/src/chat/group_avatar.h @@ -0,0 +1,30 @@ +/* Подписанная аватарка канала: SQLite, максимум 16 КиБ, router-блоки до 4 КиБ. + * Все API/callbacks — uasync; данные публикуются только после полной проверки подписи. */ +#ifndef GROUP_AVATAR_H +#define GROUP_AVATAR_H +#include +#include +struct UTUN_INSTANCE; +#define GROUP_AVATAR_MAX 16384 +#define GROUP_AVATAR_BLOCK 4096 +struct group_avatar_record { + uint64_t version; + uint16_t size; + uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; +}; +int group_avatar_init(struct UTUN_INSTANCE* inst); +void group_avatar_destroy(struct UTUN_INSTANCE* inst); +void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group); +int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* record); +/* 1=изменено, 0=прежняя/старая запись; <0 — ошибка. */ +int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* record); +/* Публикация своей картинки требует приватный Ed25519 ключ канала. size=0 — подписанное удаление. */ +int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size); +struct group_avatar_gui_request { + struct UTUN_INSTANCE* inst; + uint64_t id, group; + uint16_t size; + uint8_t data[GROUP_AVATAR_MAX]; +}; +void group_avatar_gui_set(void* arg); +#endif diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 6eb90f07..b3bd7f67 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -295,6 +295,10 @@ int topo_node_sqlite_init(sqlite3* db) { " ed25519_pubkey BLOB NOT NULL," // ключ для проверки подписи " ed25519_privkey BLOB," // ключ для создания подписи " signature BLOB NOT NULL," // подпись при создании канал приватным ключом канала channel_id(NUL-terminated) || name(NUL-terminated) || owner_node_id(8 байт, LE uint64) || x25519_pubkey(32 байта) || ed25519_pubkey(32 байта) + " avatar_version INTEGER NOT NULL DEFAULT 0," + " avatar_data BLOB," + " avatar_hash BLOB," + " avatar_signature BLOB," " last_read_msg_id INTEGER," // для скроллинга чата " last_pos_msg_id INTEGER," // для скроллинга чата " last_msg_at INTEGER DEFAULT 0," // время последнего сообщения (unix timestamp) @@ -448,9 +452,14 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, - "INSERT OR REPLACE INTO channels(channel_id, name, owner_node_id," + "INSERT INTO channels(channel_id, name, owner_node_id," " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)" - " VALUES(?,?,?,?,?,?,?,?)", -1, &stmt, NULL) != SQLITE_OK) return -1; + " VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(channel_id) DO UPDATE SET" + " name=excluded.name,owner_node_id=excluded.owner_node_id,signature=excluded.signature," + " x25519_privkey=COALESCE(excluded.x25519_privkey,channels.x25519_privkey)," + " ed25519_privkey=COALESCE(excluded.ed25519_privkey,channels.ed25519_privkey)" + " WHERE channels.x25519_pubkey=excluded.x25519_pubkey AND channels.ed25519_pubkey=excluded.ed25519_pubkey", + -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id); @@ -462,6 +471,9 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, else sqlite3_bind_null(stmt, 7); sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); + if (rc == SQLITE_DONE && !sqlite3_changes(db)) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel: rejected identity change channel=%s", channel_id); return -1; + } return rc == SQLITE_DONE ? 0 : -1; }