0.10.6
This commit is contained in:
@@ -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
@@ -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 аккаунта приложение продолжит
|
||||
показывать только уже реализованные локальные уведомления, пока процесс жив.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user