Files
SHiNE-server/Dev_Docs/Personal_Messages/Протокол_DM_v1.md
T

28 KiB
Raw Blame History

Личные сообщения (DM) — спецификация v1

Статус документа

Этот файл — актуальная логическая спецификация DM-протокола SHiNE v1.

Важно:

  • это актуальная реализованная спецификация;
  • код и документы по DM должны изменяться синхронно;
  • при любых будущих изменениях DM сначала обновляется эта спецификация и соседний документ формата.

Документ фиксирует:

  • модель DM и смысл типов сообщений;
  • правила редактирования, перешифровки и удаления;
  • роли существующих API-методов;
  • правила межсерверной маршрутизации через access_servers;
  • общее поведение сервера и БД.

Точный байтовый формат контейнера вынесен отдельно:

  • Dev_Docs/Personal_Messages/Формат_DM_v1.md

Формат клиентских технических вставок внутри plaintext вынесен отдельно:

  • Dev_Docs/Personal_Messages/Технические_вставки_DM_v1.md

Устаревшая предыдущая версия сохранена отдельно:

  • Dev_Docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md

1. Основная модель

Личное сообщение в SHiNE хранится как две логические копии одного сообщения:

  • type=1 — входящее сообщение для получателя;
  • type=2 — исходящая копия сообщения для отправителя.

Обе копии имеют общий логический идентификатор:

  • baseKey = fromLogin|toLogin|timeMs|nonce

Идентификатор конкретной копии:

  • messageKey = baseKey|messageType

Поля timeMs и nonce не меняются никогда.

nonce обязателен, потому что одного timeMs недостаточно для гарантированной уникальности.

2. Ключи и шифрование

2.1. Общий принцип

У пользователя в PDA публикуется один публичный clientKey в формате Ed25519.

Из того же ключевого материала стандартным способом выводится ключ X25519 для E2EE-шифрования DM.

Отдельный публичный dmEncKey в текущей архитектуре не хранится.

Подробности стандартного преобразования:

  • Dev_Docs/Протоколы/Преобразование_ED25519_в_X25519.md

2.2. Правило шифрования копий

  • type=1 шифруется на ключ получателя;
  • type=2 шифруется на ключ отправителя.

Следствия:

  • входящую копию может прочитать только получатель;
  • исходящую копию может прочитать только отправитель;
  • сервер не должен расшифровывать DM;
  • ciphertext у type=1 и type=2 обычно разный и не обязан совпадать побайтно.

2.3. Что именно знает сервер о body

Для контентных DM (type=1/2) сервер в v1 должен рассматривать body как opaque blob:

  • сервер проверяет внешний контейнер SHiNE_DM;
  • сервер проверяет подпись;
  • сервер проверяет служебные поля envelope;
  • сервер проверяет только то, что bodyLen > 0 и не превышает допустимый лимит;
  • сервер не обязан разбирать внутренний crypto-контейнер body;
  • сервер не обязан знать конкретный алгоритм шифрования body.

Это позволяет альтернативным клиентам использовать совместимый внешний DM-envelope при собственном формате зашифрованного payload внутри body.

Официальный UI SHiNE при приёме такого сообщения:

  • пытается расшифровать знакомый формат EncryptedBody_v1_0;
  • показывает текст сообщения при успешной расшифровке;
  • если внутренний формат не поддерживается, показывает Формат сообщения не поддерживается;
  • если формат понятен, но расшифровка не удалась, показывает Не удалось расшифровать сообщение;
  • если body повреждён или структурно битый, показывает Сообщение повреждено.

После успешной расшифровки plaintext может дополнительно содержать специальные клиентские вставки <SHiNE:...> в начале текста. Эти вставки относятся уже к уровню UI/plaintext, а не к уровню серверного DM-envelope.

2.4. Источник истины по пользователю

Для DM-проверки сервер использует:

  • локальную solana_users как кэш;
  • Solana PDA как источник истины по clientKey и access_servers.

Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA:

  • в GetUser;
  • при серверной верификации DM-подписи;
  • перед межсерверной маршрутизацией через access_servers.

3. Типы сообщений

В версии v1 используются следующие messageType:

  • 1 — входящее сообщение;
  • 2 — исходящая копия сообщения;
  • 3 — входящее подтверждение прочтения;
  • 4 — исходящая копия подтверждения прочтения;
  • 5 — сообщение удалено отправителем;
  • 6 — сообщение удалено получателем;
  • 7 — переписка удалена отправителем;
  • 8 — переписка удалена получателем.

Типы 1/2 — это обычные контентные DM, в том числе их последующие ревизии.

Типы 3/4 — служебные подтверждения прочтения.

Типы 5/6/7/8 — служебные события удаления без зашифрованного тела.

4. Базовые операции

4.1. Создание нового сообщения

Новое сообщение отправляется парой блоков через:

  • SendMessagePair
  • ReceiveOutcomingMessage как алиас

Пару создаёт автор сообщения.

Пара содержит:

  • одну входящую копию type=1;
  • одну исходящую копию type=2;
  • одинаковые fromLogin, toLogin, timeMs, nonce;
  • одинаковый логический plaintext;
  • разные ciphertext для разных владельцев копий.

4.2. Редактирование сообщения

Редактирование общего текста делает автор сообщения.

При редактировании:

  • baseKey остаётся тем же;
  • messageType остаётся тем же;
  • revisionTimeMs увеличивается;
  • автор заново шифрует обе копии и снова отправляет пару через SendMessagePair.

Если revisionTimeMs = 0, это исходная версия.

Если revisionTimeMs > 0, это новая ревизия сообщения.

4.3. Перешифровка

Перешифровка нужна для будущего сценария полной или частичной перепаковки истории сообщений при сохранении тех же логических идентификаторов сообщений.

При перешифровке:

  • baseKey остаётся прежним;
  • messageKey остаётся прежним;
  • plaintext может остаться тем же;
  • ciphertext меняется;
  • reencryptedAtMs получает ненулевое значение;
  • revisionTimeMs может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста;
  • сервер при выборе актуальной версии обязан учитывать обе метки времени.

Если сообщение уже удалено одним из типов 5/6, его перешифровывать больше нельзя.

Если переписка уже удалена типом 7/8, более старые сообщения этой пары тоже перешифровывать нельзя.

4.4. Приём входящей копии

Для server-to-server доставки одной входящей копии используется:

  • ReceiveIncomingMessage

Этот метод должен принимать:

  • новые входящие сообщения;
  • входящие обновления/редактирования;
  • будущие входящие перешифрованные копии.

5. Удаление одного сообщения

5.1. Общая логика

Удаление одного сообщения в v1 всегда глобальное у обеих сторон.

Локального удаления только у себя в этой версии протокола не вводится.

5.2. Типы удаления

  • type=5 — сообщение удалено отправителем;
  • type=6 — сообщение удалено получателем.

Удаляющее сообщение:

  • не содержит зашифрованного тела;
  • хранится в БД как tombstone;
  • терминально закрывает это сообщение;
  • не даёт больше принять никакую более позднюю содержательную версию этого же messageKey.

5.3. Метод удаления

Для удаления одного сообщения нужен отдельный метод, условно:

  • DeleteMessage

Если сервер впервые получает валидное удаляющее сообщение:

  • сохраняет tombstone в БД;
  • удаляет или замещает прежнюю версию сообщения tombstone-записью;
  • распространяет это же удаление на серверы доступа обеих сторон;
  • не принимает в будущем попытки "оживить" это сообщение.

6. Удаление всей переписки

6.1. Отдельное служебное сообщение

Удаление всей переписки между двумя пользователями — это отдельное служебное сообщение без ciphertext.

Типы:

  • type=7 — переписка удалена отправителем;
  • type=8 — переписка удалена получателем.

6.2. Граница удаления

Границей удаления считается:

  • timeMs самого служебного сообщения удаления переписки

Отдельное deleteBeforeTimeMs в этой версии не вводится.

6.3. Правило применения

Если сервер получает такое сообщение впервые:

  • сохраняет его в БД как tombstone переписки;
  • удаляет из БД все сообщения этой пары пользователей с timeMs меньше времени служебного сообщения;
  • больше не принимает новые или повторно доставленные сообщения с timeMs раньше этой границы;
  • распространяет это же сообщение удаления переписки на серверы доступа обеих сторон.

6.4. Поведение при позднем старом сообщении

Если после удаления переписки приходит старое сообщение, у которого:

  • timeMs < deleteConversationMessage.timeMs

то сервер:

  • не принимает это сообщение;
  • распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон;
  • ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.

6.5. Будущие новые сообщения

После удаления всей переписки новые сообщения между этими же пользователями разрешены, если:

  • их timeMs больше времени служебного сообщения удаления переписки.

7. Серверы и маршрутизация

7.1. access_servers

Для обычного пользователя список серверов доставки и доступа задаётся через:

  • access_servers

Это:

  • сервера доступа пользователя;
  • сервера relay;
  • сервера, через которые пользователь получает личные сообщения и другие пользовательские операции.

7.2. sync_servers

sync_servers относятся не к обычной пользовательской маршрутизации DM, а к server-to-server партнёрству серверного узла.

Они используются для:

  • синхронизации серверных данных;
  • синхронизации пользовательских блокчейнов SHiNE;
  • общей межсерверной координации.

sync_servers не являются списком пользовательских серверов доставки DM.

7.3. Несколько серверов у отправителя и получателя

Протокол должен поддерживать ситуацию, когда:

  • у отправителя несколько access_servers;
  • у получателя несколько access_servers;
  • часть серверов у сторон совпадает;
  • часть серверов уникальна.

Из этого следуют требования:

  • все DM-операции должны быть идемпотентны;
  • повторное получение уже известного события не должно ломать состояние;
  • дубль tombstone должен быть безопасен;
  • сервер не должен "оживлять" более старую версию сообщения после уже принятого tombstone.

8. Методы и их роли

8.1. Существующие методы, которые сохраняются

  • SendMessagePair
  • ReceiveOutcomingMessage
  • ReceiveIncomingMessage
  • DeleteMessage
  • DeleteConversation

Их роли в v1:

  • SendMessagePair — новая пара сообщений и редактирование старой пары автором;
  • ReceiveIncomingMessage — приём одной входящей копии, входящих редактирований и входящего read-receipt;
  • ReceiveOutcomingMessage — алиас SendMessagePair.

8.2. Новые методы, которые нужны

Отдельный legacy-метод SendDirectMessage в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки.

9. Правила валидации и применения

9.1. Общее правило по ревизиям

Для одного и того же контентного сообщения сервер сравнивает пару:

  • revisionTimeMs;
  • reencryptedAtMs.

Правило:

  • если новый revisionTimeMs больше сохранённого, сообщение применяется;
  • если revisionTimeMs равен, но новый reencryptedAtMs больше сохранённого, сообщение применяется;
  • если обе величины равны, сообщение не применяется;
  • если новая пара (revisionTimeMs, reencryptedAtMs) меньше или равна сохранённой, сообщение не применяется.

Содержимое body при этом сравнении не участвует.

То есть если сервер уже видел ту же пару (revisionTimeMs, reencryptedAtMs), он считает, что такая версия у него уже есть.

9.2. Повторный tombstone

Повторный tombstone означает ситуацию, когда сервер повторно получает то же самое событие удаления:

  • того же сообщения;
  • или той же переписки.

Это может произойти из-за:

  • повторной межсерверной доставки;
  • нескольких access_servers;
  • сетевых retry;
  • дублирующей пересылки с разных маршрутов.

Правило:

  • повторный tombstone должен быть полностью безопасен;
  • если соответствующее удаление уже сохранено, сервер ничего не меняет и просто игнорирует повтор.

9.3. Сообщения старше границы удалённой переписки

Если для пары пользователей уже есть сохранённая граница удаления переписки, и приходит:

  • обычное сообщение;
  • удаление одного сообщения;
  • повторное удаление переписки;
  • любое другое DM-событие;

у которого timeMs меньше этой границы, сервер:

  • ничего не меняет в БД;
  • не восстанавливает старую историю;
  • не применяет это событие повторно.

Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на access_servers обеих сторон, чтобы отстающие серверы сами подчистили историю.

Такие сообщения считаются частью уже удалённой истории.

9.4. Приоритет удаления переписки

Если сначала пришло удаление переписки, а потом удаление одного старого сообщения из этой переписки, сервер должен:

  • проигнорировать это удаление одного сообщения;
  • не создавать новых изменений поверх уже удалённой истории.

То же правило действует и для обычных сообщений, и для редактирований старых сообщений.

10. JSON API v1

10.1. SendMessagePair

Назначение:

  • клиент отправляет новый DM;
  • клиент отправляет редактирование старого DM;
  • клиент отправляет парную новую ревизию типов 1/2.

Request:

{
  "op": "SendMessagePair",
  "requestId": "req-123",
  "payload": {
    "incomingBlobB64": "...",
    "outgoingBlobB64": "..."
  }
}

Правила:

  • клиенту достаточно отправить пару на один любой доступный сервер;
  • сервер после принятия сам отвечает за дальнейшую межсерверную доставку.

10.2. ReceiveIncomingMessage

Назначение:

  • приём одной входящей копии по схеме server-to-server;
  • приём входящего редактирования;
  • приём входящего read-receipt.

Request:

{
  "op": "ReceiveIncomingMessage",
  "requestId": "req-456",
  "payload": {
    "incomingBlobB64": "..."
  }
}

10.3. DeleteMessage

Назначение:

  • удалить одно сообщение у обеих сторон.

Request:

{
  "op": "DeleteMessage",
  "requestId": "req-789",
  "payload": {
    "blobB64": "..."
  }
}

Ожидается контейнер типа:

  • 5, если удаление инициировал отправитель;
  • 6, если удаление инициировал получатель.

UI-следствие для клиента:

  • пользователь может удалить как своё исходящее сообщение, так и входящее;
  • для удаления входящего UI должен отправлять вариант, где удаление инициировал получатель (type=6).

10.4. DeleteConversation

Назначение:

  • удалить всю переписку до времени самого служебного сообщения.

Request:

{
  "op": "DeleteConversation",
  "requestId": "req-790",
  "payload": {
    "blobB64": "..."
  }
}

Ожидается контейнер типа:

  • 7, если удаление инициировал отправитель;
  • 8, если удаление инициировал получатель.

UI-следствие для клиента:

  • действие Очистить историю должно отправлять служебное сообщение удаления переписки;
  • для обычной клиентской кнопки очистки истории допускается вариант удаления, инициированный текущим получателем (type=8);
  • после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории;
  • отдельное UI-действие Удалить чат может дополнительно спросить, нужно ли вместе с удалением контакта также отправить DeleteConversation.

11. Межсерверная доставка

11.1. Клиентская сторона

Клиенту достаточно отправить сообщение на:

  • любой один доступный сервер.

11.2. Серверная сторона

После принятия валидного события сервер должен отправлять его:

  • на все серверы из access_servers отправителя;
  • на все серверы из access_servers получателя.

Если часть серверов совпадает, это допустимо.

Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии.

Идемпотентность обязательна.

11.3. Ошибки доставки

Если часть серверов временно недоступна:

  • это не должно отменять локальное принятие уже валидного сообщения;
  • повторная доставка может делаться отдельным retry-механизмом;
  • повторное получение того же события должно быть безопасным.

12. Хранение в БД

Основная таблица остаётся:

  • signed_messages_v2

В ней должны сохраняться:

  • обычные контентные DM;
  • tombstone одного сообщения;
  • tombstone удаления переписки.

Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы:

  • защищать от повторного приёма старых версий;
  • не терять факт удаления;
  • корректно синхронизировать событие между серверами.

Сообщение об удалении переписки тоже хранится в БД, а старые сообщения до его времени из БД удаляются.

13. Что обязательно должно измениться в коде относительно v0.5

  • сервер не должен требовать одинаковый encryptedBody у type=1 и type=2;
  • сервер не должен трактовать encryptedBody как обычный UTF-8 текст;
  • сервер не должен валидировать внутреннюю crypto-структуру body у контентных DM type=1/2;
  • DM должны реально шифроваться end-to-end;
  • удаление одного сообщения должно стать терминальным tombstone;
  • удаление всей переписки должно стать отдельным служебным событием;
  • межсерверная маршрутизация DM должна идти через access_servers;
  • сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
  • при выборе актуальной версии должен учитываться reencryptedAtMs, если revisionTimeMs совпадает;
  • legacy SendDirectMessage должен быть отключён;
  • логика должна быть безопасна для нескольких серверов у каждой стороны.

14. Что в v1 пока не входит

  • вложения в DM;
  • хранение отдельного keyId шифрования в DM;
  • ротация clientKey;
  • финальная конкретная UI-реализация массовой перешифровки;
  • физическая полная реализация DM federation в текущем коде.