# Модель безопасности STIX Chat

Этот документ описывает реализованный протокол и его ограничения. Независимый аудит не проводился. Термин «сквозное шифрование» относится к содержимому сообщений и вложений при использовании доверенного клиента; метаданные и компрометация устройств остаются отдельными рисками.

## Идентичность и восстановление

При регистрации клиент генерирует RSA-OAEP SHA-256 ключ 3072 бит для получения ключей сообщений и ECDSA P-256 SHA-256 ключ для подписей. Сервер хранит только публичные JWK и зашифрованную копию приватного набора. Поля приватных ключей не допускаются в публичных JWK.

Новый ключ резервной копии — 5 случайных английских слов из клиентского словаря. Для шифрования клиент применяет PBKDF2-SHA-256 с IV backup как salt и затем AES-256-GCM; старые base64url ключи из 32 случайных байт остаются совместимы для восстановления уже созданных аккаунтов. Резервная копия шифруется AES-256-GCM со случайным 12-байтным IV и AAD `UTF8("STIX Chat identity backup v1")`. Открытая копия — UTF-8 JSON `{version:1,encryption:<private RSA JWK>,signing:<private EC JWK>,publicKey:{encryption:<public RSA JWK>,signing:<public EC JWK>}}`. Восстановление сверяет публичные ключи копии с публичной идентичностью аккаунта. Recovery key серверу не передаётся.

Рабочие приватные `CryptoKey` сохраняются в IndexedDB как non-extractable ключи. Это затрудняет прямой экспорт через Web Crypto, но не защищает от выполнения вредоносного JavaScript в том же origin: такой код может вызвать дешифрование. Сессионный токен сохраняется в IndexedDB для повторного открытия PWA и дублируется в sessionStorage активной вкладки. Оба хранилища очищаются при выходе; выполнение вредоносного JavaScript в origin может получить bearer-токен. Не храните ключ восстановления в общем чате, логах, аналитике или URL.

На новом устройстве SMS подтверждает доступ к аккаунту, после чего для восстановления идентичности есть два способа: ключ восстановления из пяти английских слов или SMS-подтверждённый набор QR с уже авторизованного устройства. Для QR-переноса исходное устройство сначала подтверждает номер кодом из SMS и только затем показывает серию QR-кодов с приватной идентичностью; ввод ключа восстановления или дополнительного кода на новом устройстве не требуется. Потеря recovery key и всех устройств с локальными ключами делает старые сообщения недоступными. Явный reset identity создаёт новые публичные ключи и encrypted backup для будущих сообщений; он не расшифровывает старую историю и не происходит незаметно через SMS. Для проверки собеседника сравните его криптографический отпечаток по другому доверенному каналу.

## Формат сообщения, версия 1

Все бинарные значения представлены unpadded base64url. UUID — строки в стандартном формате. Используется UTF-8. JSON создаётся компактно, без пробелов и завершающего перевода строки; порядок элементов массивов существенен. Идентификаторы и base64url не содержат символов, для которых реализации JSON обычно используют разное экранирование.

1. Создайте новый `clientId` UUID для логической отправки и получите актуальный состав чата.
2. Сгенерируйте 32 случайных байта AES-ключа и случайный IV из 12 байт. При каждом редактировании используйте новый ключ и IV.
3. Открытое содержимое — UTF-8 JSON `MessageContent`: `{text, attachments, replyTo?}`. Зашифруйте AES-256-GCM, tag 128 бит. AAD равен `UTF8(JSON.stringify([1, chatId, clientId]))`. GCM tag присоединяется к ciphertext в формате Web Crypto.
4. Для каждого текущего участника, включая отправителя, зашифруйте исходные 32 байта AES-ключа его RSA-OAEP ключом с SHA-256, MGF1 SHA-256 и пустым label. Запишите результат в `keys[userId]`.
5. Отсортируйте `Object.entries(keys)` по userId лексикографически (ASCII). Подпишите `UTF8(JSON.stringify([1, chatId, clientId, ciphertext, iv, sortedEntries]))` ключом ECDSA P-256 SHA-256. Подпись — ровно 64 байта `r || s` (IEEE P1363, каждое целое по 32 байта big-endian), не ASN.1 DER.
6. Отправьте `{clientId, envelope:{version:1,ciphertext,iv,keys,signature}, attachmentIds}`. Сигнатура проверяется и сервером, и получателем. `Message.sender` содержит публичную идентичность отправителя для проверки истории после его выхода из группы.

При расшифровке сначала проверьте подпись отправителя, затем разверните собственный ключ, выполните AES-GCM с тем же AAD и проверьте структуру открытого содержимого. Вложения в расшифрованном содержимом должны соответствовать разрешённым `attachmentIds`. Ошибки подписи/аутентификации не должны превращаться в отображение непроверенного текста.

При неопределённом результате отправки клиент сохраняет уже зашифрованный запрос в IndexedDB и предлагает явный повтор с прежним `clientId`, в том числе после перезагрузки. Эта исходящая очередь не содержит открытого текста и не отправляется автоматически в фоне. Ретрай с прежними `(chatId,senderId,clientId)` идемпотентен. При конфликте состава получите свежих участников и перешифруйте для них сообщение. Сервер требует точное совпадение набора ключей с текущим составом. Не передавайте открытые сообщения в логи или поля API.

## Вложения

Каждый файл имеет отдельный случайный 32-байтный AES-ключ и 12-байтный IV. Шифрование — AES-256-GCM с 128-битным tag, без AAD. Загружаются только зашифрованные байты `application/octet-stream`. Предел — 50 MiB исходных данных и ещё 16 байт tag.

Имя, MIME, размер исходного файла, тип превью, ключ, IV и длительность аудио находятся только в зашифрованном `Attachment`. Сервер видит ID, размер ciphertext, владельца и связи с чатом/сообщением. Превью фото, видео, PDF, STL, SVG, текстов, кода и CSV/TSV генерируется на устройстве после расшифровки. Остальные типы выдаются как скачивание, их содержимое не исполняется приложением.

Загрузку может прикрепить её владелец только к тому чату, куда она была загружена. Другой участник скачивает её только через доступное ему сообщение. Неприкреплённые загрузки удаляются фоновым сборщиком после 24 часов. Сервер не может выполнять содержательную антивирусную проверку зашифрованного файла; получатель открывает скачанные документы средствами своего устройства.

## Группы и история

Новый участник получает доступ к сообщениям после вступления; старые ключи для него не переупаковываются. Удалённый участник теряет серверный доступ и не получает ключи новых сообщений. Он сохраняет возможность читать ранее скачанные сообщения, для которых уже получил ключи. Повторное вступление создаёт новую границу видимости истории.

Владелец может переименовать группу, добавлять/удалять участников и передавать владение. Обычный участник может выйти. Владелец должен сначала передать владение. Сервер проверяет эти права независимо от интерфейса.

## Аутентификация и API

SMS-код — 6 цифр, срок 5 минут, до 5 попыток. Запросы ограничены по IP/телефону; сравнение и выдача токенов выполняются с атомарным расходованием кода. Код хранится как HMAC с серверным `AUTH_SECRET`, токены — в хешированном виде. Сессия действует 30 дней, регистрационный токен 10 минут; интеграционный токен — до 365 дней и только с выбранными `read`/`write` scope. Управление интеграционными токенами требует пользовательской сессии.

Телефон хранится на сервере открыто и не возвращается в публичных профилях. Поиск по точному телефону раскрывает наличие пользователя тому, кто знает номер; это осознанная функция поиска, ограниченная авторизацией и лимитами. SMS может быть уязвим к перехвату и перевыпуску SIM. SMS-доступ сам по себе не заменяет криптографический ключ восстановления; SMS-перенос работает только если на исходном устройстве уже есть локальная приватная идентичность.

Bearer-токены отправляются только в Authorization, включая SSE через fetch. Cookie-сессий и передачи токена через URL нет. Production работает через один HTTPS origin без wildcard CORS. Доверие к `X-Forwarded-For` ограничивается конкретным адресом Caddy; не добавляйте в `TRUSTED_PROXY_CIDRS` все сети.

## Push и кеширование

Push содержит общий текст о новом сообщении и ссылку на чат, без текста сообщения, имени отправителя и телефона. Push-провайдер видит доставку к конкретной подписке. Сервер сохраняет уведомления в транзакционную очередь и повторяет временные ошибки; недействительные подписки удаляются. Разрешены только HTTPS endpoints известных публичных провайдеров; сетевой клиент дополнительно защищает от обращений к внутренним адресам.

Service worker кеширует оболочку приложения. Кеширование API, зашифрованных сообщений и файлов не предназначено для офлайн-архива переписки. Доступ к старым данным без сети не гарантируется. iOS требует установки на домашний экран и явного пользовательского действия перед запросом push-разрешения.

## Границы гарантий

- Нет forward secrecy, post-compromise security, Double Ratchet, криптографической анонимности и совместимости с Signal. Компрометация долговременного RSA-ключа может позволить прочитать ранее сохранённые ciphertext и обёртки ключей этого пользователя.
- Сервер контролирует каталог публичных ключей и доставляемый web-клиент. Проверка отпечатков по другому каналу помогает обнаруживать подмену идентичности, но не устраняет риск скомпрометированного web origin.
- Названия групп, состав, отправители, времена, телефоны, ciphertext-размеры, push-подписки и операционные сетевые метаданные видимы серверу. TLS и шифрование дисков/бэкапов защищают их на других уровнях.
- Удаление сообщения не стирает чужие скриншоты, экспорт, локальную память устройств и ранее созданные резервные копии сервера.
- История может быть восстановлена только при наличии БД, зашифрованных файлов и подходящих клиентских ключей. Бэкап сервера не заменяет recovery key пользователя.
