# Зашифрованные вложения группового чата В 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`, базовое имя тела — `.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 и очистка после их завершения удаляют оставшиеся `.uenc.tmp.` / `.file.tmp.` в `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//.uenc`, локальная открытая копия — в `decrypted//.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, конечные файлы и чужие имена сохраняются.