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

22 KiB
Raw Blame History

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

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

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

Важно:

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

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

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

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

  • 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 обычно разный и не обязан совпадать побайтно.

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

то сервер:

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

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

Их роли в v1:

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

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

Нужны как минимум отдельные операции:

  • DeleteMessage
  • DeleteConversation

Названия могут быть другими, но логически это две разные операции:

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

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

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

Для одного и того же messageKey сервер сравнивает только:

  • уже сохранённый revisionTimeMs;
  • revisionTimeMs входящего сообщения.

Правило:

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

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

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

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

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

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

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

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

Правило:

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

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

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

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

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

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

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

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, если удаление инициировал получатель.

10.4. DeleteConversation

Назначение:

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

Request:

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

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

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

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 текст;
  • DM должны реально шифроваться end-to-end;
  • удаление одного сообщения должно стать терминальным tombstone;
  • удаление всей переписки должно стать отдельным служебным событием;
  • межсерверная маршрутизация DM должна идти через access_servers;
  • логика должна быть безопасна для нескольких серверов у каждой стороны.

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

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