4.8 KiB
HTTP API зашифрованных файлов личных сообщений
1. Назначение
Файлы личных сообщений хранятся только на access-сервере отправителя и только в зашифрованном виде. HTTP API не получает исходное имя файла, MIME-тип или ключ расшифрования.
Клиент:
- генерирует случайный AES-256-GCM key и 96-bit IV;
- шифрует файл в браузере;
- считает
SHA-256(ciphertext); - переводит 32-byte hash в Base58 — это
fileIdи имя файла на диске сервера; - загружает ciphertext на свой access-сервер;
- помещает URL, key, IV, исходное имя/MIME/размер в
<S:file...>внутри plaintext DM; - обычный механизм 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.