This commit is contained in:
wt
2026-07-30 20:07:51 +07:00
commit 92660a4ba0
228 changed files with 24496 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
# Архитектура Luma
## Слои
1. `XMPPService` конфигурирует Martin, TLS/SASL, XEP-модули, MUC, MAM,
HTTP Upload, XEP-0084 PEP User Avatar и Jingle.
2. `LumaCallEngine` связывает Martin Jingle с WebRTC, получает STUN/TURN через
XEP-0215 и публикует value-type `CallSnapshot` для интерфейса.
3. `LumaOMEMOStore` адаптирует постоянное состояние к Signal storage callbacks;
AES-GCM выполняется CryptoKit.
4. `AppModel` сводит сетевые события в UI-состояние, дедуплицирует MAM/live
сообщения и сохраняет snapshot локально.
5. SwiftUI views используют один и тот же слой моделей на iOS/iPadOS/macOS.
6. watchOS не держит отдельный XMPP-сокет: iPhone передаёт компактный snapshot,
текстовые ответы возвращаются немедленно или через гарантированную очередь,
а голосовые `.m4a` — фоновой файловой передачей WatchConnectivity. iPhone
копирует временный файл внутри callback, при необходимости переподключает
XMPP, отправляет запись через тот же XEP-0363/OMEMO-пайплайн и возвращает
часам отдельный результат фактической отправки.
## Контакты
`RosterModule` Martin передаёт initial roster и последующие roster-push в
`AppModel`. Модель отдельно сохраняет множество roster JID, поэтому удаление
контакта на Prosody сразу убирает его из раздела «Люди», но не уничтожает
локальную историю чата. Стандартный XMPP roster содержит людей, а не членство в
MUC, поэтому экран «Контакты» объединяет серверный roster с сохранёнными
XEP-0045-комнатами в отдельном разделе «Групповые чаты».
## Поток сообщения
Исходящий текст получает `origin-id`. Итоговая политика шифрования вычисляется
из глобальной настройки аккаунта и переопределения конкретного чата. При
включённом режиме текст шифруется OMEMO для известных устройств контакта и
собственных дополнительных устройств; при выключенном отправляется обычный
`<body>` внутри TLS-соединения. Ошибка OMEMO никогда не вызывает автоматический
fallback на plaintext. UI сразу показывает optimistic message и обновляет
состояние по результату записи/receipt.
Входящая stanza может прийти напрямую, через carbons или MAM. Сервис определяет
peer, расшифровывает payload, использует `origin-id`/stanza id для дедупликации и
передаёт value-type envelope в `AppModel`.
Свежая установка начинает MAM с ограниченной последней страницы RSM, а не с
самого старого сообщения. Инкрементальные проходы используют сохранённый
непрозрачный UID архива как RSM `after`; timestamp с небольшим перекрытием —
только миграционный и одноразовый recovery-путь для удалённого сервером UID.
Каждая stanza проходит проверку `queryid` и источника личного архива. Martin
публикует MAM-результаты на parser queue: ограниченный inbox принимает там всю
страницу и делает единственный hand-off на main actor после финального IQ.
Дешифрование выполняется по одной stanza с уступкой event loop, а декодированные
изменения становятся видимыми только одной атомарной пачкой вместе с новой
контрольной точкой. Один foreground-проход имеет конечный бюджет страниц и
времени; ошибка страницы не перезапускает весь архив сама. При уходе приложения
с экрана и на время записи/отправки видеосообщения активный MAM-запрос
закрывается, чтобы дешифрование истории не конкурировало с камерой и upload.
Только после commit `AppModel` обновляет SwiftUI, локальный snapshot и Apple
Watch; незавершённый проход при timeout/disconnect отбрасывается целиком.
Редактирование реализовано стандартным XEP-0308: исправление получает новый id и
`<replace>` с id исходного сообщения. `AppModel` проверяет совпадение bare JID
отправителя, заменяет только текстовый payload и отмечает сообщение как
изменённое. Один и тот же путь обрабатывает live, Carbons и MAM; исправления,
пришедшие раньше исходной stanza во время синхронизации, временно удерживаются в
очереди.
Ответ содержит `<reply xmlns="urn:xmpp:reply:0">` с id и JID автора исходного
сообщения; в MUC это полный occupant JID комнаты. Для клиентов без XEP-0461 тело дополнено `>`-цитатой, а её
границы отмечены XEP-0428 в Unicode scalar offsets. На приёме Luma удаляет
fallback из отображаемого текста и строит компактный bubble исходного сообщения.
Нажатие собирает все сообщения с тем же reply target id и открывает отдельный
фокус-слой: источник, количество ответов, ветку, дату и delivery status выбранного
ответа. Если XML-метаданных нет, но тело начинается с цитаты Conversations/Monal,
legacy parser отделяет цитату от ответа и показывает неподвижный bubble без
ложной ссылки на id.
Для XEP-0045 комната хранится как отдельный тип conversation. Martin MUC module
управляет входом, приглашениями, состоянием и occupants; сообщения `groupchat`
попадают в общий `ChatMessage` pipeline с псевдонимом отправителя. Ответ в
комнате разрешён только после получения XEP-0359 `stanza-id`, выданного самой
комнатой: локальный или origin id для межклиентной MUC-цитаты не используется.
При включённом OMEMO сервис проверяет disco feature `muc_nonanonymous`, получает
реальные JID online occupants и полные affiliation lists member/admin/owner,
строит Signal sessions для устройств каждого bare JID и передаёт их в
MartinOMEMO одним encrypted groupchat stanza. Если список недоступен, хотя бы
один получатель не имеет устройства или комната скрывает JID, stanza не
отправляется. Эхо собственной зашифрованной
stanza отдельно сохраняет назначенный комнатой `stanza-id`, даже когда OMEMO
декодер считает payload дубликатом, поэтому последующие XEP-0461 ответы остаются
межклиентно совместимыми.
XEP-0424 retraction проходит через тот же plaintext/OMEMO-путь, что и исходное
сообщение. Перед применением `AppModel` проверяет conversation и JID автора,
после чего заменяет payload tombstone, очищает медиакэш и блокирует дальнейшее
редактирование/пересылку. Retract, пришедший раньше исходной stanza при MAM,
временно удерживается в ограниченной очереди. Локальное удаление физически
убирает запись из snapshot и сохраняет отдельный набор подавленных id, чтобы
последующая MAM-синхронизация не добавила её обратно.
Пересылка текста создаёт новое сообщение с явной подписью источника. Медиа не
переиспользует старый `aesgcm://` URL: клиент получает расшифрованные байты и
запускает новую XEP-0363/OMEMO-загрузку для политики шифрования выбранного чата.
При включённом OMEMO медиа сначала шифруется случайным AES-GCM ключом, затем
encrypted blob загружается по XEP-0363. В OMEMO-сообщении передаётся
`aesgcm://` URL с ключевым материалом во fragment, поэтому upload service не
получает ключ расшифрования. При выключенном режиме исходный файл загружается по
HTTPS. Тело сообщения остаётся ровно одной обычной ссылкой, поэтому другие
клиенты могут распознать файл; для plaintext дополнительно отправляется
стандартный XEP-0066 OOB URL. По XEP-0454 `aesgcm` используется как схема URI,
но не добавляется к имени файла: исходное расширение остаётся в upload URL, чтобы
Conversations, Monal и другие клиенты могли восстановить медиатип после
расшифровки. Luma при этом продолжает читать старые собственные ссылки с
суффиксом `.aesgcm`. Тип и длительность созданных в приложении voice/video-note
кодируются в имени файла без изменения расширения.
Для исходящих фото и видео preview строится до загрузки. Для входящего файла
отдельной XMPP-миниатюры нет, поэтому общий медиакэш скачивает blob один раз, при
необходимости расшифровывает его, уменьшает фото через ImageIO или извлекает
первый кадр видео через AVFoundation. Корневой SwiftUI overlay показывает
исходное фото с pan/zoom либо видео через `VideoPlayer` на всё окно приложения.
Круглые видеосообщения продолжают воспроизводить локальный файл через
`AVPlayerLayer` прямо в ленте. Для voice тот же процессор читает PCM через
`AVAudioFile` и строит waveform. Один `MediaPlaybackCoordinator` управляет
музыкой и голосовыми: он гарантирует одно активное воспроизведение,
синхронизирует прогресс, перемотку и скорость речи.
Множественный picker сначала копирует security-scoped файлы во временный draft
каталог. `MediaPreviewProcessor` строит миниатюры и длительности до отправки;
пользователь может удалить элементы и добавить подпись. Файлы затем загружаются
последовательно тем же XEP-0363 pipeline, а подпись отправляется стандартным
текстовым ответом на первое вложение, когда доступен стабильный target id.
Геопозиция запрашивается через Core Location только после явного выбора пункта
вложения. Пользователь может поправить точку на MapKit-карте; сообщение
передаётся обычным телом RFC 5870 `geo:latitude,longitude`, поэтому проходит тем
же OMEMO/plaintext-путём, что и текст, и распознаётся совместимыми клиентами.
Аватары публикуются и читаются через XEP-0084. PNG нормализуется до 512 пикселей,
PEP-события обновляют интерфейс, а локальный cache позволяет показать изображение
до завершения подключения. vCard-temp используется только как best-effort мост
для старых клиентов.
## Поток звонка
В личном чате `AppModel` сначала запрашивает доступ к микрофону и, для видео,
камере. `LumaCallEngine` выбирает доступный full JID по presence/capabilities и
начинает XEP-0353 Jingle Message Initiation на выбранный full JID, а для клиента
без этой возможности использует прямой IQ session-initiate. Адресация JMI на
конкретный presence/caps resource нужна потому, что некоторые комбинации
клиента и сервера возвращают bare `proceed`: ожидание full JID тогда блокирует
trickle ICE, а отправка на bare JID маршрутизирует `transport-info` случайному
ресурсу. SDP преобразуется между Martin Jingle RTP и WebRTC; ICE-кандидаты
передаются через transport-info.
WebRTC создаёт отдельные audio/video tracks и шифрует медиапоток DTLS-SRTP.
XEP-0215 External Service Discovery даёт временные STUN/TURN параметры Prosody;
если сервер их не публикует, остаются host candidates и резервный публичный
STUN. Перед передачей в WebRTC записи проверяются: STUN URI формируется без
TURN-only query, а TURN принимается только с полной парой временных credentials.
Если WebRTC всё же отклоняет серверную конфигурацию, движок повторяет создание с
публичным STUN и затем только с host candidates. Активный звонок проецируется в
`CallSnapshot`, поэтому SwiftUI не получает
владение сетевой сессией или peer connection. Одновременно разрешён один звонок.
Для исходящего аудио+видео звонка порядок `<content/>` в `session-accept`
приводится к порядку `m=`-линий локального offer: Jingle идентифицирует потоки по
имени, тогда как WebRTC требует стабильного порядка media sections. Локальные
trickle ICE candidates накапливаются до успешного начального Jingle stanza и
получения удалённого SDP. Full JID уже выбран по presence/caps; последующие
`proceed`, `session-accept` и `transport-info` дополнительно проверяются против
этого endpoint, поэтому Prosody не маршрутизирует кандидаты другому ресурсу.
Полученный Jingle candidate преобразуется из SDP-атрибута
`a=candidate:…` в ожидаемую WebRTC candidate line `candidate:…`. Краткий ICE
`failed` во время поступления позднего TURN/video candidate получает
восьмисекундное окно восстановления.
Каждая точка завершения сессии передаёт в `CallHistoryPolicy` направление, фазу и
причину. Движок публикует один `CallHistoryEntry`, а `AppModel` сохраняет его как
локальный `ChatMessage.Kind.system` с типизированными call-метаданными. Наличие
`connectedAt` имеет приоритет над поздней ошибкой ICE: состоявшийся звонок остаётся
успешным и получает длительность. Удаление `activeCall` до закрытия Jingle-сессии
гарантирует, что ответный callback не создаст дубликат карточки.
Текущая реализация принимает вызов при живом XMPP-соединении. Wake-up завершённого
iOS-приложения намеренно не имитируется: production-путь требует VoIP push,
PushKit/CallKit и серверной APNs-инфраструктуры.
## Локальные уведомления
`NotificationCoordinator` является delegate системного notification center и
разрешает banner/list/sound в foreground. `NotificationPolicy` создаёт
уведомление только для нового входящего сообщения и подавляет дубликат, когда
пользователь уже смотрит этот же диалог. Для выгруженного процесса по-прежнему
нужен XEP-0357/APNs.
## Следующие модули
- APNs provider + XEP-0357 registration;
- современный `urn:xmpp:omemo:2` adapter и миграция устройств;
- MIX и реакции;
- SQLite/SQLCipher вместо JSON snapshot;
- PushKit/CallKit для фоновых входящих звонков и групповые звонки;
- share extension и notification service extension.
+215
View File
@@ -0,0 +1,215 @@
# Настройка Prosody для Luma
Ниже приведён ориентир для актуальных Prosody 0.12/13. Сохраните существующие
модули аутентификации, TLS и federation вашего сервера — пример показывает
только функции, необходимые клиенту.
```lua
VirtualHost "example.org"
modules_enabled = {
-- ваши базовые модули;
"roster"; -- RFC 6121: серверный список контактов
"pep"; -- PEP: OMEMO device lists/bundles и XEP-0084 аватары
"smacks"; -- XEP-0198
"carbons"; -- XEP-0280
"mam"; -- XEP-0313
"turn_external"; -- XEP-0215: STUN/TURN для Jingle-звонков
}
-- Тот же секрет укажите в coturn как static-auth-secret.
turn_external_secret = "ЗАМЕНИТЕ_ДЛИННЫМ_СЛУЧАЙНЫМ_СЕКРЕТОМ"
turn_external_host = "turn.example.org"
turn_external_port = 3478
-- Необязательно: если coturn принимает TURN/TLS на 5349.
turn_external_tls_port = 5349
default_archive_policy = true
archive_expires_after = "1mon"
max_archive_query_results = 100
-- Рекомендуется SQL-хранилище для архива.
storage = {
archive = "sql";
}
Component "upload.example.org" "http_file_share"
-- Luma не задаёт собственного предела; выберите серверный лимит под диск
-- и reverse proxy. Ниже пример на 1 ГиБ.
http_file_share_size_limit = 1024 * 1024 * 1024
http_file_share_expires_after = "1 month" -- Prosody 13
modules_disabled = { "s2s" }
-- XEP-0045: групповые комнаты Luma/Conversations/Monal
Component "conference.example.org" "muc"
name = "Групповые чаты"
restrict_room_creation = false -- либо "local" для локальных пользователей
modules_enabled = { "muc_mam" }
muc_log_by_default = true
muc_log_expires_after = "1mon"
```
## Аудио- и видеозвонки
Сам XMPP-сервер только согласовывает Jingle-сессию. Для двух клиентов с
доступными адресами этого достаточно, но за NAT/CGNAT нужен coturn. Начиная с
Prosody 0.12 проще всего использовать встроенный `mod_turn_external`, как в
примере выше: он публикует STUN/TURN и выдаёт клиенту временные учётные данные
через XEP-0215.
Минимальные соответствующие параметры coturn:
```ini
fingerprint
use-auth-secret
static-auth-secret=ЗАМЕНИТЕ_ТЕМ_ЖЕ_СЕКРЕТОМ
realm=example.org
listening-port=3478
tls-listening-port=5349
# Для TURN/TLS также задайте cert= и pkey=.
```
Откройте для coturn UDP/TCP 3478, при использовании TLS — TCP 5349, а также
настроенный UDP relay range. После перезапуска проверьте выдачу сервиса:
```bash
prosodyctl check turn
```
Без собственного TURN Luma использует публичный STUN только для обнаружения
адресов; STUN не может ретранслировать медиапоток, поэтому звонок между двумя
сложными NAT может не установиться.
Luma проверяет ответы XEP-0215 до создания WebRTC-соединения. Для `turn`/`turns`
Prosody должен выдать и `username`, и `password`; `stuns`/`turns` допускают TCP
(или отсутствие явного `transport`), а истёкшие временные credentials
игнорируются. Если одна серверная конфигурация всё же не принимается WebRTC,
клиент повторяет запуск с публичным STUN, затем с прямыми host candidates. В
Debug-консоли при таком fallback появляется строка `Luma WebRTC:` без URL и
учётных данных.
Для группового OMEMO комната должна быть неанонимной; рекомендуется также
members-only. При создании комнаты Luma сама отправляет owner configuration с
`muc#roomconfig_whois = anyone`, `muc#roomconfig_membersonly = true` и
`muc#roomconfig_persistentroom = true`, а также разрешает ролям moderator и
participant читать member list. Для уже существующей комнаты включите в
настройках владельца показ реальных JID всем участникам и доступ к списку
участников. Если вошедший
пользователь — владелец, Luma попробует изменить `whois` при первой зашифрованной
отправке; обычный участник не может менять эту настройку.
В Prosody 0.12.x срок хранения задаётся числом секунд, например
`http_file_share_expires_after = 31 * 24 * 60 * 60`; строковый интервал выше
используйте на Prosody 13.
Для `upload.example.org` нужен корректный HTTPS URL. Если компонент не является
прямым поддоменом VirtualHost, добавьте его в `disco_items`. При reverse proxy
задайте `http_host`/`http_external_url` согласно вашей топологии. Например, для
`wthome.ru` за HTTPS reverse proxy:
```lua
Component "upload.wthome.ru" "http_file_share"
http_file_share_size_limit = 1024 * 1024 * 1024
http_host = "upload.wthome.ru"
http_external_url = "https://upload.wthome.ru/"
trusted_proxies = { "127.0.0.1", "::1" }
modules_disabled = { "s2s" }
```
Reverse proxy должен передавать `Host: upload.wthome.ru` и
`X-Forwarded-Proto: https`. Иначе Prosody может выдать клиенту `http://` URL или
маршрут, возвращающий 404; Luma намеренно не понижает upload до HTTP.
Проверьте также:
- валидный сертификат и STARTTLS на 5222 либо direct TLS на 5223;
- SRV-записи `_xmpp-client._tcp` и, если используется, `_xmpps-client._tcp`;
- доступность server disco и upload component disco;
- права пользователей на PEP/pubsub nodes;
- доставку PEP notifications `urn:xmpp:avatar:metadata+notify` контактам;
- что MAM действительно пишет в постоянное хранилище, а не fallback memory.
- доступность `conference.example.org` через service discovery и возможность
локального пользователя создать/войти в комнату.
- успешный `prosodyctl check turn` и доступность relay-портов coturn извне.
Полезные команды:
```bash
prosodyctl check config
prosodyctl check certs
prosodyctl shell module info http_file_share
prosodyctl shell http list upload.wthome.ru
```
Начиная с 0.3.0 Luma различает ошибки discovery, получения upload slot,
транспортную ошибку PUT и HTTP status. Если приложение пишет, что XEP-0363 не
найден, проверьте `disco_items`; если показывает HTTP 404/413/5xx — проверяйте
`http_host`, proxy path и лимит файла соответственно.
## Push на iOS
`mod_cloud_notify` реализует серверную сторону XEP-0357, но одного включения
модуля недостаточно. Нужен доступный по XMPP push gateway разработчика Luma,
который принимает события Prosody и отправляет APNs. В текущем MVP клиент не
регистрирует такой endpoint, потому что его адрес, APNs topic и ключи зависят от
вашей Apple Developer учётной записи.
Когда gateway будет готов, включите `cloud_notify` вместе со `smacks`, `mam` и
`carbons`; не включайте передачу реального тела или sender в push без отдельного
решения по приватности.
### Совместимость с сервером Monal
Можно использовать открытый сервер
[`monal-im/fpush`](https://github.com/monal-im/fpush), но его нужно развернуть
для Luma либо договориться с владельцем уже работающего экземпляра о добавлении
отдельного push-модуля. Подключить Luma к production endpoint Monal как к
универсальному APNs relay нельзя: APNs device token относится к конкретному
приложению, а gateway должен подписывать запросы сертификатом этого приложения
и указывать его bundle ID (`app.luma.chat`) как topic.
`fpush` подключается к Prosody как XEP-0114 component и поддерживает несколько
приложений через `pushModule`. Минимальная конфигурация его APNs-модуля выглядит
так (секреты и сертификат не храните в репозитории):
```json
{
"component": {
"componentHostname": "push.example.org",
"componentKey": "CHANGE_ME",
"serverHostname": "127.0.0.1",
"serverPort": 5347
},
"pushModules": {
"lumaProdIOS": {
"type": "apple",
"is_default_module": true,
"apns": {
"certFilePath": "/run/secrets/luma-apns.p12",
"certPassword": "CHANGE_ME",
"topic": "app.luma.chat",
"environment": "production"
},
"ratelimit": {
"ratelimitTime": "20s",
"ratelimitCleanupInterval": "300s",
"enabled": true
}
}
},
"timeout": { "xmppconnectionError": "20s" }
}
```
Для полного подключения ещё нужны:
1. Push Notifications capability и подходящий provisioning profile для Luma.
2. Регистрация iOS-приложения в APNs и получение device token.
3. Отправка XEP-0357 `<enable/>` с `node` = APNs token, JID компонента и полем
`pushModule=lumaProdIOS`; `<disable/>` при выходе из аккаунта.
4. `smacks`, `mam`, `carbons`, `cloud_notify` в Prosody и доступ компонента
`push.example.org` по component-порту или s2s — в зависимости от схемы
развёртывания.
Без APNs credentials от Apple Developer аккаунта приложение продолжит
показывать только уже реализованные локальные уведомления, пока процесс жив.
+78
View File
@@ -0,0 +1,78 @@
# Безопасность и модель угроз MVP
- Пароль XMPP сохраняется в Keychain и не попадает в `UserDefaults` или
snapshot истории.
- Проверка сертификата не отключается. Ручной host меняет маршрут подключения,
но XMPP domain/JID остаётся исходной идентичностью.
- TLS trust оценивается системным Apple Security framework как сертификат
сервера (`SecPolicyCreateSSL(true, ...)`) для домена из JID; hostname и цепочка
доверия обязательны даже при ручном адресе подключения.
- Когда OMEMO включено глобально или для конкретного чата, исходящие сообщения
не откатываются на plaintext при ошибке: ошибка показывается пользователю.
- Пользователь может осознанно отключить OMEMO глобально или для отдельного
чата. В этом режиме сервер, MAM-хранилище и участники TLS-терминации видят текст.
- OMEMO доступно и для XEP-0045, но только в неанонимной комнате: клиенту нужны
реальные bare JID участников, чтобы зашифровать ключ для каждого устройства и
расшифровать входящее сообщение от правильной Signal identity. Luma проверяет
режим комнаты, собирает online occupants и доступные списки member/admin/owner,
а при скрытом JID или отсутствии устройств отменяет отправку без plaintext
fallback. Новые комнаты Luma создаются members-only и persistent с
`muc#roomconfig_whois=anyone`.
- Первый увиденный ключ получает доверие TOFU. Изменившийся ключ становится
undecided и требует ручной проверки отпечатка.
- При включённом OMEMO вложения шифруются до загрузки, а URL с ключом находится
внутри OMEMO payload. При выключенном OMEMO файл хранится на HTTP Upload
сервисе в исходном виде; ссылка и метаданные также не имеют E2EE.
- Даже для зашифрованного blob HTTP Upload сервис видит размер и транспортное
имя файла; у voice/video-note это имя содержит тип и округлённую длительность.
- Клиент принимает только HTTPS `put`/`get` URL от XEP-0363 компонента и не
понижает транспорт загрузки до обычного HTTP.
- В OMEMO-исправлении новое тело зашифровано, но стандартный XEP-0308
`<replace>` остаётся метаданными stanza: сервер видит id исправляемого
сообщения, но не новый текст.
- Ответ содержит id исходного сообщения и JID его автора. В зависимости от
поведения OMEMO-библиотеки и клиента эти XEP-0461 метаданные могут быть видны
серверу, даже когда текст ответа зашифрован.
- XEP-0424 не гарантирует стирание: retract может быть проигнорирован сервером
или клиентом, а получатель мог сохранить исходный текст. Fallback retraction
намеренно общий и никогда не повторяет удаляемое содержимое.
- Для inline-preview расшифрованные фото, видео, музыка и голосовые временно
записываются в sandbox `tmp`; Luma очищает известные preview-файлы при смене
аккаунта, а ОС может удалить их раньше как временные данные.
- Выбранные для пакетной отправки файлы временно копируются в `tmp` для экрана
предпросмотра и удаляются при отмене либо после завершения отправки.
- Core Location вызывается только из экрана явной отправки точки. Выбранные
координаты становятся содержимым сообщения и защищены OMEMO лишь тогда, когда
шифрование включено для этого чата; при plaintext сервер и MAM видят точку.
- Аудио/видео звонка шифруются WebRTC DTLS-SRTP между конечными клиентами.
Это не OMEMO: XMPP-сервер видит Jingle-сигналинг и адрес собеседника, а
STUN/TURN-сервис видит сетевые адреса и объём трафика. TURN ретранслирует уже
зашифрованный медиапоток. Если Prosody не публикует собственный сервис через
XEP-0215, резервный публичный STUN получает запрос на определение адреса.
- На iOS файлы OMEMO-состояния и локальной истории получают data-protection
`completeUntilFirstUserAuthentication`.
## Известные ограничения
- На macOS история и OMEMO state лежат в sandbox Application Support без
дополнительного SQLCipher-слоя; защиту обеспечивает учётная запись и FileVault
пользователя. Для повышенной модели угроз нужна зашифрованная база.
- Локальная история содержит уже расшифрованные тексты, как и у большинства
клиентов. Полное удаление требует выхода с опцией удаления истории.
- «Удалить у меня» удаляет запись только из локального snapshot этого устройства
и запоминает id против повторной MAM-загрузки; оно не удаляет серверный архив,
другие собственные устройства или копию собеседника.
- Глобальная политика шифрования хранится в `UserDefaults` отдельно для каждого
JID, а переопределение чата — в локальном snapshot. Эти значения не являются
секретами, но прямо влияют на конфиденциальность следующей отправки.
- Аватары XEP-0084/vCard не шифруются OMEMO. Их видимость определяется
настройками PEP и roster на сервере; не используйте аватар как секретное фото.
- Используемая реализация MartinOMEMO поддерживает распространённый legacy
namespace. Она не должна называться полной реализацией новой редакции
XEP-0384 без отдельного interoperability test suite.
- В открытой или плохо настроенной сторонней MUC списки офлайн-участников могут
быть недоступны. Для предсказуемого группового OMEMO используйте members-only
комнату, разрешающую участникам получать affiliation lists; иначе отключите
OMEMO именно для этой комнаты осознанно.
- Jailbreak/root, скомпрометированный endpoint и запись экрана находятся вне
модели угроз MVP.