Доработать вложенные файлы и архив проекта

This commit is contained in:
AidarKC
2026-09-11 14:33:21 +03:00
parent f9ae811702
commit ef068eca81
22 changed files with 3865 additions and 216 deletions
+19 -5
View File
@@ -25,7 +25,7 @@ dm.files.storageDir=data/dm-files
dm.files.maxBytes=52428816
```
`dm.files.maxBytes` ограничивает размер ciphertext. Текущий официальный UI ограничивает исходный файл 50 MiB; AES-GCM добавляет 16-byte authentication tag.
`dm.files.maxBytes` ограничивает размер одного ciphertext-объекта. Для legacy DM file v1 это фактически ограничивало целый файл. В DM file v2 файл состоит из 1-MiB ciphertext-объектов, поэтому общий размер логического файла этим параметром не ограничивается.
## 3. PUT `/dm-files/{fileId}`
@@ -57,7 +57,7 @@ DM_FILE_UPLOAD_V1:{sessionId}:{fileId}:{encryptedSize}:{timeMs}
Сервер потоково пишет временный файл, одновременно считает SHA-256, проверяет фактический размер и только после успешной проверки атомарно перемещает объект под именем `{fileId}`.
Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна.
Повторная корректно подписанная загрузка уже существующего immutable объекта идемпотентна. Перед ответом `alreadyExists=true` сервер повторно вычисляет `SHA-256` уже сохранённого объекта и сверяет его с `fileId`; одного совпадения имени файла на диске недостаточно. Если объект повреждён или подменён, сервер удаляет некорректную копию и принимает корректный `PUT` заново.
Успешный ответ:
@@ -84,16 +84,30 @@ GET не требует пользовательской сессии: `fileId`
## 5. HEAD и OPTIONS
- `HEAD /dm-files/{fileId}` возвращает метаданные ciphertext без тела;
- `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. Ограничения v1
## 7. Ограничения legacy v1
- файл перед загрузкой целиком читается в память браузера, поэтому UI ограничен 50 MiB;
- 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))`.
+1
View File
@@ -9,6 +9,7 @@
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — доставка на
единственный access-сервер, retry-воркер и UI-статусы
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
- `docs/Personal_Messages/Файлы_DM_v2.md` — большие chunked-вложения, голосовые и BitTorrent v2 SHA-256/Merkle metadata
- `docs/API/18_DM_File_Storage_API.md` — HTTP-хранилище зашифрованных файлов DM на access-сервере отправителя
Исторический устаревший документ сохранён отдельно:
@@ -247,3 +247,19 @@ DeleteMessage принимает type=5/6. DeleteConversation принимает
Отключение функции «Передача файлов» в локальных дополнительных настройках запрещает только отправку новых файлов; ранее полученные `<S:file...>` остаются скачиваемыми.
Хранение ciphertext и HTTP-контракт описаны в `docs/API/18_DM_File_Storage_API.md`.
## 16. Дедупликация файлов и пересылка сообщений (2026-09-11)
Для HTTP-хранилища DM-файлов действует усиленное правило content-addressed хранения:
- перед загрузкой каждого ciphertext-объекта официальный UI делает `HEAD /dm-files/{fileId}`;
- если сервер подтверждает объект с тем же `fileId` и размером, `PUT` с байтами повторно не выполняется;
- сервер перед ответом на `HEAD`, `GET` и перед `alreadyExists=true` сам пересчитывает `SHA-256` сохранённого объекта и сверяет его с `fileId`;
- повреждённый или подменённый объект не считается существующим и не выдаётся клиенту; при следующем корректном `PUT` он может быть записан заново.
Пересылка личного сообщения не вводит новый тип DM и не добавляет признак «переслано». UI создаёт обычное новое контентное сообщение `type=1/2` выбранному собеседнику:
- для обычного текста переносится отображаемый текст сообщения;
- исходный `<S:reply...>` не переносится, поэтому новое сообщение не остаётся ответом на сообщение из старого чата;
- для сообщения с файлами повторно используются существующие `<S:file...>`-описатели, поэтому ciphertext не шифруется и не загружается повторно;
- сервер и получатель видят пересланное сообщение как обычное новое сообщение без служебной пометки об источнике.
@@ -172,3 +172,26 @@ fromLogin|toLogin|timeMs|nonce
- сервер не обязан понимать этот формат;
- будущие клиенты могут добавлять новые `kind`;
- клиенты, которые распознают SHiNE-вставки, должны скрывать неизвестные блоки целиком, если они стоят в начале и корректно закрыты.
## 10. Расширение `file` v2
Клиент обязан продолжать читать `v=1`. Новые chunked-вложения отправляются так:
```text
<S:file;v=2;id=ROOT_MANIFEST_ID;url=ENCODED_URL;key=BASE64URL_AES_KEY;ivp=BASE64URL_4BYTE_PREFIX;name=ENCODED_NAME;mime=ENCODED_MIME;size=123;encsize=456;chunk=1048576;chunks=7;th=TORRENT_V2_INFOHASH;pr=TORRENT_V2_PIECES_ROOT;kind=file;dur=0>
```
Дополнительные поля v2:
- `id` / `url` указывают не на весь файл, а на зашифрованный root manifest;
- `ivp` — случайный 4-byte IV prefix, из которого детерминированно строятся уникальные IV chunks/pages/root;
- `chunk` — plaintext chunk size, сейчас `1048576`;
- `chunks` — число частей;
- `th` — BitTorrent v2 SHA-256 infohash;
- `pr` — BitTorrent v2 pieces root;
- `kind=file|voice`;
- `dur` — длительность voice в миллисекундах, для обычного файла `0`.
В одном plaintext DM разрешено несколько последовательных `<S:file...>` блоков. Официальный UI собирает их в один список вложений и скрывает fallback-текст.
Подробный формат chunk encryption, manifest pages и BitTorrent v2 hashing: `Файлы_DM_v2.md`.
+124
View File
@@ -0,0 +1,124 @@
# Файлы и голосовые DM v2: chunked AES-GCM + BitTorrent v2 hashes
## Цели
DM file v2 убирает ограничение размера исходного файла, не требует держать файл целиком в RAM и сохраняет серверную модель «сервер видит только ciphertext».
Основные свойства:
- plaintext режется на куски по `1 MiB`;
- каждый кусок независимо шифруется `AES-256-GCM` одним случайным ключом файла, но с уникальным 96-bit IV;
- каждый ciphertext-кусок хранится как immutable объект `Base58(SHA-256(ciphertext))`;
- список кусков хранится в зашифрованных страницах манифеста по 256 записей;
- корневой манифест тоже зашифрован и content-addressed;
- E2EE DM содержит только ссылку на корневой манифест, AES-key, IV-prefix и пользовательские метаданные;
- один DM может содержать до 10 `<S:file;v=2...>` блоков;
- `kind=voice` использует тот же формат хранения и отдельный UI проигрывателя.
## IV и domain separation
На один файл создаётся случайный 4-byte `ivPrefix`.
12-byte IV строится как:
```text
ivPrefix[4] || uint64_be(token)
```
Диапазоны `token` разделены:
- chunks: `0 .. 2^63-1`;
- manifest pages: `2^63 + pageIndex`;
- root manifest: `0xffffffffffffffff`.
AAD также содержит домен (`chunk`, `page`, `root`), prefix и индекс. Поэтому перестановка ciphertext-кусков не проходит AES-GCM authentication.
## Manifest pages
Каждая страница после расшифровки содержит до 256 записей:
```json
{
"v": 2,
"page": 0,
"chunks": [
{
"i": 0,
"id": "Base58(SHA-256(ciphertext))",
"ps": 1048576,
"es": 1048592,
"ph": "BitTorrent-v2-piece-hash-base64url"
}
]
}
```
Страницы сами AES-GCM зашифрованы и загружаются в `/dm-files/{id}`.
## Root manifest
После расшифровки:
```json
{
"v": 2,
"scheme": "SHINE-DM-CHUNKED-AES-256-GCM",
"chunkSize": 1048576,
"fileSize": 123456789,
"chunkCount": 118,
"pages": [{"id":"...","count":118,"encryptedSize":12345}],
"torrent": {
"metaVersion": 2,
"blockLength": 16384,
"pieceLength": 1048576,
"piecesRoot": "...",
"infoHash": "..."
}
}
```
Имя файла и MIME в manifest не пишутся. Они остаются внутри E2EE DM.
## BitTorrent v2 совместимость
Хеширование соответствует BEP 52:
- базовый hash block: `16 KiB`;
- SHA-256;
- piece length: `1 MiB`;
- Merkle padding leaf = 32 zero bytes;
- `pieces root` вычисляется по правилам BitTorrent v2;
- `infoHash` = `SHA-256(bencode(info dictionary))`;
- piece-layer hash каждого 1-MiB куска хранится в зашифрованной manifest page.
Из `name`, `size`, `piecesRoot` и списка `ph` можно построить tracker-less `.torrent` v2 без повторного хеширования исходного файла.
Это совместимость метаданных и проверки контента. HTTP `/dm-files` пока не является BitTorrent peer transport: для настоящего P2P потребуется отдельный seeding/peer слой.
## Скачивание
Получатель:
1. получает и проверяет ciphertext root manifest по Base58(SHA-256);
2. расшифровывает root manifest;
3. по очереди получает manifest pages;
4. получает каждый chunk с сервера отправителя;
5. проверяет content address ciphertext;
6. расшифровывает chunk локально;
7. проверяет BitTorrent-v2 piece hash;
8. пишет plaintext на диск;
9. в конце проверяет итоговый `pieces root`.
В браузерах с File System Access API plaintext пишется на диск по частям и целиком в RAM не собирается. В остальных браузерах остаётся Blob fallback без искусственного лимита размера, но фактический предел зависит от памяти браузера.
## Голосовые
`MediaRecorder` пишет `Opus/WebM`, `Opus/Ogg` или поддерживаемый браузером audio MIME. После завершения запись проходит тот же DM file v2 pipeline.
Технический блок отличается полями:
```text
kind=voice;dur=<milliseconds>
```
Получатель видит player с Play/Pause, прогрессом и длительностью. Аудио расшифровывается только на клиенте.
@@ -380,3 +380,16 @@ UI-примечание (байтовый формат не меняет): ра
Для контентных `type=1/2` технический блок `<S:file...>` является частью обычного plaintext, который затем попадает в уже существующий зашифрованный `body`. Сам ciphertext внешнего файла в `SHiNE_DM` не включается.
Поэтому подписи контейнера, `baseKey`, `revisionTimeMs`, `reencryptedAtMs`, алгоритм E2EE DM и правила парности входящей/исходящей копий остаются прежними.
## 16. Примечание о пересылке сообщений (2026-09-11)
Пересылка не меняет байтовый формат `SHiNE_DM` и не добавляет отдельный `messageType` или флаг forward/repost.
Клиент формирует новый обычный plaintext для `type=1/2`:
- текстовая часть копируется как новое сообщение;
- `<S:reply...>` исходного сообщения удаляется;
- валидные `<S:file...>` могут быть скопированы без изменения, чтобы новое сообщение ссылалось на тот же уже загруженный зашифрованный объект;
- информация о том, из какого чата или сообщения выполнена пересылка, в контейнер не добавляется.
Таким образом подпись, `baseKey`, шифрование и структура контейнера остаются полностью прежними: меняется только содержимое нового plaintext перед стандартной отправкой.
File diff suppressed because it is too large Load Diff