119 lines
9.6 KiB
Markdown
119 lines
9.6 KiB
Markdown
# Зашифрованные вложения группового чата
|
||
|
||
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
|
||
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
|
||
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
|
||
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
|
||
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
|
||
|
||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
|
||
название «Зашифрованное вложение» вместо manifest.
|
||
|
||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
|
||
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
|
||
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
|
||
|
||
## Подписанное сообщение и контейнер
|
||
|
||
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
|
||
`<crypto_UUID:32hex>.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/<channel>/<crypto_UUID>.uenc`, локальная открытая
|
||
копия — в `decrypted/<channel>/<crypto_UUID>.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 без нового согласия и удаление канала.
|