feat: backend service
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user