fix(chat): commit avatar updates before notification and validate SQL reads

This commit is contained in:
evgeny committed 2026-10-27 05:08:31 +01:00
1 parent bc41bb68b6
commit e0598ccc22
3 files changed
+212 -108

No files matched your search

+168 -79
View File
@@ -12,6 +12,7 @@
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include <openssl/sha.h>
#include <openssl/crypto.h>
#define GA_STATE 1
#define GA_GET 2
@@ -51,7 +52,8 @@ static int ga_message(uint64_t group, const struct group_avatar_record* record,
}
static int ga_verify(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
if (!inst || !CC(inst) || !group || !r->version || r->version > INT64_MAX || r->size > GROUP_AVATAR_MAX) {
if (!inst || !CC(inst) || !CC(inst)->db || !group || group > INT64_MAX || !r ||
!r->version || r->version > INT64_MAX || r->size > GROUP_AVATAR_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: invalid record group=%llu", (unsigned long long)group); return -1;
}
uint8_t hash[32], pub[32], bytes[GROUP_AVATAR_MAX + 64]; char ch[32]; ga_channel(group, ch);
@@ -72,31 +74,40 @@ static int ga_verify(struct UTUN_INSTANCE* inst, uint64_t group, const struct gr
}
int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* r) {
if (!inst || !CC(inst) || !CC(inst)->db || !r) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid read"); return -1; }
memset(r, 0, sizeof(*r)); char ch[32]; ga_channel(group, ch); sqlite3_stmt* st = NULL;
const char* sql = "SELECT avatar_version,avatar_data,avatar_hash,avatar_signature FROM channels WHERE channel_id=?";
int rc = -1;
if (sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, ch, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
r->version = (uint64_t)sqlite3_column_int64(st, 0); int size = sqlite3_column_bytes(st, 1);
if ((!r->version && !size && !sqlite3_column_bytes(st, 2) && !sqlite3_column_bytes(st, 3)) ||
(r->version && r->version <= INT64_MAX && size <= GROUP_AVATAR_MAX &&
sqlite3_column_bytes(st, 2) == 32 && sqlite3_column_bytes(st, 3) == 64)) {
r->size = (uint16_t)size;
if (r->version) {
if (size) memcpy(r->data, sqlite3_column_blob(st, 1), size);
memcpy(r->hash, sqlite3_column_blob(st, 2), 32); memcpy(r->signature, sqlite3_column_blob(st, 3), 64);
}
rc = 0;
}
}
sqlite3_finalize(st);
if (!inst || !CC(inst) || !CC(inst)->db || !group || group > INT64_MAX || !r) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid read group=%llu", (unsigned long long)group); return -1;
}
if (!rc && r->version) rc = ga_verify(inst, group, r);
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: stored record unavailable/invalid group=%llu: %s",
(unsigned long long)group, sqlite3_errmsg(CC(inst)->db));
return rc;
struct group_avatar_record result = {0}; char ch[32]; ga_channel(group, ch); sqlite3_stmt* st = NULL;
const char* sql = "SELECT avatar_version,avatar_data,avatar_hash,avatar_signature FROM channels WHERE channel_id=?";
sqlite3* db = CC(inst)->db; const char* stage = "prepare";
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc != SQLITE_OK) goto done;
stage = "bind"; rc = sqlite3_bind_text(st, 1, ch, -1, SQLITE_STATIC); if (rc != SQLITE_OK) goto done;
stage = "step"; rc = sqlite3_step(st); if (rc != SQLITE_ROW) goto done;
result.version = (uint64_t)sqlite3_column_int64(st, 0); int size = sqlite3_column_bytes(st, 1);
stage = "record fields"; rc = SQLITE_CORRUPT;
if (sqlite3_column_type(st, 0) != SQLITE_INTEGER || result.version > INT64_MAX || size > GROUP_AVATAR_MAX) goto done;
if (!result.version) {
if (size || sqlite3_column_bytes(st, 2) || sqlite3_column_bytes(st, 3)) goto done;
} else {
if (sqlite3_column_type(st, 1) != SQLITE_BLOB || sqlite3_column_type(st, 2) != SQLITE_BLOB ||
sqlite3_column_type(st, 3) != SQLITE_BLOB || sqlite3_column_bytes(st, 2) != 32 || sqlite3_column_bytes(st, 3) != 64) goto done;
const void *data = sqlite3_column_blob(st, 1), *hash = sqlite3_column_blob(st, 2), *signature = sqlite3_column_blob(st, 3);
if ((size && !data) || !hash || !signature) { rc = SQLITE_NOMEM; goto done; }
result.size = (uint16_t)size; if (size) memcpy(result.data, data, (size_t)size);
memcpy(result.hash, hash, 32); memcpy(result.signature, signature, 64);
}
rc = SQLITE_OK;
done:
{
int final = sqlite3_finalize(st);
if (rc != SQLITE_OK || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: read group=%llu stage=%s rc=%d finalize=%d: %s",
(unsigned long long)group, stage, rc, final, sqlite3_errmsg(db)); return -1;
}
}
if (result.version && ga_verify(inst, group, &result) < 0) return -1;
*r = result; return 0;
}
static int ga_compare(uint64_t a, const uint8_t ah[32], uint64_t b, const uint8_t bh[32]) {
@@ -112,59 +123,126 @@ static void ga_changed(struct UTUN_INSTANCE* inst, uint64_t group) {
memcpy(event + 1, ch, event[0]); chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, event, event[0] + 1);
}
int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
/* Блокировка охватывает проверку допуска/версии и запись; callback здесь не вызывается. */
static int ga_begin(struct UTUN_INSTANCE* inst, uint64_t group) {
if (!inst || !CC(inst) || !CC(inst)->initialized || !CC(inst)->db || !group || group > INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: inactive core/invalid group=%llu", (unsigned long long)group); return -1;
}
sqlite3* db = CC(inst)->db;
if (!sqlite3_get_autocommit(db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: external transaction rejected group=%llu", (unsigned long long)group); return -1;
}
int rc = sqlite3_exec(db, "BEGIN IMMEDIATE /* group avatar */", NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: begin group=%llu rc=%d: %s",
(unsigned long long)group, rc, sqlite3_errmsg(db));
return rc == SQLITE_OK ? 0 : -1;
}
static int ga_finish(struct UTUN_INSTANCE* inst, uint64_t group, int result) {
sqlite3* db = CC(inst)->db; int rc;
if (result > 0) {
rc = sqlite3_exec(db, "COMMIT /* group avatar */", NULL, NULL, NULL);
if (rc == SQLITE_OK) return result;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: commit group=%llu rc=%d: %s",
(unsigned long long)group, rc, sqlite3_errmsg(db)); result = -1;
}
if (!sqlite3_get_autocommit(db)) {
rc = sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: rollback group=%llu rc=%d: %s",
(unsigned long long)group, rc, sqlite3_errmsg(db)); result = -1;
}
if (!sqlite3_get_autocommit(db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: transaction remains open group=%llu; requesting owner stop/DB close",
(unsigned long long)group); utun_instance_stop(inst); result = -1;
}
}
return result;
}
static int ga_write(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; char ch[32]; ga_channel(group, ch);
const char* sql = "UPDATE channels SET avatar_version=?,avatar_data=?,avatar_hash=?,avatar_signature=? WHERE channel_id=?";
const char* stage = "prepare"; int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) { stage = "bind version"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)r->version); }
if (rc == SQLITE_OK) { stage = "bind data"; rc = sqlite3_bind_blob(st, 2, r->data, r->size, SQLITE_STATIC); }
if (rc == SQLITE_OK) { stage = "bind hash"; rc = sqlite3_bind_blob(st, 3, r->hash, 32, SQLITE_STATIC); }
if (rc == SQLITE_OK) { stage = "bind signature"; rc = sqlite3_bind_blob(st, 4, r->signature, 64, SQLITE_STATIC); }
if (rc == SQLITE_OK) { stage = "bind channel"; rc = sqlite3_bind_text(st, 5, ch, -1, SQLITE_STATIC); }
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
if (rc == SQLITE_DONE && sqlite3_changes(db) != 1) { stage = "changes != 1"; rc = SQLITE_CONSTRAINT; }
int final = sqlite3_finalize(st);
if (rc != SQLITE_DONE || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: write group=%llu stage=%s rc=%d finalize=%d: %s",
(unsigned long long)group, stage, rc, final, sqlite3_errmsg(db)); return -1;
}
return 1;
}
/* Приём завершает всю работу с ga_peer до публикации события, способного удалить группу. */
static int ga_apply_transaction(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: missing candidate record"); return -1; }
if (ga_verify(inst, group, r) < 0) return -1;
struct group_avatar_record current;
if (group_avatar_get(inst, group, &current) < 0) return -1;
if (ga_begin(inst, group) < 0) return -1;
int result = -1; struct group_avatar_record current;
if (ga_verify(inst, group, r) < 0 || group_avatar_get(inst, group, &current) < 0) goto done;
int newer = ga_compare(r->version, r->hash, current.version, current.hash);
if (r->version == current.version && memcmp(r->hash, current.hash, 32))
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: concurrent versions group=%llu version=%llu accept=%d",
(unsigned long long)group, (unsigned long long)r->version, newer > 0);
if (newer <= 0) return 0;
sqlite3_stmt* st = NULL; char ch[32]; ga_channel(group, ch); int rc = -1;
const char* sql = "UPDATE channels SET avatar_version=?,avatar_data=?,avatar_hash=?,avatar_signature=? WHERE channel_id=?";
if (sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)r->version); sqlite3_bind_blob(st, 2, r->data, r->size, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, r->hash, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 4, r->signature, 64, SQLITE_STATIC);
sqlite3_bind_text(st, 5, ch, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_DONE && sqlite3_changes(CC(inst)->db) == 1) rc = 1;
sqlite3_finalize(st);
}
if (rc < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: database update failed group=%llu: %s",
(unsigned long long)group, sqlite3_errmsg(CC(inst)->db));
else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: saved group=%llu version=%llu bytes=%u",
(unsigned long long)group, (unsigned long long)r->version, r->size); ga_changed(inst, group);
}
return rc;
result = newer > 0 ? ga_write(inst, group, r) : 0;
done:
result = ga_finish(inst, group, result);
if (result > 0) DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: saved group=%llu version=%llu bytes=%u committed=1",
(unsigned long long)group, (unsigned long long)r->version, r->size);
return result;
}
int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
int result = ga_apply_transaction(inst, group, r);
if (result > 0) ga_changed(inst, group);
return result;
}
int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size) {
if (!inst || !CC(inst) || !CC(inst)->initialized || !CC(inst)->db || size > GROUP_AVATAR_MAX ||
(size && !data) || !member_files_member(inst, group, inst->node_id, 0)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication denied group=%llu bytes=%zu", (unsigned long long)group, size); return -1;
}
uint8_t key[32], bytes[GROUP_AVATAR_MAX + 64]; char ch[32]; ga_channel(group, ch);
if (topo_node_sqlite_channel_get_priv(CC(inst)->db, ch, key) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires group key group=%llu", (unsigned long long)group); return -1;
if (size > GROUP_AVATAR_MAX || (size && !data)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: invalid publication group=%llu bytes=%zu", (unsigned long long)group, size); return -1;
}
if (ga_begin(inst, group) < 0) return -1;
int result = -1; uint8_t key[32] = {0}, bytes[GROUP_AVATAR_MAX + 64]; char ch[32]; ga_channel(group, ch);
struct group_avatar_record r;
if (group_avatar_get(inst, group, &r) < 0 || r.version == INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: cannot allocate version group=%llu", (unsigned long long)group); return -1;
if (!member_files_member(inst, group, inst->node_id, 0)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires active membership group=%llu", (unsigned long long)group); goto done;
}
if (topo_node_sqlite_channel_get_priv(CC(inst)->db, ch, key) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires group key group=%llu", (unsigned long long)group); goto done;
}
if (group_avatar_get(inst, group, &r) < 0) goto done;
uint8_t hash[32]; SHA256(size ? data : r.data, size, hash);
if (r.version && r.size == size && !memcmp(r.hash, hash, 32)) return 0;
if (r.version && r.size == size && !memcmp(r.hash, hash, 32)) { result = 0; goto done; }
if (r.version == INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: version exhausted group=%llu", (unsigned long long)group); goto done;
}
int64_t us = ntp_time_get_us(inst); uint64_t now = us > 0 ? (uint64_t)us / 1000 : 0;
r.version = now > r.version ? now : r.version + 1; r.size = (uint16_t)size;
memcpy(r.hash, hash, 32); if (size) memcpy(r.data, data, size);
int n = ga_message(group, &r, bytes); int signed_ok = sc_ed25519_sign(key, bytes, n, r.signature); memset(key, 0, sizeof(key));
if (signed_ok != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: signing failed group=%llu", (unsigned long long)group); return -1; }
return group_avatar_apply(inst, group, &r) > 0 ? 0 : -1;
int n = ga_message(group, &r, bytes);
if (sc_ed25519_sign(key, bytes, (size_t)n, r.signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: signing failed group=%llu", (unsigned long long)group); goto done;
}
if (ga_verify(inst, group, &r) < 0) goto done;
result = ga_write(inst, group, &r);
done:
OPENSSL_cleanse(key, sizeof(key)); result = ga_finish(inst, group, result);
if (result > 0) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: saved group=%llu version=%llu bytes=%u committed=1",
(unsigned long long)group, (unsigned long long)r.version, r.size); ga_changed(inst, group);
}
return result < 0 ? -1 : 0;
}
void group_avatar_gui_set(void* arg) {
struct group_avatar_gui_request* q = arg;
if (!q || !q->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid GUI request"); u_free(q); return; }
int rc = group_avatar_set(q->inst, q->group, q->data, q->size);
uint8_t result[17]; mf_put64(result, q->id); mf_put64(result + 8, q->group); result[16] = rc ? 1 : 0;
chat_event_post(q->inst, CHAT_EVT_GROUP_AVATAR_RESULT, result, sizeof(result)); u_free(q);
@@ -337,9 +415,10 @@ static void ga_receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: block received group=%llu peer=%016llx version=%llu offset=%u bytes=%u total=%u",
(unsigned long long)group, (unsigned long long)peer, (unsigned long long)version, offset, block, size);
if (p->rx_offset == size) {
int applied = group_avatar_apply(ctx->inst, group, p->rx); ga_release(p, 0);
int applied = ga_apply_transaction(ctx->inst, group, p->rx); ga_release(p, 0);
p->requested_at = applied < 0 ? get_time_tb() : 0;
p->state_pending = 1; ga_schedule(p);
if (applied > 0) ga_changed(ctx->inst, group);
}
goto out;
malformed:
@@ -386,29 +465,39 @@ static void ga_tick(void* arg) {
static int ga_schema(sqlite3* db) {
const char* names[] = {"avatar_version", "avatar_data", "avatar_hash", "avatar_signature"}; int present[4] = {0};
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, "PRAGMA table_info(channels)", -1, &st, NULL) != SQLITE_OK) goto error;
while (sqlite3_step(st) == SQLITE_ROW) for (unsigned i = 0; i < 4; i++)
if (!strcmp((const char*)sqlite3_column_text(st, 1), names[i])) present[i] = 1;
sqlite3_finalize(st);
if (sqlite3_exec(db, "SAVEPOINT group_avatar_schema", NULL, NULL, NULL) != SQLITE_OK) goto error;
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema database missing"); return -1; }
sqlite3_stmt* st = NULL; const char* stage = "savepoint"; int started = 0, added = 0;
int rc = sqlite3_exec(db, "SAVEPOINT group_avatar_schema", NULL, NULL, NULL);
if (rc != SQLITE_OK) goto error;
started = 1; stage = "table info prepare"; rc = sqlite3_prepare_v2(db, "PRAGMA table_info(channels)", -1, &st, NULL);
if (rc != SQLITE_OK) goto error;
stage = "table info step";
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char* name = (const char*)sqlite3_column_text(st, 1);
if (!name) { rc = SQLITE_CORRUPT; goto error; }
for (unsigned i = 0; i < 4; i++) if (!strcmp(name, names[i])) present[i] = 1;
}
if (rc != SQLITE_DONE) goto error;
stage = "table info finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
for (unsigned i = 0; i < 4; i++) if (!present[i]) {
char sql[128]; snprintf(sql, sizeof(sql), "ALTER TABLE channels ADD COLUMN %s %s", names[i], i ? "BLOB" : "INTEGER NOT NULL DEFAULT 0");
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: cannot add schema column=%s: %s", names[i], sqlite3_errmsg(db));
if (sqlite3_exec(db, "ROLLBACK TO group_avatar_schema; RELEASE group_avatar_schema", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema rollback failed: %s", sqlite3_errmsg(db));
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: added schema column=%s", names[i]);
stage = names[i]; rc = sqlite3_exec(db, sql, NULL, NULL, NULL); if (rc != SQLITE_OK) goto error;
added++;
}
if (sqlite3_exec(db, "RELEASE group_avatar_schema", NULL, NULL, NULL) == SQLITE_OK) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema commit failed: %s", sqlite3_errmsg(db));
if (sqlite3_exec(db, "ROLLBACK TO group_avatar_schema; RELEASE group_avatar_schema", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema rollback after commit error failed: %s", sqlite3_errmsg(db));
return -1;
stage = "release"; rc = sqlite3_exec(db, "RELEASE group_avatar_schema", NULL, NULL, NULL); if (rc != SQLITE_OK) goto error;
if (added) DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: schema ready added_columns=%d outer_transaction=%d", added, !sqlite3_get_autocommit(db));
return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema initialization failed: %s", sqlite3_errmsg(db)); return -1;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema initialization stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema cleanup finalize rc=%d", rc);
if (started && !sqlite3_get_autocommit(db)) {
rc = sqlite3_exec(db, "ROLLBACK TO group_avatar_schema", NULL, NULL, NULL);
if (rc == SQLITE_OK) rc = sqlite3_exec(db, "RELEASE group_avatar_schema", NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema rollback rc=%d;owner must rollback/close DB: %s",
rc, sqlite3_errmsg(db));
}
return -1;
}
int group_avatar_init(struct UTUN_INSTANCE* inst) {
+40 -27
View File
@@ -10,6 +10,7 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <openssl/crypto.h>
#define PEERS_JOIN_SIG_SIZE 64
#define PEERS_JOIN_TS_SIZE 8
@@ -984,38 +985,50 @@ done:
return result;
}
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
uint8_t* ed25519_priv_out) {
if (!db || !channel_id || !ed25519_priv_out) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
const uint8_t* blob = (const uint8_t*)sqlite3_column_blob(stmt, 0);
if (!blob || sqlite3_column_bytes(stmt, 0) < 32) { sqlite3_finalize(stmt); return -1; }
memcpy(ed25519_priv_out, blob, 32);
sqlite3_finalize(stmt);
return 0;
/* Выходные ключи публикуются только после успешного завершения SELECT. */
static int channel_private_keys(sqlite3* db, const char* channel, uint8_t* x25519, uint8_t* ed25519) {
if (!db || !channel || !ed25519) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel private keys: missing argument"); return -1;
}
const char* sql = x25519 ? "SELECT x25519_privkey, ed25519_privkey FROM channels WHERE channel_id=?" :
"SELECT ed25519_privkey FROM channels WHERE channel_id=?";
sqlite3_stmt* st = NULL; uint8_t keys[64] = {0}; int columns = x25519 ? 2 : 1, result = -1;
const char* stage = "prepare"; int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc != SQLITE_OK) goto done;
stage = "bind"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); if (rc != SQLITE_OK) goto done;
stage = "step"; rc = sqlite3_step(st); if (rc != SQLITE_ROW) goto done;
for (int i = 0; i < columns; i++) {
if (sqlite3_column_type(st, i) == SQLITE_NULL) { rc = SQLITE_DONE; goto done; }
stage = "key size/type"; rc = SQLITE_CORRUPT;
if (sqlite3_column_type(st, i) != SQLITE_BLOB || sqlite3_column_bytes(st, i) != 32) goto done;
const void* key = sqlite3_column_blob(st, i); if (!key) { rc = SQLITE_NOMEM; goto done; }
memcpy(keys + i * 32, key, 32);
}
rc = SQLITE_OK;
done:
{
int final = sqlite3_finalize(st);
if (rc == SQLITE_OK && final == SQLITE_OK) {
if (x25519) memcpy(x25519, keys, 32);
memcpy(ed25519, keys + (columns - 1) * 32, 32); result = 0;
} else if (rc == SQLITE_DONE && final == SQLITE_OK) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "channel private keys unavailable channel=%s", channel);
} else DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel private keys channel=%s stage=%s rc=%d finalize=%d: %s",
channel, stage, rc, final, sqlite3_errmsg(db));
}
OPENSSL_cleanse(keys, sizeof(keys)); return result;
}
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, uint8_t* ed25519_priv_out) {
return channel_private_keys(db, channel_id, NULL, ed25519_priv_out);
}
int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out) {
if (!db || !channel_id || !x25519_priv_out || !ed25519_priv_out) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT x25519_privkey, ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(stmt, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(stmt, 1);
if (!x || sqlite3_column_bytes(stmt, 0) < 32 || !e || sqlite3_column_bytes(stmt, 1) < 32) {
sqlite3_finalize(stmt); return -1;
if (!x25519_priv_out) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel private keys: X25519 output missing"); return -1;
}
memcpy(x25519_priv_out, x, 32);
memcpy(ed25519_priv_out, e, 32);
sqlite3_finalize(stmt);
return 0;
return channel_private_keys(db, channel_id, x25519_priv_out, ed25519_priv_out);
}
/* Импорт пары: savepoint охватывает UPDATE и его завершение, внешний commit остаётся вызывающему. */
+4 -2
View File
@@ -122,11 +122,13 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature);
/* Прочитать первые 32 байта Ed25519 privkey; 0/-1, отсутствующий/короткий ключ даёт -1. */
/* Прочитать Ed25519 privkey ровно32 байта BLOB; 0/-1, отсутствие/повреждение/SQL-ошибка даёт -1.
* Проверяются bind/step/finalize; при отказе выход не меняется. */
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
uint8_t* ed25519_priv_out);
/* Прочитать оба канальных privkey (x25519 + ed25519). Возвращает 0 если оба есть. */
/* Прочитать оба канальных privkey (x25519 + ed25519), каждый ровно32 байта BLOB.
* 0 — оба прочитаны, -1 — отказ с сохранением обоих выходов. Временные ключи очищаются. */
int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out);