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:
8
go.mod
8
go.mod
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,12 +407,24 @@ 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 {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
|
|||||||
Reference in New Issue
Block a user