305 lines
31 KiB
Plaintext
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 сохраняет прежний интерфейс. Секреты, поиск новых узлов и изменение
|
|
членства в эту операцию чтения не входят.
|