Этап 6: закалка — лимиты ADR-021, аудит модели угроз, сверка документов
Лимиты: все четыре правила ADR-021 — регистрация 5/час на IP, вход 10/10 мин на IP и ник, сообщения 30/мин, прочие изменяющие 60/мин; 429 с Retry-After; X-Real-IP читается только с loopback, иначе адрес соединения — иначе заголовок отменял бы лимит на IP; карты вёдер ограничены поколениями. Аудит нашёл то, что пропустили пять раундов ревью: ADR-056: nginx вёл access_log с IP и полными путями вопреки обещанию deploy.md. Ники и социальный граф ложились в /var/log/nginx рядом с чистым журналом bare. ADR-058: «выйти на других устройствах» не обрывал уже открытый SSE — отозванная сессия продолжала получать сообщения. ADR-059: промежуточный ключ комнаты был невосстановим. Участник, пропустивший офлайн два rekey подряд, навсегда не расшифровал бы сообщения среднего ключа — вопреки обещанию storage.md о повторной попытке после получения keyId. ADR-063: ACK уходил по одному на конверт, а не пачкой. Получатель в оживлённой комнате выедал общее ведро подтверждениями и упирался в 429 на всех изменяющих запросах, включая выход из комнаты: 116 отказов за прогон стало нулём. ADR-055, 057, 060, 061, 062: ключи вёдер и границы, +dirty у bare version, 403 unknown_device не хоронит сообщение, усечение имени в подсказке ввода, kdf как оракул после повышения цели KDF. Модель угроз пополнена тем, что действительно видит оператор: push-подписки лежат в базе открытым текстом, и вместе с VAPID-ключом с той же машины это произвольное уведомление на экране блокировки. README приведён к v1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
+43
-6
@@ -80,28 +80,65 @@ func (s *Store) SetAuth(ctx context.Context, nick string, cred Credential) error
|
||||
// разъехавшиеся хеш и блоб означали бы аккаунт, в который нельзя войти
|
||||
// или ключ которого не расшифровать. При logoutOthers в той же транзакции
|
||||
// удаляются все сессии пользователя, кроме keep — текущей.
|
||||
func (s *Store) SetPassword(ctx context.Context, nick string, cred Credential, blob string, logoutOthers bool, keep []byte) error {
|
||||
//
|
||||
// Первое значение — устройства, к которым были привязаны удалённые сессии:
|
||||
// их потоки событий закрывает обработчик. Поток проверяет сессию только
|
||||
// при подключении, поэтому отозванная иначе продолжала бы получать
|
||||
// конверты до обрыва соединения (ADR-058).
|
||||
func (s *Store) SetPassword(ctx context.Context, nick string, cred Credential, blob string, logoutOthers bool, keep []byte) ([]string, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: смена пароля: %w", err)
|
||||
return nil, fmt.Errorf("store: смена пароля: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE users SET auth_hash = ?, auth_salt = ?, auth_params = ?, key_blob = ? WHERE nick = ?`,
|
||||
cred.Hash, cred.Salt, cred.Params, blob, nick); err != nil {
|
||||
return fmt.Errorf("store: смена пароля: %w", err)
|
||||
return nil, fmt.Errorf("store: смена пароля: %w", err)
|
||||
}
|
||||
var revoked []string
|
||||
if logoutOthers {
|
||||
revoked, err = revokedDevices(ctx, tx, nick, keep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM sessions WHERE nick = ? AND token_hash <> ?`, nick, keep); err != nil {
|
||||
return fmt.Errorf("store: смена пароля: %w", err)
|
||||
return nil, fmt.Errorf("store: смена пароля: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("store: смена пароля: %w", err)
|
||||
return nil, fmt.Errorf("store: смена пароля: %w", err)
|
||||
}
|
||||
return nil
|
||||
return revoked, nil
|
||||
}
|
||||
|
||||
// revokedDevices — устройства завершаемых сессий, кроме устройства текущей:
|
||||
// её оставляют, и закрывать её поток незачем.
|
||||
func revokedDevices(ctx context.Context, tx *sql.Tx, nick string, keep []byte) ([]string, error) {
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT DISTINCT device_id FROM sessions
|
||||
WHERE nick = ? AND token_hash <> ? AND device_id IS NOT NULL
|
||||
AND device_id NOT IN (SELECT device_id FROM sessions WHERE token_hash = ? AND device_id IS NOT NULL)
|
||||
ORDER BY device_id`, nick, keep, keep)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: устройства завершённых сессий: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, fmt.Errorf("store: устройства завершённых сессий: %w", err)
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: устройства завершённых сессий: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DeleteUser удаляет пользователя; устройства, сессии, контакты, членство,
|
||||
|
||||
Reference in New Issue
Block a user