package crypto import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "strings" "unicode/utf8" ) const ( // SessionTokenBytes — энтропия opaque-токена сессии (AGENT.md 7.1). SessionTokenBytes = 32 // MinPasswordLength — парольная политика MVP (AGENT.md 7.1). MinPasswordLength = 10 // MaxPasswordLength ограничивает стоимость Argon2id на длинных вводах. MaxPasswordLength = 256 ) // NewSessionToken возвращает токен для клиента и его SHA-256 для БД: // в базе хранятся только хэши токенов (AGENT.md 7.1). func NewSessionToken() (token string, hash string, err error) { buf := make([]byte, SessionTokenBytes) if _, err := rand.Read(buf); err != nil { return "", "", fmt.Errorf("generate session token: %w", err) } token = base64.RawURLEncoding.EncodeToString(buf) return token, HashToken(token), nil } // HashToken считает SHA-256 токена в hex — формат хранения в sessions.token_hash. func HashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // NewRecoveryCode возвращает резервный код 2FA в формате xxxx-xxxx-xxxx. func NewRecoveryCode() (string, error) { const alphabet = "abcdefghjkmnpqrstuvwxyz23456789" buf := make([]byte, 12) if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("generate recovery code: %w", err) } var b strings.Builder for i, v := range buf { if i > 0 && i%4 == 0 { b.WriteByte('-') } b.WriteByte(alphabet[int(v)%len(alphabet)]) } return b.String(), nil } // NormalizeEmail приводит email к каноническому виду для blind index. func NormalizeEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) } // ValidatePassword проверяет базовую политику пароля (AGENT.md 7.1). func ValidatePassword(password string) error { if utf8.RuneCountInString(password) < MinPasswordLength { return fmt.Errorf("password must be at least %d characters", MinPasswordLength) } if len(password) > MaxPasswordLength { return fmt.Errorf("password must be at most %d bytes", MaxPasswordLength) } return nil } // ValidateUsername проверяет публичный идентификатор (AGENT.md 7.1). func ValidateUsername(username string) error { if len(username) < 2 || len(username) > 32 { return fmt.Errorf("username must be 2-32 characters long") } for _, r := range username { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '.': default: return fmt.Errorf("username may contain only latin letters, digits, underscore and dot") } } return nil }