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:
yyhuni
2026-01-11 20:58:53 +08:00
parent c94fe1ec4b
commit 3946a53337
3 changed files with 19 additions and 99 deletions

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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
}