Prepare attachments with an independent keypair and both party wraps

This commit is contained in:
evgeny committed 2026-11-03 12:51:10 +01:00
1 parent 08f9efb02b
commit de94b5bbaf
2 files changed
+56 -11

No files matched your search

+50 -11
View File
@@ -45,6 +45,7 @@ struct dm_file_job {
enum dm_file_work work;
struct file_transfer_op* transfer;
uint8_t id[16], key[32], hash[32];
uint8_t wrapped[DM_FILE_KEYS_SIZE]; /* Подписывается с metadata после подготовки. */
uint64_t author, size;
char conv[64];
struct attachment_info info;
@@ -88,6 +89,46 @@ const uint8_t* dm_message_media(const uint8_t* msg, size_t len) {
return !dm_message_parse(msg, len, &view) && view.size == len ? view.media : NULL;
}
int dm_media_metadata_decode(const uint8_t* data, size_t size, struct attachment_info* info, const uint8_t** keys) {
if (!data || !info || !keys) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM metadata: invalid arguments"); return -1; }
const uint8_t* wrapped = NULL;
if (size >= 8 && !memcmp(data, "DMKEY001", 8)) {
if (size <= DM_MEDIA_KEYS_HEADER_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_DM, "DM metadata: truncated file keys"); return -1; }
wrapped = data + 8; data += DM_MEDIA_KEYS_HEADER_SIZE; size -= DM_MEDIA_KEYS_HEADER_SIZE;
}
if (attachment_decode(data, size, info)) return -1;
*keys = wrapped; return 0;
}
/* Получение ключа разрешено только действующему участнику закреплённой беседы. */
static int dm_file_key(struct UTUN_INSTANCE* inst, const uint8_t* body, size_t len, uint8_t out[32]) {
struct dm_message_view view;
if (dm_message_parse(body, len, &view) || !view.media || view.size != len) return -1;
char conv[64]; snprintf(conv, sizeof(conv), "%llu", (unsigned long long)view.conv);
struct dm_party local[2], parties[2]; uint8_t key[32], secret[32], plain[DM_DATA_MAX], nonce[DM_NONCE_SIZE];
if (dm_conversation_access(inst, conv, local, key)) return -1;
size_t size = 0; const uint8_t* wrapped; struct attachment_info info; int result = -1;
dm_build_nonce(view.conv, view.author, view.seq, nonce);
if (media_ccm_decrypt(key, nonce, view.ciphertext, view.ciphertext_size, plain, &size) ||
dm_media_metadata_decode(plain, size, &info, &wrapped)) goto done;
if (!wrapped) {
if (view.mode != DM_MODE_DEVICE) { DEBUG_WARN(DEBUG_CATEGORY_DM, "DM file: GROUPS requires independent file keys"); goto done; }
memcpy(out, key, 32); result = 0; goto done;
}
unsigned side = view.author == inst->node_id ? 0 : 1;
if (view.mode == DM_MODE_GROUPS) {
int member = dm_message_target(&view, inst->node_id, NULL, NULL);
if (member < 0) goto done;
side = (unsigned)member; memcpy(parties, view.parties, sizeof(parties));
} else { parties[side] = local[0]; parties[1 - side] = local[1]; }
if (!dm_party_private(inst, parties + side, secret))
result = dm_file_keys_open(view.conv, view.author, view.media, parties, side, secret, wrapped, out);
done:
OPENSSL_cleanse(key, sizeof(key)); OPENSSL_cleanse(secret, sizeof(secret)); OPENSSL_cleanse(plain, sizeof(plain));
if (result) DEBUG_WARN(DEBUG_CATEGORY_DM, "DM file: key unavailable conv=%s author=%llu", conv, (unsigned long long)view.author);
return result;
}
uint64_t dm_media_cipher_size(uint64_t size) {
return size + (size ? (size + DM_MEDIA_PLAIN_SIZE - 1) / DM_MEDIA_PLAIN_SIZE : 1) * DM_TAG_SIZE;
}
@@ -350,6 +391,7 @@ int dm_media_track(struct UTUN_INSTANCE* inst, int dir, const uint8_t* msg, size
sqlite3_bind_text(st, 1, id, -1, SQLITE_STATIC);
int rc = sqlite3_step(st);
if (rc != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: conversation absent conv=%s rc=%d", id, rc); sqlite3_finalize(st); return -1; }
dir = author == inst->node_id; /* Здесь направление означает владение оригиналом, а не отображение своей реплики. */
recipient = dir ? (uint64_t)sqlite3_column_int64(st, 0) : inst->node_id;
group = (uint64_t)sqlite3_column_int64(st, 1);
sqlite3_finalize(st);
@@ -581,11 +623,13 @@ static void dm_file_done(void* arg, int error) {
(unsigned long long)job->generation, job->work); goto fail;
}
if (job->work == DM_FILE_PREPARE) {
uint8_t data[DM_MEDIA_DESC_SIZE + ATTACHMENT_WIRE_MAX];
uint8_t data[DM_MEDIA_DESC_SIZE + DM_MEDIA_KEYS_HEADER_SIZE + ATTACHMENT_WIRE_MAX];
memcpy(data, job->id, 16); memcpy(data + 16, &job->size, 8); memcpy(data + 24, job->hash, 32);
int n = attachment_encode(&job->info, data + DM_MEDIA_DESC_SIZE, ATTACHMENT_WIRE_MAX);
memcpy(data + DM_MEDIA_DESC_SIZE, "DMKEY001", 8);
memcpy(data + DM_MEDIA_DESC_SIZE + 8, job->wrapped, sizeof(job->wrapped));
int n = attachment_encode(&job->info, data + DM_MEDIA_DESC_SIZE + DM_MEDIA_KEYS_HEADER_SIZE, ATTACHMENT_WIRE_MAX);
if (n < 0) goto fail;
if (dm_send(inst, job->conv, "file", data, (uint32_t)(DM_MEDIA_DESC_SIZE + n))) goto fail;
if (dm_send(inst, job->conv, "file", data, (uint32_t)(DM_MEDIA_DESC_SIZE + DM_MEDIA_KEYS_HEADER_SIZE + n))) goto fail;
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_media: prepared conv=%s bytes=%llu name=%s", job->conv,
(unsigned long long)job->size, job->info.name);
chat_event_attachment(inst, 1, job->conv, job->id, 1);
@@ -761,14 +805,9 @@ int dm_send_file(struct UTUN_INSTANCE* inst, const char* conv, const char* path,
}
snprintf(job->conv, sizeof(job->conv), "%s", conv); job->info = *info; job->remove_source = remove_source;
snprintf(job->input, sizeof(job->input), "%s", path);
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT peer_x25519 FROM dm_conversations WHERE conv_id=?");
if (!st) { u_free(job); return -1; }
sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC);
uint8_t pub[32];
int rc = sqlite3_step(st), found = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == sizeof(pub);
if (found) memcpy(pub, sqlite3_column_blob(st, 0), sizeof(pub));
sqlite3_finalize(st);
if (!found || dm_derive_content_key(inst->my_keys.private_key, pub, job->key) || RAND_bytes(job->id, sizeof(job->id)) != 1 ||
struct dm_party parties[2];
if (dm_conversation_access(inst, conv, parties, NULL) || RAND_bytes(job->id, sizeof(job->id)) != 1 ||
dm_file_keys_create(strtoull(conv, NULL, 10), inst->node_id, job->id, parties, job->wrapped, job->key) ||
dm_file_path(media, 0, job->id, ".file", job->plain, sizeof(job->plain)) ||
dm_file_path(media, 0, job->id, ".cipher", job->cipher, sizeof(job->cipher)) ||
dm_file_path(media, 0, job->id, ".encrypt", job->temp, sizeof(job->temp))) {
+6
View File
@@ -21,6 +21,12 @@ struct UTUN_INSTANCE;
#define DM_MEDIA_DESC_SIZE 56 /* UUID:16, plaintext_size:8 native-endian, SHA256(ciphertext):32. */
#define DM_MEDIA_RECEIPT_SIZE 152
#define DM_MEDIA_MAX_SIZE (1024ULL * 1024 * 1024)
#define DM_MEDIA_KEYS_HEADER_SIZE 232 /* "DMKEY001" и DM_FILE_KEYS_SIZE байт. */
/* Расшифрованная metadata: magic + две упаковки ключа + attachment_info.
* keys получает заимствованный блок ключей либо NULL для сохранённого DEVICE-файла
* прежнего формата. 0/-1; info/keys обязательны, живут вместе с data. */
int dm_media_metadata_decode(const uint8_t* data, size_t size, struct attachment_info* info, const uint8_t** keys);
/* ETCP_RT_ID_DM_MEDIA=0x38: [cmd:1][body], router-подпись обязательна; числа native-endian.
* OFFER: [recipient:8][holder:8][expires:8][каноническое file-сообщение]. A предлагает себя с expires=0.