diff --git a/doc/encrypted_attachments.md b/doc/encrypted_attachments.md new file mode 100644 index 00000000..01392230 --- /dev/null +++ b/doc/encrypted_attachments.md @@ -0,0 +1,111 @@ +# Зашифрованные вложения группового чата + +В desktop chatgui кнопка замка рядом со скрепкой включает шифрование следующего +вложения или голосового сообщения. Выбор фиксируется вместе с адресатом до +подготовки; переход в другую беседу сбрасывает переключатель. В DM используется +существующее автоматическое E2E-шифрование лички. + +Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую +подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер +передаётся через обычные media_index/media_delivery. UUID транспортного медиа +и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки, +второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер. + +## Подписанное сообщение и контейнер + +В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела — +`.uenc`. Остальные поля manifest описывают шифротекст: +размер, SHA256 всего контейнера, UUID и подписи блоков. Ключ, исходное имя, +MIME, waveform и остальные медиаметаданные в историю канала не записываются. + +Контейнер: + +```text +magic "UTENC001":8 +crypto_UUID:16 +plaintext_size:8BE +encrypted_metadata_size:2BE +SHA256(encrypted_file_body):32 +encrypted_metadata:variable +encrypted_file_body:variable +``` + +Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`. +Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт. +Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт +с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`; +формат существующего DM при их выделении не менялся. + +Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных, +все tags файла, точный размер и hash тела. Результат пишется во временный файл +и публикуется после успешной проверки и durable rename. Все длительные файловые +операции и криптография выполняются в media_async. + +## Хранение и разрешения + +Локальные таблицы не регистрируются в db_sync или Merkle: + +- `attachment_keys`: ключ автора/получателя, связь с общим message_id, + crypto_UUID, hash и пути двух копий, расшифрованные метаданные и состояние. +- `attachment_key_requests`: один запрос на `(channel,message_id,peer)`, + направление, решение и дата создания. +- `attachment_key_outbox`: команда, адресат и время следующей попытки. + +Шифротекст хранится в `media//.uenc`, локальная открытая +копия — в `decrypted//.file`. В media_files и объявлениях +держателей остаётся только шифротекст, включая backfill после restart. +Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном +открытии удалённая открытая копия восстанавливается с сохранённым ключом. + +После полной проверенной загрузки обычный клиент автоматически запрашивает +ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не +создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ. +Сетевая классификация `peers.storage` для этого не используется. + +В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения. +Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа. +Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки +и подтверждённая доставка ключа; отказ также сохраняется. + +## Адресный протокол + +Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь +`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят. +Протокол не создаёт личную беседу и не публикует служебные сообщения в канал. + +```text +magic "AK01":4 | command:1 | message_id:32 | crypto_UUID:16 | cipher_hash:32 +GRANT дополнительно содержит key:32 + +REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5 +``` + +Автор принимает запрос только на собственное опубликованное сообщение с +сохранённым ключом. Участие запрашивающего проверяется по полному подписанному +мемберу, join и дереву приглашений; удалённые мемберы отклоняются. Проверка +повторяется при решении и перед каждой отправкой GRANT. Получатель принимает +ответ только от автора, по своему существующему запросу и точной идентичности +контейнера. Конфликтующие ключи и результаты отклоняются. + +REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются +в outbox только после сохранения решения. Полученный ключ сохраняется до +RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не +создают новых строк. Таймер обрабатывает ограниченную порцию очереди с +backpressure и повтором через 5 секунд; restart продолжает очереди. + +Для доставки нужен живой маршрут обеих сторон: офлайн-mailbox этого сервиса +не реализован. Запрос ждёт появления автора; решение ждёт появления получателя. +Уже выданный ключ невозможно отозвать у получателя. Защита содержимого не +скрывает автора, канал, время и размер шифротекста. Автоматическая публикация +транскрипции зашифрованного голосового в канал не запускается. + +## Проверки + +`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip, +неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash, +лишние байты и отсутствие частичного plaintext после отказа. + +`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A, +перезапуски автора с запросами и получателя с ключом, выдача B и отказ C, +отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT, +восстановление удалённого plaintext без нового согласия и удаление канала. diff --git a/src/dm/dm_media.c b/src/dm/dm_media.c index 01003e06..8b015715 100644 --- a/src/dm/dm_media.c +++ b/src/dm/dm_media.c @@ -25,6 +25,7 @@ #include "../transport_layer/secure_channel.h" #include "../transport_layer/etcp.h" #include "../media_delivery/file_transfer.h" +#include "../media_async/encrypted_file.h" enum dm_file_work { DM_FILE_PREPARE, DM_FILE_RECEIVE, DM_FILE_CUSTODY, DM_FILE_DELETE }; struct dm_file_job;