SHA256
Первая версия вложенных файлов работает
This commit is contained in:
@@ -82,6 +82,6 @@
|
||||
входящей копии на единственный access-сервер получателя.
|
||||
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
|
||||
- `ServerHello` пока принимает заявленный `serverLogin` на доверии и не является криптографической аутентификацией.
|
||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||
- HTTP endpoints зашифрованных DM-файлов (`PUT/GET/HEAD/OPTIONS /dm-files/{fileId}`) не являются WebSocket `op` и описаны отдельно в `18_DM_File_Storage_API.md`.
|
||||
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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. Конфигурация сервера
|
||||
|
||||
```properties
|
||||
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` обязан быть равен:
|
||||
|
||||
```text
|
||||
Base58(SHA-256(ciphertext))
|
||||
```
|
||||
|
||||
### Заголовки авторизации
|
||||
|
||||
```text
|
||||
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 строка:
|
||||
|
||||
```text
|
||||
DM_FILE_UPLOAD_V1:{sessionId}:{fileId}:{encryptedSize}:{timeMs}
|
||||
```
|
||||
|
||||
Подпись проверяется публичным `session_key` из активной сессии. Допустимое отклонение времени — 60 секунд.
|
||||
|
||||
Сервер потоково пишет временный файл, одновременно считает SHA-256, проверяет фактический размер и только после успешной проверки атомарно перемещает объект под именем `{fileId}`.
|
||||
|
||||
Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна.
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```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 без тела;
|
||||
- `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.
|
||||
@@ -9,6 +9,7 @@
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — доставка на
|
||||
единственный access-сервер, retry-воркер и UI-статусы
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
|
||||
- `docs/API/18_DM_File_Storage_API.md` — HTTP-хранилище зашифрованных файлов DM на access-сервере отправителя
|
||||
|
||||
Исторический устаревший документ сохранён отдельно:
|
||||
|
||||
@@ -19,3 +20,15 @@
|
||||
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
|
||||
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
|
||||
- локальная таблица пользователей для DM считается кэшем, а источником истины остаётся Solana PDA; если серверная логика DM меняет правила lazy-import пользователей из PDA, это тоже обязательно фиксируется в документации.
|
||||
|
||||
## Зашифрованные файлы DM
|
||||
|
||||
Официальный UI v1 умеет отправлять файл как внешний зашифрованный объект:
|
||||
|
||||
- файл шифруется в браузере `AES-256-GCM`;
|
||||
- ciphertext хранится только на access-сервере отправителя под именем `Base58(SHA-256(ciphertext))`;
|
||||
- AES-key, IV, исходное имя, MIME, размер и URL находятся в `<S:file...>` внутри plaintext DM, а значит сами защищены существующим E2EE DM;
|
||||
- получатель скачивает ciphertext с сервера отправителя, проверяет SHA-256, расшифровывает локально и только затем отдаёт Blob браузеру для сохранения под исходным именем;
|
||||
- сервер файлов ключ расшифрования не получает.
|
||||
|
||||
Формат бинарного контейнера `SHiNE_DM` при этом не меняется.
|
||||
|
||||
@@ -228,3 +228,22 @@ DeleteMessage принимает type=5/6. DeleteConversation принимает
|
||||
- при удалении чата с `friend`/`close_friend` UI должен отдельно предупредить, что одна очистка истории не уберёт строку чата, и при подтверждении снять социальную связь и очистить историю.
|
||||
|
||||
Это правило не меняет wire/API-формат DM и не меняет байтовый формат tombstone.
|
||||
|
||||
|
||||
## 15. Файлы в личных сообщениях
|
||||
|
||||
Файл не встраивается байтами в `SHiNE_DM`. До отправки DM официальный браузерный клиент:
|
||||
|
||||
1. генерирует отдельные случайные `AES-256-GCM` key и IV;
|
||||
2. шифрует исходный файл локально;
|
||||
3. вычисляет `fileId = Base58(SHA-256(ciphertext))`;
|
||||
4. подписанным HTTP `PUT` загружает ciphertext на свой текущий access-сервер;
|
||||
5. отправляет обычный контентный DM `type=1/2`, plaintext которого начинается с `<S:file...>`.
|
||||
|
||||
Внутри `<S:file...>` находятся `fileId`, абсолютный URL сервера отправителя, AES-key/IV и исходная метаинформация файла. Так как весь plaintext контентного DM уже шифруется на ключ получателя/отправителя, сервер не получает ключ файла.
|
||||
|
||||
Получатель после E2EE-расшифровки DM скачивает ciphertext непосредственно с указанного access-сервера отправителя, сверяет `SHA-256`, расшифровывает файл в браузере и сохраняет исходный файл.
|
||||
|
||||
Отключение функции «Передача файлов» в локальных дополнительных настройках запрещает только отправку новых файлов; ранее полученные `<S:file...>` остаются скачиваемыми.
|
||||
|
||||
Хранение ciphertext и HTTP-контракт описаны в `docs/API/18_DM_File_Storage_API.md`.
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
- `kind` — ASCII-идентификатор типа вставки;
|
||||
- параметры отделяются `;`;
|
||||
- ключ и значение отделяются `=`;
|
||||
- значения не экранируются в v1;
|
||||
- значения обычных v1-вставок не экранируются; для `file` поля `url`, `name`, `mime` кодируются через percent-encoding (`encodeURIComponent`), чтобы `;`, `=` и `>` внутри метаданных не ломали блок;
|
||||
- канонический новый префикс: `<S:`;
|
||||
- legacy-префикс `<SHiNE:` продолжает поддерживаться при чтении.
|
||||
|
||||
@@ -127,7 +127,32 @@ fromLogin|toLogin|timeMs|nonce
|
||||
- UI может рисовать такие сообщения отдельным специальным стилем.
|
||||
- официальный UI не отправляет call-summary, если от старта исходящего звонка до его завершения прошло меньше `5` секунд.
|
||||
|
||||
## 7. Поведение официального UI
|
||||
## 7. Тип `file`
|
||||
|
||||
Формат v1:
|
||||
|
||||
```text
|
||||
<S:file;v=1;id=BASE58_SHA256;url=ENCODED_URL;key=BASE64URL_AES_KEY;iv=BASE64URL_IV;name=ENCODED_NAME;mime=ENCODED_MIME;size=123;encsize=139>📎 example.pdf
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `id` — `Base58(SHA-256(ciphertext))`;
|
||||
- `url` — абсолютный URL ciphertext на access-сервере отправителя, percent-encoded;
|
||||
- `key` — случайный 32-byte AES-256 key в Base64URL;
|
||||
- `iv` — случайный 12-byte AES-GCM IV в Base64URL;
|
||||
- `name` — исходное безопасно нормализованное имя файла, percent-encoded;
|
||||
- `mime` — исходный MIME, percent-encoded;
|
||||
- `size` — размер plaintext в байтах;
|
||||
- `encsize` — размер ciphertext в байтах.
|
||||
|
||||
`key`, `iv` и метаданные безопасно находятся здесь только потому, что весь plaintext DM затем шифруется существующим E2EE-механизмом. Они не передаются файловому HTTP endpoint отдельно.
|
||||
|
||||
Текст `📎 example.pdf` после блока служит fallback для старого клиента. Новый официальный UI вместо него рисует карточку файла и кнопку «Скачать».
|
||||
|
||||
Перед локальной AES-GCM-расшифровкой клиент обязан повторно вычислить SHA-256 скачанного ciphertext и сравнить Base58 с `id`.
|
||||
|
||||
## 8. Поведение официального UI
|
||||
|
||||
Официальный UI SHiNE в v1:
|
||||
|
||||
@@ -137,9 +162,10 @@ fromLogin|toLogin|timeMs|nonce
|
||||
- `Звонок: H:MM:SS`
|
||||
- `Звонил, но недозвонился: ...`
|
||||
- для `reply` скрывает сам блок и показывает только текст ответа;
|
||||
- для `file` скрывает fallback-текст и показывает карточку вложения с локальным download/decrypt;
|
||||
- если исходное reply-сообщение не найдено, reply-preview не показывается.
|
||||
|
||||
## 8. Совместимость
|
||||
## 9. Совместимость
|
||||
|
||||
Так как это часть plaintext, а не часть серверного envelope:
|
||||
|
||||
|
||||
@@ -371,3 +371,12 @@ UI-примечание (байтовый формат не меняет): ра
|
||||
Контейнер `type=7/8` является служебным tombstone очистки истории. Его наличие без обычных сообщений `type=1/2` не должно само по себе означать, что у пары есть видимый пользовательский диалог.
|
||||
|
||||
Следствие для UI/агрегата диалогов: `hasDialog` определяется наличием пользовательского содержимого (или непрочитанных пользовательских сообщений), а не наличием служебной записи состояния/tombstone. Формат контейнера при этом не изменяется.
|
||||
|
||||
|
||||
## 15. Вложения `<S:file>` и бинарный формат
|
||||
|
||||
Поддержка файлов не добавляет полей в `SHiNE_DM` и не меняет порядок существующих полей.
|
||||
|
||||
Для контентных `type=1/2` технический блок `<S:file...>` является частью обычного plaintext, который затем попадает в уже существующий зашифрованный `body`. Сам ciphertext внешнего файла в `SHiNE_DM` не включается.
|
||||
|
||||
Поэтому подписи контейнера, `baseKey`, `revisionTimeMs`, `reencryptedAtMs`, алгоритм E2EE DM и правила парности входящей/исходящей копий остаются прежними.
|
||||
|
||||
Reference in New Issue
Block a user