feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+61
View File
@@ -0,0 +1,61 @@
package auth
import (
"crypto/rand"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
)
// CodeLength is the fixed length of the decimal code delivered by
// SendEmailCode. The OpenAPI description ("six-digit") locks the value
// at six; tests cannot lower it without breaking the contract test
// against the schema.
const CodeLength = 6
// codeBcryptCost is the bcrypt cost used to store the hashed code in
// auth_challenges.code_hash. Cost 10 matches the convention documented
// for admin password storage in `backend/README.md` §12. Six-digit codes
// have only ~1M entropy, so the bcrypt slowdown is what bounds online
// attacks together with the per-challenge attempt ceiling.
const codeBcryptCost = bcrypt.DefaultCost
// generateCode returns a random CodeLength-character decimal string. The
// modulo bias when mapping uniform bytes to ten digits is acceptable for
// short-lived registration codes — the per-challenge attempt ceiling and
// the TTL bound abuse far more tightly than the negligible bias.
func generateCode() (string, error) {
digits := make([]byte, CodeLength)
if _, err := rand.Read(digits); err != nil {
return "", fmt.Errorf("auth: generate code: %w", err)
}
var sb strings.Builder
sb.Grow(CodeLength)
for _, b := range digits {
sb.WriteByte('0' + b%10)
}
return sb.String(), nil
}
// hashCode returns the bcrypt hash of code using the package-level cost.
func hashCode(code string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(code), codeBcryptCost)
}
// verifyCode reports whether code matches hash. The function is a thin
// wrapper around bcrypt.CompareHashAndPassword so the comparison is
// constant-time on the matching path. Returns nil on match,
// ErrCodeMismatch when the bcrypt mismatch error fires, and a wrapped
// error for any other failure (e.g. malformed hash).
func verifyCode(hash []byte, code string) error {
err := bcrypt.CompareHashAndPassword(hash, []byte(code))
if err == nil {
return nil
}
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return ErrCodeMismatch
}
return fmt.Errorf("auth: verify code: %w", err)
}