# Формат 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` ```text 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, а контейнер шифрования: ```text 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 || 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` содержит открытый контейнер ссылки на исходное сообщение: ```text 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`.