SHA256
324 lines
11 KiB
Markdown
324 lines
11 KiB
Markdown
# Формат 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-логике.
|
||
|
||
### Точная схема для `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`.
|