mirror of
https://github.com/sonr-io/crypto.git
synced 2026-08-02 23:41:37 +00:00
No commit suggestions generated
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user