7.0 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-объекта. Для 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)).