520 lines
13 KiB
Markdown
520 lines
13 KiB
Markdown
# Security Package
|
|
|
|
This package provides secure password hashing and memory-safe storage for sensitive data.
|
|
|
|
## Packages
|
|
|
|
### Password (`pkg/security/password`)
|
|
|
|
Provides Argon2id-based password hashing and verification with secure default parameters following OWASP recommendations.
|
|
|
|
### SecureString (`pkg/security/securestring`)
|
|
|
|
Memory-safe string storage using `memguard` to protect sensitive data like passwords and API keys from memory dumps and swap files.
|
|
|
|
### SecureBytes (`pkg/security/securebytes`)
|
|
|
|
Memory-safe byte slice storage using `memguard` to protect sensitive binary data.
|
|
|
|
### IPCountryBlocker (`pkg/security/ipcountryblocker`)
|
|
|
|
GeoIP-based country blocking using MaxMind's GeoLite2 database to block requests from specific countries.
|
|
|
|
## Installation
|
|
|
|
The packages are included in the project. Required dependencies:
|
|
- `github.com/awnumar/memguard` - For secure memory management
|
|
- `golang.org/x/crypto/argon2` - For password hashing
|
|
- `github.com/oschwald/geoip2-golang` - For GeoIP lookups
|
|
|
|
## Usage
|
|
|
|
### Password Hashing
|
|
|
|
```go
|
|
import (
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
|
|
)
|
|
|
|
// Create password provider
|
|
passwordProvider := password.NewPasswordProvider()
|
|
|
|
// Hash a password
|
|
plainPassword := "mySecurePassword123!"
|
|
securePass, err := securestring.NewSecureString(plainPassword)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
defer securePass.Wipe() // Always wipe after use
|
|
|
|
hashedPassword, err := passwordProvider.GenerateHashFromPassword(securePass)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
|
|
// Verify a password
|
|
match, err := passwordProvider.ComparePasswordAndHash(securePass, hashedPassword)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
if match {
|
|
// Password is correct
|
|
}
|
|
```
|
|
|
|
### Secure String Storage
|
|
|
|
```go
|
|
import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
|
|
|
|
// Store sensitive data securely
|
|
apiKey := "secret-api-key-12345"
|
|
secureKey, err := securestring.NewSecureString(apiKey)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
defer secureKey.Wipe() // Always wipe when done
|
|
|
|
// Use the secure string
|
|
keyValue := secureKey.String() // Get the value when needed
|
|
// ... use keyValue ...
|
|
|
|
// The original string should be cleared
|
|
apiKey = ""
|
|
```
|
|
|
|
### Secure Bytes Storage
|
|
|
|
```go
|
|
import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securebytes"
|
|
|
|
// Store sensitive binary data
|
|
sensitiveData := []byte{0x01, 0x02, 0x03, 0x04}
|
|
secureData, err := securebytes.NewSecureBytes(sensitiveData)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
defer secureData.Wipe()
|
|
|
|
// Use the secure bytes
|
|
data := secureData.Bytes()
|
|
// ... use data ...
|
|
|
|
// Clear the original slice
|
|
for i := range sensitiveData {
|
|
sensitiveData[i] = 0
|
|
}
|
|
```
|
|
|
|
### Generate Random Values
|
|
|
|
```go
|
|
passwordProvider := password.NewPasswordProvider()
|
|
|
|
// Generate random bytes
|
|
randomBytes, err := passwordProvider.GenerateSecureRandomBytes(32)
|
|
|
|
// Generate random hex string (length * 2 characters)
|
|
randomString, err := passwordProvider.GenerateSecureRandomString(16)
|
|
// Returns a 32-character hex string
|
|
```
|
|
|
|
### IP Country Blocking
|
|
|
|
```go
|
|
import (
|
|
"context"
|
|
"net"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcountryblocker"
|
|
)
|
|
|
|
// Create the blocker (typically done via dependency injection)
|
|
cfg, _ := config.Load()
|
|
blocker := ipcountryblocker.NewProvider(cfg, logger)
|
|
defer blocker.Close()
|
|
|
|
// Check if an IP is blocked
|
|
ip := net.ParseIP("192.0.2.1")
|
|
if blocker.IsBlockedIP(context.Background(), ip) {
|
|
// Handle blocked IP
|
|
return errors.New("access denied: your country is blocked")
|
|
}
|
|
|
|
// Check if a country code is blocked
|
|
if blocker.IsBlockedCountry("CN") {
|
|
// Country is in the blocked list
|
|
}
|
|
|
|
// Get country code for an IP
|
|
countryCode, err := blocker.GetCountryCode(context.Background(), ip)
|
|
if err != nil {
|
|
// Handle error
|
|
}
|
|
// countryCode will be ISO 3166-1 alpha-2 code like "US", "CA", "GB"
|
|
```
|
|
|
|
**Configuration**:
|
|
```bash
|
|
# Environment variables
|
|
APP_GEOLITE_DB_PATH=/path/to/GeoLite2-Country.mmdb
|
|
APP_BANNED_COUNTRIES=CN,RU,KP # Comma-separated ISO 3166-1 alpha-2 codes
|
|
```
|
|
|
|
## Password Hashing Details
|
|
|
|
### Algorithm: Argon2id
|
|
|
|
Argon2id is the recommended password hashing algorithm by OWASP. It combines:
|
|
- Argon2i: Resistant to side-channel attacks
|
|
- Argon2d: Resistant to GPU cracking attacks
|
|
|
|
### Default Parameters
|
|
|
|
```
|
|
Memory: 64 MB (65536 KB)
|
|
Iterations: 3
|
|
Parallelism: 2 threads
|
|
Salt Length: 16 bytes
|
|
Key Length: 32 bytes
|
|
```
|
|
|
|
These parameters provide strong security while maintaining reasonable performance for authentication systems.
|
|
|
|
### Hash Format
|
|
|
|
```
|
|
$argon2id$v=19$m=65536,t=3,p=2$<base64-salt>$<base64-hash>
|
|
```
|
|
|
|
Example:
|
|
```
|
|
$argon2id$v=19$m=65536,t=3,p=2$YWJjZGVmZ2hpamtsbW5vcA$9XJqrJ8fQvVrMz0FqJ7gBGqKvYLvLxC8HzPqKvYLvLxC
|
|
```
|
|
|
|
The hash includes all parameters, so it can be verified even if you change the default parameters later.
|
|
|
|
## Security Best Practices
|
|
|
|
### 1. Always Wipe Sensitive Data
|
|
|
|
```go
|
|
securePass, _ := securestring.NewSecureString(password)
|
|
defer securePass.Wipe() // Ensures cleanup even on panic
|
|
|
|
// ... use securePass ...
|
|
```
|
|
|
|
### 2. Clear Original Data
|
|
|
|
After creating a secure string/bytes, clear the original data:
|
|
|
|
```go
|
|
password := "secret"
|
|
securePass, _ := securestring.NewSecureString(password)
|
|
password = "" // Clear the original string
|
|
|
|
// Even better for byte slices:
|
|
data := []byte("secret")
|
|
secureData, _ := securebytes.NewSecureBytes(data)
|
|
for i := range data {
|
|
data[i] = 0
|
|
}
|
|
```
|
|
|
|
### 3. Minimize Exposure Time
|
|
|
|
Get values from secure storage only when needed:
|
|
|
|
```go
|
|
// Bad - exposes value for too long
|
|
value := secureString.String()
|
|
// ... lots of code ...
|
|
useValue(value)
|
|
|
|
// Good - get value right before use
|
|
// ... lots of code ...
|
|
useValue(secureString.String())
|
|
```
|
|
|
|
### 4. Use Dependency Injection
|
|
|
|
```go
|
|
import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
|
|
|
|
// In your Wire provider set
|
|
wire.NewSet(
|
|
password.ProvidePasswordProvider,
|
|
// ... other providers
|
|
)
|
|
|
|
// Use in your service
|
|
type AuthService struct {
|
|
passwordProvider password.PasswordProvider
|
|
}
|
|
|
|
func NewAuthService(pp password.PasswordProvider) *AuthService {
|
|
return &AuthService{passwordProvider: pp}
|
|
}
|
|
```
|
|
|
|
### 5. Handle Errors Properly
|
|
|
|
```go
|
|
securePass, err := securestring.NewSecureString(password)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create secure string: %w", err)
|
|
}
|
|
defer securePass.Wipe()
|
|
```
|
|
|
|
### 6. Clean Up GeoIP Resources
|
|
|
|
```go
|
|
import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcountryblocker"
|
|
|
|
// Always close the provider when done to release database resources
|
|
blocker := ipcountryblocker.NewProvider(cfg, logger)
|
|
defer blocker.Close()
|
|
```
|
|
|
|
## Memory Safety
|
|
|
|
### How It Works
|
|
|
|
The `memguard` library provides:
|
|
|
|
1. **Locked Memory**: Prevents sensitive data from being swapped to disk
|
|
2. **Guarded Heap**: Detects buffer overflows and underflows
|
|
3. **Secure Wiping**: Overwrites memory with random data before freeing
|
|
4. **Read Protection**: Makes memory pages read-only when not in use
|
|
|
|
### When to Use
|
|
|
|
Use secure storage for:
|
|
- Passwords and password hashes (during verification)
|
|
- API keys and tokens
|
|
- Encryption keys
|
|
- Private keys
|
|
- Database credentials
|
|
- OAuth secrets
|
|
- JWT signing keys
|
|
- Session tokens
|
|
- Any sensitive user data
|
|
|
|
### When NOT to Use
|
|
|
|
Don't use for:
|
|
- Public data
|
|
- Non-sensitive configuration
|
|
- Data that needs to be logged
|
|
- Data that will be stored long-term in memory
|
|
|
|
## Performance Considerations
|
|
|
|
### Password Hashing
|
|
|
|
Argon2id is intentionally slow to prevent brute-force attacks:
|
|
- Expected time: ~50-100ms per hash on modern hardware
|
|
- This is acceptable for authentication (login) operations
|
|
- DO NOT use for high-throughput operations
|
|
|
|
### Memory Usage
|
|
|
|
SecureString/SecureBytes use locked memory:
|
|
- Each instance locks a page in RAM (typically 4KB minimum)
|
|
- Don't create thousands of instances
|
|
- Reuse instances when possible
|
|
- Always wipe when done
|
|
|
|
## Examples
|
|
|
|
### Complete Login Example
|
|
|
|
```go
|
|
func (s *AuthService) Login(ctx context.Context, email, password string) (*User, error) {
|
|
// Create secure string from password
|
|
securePass, err := securestring.NewSecureString(password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer securePass.Wipe()
|
|
|
|
// Clear the original password
|
|
password = ""
|
|
|
|
// Get user from database
|
|
user, err := s.userRepo.GetByEmail(ctx, email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify password
|
|
match, err := s.passwordProvider.ComparePasswordAndHash(securePass, user.PasswordHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !match {
|
|
return nil, ErrInvalidCredentials
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
```
|
|
|
|
### Complete Registration Example
|
|
|
|
```go
|
|
func (s *AuthService) Register(ctx context.Context, email, password string) (*User, error) {
|
|
// Validate password strength first
|
|
if len(password) < 8 {
|
|
return nil, ErrWeakPassword
|
|
}
|
|
|
|
// Create secure string from password
|
|
securePass, err := securestring.NewSecureString(password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer securePass.Wipe()
|
|
|
|
// Clear the original password
|
|
password = ""
|
|
|
|
// Hash the password
|
|
hashedPassword, err := s.passwordProvider.GenerateHashFromPassword(securePass)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create user with hashed password
|
|
user := &User{
|
|
Email: email,
|
|
PasswordHash: hashedPassword,
|
|
}
|
|
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### "failed to create buffer"
|
|
|
|
**Problem**: memguard couldn't allocate locked memory
|
|
|
|
**Solutions**:
|
|
- Check system limits for locked memory (`ulimit -l`)
|
|
- Reduce number of concurrent SecureString/SecureBytes instances
|
|
- Ensure proper cleanup with `Wipe()`
|
|
|
|
### "buffer is not alive"
|
|
|
|
**Problem**: Trying to use a SecureString/SecureBytes after it was wiped
|
|
|
|
**Solutions**:
|
|
- Don't use secure data after calling `Wipe()`
|
|
- Check your defer ordering
|
|
- Create new instances if you need the data again
|
|
|
|
### Slow Performance
|
|
|
|
**Problem**: Password hashing is too slow
|
|
|
|
**Solutions**:
|
|
- This is by design for security
|
|
- Don't hash passwords in high-throughput operations
|
|
- Consider caching authentication results (with care)
|
|
- Use async operations for registration/password changes
|
|
|
|
### "failed to open GeoLite2 DB"
|
|
|
|
**Problem**: Cannot open the GeoIP2 database
|
|
|
|
**Solutions**:
|
|
- Verify APP_GEOLITE_DB_PATH points to a valid .mmdb file
|
|
- Download the GeoLite2-Country database from MaxMind
|
|
- Check file permissions
|
|
- Ensure the database file is not corrupted
|
|
|
|
### "no country found for IP"
|
|
|
|
**Problem**: IP address lookup returns no country
|
|
|
|
**Solutions**:
|
|
- This is normal for private IP ranges (10.x.x.x, 192.168.x.x, etc.)
|
|
- The IP might not be in the GeoIP2 database
|
|
- Update to a newer GeoLite2 database
|
|
- By default, unknown IPs are allowed (returns false from IsBlockedIP)
|
|
|
|
## IP Country Blocking Details
|
|
|
|
### GeoLite2 Database
|
|
|
|
The IP country blocker uses MaxMind's GeoLite2-Country database for IP geolocation.
|
|
|
|
**How to Get the Database**:
|
|
1. Create a free account at https://www.maxmind.com/en/geolite2/signup
|
|
2. Generate a license key
|
|
3. Download GeoLite2-Country database (.mmdb format)
|
|
4. Set APP_GEOLITE_DB_PATH to the file location
|
|
|
|
**Database Updates**:
|
|
- MaxMind updates GeoLite2 databases weekly
|
|
- Set up automated updates for production systems
|
|
- Database file is typically 5-10 MB
|
|
|
|
### Country Codes
|
|
|
|
Uses ISO 3166-1 alpha-2 country codes:
|
|
- US - United States
|
|
- CA - Canada
|
|
- GB - United Kingdom
|
|
- CN - China
|
|
- RU - Russia
|
|
- KP - North Korea
|
|
- etc.
|
|
|
|
Full list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
|
|
### Blocking Behavior
|
|
|
|
**Default Behavior**:
|
|
- If IP lookup fails → Allow (returns false)
|
|
- If country not found → Allow (returns false)
|
|
- If country is blocked → Block (returns true)
|
|
|
|
**To block unknown IPs**, modify IsBlockedIP to return true on error (line 101 in ipcountryblocker.go).
|
|
|
|
### Thread Safety
|
|
|
|
The provider is thread-safe:
|
|
- Uses sync.RWMutex for concurrent access to blocked countries map
|
|
- GeoIP2 Reader is thread-safe by design
|
|
- Safe to use in HTTP middleware and concurrent handlers
|
|
|
|
### Performance
|
|
|
|
**Lookup Speed**:
|
|
- In-memory database lookups are very fast (~microseconds)
|
|
- Database is memory-mapped for efficiency
|
|
- Suitable for high-traffic applications
|
|
|
|
**Memory Usage**:
|
|
- GeoLite2-Country database: ~5-10 MB in memory
|
|
- Blocked countries map: negligible (few KB)
|
|
|
|
## References
|
|
|
|
- [Argon2 RFC](https://tools.ietf.org/html/rfc9106)
|
|
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
|
|
- [memguard Documentation](https://github.com/awnumar/memguard)
|
|
- [Alex Edwards: How to Hash and Verify Passwords With Argon2 in Go](https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go)
|
|
- [MaxMind GeoLite2 Free Geolocation Data](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data)
|
|
- [ISO 3166-1 Country Codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|