Этап 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:
+25
-4
@@ -1,6 +1,7 @@
|
||||
// Экран входа и регистрации — docs/ui.md, «Вход и регистрация».
|
||||
|
||||
import { clear, confirmPanel, el, field, mark, message, setError, setNote } from "./dom.js";
|
||||
import { exportHistory } from "../export.js";
|
||||
import { EXPORT_FAILED, clear, confirmPanel, el, field, mark, message, setError, setNote } from "./dom.js";
|
||||
|
||||
const HINT = "пароль — это ключ шифрования, а не запись в базе. восстановления нет. "
|
||||
+ "не короче 12 символов; лучше — фраза из нескольких слов.";
|
||||
@@ -58,8 +59,11 @@ function screen(ctx, view, paint) {
|
||||
form.append(submit);
|
||||
|
||||
// На устройстве могут лежать ключи другого ника: вход под этим сотрёт
|
||||
// историю прежнего, поэтому сначала подтверждение (ADR-029).
|
||||
const wipe = confirmPanel("", "удалить");
|
||||
// историю прежнего, поэтому сначала подтверждение (ADR-029). История
|
||||
// на устройстве — единственная копия, и подтверждение предлагает сначала
|
||||
// сохранить её: секрет прежнего аккаунта ещё здесь, экспорту сессия
|
||||
// не нужна (ADR-014).
|
||||
const wipe = confirmPanel("", "удалить", "экспортировать");
|
||||
form.append(wipe.root);
|
||||
|
||||
const note = message();
|
||||
@@ -101,6 +105,22 @@ function screen(ctx, view, paint) {
|
||||
wipe.root.hidden = true;
|
||||
submit.hidden = false;
|
||||
};
|
||||
// Экспорт подтверждение не закрывает: архив скачался, а входить или нет —
|
||||
// отдельное решение.
|
||||
wipe.extra.addEventListener("click", async () => {
|
||||
if (wipe.extra.disabled) {
|
||||
return;
|
||||
}
|
||||
wipe.extra.disabled = true;
|
||||
fail("");
|
||||
try {
|
||||
await exportHistory();
|
||||
} catch {
|
||||
fail(EXPORT_FAILED);
|
||||
} finally {
|
||||
wipe.extra.disabled = false;
|
||||
}
|
||||
});
|
||||
wipe.no.addEventListener("click", () => {
|
||||
hideWipe();
|
||||
submit.focus();
|
||||
@@ -151,7 +171,8 @@ function screen(ctx, view, paint) {
|
||||
wipe.text.textContent = `на этом устройстве история @${other}. вход под другим ником удалит её.`;
|
||||
wipe.root.hidden = false;
|
||||
submit.hidden = true;
|
||||
wipe.yes.focus();
|
||||
// Фокус — на «экспортировать»: с него безопасно начинать.
|
||||
wipe.extra.focus();
|
||||
return;
|
||||
}
|
||||
await run();
|
||||
|
||||
Reference in New Issue
Block a user