62 lines
2.1 KiB
Go
62 lines
2.1 KiB
Go
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)
|
|
}
|