155 lines
14 KiB
Markdown
155 lines
14 KiB
Markdown
# Зашифрованные вложения группового чата
|
||
|
||
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
|
||
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
|
||
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
|
||
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
|
||
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
|
||
|
||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||
нельзя открыть или сохранить как готовое вложение. До расшифровки цитата показывает
|
||
название «Зашифрованное вложение», после — обычный preview исходного вложения.
|
||
Список чатов сохраняет общее название зашифрованного вложения вместо 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`.
|
||
MIME — непустая NUL-строка длиной1..31 байт: ASCII33..126 без двойной кавычки
|
||
и обратной косой черты. Общий `encrypted_file_validate_type` применяется в send,
|
||
write и read; это проверка представления поля, без полного разбора MIME-грамматики.
|
||
Send отклоняет неверное значение до создания задачи/ключа/worker, write — до файловых
|
||
операций. Read проверяет расшифрованное аутентифицированное поле и при ошибке сохраняет
|
||
прежние info/type. Сырые неверные строки в диагностику не выводятся.
|
||
|
||
Каждая часть шифруется 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 worker.
|
||
|
||
`encrypted_file_write/read` возвращают закрытый проверенный `file_io_temp`,
|
||
не изменяя конечное назначение. Задача владеет им до publish/discard; запись идёт
|
||
в эксклюзивно созданный соседний файл с правами 0600 на POSIX. Одинаковые файлы,
|
||
включая `/./`, hardlink и конечную ссылку назначения, отклоняются до подготовки;
|
||
публикация повторно проверяет идентичность. При ошибке read не выдаёт новые метаданные.
|
||
|
||
Режим `FILE_IO_DURABLE` действует весь срок temp: flush/sync открытого файла/close,
|
||
затем rename и сохранение каталога. После успешного rename ошибка directory sync
|
||
не откатывает установленный результат; temp уже отсоединён от владельца.
|
||
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
|
||
какие артефакты задача действительно создала или опубликовала.
|
||
|
||
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
|
||
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
|
||
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
|
||
Конечные файлы и имена других форматов этот проход сохраняет; активные temp другого
|
||
instance того же процесса остаются защищены общим реестром.
|
||
|
||
## Хранение и разрешения
|
||
|
||
Локальные таблицы не регистрируются в 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` для этого не используется.
|
||
|
||
В шапке desktop-клиента кнопка «Запросы расшифровки» видна при наличии запросов без решения
|
||
и показывает их число.
|
||
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
|
||
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
|
||
и подтверждённая доставка ключа; отказ также сохраняется.
|
||
|
||
## Адресный протокол
|
||
|
||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
|
||
`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,
|
||
лишние байты, alias обоих API, сохранность существующего назначения/метаданных,
|
||
23 сценария отказов I/O с контролем fd/temp, включая ошибку после rename.
|
||
Для MIME проверяются все256 значений первого байта, NULL и границы поля,
|
||
восемь ранних отказов writer с сохранением файлов/output, три допустимых roundtrip
|
||
и семь аутентифицированных некорректных wire-полей с сохранением info/type/target.
|
||
`test_media_primitives` проверяет оба режима temp и границу durable-публикации;
|
||
`test_attachment_send` проверяет восемь отказов MIME до ключей/worker/файлов,
|
||
затем останавливает сервис после настоящего crypto rename до completion.
|
||
|
||
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
|
||
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
|
||
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
|
||
восстановление удалённого plaintext без нового согласия и удаление канала.
|
||
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
|
||
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
|