Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,52 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
|
||||
)
|
||||
|
||||
// CheckPasswordBreachUseCase checks if a password has been compromised in data breaches
|
||||
// CWE-521: Password breach checking to prevent use of compromised passwords
|
||||
type CheckPasswordBreachUseCase struct {
|
||||
breachChecker password.BreachChecker
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideCheckPasswordBreachUseCase creates a new CheckPasswordBreachUseCase
|
||||
func ProvideCheckPasswordBreachUseCase(
|
||||
breachChecker password.BreachChecker,
|
||||
logger *zap.Logger,
|
||||
) *CheckPasswordBreachUseCase {
|
||||
return &CheckPasswordBreachUseCase{
|
||||
breachChecker: breachChecker,
|
||||
logger: logger.Named("check-password-breach-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute checks if a password has been found in known data breaches
|
||||
// Returns an error if the password has been breached
|
||||
func (uc *CheckPasswordBreachUseCase) Execute(ctx context.Context, passwordStr string) error {
|
||||
uc.logger.Debug("checking password against breach database")
|
||||
|
||||
breachCount, err := uc.breachChecker.CheckPassword(ctx, passwordStr)
|
||||
if err != nil {
|
||||
// Log error but don't fail registration/login if breach check fails
|
||||
// This is a defense-in-depth measure, not a critical security control
|
||||
uc.logger.Warn("failed to check password breach status (non-fatal)",
|
||||
zap.Error(err))
|
||||
return nil // Don't block user if service is down
|
||||
}
|
||||
|
||||
if breachCount > 0 {
|
||||
uc.logger.Warn("password found in data breaches",
|
||||
zap.Int("breach_count", breachCount))
|
||||
return fmt.Errorf("password has been found in %d data breaches and cannot be used", breachCount)
|
||||
}
|
||||
|
||||
uc.logger.Debug("password not found in breaches")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
|
||||
)
|
||||
|
||||
// CheckTenantSlugAvailabilityUseCase checks if a tenant slug is available
|
||||
type CheckTenantSlugAvailabilityUseCase struct {
|
||||
tenantRepo domaintenant.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideCheckTenantSlugAvailabilityUseCase creates a new CheckTenantSlugAvailabilityUseCase
|
||||
func ProvideCheckTenantSlugAvailabilityUseCase(
|
||||
tenantRepo domaintenant.Repository,
|
||||
logger *zap.Logger,
|
||||
) *CheckTenantSlugAvailabilityUseCase {
|
||||
return &CheckTenantSlugAvailabilityUseCase{
|
||||
tenantRepo: tenantRepo,
|
||||
logger: logger.Named("check-tenant-slug-availability-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute checks if a tenant slug is available (not already taken)
|
||||
// CWE-203: Implements timing attack mitigation to prevent tenant enumeration
|
||||
func (uc *CheckTenantSlugAvailabilityUseCase) Execute(ctx context.Context, slug string) error {
|
||||
// Record start time for timing attack mitigation
|
||||
startTime := time.Now()
|
||||
|
||||
// Always perform the database lookup
|
||||
existingTenant, err := uc.tenantRepo.GetBySlug(ctx, slug)
|
||||
|
||||
// Store the result but don't return early - prevents timing leaks
|
||||
var resultError error
|
||||
|
||||
if err == nil && existingTenant != nil {
|
||||
// CWE-532: Use redacted tenant slug for logging
|
||||
uc.logger.Warn("tenant slug already exists",
|
||||
logger.TenantSlugHash(slug),
|
||||
logger.SafeTenantSlug("tenant_slug_redacted", slug))
|
||||
resultError = domaintenant.ErrTenantExists
|
||||
} else if err != nil && err != domaintenant.ErrTenantNotFound {
|
||||
// Real database error (not "not found")
|
||||
uc.logger.Error("failed to check tenant existence", zap.Error(err))
|
||||
resultError = err
|
||||
} else {
|
||||
// CWE-532: Use redacted tenant slug for logging
|
||||
// Slug is available (err == ErrTenantNotFound or no error with nil tenant)
|
||||
uc.logger.Debug("tenant slug is available",
|
||||
logger.TenantSlugHash(slug),
|
||||
logger.SafeTenantSlug("tenant_slug_redacted", slug))
|
||||
resultError = nil
|
||||
}
|
||||
|
||||
// CWE-203: Add random delay to prevent timing attacks
|
||||
// Ensures response time is similar whether tenant exists or not
|
||||
elapsed := time.Since(startTime)
|
||||
minResponseTime := 50 * time.Millisecond // Minimum response time
|
||||
maxJitter := 30 * time.Millisecond // Random jitter to add unpredictability
|
||||
|
||||
// Generate cryptographically secure random jitter
|
||||
jitterMs, _ := rand.Int(rand.Reader, big.NewInt(int64(maxJitter.Milliseconds())))
|
||||
jitter := time.Duration(jitterMs.Int64()) * time.Millisecond
|
||||
|
||||
targetDelay := minResponseTime + jitter
|
||||
if elapsed < targetDelay {
|
||||
time.Sleep(targetDelay - elapsed)
|
||||
}
|
||||
|
||||
return resultError
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
|
||||
)
|
||||
|
||||
// GetUserByEmailUseCase retrieves a user by email for authentication
|
||||
type GetUserByEmailUseCase struct {
|
||||
userRepo domainuser.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetUserByEmailUseCase creates a new GetUserByEmailUseCase
|
||||
func ProvideGetUserByEmailUseCase(
|
||||
userRepo domainuser.Repository,
|
||||
logger *zap.Logger,
|
||||
) *GetUserByEmailUseCase {
|
||||
return &GetUserByEmailUseCase{
|
||||
userRepo: userRepo,
|
||||
logger: logger.Named("get-user-by-email-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute retrieves a user by email globally (across all tenants)
|
||||
// Returns ErrInvalidCredentials instead of ErrUserNotFound for security (timing attack prevention)
|
||||
func (uc *GetUserByEmailUseCase) Execute(ctx context.Context, email string) (*domainuser.User, error) {
|
||||
user, err := uc.userRepo.GetByEmailGlobal(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, domainuser.ErrUserNotFound) {
|
||||
// CWE-532: Use hashed email to prevent PII in logs
|
||||
uc.logger.Warn("user not found for login",
|
||||
logger.EmailHash(email))
|
||||
// Return generic error to prevent email enumeration
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
uc.logger.Error("failed to get user by email", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
// CWE-532: Use hashed email to prevent PII in logs
|
||||
uc.logger.Debug("user found for login",
|
||||
zap.String("user_id", user.ID),
|
||||
logger.EmailHash(email))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
|
||||
)
|
||||
|
||||
// HashPasswordUseCase handles password validation and hashing
|
||||
type HashPasswordUseCase struct {
|
||||
passwordProvider password.PasswordProvider
|
||||
passwordValidator password.PasswordValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideHashPasswordUseCase creates a new HashPasswordUseCase
|
||||
func ProvideHashPasswordUseCase(
|
||||
passwordProvider password.PasswordProvider,
|
||||
passwordValidator password.PasswordValidator,
|
||||
logger *zap.Logger,
|
||||
) *HashPasswordUseCase {
|
||||
return &HashPasswordUseCase{
|
||||
passwordProvider: passwordProvider,
|
||||
passwordValidator: passwordValidator,
|
||||
logger: logger.Named("hash-password-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates password strength and returns the hashed password
|
||||
func (uc *HashPasswordUseCase) Execute(plainPassword string) (string, error) {
|
||||
// Validate password strength
|
||||
if err := uc.passwordValidator.ValidatePasswordStrength(plainPassword); err != nil {
|
||||
uc.logger.Warn("password validation failed", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Hash the password using secure string
|
||||
securePassword, err := securestring.NewSecureString(plainPassword)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to create secure string", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
defer securePassword.Wipe() // Clean up password from memory
|
||||
|
||||
passwordHash, err := uc.passwordProvider.GenerateHashFromPassword(securePassword)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to hash password", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
uc.logger.Debug("password hashed successfully")
|
||||
return passwordHash, nil
|
||||
}
|
||||
153
cloud/maplepress-backend/internal/usecase/gateway/login.go
Normal file
153
cloud/maplepress-backend/internal/usecase/gateway/login.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid email or password")
|
||||
)
|
||||
|
||||
// LoginInput represents the input for user login
|
||||
type LoginInput struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
// LoginOutput represents the output after successful login
|
||||
type LoginOutput struct {
|
||||
UserID string
|
||||
UserEmail string
|
||||
UserName string
|
||||
UserRole string
|
||||
TenantID string
|
||||
}
|
||||
|
||||
// LoginUseCase handles user authentication
|
||||
// Orchestrates the login workflow by coordinating focused usecases
|
||||
type LoginUseCase struct {
|
||||
// Focused usecases
|
||||
getUserByEmailUC *GetUserByEmailUseCase
|
||||
verifyPasswordUC *VerifyPasswordUseCase
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewLoginUseCase creates a new login use case
|
||||
func NewLoginUseCase(
|
||||
getUserByEmailUC *GetUserByEmailUseCase,
|
||||
verifyPasswordUC *VerifyPasswordUseCase,
|
||||
logger *zap.Logger,
|
||||
) *LoginUseCase {
|
||||
return &LoginUseCase{
|
||||
getUserByEmailUC: getUserByEmailUC,
|
||||
verifyPasswordUC: verifyPasswordUC,
|
||||
logger: logger.Named("login-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// ProvideLoginUseCase creates a new LoginUseCase for dependency injection
|
||||
func ProvideLoginUseCase(
|
||||
getUserByEmailUC *GetUserByEmailUseCase,
|
||||
verifyPasswordUC *VerifyPasswordUseCase,
|
||||
logger *zap.Logger,
|
||||
) *LoginUseCase {
|
||||
return NewLoginUseCase(getUserByEmailUC, verifyPasswordUC, logger)
|
||||
}
|
||||
|
||||
// Execute orchestrates the login workflow using focused usecases
|
||||
// CWE-208: Observable Timing Discrepancy - Uses timing-safe authentication
|
||||
func (uc *LoginUseCase) Execute(ctx context.Context, input *LoginInput) (*LoginOutput, error) {
|
||||
// CWE-532: Use hashed email to prevent PII in logs
|
||||
uc.logger.Info("authenticating user",
|
||||
logger.EmailHash(input.Email))
|
||||
|
||||
// Step 1: Get user by email globally (no tenant_id required for login)
|
||||
// Note: This returns ErrInvalidCredentials (not ErrUserNotFound) for security
|
||||
user, err := uc.getUserByEmailUC.Execute(ctx, input.Email)
|
||||
|
||||
// CWE-208: TIMING ATTACK MITIGATION
|
||||
// We must ALWAYS verify the password, even if the user doesn't exist.
|
||||
// This prevents timing-based user enumeration attacks.
|
||||
//
|
||||
// Timing attack scenario without mitigation:
|
||||
// - If user exists: database lookup (~10ms) + Argon2 hashing (~100ms) = ~110ms
|
||||
// - If user doesn't exist: database lookup (~10ms) = ~10ms
|
||||
// Attacker can measure response time to enumerate valid email addresses.
|
||||
//
|
||||
// With mitigation:
|
||||
// - If user exists: database lookup + Argon2 hashing
|
||||
// - If user doesn't exist: database lookup + Argon2 dummy hashing
|
||||
// Both paths take approximately the same time (~110ms).
|
||||
|
||||
var passwordHash string
|
||||
userExists := (err == nil && user != nil)
|
||||
|
||||
if userExists {
|
||||
// User exists - use real password hash
|
||||
if user.SecurityData != nil {
|
||||
passwordHash = user.SecurityData.PasswordHash
|
||||
}
|
||||
}
|
||||
// If user doesn't exist, passwordHash remains empty string
|
||||
// The verifyPasswordUC will use dummy hash for timing safety
|
||||
|
||||
// Step 2: Verify password - ALWAYS executed regardless of user existence
|
||||
if err := uc.verifyPasswordUC.ExecuteTimingSafe(input.Password, passwordHash, userExists); err != nil {
|
||||
// CWE-532: Use hashed email to prevent PII in logs
|
||||
if userExists {
|
||||
uc.logger.Warn("login failed: password verification failed",
|
||||
logger.EmailHash(input.Email),
|
||||
zap.String("tenant_id", user.TenantID))
|
||||
} else {
|
||||
uc.logger.Warn("login failed: user not found",
|
||||
logger.EmailHash(input.Email))
|
||||
}
|
||||
// Always return the same generic error regardless of reason
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Now check if user lookup failed (after timing-safe password verification)
|
||||
if err != nil {
|
||||
// This should never happen because ExecuteTimingSafe should have failed
|
||||
// But keep for safety
|
||||
uc.logger.Error("unexpected error after password verification", zap.Error(err))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// CWE-532: Use hashed email to prevent PII in logs
|
||||
uc.logger.Info("user authenticated successfully",
|
||||
zap.String("user_id", user.ID),
|
||||
logger.EmailHash(user.Email),
|
||||
zap.String("tenant_id", user.TenantID))
|
||||
|
||||
// Convert role to string (1="executive", 2="manager", 3="staff")
|
||||
roleStr := getRoleString(user.Role)
|
||||
|
||||
// Step 3: Build output
|
||||
return &LoginOutput{
|
||||
UserID: user.ID,
|
||||
UserEmail: user.Email,
|
||||
UserName: user.Name,
|
||||
UserRole: roleStr,
|
||||
TenantID: user.TenantID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getRoleString converts numeric role to string representation
|
||||
func getRoleString(role int) string {
|
||||
switch role {
|
||||
case 1:
|
||||
return "executive"
|
||||
case 2:
|
||||
return "manager"
|
||||
case 3:
|
||||
return "staff"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
|
||||
domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
|
||||
)
|
||||
|
||||
// RegisterInput represents the input for user registration validation
|
||||
type RegisterInput struct {
|
||||
Email string
|
||||
Password string
|
||||
FirstName string
|
||||
LastName string
|
||||
TenantName string
|
||||
TenantSlug string
|
||||
Timezone string
|
||||
|
||||
// Consent fields
|
||||
AgreeTermsOfService bool
|
||||
AgreePromotions bool
|
||||
AgreeToTrackingAcrossThirdPartyAppsAndServices bool
|
||||
|
||||
// Optional: IP address for audit trail
|
||||
CreatedFromIPAddress string
|
||||
}
|
||||
|
||||
// ValidateRegistrationInputUseCase validates registration input
|
||||
type ValidateRegistrationInputUseCase struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideValidateRegistrationInputUseCase creates a new ValidateRegistrationInputUseCase
|
||||
func ProvideValidateRegistrationInputUseCase(logger *zap.Logger) *ValidateRegistrationInputUseCase {
|
||||
return &ValidateRegistrationInputUseCase{
|
||||
logger: logger.Named("validate-registration-input-usecase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates the registration input fields
|
||||
func (uc *ValidateRegistrationInputUseCase) Execute(input *RegisterInput) error {
|
||||
if input.Email == "" {
|
||||
uc.logger.Warn("email is required")
|
||||
return domainuser.ErrEmailRequired
|
||||
}
|
||||
|
||||
if input.Password == "" {
|
||||
uc.logger.Warn("password is required")
|
||||
return domainuser.ErrPasswordRequired
|
||||
}
|
||||
|
||||
if input.FirstName == "" {
|
||||
uc.logger.Warn("first name is required")
|
||||
return domainuser.ErrFirstNameRequired
|
||||
}
|
||||
|
||||
if input.LastName == "" {
|
||||
uc.logger.Warn("last name is required")
|
||||
return domainuser.ErrLastNameRequired
|
||||
}
|
||||
|
||||
if input.TenantName == "" {
|
||||
uc.logger.Warn("tenant name is required")
|
||||
return domaintenant.ErrNameRequired
|
||||
}
|
||||
|
||||
if input.TenantSlug == "" {
|
||||
uc.logger.Warn("tenant slug is required")
|
||||
return domaintenant.ErrSlugRequired
|
||||
}
|
||||
|
||||
// Validate Terms of Service agreement (REQUIRED)
|
||||
if !input.AgreeTermsOfService {
|
||||
uc.logger.Warn("terms of service agreement is required")
|
||||
return domainuser.ErrTermsOfServiceRequired
|
||||
}
|
||||
|
||||
// Note: AgreePromotions and AgreeToTrackingAcrossThirdPartyAppsAndServices
|
||||
// are optional (defaults to false if not provided)
|
||||
|
||||
// CWE-532: Use hashed/redacted fields to prevent PII in logs
|
||||
uc.logger.Debug("registration input validated successfully",
|
||||
logger.EmailHash(input.Email),
|
||||
logger.TenantSlugHash(input.TenantSlug),
|
||||
zap.Bool("agree_terms", input.AgreeTermsOfService),
|
||||
zap.Bool("agree_promotions", input.AgreePromotions),
|
||||
zap.Bool("agree_tracking", input.AgreeToTrackingAcrossThirdPartyAppsAndServices))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue