230 lines
24 KiB
Plaintext
230 lines
24 KiB
Plaintext
Личные сообщения: устройства и подгруппы
|
|
======================================
|
|
|
|
Дата: 03.11.2026. Ветка реализации: feature/dm-devgroups.
|
|
Статус: реализация начата; доступ после выхода согласован в разделе 9.
|
|
Выполнение этапов отмечается ниже.
|
|
|
|
1. Два режима беседы
|
|
-------------------
|
|
DEVICE — существующая личка конкретное устройство ↔ конкретное устройство,
|
|
включая переписку между своими устройствами. Наличие подгрупп ничего не меняет.
|
|
Сохраняются существующие идентификаторы таких бесед и поведение интерфейса.
|
|
|
|
GROUPS — один режим для обеих сторон: сторона представлена своей подгруппой,
|
|
а при отсутствии подгруппы — конкретным устройством. Это не четыре режима.
|
|
Выбранные стороны фиксируются в беседе, самопроизвольного перехода между
|
|
режимами при создании/удалении подгруппы нет. Переименование не меняет ID.
|
|
|
|
Send message на подгруппе открывает GROUPS. На устройстве с подгруппой — DEVICE.
|
|
Если устройство назначения без подгруппы, а у отправителя подгруппа есть,
|
|
интерфейс предлагает «С подгруппами» / «Только устройства».
|
|
Если подгрупп нет у обеих сторон — DEVICE.
|
|
Идентичности сторон: тип + ID узла или публичный Ed25519 ID подгруппы;
|
|
криптографический X25519 ключ связан с проверенной подписанной записью.
|
|
ID GROUPS выводится из режима и канонически упорядоченной пары идентичностей.
|
|
Короткий conv_id не заменяет сравнение полной пары при загрузке/приёме.
|
|
|
|
2. Получатели и сообщения
|
|
------------------------
|
|
Для каждого сообщения фиксируется список всех известных отправляющему узлу
|
|
проверенных устройств обеих сторон, включая офлайн-устройства. Используются
|
|
существующие node registry и проверка подписей devgroup; новый глобальный
|
|
каталог и поиск неизвестных устройств не вводятся. Источник уже имеет копию.
|
|
Устройства, обнаруженные позднее, участвуют в следующих сообщениях; доставка
|
|
старой истории новым устройствам в эту задачу не входит.
|
|
DEVICE адресуется только выбранным двум узлам.
|
|
При превышении допустимого размера списка отправка отклоняется целиком:
|
|
нельзя молча обрезать адресатов или показывать успешную полную доставку.
|
|
|
|
Ключ сообщения: (conv_id, author_node_id, seq). Счётчик устойчивый, отдельный
|
|
для автора в беседе. Время не определяет уникальность. Проверенные собственные
|
|
реплики показываются исходящими, но не становятся новым источником рассылки.
|
|
Повтор допускается только с тем же подписанным телом; конфликт отклоняется.
|
|
Подпись охватывает стороны, снимок адресатов, содержимое и дескриптор вложения.
|
|
Все изменения истории, счётчиков и очереди атомарны. ACK только после commit.
|
|
|
|
Для каждого адресата отдельно учитываются попытки, дедлайн прямой доставки,
|
|
принятие промежуточным сервером и подписанный ACK получателя. Сначала доставка
|
|
через существующий маршрут P2P; маршрутизируемый транзит ETCP допустим.
|
|
При отсутствии маршрута или истечении времени без ACK — offline-mailbox S.
|
|
Наличие маршрута само по себе не должно бесконечно откладывать fallback.
|
|
Нет разрешённой общей CHAT-группы/сервера — явное ожидание с причиной в логе.
|
|
Принятие S и транспортный ACK не означают доставку в историю адресата.
|
|
Повторы, рестарт и параллельный путь через S не создают второе сообщение.
|
|
|
|
3. Шифрование
|
|
-------------
|
|
В GROUPS стороны используют ключ подгруппы, при её отсутствии — ключ узла.
|
|
В DEVICE всегда ключи конкретных устройств. Текст доступен обеим сторонам.
|
|
|
|
Для КАЖДОГО вложения создаётся отдельная X25519 пара F_public/F_private.
|
|
Один шифротекст хранится и передаётся всеми источниками без перешифрования.
|
|
F_private независимо упаковывается два раза: открытым ключом стороны
|
|
отправителя и открытым ключом стороны получателя. Обе упаковки находятся
|
|
в подписанном сообщении. Любой член PM раскрывает доступную ему упаковку.
|
|
Это две независимые упаковки, не последовательное двойное шифрование.
|
|
Серверу хранения передаются только шифротекст и подписанный дескриптор.
|
|
|
|
Криптографическая реализация: стандартный HPKE X25519/HKDF-SHA256 для
|
|
инкапсуляции к F_public; export-only контекст выводит ключ существующего
|
|
потокового AES-256-CCM. HPKE AES-256-GCM независимо оборачивает F_private
|
|
для каждой стороны. Encapsulation, параметры, UUID, размеры и SHA256
|
|
шифротекста входят в подписанный дескриптор. Info/AAD разделяют назначения
|
|
и связывают операции с беседой, автором, UUID и стороной; не создают зависимости
|
|
ключа от ещё не вычисленного хеша шифротекста. Все входы вывода файлового
|
|
ключа фиксируются до фоновой подготовки файла. Секреты очищаются после работы.
|
|
Требование OpenSSL HPKE проверяется сборкой на всех платформах, без самодельного
|
|
криптографического fallback. Повтор отправки использует исходные ключи/тело;
|
|
новая подготовка — новые UUID и пару. Nonce не переиспользуется с другим текстом.
|
|
|
|
4. Независимые факты и автозагрузка
|
|
----------------------------------
|
|
MESSAGE_RECEIVED: сообщение сохранено адресатом.
|
|
FILE_RECEIVED: файл полностью скачан, проверен и опубликован локально.
|
|
TAKEN: получатель действительно открыл вложение по действию пользователя.
|
|
Эти факты не заменяют друг друга; квитанции подписывает конкретное устройство.
|
|
Автозагрузка, получение сообщения, показ карточки/миниатюры и начало скачивания
|
|
не создают TAKEN. Открытие на устройстве стороны отправителя не удаляет custody.
|
|
В DEVICE между своими устройствами выбранное второе устройство — получатель.
|
|
|
|
Автозагрузка соблюдает настройки локального устройства и лимит размера.
|
|
Ручное скачивание разрешено при выключенной автозагрузке. Поскольку отправитель
|
|
не знает чужие локальные настройки, отсутствие готовой копии/отказ от
|
|
автозагрузки — отдельный результат файловой доставки, не ACK сообщения.
|
|
Сначала передаём P2P. Если остались устройства без файла (в том числе свои)
|
|
из-за офлайна, настроек или ошибки — заказываем хранение на M через S.
|
|
Этот заказ существует независимо от очереди сообщений: сообщение может быть
|
|
подтверждено всеми устройствами, а файл ещё нужен. Один blob на вложение.
|
|
Исключение: при политике «до загрузки всеми получателями» выполнение этого
|
|
условия завершает хранение; собственные офлайн-устройства его не продлевают.
|
|
|
|
5. Источники и скачивание
|
|
------------------------
|
|
Перед скачиванием параллельно опрашиваются все известные онлайн-устройства
|
|
своей стороны, собеседника и назначенные серверы хранения. Запрос HAVE относится
|
|
к точному UUID/хешу и имеет ID. Ответ подтверждает наличие проверенной копии.
|
|
В ограниченном окне собираются ответы, выбирается минимальный измеренный RTT
|
|
запроса к конечному источнику. Одновременно идёт одна передача данных.
|
|
После ошибки — следующий ответивший источник; исчерпаны — новый ограниченный
|
|
цикл проб. Поздние ответы старого запроса игнорируются; успешную передачу
|
|
не переключаем из-за позднего меньшего RTT.
|
|
|
|
Каждый получивший файл узел сохраняет проверенный шифротекст и может отдавать
|
|
его разрешённым участникам этой беседы. Проверяется право конкретного узла,
|
|
а не знание UUID. M не получает секретов. FILE_RECEIVED — факт успешного
|
|
сохранения в прошлом, не обещание вечного хранения локального кеша.
|
|
Переход между источниками первоначально начинает временный файл с нуля:
|
|
существующий file_transfer поддерживает resume внутри передачи одному пиру,
|
|
но не подтверждает совместимость временного файла разных операций.
|
|
|
|
6. Политика M и удаление
|
|
-----------------------
|
|
Настройка сервера: dm_media_delete_on_open (по умолчанию 1).
|
|
Срок — существующая dm_media_ttl_sec. Политика и expires_at фиксируются при
|
|
первом принятии вложения. Дубликаты/рестарт срок не продлевают; изменение
|
|
настройки действует для новых заданий. S получает принятую политику и срок.
|
|
|
|
delete_on_open=1 delete_on_open=0
|
|
истёк срок удалить удалить
|
|
первый TAKEN получателя удалить сам по себе не удаляет
|
|
все получатели FILE_RECEIVED
|
|
недостаточно удалить
|
|
|
|
«Все получатели» — фиксированный получательский список этого сообщения;
|
|
в DEVICE в нём один узел. Собственные устройства отправителя не задерживают
|
|
удаление. Список не расширяется задним числом. Сервер проверяет отдельные
|
|
подписанные квитанции, а не доверяет неподписанному счётчику от S.
|
|
При delete_on_open=1 автозагрузка всеми не удаляет уже принятую копию.
|
|
При delete_on_open=0 TAKEN до загрузки остальными не запрещает нужный STORE.
|
|
|
|
TAKEN — факт открытия, не безусловная команда DELETE. Его подпись и принадлежность
|
|
открывшего получательской стороне данного сообщения проверяются до применения
|
|
политики. Поддельные, чужие, повторные квитанции не меняют условие завершения.
|
|
Если условие завершения уже выполнено, запоздалый STORE не создаёт новую копию.
|
|
Удаление: устойчиво записать причину завершения → запретить новые выдачи →
|
|
отменить передачи custody → удалить файл worker-ом → сохранить tombstone →
|
|
подтвердить физическое удаление. Поздние STORE/STORED/callback не воскрешают файл.
|
|
Локальный endpoint-кеш и custody разделены; удаление M не удаляет личную копию.
|
|
При недоступном M удаление происходит после получения команды или по его TTL.
|
|
Сообщение и ключевые упаковки сохраняются независимо от времени жизни файла M.
|
|
После удаления M доступ зависит от копий участников: офлайн — ожидание,
|
|
утрата всех копий — файл недоступен. Не обещаем восстановление без источников.
|
|
|
|
7. Владение модулями и состояния
|
|
--------------------------------
|
|
dm_identity: типизированные стороны, их разрешение и проверенные устройства.
|
|
dm_protocol: единый разбор/сериализация подписанного тела и квитанций;
|
|
не размножать фиксированные смещения по core/mailbox/media/frontend.
|
|
dm_core: беседы, история и доставка сообщений адресатам.
|
|
dm_history: локальное удаление, поколения и защита от повторного появления.
|
|
dm_media: подготовка, локальные файлы, поиск источников, автозагрузка/открытие.
|
|
dm_mailbox: промежуточная доставка сообщений.
|
|
dm_mailbox_media: устойчивый координатор хранения и подтверждений.
|
|
file_transfer: существующий асинхронный транспорт неизменяемого файла.
|
|
|
|
SQLite/маршрутизация/состояния выполняются в uasync-потоке; операции с файлами
|
|
и тяжёлая криптография — существующими media workers. Сохраняются generation,
|
|
busy/hold и порядок quiesce/drain/destroy. Нет второй независимой DM-системы.
|
|
Список доставки и сообщения имеют одного владельца; реплики не создают рассылку.
|
|
Изменение формата допустимо без совместимости со старым сетевым протоколом.
|
|
Не сбрасывать счётчики/историю существующих прямых бесед и не переподписывать
|
|
старое сообщение с уже использованным ID. Преобразование схемы и точное
|
|
представление старых записей требуется проверить до изменения рабочего кода.
|
|
|
|
Qt/Android/headless получают итоговый conv_id от ядра и явный mode;
|
|
не вычисляют результат по двум node_id. Асинхронный ответ сопоставляется
|
|
по request_id. Перепроверить приглашения/удаление/поколения и маршрут звонка,
|
|
чтобы выбор конкретного устройства не потерялся при появлении GROUPS.
|
|
|
|
Логи dm/media отражают ID беседы/сообщения/файла, адресата, переход состояния,
|
|
попытку, выбранный маршрут/RTT, принятый срок/политику и причину удаления/отказа.
|
|
Ключи, plaintext и PIN в лог не выводятся. Ошибочные ветки не молчат.
|
|
|
|
8. Этапы реализации и проверки
|
|
-----------------------------
|
|
[+] Создать отдельную ветку и сохранить план; исходный рабочий diff сохранён
|
|
в /tmp/utun2-before-dm-devgroups.patch, существующие UI-правки не откатываются.
|
|
[ ] Идентичности/проверка membership, два режима, канонический ID GROUPS,
|
|
разрешение списка известных устройств; unit-тесты подделки/коллизии/лимита.
|
|
[ ] Подписанный формат, счётчики по автору, схема истории/доставки и её переход;
|
|
DEVICE↔DEVICE и свои устройства, два автора с seq=1, точный/конфликтный повтор.
|
|
[ ] Рассылка, per-device ACK, fallback по дедлайну, mailbox/restart;
|
|
A1/A2/B1/B2, офлайн собственный узел, отсутствующий общий канал, поздний ACK.
|
|
[ ] Персональная пара ключей файла и две упаковки, потоковая подготовка;
|
|
обе стороны раскрывают один blob, третья не раскрывает, порча/чужой контекст
|
|
отвергаются; сборки desktop/Android проверяют наличие HPKE.
|
|
[ ] Автозагрузка/ручная загрузка, FILE_RECEIVED/TAKEN, сохранение ciphertext,
|
|
независимый заказ хранения, параллельный HAVE/RTT/failover и авторизация.
|
|
[ ] Политика M, TTL и завершение по получательскому snapshot; потеря/дубли
|
|
квитанций, рестарт, поздние STORE/STORED, удаление во время worker/передачи,
|
|
SQL/дисковые ошибки, совмещённый S=M и endpoint=M.
|
|
[ ] Qt/Android/headless: действия подгруппы/устройства, выбор режима при fallback,
|
|
открытие файла, статусы отдельных фактов, request_id и возврат conv_id ядром.
|
|
[ ] Обновить нормативную документацию; выполнить целевые DM/devgroup тесты,
|
|
Qt/Android проверки и полный ./check.sh. Известные исходные отказы из
|
|
doc/tasks.md отделять от новых регрессий. Итог фиксировать с логами.
|
|
|
|
9. Обнаруженный вопрос: выход из подгруппы
|
|
----------------------------------------
|
|
chat_devgroup_leave → cd_commit удаляет devgroup_local, включая приватный
|
|
X25519 ключ подгруппы (src/chat/chat_devgroup.c). Стороны GROUPS закреплены,
|
|
а упаковка F_private адресована этому ключу. Поэтому после выхода без
|
|
дополнительного хранения секретов нельзя расшифровать ни ещё не скачанные
|
|
вложения, ни ранее адресованные, но ещё не полученные сообщения.
|
|
Неявное переключение старой беседы на DEVICE неверно: это другие ключи и ID.
|
|
|
|
Решение пользователя: выход завершает получение старой GROUPS-беседы;
|
|
оставляем читаемыми только уже сохранённую историю и скачанные вложения.
|
|
Прежний групповой X25519 секрет не сохраняем. Локальные ключи конкретных бесед
|
|
и уже сохранённых вложений защищаем собственным ключом устройства. Отправка,
|
|
новый приём и скачивание отсутствующих вложений старой GROUPS-беседы закрываются;
|
|
незавершённые передачи/worker отменяются и не публикуют результат после выхода.
|
|
Закреплённая беседа не меняет режим; новую переписку открываем отдельной беседой.
|
|
|
|
Это также выбор доступа, а не только удобства: существующий выход не меняет
|
|
ключи на остальных устройствах. Сохранённый групповой X25519 секрет технически
|
|
подходит и для будущих шифротекстов этой группы; строгий криптографический
|
|
отзыв требует отдельного протокола ротации ключей, которого сейчас нет.
|
|
Уточнение получено до изменения протокола; реализация продолжается.
|