Этап 5: история — экспорт и импорт .bare, устройства, место, пагинация
Экспорт: ключ архива из секрета аккаунта через HKDF, заголовок ровно 65 байт (magic, версия, соль, 32 сырых байта отпечатка владельца, iv) и он же целиком AAD шифротекста. Ника владельца в файле нет. Импорт сверяет отпечаток до расшифровки, сливает идемпотентно по id, а записи peers берёт только для ников, которых в локальном TOFU ещё нет: архивом доверие к ключу не перебить. Настройки: устройства с датой и пометкой «это устройство», «занято N МБ», кнопка «экспортировать» в подтверждении выхода и в подтверждении входа под другим ником — долг этапа 1 и обещание ADR-029 закрыты. Лента: страницы по 50 с подгрузкой вверх без прыжка прокрутки; новая страница вставляется, а не пересобирает ленту. ADR-050: импорт не перезаписывает лежащую запись — у своей есть состояние отправки, которого в архиве нет. ADR-054: архив — недоверенный ввод. Ревью собрало архивы с ts вне диапазона Date, мусорным lastId, ником с bidi-переопределением и roomId с обходом пути: каждый из них навсегда ломал ленту или счётчик. Теперь форма ника, roomId, id, ts и автора проверяется, а lastId из файла не читается вовсе. ADR-051, 052, 053: тексты и кнопки, устройства и место, страницы ленты без виртуализации. Приёмка: формат сверен побайтно на модулях, скачанных с боевого сервера, — смещения заголовка, отпечаток сырыми байтами, новые соль и iv на каждый экспорт, подмена любого байта заголовка ломает расшифровку, чужой секрет не открывает, семь видов битых файлов отвергнуты. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# ADR-054: Архив — недоверенный ввод: форму записей проверяет клиент
|
||||
|
||||
## Контекст
|
||||
|
||||
Архив собрал владелец аккаунта: ключ выводится из секрета аккаунта, а заголовок целиком лежит под тегом AEAD (ADR-014). Отсюда легко сделать неверный вывод — что содержимому файла можно верить.
|
||||
|
||||
Разбирается он на устройстве и ложится в базу рядом с настоящей историей. На сетевом пути форму держит сервер (`internal/api/valid.go`): ник — `[a-z0-9_]{2,32}` (ADR-019), идентификаторы — 22 символа base64url (`docs/crypto.md`), `ts` сервер ставит сам (ADR-017). Поэтому `sync.js` и обходится проверкой типа. У архива такой опоры нет: тег AEAD ловит порчу, но всё, что лежит под тегом, написал клиент — своей же прошлой или будущей версии. Архив живёт дольше версии, которая его собрала, и его разбор — единственное место, где клиент ест данные, которых больше никто не проверял.
|
||||
|
||||
Цена видна на двух примерах. `ts` вне диапазона `Date` роняет отрисовку ленты на своей строке: `Intl` бросает `RangeError`, лента обрывается, чат не открывается больше никогда. Чат с ником не по форме нельзя ни открыть маршрутом (`docs/ui.md`, «Каркас»), ни убрать из списка — карточка контакта до такого ника не доходит. Убрать негодную запись из базы нечем: экрана для этого нет и не будет.
|
||||
|
||||
Отдельный вопрос — незнакомая версия. Клиент отвечает на неё тем же текстом, что и на порчу, а `docs/ui.md` этого не говорит.
|
||||
|
||||
## Решение
|
||||
|
||||
- Форма проверяется при разборе, до записи в базу. Ник — `[a-z0-9_]{2,32}` (ADR-019); `roomId` — 22 символа base64url (`docs/crypto.md`, «Идентификаторы»); `id` сообщения — ULID; `ts` — целое от нуля до 8 640 000 000 000 000 (предел `Date`). Автор сообщения в личном чате — свой ник или ник собеседника: третьего в переписке двоих не бывает. В комнате автором бывает и вышедший участник, поэтому там сверяется только форма ника.
|
||||
- Что не по форме, до базы не доходит: пропускается запись целиком, а не поле.
|
||||
- Ничего, что устройство может посчитать само, из архива не читается: `lastId` чата считается по добавленному (ADR-050).
|
||||
- Незнакомая версия — файла в заголовке или нагрузки в поле `v` — показывается как «файл повреждён». Третьего текста нет: разобрать такой архив это устройство всё равно не может, а строку под формат, которого ещё нет, пришлось бы придумывать.
|
||||
- Записано в `docs/storage.md` и `docs/crypto.md`.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Запись, которую нельзя ни открыть, ни убрать, в базу не попадает.
|
||||
- Правила формы живут в двух местах: на сервере — для сети, в клиенте — для архива. Это цена того, что архив приходит с диска, а не из протокола.
|
||||
- Архив будущей версии старый клиент назовёт повреждённым. Цена принята: версия формата пока одна, а вторая заведёт свой текст тем же порядком — сначала `docs/ui.md`.
|
||||
- Проверка не защищает от оператора и не претендует на это: подделать архив без секрета аккаунта нельзя, а порчу ловит тег AEAD. Она защищает от собственных ошибок — от того, что записал клиент другой версии, и от того, что запишет он же завтра.
|
||||
Reference in New Issue
Block a user