No commit suggestions generated

This commit is contained in:
Prad Nukala
2025-10-09 15:10:39 -04:00
commit a934caa7d3
323 changed files with 98121 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
// Package password provides secure password handling and validation
package password
import (
"crypto/rand"
"fmt"
"unicode"
)
// PasswordConfig defines password policy requirements
type PasswordConfig struct {
MinLength int
MaxLength int
RequireUppercase bool
RequireLowercase bool
RequireDigits bool
RequireSpecial bool
MinEntropy float64
}
// DefaultPasswordConfig returns secure default password requirements
func DefaultPasswordConfig() *PasswordConfig {
return &PasswordConfig{
MinLength: 12,
MaxLength: 128,
RequireUppercase: true,
RequireLowercase: true,
RequireDigits: true,
RequireSpecial: true,
MinEntropy: 50.0, // bits
}
}
// Validator validates passwords against security policies
type Validator struct {
config *PasswordConfig
}
// NewValidator creates a password validator with the given configuration
func NewValidator(config *PasswordConfig) *Validator {
if config == nil {
config = DefaultPasswordConfig()
}
return &Validator{config: config}
}
// Validate checks if a password meets security requirements
func (v *Validator) Validate(password []byte) error {
// Check length
if len(password) < v.config.MinLength {
return fmt.Errorf("password must be at least %d characters", v.config.MinLength)
}
if len(password) > v.config.MaxLength {
return fmt.Errorf("password must not exceed %d characters", v.config.MaxLength)
}
// Check character requirements
var hasUpper, hasLower, hasDigit, hasSpecial bool
for _, ch := range string(password) {
switch {
case unicode.IsUpper(ch):
hasUpper = true
case unicode.IsLower(ch):
hasLower = true
case unicode.IsDigit(ch):
hasDigit = true
case unicode.IsSpace(ch):
// Spaces are allowed but not counted as special
case unicode.IsPunct(ch) || unicode.IsSymbol(ch):
hasSpecial = true
}
}
if v.config.RequireUppercase && !hasUpper {
return fmt.Errorf("password must contain at least one uppercase letter")
}
if v.config.RequireLowercase && !hasLower {
return fmt.Errorf("password must contain at least one lowercase letter")
}
if v.config.RequireDigits && !hasDigit {
return fmt.Errorf("password must contain at least one digit")
}
if v.config.RequireSpecial && !hasSpecial {
return fmt.Errorf("password must contain at least one special character")
}
// Check entropy
entropy := v.calculateEntropy(password)
if entropy < v.config.MinEntropy {
return fmt.Errorf("password entropy too low: %.1f bits (minimum: %.1f)",
entropy, v.config.MinEntropy)
}
return nil
}
// calculateEntropy estimates password entropy in bits
func (v *Validator) calculateEntropy(password []byte) float64 {
// Count unique characters
charSet := make(map[byte]bool)
for _, b := range password {
charSet[b] = true
}
// Estimate character pool size
poolSize := 0
var hasUpper, hasLower, hasDigit, hasSpecial bool
for ch := range charSet {
r := rune(ch)
switch {
case unicode.IsUpper(r):
hasUpper = true
case unicode.IsLower(r):
hasLower = true
case unicode.IsDigit(r):
hasDigit = true
case unicode.IsPunct(r) || unicode.IsSymbol(r):
hasSpecial = true
}
}
if hasLower {
poolSize += 26
}
if hasUpper {
poolSize += 26
}
if hasDigit {
poolSize += 10
}
if hasSpecial {
poolSize += 32 // Common special characters
}
if poolSize == 0 {
return 0
}
// Calculate entropy: log2(poolSize^length)
// Simplified: length * log2(poolSize)
bitsPerChar := 0.0
temp := poolSize
for temp > 0 {
bitsPerChar++
temp >>= 1
}
return float64(len(password)) * bitsPerChar
}
// GenerateSalt generates a cryptographically secure random salt
func GenerateSalt(size int) ([]byte, error) {
if size < 16 {
return nil, fmt.Errorf("salt size must be at least 16 bytes")
}
salt := make([]byte, size)
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("failed to generate salt: %w", err)
}
return salt, nil
}
// SecureCompare performs constant-time comparison of two byte slices
func SecureCompare(a, b []byte) bool {
if len(a) != len(b) {
return false
}
var result byte
for i := 0; i < len(a); i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
// ZeroBytes overwrites a byte slice with zeros
func ZeroBytes(b []byte) {
for i := range b {
b[i] = 0
}
}
+207
View File
@@ -0,0 +1,207 @@
package password
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidator_Validate(t *testing.T) {
validator := NewValidator(DefaultPasswordConfig())
testCases := []struct {
name string
password string
wantErr bool
errMsg string
}{
{
name: "too short",
password: "Short1!",
wantErr: true,
errMsg: "at least 12 characters",
},
{
name: "too long",
password: string(make([]byte, 129)),
wantErr: true,
errMsg: "not exceed 128 characters",
},
{
name: "missing uppercase",
password: "longenoughpassword123!",
wantErr: true,
errMsg: "uppercase letter",
},
{
name: "missing lowercase",
password: "LONGENOUGHPASSWORD123!",
wantErr: true,
errMsg: "lowercase letter",
},
{
name: "missing digit",
password: "LongEnoughPassword!",
wantErr: true,
errMsg: "one digit",
},
{
name: "missing special",
password: "LongEnoughPassword123",
wantErr: true,
errMsg: "special character",
},
{
name: "valid password",
password: "ValidPassword123!",
wantErr: false,
},
{
name: "complex valid password",
password: "MyS3cur3P@ssw0rd!2024",
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validator.Validate([]byte(tc.password))
if tc.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidator_CustomConfig(t *testing.T) {
config := &PasswordConfig{
MinLength: 8,
MaxLength: 64,
RequireUppercase: false,
RequireLowercase: true,
RequireDigits: true,
RequireSpecial: false,
MinEntropy: 30.0,
}
validator := NewValidator(config)
// Should pass with custom config
err := validator.Validate([]byte("simple123"))
assert.NoError(t, err)
// Should fail - too short
err = validator.Validate([]byte("short1"))
assert.Error(t, err)
// Should fail - no digits
err = validator.Validate([]byte("simplepass"))
assert.Error(t, err)
}
func TestGenerateSalt(t *testing.T) {
// Test valid salt generation
salt, err := GenerateSalt(32)
require.NoError(t, err)
assert.Len(t, salt, 32)
// Test different salt each time
salt2, err := GenerateSalt(32)
require.NoError(t, err)
assert.NotEqual(t, salt, salt2)
// Test minimum size enforcement
_, err = GenerateSalt(8)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least 16 bytes")
}
func TestSecureCompare(t *testing.T) {
// Test equal slices
a := []byte("password")
b := []byte("password")
assert.True(t, SecureCompare(a, b))
// Test different slices
c := []byte("different")
assert.False(t, SecureCompare(a, c))
// Test different lengths
d := []byte("pass")
assert.False(t, SecureCompare(a, d))
// Test empty slices
assert.True(t, SecureCompare([]byte{}, []byte{}))
}
func TestZeroBytes(t *testing.T) {
password := []byte("sensitive")
ZeroBytes(password)
for _, b := range password {
assert.Equal(t, byte(0), b)
}
}
func TestCalculateEntropy(t *testing.T) {
validator := NewValidator(nil)
testCases := []struct {
name string
password string
minEntropy float64
}{
{
name: "lowercase only",
password: "abcdefghij",
minEntropy: 40,
},
{
name: "alphanumeric",
password: "Abc123",
minEntropy: 30,
},
{
name: "complex",
password: "MyP@ssw0rd!",
minEntropy: 50,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
entropy := validator.calculateEntropy([]byte(tc.password))
assert.GreaterOrEqual(t, entropy, tc.minEntropy)
})
}
}
func BenchmarkValidate(b *testing.B) {
validator := NewValidator(DefaultPasswordConfig())
password := []byte("ValidPassword123!")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator.Validate(password)
}
}
func BenchmarkGenerateSalt(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = GenerateSalt(32)
}
}
func BenchmarkSecureCompare(b *testing.B) {
a := []byte("password123")
c := []byte("password123")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = SecureCompare(a, c)
}
}