Implement durable encrypted attachment grants and requests
This commit is contained in:
1 parent
e45401449e
commit
321ed8e747
2 files changed
+630
No files matched your search
@@ -0,0 +1,584 @@
|
||||
/* Автор выдаёт случайный ключ отдельному проверенному участнику после решения UI.
|
||||
* Сеть хранит только контейнеры; decoded-копии не попадают в media_files. */
|
||||
#include "chat_attachment.h"
|
||||
#include "chat_core_priv.h"
|
||||
#include "chat_event.h"
|
||||
#include "chat_feedback.h"
|
||||
#include "member_sync.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../routing_layer/etcp_router.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../media_delivery/media_index.h"
|
||||
#include "../media_async/media_async.h"
|
||||
#include "../../lib/platform_compat.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/crypto.h>
|
||||
#include <errno.h>
|
||||
|
||||
enum { AK_REQUEST = 1, AK_REQUEST_ACK, AK_GRANT, AK_DENY, AK_RESULT_ACK };
|
||||
#define AK_BODY 85
|
||||
#define AK_WIRE (AK_BODY + 32)
|
||||
|
||||
struct ak_file {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32], source[1024], cipher[1024], plain[1024], temporary[1040], type[32];
|
||||
uint8_t id[16], key[32], message[32], hash[32], reply[32];
|
||||
struct attachment_info info;
|
||||
uint64_t author, request;
|
||||
int encrypt, error;
|
||||
};
|
||||
|
||||
/* Все ошибки SQLite содержат конкретную операцию. */
|
||||
static sqlite3_stmt* ak_prepare(struct UTUN_INSTANCE* inst, const char* sql) {
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (!CC(inst) || sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL) != SQLITE_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: prepare failed sql=%s error=%s", sql,
|
||||
CC(inst) ? sqlite3_errmsg(CC(inst)->db) : "stopped");
|
||||
return st;
|
||||
}
|
||||
|
||||
/* Statement всегда освобождается. */
|
||||
static int ak_step(struct UTUN_INSTANCE* inst, sqlite3_stmt* st) {
|
||||
if (!st) return -1;
|
||||
int rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: write failed rc=%d: %s", rc, sqlite3_errmsg(CC(inst)->db));
|
||||
sqlite3_finalize(st);
|
||||
return rc == SQLITE_DONE ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Управление транзакциями без открытых statement. */
|
||||
static int ak_exec(struct UTUN_INSTANCE* inst, const char* sql) {
|
||||
int rc = sqlite3_exec(CC(inst)->db, sql, NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: %s failed: %s", sql, sqlite3_errmsg(CC(inst)->db));
|
||||
return rc == SQLITE_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Канал допускает только каноническое десятичное имя. */
|
||||
static int ak_channel(const char* channel) {
|
||||
return channel && channel[0] && channel[0] != '0' && strlen(channel) < 20 && strspn(channel, "0123456789") == strlen(channel);
|
||||
}
|
||||
|
||||
/* Изменения UI сигнализируются только после commit; секреты в события не входят. */
|
||||
static void ak_changed(struct UTUN_INSTANCE* inst, const char* channel) {
|
||||
uint8_t event[33]; size_t n = strlen(channel);
|
||||
event[0] = (uint8_t)n; memcpy(event + 1, channel, n);
|
||||
chat_event_post(inst, CHAT_EVT_ATTACHMENT_KEYS_CHANGED, event, (int)n + 1);
|
||||
}
|
||||
|
||||
/* UUID не зависит от пользовательского имени или путей. */
|
||||
static void ak_hex(const uint8_t id[16], char hex[33]) {
|
||||
for (unsigned i = 0; i < 16; i++) snprintf(hex + 2 * i, 3, "%02x", id[i]);
|
||||
}
|
||||
|
||||
/* Строгое имя UUID.uenc из подписанного manifest, без разделителей пути. */
|
||||
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]) {
|
||||
uint8_t name[64];
|
||||
const char* pipe = body ? memchr(body, '|', length) : NULL;
|
||||
if (!pipe || body[0] != '*' || pipe - body != 53 || b64_decode(body + 1, 52, name, sizeof(name)) != 37 ||
|
||||
memcmp(name + 32, ".uenc", 5)) goto invalid;
|
||||
for (unsigned i = 0; i < 16; i++) {
|
||||
unsigned value = 0;
|
||||
for (unsigned j = 0; j < 2; j++) {
|
||||
uint8_t c = name[i * 2 + j];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) goto invalid;
|
||||
value = value * 16 + (c <= '9' ? c - '0' : c - 'a' + 10);
|
||||
}
|
||||
id[i] = (uint8_t)value;
|
||||
}
|
||||
return 0;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid encrypted manifest identity bytes=%zu", length); return -1;
|
||||
}
|
||||
|
||||
/* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */
|
||||
static int ak_paths(struct ak_file* job) {
|
||||
const char* slash = strrchr(CC(job->inst)->db_path, '/');
|
||||
char base[512], hex[33]; ak_hex(job->id, hex);
|
||||
if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path);
|
||||
else snprintf(base, sizeof(base), ".");
|
||||
int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex);
|
||||
int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex);
|
||||
if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: path too long channel=%s", job->channel); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Каталоги создаются в worker до первого открытия файла. */
|
||||
static int ak_parent(const char* path) {
|
||||
char parent[1040]; snprintf(parent, sizeof(parent), "%s", path);
|
||||
for (char* p = parent + 1; *p; p++) {
|
||||
if (*p != '/') continue;
|
||||
*p = 0; int rc = utun_mkdir(parent, 0700); *p = '/';
|
||||
if (rc && errno != EEXIST) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: mkdir failed path=%s: %s", parent, strerror(errno)); return -1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Никаких обращений к БД или инстансу из worker. */
|
||||
static void ak_file_work(void* arg) {
|
||||
struct ak_file* job = arg; job->error = -1;
|
||||
if (ak_parent(job->plain) || ak_parent(job->cipher)) return;
|
||||
snprintf(job->temporary, sizeof(job->temporary), "%s.part", job->encrypt ? job->cipher : job->plain);
|
||||
if (job->encrypt) {
|
||||
if (ma_copy_file(job->source, job->plain) || encrypted_file_sync(job->plain) ||
|
||||
encrypted_file_write(job->plain, job->temporary, job->key, job->author, job->id, &job->info, job->type)) return;
|
||||
} else if (encrypted_file_read(job->cipher, job->temporary, job->key, job->author, job->id, job->hash, &job->info, job->type)) return;
|
||||
if (rename(job->temporary, job->encrypt ? job->cipher : job->plain)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: publish failed: %s", strerror(errno)); return;
|
||||
}
|
||||
job->error = 0;
|
||||
}
|
||||
|
||||
/* Проверка существующей строки предотвращает публикацию после удаления канала. */
|
||||
static int ak_present(struct ak_file* job) {
|
||||
sqlite3_stmt* st = ak_prepare(job->inst, "SELECT 1 FROM attachment_keys WHERE channel=? AND crypto_id=?");
|
||||
if (!st) return 0;
|
||||
sqlite3_bind_text(st, 1, job->channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job->id, 16, SQLITE_STATIC);
|
||||
int rc = sqlite3_step(st); sqlite3_finalize(st);
|
||||
if (rc != SQLITE_ROW && rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: presence lookup failed rc=%d", rc);
|
||||
return rc == SQLITE_ROW;
|
||||
}
|
||||
|
||||
/* Сохранение metadata и открытой копии не меняет сетевой индекс. */
|
||||
static void ak_file_done(void* arg, int error) {
|
||||
struct ak_file* job = arg; struct UTUN_INSTANCE* inst = job->inst;
|
||||
int valid = CC(inst) && !CC(inst)->attachment_closing && ak_present(job);
|
||||
int success = !error && !job->error && valid;
|
||||
if (success && !job->encrypt) {
|
||||
uint8_t info[ATTACHMENT_WIRE_MAX]; int n = attachment_encode(&job->info, info, sizeof(info));
|
||||
sqlite3_stmt* st = n < 0 ? NULL : ak_prepare(inst,
|
||||
"UPDATE attachment_keys SET info=?,content_type=?,ready=1,decrypting=0,error=0 WHERE channel=? AND crypto_id=?");
|
||||
if (st) {
|
||||
sqlite3_bind_blob(st, 1, info, n, SQLITE_STATIC); sqlite3_bind_text(st, 2, job->type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 3, job->channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 4, job->id, 16, SQLITE_STATIC);
|
||||
}
|
||||
success = st && !ak_step(inst, st);
|
||||
}
|
||||
if (success && job->encrypt) {
|
||||
char name[38], hex[33]; ak_hex(job->id, hex); snprintf(name, sizeof(name), "%s.uenc", hex);
|
||||
struct chat_msg_submit request = { .inst = inst, .data = (uint8_t*)name, .data_len = 37, .request_id = job->request };
|
||||
snprintf(request.channel_id, sizeof(request.channel_id), "%s", job->channel);
|
||||
snprintf(request.content_type, sizeof(request.content_type), "%s", ENCRYPTED_ATTACHMENT_TYPE);
|
||||
snprintf(request.media_src, sizeof(request.media_src), "%s", job->cipher);
|
||||
snprintf(request.media_dest, sizeof(request.media_dest), "%s", job->cipher);
|
||||
memcpy(request.reply_to, job->reply, 32);
|
||||
chat_core_submit_media_message(inst, &request);
|
||||
} else if (!success) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: worker failed channel=%s encrypt=%d error=%d worker=%d current=%d",
|
||||
job->channel, job->encrypt, error, job->error, valid);
|
||||
if (valid) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_keys SET decrypting=0,error=1 WHERE channel=? AND crypto_id=?");
|
||||
if (st) { sqlite3_bind_text(st, 1, job->channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job->id, 16, SQLITE_STATIC); ak_step(inst, st); }
|
||||
}
|
||||
if (job->encrypt) chat_event_submitted(inst, job->request, 0);
|
||||
}
|
||||
if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed");
|
||||
if ((!success || !valid) && job->temporary[0] && remove(job->temporary) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary cleanup failed");
|
||||
if (!valid) {
|
||||
if (remove(job->plain) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
|
||||
if (job->encrypt && remove(job->cipher) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
|
||||
}
|
||||
if (valid) ak_changed(inst, job->channel);
|
||||
OPENSSL_cleanse(job->key, 32); u_free(job);
|
||||
}
|
||||
|
||||
/* Сначала durable ключ, затем подготовка файла; потерять ключ опубликованного файла нельзя. */
|
||||
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) {
|
||||
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !inst->media_async || !ak_channel(channel) ||
|
||||
!source || strlen(source) >= 1024 || !type || strlen(type) >= 32 || attachment_validate(info)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid send arguments/state"); return -1;
|
||||
}
|
||||
struct ak_file* job = u_calloc(1, sizeof(*job));
|
||||
if (!job) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: send allocation failed"); return -1; }
|
||||
job->inst = inst; job->encrypt = 1; job->author = inst->node_id; job->request = request; job->info = *info;
|
||||
snprintf(job->channel, sizeof(job->channel), "%s", channel); snprintf(job->source, sizeof(job->source), "%s", source);
|
||||
snprintf(job->type, sizeof(job->type), "%s", type); memcpy(job->reply, reply, 32);
|
||||
uint8_t encoded[ATTACHMENT_WIRE_MAX]; int n = attachment_encode(info, encoded, sizeof(encoded));
|
||||
if (RAND_bytes(job->id, 16) != 1 || RAND_bytes(job->key, 32) != 1 || ak_paths(job) || n < 0) goto fail;
|
||||
sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_keys(channel,crypto_id,author,key,cipher_path,plain_path,info,content_type,ready)"
|
||||
" VALUES(?,?,?,?,?,?,?,?,1)");
|
||||
if (!st) goto fail;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job->id, 16, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id); sqlite3_bind_blob(st, 4, job->key, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 5, job->cipher, -1, SQLITE_STATIC); sqlite3_bind_text(st, 6, job->plain, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(st, 7, encoded, n, SQLITE_STATIC); sqlite3_bind_text(st, 8, type, -1, SQLITE_STATIC);
|
||||
if (ak_step(inst, st)) goto fail;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: key saved before encryption channel=%s", channel);
|
||||
media_async_submit(inst->media_async, inst->ua, ak_file_work, job, ak_file_done, job);
|
||||
return 0;
|
||||
fail:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot prepare send channel=%s", channel);
|
||||
OPENSSL_cleanse(job->key, 32); u_free(job); return -1;
|
||||
}
|
||||
|
||||
/* Сохраняется идентичность подписанного контейнера до выхода сообщения в сеть. */
|
||||
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]) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_keys SET message_id=?,cipher_hash=?"
|
||||
" WHERE channel=? AND crypto_id=? AND author=? AND key IS NOT NULL AND (message_id IS NULL OR message_id=?)");
|
||||
if (!st) return -1;
|
||||
sqlite3_bind_blob(st, 1, message, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 2, hash, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 3, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 4, id, 16, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 5, (sqlite3_int64)inst->node_id); sqlite3_bind_blob(st, 6, message, 32, SQLITE_STATIC);
|
||||
if (ak_step(inst, st) || sqlite3_changes(CC(inst)->db) != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: author key missing or already bound channel=%s", channel); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Запись поступает только из проверенной истории; размер и 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;
|
||||
}
|
||||
char* copy = u_malloc(length + 1);
|
||||
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: message allocation failed"); return -1; }
|
||||
memcpy(copy, json, length); copy[length] = 0;
|
||||
if (!strstr(copy, "\"ct\":\"" ENCRYPTED_ATTACHMENT_TYPE "\"")) { u_free(copy); return 0; }
|
||||
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;
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/* Кодограммы привязаны к immutable сообщению, UUID и полному hash контейнера. */
|
||||
static int ak_body(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint8_t cmd, uint8_t wire[AK_WIRE]) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT crypto_id,cipher_hash,key FROM attachment_keys WHERE channel=? AND message_id=?");
|
||||
if (!st) return -1;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
int rc = sqlite3_step(st), ok = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 16 && sqlite3_column_bytes(st, 1) == 32 &&
|
||||
(cmd != AK_GRANT || sqlite3_column_bytes(st, 2) == 32);
|
||||
if (ok) {
|
||||
memcpy(wire, "AK01", 4); wire[4] = cmd; memcpy(wire + 5, message, 32);
|
||||
memcpy(wire + 37, sqlite3_column_blob(st, 0), 16); memcpy(wire + 53, sqlite3_column_blob(st, 1), 32);
|
||||
if (cmd == AK_GRANT) memcpy(wire + AK_BODY, sqlite3_column_blob(st, 2), 32);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (!ok) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: wire identity/key unavailable channel=%s command=%u rc=%d", channel, cmd, rc);
|
||||
return ok ? (cmd == AK_GRANT ? AK_WIRE : AK_BODY) : -1;
|
||||
}
|
||||
|
||||
/* Неподписанные и незашифрованные ответы сервис не принимает. */
|
||||
static int ak_send(struct UTUN_INSTANCE* inst, const char* channel, uint64_t peer, const uint8_t* body, size_t length) {
|
||||
uint64_t group = strtoull(channel, NULL, 10);
|
||||
if (!etcp_router_has_route(inst, group, peer)) return 1;
|
||||
struct ETCP_ROUTER_CONN* conn = etcp_router_conn_get(inst, group, peer, ETCP_RT_ID_ATTACHMENT_KEYS);
|
||||
if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: router channel allocation failed"); return -1; }
|
||||
if (!etcp_router_send_q_has_room(inst, group, peer, ETCP_RT_ID_ATTACHMENT_KEYS)) return 1;
|
||||
return etcp_router_conn_send(conn, body, length, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
|
||||
}
|
||||
|
||||
/* Очередь содержит только команду; ключ читается при отправке из отдельной таблицы. */
|
||||
static int ak_queue(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int cmd) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_key_outbox(channel,message_id,peer,command) VALUES(?,?,?,?)"
|
||||
" ON CONFLICT(channel,message_id,peer) DO UPDATE SET command=excluded.command,retry_at=0");
|
||||
if (!st) return -1;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); sqlite3_bind_int(st, 4, cmd); return ak_step(inst, st);
|
||||
}
|
||||
|
||||
/* Разрешение проверяет полное подписанное членство и immutable историю. */
|
||||
static int ak_authorized(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer) {
|
||||
uint8_t key[32];
|
||||
if (member_sync_verified_ed25519(inst, channel, peer, key)) return 0;
|
||||
char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM msg_%s m JOIN attachment_keys a ON a.message_id=m.message_id"
|
||||
" AND a.channel=? WHERE m.message_id=? AND m.node_id=? AND a.author=? AND length(a.key)=32", channel);
|
||||
sqlite3_stmt* st = ak_prepare(inst, sql);
|
||||
if (!st) return 0;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id); sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id);
|
||||
int rc = sqlite3_step(st); sqlite3_finalize(st);
|
||||
if (rc != SQLITE_ROW) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: request has no local author key/message channel=%s peer=%llu rc=%d",
|
||||
channel, (unsigned long long)peer, rc);
|
||||
return rc == SQLITE_ROW;
|
||||
}
|
||||
|
||||
/* Подтверждение протокола посылается только после durable записи. */
|
||||
static void ak_ack(struct UTUN_INSTANCE* inst, const char* channel, uint64_t peer, const uint8_t* body, uint8_t cmd) {
|
||||
uint8_t ack[AK_BODY]; memcpy(ack, body, sizeof(ack)); ack[4] = cmd;
|
||||
int rc = ak_send(inst, channel, peer, ack, sizeof(ack));
|
||||
if (rc < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: acknowledgment deferred channel=%s command=%u", channel, cmd);
|
||||
}
|
||||
|
||||
/* Приём строго адресного E2E control. Дубликат возвращает сохранённое решение. */
|
||||
static void ak_receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
struct UTUN_INSTANCE* inst = conn->instance;
|
||||
uint8_t* d = entry->dgram;
|
||||
uint64_t peer = 0, group = 0, target = 0;
|
||||
char channel[32];
|
||||
if (!CC(inst) || CC(inst)->attachment_closing || entry->len < ROUTER_SVC_PAYLOAD_OFF + AK_BODY) goto invalid;
|
||||
memcpy(&peer, d + ROUTER_SVC_SRC_OFF, 8); memcpy(&group, d + ROUTER_SVC_GROUP_OFF, 8); memcpy(&target, d + ROUTER_SVC_DST_OFF, 8);
|
||||
if (target != inst->node_id || peer == inst->node_id ||
|
||||
(d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) goto invalid;
|
||||
snprintf(channel, sizeof(channel), "%llu", (unsigned long long)group);
|
||||
uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF; size_t bytes = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
uint8_t cmd = body[4], expected[AK_WIRE];
|
||||
if (!ak_channel(channel) || memcmp(body, "AK01", 4) || cmd < AK_REQUEST || cmd > AK_RESULT_ACK ||
|
||||
bytes != (cmd == AK_GRANT ? AK_WIRE : AK_BODY) || ak_body(inst, channel, body + 5, AK_REQUEST, expected) < 0 ||
|
||||
memcmp(body + 5, expected + 5, AK_BODY - 5)) goto invalid;
|
||||
if (cmd == AK_REQUEST) {
|
||||
if (!ak_authorized(inst, channel, body + 5, peer) || ak_exec(inst, "SAVEPOINT attachment_request")) goto invalid;
|
||||
sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at)"
|
||||
" VALUES(?,?,?,1,0,unixepoch()) ON CONFLICT(channel,message_id,peer) DO NOTHING");
|
||||
if (st) { sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); }
|
||||
if (!st || ak_step(inst, st)) goto rollback_request;
|
||||
st = ak_prepare(inst, "SELECT state FROM attachment_key_requests WHERE channel=? AND message_id=? AND peer=? AND incoming=1");
|
||||
if (!st) goto rollback_request;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
int rc = sqlite3_step(st), state = rc == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st);
|
||||
if (state < 0 || (state && ak_queue(inst, channel, body + 5, peer, state == 3 ? AK_DENY : AK_GRANT)) ||
|
||||
ak_exec(inst, "RELEASE attachment_request")) goto rollback_request;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: request saved channel=%s peer=%llu state=%d", channel, (unsigned long long)peer, state);
|
||||
ak_ack(inst, channel, peer, body, AK_REQUEST_ACK); ak_changed(inst, channel); goto done;
|
||||
rollback_request:
|
||||
ak_exec(inst, "ROLLBACK TO attachment_request"); ak_exec(inst, "RELEASE attachment_request"); goto done;
|
||||
}
|
||||
if (ak_exec(inst, "SAVEPOINT attachment_result")) goto done;
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT incoming,state FROM attachment_key_requests WHERE channel=? AND message_id=? AND peer=?");
|
||||
if (!st) goto rollback_result;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
int rc = sqlite3_step(st), incoming = rc == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1,
|
||||
state = rc == SQLITE_ROW ? sqlite3_column_int(st, 1) : -1; sqlite3_finalize(st);
|
||||
if (incoming < 0 || (cmd == AK_RESULT_ACK ? !incoming || state == 0 : incoming)) goto rollback_result;
|
||||
if (cmd == AK_GRANT || cmd == AK_DENY) {
|
||||
if ((state == 2 && cmd != AK_GRANT) || (state == 3 && cmd != AK_DENY)) goto rollback_result;
|
||||
if (cmd == AK_GRANT) {
|
||||
st = ak_prepare(inst, "UPDATE attachment_keys SET key=?,error=0 WHERE channel=? AND message_id=? AND author=?"
|
||||
" AND (key IS NULL OR key=?)");
|
||||
if (!st) goto rollback_result;
|
||||
sqlite3_bind_blob(st, 1, body + AK_BODY, 32, SQLITE_STATIC); sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(st, 3, body + 5, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)peer);
|
||||
sqlite3_bind_blob(st, 5, body + AK_BODY, 32, SQLITE_STATIC);
|
||||
if (ak_step(inst, st) || sqlite3_changes(CC(inst)->db) != 1) goto rollback_result;
|
||||
}
|
||||
}
|
||||
int next = cmd == AK_REQUEST_ACK ? (state == 0 ? 1 : state) : cmd == AK_RESULT_ACK ? (state == 3 ? 3 : 4) : cmd == AK_GRANT ? 2 : 3;
|
||||
st = ak_prepare(inst, "UPDATE attachment_key_requests SET state=? WHERE channel=? AND message_id=? AND peer=?");
|
||||
if (!st) goto rollback_result;
|
||||
sqlite3_bind_int(st, 1, next); sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(st, 3, body + 5, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)peer);
|
||||
if (ak_step(inst, st)) goto rollback_result;
|
||||
st = ak_prepare(inst, "DELETE FROM attachment_key_outbox WHERE channel=? AND message_id=? AND peer=? AND command=?");
|
||||
if (!st) goto rollback_result;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
sqlite3_bind_int(st, 4, cmd == AK_RESULT_ACK ? (state == 3 ? AK_DENY : AK_GRANT) : AK_REQUEST);
|
||||
if (ak_step(inst, st) || ak_exec(inst, "RELEASE attachment_result")) goto rollback_result;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: control committed channel=%s peer=%llu command=%u state=%d", channel,
|
||||
(unsigned long long)peer, cmd, next);
|
||||
if (cmd == AK_GRANT || cmd == AK_DENY) ak_ack(inst, channel, peer, body, AK_RESULT_ACK);
|
||||
ak_changed(inst, channel); goto done;
|
||||
rollback_result:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unexpected/conflicting control channel=%s peer=%llu command=%u", channel,
|
||||
(unsigned long long)peer, cmd);
|
||||
ak_exec(inst, "ROLLBACK TO attachment_result"); ak_exec(inst, "RELEASE attachment_result"); goto done;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: rejected control source=%llu group=%llu bytes=%u", (unsigned long long)peer,
|
||||
(unsigned long long)group, (unsigned)entry->len);
|
||||
done:
|
||||
queue_dgram_free(entry); queue_entry_free(entry);
|
||||
}
|
||||
|
||||
/* Только явное решение автора создаёт GRANT в durable outbox. */
|
||||
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve) {
|
||||
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !ak_channel(channel) || !ak_authorized(inst, channel, message, peer)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: decision rejected by authorization"); return -1;
|
||||
}
|
||||
if (ak_exec(inst, "SAVEPOINT attachment_decision")) return -1;
|
||||
sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_key_requests SET state=? WHERE channel=? AND message_id=? AND peer=? AND incoming=1 AND state=0");
|
||||
if (!st) goto rollback;
|
||||
sqlite3_bind_int(st, 1, approve ? 2 : 3); sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)peer);
|
||||
if (ak_step(inst, st) || sqlite3_changes(CC(inst)->db) != 1 || ak_queue(inst, channel, message, peer, approve ? AK_GRANT : AK_DENY) ||
|
||||
ak_exec(inst, "RELEASE attachment_decision")) goto rollback;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: author decision channel=%s peer=%llu approve=%d", channel, (unsigned long long)peer, approve);
|
||||
ak_changed(inst, channel); return 0;
|
||||
rollback:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: decision failed channel=%s peer=%llu", channel, (unsigned long long)peer);
|
||||
ak_exec(inst, "ROLLBACK TO attachment_decision"); ak_exec(inst, "RELEASE attachment_decision"); return -1;
|
||||
}
|
||||
|
||||
/* GUI передаёт зафиксированные ID, результат возвращается после записи решения. */
|
||||
void chat_attachment_decision_trampoline(void* arg) {
|
||||
struct chat_attachment_decision* request = arg;
|
||||
int rc = chat_attachment_decide(request->inst, request->channel, request->message, request->peer, request->approve);
|
||||
uint8_t event[34]; size_t n = strlen(request->channel);
|
||||
event[0] = (uint8_t)n; memcpy(event + 1, request->channel, n); event[n + 1] = rc ? 0 : 1;
|
||||
chat_event_post(request->inst, CHAT_EVT_ATTACHMENT_KEYS_CHANGED, event, (int)n + 2); u_free(request);
|
||||
}
|
||||
|
||||
/* Пользовательский интерес хранится отдельно от фоновой storage-копии. */
|
||||
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_keys SET wanted=1,error=0 WHERE channel=? AND message_id=?");
|
||||
if (!st) return;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC); ak_step(inst, st);
|
||||
}
|
||||
|
||||
/* Автозагрузка обычного участника запрашивает доступ; storage-реплика хранит только ciphertext. */
|
||||
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]) {
|
||||
char sql[256]; snprintf(sql, sizeof(sql), "UPDATE attachment_keys SET downloaded=1,wanted=max(wanted,"
|
||||
"NOT EXISTS(SELECT 1 FROM peers_%s WHERE node_id=? AND storage=1)) WHERE channel=? AND message_id=?", channel);
|
||||
sqlite3_stmt* st = ak_prepare(inst, sql);
|
||||
if (!st) return;
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(st, 3, message, 32, SQLITE_STATIC);
|
||||
if (!ak_step(inst, st)) { DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: ciphertext downloaded channel=%s", channel); ak_changed(inst, channel); }
|
||||
}
|
||||
|
||||
/* Один проход ограничен; retry_at даёт всем задачам очередь без голодания. */
|
||||
static void ak_pump(struct UTUN_INSTANCE* inst) {
|
||||
for (unsigned i = 0; i < 16; i++) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT channel,message_id,peer,command FROM attachment_key_outbox"
|
||||
" WHERE retry_at<=unixepoch() ORDER BY retry_at,rowid LIMIT 1");
|
||||
if (!st) return;
|
||||
int rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_ROW) { sqlite3_finalize(st); if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: outbox read failed rc=%d", rc); return; }
|
||||
char channel[32]; uint8_t message[32], wire[AK_WIRE];
|
||||
snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); memcpy(message, sqlite3_column_blob(st, 1), 32);
|
||||
uint64_t peer = (uint64_t)sqlite3_column_int64(st, 2); int cmd = sqlite3_column_int(st, 3); sqlite3_finalize(st);
|
||||
st = ak_prepare(inst, "UPDATE attachment_key_outbox SET retry_at=unixepoch()+5 WHERE channel=? AND message_id=? AND peer=?");
|
||||
if (!st) return;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); if (ak_step(inst, st)) return;
|
||||
if (cmd == AK_GRANT && !ak_authorized(inst, channel, message, peer)) continue;
|
||||
int n = ak_body(inst, channel, message, (uint8_t)cmd, wire);
|
||||
if (n > 0) {
|
||||
rc = ak_send(inst, channel, peer, wire, (size_t)n);
|
||||
if (rc < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: send deferred channel=%s peer=%llu command=%d", channel, (unsigned long long)peer, cmd);
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: outbox attempt channel=%s peer=%llu command=%d route=%d", channel,
|
||||
(unsigned long long)peer, cmd, rc == 0);
|
||||
}
|
||||
OPENSSL_cleanse(wire, sizeof(wire));
|
||||
}
|
||||
}
|
||||
|
||||
/* Восстанавливаем обработку файлов и запросов из устойчивых данных. */
|
||||
static void ak_tick(void* arg) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
if (!CC(inst) || CC(inst)->attachment_closing) return;
|
||||
CC(inst)->attachment_timer = NULL;
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT channel,message_id,crypto_id,author,key,cipher_hash,cipher_path,plain_path FROM attachment_keys a"
|
||||
" WHERE downloaded=1 AND wanted=1 AND ready=0 AND decrypting=0 AND error=0 AND"
|
||||
" (length(key)=32 OR NOT EXISTS(SELECT 1 FROM attachment_key_requests r WHERE r.channel=a.channel AND r.message_id=a.message_id)) LIMIT 1");
|
||||
if (!st) break;
|
||||
int rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_ROW) { sqlite3_finalize(st); if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: pending file read failed rc=%d", rc); break; }
|
||||
struct ak_file* job = u_calloc(1, sizeof(*job));
|
||||
if (!job) { sqlite3_finalize(st); DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: receive allocation failed"); break; }
|
||||
job->inst = inst;
|
||||
snprintf(job->channel, sizeof(job->channel), "%s", sqlite3_column_text(st, 0)); memcpy(job->message, sqlite3_column_blob(st, 1), 32);
|
||||
memcpy(job->id, sqlite3_column_blob(st, 2), 16); job->author = (uint64_t)sqlite3_column_int64(st, 3);
|
||||
int has_key = sqlite3_column_bytes(st, 4) == 32;
|
||||
if (has_key) memcpy(job->key, sqlite3_column_blob(st, 4), 32);
|
||||
memcpy(job->hash, sqlite3_column_blob(st, 5), 32);
|
||||
snprintf(job->cipher, sizeof(job->cipher), "%s", sqlite3_column_text(st, 6));
|
||||
snprintf(job->plain, sizeof(job->plain), "%s", sqlite3_column_text(st, 7)); sqlite3_finalize(st);
|
||||
if (has_key) {
|
||||
st = ak_prepare(inst, "UPDATE attachment_keys SET decrypting=1 WHERE channel=? AND message_id=?");
|
||||
if (st) { sqlite3_bind_text(st, 1, job->channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job->message, 32, SQLITE_STATIC); }
|
||||
if (!st || ak_step(inst, st)) { OPENSSL_cleanse(job->key, 32); u_free(job); break; }
|
||||
media_async_submit(inst->media_async, inst->ua, ak_file_work, job, ak_file_done, job);
|
||||
} else {
|
||||
int failed = ak_exec(inst, "SAVEPOINT attachment_outgoing");
|
||||
st = failed ? NULL : ak_prepare(inst, "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at) VALUES(?,?,?,0,0,unixepoch())");
|
||||
if (st) { sqlite3_bind_text(st, 1, job->channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, job->message, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)job->author); }
|
||||
if (!failed && (!st || ak_step(inst, st) || ak_queue(inst, job->channel, job->message, job->author, AK_REQUEST) ||
|
||||
ak_exec(inst, "RELEASE attachment_outgoing"))) {
|
||||
ak_exec(inst, "ROLLBACK TO attachment_outgoing"); ak_exec(inst, "RELEASE attachment_outgoing"); failed = 1;
|
||||
}
|
||||
if (!failed) { DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: decryption request queued channel=%s author=%llu", job->channel,
|
||||
(unsigned long long)job->author); ak_changed(inst, job->channel); }
|
||||
u_free(job); if (failed) break;
|
||||
}
|
||||
}
|
||||
ak_pump(inst);
|
||||
CC(inst)->attachment_timer = uasync_set_timeout(inst->ua, 10000, inst, ak_tick, "attachment keys");
|
||||
if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed");
|
||||
}
|
||||
|
||||
/* Отдельные локальные таблицы никогда не регистрируются в db_sync/Merkle. */
|
||||
int chat_attachment_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!CC(inst) || !inst->media_async) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid initialization"); return -1; }
|
||||
if (ak_exec(inst, "CREATE TABLE IF NOT EXISTS attachment_keys("
|
||||
"channel TEXT NOT NULL,crypto_id BLOB NOT NULL CHECK(length(crypto_id)=16),message_id BLOB,author INTEGER NOT NULL,"
|
||||
"key BLOB CHECK(key IS NULL OR length(key)=32),cipher_hash BLOB,cipher_path TEXT NOT NULL,plain_path TEXT NOT NULL,"
|
||||
"info BLOB,content_type TEXT,ready INTEGER NOT NULL DEFAULT 0,downloaded INTEGER NOT NULL DEFAULT 0,"
|
||||
"wanted INTEGER NOT NULL DEFAULT 0,decrypting INTEGER NOT NULL DEFAULT 0,error INTEGER NOT NULL DEFAULT 0,"
|
||||
"PRIMARY KEY(channel,crypto_id),UNIQUE(channel,message_id));"
|
||||
"CREATE TABLE IF NOT EXISTS attachment_key_requests(channel TEXT NOT NULL,message_id BLOB NOT NULL,peer INTEGER NOT NULL,"
|
||||
"incoming INTEGER NOT NULL,state INTEGER NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(channel,message_id,peer));"
|
||||
"CREATE TABLE IF NOT EXISTS attachment_key_outbox(channel TEXT NOT NULL,message_id BLOB NOT NULL,peer INTEGER NOT NULL,"
|
||||
"command INTEGER NOT NULL,retry_at INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(channel,message_id,peer));"
|
||||
"UPDATE attachment_keys SET decrypting=0; UPDATE attachment_key_outbox SET retry_at=0;")) return -1;
|
||||
CC(inst)->attachment_closing = 0;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_ATTACHMENT_KEYS, ak_receive)) return -1;
|
||||
CC(inst)->attachment_timer = uasync_set_timeout(inst->ua, 10000, inst, ak_tick, "attachment keys");
|
||||
if (!CC(inst)->attachment_timer) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: initial timer allocation failed"); return -1; }
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: initialized persistent requests and separate ciphertext/plaintext storage"); return 0;
|
||||
}
|
||||
|
||||
/* Сначала снять сеть/таймер, потом дождаться workers с живым chat_core. */
|
||||
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst) {
|
||||
if (!CC(inst)) return;
|
||||
CC(inst)->attachment_closing = 1;
|
||||
if (CC(inst)->attachment_timer) uasync_cancel_timeout(inst->ua, CC(inst)->attachment_timer);
|
||||
CC(inst)->attachment_timer = NULL;
|
||||
if (inst->router_bindings.callbacks[ETCP_RT_ID_ATTACHMENT_KEYS]) etcp_router_unbind(inst, ETCP_RT_ID_ATTACHMENT_KEYS);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: quiesced, persistent decisions preserved");
|
||||
}
|
||||
|
||||
void chat_attachment_destroy(struct UTUN_INSTANCE* inst) {
|
||||
chat_attachment_quiesce(inst);
|
||||
}
|
||||
|
||||
/* Удаление ключей отменяет completion активных workers; они удалят свои результаты. */
|
||||
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT plain_path FROM attachment_keys WHERE channel=?");
|
||||
if (st) {
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); int rc;
|
||||
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
||||
const char* path = (const char*)sqlite3_column_text(st, 0);
|
||||
if (path && remove(path) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: plaintext removal failed path=%s", path);
|
||||
}
|
||||
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: channel cleanup scan failed rc=%d", rc);
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
const char* tables[] = { "attachment_key_outbox", "attachment_key_requests", "attachment_keys" };
|
||||
for (unsigned i = 0; i < 3; i++) {
|
||||
char sql[128]; snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE channel=?", tables[i]); st = ak_prepare(inst, sql);
|
||||
if (st) { sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); ak_step(inst, st); }
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: channel keys and requests removed channel=%s", channel);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Зашифрованные групповые вложения и ручная выдача ключей.
|
||||
* Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны.
|
||||
* init после media_async; quiesce до его destroy, destroy после workers.
|
||||
* Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета.
|
||||
* Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */
|
||||
#ifndef CHAT_ATTACHMENT_H
|
||||
#define CHAT_ATTACHMENT_H
|
||||
#include "../media_async/attachment.h"
|
||||
#include "../media_async/encrypted_file.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
struct UTUN_INSTANCE;
|
||||
int chat_attachment_init(struct UTUN_INSTANCE* inst);
|
||||
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst);
|
||||
void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
|
||||
/* source — собственный подготовленный файл приложения, передаётся во владение. */
|
||||
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-prefix тела медиа. */
|
||||
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
|
||||
/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */
|
||||
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 сообщение; обычные сообщения игнорируются. */
|
||||
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);
|
||||
/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */
|
||||
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */
|
||||
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve);
|
||||
struct chat_attachment_decision {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32];
|
||||
uint8_t message[32];
|
||||
uint64_t peer;
|
||||
int approve;
|
||||
};
|
||||
void chat_attachment_decision_trampoline(void* arg);
|
||||
/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */
|
||||
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
Reference in new issue
Block a user