155 lines
9.3 KiB
C
155 lines
9.3 KiB
C
/*
|
||
* dm_core.h — прямой p2p чат между двумя пользователями (DM)
|
||
*
|
||
* Личная беседа не создаёт отдельную TOPO_GROUP и не синхронизируется через member_sync/Merkle.
|
||
* Для доставки использует маршрут из общей CHAT-группы участников.
|
||
* - conv_id и content_key детерминированно выводятся обеими сторонами
|
||
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров.
|
||
* - сообщения идут через etcp_router (прямое соединение или релей),
|
||
* при недоступности пира — через dm_mailbox (суперузлы).
|
||
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519.
|
||
*
|
||
* Два независимых направленных потока (я→пир и пир→я) с монотонным seq.
|
||
*/
|
||
|
||
#ifndef DM_CORE_H
|
||
#define DM_CORE_H
|
||
|
||
#include <stdint.h>
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
#include "dm_media.h"
|
||
|
||
struct UTUN_INSTANCE;
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ──
|
||
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][media?:56][sig:64]
|
||
* media присутствует только для ct="file", подписан вместе с сообщением.
|
||
* data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. */
|
||
#define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */
|
||
#define DM_MSG_SIG_SIZE 64
|
||
#define DM_DATA_MAX 1024
|
||
#define DM_RECEIPT_SIZE 136
|
||
|
||
/* Вычислить полную длину канонического тела из его заголовка.
|
||
* Возвращает 1 и *out_len, либо 0 если буфер мал/повреждён. */
|
||
static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) {
|
||
if (!msg || !out_len || avail < DM_MSG_FIXED_HDR) return 0;
|
||
uint8_t ct_len = msg[32];
|
||
if (avail < (size_t)DM_MSG_FIXED_HDR + ct_len + 2) return 0;
|
||
uint16_t data_len;
|
||
memcpy(&data_len, msg + DM_MSG_FIXED_HDR + ct_len, 2);
|
||
if (data_len < 16 || data_len > DM_DATA_MAX + 16) return 0;
|
||
size_t media_len = ct_len == 4 && !memcmp(msg + DM_MSG_FIXED_HDR, "file", 4) ? DM_MEDIA_DESC_SIZE : 0;
|
||
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + media_len + DM_MSG_SIG_SIZE;
|
||
if (avail < total) return 0;
|
||
*out_len = total;
|
||
return 1;
|
||
}
|
||
|
||
/* Общие операции протокола DM; все вызовы в uasync-потоке.
|
||
* group_id — только область маршрутизации, идентичность беседы от неё не зависит.
|
||
* route_group выбирает общую группу с живым путём, возвращает 1/0.
|
||
* verify_message проверяет каноническое тело и подпись автора для recipient.
|
||
* accept_message возвращает 0 только после commit (включая точный повтор).
|
||
* Квитанция: ["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(msg):32][sig:64].
|
||
* verify_receipt проверяет подпись получателя и при msg!=NULL точное соответствие телу.
|
||
* accept_receipt удаляет только соответствующее исходящее pending-сообщение. */
|
||
int dm_route_group(struct UTUN_INSTANCE* inst, uint64_t peer, uint64_t preferred, uint64_t* group_id);
|
||
int dm_verify_message(struct UTUN_INSTANCE* inst, uint64_t recipient, const uint8_t* msg, size_t len);
|
||
int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len, uint8_t receipt[DM_RECEIPT_SIZE]);
|
||
int dm_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE], const uint8_t* msg, size_t len);
|
||
int dm_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE]);
|
||
|
||
/* Жизненный цикл: инициализация после chat_core_init, destroy перед chat_core_destroy. */
|
||
int dm_core_init(struct UTUN_INSTANCE* inst);
|
||
void dm_core_destroy(struct UTUN_INSTANCE* inst);
|
||
|
||
/* Начать беседу с пользователем (peer). source_ch_id — канал, где нашли target
|
||
* (общая группа; нужен для маршрутизации и подключения). Создаёт беседу если её
|
||
* ещё нет, выводит conv_id/ключи. Подключение для передачи медиа принадлежит media.
|
||
* Возвращает 0 при успехе, <0 при ошибке. */
|
||
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
|
||
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
|
||
const char* peer_name, const char* source_ch_id);
|
||
|
||
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
|
||
* доставляет (прямое соединение при доступности пира, иначе mailbox).
|
||
* conv_id — строковое представление (как в dm_conversations.conv_id).
|
||
* Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media.
|
||
* Для ct="invite" data=[group_name:utf8][NUL][полная ссылка:utf8], без завершающего NUL.
|
||
* Имя и ссылка шифруются и подписываются вместе; GUI получает data=ссылка, tags.group_name=имя. */
|
||
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
|
||
const uint8_t* data, uint32_t data_len);
|
||
|
||
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */
|
||
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
|
||
|
||
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
|
||
* count=0 → все, offset=0 → с начала. */
|
||
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
|
||
char* buf, size_t buf_size, size_t* out_len);
|
||
|
||
/* ── Трамплины для gui_bridge (GUI → uasync) ── */
|
||
|
||
/* Начать беседу с узлом из общей группы. Трамплин сам находит x25519/ed25519
|
||
* пира в таблице nodes (как headless hc_handle_dm_start), после чего вызывает
|
||
* dm_start(). Результат — событие CHAT_EVT_DM_CONV_UPDATED. */
|
||
struct dm_start_req {
|
||
struct UTUN_INSTANCE* inst;
|
||
uint64_t peer_node_id;
|
||
char source_ch_id[64];
|
||
char peer_name[128];
|
||
};
|
||
void dm_start_trampoline(void* arg);
|
||
|
||
/* Отправить текстовое сообщение в беседу (content_type="text"). */
|
||
struct dm_send_req {
|
||
struct UTUN_INSTANCE* inst;
|
||
char conv_id[64];
|
||
uint32_t data_len;
|
||
uint8_t* data; /* data_len байт (выделяется в хвосте структуры) */
|
||
};
|
||
void dm_send_trampoline(void* arg);
|
||
|
||
/* Приглашение в выбранный канал через PM: свежая ссылка после ACK регистрации,
|
||
* затем dm_send("invite") с названием группы. Получатель фиксирован независимо от текущего UI.
|
||
* Один ожидающий запрос на instance; результат CHAT_EVT_DM_INVITE_RESULT.
|
||
* cancel=1 отменяет только ожидающий request_id; отправленное PM не отзывается.
|
||
* arg=u_calloc(dm_invite_req); обработчик/финальный callback освобождает arg. */
|
||
enum dm_invite_result {
|
||
DM_INVITE_OK = 0, DM_INVITE_START_ERROR = -1, DM_INVITE_BUILD_ERROR = -2,
|
||
DM_INVITE_SEND_ERROR = -3, DM_INVITE_CANCELLED = -4, DM_INVITE_BUSY = -5
|
||
};
|
||
struct dm_invite_req {
|
||
struct UTUN_INSTANCE* inst;
|
||
uint64_t request_id, peer_node_id, channel_id;
|
||
char source_ch_id[64]; /* общая группа для новой PM; из шапки PM может быть пустой */
|
||
char peer_name[128];
|
||
int cancel;
|
||
struct chat_invite_build* pending; /* только uasync */
|
||
};
|
||
void dm_invite_trampoline(void* arg);
|
||
|
||
/* Запросить последние 200 сообщений со статусами. CHAT_EVT_DM_MESSAGES:
|
||
* [conv_id_len:1][conv_id:var][JSON:utf8]. Все чтения в uasync. */
|
||
struct dm_messages_req {
|
||
struct UTUN_INSTANCE* inst;
|
||
char conv_id[64];
|
||
};
|
||
void dm_messages_trampoline(void* arg);
|
||
/* Живой BGP-маршрут PM; group_id=0 в снимке означает недоступность. Все вызовы в uasync. */
|
||
int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id);
|
||
void dm_call_route_trampoline(void* arg); /* аргумент: dm_messages_req */
|
||
struct dm_conversations_req { struct UTUN_INSTANCE* inst; };
|
||
void dm_conversations_trampoline(void* arg);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
#endif /* DM_CORE_H */
|