package auth import ( "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/sha3" "encoding/base64" "errors" "fmt" "hash" "log/slog" "net/url" "regexp" "strings" "gopkg.awl.red/bizdex/config" ) const ( PersonRequestTypeUserApprove = "user_approve" PersonRequestTypePasswordReset = "password_reset" PersonRequestTypeChangeEmail = "change_email" PersonRequestTypeConfirmEmail = "confirm_email" ) var ( ErrNoUsername = errors.New("no username provided") ErrNoFirstname = errors.New("no firstname provided") ErrNoLastname = errors.New("no lastname provided") ErrPasswordMismatch = errors.New("password and confirm password do not match") ErrPasswordTooShort = errors.New("password too short") ) type VettedSignupRequest struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` Username string `json:"username"` Email string `json:"email"` RequestText string `json:"request_text"` Hashed string `json:"hashed"` } func HandleSignupForm(f url.Values, cfg *config.Config) (*VettedSignupRequest, error) { signupFails := []error{} firstname := f.Get("firstname") lastname := f.Get("lastname") username := f.Get("username") email := f.Get("email") password := f.Get("password") passwordConfirm := f.Get("password_confirm") requestText := f.Get("request_text") emailReStrs := strings.Split(cfg.EmailRegex, "@@@") if email != "" || cfg.EmailRequired { for _, reStr := range emailReStrs { re, err := regexp.Compile(reStr) if err != nil { slog.Error("bad email regex", "err", err, "re", reStr) return nil, err } if !re.MatchString(email) { signupFails = append(signupFails, fmt.Errorf("failed email regexp test %q", reStr)) } } } if username == "" { signupFails = append(signupFails, ErrNoUsername) } if firstname == "" { signupFails = append(signupFails, ErrNoFirstname) } if lastname == "" { signupFails = append(signupFails, ErrNoLastname) } if password != passwordConfirm { signupFails = append(signupFails, ErrPasswordMismatch) } if password != "" && len(password) < cfg.PasswordMin { signupFails = append(signupFails, ErrPasswordTooShort) } hashed, err := toPwHash(cfg.PasswordHashStrategy, cfg.PasswordSaltLength, password, cfg.PasswordRounds, cfg.PasswordKeyLength) if err != nil { signupFails = append(signupFails, err) } if allErrs := errors.Join(signupFails...); allErrs != nil { return nil, allErrs } return &VettedSignupRequest{ Firstname: firstname, Lastname: lastname, Username: username, Email: email, RequestText: requestText, Hashed: hashed, }, nil } func toPwHash(strategy string, saltLength int, password string, rounds int, keyLength int) (string, error) { var hsh func() hash.Hash switch strategy { case "SHA2_256": hsh = sha256.New case "SHA3_256": hsh = func() hash.Hash { return sha3.New256() } case "SHA3_512": hsh = func() hash.Hash { return sha3.New512() } default: slog.Warn("unknown password hash strategy", "unknown", strategy, "chosen_default", "SHA_256") hsh = sha256.New } saltBytes, err := newRandBytes(saltLength) if err != nil { return "", err } keyBytes, err := pbkdf2.Key(hsh, password, saltBytes, rounds, keyLength) if err != nil { return "", err } // strategy:salt:rounds:key_length:hash hashed := fmt.Sprintf("%s:%s:%d:%d:%s", strategy, AsB64(saltBytes), rounds, keyLength, AsB64(keyBytes), ) return hashed, nil } func AsB64(bs []byte) string { return base64.StdEncoding.EncodeToString(bs) } func newRandBytes(length int) ([]byte, error) { // salt := make([]byte, length) // for i := 0; i < length; i += 1 { // randByte, err := rand.Int(rand.Reader, big.NewInt(int64(256))) // if err != nil { // return nil, err // } // salt[i] = byte(randByte.Int64()) // } // return salt, nil randomBytes := make([]byte, length) _, err := rand.Read(randomBytes) return randomBytes, err } func NewToken() string { tokenBytes, _ := newRandBytes(64) return AsB64(tokenBytes) } const DelSessionCookie = "Session=; Path=/; Max-Age=0; HttpOnly; Secure; Partitioned; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT"