Первая версия вложенных файлов работает

This commit is contained in:
AidarKC
2026-09-11 02:38:02 +03:00
parent ea0098d705
commit f9ae811702
29 changed files with 1463 additions and 57 deletions
+1 -1
View File
@@ -82,6 +82,6 @@
входящей копии на единственный access-сервер получателя.
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
- `ServerHello` пока принимает заявленный `serverLogin` на доверии и не является криптографической аутентификацией.
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
- HTTP endpoints зашифрованных DM-файлов (`PUT/GET/HEAD/OPTIONS /dm-files/{fileId}`) не являются WebSocket `op` и описаны отдельно в `18_DM_File_Storage_API.md`.
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
+99
View File
@@ -0,0 +1,99 @@
# HTTP API зашифрованных файлов личных сообщений
## 1. Назначение
Файлы личных сообщений хранятся только на access-сервере отправителя и только в зашифрованном виде.
HTTP API не получает исходное имя файла, MIME-тип или ключ расшифрования.
Клиент:
1. генерирует случайный AES-256-GCM key и 96-bit IV;
2. шифрует файл в браузере;
3. считает `SHA-256(ciphertext)`;
4. переводит 32-byte hash в Base58 — это `fileId` и имя файла на диске сервера;
5. загружает ciphertext на свой access-сервер;
6. помещает URL, key, IV, исходное имя/MIME/размер в `<S:file...>` внутри plaintext DM;
7. обычный механизм E2EE DM шифрует эту техническую вставку отдельно для отправителя и получателя.
Следствие: сервер файлов не знает AES-key и не может расшифровать вложение.
## 2. Конфигурация сервера
```properties
dm.files.enabled=true
dm.files.storageDir=data/dm-files
dm.files.maxBytes=52428816
```
`dm.files.maxBytes` ограничивает размер ciphertext. Текущий официальный UI ограничивает исходный файл 50 MiB; AES-GCM добавляет 16-byte authentication tag.
## 3. PUT `/dm-files/{fileId}`
Загружает immutable ciphertext.
`fileId` обязан быть равен:
```text
Base58(SHA-256(ciphertext))
```
### Заголовки авторизации
```text
Content-Type: application/octet-stream
X-Shine-Session-Id: <active session id>
X-Shine-Time-Ms: <unix time ms>
X-Shine-Content-Length: <ciphertext bytes>
X-Shine-Signature: <Ed25519 signature, Base64>
```
Подписываемая UTF-8 строка:
```text
DM_FILE_UPLOAD_V1:{sessionId}:{fileId}:{encryptedSize}:{timeMs}
```
Подпись проверяется публичным `session_key` из активной сессии. Допустимое отклонение времени — 60 секунд.
Сервер потоково пишет временный файл, одновременно считает SHA-256, проверяет фактический размер и только после успешной проверки атомарно перемещает объект под именем `{fileId}`.
Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна.
Успешный ответ:
```json
{
"ok": true,
"fileId": "...",
"size": 12345,
"alreadyExists": false
}
```
Основные ошибки: `BAD_FILE_ID`, `MISSING_UPLOAD_AUTH`, `UPLOAD_AUTH_EXPIRED`, `BAD_UPLOAD_SIGNATURE`, `FILE_TOO_LARGE`, `LENGTH_MISMATCH`, `HASH_MISMATCH`.
## 4. GET `/dm-files/{fileId}`
Возвращает только сохранённый ciphertext как `application/octet-stream`.
GET не требует пользовательской сессии: `fileId` является непредсказуемым 256-bit content address, а без AES-key содержимое остаётся зашифрованным. AES-key передаётся только внутри E2EE DM.
Ответ разрешает cross-origin чтение (`Access-Control-Allow-Origin: *`), потому что получатель может быть подключён к другому access-серверу и должен скачать ciphertext непосредственно с сервера отправителя.
Перед расшифрованием официальный UI повторно проверяет `Base58(SHA-256(downloadedCiphertext)) == fileId`.
## 5. HEAD и OPTIONS
- `HEAD /dm-files/{fileId}` возвращает метаданные ciphertext без тела;
- `OPTIONS /dm-files/*` обслуживает CORS preflight для браузерного PUT.
## 6. Reverse proxy
Caddy/Nginx должен проксировать `/dm-files/*` в тот же Jetty, что обслуживает `/ws`. Route должен находиться до SPA fallback.
## 7. Ограничения v1
- файл перед загрузкой целиком читается в память браузера, поэтому UI ограничен 50 MiB;
- удаления/TTL/garbage collection пока нет;
- если ciphertext уже загружен, а отправка E2EE DM затем не удалась, объект может остаться orphan-файлом;
- resumable/chunked upload не входит в v1.
+13
View File
@@ -9,6 +9,7 @@
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — доставка на
единственный access-сервер, retry-воркер и UI-статусы
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
- `docs/API/18_DM_File_Storage_API.md` — HTTP-хранилище зашифрованных файлов DM на access-сервере отправителя
Исторический устаревший документ сохранён отдельно:
@@ -19,3 +20,15 @@
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
- локальная таблица пользователей для DM считается кэшем, а источником истины остаётся Solana PDA; если серверная логика DM меняет правила lazy-import пользователей из PDA, это тоже обязательно фиксируется в документации.
## Зашифрованные файлы DM
Официальный UI v1 умеет отправлять файл как внешний зашифрованный объект:
- файл шифруется в браузере `AES-256-GCM`;
- ciphertext хранится только на access-сервере отправителя под именем `Base58(SHA-256(ciphertext))`;
- AES-key, IV, исходное имя, MIME, размер и URL находятся в `<S:file...>` внутри plaintext DM, а значит сами защищены существующим E2EE DM;
- получатель скачивает ciphertext с сервера отправителя, проверяет SHA-256, расшифровывает локально и только затем отдаёт Blob браузеру для сохранения под исходным именем;
- сервер файлов ключ расшифрования не получает.
Формат бинарного контейнера `SHiNE_DM` при этом не меняется.
@@ -228,3 +228,22 @@ DeleteMessage принимает type=5/6. DeleteConversation принимает
- при удалении чата с `friend`/`close_friend` UI должен отдельно предупредить, что одна очистка истории не уберёт строку чата, и при подтверждении снять социальную связь и очистить историю.
Это правило не меняет wire/API-формат DM и не меняет байтовый формат tombstone.
## 15. Файлы в личных сообщениях
Файл не встраивается байтами в `SHiNE_DM`. До отправки DM официальный браузерный клиент:
1. генерирует отдельные случайные `AES-256-GCM` key и IV;
2. шифрует исходный файл локально;
3. вычисляет `fileId = Base58(SHA-256(ciphertext))`;
4. подписанным HTTP `PUT` загружает ciphertext на свой текущий access-сервер;
5. отправляет обычный контентный DM `type=1/2`, plaintext которого начинается с `<S:file...>`.
Внутри `<S:file...>` находятся `fileId`, абсолютный URL сервера отправителя, AES-key/IV и исходная метаинформация файла. Так как весь plaintext контентного DM уже шифруется на ключ получателя/отправителя, сервер не получает ключ файла.
Получатель после E2EE-расшифровки DM скачивает ciphertext непосредственно с указанного access-сервера отправителя, сверяет `SHA-256`, расшифровывает файл в браузере и сохраняет исходный файл.
Отключение функции «Передача файлов» в локальных дополнительных настройках запрещает только отправку новых файлов; ранее полученные `<S:file...>` остаются скачиваемыми.
Хранение ciphertext и HTTP-контракт описаны в `docs/API/18_DM_File_Storage_API.md`.
@@ -68,7 +68,7 @@
- `kind` — ASCII-идентификатор типа вставки;
- параметры отделяются `;`;
- ключ и значение отделяются `=`;
- значения не экранируются в v1;
- значения обычных v1-вставок не экранируются; для `file` поля `url`, `name`, `mime` кодируются через percent-encoding (`encodeURIComponent`), чтобы `;`, `=` и `>` внутри метаданных не ломали блок;
- канонический новый префикс: `<S:`;
- legacy-префикс `<SHiNE:` продолжает поддерживаться при чтении.
@@ -127,7 +127,32 @@ fromLogin|toLogin|timeMs|nonce
- UI может рисовать такие сообщения отдельным специальным стилем.
- официальный UI не отправляет call-summary, если от старта исходящего звонка до его завершения прошло меньше `5` секунд.
## 7. Поведение официального UI
## 7. Тип `file`
Формат v1:
```text
<S:file;v=1;id=BASE58_SHA256;url=ENCODED_URL;key=BASE64URL_AES_KEY;iv=BASE64URL_IV;name=ENCODED_NAME;mime=ENCODED_MIME;size=123;encsize=139>📎 example.pdf
```
Поля:
- `id``Base58(SHA-256(ciphertext))`;
- `url` — абсолютный URL ciphertext на access-сервере отправителя, percent-encoded;
- `key` — случайный 32-byte AES-256 key в Base64URL;
- `iv` — случайный 12-byte AES-GCM IV в Base64URL;
- `name` — исходное безопасно нормализованное имя файла, percent-encoded;
- `mime` — исходный MIME, percent-encoded;
- `size` — размер plaintext в байтах;
- `encsize` — размер ciphertext в байтах.
`key`, `iv` и метаданные безопасно находятся здесь только потому, что весь plaintext DM затем шифруется существующим E2EE-механизмом. Они не передаются файловому HTTP endpoint отдельно.
Текст `📎 example.pdf` после блока служит fallback для старого клиента. Новый официальный UI вместо него рисует карточку файла и кнопку «Скачать».
Перед локальной AES-GCM-расшифровкой клиент обязан повторно вычислить SHA-256 скачанного ciphertext и сравнить Base58 с `id`.
## 8. Поведение официального UI
Официальный UI SHiNE в v1:
@@ -137,9 +162,10 @@ fromLogin|toLogin|timeMs|nonce
- `Звонок: H:MM:SS`
- `Звонил, но недозвонился: ...`
- для `reply` скрывает сам блок и показывает только текст ответа;
- для `file` скрывает fallback-текст и показывает карточку вложения с локальным download/decrypt;
- если исходное reply-сообщение не найдено, reply-preview не показывается.
## 8. Совместимость
## 9. Совместимость
Так как это часть plaintext, а не часть серверного envelope:
@@ -371,3 +371,12 @@ UI-примечание (байтовый формат не меняет): ра
Контейнер `type=7/8` является служебным tombstone очистки истории. Его наличие без обычных сообщений `type=1/2` не должно само по себе означать, что у пары есть видимый пользовательский диалог.
Следствие для UI/агрегата диалогов: `hasDialog` определяется наличием пользовательского содержимого (или непрочитанных пользовательских сообщений), а не наличием служебной записи состояния/tombstone. Формат контейнера при этом не изменяется.
## 15. Вложения `<S:file>` и бинарный формат
Поддержка файлов не добавляет полей в `SHiNE_DM` и не меняет порядок существующих полей.
Для контентных `type=1/2` технический блок `<S:file...>` является частью обычного plaintext, который затем попадает в уже существующий зашифрованный `body`. Сам ciphertext внешнего файла в `SHiNE_DM` не включается.
Поэтому подписи контейнера, `baseKey`, `revisionTimeMs`, `reencryptedAtMs`, алгоритм E2EE DM и правила парности входящей/исходящей копий остаются прежними.