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, смена пароля, выключенная регистрация, отсутствие секретов в логах
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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) }
|
||||
Reference in New Issue
Block a user