Commit verified file receipts separately from message delivery

This commit is contained in:
evgeny committed 2026-11-03 12:54:43 +01:00
1 parent 9ba34b0faa
commit 5013fc22d8
2 files changed
+20 -42

No files matched your search

+4
View File
@@ -217,6 +217,8 @@ utun_CORE_SOURCES = \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_media_receipt.c \
dm/dm_media_receipt.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
@@ -424,6 +426,8 @@ libutun_a_SOURCES = \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_media_receipt.c \
dm/dm_media_receipt.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
+16 -42
View File
@@ -12,6 +12,7 @@
#include "../../lib/debug_config.h"
#include "../../lib/json_flat.h"
#include "dm_media_priv.h"
#include "dm_media_receipt.h"
#include "dm_history.h"
#include "dm_crypto.h"
#include "dm_mailbox.h"
@@ -347,35 +348,6 @@ void dm_media_notify(struct UTUN_INSTANCE* inst, uint64_t conv) {
chat_event_post(inst, CHAT_EVT_DM_MSG_RECEIVED, evt, n + 9);
}
/* Квитанция подписывает UUID и хеш всего сообщения с descriptor. */
int dm_media_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t* r, const uint8_t* msg, size_t len) {
uint64_t conv, author, recipient, seq;
if (!inst || !r || memcmp(r, "DMFILE01", 8)) goto invalid;
memcpy(&conv, r + 8, 8); memcpy(&seq, r + 16, 8);
memcpy(&author, r + 24, 8); memcpy(&recipient, r + 32, 8);
if (!author || !recipient || author == recipient || !seq || seq > INT64_MAX ||
conv != dm_derive_conv_id(author, recipient)) goto invalid;
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT ed25519_pubkey FROM nodes WHERE node_id=?");
if (!st) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
uint8_t key[32];
int found = sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32;
if (found) memcpy(key, sqlite3_column_blob(st, 0), sizeof(key));
sqlite3_finalize(st);
if (!found || sc_ed25519_verify(key, r, 88, r + 88) != SC_OK) goto invalid;
if (msg) {
const uint8_t* desc = dm_message_media(msg, len);
uint8_t hash[32];
if (!desc || memcmp(r + 8, msg, 16) || memcmp(r + 24, msg + 24, 8) || memcmp(r + 40, desc, 16)) goto invalid;
SHA256(msg, len, hash);
if (memcmp(hash, r + 56, 32)) goto invalid;
}
return 0;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_media: rejected media receipt body_bytes=%zu", len);
return -1;
}
/* Транзакция dm_core связывает историю, очередь и состояние файла. */
int dm_media_track(struct UTUN_INSTANCE* inst, int dir, const uint8_t* msg, size_t len) {
const uint8_t* desc = dm_message_media(msg, len);
@@ -421,7 +393,7 @@ int dm_media_track(struct UTUN_INSTANCE* inst, int dir, const uint8_t* msg, size
int exact = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == (int)len && !memcmp(sqlite3_column_blob(st, 0), msg, len);
sqlite3_finalize(st);
if (!exact) DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: conflicting UUID/body conv=%s seq=%llu", id, (unsigned long long)seq);
return exact ? 0 : -1;
return exact ? dm_media_targets_seed(inst, msg, len, recipient) : -1;
}
/* Квитанция endpoint удаляет только media pending, история/собственный файл остаются. */
@@ -449,13 +421,14 @@ int dm_media_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t* r) {
if (exact) { DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM files: late receipt for deleted file conv=%s", id); return 0; }
DEBUG_WARN(DEBUG_CATEGORY_DM, "DM files: receipt for unknown/conflicting deleted file conv=%s", id); return -1;
}
if (rc != SQLITE_ROW || dm_media_verify_receipt(inst, r, sqlite3_column_blob(st, 0), (size_t)sqlite3_column_bytes(st, 0))) {
if (rc != SQLITE_ROW || dm_media_receipt_save(inst, r, sqlite3_column_blob(st, 0), (size_t)sqlite3_column_bytes(st, 0))) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_media: receipt does not match outgoing artifact rc=%d", rc);
sqlite3_finalize(st);
return -1;
}
int already = sqlite3_column_bytes(st, 1) == DM_MEDIA_RECEIPT_SIZE;
sqlite3_finalize(st);
if (dm_media_receipt_kind(r) == 1) return 0;
st = dm_media_prepare(inst, "UPDATE dm_files SET receipt=? WHERE id=? AND dir=1");
if (!st) return -1;
sqlite3_bind_blob(st, 1, r, DM_MEDIA_RECEIPT_SIZE, SQLITE_STATIC); sqlite3_bind_blob(st, 2, r + 40, 16, SQLITE_STATIC);
@@ -572,6 +545,11 @@ static void dm_file_work(void* arg) {
job->error = media_encrypt_stream(job->key, job->author, job->id, in, out, &job->size, job->hash);
} else if (job->work == DM_FILE_RECEIVE) {
job->error = media_decrypt_stream(job->key, job->author, job->id, job->size, job->hash, in, out);
if (!job->error) {
FILE* durable = fopen(job->cipher, "rb+");
if (!durable) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: received ciphertext sync open failed"); job->error = -1; }
else { job->error = dm_file_sync(durable); if (fclose(durable)) job->error = -1; }
}
} else {
SHA256_CTX hash;
SHA256_Init(&hash);
@@ -658,22 +636,18 @@ static void dm_file_done(void* arg, int error) {
goto fail;
}
uint8_t receipt[DM_MEDIA_RECEIPT_SIZE];
if (!custody) {
memcpy(receipt, "DMFILE01", 8); memcpy(receipt + 8, body, 16);
memcpy(receipt + 24, body + 24, 8); memcpy(receipt + 32, &inst->node_id, 8);
memcpy(receipt + 40, job->id, 16); SHA256(body, len, receipt + 56);
if (sc_ed25519_sign(inst->my_ed25519_privkey, receipt, 88, receipt + 88) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: media receipt signing failed");
goto fail;
}
}
if (!custody && dm_media_receipt_make(inst, body, len, 0, receipt)) goto fail;
if (dm_media_exec(inst, "BEGIN IMMEDIATE")) goto fail;
st = dm_media_prepare(inst, custody ? "UPDATE dm_custody SET ready=1 WHERE id=? AND ready=0" :
"UPDATE dm_files SET ready=1,downloading=0,receipt=? WHERE id=? AND ready<=0");
if (!st) goto fail;
if (!st) { dm_media_exec(inst, "ROLLBACK"); goto fail; }
int idx = 1;
if (!custody) sqlite3_bind_blob(st, idx++, receipt, sizeof(receipt), SQLITE_STATIC);
sqlite3_bind_blob(st, idx, job->id, 16, SQLITE_STATIC);
if (dm_media_step(inst, st) || sqlite3_changes(inst->topo_sqlite_db) != 1) goto fail;
if (dm_media_step(inst, st) || sqlite3_changes(inst->topo_sqlite_db) != 1 ||
(!custody && dm_media_receipt_save(inst, receipt, body, len)) || dm_media_exec(inst, "COMMIT")) {
dm_media_exec(inst, "ROLLBACK"); goto fail;
}
if (!custody && ready < 0)
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_media: recovered expired file conv=%s generation=%llu bytes=%llu",
job->conv, (unsigned long long)job->generation, (unsigned long long)job->size);