// Package customerauth adds password-based signup/login to the profile service. // // It deliberately uses only the standard library: PBKDF2-HMAC-SHA256 for // password hashing (crypto/pbkdf2, Go 1.24+) and HMAC-SHA256-signed cookies for // sessions (crypto/hmac). No password ever leaves the server in plaintext and // the session token is opaque + tamper-evident. package customerauth import ( "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "strconv" "strings" ) // pbkdf2Iterations is the work factor for PBKDF2-HMAC-SHA256. 210_000 matches // the current OWASP recommendation for PBKDF2-SHA256. const pbkdf2Iterations = 210_000 const ( saltLen = 16 keyLen = 32 ) // HashPassword returns an encoded PBKDF2 hash of the form // // pbkdf2-sha256$$$ // // with a fresh random salt. The encoded string is self-describing so Verify can // read the parameters back without external configuration. func HashPassword(password string) (string, error) { salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("customerauth: read salt: %w", err) } dk, err := pbkdf2.Key(sha256.New, password, salt, pbkdf2Iterations, keyLen) if err != nil { return "", fmt.Errorf("customerauth: pbkdf2: %w", err) } return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", pbkdf2Iterations, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(dk), ), nil } // VerifyPassword reports whether password matches the encoded hash. It is // constant-time in the hash comparison and returns false for any malformed // encoding rather than erroring, so callers can treat it as a simple predicate. func VerifyPassword(password, encoded string) bool { parts := strings.Split(encoded, "$") if len(parts) != 4 || parts[0] != "pbkdf2-sha256" { return false } iter, err := strconv.Atoi(parts[1]) if err != nil || iter <= 0 { return false } salt, err := base64.RawStdEncoding.DecodeString(parts[2]) if err != nil { return false } want, err := base64.RawStdEncoding.DecodeString(parts[3]) if err != nil { return false } got, err := pbkdf2.Key(sha256.New, password, salt, iter, len(want)) if err != nil { return false } return subtle.ConstantTimeCompare(got, want) == 1 }