package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) const ( // Argon2 parameters (OWASP recommended) argonTime = 3 argonMemory = 64 * 1024 // 64 MB argonThreads = 4 argonKeyLen = 32 saltLen = 16 ) // HashPassword creates a secure hash using Argon2id func HashPassword(password string) (string, error) { // Generate random salt salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { return "", err } // Hash password hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) // Encode as: $argon2id$v=19$m=65536,t=3,p=4$salt$hash b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads, b64Salt, b64Hash), nil } // VerifyPassword checks if password matches hash (constant-time comparison) func VerifyPassword(password, encodedHash string) (bool, error) { // Parse hash format parts := strings.Split(encodedHash, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, fmt.Errorf("invalid hash format") } // Parse parameters var memory, time uint32 var threads uint8 _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads) if err != nil { return false, err } // Decode salt and hash salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } // Hash input password with same parameters hash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(expectedHash))) // Constant-time comparison return subtle.ConstantTimeCompare(hash, expectedHash) == 1, nil } // ValidatePasswordStrength checks password meets minimum requirements func ValidatePasswordStrength(password string) error { if len(password) < 12 { return fmt.Errorf("password must be at least 12 characters") } hasUpper := false hasLower := false hasDigit := false for _, char := range password { switch { case 'A' <= char && char <= 'Z': hasUpper = true case 'a' <= char && char <= 'z': hasLower = true case '0' <= char && char <= '9': hasDigit = true } } if !hasUpper || !hasLower || !hasDigit { return fmt.Errorf("password must contain uppercase, lowercase, and digits") } return nil }