monorepo/cloud/maplepress-backend/internal/usecase/gateway/verify_password.go

105 lines
3.7 KiB
Go

package gateway
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
)
// VerifyPasswordUseCase verifies a password against a hash
type VerifyPasswordUseCase struct {
passwordProvider password.PasswordProvider
logger *zap.Logger
}
// ProvideVerifyPasswordUseCase creates a new VerifyPasswordUseCase
func ProvideVerifyPasswordUseCase(
passwordProvider password.PasswordProvider,
logger *zap.Logger,
) *VerifyPasswordUseCase {
return &VerifyPasswordUseCase{
passwordProvider: passwordProvider,
logger: logger.Named("verify-password-usecase"),
}
}
// Execute verifies a plain password against a hashed password
// Returns ErrInvalidCredentials if password doesn't match (for security)
func (uc *VerifyPasswordUseCase) Execute(plainPassword, passwordHash string) error {
// Create secure string from password
securePassword, err := securestring.NewSecureString(plainPassword)
if err != nil {
uc.logger.Error("failed to create secure password", zap.Error(err))
return fmt.Errorf("failed to process password: %w", err)
}
defer securePassword.Wipe() // Clean up password from memory
// Verify password
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, passwordHash)
if err != nil {
uc.logger.Error("failed to compare password and hash", zap.Error(err))
return fmt.Errorf("failed to verify password: %w", err)
}
if !match {
uc.logger.Debug("password verification failed")
return ErrInvalidCredentials
}
uc.logger.Debug("password verified successfully")
return nil
}
// ExecuteTimingSafe verifies a password in a timing-safe manner
// CWE-208: Observable Timing Discrepancy - Prevents user enumeration via timing attacks
//
// This method ALWAYS performs password hashing, even when the user doesn't exist,
// to ensure constant-time behavior regardless of whether the email exists in the system.
//
// Parameters:
// - plainPassword: The password to verify
// - passwordHash: The hash to compare against (empty string if user doesn't exist)
// - userExists: Whether the user exists in the system
//
// Returns ErrInvalidCredentials if verification fails for any reason
func (uc *VerifyPasswordUseCase) ExecuteTimingSafe(plainPassword, passwordHash string, userExists bool) error {
// Create secure string from password
securePassword, err := securestring.NewSecureString(plainPassword)
if err != nil {
uc.logger.Error("failed to create secure password", zap.Error(err))
return fmt.Errorf("failed to process password: %w", err)
}
defer securePassword.Wipe() // Clean up password from memory
if !userExists || passwordHash == "" {
// User doesn't exist or no password hash available
// Perform dummy password hashing to maintain constant time
uc.logger.Debug("performing timing-safe dummy password verification")
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, password.DummyPasswordHash)
if err != nil {
// Even if dummy verification fails, we don't care about the error
// The important part is that we spent the same amount of time
uc.logger.Debug("dummy password verification completed", zap.Error(err))
}
_ = match // Explicitly ignore the result
return ErrInvalidCredentials
}
// User exists - perform real password verification
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, passwordHash)
if err != nil {
uc.logger.Error("failed to compare password and hash", zap.Error(err))
return ErrInvalidCredentials
}
if !match {
uc.logger.Debug("password verification failed")
return ErrInvalidCredentials
}
uc.logger.Debug("password verified successfully")
return nil
}