diff --git a/src/dm/dm_media.c b/src/dm/dm_media.c index e9bf2f16d..53275d20d 100644 --- a/src/dm/dm_media.c +++ b/src/dm/dm_media.c @@ -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))) { diff --git a/src/dm/dm_media.h b/src/dm/dm_media.h index a551a63d0..02ce3c6a2 100644 --- a/src/dm/dm_media.h +++ b/src/dm/dm_media.h @@ -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.