Files
SHiNE-server/docs/Personal_Messages/Формат_DM_v1.md
T

12 KiB
Raw Blame History

Формат DM v1

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

Этот файл фиксирует байтовый формат DM-контейнеров SHiNE v1.

Важно:

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

Логика протокола, API и поведение сервера описаны отдельно:

  • Dev_Docs/Personal_Messages/Протокол_DM_v1.md

1. Общие правила

  • Числа кодируются в big-endian.
  • toLogin и fromLogin — ASCII строки длиной 1..60.
  • Подпись — Ed25519, 64 байта.
  • nonceu32.
  • messageTypeu8.
  • timeMs, revisionTimeMs, reencryptedAtMsu64.

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

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

3. Контейнер SHiNE_DM

SHiNE_DM
- prefix = "SHiNE_DM"
- formatVersionMajor: u8
- formatVersionMinor: u8
- toLoginLen: u8
- toLogin
- fromLoginLen: u8
- fromLogin
- timeMs: u64
- nonce: u32
- messageType: u8
- revisionTimeMs: u64
- reencryptedAtMs: u64
- bodyLen: u32
- body
- signature: [64]

4. Смысл полей SHiNE_DM

timeMs

Это время исходного создания сообщения.

Для типов 7/8 это время самого служебного сообщения удаления переписки и одновременно граница, раньше которой сообщения этой пары считаются недействительными.

nonce

Дополнительное поле уникальности сообщения.

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

messageType

Определяет смысл контейнера:

  • обычное сообщение;
  • исходящая копия;
  • read-receipt;
  • удаление одного сообщения;
  • удаление всей переписки.

revisionTimeMs

  • 0 для исходной версии контентного сообщения;
  • ненулевое значение для новой текстовой ревизии сообщения;
  • для типов 5/6 это время tombstone удаления одного сообщения;
  • для типов 7/8 всегда 0.

То есть revisionTimeMs отвечает именно за изменение смыслового содержимого сообщения, а не за сам факт перешифровки.

reencryptedAtMs

  • 0, если перешифровки не было;
  • ненулевое значение, если копия сообщения была перешифрована.

Для типов 5/6/7/8 должно быть 0.

reencryptedAtMs не требует, чтобы revisionTimeMs был ненулевым. Допустим сценарий:

  • revisionTimeMs = 0
  • reencryptedAtMs > 0

если исходное сообщение не редактировалось, но было перешифровано позже.

bodyLen

  • > 0 для типов 1/2/3/4;
  • 0 для типов 5/6/7/8;
  • для всех типов используется один и тот же контейнер SHiNE_DM.

5. Поле body для типов 1/2

Для обычного контентного DM поле body содержит не голый ciphertext, а контейнер шифрования:

EncryptedBody_v1_0
- cryptoMethod: u8
- cryptoVersion: u8
- ephemeralPubKeyLen: u8
- ephemeralPubKey: bytes[ephemeralPubKeyLen]
- ivLen: u8
- iv: bytes[ivLen]
- cipherTextLen: u32
- cipherText: bytes[cipherTextLen]

Смысл полей EncryptedBody_v1_0

cryptoMethod

Идентификатор метода шифрования.

В версии v1 резервируется:

  • 1X25519 + HKDF-SHA256 + AES-256-GCM

cryptoVersion

Версия конкретного метода шифрования.

Для текущей версии:

  • 0

ephemeralPubKey

Публичный ephemeral X25519 ключ отправителя конкретной ревизии.

Практически ожидается длина:

  • 32

iv

Nonce/IV для AES-GCM.

Практически ожидается длина:

  • 12

cipherText

Зашифрованное содержимое сообщения.

Сервер не должен трактовать это поле как UTF-8 текст и не должен пытаться расшифровывать его в обычной DM-логике.

Важно:

  • официальный UI SHiNE сейчас использует именно EncryptedBody_v1_0;
  • сервер для контентных DM type=1/2 не должен требовать, чтобы body обязательно был распознан как EncryptedBody_v1_0;
  • сервер должен хранить и пересылать body как opaque bytes, если внешний контейнер SHiNE_DM валиден.

Точная схема для cryptoMethod = 1, cryptoVersion = 0

  • публичный ключ получателя для E2EE получается как стандартное преобразование Ed25519 -> X25519;
  • отправитель генерирует ephemeral X25519 приватный ключ и кладёт соответствующий ephemeralPubKey в контейнер;
  • общий секрет вычисляется как X25519(ephemeralPrivKey, recipientX25519PubKey);
  • HKDF-SHA256 использует:
    • salt = ephemeralPubKey || recipientX25519PubKey
    • info = "SHiNE_DM|1|0|X25519+HKDF-SHA256+AES-256-GCM" в ASCII
    • outputLen = 32
  • полученные 32 байта используются как ключ AES-256-GCM;
  • поле cipherText хранит стандартный результат библиотеки AES-GCM в виде:
    • ciphertext || tag

6. Поле body для типов 3/4

Для read-receipt типов 3/4 поле body содержит открытый контейнер ссылки на исходное сообщение:

ReadReceiptBody_v1_0
- refToLoginLen: u8
- refToLogin
- refFromLoginLen: u8
- refFromLogin
- refTimeMs: u64
- refNonce: u32

Смысл:

  • refToLogin
  • refFromLogin
  • refTimeMs
  • refNonce

однозначно указывают на логическое сообщение по его baseKey.

Отдельный refType в v1 не нужен, потому что подтверждение прочтения относится к самому логическому сообщению, а не к выбору между копиями type=1 и type=2.

7. Контент типов 1/2

Для типов 1/2:

  • bodyLen > 0
  • body обязан быть контейнером EncryptedBody_v1_0
  • ciphertext у type=1 и type=2 может быть разным

Новое сообщение

  • revisionTimeMs = 0
  • reencryptedAtMs = 0

Редактирование

  • revisionTimeMs > 0
  • reencryptedAtMs = 0 или больше нуля, если одновременно произошла перешифровка

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

  • revisionTimeMs = 0 или > 0
  • reencryptedAtMs > 0

8. Контент типов 3/4

Типы:

  • 3 — входящее подтверждение прочтения
  • 4 — исходящая копия подтверждения прочтения

Правила:

  • bodyLen > 0
  • body обязан быть контейнером ReadReceiptBody_v1_0
  • revisionTimeMs = 0
  • reencryptedAtMs = 0

Тип 3 и тип 4 по стилю полностью подчиняются общему контейнеру SHiNE_DM:

  • тот же prefix = "SHiNE_DM"
  • те же formatVersionMajor = 1
  • те же formatVersionMinor = 0

Различается только messageType и формат body.

9. Контент типов 5/6

Типы:

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

Правила:

  • bodyLen = 0
  • body отсутствует
  • revisionTimeMs > 0
  • reencryptedAtMs = 0

Это terminal tombstone конкретного сообщения.

После принятия такого контейнера содержательная версия этого же messageKey больше не должна приниматься.

10. Контент типов 7/8

Типы:

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

Правила:

  • bodyLen = 0
  • body отсутствует
  • revisionTimeMs = 0
  • reencryptedAtMs = 0

Здесь роль границы удаления играет:

  • timeMs самого контейнера

После принятия такого контейнера:

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

11. Подпись

Поле signature всегда подписывает весь контейнер от prefix до конца body включительно.

Правило подписи по messageType:

  • 1, 2, 5, 7 подписывает отправитель сообщения;
  • 6, 8 подписывает получатель сообщения.

Практическое UI-следствие:

  • клиентский UI может инициировать удаление входящего сообщения через type=6;
  • клиентский UI может инициировать очистку истории переписки через type=8;
  • сами контейнеры и байтовый формат при этом не отличаются от уже описанных типов 5/6/7/8.

12. Какие поля должны совпадать у пары 1/2

При обычной парной отправке через SendMessagePair у двух копий должны совпадать:

  • fromLogin
  • toLogin
  • timeMs
  • nonce
  • revisionTimeMs

Дополнительно:

  • логический plaintext должен быть одинаковым;
  • ciphertext может различаться;
  • reencryptedAtMs при одной парной ревизии тоже должен совпадать.

13. Примечание о поддержке

В версии DM v1 все типы 1..8 используют единый контейнер SHiNE_DM.