12 KiB
Формат 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 байта. nonce—u32.messageType—u8.timeMs,revisionTimeMs,reencryptedAtMs—u64.
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 = 0reencryptedAtMs > 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 резервируется:
1—X25519 + 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 || recipientX25519PubKeyinfo = "SHiNE_DM|1|0|X25519+HKDF-SHA256+AES-256-GCM"в ASCIIoutputLen = 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
Смысл:
refToLoginrefFromLoginrefTimeMsrefNonce
однозначно указывают на логическое сообщение по его baseKey.
Отдельный refType в v1 не нужен, потому что подтверждение прочтения относится к самому логическому сообщению, а не к выбору между копиями type=1 и type=2.
7. Контент типов 1/2
Для типов 1/2:
bodyLen > 0bodyобязан быть контейнеромEncryptedBody_v1_0- ciphertext у
type=1иtype=2может быть разным
Новое сообщение
revisionTimeMs = 0reencryptedAtMs = 0
Редактирование
revisionTimeMs > 0reencryptedAtMs = 0или больше нуля, если одновременно произошла перешифровка
Перешифровка
revisionTimeMs = 0или> 0reencryptedAtMs > 0
8. Контент типов 3/4
Типы:
3— входящее подтверждение прочтения4— исходящая копия подтверждения прочтения
Правила:
bodyLen > 0bodyобязан быть контейнеромReadReceiptBody_v1_0revisionTimeMs = 0reencryptedAtMs = 0
Тип 3 и тип 4 по стилю полностью подчиняются общему контейнеру SHiNE_DM:
- тот же
prefix = "SHiNE_DM" - те же
formatVersionMajor = 1 - те же
formatVersionMinor = 0
Различается только messageType и формат body.
9. Контент типов 5/6
Типы:
5— сообщение удалено отправителем6— сообщение удалено получателем
Правила:
bodyLen = 0bodyотсутствуетrevisionTimeMs > 0reencryptedAtMs = 0
Это terminal tombstone конкретного сообщения.
После принятия такого контейнера содержательная версия этого же messageKey больше не должна приниматься.
10. Контент типов 7/8
Типы:
7— переписка удалена отправителем8— переписка удалена получателем
Правила:
bodyLen = 0bodyотсутствуетrevisionTimeMs = 0reencryptedAtMs = 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 у двух копий должны совпадать:
fromLogintoLogintimeMsnoncerevisionTimeMs
Дополнительно:
- логический plaintext должен быть одинаковым;
- ciphertext может различаться;
reencryptedAtMsпри одной парной ревизии тоже должен совпадать.
13. Примечание о поддержке
В версии DM v1 все типы 1..8 используют единый контейнер SHiNE_DM.