Реализовать SHiNE_DM v1 с E2EE и tombstone

This commit is contained in:
AidarKC
2026-07-07 18:07:10 +04:00
parent 9c588bd9b5
commit 1b7a2a8f7c
33 changed files with 1636 additions and 445 deletions
+2
View File
@@ -61,6 +61,8 @@
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
| `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки |
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
| `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию |
@@ -73,7 +73,7 @@
- `incomingBlobB64` — блок `type=1` или `type=3`
- `outgoingBlobB64` — блок `type=2` или `type=4`
Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.
Все типы в этой паре используют бинарный формат `SHiNE_DM`.
### Запрос
@@ -110,8 +110,7 @@
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
- `400 / BAD_CONTENT_FORMAT` — для контентного сообщения пришёл не `SHiNE_DM`
- `400 / ATTACHMENTS_DISABLED` — в `SHiNE_DM` пришёл `attachmentsCount != 0`
- `400 / BAD_CRYPTO_METHOD` — неподдерживаемый метод шифрования контейнера `EncryptedBody_v1_0`
- `404 / USER_NOT_FOUND` — один из логинов не найден
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
@@ -123,6 +122,11 @@
Используется там, где нужно принять только incoming-вариант сообщения.
Принимаемые типы:
- `type=1` — входящая копия сообщения
- `type=3` — входящий read-receipt
### Запрос
```json
@@ -135,7 +139,39 @@
}
```
## 5. `AckSessionDelivery`
## 5. `DeleteMessage`
Принимает один signed DM-блок `type=5` или `type=6`.
### Запрос
```json
{
"op": "DeleteMessage",
"requestId": "dm-del-001",
"payload": {
"blobB64": "BASE64_DELETE_BLOCK"
}
}
```
## 6. `DeleteConversation`
Принимает один signed DM-блок `type=7` или `type=8`.
### Запрос
```json
{
"op": "DeleteConversation",
"requestId": "dm-del-all-001",
"payload": {
"blobB64": "BASE64_DELETE_CONVERSATION_BLOCK"
}
}
```
## 7. `AckSessionDelivery`
Требует авторизации. Подтверждает доставку в текущую сессию.
@@ -151,7 +187,7 @@
}
```
## 6. Событие `SignedMessageArrived`
## 8. Событие `SignedMessageArrived`
Сервер присылает его по WebSocket в активные сессии адресата.
@@ -174,18 +210,19 @@
Если это новая ревизия того же письма, `messageKey` остаётся тем же, а `revisionTimeMs` меняется внутри бинарного блока.
## 7. `CallInviteBroadcast`
Для типов `5/6/7/8` событие тоже приходит в таком же конверте, но логика применения определяется `messageType` и бинарным `blobB64`.
## 9. `CallInviteBroadcast`
Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.
## 8. `CallSignalToSession`
## 10. `CallSignalToSession`
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
## 9. Замечания
## 11. Замечания
- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
- контентные DM `type=1/2` используют `SHiNE_DM`
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
- все DM-типы `1..8` используют `SHiNE_DM`
- сервер не расшифровывает DM и не использует ciphertext как preview текста
- сервер хранит последнюю применённую ревизию контентного сообщения и отдельные tombstone-события удаления
- HTTP endpoints для DM-файлов сейчас отсутствуют
@@ -0,0 +1,19 @@
# DM v1: E2EE, tombstone и fan-out
- краткое описание фичи:
Новый протокол личных сообщений `SHiNE_DM` v1: реальные зашифрованные тела `X25519 + HKDF-SHA256 + AES-256-GCM`, read-receipt в том же контейнере, удаление одного сообщения, удаление истории переписки и межсерверная доставка по `access_servers`.
- что именно проверять:
1. Отправка нового DM между двумя пользователями и чтение текста на стороне получателя.
2. Отправка/чтение своей исходящей копии у отправителя.
3. Редактирование сообщения с тем же `baseKey` и ростом `revisionTimeMs`.
4. Подтверждение прочтения `type=3/4`.
5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
- ожидаемый результат:
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
- статус:
pending
+17
View File
@@ -0,0 +1,17 @@
# Личные сообщения SHiNE
Эта папка содержит актуальную документацию по личным сообщениям SHiNE.
Точка входа:
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md` — логика протокола, роли API, серверное поведение, routing по `access_servers`
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md` — точный бинарный формат контейнера `SHiNE_DM`
Исторический устаревший документ сохранён отдельно:
- `Dev_Docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md`
Правило сопровождения:
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
@@ -2,13 +2,13 @@
## Статус документа
Этот файл — актуальная логическая спецификация DM-протокола SHiNE следующего этапа.
Этот файл — актуальная логическая спецификация DM-протокола SHiNE v1.
Важно:
- это целевая спецификация;
- в текущем коде она ещё реализована не полностью;
- при реализации код должен приводиться именно к этой спецификации, а не наоборот.
- это актуальная реализованная спецификация;
- код и документы по DM должны изменяться синхронно;
- при любых будущих изменениях DM сначала обновляется эта спецификация и соседний документ формата.
Документ фиксирует:
@@ -151,7 +151,6 @@
- новые входящие сообщения;
- входящие обновления/редактирования;
- входящие служебные события удаления;
- будущие входящие перешифрованные копии.
## 5. Удаление одного сообщения
@@ -286,7 +285,7 @@
Их роли в v1:
- `SendMessagePair` — новая пара сообщений и редактирование старой пары автором;
- `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и служебных DM-событий;
- `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и входящего read-receipt;
- `ReceiveOutcomingMessage` — алиас `SendMessagePair`.
### 8.2. Новые методы, которые нужны
@@ -399,7 +398,6 @@ Request:
- приём одной входящей копии по схеме server-to-server;
- приём входящего редактирования;
- приём служебного входящего события удаления;
- приём входящего read-receipt.
Request:
@@ -6,9 +6,9 @@
Важно:
- это целевой формат следующего этапа;
- в текущем коде он ещё реализован не полностью;
- особенно это касается перехода всех типов `1..8` на единый контейнер `SHiNE_DM`.
- это актуальный формат, реализованный в текущем коде;
- контейнер `SHiNE_DM` используется для всех типов `1..8`;
- при любых будущих изменениях DM этот документ обновляется одновременно с кодом.
Логика протокола, API и поведение сервера описаны отдельно:
@@ -156,6 +156,19 @@ Nonce/IV для `AES-GCM`.
Сервер не должен трактовать это поле как 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` содержит открытый контейнер ссылки на исходное сообщение:
@@ -291,8 +304,6 @@ ReadReceiptBody_v1_0
- ciphertext может различаться;
- `reencryptedAtMs` при одной парной ревизии тоже должен совпадать.
## 13. Примечание о текущем коде
## 13. Примечание о поддержке
В текущем коде типы `3/4` пока ещё реализованы старым контейнером `SHiNE_dm2`.
Для версии протокола v1 целевой формат фиксируется уже как единый `SHiNE_DM` для всех типов `1..8`.
В версии DM v1 все типы `1..8` используют единый контейнер `SHiNE_DM`.