Index attachment manifests atomically and classify invalid identities

This commit is contained in:
evgeny committed 2026-10-27 02:15:14 +01:00
1 parent fe75a493ef
commit 45d5aaa7a1
2 files changed
+80 -24

No files matched your search

+73 -21
View File
@@ -430,8 +430,8 @@ fail:
/* Запись поступает только из проверенной истории; размер и hash относятся к ciphertext. */
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
uint64_t author, const char* json, size_t length) {
if (!CC(inst) || !json || !ak_channel(channel) || length > 65536) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid observed message"); return -1;
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !json || !message || !author || !ak_channel(channel) || length > 65536) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid observed message/state"); return -2;
}
char* copy = u_malloc(length + 1);
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: message allocation failed"); return -1; }
@@ -440,29 +440,81 @@ int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, con
char* body = strstr(copy, "\"d\":\""); char* end = body ? strchr(body + 5, '"') : NULL;
struct media_index_result result = {0}; struct ak_file job = { .inst = inst };
snprintf(job.channel, sizeof(job.channel), "%s", channel);
int rc = -1;
int result_code = -2, rc = SQLITE_MISMATCH; sqlite3_stmt* st = NULL;
const char* stage = "manifest";
if (!end) goto done;
body += 5; *end = 0;
if (chat_attachment_identity(body, (size_t)(end - body), job.id) || chat_msg_parse_media_body(body, &result) || ak_paths(&job)) goto done;
sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_keys(channel,crypto_id,message_id,author,cipher_hash,cipher_path,plain_path)"
" VALUES(?,?,?,?,?,?,?) ON CONFLICT(channel,crypto_id) DO NOTHING");
if (!st) goto done;
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job.id, 16, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)author);
sqlite3_bind_blob(st, 5, result.content_hash, 32, SQLITE_STATIC); sqlite3_bind_text(st, 6, job.cipher, -1, SQLITE_STATIC);
sqlite3_bind_text(st, 7, job.plain, -1, SQLITE_STATIC); rc = ak_step(inst, st);
if (!rc) {
st = ak_prepare(inst, "SELECT 1 FROM attachment_keys WHERE channel=? AND crypto_id=? AND message_id=? AND author=? AND cipher_hash=?");
if (!st) { rc = -1; goto done; }
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job.id, 16, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)author);
sqlite3_bind_blob(st, 5, result.content_hash, 32, SQLITE_STATIC);
rc = sqlite3_step(st) == SQLITE_ROW ? 0 : -1; sqlite3_finalize(st);
if (chat_attachment_identity(body, (size_t)(end - body), job.id) || chat_msg_parse_media_body(body, &result)) goto done;
result_code = -1;
if (ak_paths(&job)) goto done;
sqlite3* db = CC(inst)->db;
if (!sqlite3_get_autocommit(db)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: observation rejected inside external transaction channel=%s", channel); goto done;
}
stage = "begin observation"; rc = sqlite3_exec(db, "BEGIN IMMEDIATE /* attachment observation */", NULL, NULL, NULL);
if (rc != SQLITE_OK) goto fail;
stage = "prepare identity";
rc = sqlite3_prepare_v2(db, "SELECT message_id,author,cipher_hash FROM attachment_keys WHERE channel=? AND crypto_id=?", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
stage = "bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind identity"; rc = sqlite3_bind_blob(st, 2, job.id, 16, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "read identity"; rc = sqlite3_step(st);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) goto fail;
int exists = rc == SQLITE_ROW, matches = !exists;
if (exists && sqlite3_column_bytes(st, 0) == 32 && sqlite3_column_bytes(st, 2) == 32 &&
sqlite3_column_blob(st, 0) && sqlite3_column_blob(st, 2)) {
matches = (uint64_t)sqlite3_column_int64(st, 1) == author && !memcmp(sqlite3_column_blob(st, 0), message, 32) &&
!memcmp(sqlite3_column_blob(st, 2), result.content_hash, 32);
}
stage = "finalize identity"; rc = sqlite3_finalize(st); st = NULL;
if (rc != SQLITE_OK) goto fail;
if (!matches) { result_code = -2; stage = "colliding identity"; rc = SQLITE_CONSTRAINT; goto fail; }
if (!exists) {
stage = "prepare observation";
rc = sqlite3_prepare_v2(db, "INSERT INTO attachment_keys(channel,crypto_id,message_id,author,cipher_hash,cipher_path,plain_path)"
" VALUES(?,?,?,?,?,?,?)", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
stage = "bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind identity"; rc = sqlite3_bind_blob(st, 2, job.id, 16, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind message"; rc = sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind author"; rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)author);
if (rc != SQLITE_OK) goto fail;
stage = "bind hash"; rc = sqlite3_bind_blob(st, 5, result.content_hash, 32, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind cipher path"; rc = sqlite3_bind_text(st, 6, job.cipher, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "bind plain path"; rc = sqlite3_bind_text(st, 7, job.plain, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto fail;
stage = "insert observation"; rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
int changed = sqlite3_changes(db);
stage = "finalize observation"; rc = sqlite3_finalize(st); st = NULL;
if (rc != SQLITE_OK) goto fail;
if (changed != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: observation rows=%d expected=1 channel=%s", changed, channel);
stage = "row count"; rc = SQLITE_CONSTRAINT; goto fail;
}
}
stage = "commit observation"; rc = sqlite3_exec(db, "COMMIT /* attachment observation */", NULL, NULL, NULL);
if (rc != SQLITE_OK) goto fail;
if (!exists) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: manifest indexed channel=%s author=%llu message=%02x%02x%02x%02x",
channel, (unsigned long long)author, message[0], message[1], message[2], message[3]);
result_code = 0; goto done;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: observation channel=%s stage=%s rc=%d (%s) db=%s",
channel, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
int final_rc = sqlite3_finalize(st);
if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: observation cleanup finalize rc=%d", final_rc);
ak_rollback(inst, "observation", channel);
done:
if (rc) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: rejected manifest/colliding identity channel=%s author=%llu", channel,
(unsigned long long)author);
media_index_result_free(&result); u_free(copy); return rc;
if (result_code) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: manifest not indexed channel=%s author=%llu result=%d stage=%s",
channel, (unsigned long long)author, result_code, stage);
media_index_result_free(&result); u_free(copy); return result_code;
}
/* Кодограммы привязаны к immutable сообщению, UUID и полному hash контейнера. */
+7 -3
View File
@@ -49,14 +49,18 @@ void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий.
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1, id меняется только при успехе. Подпись проверяет вызывающий.
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД.
/* Собственной транзакцией связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1.
* Массивы копируются в БД. Повтор той же пары допустим, её изменение запрещено; отказ сохраняет прежние поля.
* Внешняя транзакция отклоняется без изменений; все SQL-этапы и commit проверяются. */
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
const uint8_t message[32], const uint8_t hash[32]);
// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются.
/* Учесть проверенное db_sync сообщение собственной транзакцией, не меняя ключ/состояние существующей идентичности.
* 0 — записано/точный дубликат/обычное сообщение, -1 — SQL/OOM-отказ для повтора, -2 — неверный manifest/коллизия.
* Внешняя транзакция запрещена. Подпись проверяет вызывающий; JSON заимствован до возврата. */
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
uint64_t author, const char* json, size_t length);