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) }