From 750223b2ccb6605cf5d940117ee0ee260e174d5c Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 17:44:31 +0700 Subject: [PATCH] Add challenge generator --- internal/pow/generator.go | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/pow/generator.go diff --git a/internal/pow/generator.go b/internal/pow/generator.go new file mode 100644 index 0000000..bc765b6 --- /dev/null +++ b/internal/pow/generator.go @@ -0,0 +1,97 @@ +package pow + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "time" +) + +// Generator handles creation of PoW challenges with HMAC signing +type Generator struct { + config *Config +} + +// GenerateOption represents a functional option for challenge generation +type generateOption func(*Challenge) + +// WithDifficulty sets custom difficulty for the challenge +func WithDifficulty(difficulty int) generateOption { + return func(c *Challenge) { + c.Difficulty = difficulty + } +} + +// NewGenerator creates a new challenge generator +func NewGenerator(config *Config) *Generator { + return &Generator{ + config: config, + } +} + +// GenerateChallenge creates a new HMAC-signed PoW challenge +func (g *Generator) GenerateChallenge(opts ...generateOption) (*Challenge, error) { + // Create challenge with defaults (no random data yet) + challenge := &Challenge{ + Timestamp: time.Now().Unix(), + Difficulty: g.config.DefaultDifficulty, + Resource: g.config.Resource, + } + + // Apply options + for _, opt := range opts { + opt(challenge) + } + + // Validate difficulty bounds + if challenge.Difficulty < g.config.MinDifficulty || challenge.Difficulty > g.config.MaxDifficulty { + return nil, ErrInvalidDifficulty + } + + // Generate cryptographic random data only after validation + challenge.Random = g.generateRandom(12) // 12 hex chars = 6 bytes + + // Sign the challenge with HMAC + signature, err := g.signChallenge(challenge) + if err != nil { + return nil, fmt.Errorf("failed to sign challenge: %w", err) + } + + challenge.HMAC = signature + return challenge, nil +} + +// signChallenge creates HMAC signature for the challenge +func (g *Generator) signChallenge(challenge *Challenge) (string, error) { + // Create canonical string for signing (excluding HMAC field) + canonical := g.canonicalChallengeString(challenge) + + // Create HMAC + h := hmac.New(sha256.New, g.config.HMACSecret) + h.Write([]byte(canonical)) + signature := h.Sum(nil) + + // Return base64url encoded signature + return base64.RawURLEncoding.EncodeToString(signature), nil +} + +// canonicalChallengeString creates a consistent string representation for HMAC +func (g *Generator) canonicalChallengeString(challenge *Challenge) string { + // Use consistent field ordering: timestamp:difficulty:resource:random + return fmt.Sprintf("%d:%d:%s:%s", + challenge.Timestamp, + challenge.Difficulty, + challenge.Resource, + challenge.Random, + ) +} + +// generateRandom creates cryptographic random hex string +func (g *Generator) generateRandom(length int) string { + bytes := make([]byte, length/2) + rand.Read(bytes) + return hex.EncodeToString(bytes) +}