Files
SHiNE-server/docs/API/18_DM_File_Storage_API.md
T

7.0 KiB
Raw Blame History

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. Конфигурация сервера

dm.files.enabled=true
dm.files.storageDir=data/dm-files
dm.files.maxBytes=52428816

dm.files.maxBytes ограничивает размер одного ciphertext-объекта. Для legacy DM file v1 это фактически ограничивало целый файл. В DM file v2 файл состоит из 1-MiB ciphertext-объектов, поэтому общий размер логического файла этим параметром не ограничивается.

3. PUT /dm-files/{fileId}

Загружает immutable ciphertext.

fileId обязан быть равен:

Base58(SHA-256(ciphertext))

Заголовки авторизации

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 строка:

DM_FILE_UPLOAD_V1:{sessionId}:{fileId}:{encryptedSize}:{timeMs}

Подпись проверяется публичным session_key из активной сессии. Допустимое отклонение времени — 60 секунд.

Сервер потоково пишет временный файл, одновременно считает SHA-256, проверяет фактический размер и только после успешной проверки атомарно перемещает объект под именем {fileId}.

Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна. Перед ответом alreadyExists=true сервер повторно вычисляет SHA-256 уже сохранённого объекта и сверяет его с fileId; одного совпадения имени файла на диске недостаточно. Если объект повреждён или подменён, сервер удаляет некорректную копию и принимает корректный PUT заново.

Успешный ответ:

{
  "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 без тела только после повторной проверки Base58(SHA-256(ciphertext)) == fileId; официальный UI использует этот запрос перед PUT, чтобы не отправлять уже существующие байты повторно;
  • OPTIONS /dm-files/* обслуживает CORS preflight для браузерного PUT.

6. Reverse proxy

Caddy/Nginx должен проксировать /dm-files/* в тот же Jetty, что обслуживает /ws. Route должен находиться до SPA fallback.

7. Ограничения legacy v1

  • legacy-файл перед загрузкой целиком читается в память браузера и ограничен 50 MiB;
  • новые отправки официального UI используют v2 и этого ограничения не имеют;
  • удаления/TTL/garbage collection пока нет;
  • если ciphertext уже загружен, а отправка E2EE DM затем не удалась, объект может остаться orphan-файлом;
  • resumable/chunked upload не входит в v1.

8. DM file v2: большие файлы

Начиная с UI v2 один логический файл не загружается одним HTTP-объектом. Клиент режет plaintext на 1 MiB части и каждый AES-GCM ciphertext-кусок загружает отдельным обычным PUT /dm-files/{fileId}.

Поэтому dm.files.maxBytes — это лимит одного immutable HTTP-объекта, а не всего пользовательского файла. При стандартном chunkSize=1 MiB общий размер файла этим параметром не ограничивается.

Манифест также хранится через тот же immutable API:

  • encrypted manifest page — до 256 chunk descriptors;
  • encrypted root manifest — ссылки на страницы + BitTorrent v2 root metadata.

Никаких новых доверенных серверных операций для v2 не требуется: сервер по-прежнему только проверяет подпись PUT, длину и Base58(SHA-256(ciphertext)).