Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,435 @@
package validator
import (
"fmt"
"math"
"strings"
"unicode"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
)
const (
// MinJWTSecretLength is the minimum required length for JWT secrets (256 bits)
MinJWTSecretLength = 32
// RecommendedJWTSecretLength is the recommended length for JWT secrets (512 bits)
RecommendedJWTSecretLength = 64
// MinEntropyBits is the minimum Shannon entropy in bits per character
// For reference: random base64 has ~6 bits/char, we require minimum 4.0
MinEntropyBits = 4.0
// MinProductionEntropyBits is the minimum entropy required for production
MinProductionEntropyBits = 4.5
// MaxRepeatingCharacters is the maximum allowed consecutive repeating characters
MaxRepeatingCharacters = 3
)
// WeakSecrets contains common weak/default secrets that should never be used
var WeakSecrets = []string{
"secret",
"password",
"changeme",
"change-me",
"change_me",
"12345",
"123456",
"1234567",
"12345678",
"123456789",
"1234567890",
"default",
"test",
"testing",
"admin",
"administrator",
"root",
"qwerty",
"qwertyuiop",
"letmein",
"welcome",
"monkey",
"dragon",
"master",
"sunshine",
"princess",
"football",
"starwars",
"baseball",
"superman",
"iloveyou",
"trustno1",
"hello",
"abc123",
"password123",
"admin123",
"guest",
"user",
"demo",
"sample",
"example",
}
// DangerousPatterns contains patterns that indicate a secret should be changed
var DangerousPatterns = []string{
"change",
"replace",
"update",
"modify",
"sample",
"example",
"todo",
"fixme",
"temp",
"temporary",
}
// CredentialValidator validates credentials and secrets for security issues
type CredentialValidator interface {
ValidateJWTSecret(secret string, environment string) error
ValidateAllCredentials(cfg *config.Config) error
}
type credentialValidator struct{}
// NewCredentialValidator creates a new credential validator
func NewCredentialValidator() CredentialValidator {
return &credentialValidator{}
}
// ValidateJWTSecret validates JWT secret strength and security
// CWE-798: Comprehensive validation to prevent hard-coded/weak credentials
func (v *credentialValidator) ValidateJWTSecret(secret string, environment string) error {
// Check minimum length
if len(secret) < MinJWTSecretLength {
return fmt.Errorf(
"JWT secret is too short (%d characters). Minimum required: %d characters (256 bits). "+
"Generate a secure secret with: openssl rand -base64 64",
len(secret),
MinJWTSecretLength,
)
}
// Check for common weak secrets (case-insensitive)
secretLower := strings.ToLower(secret)
for _, weak := range WeakSecrets {
if secretLower == weak || strings.Contains(secretLower, weak) {
return fmt.Errorf(
"JWT secret cannot contain common weak value: '%s'. "+
"Generate a secure secret with: openssl rand -base64 64",
weak,
)
}
}
// Check for dangerous patterns indicating default/placeholder values
for _, pattern := range DangerousPatterns {
if strings.Contains(secretLower, pattern) {
return fmt.Errorf(
"JWT secret contains suspicious pattern '%s' which suggests it's a placeholder. "+
"Generate a secure secret with: openssl rand -base64 64",
pattern,
)
}
}
// Check for repeating character patterns (e.g., "aaaa", "1111")
if err := checkRepeatingPatterns(secret); err != nil {
return fmt.Errorf(
"JWT secret validation failed: %s. "+
"Generate a secure secret with: openssl rand -base64 64",
err.Error(),
)
}
// Check for sequential patterns (e.g., "abcd", "1234")
if hasSequentialPattern(secret) {
return fmt.Errorf(
"JWT secret contains sequential patterns (e.g., 'abcd', '1234') which reduces entropy. "+
"Generate a secure secret with: openssl rand -base64 64",
)
}
// Calculate Shannon entropy
entropy := calculateShannonEntropy(secret)
minEntropy := MinEntropyBits
if environment == "production" {
minEntropy = MinProductionEntropyBits
}
if entropy < minEntropy {
return fmt.Errorf(
"JWT secret has insufficient entropy: %.2f bits/char (minimum: %.1f bits/char for %s). "+
"The secret appears to have low randomness. "+
"Generate a secure secret with: openssl rand -base64 64",
entropy,
minEntropy,
environment,
)
}
// In production, enforce stricter requirements
if environment == "production" {
// Check recommended length for production
if len(secret) < RecommendedJWTSecretLength {
return fmt.Errorf(
"JWT secret is too short for production environment (%d characters). "+
"Recommended: %d characters (512 bits). "+
"Generate a secure secret with: openssl rand -base64 64",
len(secret),
RecommendedJWTSecretLength,
)
}
// Check for sufficient character complexity
if !hasSufficientComplexity(secret) {
return fmt.Errorf(
"JWT secret has insufficient complexity for production. It should contain a mix of uppercase, lowercase, " +
"digits, and special characters (at least 3 types). Generate a secure secret with: openssl rand -base64 64",
)
}
// Validate base64-like characteristics (recommended generation method)
if !looksLikeBase64(secret) {
return fmt.Errorf(
"JWT secret does not appear to be randomly generated (expected base64-like characteristics). "+
"Generate a secure secret with: openssl rand -base64 64",
)
}
}
return nil
}
// ValidateAllCredentials validates all credentials in the configuration
func (v *credentialValidator) ValidateAllCredentials(cfg *config.Config) error {
var errors []string
// Validate JWT Secret
if err := v.ValidateJWTSecret(cfg.App.JWTSecret, cfg.App.Environment); err != nil {
errors = append(errors, fmt.Sprintf("JWT Secret validation failed: %s", err.Error()))
}
// In production, ensure other critical configs are not using defaults/placeholders
if cfg.App.Environment == "production" {
// Check Meilisearch API key
if cfg.Meilisearch.APIKey == "" {
errors = append(errors, "Meilisearch API key must be set in production")
} else if containsDangerousPattern(cfg.Meilisearch.APIKey) {
errors = append(errors, "Meilisearch API key appears to be a placeholder/default value")
}
// Check database hosts are not using localhost
for _, host := range cfg.Database.Hosts {
if strings.Contains(strings.ToLower(host), "localhost") || host == "127.0.0.1" {
errors = append(errors, "Database hosts should not use localhost in production")
break
}
}
// Check cache host is not localhost
if strings.Contains(strings.ToLower(cfg.Cache.Host), "localhost") || cfg.Cache.Host == "127.0.0.1" {
errors = append(errors, "Cache host should not use localhost in production")
}
}
if len(errors) > 0 {
return fmt.Errorf("credential validation failed:\n - %s", strings.Join(errors, "\n - "))
}
return nil
}
// calculateShannonEntropy calculates the Shannon entropy of a string in bits per character
// Shannon entropy measures the randomness/unpredictability of data
// Formula: H(X) = -Σ(p(x) * log2(p(x))) where p(x) is the probability of character x
func calculateShannonEntropy(s string) float64 {
if len(s) == 0 {
return 0
}
// Count character frequencies
frequencies := make(map[rune]int)
for _, char := range s {
frequencies[char]++
}
// Calculate entropy
var entropy float64
length := float64(len(s))
for _, count := range frequencies {
probability := float64(count) / length
entropy -= probability * math.Log2(probability)
}
return entropy
}
// hasSufficientComplexity checks if the secret has a good mix of character types
// Requires at least 3 out of 4 character types for production
func hasSufficientComplexity(secret string) bool {
var (
hasUpper bool
hasLower bool
hasDigit bool
hasSpecial bool
)
for _, char := range secret {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsDigit(char):
hasDigit = true
default:
hasSpecial = true
}
}
// Require at least 3 out of 4 character types
count := 0
if hasUpper {
count++
}
if hasLower {
count++
}
if hasDigit {
count++
}
if hasSpecial {
count++
}
return count >= 3
}
// checkRepeatingPatterns checks for excessive repeating characters
func checkRepeatingPatterns(s string) error {
if len(s) < 2 {
return nil
}
repeatCount := 1
lastChar := rune(s[0])
for _, char := range s[1:] {
if char == lastChar {
repeatCount++
if repeatCount > MaxRepeatingCharacters {
return fmt.Errorf(
"contains %d consecutive repeating characters ('%c'), maximum allowed: %d",
repeatCount,
lastChar,
MaxRepeatingCharacters,
)
}
} else {
repeatCount = 1
lastChar = char
}
}
return nil
}
// hasSequentialPattern detects common sequential patterns
func hasSequentialPattern(s string) bool {
if len(s) < 4 {
return false
}
// Check for at least 4 consecutive sequential characters
for i := 0; i < len(s)-3; i++ {
// Check ascending sequence (e.g., "abcd", "1234")
if s[i+1] == s[i]+1 && s[i+2] == s[i]+2 && s[i+3] == s[i]+3 {
return true
}
// Check descending sequence (e.g., "dcba", "4321")
if s[i+1] == s[i]-1 && s[i+2] == s[i]-2 && s[i+3] == s[i]-3 {
return true
}
}
return false
}
// looksLikeBase64 checks if the string has base64-like characteristics
// Base64 uses: A-Z, a-z, 0-9, +, /, and = for padding
func looksLikeBase64(s string) bool {
if len(s) < MinJWTSecretLength {
return false
}
var (
hasUpper bool
hasLower bool
hasDigit bool
validChars int
)
// Base64 valid characters
for _, char := range s {
switch {
case char >= 'A' && char <= 'Z':
hasUpper = true
validChars++
case char >= 'a' && char <= 'z':
hasLower = true
validChars++
case char >= '0' && char <= '9':
hasDigit = true
validChars++
case char == '+' || char == '/' || char == '=' || char == '-' || char == '_':
validChars++
default:
// Invalid character for base64
return false
}
}
// Should have good mix of character types typical of base64
charTypesCount := 0
if hasUpper {
charTypesCount++
}
if hasLower {
charTypesCount++
}
if hasDigit {
charTypesCount++
}
// Base64 typically has at least uppercase, lowercase, and digits
// Also check that it doesn't look like a repeated pattern
if charTypesCount < 3 {
return false
}
// Check for repeated patterns (e.g., "AbCd12!@" repeated)
// If the string has low unique character count relative to its length, it's probably not random
uniqueChars := make(map[rune]bool)
for _, char := range s {
uniqueChars[char] = true
}
// Random base64 should have at least 50% unique characters for strings over 32 chars
uniqueRatio := float64(len(uniqueChars)) / float64(len(s))
return uniqueRatio >= 0.4 // At least 40% unique characters
}
// containsDangerousPattern checks if a string contains any dangerous patterns
func containsDangerousPattern(value string) bool {
valueLower := strings.ToLower(value)
for _, pattern := range DangerousPatterns {
if strings.Contains(valueLower, pattern) {
return true
}
}
return false
}

View file

@ -0,0 +1,113 @@
package validator
import (
"testing"
)
// Simplified comprehensive test for JWT secret validation
func TestJWTSecretValidation(t *testing.T) {
validator := NewCredentialValidator()
// Good secrets - these should pass
goodSecrets := []struct {
name string
secret string
env string
}{
{
name: "Good 32-char for dev",
secret: "ima7xR+9nT0Yz0jKVu/QwtkqdAaU+3Ki",
env: "development",
},
{
name: "Good 64-char for prod",
secret: "1WDduocStecRuIv+Us1t/RnYDoW1ZcEEbU+H+WykJG+IT5WnijzBb8uUPzGKju+D",
env: "production",
},
}
for _, tt := range goodSecrets {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateJWTSecret(tt.secret, tt.env)
if err != nil {
t.Errorf("Expected no error for valid secret, got: %v", err)
}
})
}
// Bad secrets - these should fail
badSecrets := []struct {
name string
secret string
env string
mustContain string
}{
{
name: "Too short",
secret: "short",
env: "development",
mustContain: "too short",
},
{
name: "Common weak - password",
secret: "password-is-not-secure-but-32char",
env: "development",
mustContain: "common weak value",
},
{
name: "Dangerous pattern",
secret: "please-change-this-ima7xR+9nT0Yz",
env: "development",
mustContain: "suspicious pattern",
},
{
name: "Repeating characters",
secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
env: "development",
mustContain: "consecutive repeating characters",
},
{
name: "Sequential pattern",
secret: "abcdefghijklmnopqrstuvwxyzabcdef",
env: "development",
mustContain: "sequential patterns",
},
{
name: "Low entropy",
secret: "abababababababababababababababab",
env: "development",
mustContain: "insufficient entropy",
},
{
name: "Prod too short",
secret: "ima7xR+9nT0Yz0jKVu/QwtkqdAaU+3Ki",
env: "production",
mustContain: "too short for production",
},
}
for _, tt := range badSecrets {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateJWTSecret(tt.secret, tt.env)
if err == nil {
t.Errorf("Expected error containing '%s', got no error", tt.mustContain)
} else if !contains(err.Error(), tt.mustContain) {
t.Errorf("Expected error containing '%s', got: %v", tt.mustContain, err)
}
})
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View file

@ -0,0 +1,535 @@
package validator
import (
"strings"
"testing"
)
func TestCalculateShannonEntropy(t *testing.T) {
tests := []struct {
name string
input string
minBits float64
maxBits float64
expected string
}{
{
name: "Empty string",
input: "",
minBits: 0,
maxBits: 0,
expected: "should have 0 entropy",
},
{
name: "All same character",
input: "aaaaaaaaaa",
minBits: 0,
maxBits: 0,
expected: "should have very low entropy",
},
{
name: "Low entropy - repeated pattern",
input: "abcabcabcabc",
minBits: 1.5,
maxBits: 2.0,
expected: "should have low entropy",
},
{
name: "Medium entropy - simple password",
input: "Password123",
minBits: 3.0,
maxBits: 4.5,
expected: "should have medium entropy",
},
{
name: "High entropy - random base64",
input: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
minBits: 4.0,
maxBits: 6.0,
expected: "should have high entropy",
},
{
name: "Very high entropy - long random base64",
input: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
minBits: 4.5,
maxBits: 6.5,
expected: "should have very high entropy",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entropy := calculateShannonEntropy(tt.input)
if entropy < tt.minBits || entropy > tt.maxBits {
t.Errorf("%s: got %.2f bits/char, expected between %.1f and %.1f", tt.expected, entropy, tt.minBits, tt.maxBits)
}
})
}
}
func TestHasSufficientComplexity(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{
name: "Empty string",
input: "",
expected: false,
},
{
name: "Only lowercase",
input: "abcdefghijklmnop",
expected: false,
},
{
name: "Only uppercase",
input: "ABCDEFGHIJKLMNOP",
expected: false,
},
{
name: "Only digits",
input: "1234567890",
expected: false,
},
{
name: "Lowercase + uppercase",
input: "AbCdEfGhIjKl",
expected: false,
},
{
name: "Lowercase + digits",
input: "abc123def456",
expected: false,
},
{
name: "Uppercase + digits",
input: "ABC123DEF456",
expected: false,
},
{
name: "Lowercase + uppercase + digits",
input: "Abc123Def456",
expected: true,
},
{
name: "Lowercase + uppercase + special",
input: "AbC+DeF/GhI=",
expected: true,
},
{
name: "Lowercase + digits + special",
input: "abc123+def456/",
expected: true,
},
{
name: "All four types",
input: "Abc123+Def456/",
expected: true,
},
{
name: "Base64 string",
input: "K8vN2mP9sQ4tR7wY3zA6b+xK8vN2mP9sQ4tR7wY3zA6b=",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasSufficientComplexity(tt.input)
if result != tt.expected {
t.Errorf("hasSufficientComplexity(%q) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestCheckRepeatingPatterns(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
}{
{
name: "Empty string",
input: "",
shouldErr: false,
},
{
name: "Single character",
input: "a",
shouldErr: false,
},
{
name: "No repeating",
input: "abcdefgh",
shouldErr: false,
},
{
name: "Two repeating (ok)",
input: "aabcdeef",
shouldErr: false,
},
{
name: "Three repeating (ok)",
input: "aaabcdeee",
shouldErr: false,
},
{
name: "Four repeating (error)",
input: "aaaabcde",
shouldErr: true,
},
{
name: "Five repeating (error)",
input: "aaaaabcde",
shouldErr: true,
},
{
name: "Multiple groups of three (ok)",
input: "aaabbbccc",
shouldErr: false,
},
{
name: "Repeating in middle (error)",
input: "abcdddddef",
shouldErr: true,
},
{
name: "Repeating at end (error)",
input: "abcdefgggg",
shouldErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkRepeatingPatterns(tt.input)
if (err != nil) != tt.shouldErr {
t.Errorf("checkRepeatingPatterns(%q) error = %v, shouldErr = %v", tt.input, err, tt.shouldErr)
}
})
}
}
func TestHasSequentialPattern(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{
name: "Empty string",
input: "",
expected: false,
},
{
name: "Too short",
input: "abc",
expected: false,
},
{
name: "No sequential",
input: "acegikmo",
expected: false,
},
{
name: "Ascending sequence - abcd",
input: "xyzabcdefg",
expected: true,
},
{
name: "Descending sequence - dcba",
input: "xyzdcbafg",
expected: true,
},
{
name: "Ascending digits - 1234",
input: "abc1234def",
expected: true,
},
{
name: "Descending digits - 4321",
input: "abc4321def",
expected: true,
},
{
name: "Random characters",
input: "xK8vN2mP9sQ4",
expected: false,
},
{
name: "Base64-like",
input: "K8vN2mP9sQ4tR7wY3zA6b",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasSequentialPattern(tt.input)
if result != tt.expected {
t.Errorf("hasSequentialPattern(%q) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestLooksLikeBase64(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{
name: "Empty string",
input: "",
expected: false,
},
{
name: "Too short",
input: "abc",
expected: false,
},
{
name: "Only lowercase",
input: "abcdefghijklmnopqrstuvwxyzabcdef",
expected: false,
},
{
name: "Real base64",
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b=",
expected: true,
},
{
name: "Base64 without padding",
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b",
expected: true,
},
{
name: "Base64 with URL-safe chars",
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b-_",
expected: true,
},
{
name: "Generated secret",
input: "xK8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b",
expected: true,
},
{
name: "Simple password",
input: "Password123!Password123!Password123!",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := looksLikeBase64(tt.input)
if result != tt.expected {
t.Errorf("looksLikeBase64(%q) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestValidateJWTSecret(t *testing.T) {
validator := NewCredentialValidator()
tests := []struct {
name string
secret string
environment string
shouldErr bool
errContains string
}{
{
name: "Too short - 20 chars",
secret: "12345678901234567890",
environment: "development",
shouldErr: true,
errContains: "too short",
},
{
name: "Minimum length - 32 chars (acceptable for dev)",
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
environment: "development",
shouldErr: false,
},
{
name: "Common weak secret - contains password",
secret: "my-password-is-secure-123456789012",
environment: "development",
shouldErr: true,
errContains: "common weak value",
},
{
name: "Common weak secret - secret",
secret: "secretsecretsecretsecretsecretsec",
environment: "development",
shouldErr: true,
errContains: "common weak value",
},
{
name: "Common weak secret - contains 12345",
secret: "abcd12345efghijklmnopqrstuvwxyz",
environment: "development",
shouldErr: true,
errContains: "common weak value",
},
{
name: "Dangerous pattern - change",
secret: "please-change-this-j8EJm9ZKnuTYxcVK",
environment: "development",
shouldErr: true,
errContains: "suspicious pattern",
},
{
name: "Dangerous pattern - sample",
secret: "sample-secret-j8EJm9ZKnuTYxcVKQ",
environment: "development",
shouldErr: true,
errContains: "suspicious pattern",
},
{
name: "Repeating characters",
secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
environment: "development",
shouldErr: true,
errContains: "consecutive repeating characters",
},
{
name: "Sequential pattern - abcd",
secret: "abcdefghijklmnopqrstuvwxyzabcdef",
environment: "development",
shouldErr: true,
errContains: "sequential patterns",
},
{
name: "Sequential pattern - 1234",
secret: "12345678901234567890123456789012",
environment: "development",
shouldErr: true,
errContains: "sequential patterns",
},
{
name: "Low entropy secret",
secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP",
environment: "development",
shouldErr: true,
errContains: "insufficient entropy",
},
{
name: "Good secret - base64 style (dev)",
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
environment: "development",
shouldErr: false,
},
{
name: "Good secret - longer (dev)",
secret: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
environment: "development",
shouldErr: false,
},
{
name: "Production - too short (32 chars)",
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
environment: "production",
shouldErr: true,
errContains: "too short for production",
},
{
name: "Production - insufficient complexity",
secret: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01",
environment: "production",
shouldErr: true,
errContains: "insufficient complexity",
},
{
name: "Production - low entropy pattern",
secret: strings.Repeat("AbCd12!@", 8), // 64 chars but repetitive
environment: "production",
shouldErr: true,
errContains: "insufficient entropy",
},
{
name: "Production - good secret",
secret: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
environment: "production",
shouldErr: false,
},
{
name: "Production - excellent secret with padding",
secret: "7mK2nP8sR4wT6xZ3bA5cxK7mN1oQ9uS4vY2zA6bxK7mN1oQ9uS4vY2zA6b+W0E=",
environment: "production",
shouldErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateJWTSecret(tt.secret, tt.environment)
if tt.shouldErr {
if err == nil {
t.Errorf("ValidateJWTSecret() expected error containing %q, got no error", tt.errContains)
} else if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("ValidateJWTSecret() error = %q, should contain %q", err.Error(), tt.errContains)
}
} else {
if err != nil {
t.Errorf("ValidateJWTSecret() unexpected error: %v", err)
}
}
})
}
}
func TestValidateJWTSecret_EdgeCases(t *testing.T) {
validator := NewCredentialValidator()
t.Run("Secret with mixed weak patterns", func(t *testing.T) {
secret := "password123admin" // Contains multiple weak patterns
err := validator.ValidateJWTSecret(secret, "development")
if err == nil {
t.Error("Expected error for secret containing weak patterns, got nil")
}
})
t.Run("Secret exactly at minimum length", func(t *testing.T) {
// 32 characters exactly
secret := "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx"
err := validator.ValidateJWTSecret(secret, "development")
if err != nil {
t.Errorf("Expected no error for 32-char secret in development, got: %v", err)
}
})
t.Run("Secret exactly at recommended length", func(t *testing.T) {
// 64 characters exactly - using real random base64
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFir"
err := validator.ValidateJWTSecret(secret, "production")
if err != nil {
t.Errorf("Expected no error for 64-char secret in production, got: %v", err)
}
})
}
// Benchmark tests to ensure validation is performant
func BenchmarkCalculateShannonEntropy(b *testing.B) {
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR"
b.ResetTimer()
for i := 0; i < b.N; i++ {
calculateShannonEntropy(secret)
}
}
func BenchmarkValidateJWTSecret(b *testing.B) {
validator := NewCredentialValidator()
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator.ValidateJWTSecret(secret, "production")
}
}

View file

@ -0,0 +1,6 @@
package validator
// ProvideCredentialValidator provides a credential validator for dependency injection
func ProvideCredentialValidator() CredentialValidator {
return NewCredentialValidator()
}