Files
utun2/doc/dm_devgroup_plan.txt
T

305 lines
31 KiB
Plaintext

Личные сообщения: устройства и подгруппы
======================================
Дата: 03.11.2026. Ветка реализации: feature/dm-devgroups.
Статус: реализовано; доступ после выхода согласован в разделе 9.
Результаты сборок и проверок — раздел 10.
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). Устойчивый счётчик автора общий
для всех его бесед: это сохраняет уникальность mailbox (recipient,author,seq),
когда одна пара устройств одновременно переписывается в DEVICE и GROUPS.
История различает авторов независимо от UI-направления. Время не определяет
уникальность. Проверенные собственные
реплики показываются исходящими, но не становятся новым источником рассылки.
Повтор допускается только с тем же подписанным телом; конфликт отклоняется.
Подпись охватывает стороны, снимок адресатов, содержимое и дескриптор вложения.
Все изменения истории, счётчиков и очереди атомарны. 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 секрет технически
подходит и для будущих шифротекстов этой группы; строгий криптографический
отзыв требует отдельного протокола ротации ключей, которого сейчас нет.
Уточнение получено до изменения протокола и реализовано.
10. Результаты реализации и проверки — 03.11.2026
-------------------------------------------------
Нормативное описание протокола/таблиц/API: doc/dm_arch.md и src/dm/*.h.
Настройка M в [chatserver]: dm_media_delete_on_open=yes/no; default=yes.
TTL и политика фиксируются при первом STORE, а не при последующих повторах.
Qt/Android и CLI используют итоговый ID беседы, возвращённый ядром.
CLI: dm_start --mode device|groups; dm_file ... download|opened.
Проверены:
- make -C src -j4 и полная desktop Qt-сборка — успешно.
- ./check.sh — 236 PASS / 238. Все DM/devgroup-наборы прошли. Два ранее
зарегистрированных таймаута по 90 s: test_group_connect и
test_group_connect_standby (doc/tasks.md, задача от 02.11.2026).
Лог: /tmp/dm-devgroups-final-check2.log; подробные логи: tests/logs/.
- test_dm_identity/schema/groups: проверенные стороны, snapshot, независимые
авторы, собственные реплики, отказ при подделке, две упаковки одного файла,
обе политики и закрытие при выходе/rollback. Проверки A1/A2/B1/B2 выполняются
через реальные ядра/SQLite и обработчики приёма; сетевой mailbox проверяет
test_dm_e2e, файловый транспорт и M — test_dm_media.
- test_dm_media: 17 фаз, реальный транспорт, TTL, потерянные квитанции,
SQL-отказы, рестарты S/M, endpoint=M, выключенная автозагрузка,
явное открытие и восстановление утраченного файла отправителя с реплики.
Проверено побайтовое содержимое и отсутствие утечек tracked allocations.
Лог отдельного запуска: /tmp/dm-devgroups-source-recovery.log.
- Qt ctest: 121 PASS / 122. Меню подгруппы и DM-удаление прошли.
Единственный отказ test_gui_language: исходная незакоммиченная английская
строка «Deleted accounts» уже переведена как «Deleted», тест ожидает исходник.
Эта пользовательская правка сохранена. Лог: /tmp/dm-devgroups-qt-final-tests.log.
- Android assembleDebug — успешно: /tmp/dm-devgroups-android-final-build.log.
PendingDmOpenTest 4/4, MemberRepositoryTest 9/9,
ConversationSubmissionsTest 4/4 — успешно. Логи:
/tmp/dm-devgroups-android-focused.log,
/tmp/dm-devgroups-android-submissions.log.
Общая зависимость Android unit-test запускает также CallServiceLifecycleTest:
его прежние 28 отказов из 58 воспроизвелись; задача уже есть в doc/tasks.md.
Целевые PM-наборы запущены отдельно от этих зависимостей.
Исправленные при проверке регрессии: ранняя ошибка приглашения без conv_id,
устаревшая SQL-инъекция test_dm_load, старое ожидание отсутствующего меню
чужой подгруппы, уведомление UI только о первом файловом получателе,
невозможность повторно скачать файл на исходное устройство автора.
События открытия сбрасываются при очистке состояния UI; превью/экспорт
и собственное открытие отправителем не создают TAKEN получателя.
Работа закоммичена в feature/dm-devgroups; исходные пользовательские изменения
также закоммичены отдельными коммитами по просьбе пользователя. Push, установка на телефоны и развёртывание
не выполнялись. Интерактивный сценарий на физических устройствах не запускался.
11. Панель устройств групповой беседы — 03.11.2026
------------------------------------------------
Панель показывает текущий проверенный состав закреплённых сторон GROUPS:
«Мои устройства» и «Устройства собеседника». Offline-узлы остаются видимыми;
сторона без подгруппы содержит одно устройство. Список доставки каждого уже
отправленного сообщения остаётся неизменным при обновлении панели.
Общий API dm_members_snapshot возвращает CHAT_EVT_DM_MEMBERS (73):
conv_len:u8,conv:UTF8,generation:LE64,count:LE16,(side:u8,member:wire260)*count.
Side 0 — локальная сторона, 1 — собеседник. Используется существующий codec
chat_member_display; имена/live-состояние берутся из общей CHAT-области.
Известный узел без общей CHAT-области также виден, без выдуманного online.
dm_identity_members допускает пустую подгруппу; отправка через
dm_identity_targets по-прежнему требует хотя бы одно устройство обеих сторон.
Закрытая или DEVICE-беседа возвращает пустой состав этим API.
Qt заменяет содержимое панели участников при выборе GROUPS; Android открывает
панель кнопкой участников в заголовке PM. Пока панель видна, состав и online
обновляются каждые 2 s. Qt сохраняет выбор/прокрутку и не перестраивает
неизменившийся снимок. Ответ другой беседы/поколения, усечённая запись,
повторный node_id и недопустимая сторона отклоняются без частичного обновления.
DEVICE сохраняет прежний интерфейс. Секреты, поиск новых узлов и изменение
членства в эту операцию чтения не входят.