13 KiB
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 managementgolang.org/x/crypto/argon2- For password hashinggithub.com/oschwald/geoip2-golang- For GeoIP lookups
Usage
Password Hashing
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
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
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
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
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:
# 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
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:
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:
// 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
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
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
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:
- Locked Memory: Prevents sensitive data from being swapped to disk
- Guarded Heap: Detects buffer overflows and underflows
- Secure Wiping: Overwrites memory with random data before freeing
- 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
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
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:
- Create a free account at https://www.maxmind.com/en/geolite2/signup
- Generate a license key
- Download GeoLite2-Country database (.mmdb format)
- 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)