diff --git a/src/chat/chat_attachment.c b/src/chat/chat_attachment.c index 2db1d8a5..8821f17e 100644 --- a/src/chat/chat_attachment.c +++ b/src/chat/chat_attachment.c @@ -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 контейнера. */ diff --git a/src/chat/chat_attachment.h b/src/chat/chat_attachment.h index 84af2ec7..24171b52 100644 --- a/src/chat/chat_attachment.h +++ b/src/chat/chat_attachment.h @@ -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);