From 353ef42752810d5cf6eace15d914b9c97d34af55 Mon Sep 17 00:00:00 2001 From: ycc Date: Mon, 2 Feb 2026 18:53:39 +0100 Subject: [PATCH] pw protected invitation get destroyed after 3 failed retrieval attempts --- server/invitation.go | 50 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/server/invitation.go b/server/invitation.go index bca8726..9440e61 100644 --- a/server/invitation.go +++ b/server/invitation.go @@ -2,6 +2,7 @@ package server import ( "crypto/rand" + "crypto/subtle" "errors" "fmt" "math/big" @@ -27,6 +28,24 @@ func (r *RedisRouter) StoreInvitation(invitation []byte, timeout int, password s } func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error) { + // Check failed attempts counter + failedAttemptsKey := "mwfa:" + id + failedAttempts := 0 // Default when key doesn't exist + val, err := r.Client.Get(failedAttemptsKey).Int() + if err == nil { + failedAttempts = val + } else if err != redis.Nil { + return nil, fmt.Errorf("failed to check attempts: %w", err) + } + // If err == redis.Nil, key doesn't exist, so failedAttempts stays 0 + + // If already hit the limit, delete invitation and fail + if failedAttempts >= 3 { + r.deleteInvitation(id) + return nil, errors.New("invitation locked due to too many failed attempts") + } + + // Check if password is required passRequired := false expectedpass, err := r.Client.Get("mwpw:" + id).Result() if err != nil { @@ -34,16 +53,43 @@ func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error) } else { passRequired = true } - if passRequired && password != expectedpass { - return nil, errors.New("auth failed") + + // Validate password with constant-time comparison + if passRequired { + if subtle.ConstantTimeCompare([]byte(password), []byte(expectedpass)) != 1 { + // Increment failed attempts + newCount := failedAttempts + 1 + r.Client.Set(failedAttemptsKey, newCount, 0) + + // If this was the 3rd attempt, delete invitation + if newCount >= 3 { + r.deleteInvitation(id) + return nil, errors.New("auth failed - invitation destroyed after 3 attempts") + } + return nil, errors.New("auth failed") + } } + + // Success - get invitation data mwiv, err := r.Client.Get("mwiv:" + id).Result() if err != nil { return nil, err } + + // Clear failed attempts counter on successful access + r.Client.Del(failedAttemptsKey) + return []byte(mwiv), nil } +// deleteInvitation removes all invitation-related keys from Redis +func (r *RedisRouter) deleteInvitation(id string) { + r.Client.Del("mwiv:" + id) // invitation data + r.Client.Del("mwpw:" + id) // password + r.Client.Del("mwfa:" + id) // failed attempts + r.Client.Del("mwan:" + id) // answer to invitation +} + func (r *RedisRouter) StoreAnswerToInvitation(id string, timeout int, invitation []byte, serverTimeout int) time.Time { if timeout > serverTimeout { timeout = serverTimeout