# 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/размер в `` внутри 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-объекта. Для legacy DM file v1 это фактически ограничивало целый файл. В DM file v2 файл состоит из 1-MiB ciphertext-объектов, поэтому общий размер логического файла этим параметром не ограничивается. ## 3. PUT `/dm-files/{fileId}` Загружает immutable ciphertext. `fileId` обязан быть равен: ```text Base58(SHA-256(ciphertext)) ``` ### Заголовки авторизации ```text Content-Type: application/octet-stream X-Shine-Session-Id: X-Shine-Time-Ms: X-Shine-Content-Length: X-Shine-Signature: ``` Подписываемая UTF-8 строка: ```text DM_FILE_UPLOAD_V1:{sessionId}:{fileId}:{encryptedSize}:{timeMs} ``` Подпись проверяется публичным `session_key` из активной сессии. Допустимое отклонение времени — 60 секунд. Сервер потоково пишет временный файл, одновременно считает SHA-256, проверяет фактический размер и только после успешной проверки атомарно перемещает объект под именем `{fileId}`. Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна. Перед ответом `alreadyExists=true` сервер повторно вычисляет `SHA-256` уже сохранённого объекта и сверяет его с `fileId`; одного совпадения имени файла на диске недостаточно. Если объект повреждён или подменён, сервер удаляет некорректную копию и принимает корректный `PUT` заново. Успешный ответ: ```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 без тела только после повторной проверки `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))`.