Files

86 lines
2.8 KiB
Go
Raw Permalink Normal View History

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
}