Этап 1: аккаунты — argon2id, сессии, ключевой блоб, вход и регистрация
Сервер: миграция 001 со всей схемой storage.md, store на modernc.org/sqlite (WAL, foreign_keys, один писатель), фоновая чистка раз в час, argon2id с параметрами ADR-021 и сверкой constant-time, сессии по SHA-256 токена, cookie bare_session, глобальная проверка Origin, девять эндпоинтов аккаунта. Ник в журнал не попадает: для /api/ пишется шаблон маршрута. Клиент: crypto.js по crypto.md построчно — мастер из пароля, два независимых ключа из мастера, ключевой блоб с ником в AAD, отпечаток от сырой точки; db.js со всеми хранилищами версии 1; экран входа и регистрации, настройки со сменой пароля, выходом и удалением аккаунта. Пароль не покидает клиент: проверено на боевом сервере — ни пароля, ни priv.d ни в одном теле запроса, вход на втором устройстве даёт тот же отпечаток. ADR-027: код internal для 500, причина только в журнале. ADR-028: тексты состояний клиента сведены в ui.md. ADR-029: вход под другим ником стирает историю только после подтверждения. ADR-030: верхняя граница итераций KDF, проверка границ на обеих сторонах. ADR-031: служебный выход перед повторным входом не заканчивает сеанс. ADR-032: каталог состояния 0700, файлы базы 0600. Прямые зависимости: modernc.org/sqlite, golang.org/x/crypto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ authKey = HKDF-SHA256(master, salt = пусто, info = "bare-auth-v1", 32 ба
|
||||
kek = HKDF-SHA256(master, salt = пусто, info = "bare-kek-v1") → AES-GCM-256
|
||||
```
|
||||
|
||||
`iter` — из `GET /api/kdf?nick=` перед входом, из `GET /api/config` при регистрации. Целевое значение сервера — 1 000 000, нижняя граница — 600 000 (ADR-013). WebCrypto: `deriveBits` из PBKDF2, результат импортируется `importKey("raw", …, "HKDF")`, дальше `deriveBits`/`deriveKey`.
|
||||
`iter` — из `GET /api/kdf?nick=` перед входом, из `GET /api/config` при регистрации. Целевое значение сервера — 1 000 000, границы — от 600 000 до 10 000 000 (ADR-013, ADR-030). Границы держат обе стороны: сервер не принимает блоб с `iter` вне них, клиент проверяет пришедшее число до `deriveBits` и не считает по нему ничего. WebCrypto: `deriveBits` из PBKDF2, результат импортируется `importKey("raw", …, "HKDF")`, дальше `deriveBits`/`deriveKey`.
|
||||
|
||||
`authKey` — единственное, что уходит на сервер. Пароль и `master` не покидают память клиента и не пишутся в IndexedDB.
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-027: Код `internal` для сбоя на стороне сервера
|
||||
|
||||
## Контекст
|
||||
|
||||
[ADR-026](026-protocol-error-codes.md) сделал перечень кодов ошибок в `protocol.md` исчерпывающим: клиент разбирает поле `error`, а не статус. Кода для `500` в перечне нет, а сбои существуют — недоступная база, ошибка записи. Этап 1 упёрся в это на первом же запросе к хранилищу: отвечать телом без кода нельзя, придумывать код в коде молча — тоже.
|
||||
|
||||
## Решение
|
||||
|
||||
- Сбой на стороне сервера — `500` с кодом `internal`. Сообщение общее и не зависит от причины.
|
||||
- Причина уходит только в журнал сервера: ни текст ошибки базы, ни имена таблиц, ни ник клиенту не показываются. Наружу — код и статус.
|
||||
- Код добавлен в перечень «Коды ошибок» `protocol.md` и в общие правила.
|
||||
- `internal` — не ветка протокола, а признак поломки: ни один сценарий клиента на него не рассчитывает, повтор запроса допустим.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Перечень кодов снова исчерпывающий: у любого ответа сервера есть разбираемый код.
|
||||
- Оператор видит причину в `journalctl`, клиент — нет.
|
||||
- `500` в журнале означает ошибку в коде или в окружении и разбирается, а не считается нормой.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR-028: Тексты состояний клиента
|
||||
|
||||
## Контекст
|
||||
|
||||
`docs/ui.md` задаёт пять ошибок формы входа и тексты экранов. Этап 1 упёрся в состояния, которых в этих перечнях нет, а показать их надо:
|
||||
|
||||
- запрос не дошёл (сети нет, сервер молчит) и ответ с кодом, на который у клиента нет сценария, — `500 internal` (ADR-027), `429`, `too_large`;
|
||||
- ключевой блоб не разбирается или не расшифровывается;
|
||||
- `iter` в блобе расходится с ответом `GET /api/kdf` — `docs/crypto.md` прямо требует показать это ошибкой;
|
||||
- пароль короче 12 символов: проверить длину может только клиент, сервер пароля не видит (ADR-013, ADR-015);
|
||||
- в настройках — несовпадение нового пароля с повтором, подтверждение опасной операции не тем паролем, ответ об успешной смене пароля.
|
||||
|
||||
Придумывать эти строки в коде молча нельзя: тексты — часть интерфейса, а не деталь реализации.
|
||||
|
||||
## Решение
|
||||
|
||||
Перечни `docs/ui.md` дополняются разделом «Тексты состояний». Правила прежние: строчные, коротко, говорят, что случилось. Ошибка — строкой цветом `mark`, ответ об успехе — той же строкой цветом `mute`.
|
||||
|
||||
- «нет соединения» — запрос не дошёл. Тот же текст, что у полосы в чате: состояние одно.
|
||||
- «сервер не справился, попробуйте позже» — код ответа, на который у клиента нет сценария.
|
||||
- «слишком часто, попробуйте позже» — `429 rate_limited`.
|
||||
- «пароль: не короче 12 символов» — проверка клиента при регистрации и смене пароля.
|
||||
- «пароли не совпадают» — новый пароль и повтор различаются.
|
||||
- «ключ аккаунта повреждён» — блоб не разобран, не расшифрован или не соответствует публичному ключу аккаунта.
|
||||
- «параметры ключа не совпали» — `iter` блоба не равен ответу `GET /api/kdf`.
|
||||
- «неверный пароль» — `401 invalid_credentials` в настройках, где ник заведомо свой.
|
||||
- «пароль изменён» — ответ на успешную смену.
|
||||
- «аккаунт и вся история будут удалены навсегда.» — подтверждение удаления аккаунта.
|
||||
|
||||
## Следствия
|
||||
|
||||
- `docs/ui.md` остаётся единственным местом, где живут тексты интерфейса.
|
||||
- Клиент разбирает `error` по перечню `docs/protocol.md`; всё, чего в перечне нет, и всё, что случилось до ответа, сводится к двум строкам — «нет соединения» и «сервер не справился, попробуйте позже».
|
||||
- Новый экран приносит свои тексты в `docs/ui.md` тем же порядком: сначала документ, потом код.
|
||||
@@ -0,0 +1,21 @@
|
||||
# ADR-029: Вход под другим ником стирает историю только после подтверждения
|
||||
|
||||
## Контекст
|
||||
|
||||
`docs/storage.md` держит правило «один аккаунт на браузерный профиль»: база стирается целиком, потому что история на устройстве — единственная копия. Стирание там разрешено только после подтверждения, и `docs/ui.md` описывает это подтверждение ровно в одном месте — у кнопки «выйти» в настройках.
|
||||
|
||||
Экран входа в это правило не попал, а достижим с целой базой: по `docs/ui.md` («Сеть и состояния») `401` выбрасывает на экран входа и намеренно оставляет IndexedDB нетронутой. Ввод другого ника в форму входа или регистрации уносил всю историю прежнего аккаунта молча, до единого вопроса.
|
||||
|
||||
Просто отказать во входе под другим ником нельзя: с экрана входа выйти из прежнего аккаунта нечем — сессии уже нет, настройки недоступны. Отказ запер бы устройство.
|
||||
|
||||
## Решение
|
||||
|
||||
- Если на устройстве лежат ключи другого ника, экран входа спрашивает подтверждение до вычисления ключа: «на этом устройстве история @nick. вход под другим ником удалит её.» с кнопками «удалить» и «отмена». Текст — в `docs/ui.md`, «Вход и регистрация».
|
||||
- Подтверждение — согласие, а не стирание: база уносится там же, где и раньше, — после успешного входа или регистрации. Отказ сервера ничего не удаляет.
|
||||
- Правило «один аккаунт на браузерный профиль» остаётся. Меняется одно: молчаливого стирания нет ни на одном экране.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Единственная копия истории не исчезает без вопроса ни в одном сценарии.
|
||||
- Кнопки «экспортировать» в этом подтверждении нет: экспорта нет вовсе до этапа 5. Когда он появится, кнопка придёт сюда тем же порядком — сначала `docs/ui.md`.
|
||||
- Сравнивается ник, а не отпечаток: перерегистрация под тем же ником вопроса не вызовет. Смена ключа у знакомого ника — предмет TOFU (ADR-016), этап 3.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-030: Верхняя граница итераций KDF и проверка границ на клиенте
|
||||
|
||||
Уточняет [ADR-013](013-password-policy-kdf.md): нижняя граница остаётся, к ней добавляется верхняя.
|
||||
|
||||
## Контекст
|
||||
|
||||
ADR-013 задаёт целевое число итераций PBKDF2 и нижнюю границу, верхней нет. `iter` — единственное поле ключевого блоба, которое сервер разбирает сам и потом сам же раздаёт клиентам через `GET /api/kdf`, то есть отвечает за его вменяемость. Регистрация одним запросом с `iter = 10^12` принималась: аккаунт после этого нельзя ни открыть, ни удалить — обе операции начинаются с PBKDF2, который не заканчивается.
|
||||
|
||||
С другой стороны, клиент брал число итераций из `GET /api/kdf` и `GET /api/config` как есть и считал по нему `authKey`, который тут же уходит на сервер. Нижнюю границу не проверял никто, кроме сервера, и только у блоба — а PBKDF2 считает клиент, и проверить параметр перед вычислением может только он.
|
||||
|
||||
## Решение
|
||||
|
||||
- Границы числа итераций — от 600 000 до 10 000 000. Верхняя — порядок над целевым значением 1 000 000: запас на повышение и предел, за которым вход перестаёт заканчиваться.
|
||||
- Сервер отвергает ключевой блоб с `iter` вне границ: `400 invalid`, `field: blob`.
|
||||
- Клиент проверяет границы до `deriveBits`: и число из `GET /api/kdf` и `GET /api/config`, и `iter` при разборе блоба. Число от сервера вне границ — «параметры ключа не совпали»; `iter` блоба вне границ — «ключ аккаунта повреждён», как любой другой дефект его формы (ADR-028).
|
||||
- Границы записаны в `docs/crypto.md` рядом с целевым значением.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Аккаунт с неоткрываемым `iter` завести нельзя.
|
||||
- Ослабить KDF ответом `/api/kdf` тоже нельзя: границу держат обе стороны, и клиентская стоит раньше вычисления. Активно-злонамеренный оператор остаётся вне модели угроз — он подменит и сам клиент.
|
||||
- Поднять целевое значение выше верхней границы без правки границы не выйдет. Это и требуется: такое повышение — решение, а не настройка.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-031: Служебный выход перед повторным входом
|
||||
|
||||
## Контекст
|
||||
|
||||
Ключевой блоб отдаёт только `POST /api/login`: `GET /api/me` его не возвращает, отдельного эндпоинта в `docs/protocol.md` нет. Поэтому смена пароля проходит через вход. Вход заводит новую сессию и перезаписывает cookie, а cookie — `HttpOnly`: прежний токен после этого недостижим, закрыть ту сессию клиенту уже нечем. Оставлять её живой нельзя — украденная cookie пережила бы смену пароля, ради которой всё и затевалось. Значит, выход идёт первым, до входа.
|
||||
|
||||
Но `POST /api/logout` — обычный непубличный запрос, и `401 unauthenticated` на нём по `docs/ui.md` («Сеть и состояния») выбрасывает на экран входа. Отсюда отказ. Смена пароля с неверным старым паролем закрывает сессию и падает на входе; клиент остаётся на настройках и показывает «неверный пароль». Вторая попытка, уже с верным паролем, начинается с того же служебного выхода, получает `401` — и уходит на экран входа молча: ошибка пишется в узел, которого в документе уже нет. Удаление аккаунта после такой попытки получает `401` на `DELETE /api/me` и тоже уезжает на экран входа, ничего не удалив.
|
||||
|
||||
## Решение
|
||||
|
||||
- Смена пароля и удаление аккаунта начинаются со служебного выхода, потом входят заново. Порядок «выход → вход» не оставляет на сервере сессию, токена от которой нет ни у кого.
|
||||
- Служебный выход не заканчивает сеанс для пользователя. `401 unauthenticated` на нём означает «сессии и так нет» и считается успехом: следующий шаг открывает новую. Обработчик истёкшей сессии на таком ответе не зовётся, ошибка не бросается. Прочие отказы — нет сети, `500` — поднимаются наверх и показываются как есть: при живой сессии входить заново нельзя.
|
||||
- Мимо обработчика идёт ровно этот вызов. `401 unauthenticated` на любом другом запросе по-прежнему ведёт на экран входа с сохранением IndexedDB.
|
||||
- Удаление аккаунта входит прямым `POST /api/login`, без разбора блоба: аккаунт с испорченным блобом обязан удаляться.
|
||||
- Кнопка «выйти» пользуется тем же вызовом: сеанс там заканчивает сам клиент — стирает базу и рисует экран входа, — а не ответ сервера.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Сорвавшаяся смена пароля оставляет клиент без сессии, но на своём экране и со своей строкой: «неверный пароль», «нет соединения». Следующая попытка — смена пароля или удаление аккаунта — начинается с того же служебного выхода и проходит целиком.
|
||||
- Перезагрузка страницы в этом состоянии показывает экран входа: `GET /api/me` отвечает `401`, IndexedDB цела. Это обычный сценарий истёкшей сессии, отдельного обхождения не требует.
|
||||
- Сервер не меняется: `POST /api/logout` и `POST /api/login` работают как записано в `docs/protocol.md`.
|
||||
- В `docs/ui.md` правило уточняется до `401 unauthenticated`: `401 invalid_credentials` — ошибка формы, на экран входа она не выбрасывала и раньше.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-032: Права на каталог состояния и файлы базы
|
||||
|
||||
Уточняет [ADR-022](022-deploy-nginx-systemd.md): к юниту добавлены `StateDirectoryMode` и `UMask`.
|
||||
|
||||
## Контекст
|
||||
|
||||
`docs/deploy.md` задавал владельца `/var/lib/bare`, но не режим. `StateDirectory=bare` создаёт каталог с режимом 0755, SQLite кладёт базу с 0644 — на целевой машине, где живут ещё десяток сайтов и чужие сервисы, файл базы читал любой локальный пользователь.
|
||||
|
||||
В базе нет плейнтекста, но есть `argon2id(authKey)` и ключевые блобы. Модель угроз прямо называет стойкость блоба к оффлайн-перебору равной стойкости пароля: раздавать этот материал соседям по машине незачем. Пункт «кража базы или бэкапа» подразумевает злоумышленника, а не любого пользователя системы.
|
||||
|
||||
## Решение
|
||||
|
||||
- Каталог `/var/lib/bare` — режим 0700, владелец `bare`. В юните `StateDirectoryMode=0700`, чтобы это переживало рестарт.
|
||||
- Файлы базы — 0600. В юните `UMask=0077`: `bare.db`, `-wal` и `-shm` создаются закрытыми.
|
||||
- `/etc/bare` — 0700, `/etc/bare/env` — 0600, владелец `root`: там VAPID-ключи.
|
||||
- Бэкап наследует те же права; `VACUUM INTO` пишет в тот же каталог.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Локальный пользователь без root не читает ни базу, ни секреты окружения.
|
||||
- От оператора машины это не защищает и не должно: он остаётся вне модели угроз.
|
||||
- Восстановление из бэкапа требует восстановить и права; строка про это есть в `docs/deploy.md`.
|
||||
+7
-2
@@ -16,8 +16,11 @@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bar
|
||||
sudo useradd --system --home /var/lib/bare --shell /usr/sbin/nologin bare
|
||||
sudo mkdir -p /opt/bare /var/lib/bare /etc/bare
|
||||
sudo chown bare:bare /var/lib/bare
|
||||
sudo chmod 0700 /var/lib/bare /etc/bare
|
||||
```
|
||||
|
||||
Права закрыты намеренно (ADR-032): в базе лежат `argon2id(authKey)` и ключевые блобы, машина общая.
|
||||
|
||||
`/etc/bare/env` (владелец root, режим 0600):
|
||||
|
||||
```
|
||||
@@ -48,6 +51,8 @@ ExecStart=/opt/bare/bare serve
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
StateDirectory=bare
|
||||
StateDirectoryMode=0700
|
||||
UMask=0077
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
@@ -128,8 +133,8 @@ ssh xmatic 'sudo install -m 0755 -o root -g root /tmp/bare /opt/bare/bare && sud
|
||||
|
||||
## Бэкап
|
||||
|
||||
`sqlite3 /var/lib/bare/bare.db "VACUUM INTO '/var/lib/bare/backup.db'"` или копия файла при остановленном сервисе. В базе только шифротексты и метаданные — бэкап не содержит переписки.
|
||||
`sqlite3 /var/lib/bare/bare.db "VACUUM INTO '/var/lib/bare/backup.db'"` или копия файла при остановленном сервисе. В базе только шифротексты и метаданные — бэкап не содержит переписки. Копия наследует режим 0600 (ADR-032); при восстановлении в другое место права надо выставить руками.
|
||||
|
||||
## Логи
|
||||
|
||||
Сервер пишет в stdout: время, метод, путь, статус, длительность; ник — только для ошибок аутентификации по лимитам; IP не пишется. journald хранит по своим правилам.
|
||||
Сервер пишет в stdout: время, метод, путь, статус, длительность; для маршрутов `/api/` вместо пути пишется шаблон (`/api/users/{nick}`), чтобы ник не попадал в журнал, а если отказ случился до маршрутизации (`Origin`, предел тела) и шаблона ещё нет — просто `/api/`; ник — только для ошибок аутентификации по лимитам; IP не пишется. Причины ответов `500 internal` (ADR-027) пишутся отдельной строкой, без данных запроса. journald хранит по своим правилам.
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ HTTP-API под `/api/`, JSON в обе стороны, `Content-Type: applicati
|
||||
- Тело запроса — до 32 КиБ, иначе `413 too_large`.
|
||||
- Rate limiting — `429` с `Retry-After` (секунды).
|
||||
- Неизвестный путь — `404 not_found`; неверный JSON — `400 bad_json`; валидация — `400 invalid` с полем `field`.
|
||||
- Сбой на стороне сервера — `500 internal`; причина остаётся в журнале сервера и клиенту не показывается (ADR-027).
|
||||
- Неподдерживаемый метод на известном пути — тоже `404 not_found`: кода `405` в протоколе нет (ADR-026).
|
||||
|
||||
## Типы
|
||||
@@ -127,7 +128,7 @@ event: ready data: {}
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
`unauthenticated`, `bad_origin`, `unknown_device`, `bad_json`, `invalid`, `invalid_nick`, `nick_taken`, `invite_required`, `invalid_invite`, `invalid_credentials`, `unknown_user`, `self`, `device_conflict`, `clock_skew`, `not_member`, `unknown_key`, `not_owner`, `owner`, `key_exists`, `keys_mismatch`, `not_found`, `rate_limited`, `too_large`.
|
||||
`unauthenticated`, `bad_origin`, `unknown_device`, `bad_json`, `invalid`, `invalid_nick`, `nick_taken`, `invite_required`, `invalid_invite`, `invalid_credentials`, `unknown_user`, `self`, `device_conflict`, `clock_skew`, `not_member`, `unknown_key`, `not_owner`, `owner`, `key_exists`, `keys_mismatch`, `not_found`, `rate_limited`, `too_large`, `internal`.
|
||||
|
||||
## Статика и служебное
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ DELETE FROM sessions WHERE expires_at < :now;
|
||||
|
||||
## Клиент — IndexedDB
|
||||
|
||||
База `bare`, версия 1. Один аккаунт на браузерный профиль: выход из аккаунта стирает базу целиком после подтверждения (история на этом устройстве — единственная копия).
|
||||
База `bare`, версия 1. Один аккаунт на браузерный профиль: выход из аккаунта стирает базу целиком после подтверждения (история на этом устройстве — единственная копия). Вход под другим ником стирает её так же и тоже после подтверждения — на экране входа (ADR-029).
|
||||
|
||||
```
|
||||
meta key: string → value
|
||||
|
||||
+21
-4
@@ -16,7 +16,9 @@
|
||||
|
||||
> пароль — это ключ шифрования, а не запись в базе. восстановления нет. не короче 12 символов; лучше — фраза из нескольких слов.
|
||||
|
||||
Кнопка одна, в стиле строки ввода. Пока идёт PBKDF2 — состояние «вычисляем ключ…», кнопка заблокирована. Ошибки — строкой под формой цветом `mark`: «неверный ник или пароль», «ник занят», «ник: 2–32 символа, a–z, 0–9, _», «нужен инвайт-код», «инвайт-код не подходит».
|
||||
Кнопка одна, в стиле строки ввода. Пока идёт PBKDF2 — состояние «вычисляем ключ…», кнопка заблокирована. Ошибки — строкой под формой цветом `mark`: «неверный ник или пароль», «ник занят», «ник: 2–32 символа, a–z, 0–9, _», «нужен инвайт-код», «инвайт-код не подходит». Форму ника и длину пароля клиент проверяет сам, до PBKDF2, в обоих режимах. Остальные состояния — «Тексты состояний».
|
||||
|
||||
Если на устройстве лежат ключи другого ника, до вычисления ключа — подтверждение «на этом устройстве история @nick. вход под другим ником удалит её.» с кнопками «удалить» и «отмена» (ADR-029). База стирается после успешного входа или регистрации; отказ сервера её не трогает.
|
||||
|
||||
## Список чатов (сайдбар)
|
||||
|
||||
@@ -53,9 +55,9 @@
|
||||
- «установить приложение»: кнопка, если есть `beforeinstallprompt`; на iOS — инструкция «поделиться → на экран «домой»».
|
||||
- «устройства»: список `id` (первые 8 символов), дата, «это устройство», «удалить».
|
||||
- «история»: «занято N МБ»; «экспорт» → скачивание `.bare`; «импорт» → выбор файла → «добавлено N сообщений» / «архив создан другим аккаунтом» / «файл повреждён».
|
||||
- «сменить пароль»: старый, новый, повтор; чекбокс «выйти на других устройствах».
|
||||
- «сменить пароль»: старый, новый, повтор; чекбокс «выйти на других устройствах». Ответ — «пароль изменён».
|
||||
- «выйти»: подтверждение «история на этом устройстве будет удалена. экспортировать сначала?» с кнопками «экспортировать», «выйти», «отмена».
|
||||
- «удалить аккаунт»: пароль + подтверждение.
|
||||
- «удалить аккаунт»: пароль + подтверждение «аккаунт и вся история будут удалены навсегда.» с кнопками «удалить» и «отмена».
|
||||
|
||||
## Баннер установки (iOS)
|
||||
|
||||
@@ -70,7 +72,22 @@
|
||||
- SSE переподключается браузером; после `ready` клиент перечитывает комнаты и контакты и повторяет `pending`.
|
||||
- Без сети: полоса «нет соединения» цветом `stone` над вводом; ввод не блокируется — сообщения уходят в `pending`.
|
||||
- `clock_skew` — «проверьте часы на устройстве: расхождение больше 5 минут».
|
||||
- `401` на любом запросе — выход на экран входа с сохранением IndexedDB (сессия истекла, история остаётся).
|
||||
- `401 unauthenticated` на любом запросе — выход на экран входа с сохранением IndexedDB (сессия истекла, история остаётся). Исключение одно: служебный выход перед повторным входом при смене пароля и удалении аккаунта (ADR-031) — там этот ответ означает, что сессии и так нет.
|
||||
|
||||
## Тексты состояний
|
||||
|
||||
Общие для всех форм строки (ADR-028). Ошибка — цветом `mark`, ответ об успехе — цветом `mute`, место одно.
|
||||
|
||||
| состояние | текст |
|
||||
|---|---|
|
||||
| запрос не дошёл | «нет соединения» |
|
||||
| код ответа, на который нет сценария (`internal`, `too_large`, прочее) | «сервер не справился, попробуйте позже» |
|
||||
| `429 rate_limited` | «слишком часто, попробуйте позже» |
|
||||
| пароль короче 12 символов | «пароль: не короче 12 символов» |
|
||||
| новый пароль и повтор различаются | «пароли не совпадают» |
|
||||
| ключевой блоб не разобран, не расшифрован или не соответствует публичному ключу | «ключ аккаунта повреждён» |
|
||||
| `iter` блоба не равен ответу `GET /api/kdf` | «параметры ключа не совпали» |
|
||||
| `401 invalid_credentials` в настройках | «неверный пароль» |
|
||||
|
||||
## Доступность
|
||||
|
||||
|
||||
Reference in New Issue
Block a user