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

330 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Формат 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`.