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

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.