Files
grendervill 16218ee045 feat(auth): сервис аккаунтов и аутентификации Фазы 1
- регистрация: политика пароля (≥10 + локальный словарь 10k утечек, SecLists MIT),
  username и email без учёта регистра, шифрование email с привязкой к аккаунту,
  blind index для поиска, автовступление в главный сервер с ролью @user
- вход: Argon2id+pepper, TOTP при включённом 2FA, блокировка после серии неудач,
  события безопасности; единый ответ на неверный пароль и неизвестный аккаунт
- сессии: opaque-токены (в БД только SHA-256), ротация токена, logout/logout-all,
  отзыв остальных сессий при смене пароля, step-up с окном 10 минут
- 2FA: настройка секрета с QR-URL, подтверждение кодом, 8 резервных кодов
  (хранятся хэшами), обязательность для инстанс-админов, запрет отключения
- тесты на реальной SQLite: регистрация/вход/дубликаты, слабые и утёкшие пароли,
  ротация и logout-all, TOTP и резервные коды, step-up, смена пароля,
  выключенная регистрация, отсутствие секретов в логах
2026-09-19 21:29:07 +03:00

56 lines
1.5 KiB
Go

package auth
import (
"bufio"
"embed"
"fmt"
"strings"
"glchat/internal/crypto"
)
//go:embed data/leaked-passwords.txt
var leakedPasswordsFS embed.FS
// passwordPolicy проверяет пароль по локальному словарю утечек: внешние API
// запрещены (AGENT.md 7.1).
type passwordPolicy struct {
leaked map[string]struct{}
}
func newPasswordPolicy() (*passwordPolicy, error) {
file, err := leakedPasswordsFS.Open("data/leaked-passwords.txt")
if err != nil {
return nil, fmt.Errorf("open leaked password list: %w", err)
}
defer file.Close()
leaked := make(map[string]struct{}, 10000)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
word := strings.TrimSpace(scanner.Text())
if word == "" {
continue
}
leaked[strings.ToLower(word)] = struct{}{}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("read leaked password list: %w", err)
}
return &passwordPolicy{leaked: leaked}, nil
}
// validate проверяет длину и отсутствие пароля в словаре утечек.
func (p *passwordPolicy) validate(password string) error {
if err := crypto.ValidatePassword(password); err != nil {
return err
}
if _, found := p.leaked[strings.ToLower(password)]; found {
return fmt.Errorf("password occurs in the leaked password list")
}
return nil
}
// LeakedCount возвращает размер словаря (для диагностики и метрик).
func (p *passwordPolicy) LeakedCount() int { return len(p.leaked) }