SHA256
Доработать вложенные файлы и архив проекта
This commit is contained in:
@@ -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))`.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user