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
This commit is contained in:
2026-05-27 16:31:38 +03:00
parent 01cce981c5
commit 81c42e1a9a
3 changed files with 31 additions and 14 deletions

8
go.mod
View File

@@ -1,6 +1,6 @@
module gitea.mrixs.me/Mrixs/MrixsCraft-server module gitea.mrixs.me/Mrixs/MrixsCraft-server
go 1.22 go 1.25.0
require github.com/jackc/pgx/v5 v5.6.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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
golang.org/x/crypto v0.17.0 // indirect golang.org/x/crypto v0.52.0
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.37.0 // indirect
) )

View File

@@ -179,7 +179,11 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) {
} }
uuid := auth.GenerateUUID() 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(), _, err = h.db.Pool().Exec(r.Context(),
`INSERT INTO users (username, email, password_hash, uuid, role) `INSERT INTO users (username, email, password_hash, uuid, role)

View File

@@ -4,16 +4,17 @@ package auth
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
) )
@@ -380,10 +381,10 @@ func ExtractBearer(h string) string {
return "" 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 { func VerifyPassword(password, hash string) bool {
h := sha256.Sum256([]byte(password)) err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return subtle.ConstantTimeCompare([]byte(hex.EncodeToString(h[:])), []byte(hash)) == 1 return err == nil
} }
// GenerateToken creates a random hex token (16 bytes → 32 hex chars). // GenerateToken creates a random hex token (16 bytes → 32 hex chars).
@@ -406,11 +407,23 @@ func writeError(w http.ResponseWriter, status int, err, msg string) {
}) })
} }
// HashPassword returns the SHA-256 hex of a password for storage. // HashPassword returns a bcrypt hash of the password for storage.
func HashPassword(password string) string { func HashPassword(password string) (string, error) {
h := sha256.Sum256([]byte(password)) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return hex.EncodeToString(h[:]) 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. // GenerateUUID creates a random UUID v4-like string.
func GenerateUUID() string { func GenerateUUID() string {