mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
refactor(go-backend): switch from Django pbkdf2 to bcrypt
- Simplify password.go to use bcrypt (standard Go approach) - Remove Django password compatibility (not needed for fresh deployment) - Update auth_handler to use VerifyPassword() - All tests passing
This commit is contained in:
@@ -68,30 +68,6 @@ func TestJWTManager_ExpiredToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyDjangoPassword(t *testing.T) {
|
||||
// This is a real Django password hash for "admin"
|
||||
// Generated with: from django.contrib.auth.hashers import make_password; make_password("admin")
|
||||
djangoHash := "pbkdf2_sha256$600000$test_salt_here$7Ks8uN5FVNyXf0ItS5UqxTpmLZqLhBJKZQp5qJ5KXAQ="
|
||||
|
||||
// Test with correct password - this will fail because the hash is fake
|
||||
// In real tests, you'd use an actual Django-generated hash
|
||||
result := VerifyDjangoPassword("admin", djangoHash)
|
||||
// We expect false because the hash above is not a real hash
|
||||
if result {
|
||||
t.Log("Password verification passed (unexpected with fake hash)")
|
||||
}
|
||||
|
||||
// Test with wrong format
|
||||
if VerifyDjangoPassword("admin", "invalid-format") {
|
||||
t.Error("Should return false for invalid format")
|
||||
}
|
||||
|
||||
// Test with wrong algorithm
|
||||
if VerifyDjangoPassword("admin", "bcrypt$600000$salt$hash") {
|
||||
t.Error("Should return false for wrong algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
password := "test-password-123"
|
||||
|
||||
@@ -101,27 +77,22 @@ func TestHashPassword(t *testing.T) {
|
||||
t.Fatalf("Failed to hash password: %v", err)
|
||||
}
|
||||
|
||||
// Verify format
|
||||
if !hasValidDjangoFormat(hash) {
|
||||
t.Errorf("Hash should have Django format, got: %s", hash)
|
||||
// Hash should start with bcrypt prefix
|
||||
if len(hash) < 60 {
|
||||
t.Errorf("Hash should be at least 60 chars, got %d", len(hash))
|
||||
}
|
||||
|
||||
// Verify the password against the hash
|
||||
if !VerifyDjangoPassword(password, hash) {
|
||||
if !VerifyPassword(password, hash) {
|
||||
t.Error("Password verification should pass for correct password")
|
||||
}
|
||||
|
||||
// Verify wrong password fails
|
||||
if VerifyDjangoPassword("wrong-password", hash) {
|
||||
if VerifyPassword("wrong-password", hash) {
|
||||
t.Error("Password verification should fail for wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
func hasValidDjangoFormat(hash string) bool {
|
||||
parts := len(hash) > 0 && hash[:13] == "pbkdf2_sha256"
|
||||
return parts
|
||||
}
|
||||
|
||||
func TestHashPassword_Uniqueness(t *testing.T) {
|
||||
password := "same-password"
|
||||
|
||||
@@ -134,10 +105,10 @@ func TestHashPassword_Uniqueness(t *testing.T) {
|
||||
}
|
||||
|
||||
// But both should verify correctly
|
||||
if !VerifyDjangoPassword(password, hash1) {
|
||||
if !VerifyPassword(password, hash1) {
|
||||
t.Error("First hash should verify")
|
||||
}
|
||||
if !VerifyDjangoPassword(password, hash2) {
|
||||
if !VerifyPassword(password, hash2) {
|
||||
t.Error("Second hash should verify")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// Django default PBKDF2 iterations
|
||||
defaultIterations = 600000
|
||||
// Key length for PBKDF2
|
||||
keyLength = 32
|
||||
)
|
||||
|
||||
// VerifyDjangoPassword verifies a password against Django's pbkdf2_sha256 hash
|
||||
// Django format: pbkdf2_sha256$iterations$salt$hash
|
||||
func VerifyDjangoPassword(password, encoded string) bool {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm := parts[0]
|
||||
if algorithm != "pbkdf2_sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
iterations, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
salt := parts[2]
|
||||
expectedHash := parts[3]
|
||||
|
||||
// Decode the expected hash from base64
|
||||
expectedBytes, err := base64.StdEncoding.DecodeString(expectedHash)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compute PBKDF2 hash
|
||||
computedHash := pbkdf2.Key([]byte(password), []byte(salt), iterations, len(expectedBytes), sha256.New)
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
return subtle.ConstantTimeCompare(computedHash, expectedBytes) == 1
|
||||
}
|
||||
|
||||
// HashPassword creates a Django-compatible password hash
|
||||
// HashPassword creates a bcrypt hash of the password
|
||||
func HashPassword(password string) (string, error) {
|
||||
// Generate random salt (12 bytes, base64 encoded = 16 chars)
|
||||
saltBytes := make([]byte, 12)
|
||||
if _, err := rand.Read(saltBytes); err != nil {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
salt := base64.StdEncoding.EncodeToString(saltBytes)
|
||||
|
||||
// Compute PBKDF2 hash
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), defaultIterations, keyLength, sha256.New)
|
||||
hashBase64 := base64.StdEncoding.EncodeToString(hash)
|
||||
|
||||
// Format: pbkdf2_sha256$iterations$salt$hash
|
||||
return fmt.Sprintf("pbkdf2_sha256$%d$%s$%s", defaultIterations, salt, hashBase64), nil
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies a password against a bcrypt hash
|
||||
func VerifyPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !auth.VerifyDjangoPassword(req.Password, user.Password) {
|
||||
if !auth.VerifyPassword(req.Password, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user