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

4.8 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. Текущий официальный UI ограничивает исходный файл 50 MiB; AES-GCM добавляет 16-byte authentication tag.

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 объекта идемпотентна.

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

{
  "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.