From 81c42e1a9a17f907c5b44a24b2ba2f27285bbf11 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Wed, 27 May 2026 16:31:38 +0300 Subject: [PATCH] feat: migrate passwords from SHA-256 to bcrypt - Replace SHA-256 hex hashing with bcrypt (cost 10) for password storage - VerifyPassword now uses bcrypt.CompareHashAndPassword - HashPassword returns (string, error) instead of string - Add IsBcryptHash helper to detect legacy hashes for future migration - Remove duplicate verifyPassword from api.go (already done in prev commit) - Promote golang.org/x/crypto to direct dependency --- go.mod | 8 ++++---- internal/api/api.go | 6 +++++- internal/auth/auth.go | 31 ++++++++++++++++++++++--------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index b2c73c0..9e7c1a6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitea.mrixs.me/Mrixs/MrixsCraft-server -go 1.22 +go 1.25.0 require github.com/jackc/pgx/v5 v5.6.0 @@ -8,7 +8,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.52.0 + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/internal/api/api.go b/internal/api/api.go index 227f33a..7e5d6ac 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -179,7 +179,11 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { } uuid := auth.GenerateUUID() - passwordHash := auth.HashPassword(req.Password) + passwordHash, err := auth.HashPassword(req.Password) + if err != nil { + writeError(w, http.StatusInternalServerError, "Failed to hash password") + return + } _, err = h.db.Pool().Exec(r.Context(), `INSERT INTO users (username, email, password_hash, uuid, role) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1c427a5..645092b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -4,16 +4,17 @@ package auth import ( "context" "crypto/rand" - "crypto/sha256" - "crypto/subtle" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net/http" "strings" "time" + "golang.org/x/crypto/bcrypt" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" ) @@ -380,10 +381,10 @@ func ExtractBearer(h string) string { return "" } -// VerifyPassword checks a plaintext password against a SHA-256 hex hash. +// VerifyPassword checks a plaintext password against a stored bcrypt hash. func VerifyPassword(password, hash string) bool { - h := sha256.Sum256([]byte(password)) - return subtle.ConstantTimeCompare([]byte(hex.EncodeToString(h[:])), []byte(hash)) == 1 + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil } // GenerateToken creates a random hex token (16 bytes → 32 hex chars). @@ -406,12 +407,24 @@ func writeError(w http.ResponseWriter, status int, err, msg string) { }) } -// HashPassword returns the SHA-256 hex of a password for storage. -func HashPassword(password string) string { - h := sha256.Sum256([]byte(password)) - return hex.EncodeToString(h[:]) +// HashPassword returns a bcrypt hash of the password for storage. +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", fmt.Errorf("hashing password: %w", err) + } + return string(hash), nil } +// IsBcryptHash reports whether the given hash looks like a bcrypt hash +// (starts with $2a$, $2b$, or $2y$). Used to detect legacy SHA-256 hashes. +func IsBcryptHash(hash string) bool { + return strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") +} + +// ErrPasswordHashing is returned when bcrypt hashing fails. +var ErrPasswordHashing = errors.New("password hashing failed") + // GenerateUUID creates a random UUID v4-like string. func GenerateUUID() string { b := make([]byte, 16)